| # Copyright 2023 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Verify that the LED snapshot works correctly.""" |
| |
| import logging |
| import os.path |
| |
| import camera_properties_utils |
| import capture_request_utils |
| import image_processing_utils |
| import its_base_test |
| import its_session_utils |
| import lighting_control_utils |
| from mobly import test_runner |
| |
| _AE_MODES = {0: 'OFF', 1: 'ON', 2: 'ON_AUTO_FLASH', 3: 'ON_ALWAYS_FLASH', |
| 4: 'ON_AUTO_FLASH_REDEYE', 5: 'ON_EXTERNAL_FLASH'} |
| _AE_STATES = {0: 'INACTIVE', 1: 'SEARCHING', 2: 'CONVERGED', 3: 'LOCKED', |
| 4: 'FLASH_REQUIRED', 5: 'PRECAPTURE'} |
| _FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING', |
| 2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED', |
| 4: 'FLASH_STATE_PARTIAL'} |
| _FORMAT_NAMES = ('jpeg', 'yuv') |
| _IMG_SIZES = ((640, 480), (640, 360)) |
| _VGA_SIZE = (640, 480) |
| _CH_FULL_SCALE = 255 |
| _TEST_NAME = os.path.splitext(os.path.basename(__file__))[0] |
| _AE_MODE_ON_AUTO_FLASH = 2 |
| _CAPTURE_INTENT_PREVIEW = 1 |
| _CAPTURE_INTENT_STILL_CAPTURE = 2 |
| _AE_PRECAPTURE_TRIGGER_START = 1 |
| _AE_PRECAPTURE_TRIGGER_IDLE = 0 |
| _FLASH_MEAN_MIN = 50 |
| _FLASH_MEAN_MAX = 200 |
| _WB_MIN = 0.8 |
| _WB_MAX = 1.2 |
| _COLOR_CHANNELS = ('R', 'G', 'B') |
| |
| |
| def _take_captures(out_surfaces, cam, img_name, flash=False): |
| """Takes captures and returns the captured image. |
| |
| Args: |
| out_surfaces: |
| cam: ItsSession util object |
| img_name: image name to be saved. |
| flash: True if the capture needs to be taken with Flash ON |
| |
| Returns: |
| cap: captured image object as defined by |
| ItsSessionUtils.do_capture() |
| """ |
| cam.do_3a(do_af=False) |
| if not flash: |
| cap_req = capture_request_utils.auto_capture_request() |
| cap_req[ |
| 'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE |
| cap = cam.do_capture(cap_req, out_surfaces) |
| else: |
| cap = capture_request_utils.take_captures_with_flash(cam, out_surfaces) |
| |
| img = image_processing_utils.convert_capture_to_rgb_image(cap) |
| # Save captured image |
| image_processing_utils.write_image(img, img_name) |
| return cap |
| |
| |
| def _is_color_mean_valid(means, color_channel, fmt_name, width, height): |
| """Checks if the mean for color_channel is within the range. |
| |
| Computes means for color_channel specified and checks whether |
| it is within the acceptable range. |
| Args: |
| means: list of means in float |
| color_channel: String; values must be one of the color |
| channels in _COLOR_CHANNELS |
| fmt_name: Format to be tested |
| width: width of the image to be tested |
| height: height of the image to be tested |
| |
| Returns: |
| True if the color mean is within the range and returns False |
| if invalid. |
| """ |
| if color_channel not in _COLOR_CHANNELS: |
| raise AssertionError('Invalid color_channel.') |
| |
| if color_channel == 'R': |
| color_mean = means[0] |
| elif color_channel == 'G': |
| color_mean = means[1] |
| else: |
| color_mean = means[2] |
| |
| if not _FLASH_MEAN_MIN <= color_mean <= _FLASH_MEAN_MAX: |
| logging.debug('Flash image mean %s not' |
| ' within limits for channel %s.' |
| ' Format: %s,' |
| ' Size: %sx%s', color_mean, color_channel, |
| fmt_name, width, height) |
| return False |
| else: |
| return True |
| |
| |
| class LedSnapshotTest(its_base_test.ItsBaseTest): |
| """Tests if LED snapshot works correctly. |
| |
| In this test we capture the failure that the LED snapshot is not too dark, |
| too bright or producing a strange color tint. |
| |
| During the test 3 images are captured for each format in _FORMAT_NAMES |
| and size in _IMG_SIZES: |
| 1. Lights ON, AUTO_FLASH set to OFF -> Baseline capture without any flash. |
| 2. Lights OFF, AUTO_FLASH set to OFF -> Ensures dark lighting conditions |
| to trigger the flash. |
| 3. Lights OFF, AUTO_FLASH set to ON -> Still capture with flash |
| |
| For all the 3 pictures we compute the image means and log them. |
| For the capture with flash triggered, we compare the mean to be within the |
| minimum and maximum threshold level. The capture with flash should not be too |
| dark or too bright. |
| In order to ensure the white balance, the ratio of R/G and B/G is also |
| compared to be within the pre-decided threshold level. |
| Failures will be reported if any of the measuremenet is out of range. |
| """ |
| |
| def test_led_snapshot(self): |
| test_name = os.path.join(self.log_path, _TEST_NAME) |
| |
| with its_session_utils.ItsSession( |
| device_id=self.dut.serial, |
| camera_id=self.camera_id, |
| hidden_physical_id=self.hidden_physical_id) as cam: |
| props = cam.get_camera_properties() |
| props = cam.override_with_hidden_physical_camera_props(props) |
| |
| # check SKIP conditions |
| first_api_level = its_session_utils.get_first_api_level( |
| self.dut.serial) |
| camera_properties_utils.skip_unless( |
| camera_properties_utils.flash(props) and |
| first_api_level >= its_session_utils.ANDROID14_API_LEVEL) |
| failure_messages = [] |
| # establish connection with lighting controller |
| arduino_serial_port = lighting_control_utils.lighting_control( |
| self.lighting_cntl, self.lighting_ch) |
| for fmt_name in _FORMAT_NAMES: |
| for size in _IMG_SIZES: |
| width, height = size |
| if not (fmt_name == 'yuv' and size == _VGA_SIZE): |
| output_sizes = capture_request_utils.get_available_output_sizes( |
| fmt_name, props, match_ar_size=size) |
| if not output_sizes: |
| if size != _VGA_SIZE: |
| logging.debug('No output sizes for format %s, size %sx%s', |
| fmt_name, width, height) |
| continue |
| else: |
| raise AssertionError(f'No output sizes for format {fmt_name}, ' |
| f'size {width}x{height}') |
| # pick smallest size out of available output sizes |
| width, height = output_sizes[-1] |
| |
| out_surfaces = {'format': fmt_name, 'width': width, 'height': height} |
| logging.debug( |
| 'Testing %s format, size: %dx%d', fmt_name, width, height) |
| |
| # take capture with lights on - no flash |
| logging.debug( |
| 'Taking reference frame with lights on and no flash.') |
| img_prefix = f'{test_name}_{fmt_name}_{width}x{height}' |
| light_on_img_name = f'{img_prefix}_lights_on.jpg' |
| _take_captures(out_surfaces, cam, light_on_img_name, flash=False) |
| |
| # turn OFF lights to darken scene |
| lighting_control_utils.set_lighting_state( |
| arduino_serial_port, self.lighting_ch, 'OFF') |
| |
| # take capture with no flash as baseline |
| logging.debug( |
| 'Taking reference frame with lights off and no auto-flash.') |
| no_flash_req = capture_request_utils.auto_capture_request() |
| no_flash_req[ |
| 'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE |
| no_flash_img_name = f'{img_prefix}_no_flash.jpg' |
| _take_captures(out_surfaces, cam, no_flash_img_name, flash=False) |
| |
| # take capture with auto flash enabled |
| logging.debug('Taking capture with auto flash enabled.') |
| flash_fired = False |
| flash_img_name = f'{img_prefix}_flash.jpg' |
| cap = _take_captures(out_surfaces, cam, flash_img_name, flash=True) |
| img = image_processing_utils.convert_capture_to_rgb_image(cap) |
| |
| # evaluate captured image |
| metadata = cap['metadata'] |
| exp = int(metadata['android.sensor.exposureTime']) |
| iso = int(metadata['android.sensor.sensitivity']) |
| logging.debug('cap ISO: %d, exp: %d ns', iso, exp) |
| logging.debug('AE_MODE (cap): %s', |
| _AE_MODES[metadata['android.control.aeMode']]) |
| ae_state = _AE_STATES[metadata['android.control.aeState']] |
| logging.debug('AE_STATE (cap): %s', ae_state) |
| flash_state = _FLASH_STATES[metadata['android.flash.state']] |
| logging.debug('FLASH_STATE: %s', flash_state) |
| if flash_state == 'FLASH_STATE_FIRED': |
| logging.debug('Flash fired') |
| flash_fired = True |
| flash_means = image_processing_utils.compute_image_means(img) |
| logging.debug('Image means with flash: %s', flash_means) |
| flash_means = [i * _CH_FULL_SCALE for i in flash_means] |
| logging.debug('Flash capture rgb means: %s', flash_means) |
| |
| # Verify that R/G and B/G ratios are within the limits |
| r_g_ratio = flash_means[0]/ flash_means[1] |
| logging.debug('R/G ratio: %s fmt: %s, WxH: %sx%s', |
| r_g_ratio, fmt_name, width, height) |
| b_g_ratio = flash_means[2]/flash_means[1] |
| logging.debug('B/G ratio: %s fmt: %s, WxH: %sx%s', |
| b_g_ratio, fmt_name, width, height) |
| |
| if not _WB_MIN <= r_g_ratio <= _WB_MAX: |
| failure_messages.append(f'R/G ratio: {r_g_ratio} not within' |
| f' the limits. Format: {fmt_name},' |
| f' Size: {width}x{height}') |
| if not _WB_MIN <= b_g_ratio <= _WB_MAX: |
| failure_messages.append(f'B/G ratio: {b_g_ratio} not within' |
| f' the limits. Format: {fmt_name},' |
| f' Size: {width}x{height}') |
| |
| # Check whether the image means for each color channel is |
| # within the limits or not. |
| valid_color = True |
| for color in _COLOR_CHANNELS: |
| valid_color = _is_color_mean_valid(flash_means, color, |
| fmt_name, width, height) |
| if not valid_color: |
| failure_messages.append( |
| f'Flash image mean not within limits for channel {color}.' |
| f' Format: {fmt_name},Size: {width}x{height}') |
| |
| if not flash_fired: |
| raise AssertionError( |
| 'Flash was not fired. Format:{fmt_name}, Size:{width}x{height}') |
| |
| # turn the lights back on |
| lighting_control_utils.set_lighting_state( |
| arduino_serial_port, self.lighting_ch, 'ON') |
| |
| # assert correct behavior for all formats |
| if failure_messages: |
| raise AssertionError('\n'.join(failure_messages)) |
| |
| if __name__ == '__main__': |
| test_runner.main() |