1# Copyright 2023 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Verify that the LED snapshot works correctly.""" 15 16import logging 17import os.path 18 19import camera_properties_utils 20import capture_request_utils 21import image_processing_utils 22import its_base_test 23import its_session_utils 24import lighting_control_utils 25from mobly import test_runner 26 27_AE_MODES = {0: 'OFF', 1: 'ON', 2: 'ON_AUTO_FLASH', 3: 'ON_ALWAYS_FLASH', 28 4: 'ON_AUTO_FLASH_REDEYE', 5: 'ON_EXTERNAL_FLASH'} 29_AE_STATES = {0: 'INACTIVE', 1: 'SEARCHING', 2: 'CONVERGED', 3: 'LOCKED', 30 4: 'FLASH_REQUIRED', 5: 'PRECAPTURE'} 31_FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING', 32 2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED', 33 4: 'FLASH_STATE_PARTIAL'} 34_FORMAT_NAMES = ('jpeg', 'yuv') 35_IMG_SIZES = ((640, 480), (640, 360)) 36_VGA_SIZE = (640, 480) 37_CH_FULL_SCALE = 255 38_TEST_NAME = os.path.splitext(os.path.basename(__file__))[0] 39_AE_MODE_ON_AUTO_FLASH = 2 40_CAPTURE_INTENT_PREVIEW = 1 41_CAPTURE_INTENT_STILL_CAPTURE = 2 42_AE_PRECAPTURE_TRIGGER_START = 1 43_AE_PRECAPTURE_TRIGGER_IDLE = 0 44_FLASH_MEAN_MIN = 50 45_FLASH_MEAN_MAX = 200 46_WB_MIN = 0.8 47_WB_MAX = 1.2 48_COLOR_CHANNELS = ('R', 'G', 'B') 49 50 51def _take_captures(out_surfaces, cam, img_name, flash=False): 52 """Takes captures and returns the captured image. 53 54 Args: 55 out_surfaces: 56 cam: ItsSession util object 57 img_name: image name to be saved. 58 flash: True if the capture needs to be taken with Flash ON 59 60 Returns: 61 cap: captured image object as defined by 62 ItsSessionUtils.do_capture() 63 """ 64 cam.do_3a(do_af=False) 65 if not flash: 66 cap_req = capture_request_utils.auto_capture_request() 67 cap_req[ 68 'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE 69 cap = cam.do_capture(cap_req, out_surfaces) 70 else: 71 cap = capture_request_utils.take_captures_with_flash(cam, out_surfaces) 72 73 img = image_processing_utils.convert_capture_to_rgb_image(cap) 74 # Save captured image 75 image_processing_utils.write_image(img, img_name) 76 return cap 77 78 79def _is_color_mean_valid(means, color_channel, fmt_name, width, height): 80 """Checks if the mean for color_channel is within the range. 81 82 Computes means for color_channel specified and checks whether 83 it is within the acceptable range. 84 Args: 85 means: list of means in float 86 color_channel: String; values must be one of the color 87 channels in _COLOR_CHANNELS 88 fmt_name: Format to be tested 89 width: width of the image to be tested 90 height: height of the image to be tested 91 92 Returns: 93 True if the color mean is within the range and returns False 94 if invalid. 95 """ 96 if color_channel not in _COLOR_CHANNELS: 97 raise AssertionError('Invalid color_channel.') 98 99 if color_channel == 'R': 100 color_mean = means[0] 101 elif color_channel == 'G': 102 color_mean = means[1] 103 else: 104 color_mean = means[2] 105 106 if not _FLASH_MEAN_MIN <= color_mean <= _FLASH_MEAN_MAX: 107 logging.debug('Flash image mean %s not' 108 ' within limits for channel %s.' 109 ' Format: %s,' 110 ' Size: %sx%s', color_mean, color_channel, 111 fmt_name, width, height) 112 return False 113 else: 114 return True 115 116 117class LedSnapshotTest(its_base_test.ItsBaseTest): 118 """Tests if LED snapshot works correctly. 119 120 In this test we capture the failure that the LED snapshot is not too dark, 121 too bright or producing a strange color tint. 122 123 During the test 3 images are captured for each format in _FORMAT_NAMES 124 and size in _IMG_SIZES: 125 1. Lights ON, AUTO_FLASH set to OFF -> Baseline capture without any flash. 126 2. Lights OFF, AUTO_FLASH set to OFF -> Ensures dark lighting conditions 127 to trigger the flash. 128 3. Lights OFF, AUTO_FLASH set to ON -> Still capture with flash 129 130 For all the 3 pictures we compute the image means and log them. 131 For the capture with flash triggered, we compare the mean to be within the 132 minimum and maximum threshold level. The capture with flash should not be too 133 dark or too bright. 134 In order to ensure the white balance, the ratio of R/G and B/G is also 135 compared to be within the pre-decided threshold level. 136 Failures will be reported if any of the measuremenet is out of range. 137 """ 138 139 def test_led_snapshot(self): 140 test_name = os.path.join(self.log_path, _TEST_NAME) 141 142 with its_session_utils.ItsSession(device_id=self.dut.serial, 143 camera_id=self.camera_id, 144 hidden_physical_id=self.hidden_physical_id) as cam: 145 props = cam.get_camera_properties() 146 props = cam.override_with_hidden_physical_camera_props(props) 147 148 # check SKIP conditions 149 vendor_api_level = its_session_utils.get_vendor_api_level( 150 self.dut.serial) 151 camera_properties_utils.skip_unless( 152 camera_properties_utils.flash(props) and 153 vendor_api_level >= its_session_utils.ANDROID14_API_LEVEL) 154 failure_messages = [] 155 # establish connection with lighting controller 156 arduino_serial_port = lighting_control_utils.lighting_control( 157 self.lighting_cntl, self.lighting_ch) 158 for fmt_name in _FORMAT_NAMES: 159 for size in _IMG_SIZES: 160 width, height = size 161 if not (fmt_name == 'yuv' and size == _VGA_SIZE): 162 output_sizes = capture_request_utils.get_available_output_sizes( 163 fmt_name, props, match_ar_size=size) 164 if not output_sizes: 165 if size != _VGA_SIZE: 166 logging.debug('No output sizes for format %s, size %sx%s', 167 fmt_name, width, height) 168 continue 169 else: 170 raise AssertionError(f'No output sizes for format {fmt_name}, ' 171 f'size {width}x{height}') 172 # pick smallest size out of available output sizes 173 width, height = output_sizes[-1] 174 175 out_surfaces = {'format': fmt_name, 'width': width, 'height': height} 176 logging.debug( 177 'Testing %s format, size: %dx%d', fmt_name, width, height) 178 179 # take capture with lights on - no flash 180 logging.debug( 181 'Taking reference frame with lights on and no flash.') 182 img_prefix = f'{test_name}_{fmt_name}_{width}x{height}' 183 light_on_img_name = f'{img_prefix}_lights_on.jpg' 184 _take_captures(out_surfaces, cam, light_on_img_name, flash=False) 185 186 # turn OFF lights to darken scene 187 lighting_control_utils.set_lighting_state( 188 arduino_serial_port, self.lighting_ch, 'OFF') 189 190 # take capture with no flash as baseline 191 logging.debug( 192 'Taking reference frame with lights off and no auto-flash.') 193 no_flash_req = capture_request_utils.auto_capture_request() 194 no_flash_req[ 195 'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE 196 no_flash_img_name = f'{img_prefix}_no_flash.jpg' 197 _take_captures(out_surfaces, cam, no_flash_img_name, flash=False) 198 199 # take capture with auto flash enabled 200 logging.debug('Taking capture with auto flash enabled.') 201 flash_fired = False 202 flash_img_name = f'{img_prefix}_flash.jpg' 203 cap = _take_captures(out_surfaces, cam, flash_img_name, flash=True) 204 img = image_processing_utils.convert_capture_to_rgb_image(cap) 205 206 # evaluate captured image 207 metadata = cap['metadata'] 208 exp = int(metadata['android.sensor.exposureTime']) 209 iso = int(metadata['android.sensor.sensitivity']) 210 logging.debug('cap ISO: %d, exp: %d ns', iso, exp) 211 logging.debug('AE_MODE (cap): %s', 212 _AE_MODES[metadata['android.control.aeMode']]) 213 ae_state = _AE_STATES[metadata['android.control.aeState']] 214 logging.debug('AE_STATE (cap): %s', ae_state) 215 flash_state = _FLASH_STATES[metadata['android.flash.state']] 216 logging.debug('FLASH_STATE: %s', flash_state) 217 if flash_state == 'FLASH_STATE_FIRED': 218 logging.debug('Flash fired') 219 flash_fired = True 220 flash_means = image_processing_utils.compute_image_means(img) 221 logging.debug('Image means with flash: %s', flash_means) 222 flash_means = [i * _CH_FULL_SCALE for i in flash_means] 223 logging.debug('Flash capture rgb means: %s', flash_means) 224 225 # Verify that R/G and B/G ratios are within the limits 226 r_g_ratio = flash_means[0]/ flash_means[1] 227 logging.debug('R/G ratio: %s fmt: %s, WxH: %sx%s', 228 r_g_ratio, fmt_name, width, height) 229 b_g_ratio = flash_means[2]/flash_means[1] 230 logging.debug('B/G ratio: %s fmt: %s, WxH: %sx%s', 231 b_g_ratio, fmt_name, width, height) 232 233 if not _WB_MIN <= r_g_ratio <= _WB_MAX: 234 failure_messages.append(f'R/G ratio: {r_g_ratio} not within' 235 f' the limits. Format: {fmt_name},' 236 f' Size: {width}x{height}') 237 if not _WB_MIN <= b_g_ratio <= _WB_MAX: 238 failure_messages.append(f'B/G ratio: {r_g_ratio} not within' 239 f' the limits. Format: {fmt_name},' 240 f' Size: {width}x{height}') 241 242 # Check whether the image means for each color channel is 243 # within the limits or not. 244 valid_color = True 245 for color in _COLOR_CHANNELS: 246 valid_color = _is_color_mean_valid(flash_means, color, 247 fmt_name, width, height) 248 if not valid_color: 249 failure_messages.append( 250 f'Flash image mean not within limits for channel {color}.' 251 f' Format: {fmt_name},Size: {width}x{height}') 252 253 if not flash_fired: 254 raise AssertionError( 255 'Flash was not fired. Format:{fmt_name}, Size:{width}x{height}') 256 257 # turn the lights back on 258 lighting_control_utils.set_lighting_state( 259 arduino_serial_port, self.lighting_ch, 'ON') 260 261 # assert correct behavior for all formats 262 if failure_messages: 263 raise AssertionError('\n'.join(failure_messages)) 264 265if __name__ == '__main__': 266 test_runner.main() 267