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