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( 143 device_id=self.dut.serial, 144 camera_id=self.camera_id, 145 hidden_physical_id=self.hidden_physical_id) as cam: 146 props = cam.get_camera_properties() 147 props = cam.override_with_hidden_physical_camera_props(props) 148 149 # check SKIP conditions 150 first_api_level = its_session_utils.get_first_api_level( 151 self.dut.serial) 152 camera_properties_utils.skip_unless( 153 camera_properties_utils.flash(props) and 154 first_api_level >= its_session_utils.ANDROID14_API_LEVEL) 155 failure_messages = [] 156 # establish connection with lighting controller 157 arduino_serial_port = lighting_control_utils.lighting_control( 158 self.lighting_cntl, self.lighting_ch) 159 for fmt_name in _FORMAT_NAMES: 160 for size in _IMG_SIZES: 161 width, height = size 162 if not (fmt_name == 'yuv' and size == _VGA_SIZE): 163 output_sizes = capture_request_utils.get_available_output_sizes( 164 fmt_name, props, match_ar_size=size) 165 if not output_sizes: 166 if size != _VGA_SIZE: 167 logging.debug('No output sizes for format %s, size %sx%s', 168 fmt_name, width, height) 169 continue 170 else: 171 raise AssertionError(f'No output sizes for format {fmt_name}, ' 172 f'size {width}x{height}') 173 # pick smallest size out of available output sizes 174 width, height = output_sizes[-1] 175 176 out_surfaces = {'format': fmt_name, 'width': width, 'height': height} 177 logging.debug( 178 'Testing %s format, size: %dx%d', fmt_name, width, height) 179 180 # take capture with lights on - no flash 181 logging.debug( 182 'Taking reference frame with lights on and no flash.') 183 img_prefix = f'{test_name}_{fmt_name}_{width}x{height}' 184 light_on_img_name = f'{img_prefix}_lights_on.jpg' 185 _take_captures(out_surfaces, cam, light_on_img_name, flash=False) 186 187 # turn OFF lights to darken scene 188 lighting_control_utils.set_lighting_state( 189 arduino_serial_port, self.lighting_ch, 'OFF') 190 191 # take capture with no flash as baseline 192 logging.debug( 193 'Taking reference frame with lights off and no auto-flash.') 194 no_flash_req = capture_request_utils.auto_capture_request() 195 no_flash_req[ 196 'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE 197 no_flash_img_name = f'{img_prefix}_no_flash.jpg' 198 _take_captures(out_surfaces, cam, no_flash_img_name, flash=False) 199 200 # take capture with auto flash enabled 201 logging.debug('Taking capture with auto flash enabled.') 202 flash_fired = False 203 flash_img_name = f'{img_prefix}_flash.jpg' 204 cap = _take_captures(out_surfaces, cam, flash_img_name, flash=True) 205 img = image_processing_utils.convert_capture_to_rgb_image(cap) 206 207 # evaluate captured image 208 metadata = cap['metadata'] 209 exp = int(metadata['android.sensor.exposureTime']) 210 iso = int(metadata['android.sensor.sensitivity']) 211 logging.debug('cap ISO: %d, exp: %d ns', iso, exp) 212 logging.debug('AE_MODE (cap): %s', 213 _AE_MODES[metadata['android.control.aeMode']]) 214 ae_state = _AE_STATES[metadata['android.control.aeState']] 215 logging.debug('AE_STATE (cap): %s', ae_state) 216 flash_state = _FLASH_STATES[metadata['android.flash.state']] 217 logging.debug('FLASH_STATE: %s', flash_state) 218 if flash_state == 'FLASH_STATE_FIRED': 219 logging.debug('Flash fired') 220 flash_fired = True 221 flash_means = image_processing_utils.compute_image_means(img) 222 logging.debug('Image means with flash: %s', flash_means) 223 flash_means = [i * _CH_FULL_SCALE for i in flash_means] 224 logging.debug('Flash capture rgb means: %s', flash_means) 225 226 # Verify that R/G and B/G ratios are within the limits 227 r_g_ratio = flash_means[0]/ flash_means[1] 228 logging.debug('R/G ratio: %s fmt: %s, WxH: %sx%s', 229 r_g_ratio, fmt_name, width, height) 230 b_g_ratio = flash_means[2]/flash_means[1] 231 logging.debug('B/G ratio: %s fmt: %s, WxH: %sx%s', 232 b_g_ratio, fmt_name, width, height) 233 234 if not _WB_MIN <= r_g_ratio <= _WB_MAX: 235 failure_messages.append(f'R/G ratio: {r_g_ratio} not within' 236 f' the limits. Format: {fmt_name},' 237 f' Size: {width}x{height}') 238 if not _WB_MIN <= b_g_ratio <= _WB_MAX: 239 failure_messages.append(f'B/G ratio: {r_g_ratio} not within' 240 f' the limits. Format: {fmt_name},' 241 f' Size: {width}x{height}') 242 243 # Check whether the image means for each color channel is 244 # within the limits or not. 245 valid_color = True 246 for color in _COLOR_CHANNELS: 247 valid_color = _is_color_mean_valid(flash_means, color, 248 fmt_name, width, height) 249 if not valid_color: 250 failure_messages.append( 251 f'Flash image mean not within limits for channel {color}.' 252 f' Format: {fmt_name},Size: {width}x{height}') 253 254 if not flash_fired: 255 raise AssertionError( 256 'Flash was not fired. Format:{fmt_name}, Size:{width}x{height}') 257 258 # turn the lights back on 259 lighting_control_utils.set_lighting_state( 260 arduino_serial_port, self.lighting_ch, 'ON') 261 262 # assert correct behavior for all formats 263 if failure_messages: 264 raise AssertionError('\n'.join(failure_messages)) 265 266if __name__ == '__main__': 267 test_runner.main() 268