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 20import matplotlib 21from matplotlib import pylab 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_LOCKED = 3 32_LUMA_LOCKED_RTOL_EV_SM = 0.05 33_LUMA_LOCKED_RTOL_EV_LG = 0.10 34_NAME = os.path.splitext(os.path.basename(__file__))[0] 35_NUM_UNSATURATED_EVS = 3 36_PATCH_H = 0.1 # center 10% 37_PATCH_W = 0.1 38_PATCH_X = 0.5 - _PATCH_W/2 39_PATCH_Y = 0.5 - _PATCH_H/2 40_THRESH_CONVERGE_FOR_EV = 8 # AE must converge within this num 41_VGA_W, _VGA_H = 640, 480 42_YUV_FULL_SCALE = 255.0 43_YUV_SAT_MIN = 250.0 44_YUV_SAT_TOL = 3.0 45 46 47def create_request_with_ev(ev): 48 """Create request with EV value.""" 49 req = capture_request_utils.auto_capture_request() 50 req['android.control.aeExposureCompensation'] = ev 51 req['android.control.aeLock'] = True 52 req['android.control.awbLock'] = True 53 return req 54 55 56class EvCompensationBasicTest(its_base_test.ItsBaseTest): 57 """Tests that EV compensation is applied.""" 58 59 def test_ev_compensation_basic(self): 60 logging.debug('Starting %s', _NAME) 61 with its_session_utils.ItsSession( 62 device_id=self.dut.serial, 63 camera_id=self.camera_id, 64 hidden_physical_id=self.hidden_physical_id) as cam: 65 props = cam.get_camera_properties() 66 props = cam.override_with_hidden_physical_camera_props(props) 67 log_path = self.log_path 68 test_name_w_path = os.path.join(log_path, _NAME) 69 70 # check SKIP conditions 71 camera_properties_utils.skip_unless( 72 camera_properties_utils.ev_compensation(props) and 73 camera_properties_utils.ae_lock(props) and 74 camera_properties_utils.awb_lock(props)) 75 76 # Load chart for scene 77 its_session_utils.load_scene( 78 cam, props, self.scene, self.tablet, 79 its_session_utils.CHART_DISTANCE_NO_SCALING) 80 81 # Create ev compensation changes 82 ev_per_step = capture_request_utils.rational_to_float( 83 props['android.control.aeCompensationStep']) 84 steps_per_ev = int(1.0 / ev_per_step) 85 evs = range(-2 * steps_per_ev, 2 * steps_per_ev + 1, steps_per_ev) 86 luma_locked_rtols = [_LUMA_LOCKED_RTOL_EV_LG, 87 _LUMA_LOCKED_RTOL_EV_SM, 88 _LUMA_LOCKED_RTOL_EV_SM, 89 _LUMA_LOCKED_RTOL_EV_SM, 90 _LUMA_LOCKED_RTOL_EV_LG] 91 92 # Converge 3A, and lock AE once converged. skip AF trigger as 93 # dark/bright scene could make AF convergence fail and this test 94 # doesn't care the image sharpness. 95 mono_camera = camera_properties_utils.mono_camera(props) 96 cam.do_3a(ev_comp=0, lock_ae=True, lock_awb=True, do_af=False, 97 mono_camera=mono_camera) 98 99 # Do captures and extract information 100 largest_yuv = capture_request_utils.get_largest_yuv_format(props) 101 match_ar = (largest_yuv['width'], largest_yuv['height']) 102 fmt = capture_request_utils.get_near_vga_yuv_format( 103 props, match_ar=match_ar) 104 if fmt['width'] * fmt['height'] > _VGA_W * _VGA_H: 105 fmt = {'format': 'yuv', 'width': _VGA_W, 'height': _VGA_H} 106 logging.debug('YUV size: %d x %d', fmt['width'], fmt['height']) 107 lumas = [] 108 for j, ev in enumerate(evs): 109 luma_locked_rtol = luma_locked_rtols[j] 110 # Capture a single shot with the same EV comp and locked AE. 111 req = create_request_with_ev(ev) 112 caps = cam.do_capture([req]*_THRESH_CONVERGE_FOR_EV, fmt) 113 luma_locked = [] 114 for i, cap in enumerate(caps): 115 if cap['metadata']['android.control.aeState'] == _LOCKED: 116 ev_meta = cap['metadata']['android.control.aeExposureCompensation'] 117 exp = cap['metadata']['android.sensor.exposureTime'] 118 iso = cap['metadata']['android.sensor.sensitivity'] 119 logging.debug('cap EV: %d, exp: %dns, ISO: %d', ev_meta, exp, iso) 120 if ev != ev_meta: 121 raise AssertionError( 122 f'EV compensation cap != req! cap: {ev_meta}, req: {ev}') 123 luma = image_processing_utils.extract_luma_from_patch( 124 cap, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 125 luma_locked.append(luma) 126 if i == _THRESH_CONVERGE_FOR_EV-1: 127 lumas.append(luma) 128 if not math.isclose(min(luma_locked), max(luma_locked), 129 rel_tol=luma_locked_rtol): 130 raise AssertionError(f'EV {ev} burst lumas: {luma_locked}, ' 131 f'RTOL: {luma_locked_rtol}') 132 logging.debug('lumas per frame ev %d: %s', ev, str(luma_locked)) 133 logging.debug('mean lumas in AE locked captures: %s', str(lumas)) 134 if caps[_THRESH_CONVERGE_FOR_EV-1]['metadata'][ 135 'android.control.aeState'] != _LOCKED: 136 raise AssertionError(f'No AE lock by {_THRESH_CONVERGE_FOR_EV} frame.') 137 138 # Create plot 139 pylab.figure(_NAME) 140 pylab.plot(evs, lumas, '-ro') 141 pylab.title(_NAME) 142 pylab.xlabel('EV Compensation') 143 pylab.ylabel('Mean Luma (Normalized)') 144 matplotlib.pyplot.savefig(f'{test_name_w_path}_plot_means.png') 145 146 # Trim extra saturated images 147 while (lumas[-2] >= _YUV_SAT_MIN/_YUV_FULL_SCALE and 148 lumas[-1] >= _YUV_SAT_MIN/_YUV_FULL_SCALE and 149 len(lumas) > 2): 150 lumas.pop(-1) 151 logging.debug('Removed saturated image.') 152 153 # Only allow positive EVs to give saturated image 154 if len(lumas) < _NUM_UNSATURATED_EVS: 155 raise AssertionError( 156 f'>{_NUM_UNSATURATED_EVS-1} unsaturated images needed.') 157 min_luma_diffs = min(np.diff(lumas)) 158 logging.debug('Min of luma value difference between adjacent ev comp: %.3f', 159 min_luma_diffs) 160 161 # Assert unsaturated lumas increasing with increasing ev comp. 162 if min_luma_diffs <= 0: 163 raise AssertionError('Lumas not increasing with ev comp! ' 164 f'EVs: {list(evs)}, lumas: {lumas}') 165 166 167if __name__ == '__main__': 168 test_runner.main() 169