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 manual flash strength control (SINGLE capture mode) works correctly.""" 15 16import logging 17import os.path 18 19from mobly import test_runner 20import camera_properties_utils 21import capture_request_utils 22import image_processing_utils 23import its_base_test 24import its_session_utils 25import lighting_control_utils 26 27_TESTING_AE_MODES = (0, 1, 2) 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_FLASH_STATES = {0: 'FLASH_STATE_UNAVAILABLE', 1: 'FLASH_STATE_CHARGING', 34 2: 'FLASH_STATE_READY', 3: 'FLASH_STATE_FIRED', 35 4: 'FLASH_STATE_PARTIAL'} 36_FORMAT_NAME = 'yuv' 37_IMG_SIZE = (640, 480) 38_PATCH_H = 0.5 # center 50% 39_PATCH_W = 0.5 40_PATCH_X = 0.5-_PATCH_W/2 41_PATCH_Y = 0.5-_PATCH_H/2 42_TEST_NAME = os.path.splitext(os.path.basename(__file__))[0] 43_CAPTURE_INTENT_STILL_CAPTURE = 2 44_MAX_FLASH_STRENGTH = 'android.flash.singleStrengthMaxLevel' 45_MAX_TORCH_STRENGTH = 'android.flash.torchStrengthMaxLevel' 46_BRIGHTNESS_MEAN_ATOL = 15 # Tolerance for brightness mean 47_STRENGTH_STEPS = 3 # Steps of flash strengths to be tested 48 49 50def _take_captures(out_surfaces, cam, img_name, ae_mode, strength=0): 51 """Takes captures and returns the captured image. 52 53 Args: 54 out_surfaces: list; valid output surfaces for caps. 55 cam: ItsSession util object. 56 img_name: image name to be saved. 57 ae_mode: AE mode to be tested with. 58 strength: Flash strength that flash should be fired with. 59 Note that 0 is for baseline capture. 60 61 Returns: 62 cap: captured image object as defined by 63 ItsSessionUtils.do_capture(). 64 """ 65 cam.do_3a(do_af=False) 66 # Take base image without flash 67 if strength == 0: 68 cap_req = capture_request_utils.auto_capture_request() 69 cap_req[ 70 'android.control.captureIntent'] = _CAPTURE_INTENT_STILL_CAPTURE 71 cap_req['android.control.aeMode'] = 0 72 cap = cam.do_capture(cap_req, out_surfaces) 73 logging.debug('Capturing base image without flash') 74 # Take capture with flash strength 75 else: 76 cap = capture_request_utils.take_captures_with_flash_strength( 77 cam, out_surfaces, ae_mode, strength) 78 logging.debug('Capturing image with flash strength: %s', strength) 79 img = image_processing_utils.convert_capture_to_rgb_image(cap) 80 # Save captured image 81 image_processing_utils.write_image(img, img_name) 82 return cap 83 84 85def _get_mean(cap, props): 86 """Evaluate captured image by extracting means in the center patch. 87 88 Args: 89 cap: captured image object as defined by 90 ItsSessionUtils.do_capture(). 91 props: Camera properties object. 92 93 Returns: 94 mean: (float64) calculated mean of image center patch. 95 """ 96 metadata = cap['metadata'] 97 exp = int(metadata['android.sensor.exposureTime']) 98 iso = int(metadata['android.sensor.sensitivity']) 99 flash_exp_x_iso = [] 100 logging.debug('cap ISO: %d, exp: %d ns', iso, exp) 101 logging.debug('AE_MODE (cap): %s', 102 _AE_MODES[metadata['android.control.aeMode']]) 103 ae_state = _AE_STATES[metadata['android.control.aeState']] 104 logging.debug('AE_STATE (cap): %s', ae_state) 105 flash_state = _FLASH_STATES[metadata['android.flash.state']] 106 logging.debug('FLASH_STATE: %s', flash_state) 107 108 flash_exp_x_iso = exp*iso 109 y, _, _ = image_processing_utils.convert_capture_to_planes( 110 cap, props) 111 patch = image_processing_utils.get_image_patch( 112 y, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 113 flash_mean = image_processing_utils.compute_image_means( 114 patch)[0]*255 115 flash_grad = image_processing_utils.compute_image_max_gradients( 116 patch)[0]*255 117 118 # log results 119 logging.debug('Flash exposure X ISO %d', flash_exp_x_iso) 120 logging.debug('Flash frames Y grad: %.4f', flash_grad) 121 logging.debug('Flash frames Y mean: %.4f', flash_mean) 122 return flash_mean 123 124 125def _compare_means(formats_means, ae_mode, flash_strengths): 126 """Analyzes test results and generates failure messages. 127 128 If AE_MODE is ON/OFF, capture should show mean differences 129 in flash strengths. If AE_MODE is ON_AUTO_FLASH, flash 130 strength should be overwritten hence no mean difference in captures. 131 132 Args: 133 formats_means: list of calculated means of image center patches. 134 ae_mode: requested AE mode during testing. 135 flash_strengths: list of flash strength values requested during testing. 136 137 Returns: 138 failure_messages: (list of string) list of error messages. 139 """ 140 failure_messages = [] 141 if ae_mode in _AE_MODE_FLASH_CONTROL: 142 for mean in range(1, len(formats_means)-1): 143 if formats_means[mean] >= formats_means[mean+1]: 144 msg = ( 145 f'Capture with CONTROL_AE_MODE {_AE_MODES[ae_mode]}. ' 146 f'Strength {flash_strengths[mean]} mean: {formats_means[mean]}; ' 147 f'Strength {flash_strengths[mean+1]} mean: ' 148 f'{formats_means[mean+1]}. ' 149 f'Mean of {flash_strengths[mean+1]} should be brighter than ' 150 f'Mean of {flash_strengths[mean]}. ' 151 ) 152 failure_messages.append(msg) 153 else: 154 for mean in range(1, len(formats_means)-1): 155 diff = abs(formats_means[mean] - formats_means[mean+1]) 156 if diff > _BRIGHTNESS_MEAN_ATOL: 157 msg = ( 158 f'Capture with CONTROL_AE_MODE {_AE_MODES[ae_mode]}. ' 159 f'Strength {flash_strengths[mean]} mean: {formats_means[mean]}; ' 160 f'Strength {flash_strengths[mean+1]} mean: ' 161 f'{formats_means[mean+1]}. ' 162 f'Diff: {diff}; ATOL: {_BRIGHTNESS_MEAN_ATOL}. ' 163 ) 164 failure_messages.append(msg) 165 return failure_messages 166 167 168class FlashStrengthTest(its_base_test.ItsBaseTest): 169 """Test if flash strength control (SINGLE capture mode) feature works as intended.""" 170 171 def test_flash_strength(self): 172 name_with_path = os.path.join(self.log_path, _TEST_NAME) 173 174 with its_session_utils.ItsSession( 175 device_id=self.dut.serial, 176 camera_id=self.camera_id, 177 hidden_physical_id=self.hidden_physical_id) as cam: 178 props = cam.get_camera_properties() 179 props = cam.override_with_hidden_physical_camera_props(props) 180 181 # check SKIP conditions 182 max_flash_strength = props[_MAX_FLASH_STRENGTH] 183 max_torch_strength = props[_MAX_TORCH_STRENGTH] 184 camera_properties_utils.skip_unless( 185 camera_properties_utils.flash(props) and 186 max_flash_strength > 1 and max_torch_strength > 1) 187 # establish connection with lighting controller 188 arduino_serial_port = lighting_control_utils.lighting_control( 189 self.lighting_cntl, self.lighting_ch) 190 191 # turn OFF lights to darken scene 192 lighting_control_utils.set_lighting_state( 193 arduino_serial_port, self.lighting_ch, 'OFF') 194 195 failure_messages = [] 196 # list with no flash (baseline), linear strength steps, max strength 197 flash_strengths = [max_flash_strength*i/_STRENGTH_STEPS for i in 198 range(_STRENGTH_STEPS)] 199 flash_strengths.append(max_flash_strength) 200 logging.debug('Testing flash strengths: %s', flash_strengths) 201 # loop through ae modes to be tested 202 for ae_mode in _TESTING_AE_MODES: 203 formats_means = [] 204 # loop through flash strengths 205 for strength in flash_strengths: 206 if 0 < strength <= 1: 207 logging.debug('Flash strength value <=1, test case ignored') 208 else: 209 # naming images to be captured 210 img_name = f'{name_with_path}_ae_mode={ae_mode}_flash_strength={strength}.jpg' 211 # check if testing image size is supported, if not use mid size 212 output_sizes = capture_request_utils.get_available_output_sizes( 213 _FORMAT_NAME, props) 214 if _IMG_SIZE in output_sizes: 215 width, height = _IMG_SIZE 216 logging.debug( 217 'Testing with default image size: %dx%d', width, height 218 ) 219 else: 220 width, height = output_sizes[len(output_sizes)//2] 221 logging.debug( 222 'Default size not supported, testing with size: %dx%d', 223 width, height 224 ) 225 # defining out_surfaces 226 out_surfaces = {'format': _FORMAT_NAME, 227 'width': width, 'height': height} 228 # take capture and evaluate 229 cap = _take_captures(out_surfaces, cam, img_name, ae_mode, strength) 230 formats_means.append(_get_mean(cap, props)) 231 check_mean = True 232 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 233 if ( 234 ae_mode == 1 and 235 first_api_level <= its_session_utils.ANDROID15_API_LEVEL 236 ): 237 check_mean = False 238 # Compare means and assert PASS/FAIL 239 if check_mean: 240 failure_messages += _compare_means(formats_means, 241 ae_mode, flash_strengths) 242 243 # turn the lights back on 244 lighting_control_utils.set_lighting_state( 245 arduino_serial_port, self.lighting_ch, 'ON') 246 if failure_messages: 247 raise AssertionError('\n'.join(failure_messages)) 248 249if __name__ == '__main__': 250 test_runner.main() 251