1# Copyright 2014 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"""Verifies EV compensation is applied.""" 15 16 17import logging 18import math 19import os.path 20 21from matplotlib import pyplot as plt 22from mobly import test_runner 23import numpy as np 24 25import its_base_test 26import camera_properties_utils 27import capture_request_utils 28import image_processing_utils 29import its_session_utils 30 31 32_LINEAR_TONEMAP_CURVE = [0.0, 0.0, 1.0, 1.0] 33_LOCKED = 3 34_LUMA_DELTA_ATOL = 0.05 35_LUMA_DELTA_ATOL_SAT = 0.10 36_LUMA_LOCKED_RTOL_EV_SM = 0.05 37_LUMA_LOCKED_RTOL_EV_LG = 0.10 38_LUMA_SAT_THRESH = 0.75 # luma value at which ATOL changes from MID to SAT 39_NAME = os.path.splitext(os.path.basename(__file__))[0] 40_NUM_UNSATURATED_EVS = 3 41_PATCH_H = 0.1 # center 10% 42_PATCH_W = 0.1 43_PATCH_X = 0.5 - _PATCH_W/2 44_PATCH_Y = 0.5 - _PATCH_H/2 45_THRESH_CONVERGE_FOR_EV = 8 # AE must converge within this num 46_VGA_W, _VGA_H = 640, 480 47_YUV_FULL_SCALE = 255 48_YUV_SAT_MIN = 250 49 50 51def _assert_correct_advanced_ev_compensation( 52 imgs, ev_steps, lumas, expected_lumas, luma_delta_atols, log_path): 53 """Assert correct advanced EV compensation behavior. 54 55 Args: 56 imgs: list of image arrays from captures. 57 ev_steps: list of EV compensation steps. 58 lumas: measured luma values over EV steps. 59 expected_lumas: expected luma values over EV steps. 60 luma_delta_atols: ATOLs for luma change for each EV step. 61 log_path: pointer to location to save files. 62 """ 63 failed_test = False 64 e_msg = [] 65 for i, luma in enumerate(lumas): 66 luma_delta_atol = luma_delta_atols[i] 67 logging.debug('EV step: %3d, luma: %.3f, model: %.3f, ATOL: %.2f', 68 ev_steps[i], luma, expected_lumas[i], luma_delta_atol) 69 if not math.isclose(luma, expected_lumas[i], abs_tol=luma_delta_atol): 70 failed_test = True 71 e_msg.append(f'measured: {lumas[i]}, model: {expected_lumas[i]}, ' 72 f'ATOL: {luma_delta_atol}. ') 73 if failed_test: 74 test_name_w_path = os.path.join(log_path, f'{_NAME}_advanced') 75 for i, img in enumerate(imgs): 76 image_processing_utils.write_image( 77 img, f'{test_name_w_path}_{ev_steps[i]}.jpg') 78 raise AssertionError( 79 f'Measured/modeled luma deltas too large! {e_msg}') 80 81 82def _extract_capture_luma(cap, ev): 83 """Extract and log metadata while calculating luma value. 84 85 Args: 86 cap: capture object. 87 ev: integer EV value. 88 89 Returns: 90 luma: the average luma of the center patch of the capture. 91 """ 92 ev_meta = cap['metadata']['android.control.aeExposureCompensation'] 93 exp = cap['metadata']['android.sensor.exposureTime'] 94 iso = cap['metadata']['android.sensor.sensitivity'] 95 logging.debug('cap EV: %d, exp: %dns, ISO: %d', ev_meta, exp, iso) 96 if ev != ev_meta: 97 raise AssertionError( 98 f'EV compensation cap != req! cap: {ev_meta}, req: {ev}') 99 luma = image_processing_utils.extract_luma_from_patch( 100 cap, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 101 return luma 102 103 104def _create_basic_plot(evs, lumas, log_path): 105 """Create plot for basic EV compensation. 106 107 Args: 108 evs: list of EV compensation steps. 109 lumas: list of measured luma values. 110 log_path: string pointer to results area. 111 """ 112 test_name = f'{_NAME}_basic' 113 test_name_w_path = os.path.join(log_path, test_name) 114 plt.figure(test_name) 115 plt.plot(evs, lumas, '-ro') 116 plt.title(test_name) 117 plt.xlabel('EV Compensation') 118 plt.ylabel('Mean Luma (Normalized)') 119 plt.savefig(f'{test_name_w_path}_plot.png') 120 121 122def _create_advanced_plot(ev_steps, lumas, expected_lumas, log_path): 123 """Create plot for advanced EV compensation. 124 125 Args: 126 ev_steps: list of EV compensation steps. 127 lumas: list of measured luma values. 128 expected_lumas: list of expected luma values. 129 log_path: string pointer to results area. 130 """ 131 132 test_name = f'{_NAME}_advanced' 133 test_name_w_path = os.path.join(log_path, test_name) 134 plt.figure(test_name) 135 plt.plot(ev_steps, lumas, '-ro', label='measured', alpha=0.7) 136 plt.plot(ev_steps, expected_lumas, '-bo', label='expected', alpha=0.7) 137 plt.title(test_name) 138 plt.xlabel('EV Compensation') 139 plt.ylabel('Mean Luma (Normalized)') 140 plt.legend(loc='lower right', numpoints=1, fancybox=True) 141 plt.savefig(f'{test_name_w_path}_plot.png') 142 143 144def _create_basic_request_with_ev(ev): 145 """Create basic request with EV value. 146 147 Args: 148 ev: EV value to set. 149 150 Returns: 151 A request object with the given EV value. 152 """ 153 req = capture_request_utils.auto_capture_request() 154 req['android.control.aeExposureCompensation'] = ev 155 req['android.control.aeLock'] = True 156 req['android.control.awbLock'] = True 157 return req 158 159 160def _create_advanced_request_with_ev(ev): 161 """Create advanced request with the ev compensation step. 162 163 Args: 164 ev: EV value to set. 165 166 Returns: 167 A request object with the given EV value. 168 """ 169 req = capture_request_utils.auto_capture_request() 170 req['android.control.aeExposureCompensation'] = ev 171 req['android.control.aeLock'] = True 172 req['android.control.awbLock'] = True 173 # Use linear tonemap to avoid brightness being impacted by tone curves. 174 req['android.tonemap.mode'] = 0 175 req['android.tonemap.curve'] = {'red': _LINEAR_TONEMAP_CURVE, 176 'green': _LINEAR_TONEMAP_CURVE, 177 'blue': _LINEAR_TONEMAP_CURVE} 178 return req 179 180 181def _create_basic_ev_comp_changes(props): 182 """Create basic ev compensation steps and shifts from control params. 183 184 Args: 185 props: camera properties. 186 187 Returns: 188 evs: list of EV compensation steps. 189 luma_locked_rtols: list of RTOLs for captures with luma locked. 190 """ 191 ev_per_step = capture_request_utils.rational_to_float( 192 props['android.control.aeCompensationStep']) 193 steps_per_ev = int(1.0 / ev_per_step) 194 evs = range(-2 * steps_per_ev, 2 * steps_per_ev + 1, steps_per_ev) 195 luma_locked_rtols = [_LUMA_LOCKED_RTOL_EV_LG, 196 _LUMA_LOCKED_RTOL_EV_SM, 197 _LUMA_LOCKED_RTOL_EV_SM, 198 _LUMA_LOCKED_RTOL_EV_SM, 199 _LUMA_LOCKED_RTOL_EV_LG] 200 return evs, luma_locked_rtols 201 202 203def _create_advanced_ev_comp_changes(props): 204 """Create advanced ev compensation steps and shifts from control params. 205 206 Args: 207 props: camera properties. 208 209 Returns: 210 EV steps list and EV shifts list. 211 """ 212 ev_compensation_range = props['android.control.aeCompensationRange'] 213 range_min = ev_compensation_range[0] 214 range_max = ev_compensation_range[1] 215 ev_per_step = capture_request_utils.rational_to_float( 216 props['android.control.aeCompensationStep']) 217 logging.debug('ev_step_size_in_stops: %.3f', ev_per_step) 218 steps_per_ev = int(round(1.0 / ev_per_step)) 219 ev_steps = range(range_min, range_max + 1, steps_per_ev) 220 ev_shifts = [pow(2, step * ev_per_step) for step in ev_steps] 221 return ev_steps, ev_shifts 222 223 224class EvCompensationTest(its_base_test.ItsBaseTest): 225 """Tests that EV compensation is applied.""" 226 227 def test_ev_compensation(self): 228 # Basic test code 229 logging.debug('Starting %s_basic', _NAME) 230 with its_session_utils.ItsSession( 231 device_id=self.dut.serial, 232 camera_id=self.camera_id, 233 hidden_physical_id=self.hidden_physical_id) as cam: 234 props = cam.get_camera_properties() 235 props = cam.override_with_hidden_physical_camera_props(props) 236 log_path = self.log_path 237 238 # Check common basic/advanced SKIP conditions 239 camera_properties_utils.skip_unless( 240 camera_properties_utils.ev_compensation(props) and 241 camera_properties_utils.ae_lock(props) and 242 camera_properties_utils.awb_lock(props)) 243 244 # Load chart for scene 245 its_session_utils.load_scene( 246 cam, props, self.scene, self.tablet, 247 its_session_utils.CHART_DISTANCE_NO_SCALING) 248 249 # Create basic EV compensation changes 250 evs, luma_locked_rtols = _create_basic_ev_comp_changes(props) 251 252 # Converge 3A, and lock AE once converged. skip AF trigger as 253 # dark/bright scene could make AF convergence fail and this test 254 # doesn't care the image sharpness. 255 mono_camera = camera_properties_utils.mono_camera(props) 256 cam.do_3a(ev_comp=0, lock_ae=True, lock_awb=True, do_af=False, 257 mono_camera=mono_camera) 258 259 # Do captures and extract information 260 largest_yuv = capture_request_utils.get_largest_format('yuv', props) 261 match_ar = (largest_yuv['width'], largest_yuv['height']) 262 fmt = capture_request_utils.get_near_vga_yuv_format( 263 props, match_ar=match_ar) 264 if fmt['width'] * fmt['height'] > _VGA_W * _VGA_H: 265 fmt = {'format': 'yuv', 'width': _VGA_W, 'height': _VGA_H} 266 logging.debug('YUV size: %d x %d', fmt['width'], fmt['height']) 267 lumas = [] 268 for j, ev in enumerate(evs): 269 luma_locked_rtol = luma_locked_rtols[j] 270 # Capture a single shot with the same EV comp and locked AE. 271 req = _create_basic_request_with_ev(ev) 272 caps = cam.do_capture([req]*_THRESH_CONVERGE_FOR_EV, fmt) 273 luma_locked = [] 274 for i, cap in enumerate(caps): 275 if cap['metadata']['android.control.aeState'] == _LOCKED: 276 luma = _extract_capture_luma(cap, ev) 277 luma_locked.append(luma) 278 if i == _THRESH_CONVERGE_FOR_EV-1: 279 lumas.append(luma) 280 if not math.isclose(min(luma_locked), max(luma_locked), 281 rel_tol=luma_locked_rtol): 282 raise AssertionError(f'EV {ev} burst lumas: {luma_locked}, ' 283 f'RTOL: {luma_locked_rtol}') 284 logging.debug('lumas per frame ev %d: %s', ev, luma_locked) 285 logging.debug('mean lumas in AE locked captures: %s', lumas) 286 if caps[_THRESH_CONVERGE_FOR_EV-1]['metadata'][ 287 'android.control.aeState'] != _LOCKED: 288 raise AssertionError(f'No AE lock by {_THRESH_CONVERGE_FOR_EV} frame.') 289 290 # Create basic plot 291 _create_basic_plot(evs, lumas, log_path) 292 293 # Trim extra saturated images 294 while (lumas[-2] >= _YUV_SAT_MIN/_YUV_FULL_SCALE and 295 lumas[-1] >= _YUV_SAT_MIN/_YUV_FULL_SCALE and 296 len(lumas) > 2): 297 lumas.pop(-1) 298 logging.debug('Removed saturated image.') 299 300 # Only allow positive EVs to give saturated image 301 if len(lumas) < _NUM_UNSATURATED_EVS: 302 raise AssertionError( 303 f'>{_NUM_UNSATURATED_EVS-1} unsaturated images needed.') 304 min_luma_diffs = min(np.diff(lumas)) 305 logging.debug( 306 'Min of luma value difference between adjacent ev comp: %.3f', 307 min_luma_diffs 308 ) 309 310 # Assert unsaturated lumas increasing with increasing ev comp. 311 if min_luma_diffs <= 0: 312 raise AssertionError('Lumas not increasing with ev comp! ' 313 f'EVs: {list(evs)}, lumas: {lumas}') 314 315 # Advanced test code 316 logging.debug('Starting %s_advanced', _NAME) 317 318 # check advanced SKIP conditions 319 if not (camera_properties_utils.manual_sensor(props) and 320 camera_properties_utils.manual_post_proc(props) and 321 camera_properties_utils.per_frame_control(props) 322 ): 323 return 324 325 # Create advanced EV compensation changes 326 ev_steps, ev_shifts = _create_advanced_ev_comp_changes(props) 327 328 # Converge 3A, and lock AE once converged. skip AF trigger as 329 # dark/bright scene could make AF convergence fail and this test 330 # doesn't care the image sharpness. 331 cam.do_3a(ev_comp=0, lock_ae=True, lock_awb=True, do_af=False, 332 mono_camera=mono_camera) 333 334 # Create requests and capture 335 match_ar = (largest_yuv['width'], largest_yuv['height']) 336 fmt = capture_request_utils.get_near_vga_yuv_format( 337 props, match_ar=match_ar) 338 imgs = [] 339 lumas = [] 340 for ev in ev_steps: 341 # Capture a single shot with the same EV comp and locked AE. 342 req = _create_advanced_request_with_ev(ev) 343 caps = cam.do_capture([req]*_THRESH_CONVERGE_FOR_EV, fmt) 344 for cap in caps: 345 if cap['metadata']['android.control.aeState'] == _LOCKED: 346 ev_meta = cap['metadata']['android.control.aeExposureCompensation'] 347 if ev_meta != ev: 348 raise AssertionError( 349 f'EV comp capture != request! cap: {ev_meta}, req: {ev}') 350 imgs.append( 351 image_processing_utils.convert_capture_to_rgb_image(cap)) 352 lumas.append(image_processing_utils.extract_luma_from_patch( 353 cap, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)) 354 break 355 if caps[_THRESH_CONVERGE_FOR_EV-1]['metadata'][ 356 'android.control.aeState'] != _LOCKED: 357 raise AssertionError('AE does not reach locked state in ' 358 f'{_THRESH_CONVERGE_FOR_EV} frames.') 359 logging.debug('lumas in AE locked captures: %s', str(lumas)) 360 361 # Create advanced plot 362 i_mid = len(ev_steps) // 2 363 luma_normal = lumas[i_mid] / ev_shifts[i_mid] 364 expected_lumas = [min(1.0, luma_normal*shift) for shift in ev_shifts] 365 _create_advanced_plot(ev_steps, lumas, expected_lumas, log_path) 366 367 # Assert correct behavior for advanced EV compensation 368 luma_delta_atols = [_LUMA_DELTA_ATOL if l < _LUMA_SAT_THRESH 369 else _LUMA_DELTA_ATOL_SAT for l in expected_lumas] 370 _assert_correct_advanced_ev_compensation( 371 imgs, ev_steps, lumas, expected_lumas, luma_delta_atols, log_path 372 ) 373 374 375if __name__ == '__main__': 376 test_runner.main() 377