1# Copyright 2024 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 flash strength control in TORCH mode works correctly during camera use.""" 15 16import logging 17import os.path 18 19from mobly import test_runner 20import numpy as np 21import its_base_test 22import camera_properties_utils 23import capture_request_utils 24import image_processing_utils 25import its_session_utils 26import lighting_control_utils 27 28_AE_MODE_FLASH_CONTROL = (0, 1) 29_AE_MODES = {0: 'OFF', 1: 'ON', 2: 'ON_AUTO_FLASH', 3: 'ON_ALWAYS_FLASH', 30 4: 'ON_AUTO_FLASH_REDEYE', 5: 'ON_EXTERNAL_FLASH'} 31_AE_STATES = {0: 'INACTIVE', 1: 'SEARCHING', 2: 'CONVERGED', 3: 'LOCKED', 32 4: 'FLASH_REQUIRED', 5: 'PRECAPTURE'} 33_BRIGHTNESS_MEAN_ATOL = 5 # Tolerance for brightness mean 34_BURST_LEN = 5 35_CAPTURE_INTENT_PREVIEW = 1 36_CAPTURE_INTENT_STILL_CAPTURE = 2 37_FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING', 38 2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED', 39 4: 'FLASH_STATE_PARTIAL'} 40_FORMAT_NAME = 'yuv' 41_IMG_SIZE = (640, 360) 42_MAX_SINGLE_STRENGTH_PROP_KEY = 'android.flash.singleStrengthMaxLevel' 43_MAX_TORCH_STRENGTH_PROP_KEY = 'android.flash.torchStrengthMaxLevel' 44_PATCH_H = 0.25 # center 25% 45_PATCH_W = 0.25 46_PATCH_X = 0.5-_PATCH_W/2 47_PATCH_Y = 0.5-_PATCH_H/2 48_SINGLE_STRENGTH_CONTROL_THRESHOLD = 1 49_STRENGTH_STEPS = 3 # Steps of flash strengths to be tested 50_TEST_NAME = os.path.splitext(os.path.basename(__file__))[0] 51_TESTING_AE_MODES = (0, 1, 2) 52_TORCH_MODE = 2 53_TORCH_STRENGTH_CONTROL_THRESHOLD = 1 54_TORCH_STRENGTH_MIN = 0 55 56 57# TODO: b/344675052 - Add torch strength control in do_3a() 58def _take_captures( 59 self, arduino_serial_port, out_surfaces, cam, 60 img_name_prefix, ae_mode, torch_strength 61): 62 """Takes video captures and returns the captured images. 63 64 Args: 65 self: ItsBaseTest object; used for lighting control. 66 arduino_serial_port: serial port pointer; used for lighting control 67 out_surfaces: list; valid output surfaces for caps. 68 cam: ItsSession util object. 69 img_name_prefix: image name to be saved, log_path included. 70 ae_mode: AE mode to be tested with. 71 torch_strength: Flash strength that flash should be fired with. 72 Note that 0 is for baseline capture. 73 74 Returns: 75 caps: list of capture objects as described by cam.do_capture(). 76 """ 77 # Take base image without flash 78 if torch_strength == 0: 79 # turn OFF lights to darken scene 80 lighting_control_utils.set_lighting_state( 81 arduino_serial_port, self.lighting_ch, 'OFF' 82 ) 83 cam.do_3a(do_af=False, lock_awb=True) 84 cap_req = capture_request_utils.auto_capture_request() 85 cap_req[ 86 'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE 87 cap_req['android.control.aeMode'] = 0 # AE_MODE_OFF 88 cap_req['android.control.awbLock'] = True 89 cap = cam.do_capture(cap_req, out_surfaces) 90 # turn the lights back on 91 lighting_control_utils.set_lighting_state( 92 arduino_serial_port, self.lighting_ch, 'ON' 93 ) 94 return [cap] 95 96 # Take multiple still captures with torch strength 97 else: 98 cam.do_3a(do_af=False, lock_awb=True) 99 # turn OFF lights to darken scene 100 lighting_control_utils.set_lighting_state( 101 arduino_serial_port, self.lighting_ch, 'OFF' 102 ) 103 cap_req = capture_request_utils.auto_capture_request() 104 cap_req['android.control.aeMode'] = ae_mode 105 cap_req['android.control.captureIntent'] = _CAPTURE_INTENT_PREVIEW 106 cap_req['android.control.aeLock'] = True 107 cap_req['android.control.awbLock'] = True # AWB Lock 108 cap_req['android.flash.mode'] = _TORCH_MODE 109 cap_req['android.flash.strengthLevel'] = torch_strength 110 reqs = [cap_req] * _BURST_LEN 111 caps = cam.do_capture(reqs, out_surfaces) 112 # turn the lights back on 113 lighting_control_utils.set_lighting_state( 114 arduino_serial_port, self.lighting_ch, 'ON' 115 ) 116 for i, cap in enumerate(caps): 117 img = image_processing_utils.convert_capture_to_rgb_image(cap) 118 # Save captured image 119 image_processing_utils.write_image(img, f'{img_name_prefix}{i}.jpg') 120 return caps 121 122 123def _get_img_patch_mean(caps, props): 124 """Evaluate captured image by extracting means in the center patch. 125 126 Args: 127 caps: captured list of image object as defined by 128 ItsSessionUtils.do_capture(). 129 props: Camera properties object. 130 131 Returns: 132 mean: (list of float64) calculated means of Y plane center patch. 133 """ 134 flash_means = [] 135 for cap in caps: 136 metadata = cap['metadata'] 137 exp = int(metadata['android.sensor.exposureTime']) 138 iso = int(metadata['android.sensor.sensitivity']) 139 flash_exp_x_iso = [] 140 logging.debug('cap ISO: %d, exp: %d ns', iso, exp) 141 logging.debug('AE_MODE (cap): %s', 142 _AE_MODES[metadata['android.control.aeMode']]) 143 ae_state = _AE_STATES[metadata['android.control.aeState']] 144 logging.debug('AE_STATE (cap): %s', ae_state) 145 flash_state = _FLASH_STATES[metadata['android.flash.state']] 146 logging.debug('FLASH_STATE: %s', flash_state) 147 logging.debug('FLASH_STRENGTH: %s', metadata['android.flash.strengthLevel']) 148 149 flash_exp_x_iso = exp*iso 150 y, _, _ = image_processing_utils.convert_capture_to_planes( 151 cap, props) 152 patch = image_processing_utils.get_image_patch( 153 y, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 154 flash_mean = image_processing_utils.compute_image_means( 155 patch)[0]*255 156 flash_means.append(flash_mean) 157 158 logging.debug('Flash exposure X ISO %d', flash_exp_x_iso) 159 logging.debug('Flash frames Y mean: %.4f', flash_mean) 160 return flash_means 161 162 163def _compare_means(formats_means, ae_mode, flash_strengths): 164 """Compares the means of the captured images at different strength levels. 165 166 If AE_MODE is ON/OFF, capture should show mean differences 167 in flash strengths. If AE_MODE is ON_AUTO_FLASH, flash 168 strength should be overwritten hence no mean difference in captures. 169 170 Args: 171 formats_means: list of calculated means of image center patches of all req. 172 ae_mode: requested AE mode during testing. 173 flash_strengths: list of flash strength values requested during testing. 174 175 Returns: 176 failure_messages: (list of string) list of error messages. 177 """ 178 179 failure_messages = [] 180 strength_means = [np.average(x) for x in formats_means] 181 # Intentionally omitting frame-to-frame sameness check of last burst 182 for i, burst_means in enumerate(formats_means[:-1]): 183 # Check for strength brightness with averages of same strength captures 184 if (strength_means[i] >= strength_means[i+1] and 185 ae_mode in _AE_MODE_FLASH_CONTROL): 186 msg = ( 187 f'Capture with AE_CONTROL_MODE OFF/ON. AE_MODE: {ae_mode}; ' 188 f'Strength {flash_strengths[i]} mean: {strength_means[i]}; ' 189 f'Strength {flash_strengths[i+1]} mean: {strength_means[i+1]}; ' 190 f'Mean of {flash_strengths[i+1]} should be brighter than ' 191 f'Mean of {flash_strengths[i]}!' 192 ) 193 failure_messages.append(msg) 194 for j in range(len(burst_means)-1): 195 # Check for frame-to-frame sameness 196 diff = abs(burst_means[j] - burst_means[j+1]) 197 if diff > _BRIGHTNESS_MEAN_ATOL: 198 if ae_mode in _AE_MODE_FLASH_CONTROL: 199 msg = ( 200 f'Capture with AE_CONTROL_MODE OFF/ON. AE_MODE: {ae_mode}; ' 201 f'Strength {flash_strengths[i]} capture {j} mean: ' 202 f'{burst_means[j]},' 203 f'Strength {flash_strengths[i+1]} capture {j+1} mean: ' 204 f'{burst_means[j+1]}, ' 205 f'Torch strength is not consistent between captures ' 206 f'Diff: {diff}; TOL: {_BRIGHTNESS_MEAN_ATOL}' 207 ) 208 else: 209 msg = ( 210 f'Capture with AE_CONTROL_MODE ON_AUTO_FLASH. ' 211 f'Strength {flash_strengths[i]} mean: {burst_means[j]}, ' 212 f'Strength {flash_strengths[i+1]} mean: ' 213 f'{burst_means[j+1]}. ' 214 f'Diff: {diff}; TOL: {_BRIGHTNESS_MEAN_ATOL}' 215 ) 216 failure_messages.append(msg) 217 218 return failure_messages 219 220 221class TorchStrengthTest(its_base_test.ItsBaseTest): 222 """Test if torch strength control feature works as intended.""" 223 224 def test_torch_strength(self): 225 name_with_path = os.path.join(self.log_path, _TEST_NAME) 226 227 with its_session_utils.ItsSession( 228 device_id=self.dut.serial, 229 camera_id=self.camera_id, 230 hidden_physical_id=self.hidden_physical_id) as cam: 231 props = cam.get_camera_properties() 232 props = cam.override_with_hidden_physical_camera_props(props) 233 234 # check SKIP conditions 235 max_flash_strength = props[_MAX_SINGLE_STRENGTH_PROP_KEY] 236 max_torch_strength = props[_MAX_TORCH_STRENGTH_PROP_KEY] 237 camera_properties_utils.skip_unless( 238 camera_properties_utils.flash(props) and 239 max_flash_strength > _SINGLE_STRENGTH_CONTROL_THRESHOLD and 240 max_torch_strength > _TORCH_STRENGTH_CONTROL_THRESHOLD 241 ) 242 243 # establish connection with lighting controller 244 arduino_serial_port = lighting_control_utils.lighting_control( 245 self.lighting_cntl, self.lighting_ch 246 ) 247 248 failure_messages = [] 249 # testing at 80% of max strength 250 max_torch_strength = max_torch_strength * 0.8 251 # list with no torch (baseline), linear strength steps, 0.8 max strength 252 torch_strengths = [max_torch_strength*i/_STRENGTH_STEPS for i in 253 range(_STRENGTH_STEPS)] 254 torch_strengths.append(max_torch_strength) 255 logging.debug('Testing flash strengths: %s', torch_strengths) 256 for ae_mode in _TESTING_AE_MODES: 257 formats_means = [] 258 for strength in torch_strengths: 259 if (_TORCH_STRENGTH_MIN < strength <= 260 _TORCH_STRENGTH_CONTROL_THRESHOLD): 261 logging.debug('Torch strength value <= %d, test case ignored', 262 _TORCH_STRENGTH_CONTROL_THRESHOLD) 263 else: 264 # naming images to be captured 265 img_name_prefix = ( 266 f'{name_with_path}_ae_mode={ae_mode}_' 267 f'torch_strength={strength}_' 268 ) 269 # defining out_surfaces 270 width, height = _IMG_SIZE 271 out_surfaces = {'format': _FORMAT_NAME, 272 'width': width, 'height': height} 273 # take capture and evaluate 274 caps = _take_captures( 275 self, arduino_serial_port, out_surfaces, cam, 276 img_name_prefix, ae_mode, strength 277 ) 278 formats_means.append(_get_img_patch_mean(caps, props)) 279 280 # Compare means and compose failure messages 281 failure_messages += _compare_means(formats_means, 282 ae_mode, torch_strengths) 283 284 # turn the lights back on 285 lighting_control_utils.set_lighting_state( 286 arduino_serial_port, self.lighting_ch, 'ON') 287 288 # assert correct behavior and print error message(s) 289 if failure_messages: 290 raise AssertionError('\n'.join(failure_messages)) 291 292if __name__ == '__main__': 293 test_runner.main() 294 295