1# Copyright 2013 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 correct exposure control.""" 15 16 17import logging 18import os.path 19import matplotlib 20from matplotlib import pylab 21 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 30import target_exposure_utils 31 32_EXP_CORRECTION_FACTOR = 2 # mult or div factor to correct brightness 33_NAME = os.path.splitext(os.path.basename(__file__))[0] 34_NUM_PTS_2X_GAIN = 3 # 3 points every 2x increase in gain 35_PATCH_H = 0.1 # center 10% patch params 36_PATCH_W = 0.1 37_PATCH_X = 0.5 - _PATCH_W/2 38_PATCH_Y = 0.5 - _PATCH_H/2 39_RAW_STATS_GRID = 9 # define 9x9 (11.11%) spacing grid for rawStats processing 40_RAW_STATS_XY = _RAW_STATS_GRID//2 # define X, Y location for center rawStats 41_THRESH_MIN_LEVEL = 0.1 42_THRESH_MAX_LEVEL = 0.9 43_THRESH_MAX_LEVEL_DIFF = 0.045 44_THRESH_MAX_LEVEL_DIFF_WIDE_RANGE = 0.06 45_THRESH_MAX_OUTLIER_DIFF = 0.1 46_THRESH_ROUND_DOWN_ISO = 0.04 47_THRESH_ROUND_DOWN_EXP = 0.03 48_THRESH_ROUND_DOWN_EXP0 = 1.00 # RTOL @0ms exp; theoretical limit @ 4-line exp 49_THRESH_EXP_KNEE = 6E6 # exposures less than knee have relaxed tol 50_WIDE_EXP_RANGE_THRESH = 64.0 # threshold for 'wide' range sensor 51 52 53def adjust_exp_for_brightness( 54 cam, props, fmt, exp, iso, sync_latency, test_name_with_path): 55 """Take an image and adjust exposure and sensitivity. 56 57 Args: 58 cam: camera object 59 props: camera properties dict 60 fmt: capture format 61 exp: exposure time (ns) 62 iso: sensitivity 63 sync_latency: number for sync latency 64 test_name_with_path: path for saved files 65 66 Returns: 67 adjusted exposure 68 """ 69 req = capture_request_utils.manual_capture_request( 70 iso, exp, 0.0, True, props) 71 cap = its_session_utils.do_capture_with_latency( 72 cam, req, sync_latency, fmt) 73 img = image_processing_utils.convert_capture_to_rgb_image(cap) 74 image_processing_utils.write_image( 75 img, f'{test_name_with_path}.jpg') 76 patch = image_processing_utils.get_image_patch( 77 img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 78 r, g, b = image_processing_utils.compute_image_means(patch) 79 logging.debug('Sample RGB values: %.3f, %.3f, %.3f', r, g, b) 80 if g < _THRESH_MIN_LEVEL: 81 exp *= _EXP_CORRECTION_FACTOR 82 logging.debug('exp increased by %dx: %d', _EXP_CORRECTION_FACTOR, exp) 83 elif g > _THRESH_MAX_LEVEL: 84 exp //= _EXP_CORRECTION_FACTOR 85 logging.debug('exp decreased to 1/%dx: %d', _EXP_CORRECTION_FACTOR, exp) 86 return exp 87 88 89def plot_rgb_means(title, x, r, g, b, test_name_with_path): 90 """Plot the RGB mean data. 91 92 Args: 93 title: string for figure title 94 x: x values for plot, gain multiplier 95 r: r plane means 96 g: g plane means 97 b: b plane menas 98 test_name_with_path: path for saved files 99 """ 100 pylab.figure(title) 101 pylab.semilogx(x, r, 'ro-') 102 pylab.semilogx(x, g, 'go-') 103 pylab.semilogx(x, b, 'bo-') 104 pylab.title(f'{_NAME} {title}') 105 pylab.xlabel('Gain Multiplier') 106 pylab.ylabel('Normalized RGB Plane Avg') 107 pylab.minorticks_off() 108 pylab.xticks(x[0::_NUM_PTS_2X_GAIN], x[0::_NUM_PTS_2X_GAIN]) 109 pylab.ylim([0, 1]) 110 plot_name = f'{test_name_with_path}_plot_rgb_means.png' 111 matplotlib.pyplot.savefig(plot_name) 112 113 114def plot_raw_means(title, x, r, gr, gb, b, test_name_with_path): 115 """Plot the RAW mean data. 116 117 Args: 118 title: string for figure title 119 x: x values for plot, gain multiplier 120 r: R plane means 121 gr: Gr plane means 122 gb: Gb plane means 123 b: B plane menas 124 test_name_with_path: path for saved files 125 """ 126 pylab.figure(title) 127 pylab.semilogx(x, r, 'ro-', label='R') 128 pylab.semilogx(x, gr, 'go-', label='Gr') 129 pylab.semilogx(x, gb, 'ko-', label='Gb') 130 pylab.semilogx(x, b, 'bo-', label='B') 131 pylab.title(f'{_NAME} {title}') 132 pylab.xlabel('Gain Multiplier') 133 pylab.ylabel('Normalized RAW Plane Avg') 134 pylab.minorticks_off() 135 pylab.xticks(x[0::_NUM_PTS_2X_GAIN], x[0::_NUM_PTS_2X_GAIN]) 136 pylab.ylim([0, 1]) 137 pylab.legend(numpoints=1) 138 plot_name = f'{test_name_with_path}_plot_raw_means.png' 139 matplotlib.pyplot.savefig(plot_name) 140 141 142def check_line_fit(color, mults, values, thresh_max_level_diff): 143 """Find line fit and check values. 144 145 Check for linearity. Verify sample pixel mean values are close to each 146 other. Also ensure that the images aren't clamped to 0 or 1 147 (which would also make them look like flat lines). 148 149 Args: 150 color: string to define RGB or RAW channel 151 mults: list of multiplication values for gain*m, exp/m 152 values: mean values for chan 153 thresh_max_level_diff: threshold for max difference 154 """ 155 156 m, b = np.polyfit(mults, values, 1).tolist() 157 min_val = min(values) 158 max_val = max(values) 159 max_diff = max_val - min_val 160 logging.debug('Channel %s line fit (y = mx+b): m = %f, b = %f', color, m, b) 161 logging.debug('Channel min %f max %f diff %f', min_val, max_val, max_diff) 162 if max_diff >= thresh_max_level_diff: 163 raise AssertionError(f'max_diff: {max_diff:.4f}, ' 164 f'THRESH: {thresh_max_level_diff:.3f}') 165 if not _THRESH_MAX_LEVEL > b > _THRESH_MIN_LEVEL: 166 raise AssertionError(f'b: {b:.2f}, THRESH_MIN: {_THRESH_MIN_LEVEL}, ' 167 f'THRESH_MAX: {_THRESH_MAX_LEVEL}') 168 for v in values: 169 if not _THRESH_MAX_LEVEL > v > _THRESH_MIN_LEVEL: 170 raise AssertionError(f'v: {v:.2f}, THRESH_MIN: {_THRESH_MIN_LEVEL}, ' 171 f'THRESH_MAX: {_THRESH_MAX_LEVEL}') 172 173 if abs(v - b) >= _THRESH_MAX_OUTLIER_DIFF: 174 raise AssertionError(f'v: {v:.2f}, b: {b:.2f}, ' 175 f'THRESH_DIFF: {_THRESH_MAX_OUTLIER_DIFF}') 176 177 178def get_raw_active_array_size(props): 179 """Return the active array w, h from props.""" 180 aaw = (props['android.sensor.info.preCorrectionActiveArraySize']['right'] - 181 props['android.sensor.info.preCorrectionActiveArraySize']['left']) 182 aah = (props['android.sensor.info.preCorrectionActiveArraySize']['bottom'] - 183 props['android.sensor.info.preCorrectionActiveArraySize']['top']) 184 return aaw, aah 185 186 187class ExposureXIsoTest(its_base_test.ItsBaseTest): 188 """Test that a constant brightness is seen as ISO and exposure time vary. 189 190 Take a series of shots that have ISO and exposure time chosen to balance 191 each other; result should be the same brightness, but over the sequence 192 the images should get noisier. 193 """ 194 195 def test_exposure_x_iso(self): 196 mults = [] 197 r_means = [] 198 g_means = [] 199 b_means = [] 200 raw_r_means = [] 201 raw_gr_means = [] 202 raw_gb_means = [] 203 raw_b_means = [] 204 thresh_max_level_diff = _THRESH_MAX_LEVEL_DIFF 205 206 with its_session_utils.ItsSession( 207 device_id=self.dut.serial, 208 camera_id=self.camera_id, 209 hidden_physical_id=self.hidden_physical_id) as cam: 210 props = cam.get_camera_properties() 211 props = cam.override_with_hidden_physical_camera_props(props) 212 test_name_with_path = os.path.join(self.log_path, _NAME) 213 214 # Check SKIP conditions 215 camera_properties_utils.skip_unless( 216 camera_properties_utils.compute_target_exposure(props)) 217 218 # Load chart for scene 219 its_session_utils.load_scene( 220 cam, props, self.scene, self.tablet, 221 its_session_utils.CHART_DISTANCE_NO_SCALING) 222 223 # Initialize params for requests 224 debug = self.debug_mode 225 raw_avlb = (camera_properties_utils.raw16(props) and 226 camera_properties_utils.manual_sensor(props)) 227 sync_latency = camera_properties_utils.sync_latency(props) 228 logging.debug('sync latency: %d frames', sync_latency) 229 largest_yuv = capture_request_utils.get_largest_yuv_format(props) 230 match_ar = (largest_yuv['width'], largest_yuv['height']) 231 fmt = capture_request_utils.get_near_vga_yuv_format( 232 props, match_ar=match_ar) 233 e, s = target_exposure_utils.get_target_exposure_combos( 234 self.log_path, cam)['minSensitivity'] 235 236 # Take a shot and adjust parameters for brightness 237 logging.debug('Target exposure combo values. exp: %d, iso: %d', 238 e, s) 239 e = adjust_exp_for_brightness( 240 cam, props, fmt, e, s, sync_latency, test_name_with_path) 241 242 # Initialize values to define test range 243 s_e_product = s * e 244 expt_range = props['android.sensor.info.exposureTimeRange'] 245 sens_range = props['android.sensor.info.sensitivityRange'] 246 m = 1.0 247 248 # Do captures with a range of exposures, but constant s*e 249 while s*m < sens_range[1] and e/m > expt_range[0]: 250 mults.append(m) 251 s_req = round(s * m) 252 e_req = s_e_product // s_req 253 logging.debug('Testing s: %d, e: %dns', s_req, e_req) 254 req = capture_request_utils.manual_capture_request( 255 s_req, e_req, 0.0, True, props) 256 cap = its_session_utils.do_capture_with_latency( 257 cam, req, sync_latency, fmt) 258 s_res = cap['metadata']['android.sensor.sensitivity'] 259 e_res = cap['metadata']['android.sensor.exposureTime'] 260 # determine exposure tolerance based on exposure time 261 if e_req >= _THRESH_EXP_KNEE: 262 thresh_round_down_exp = _THRESH_ROUND_DOWN_EXP 263 else: 264 thresh_round_down_exp = ( 265 _THRESH_ROUND_DOWN_EXP + 266 (_THRESH_ROUND_DOWN_EXP0 - _THRESH_ROUND_DOWN_EXP) * 267 (_THRESH_EXP_KNEE - e_req) / _THRESH_EXP_KNEE) 268 if not 0 <= s_req - s_res < s_req * _THRESH_ROUND_DOWN_ISO: 269 raise AssertionError(f's_req: {s_req}, s_res: {s_res}, ' 270 f'RTOL=-{_THRESH_ROUND_DOWN_ISO*100}%') 271 if not 0 <= e_req - e_res < e_req * thresh_round_down_exp: 272 raise AssertionError(f'e_req: {e_req}ns, e_res: {e_res}ns, ' 273 f'RTOL=-{thresh_round_down_exp*100}%') 274 s_e_product_res = s_res * e_res 275 req_res_ratio = s_e_product / s_e_product_res 276 logging.debug('Capture result s: %d, e: %dns', s_res, e_res) 277 img = image_processing_utils.convert_capture_to_rgb_image(cap) 278 image_processing_utils.write_image( 279 img, f'{test_name_with_path}_mult={m:.2f}.jpg') 280 patch = image_processing_utils.get_image_patch( 281 img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 282 rgb_means = image_processing_utils.compute_image_means(patch) 283 284 # Adjust for the difference between request and result 285 r_means.append(rgb_means[0] * req_res_ratio) 286 g_means.append(rgb_means[1] * req_res_ratio) 287 b_means.append(rgb_means[2] * req_res_ratio) 288 289 # Do with RAW_STATS space if debug 290 if raw_avlb and debug: 291 aaw, aah = get_raw_active_array_size(props) 292 fmt_raw = {'format': 'rawStats', 293 'gridWidth': aaw//_RAW_STATS_GRID, 294 'gridHeight': aah//_RAW_STATS_GRID} 295 raw_cap = its_session_utils.do_capture_with_latency( 296 cam, req, sync_latency, fmt_raw) 297 r, gr, gb, b = image_processing_utils.convert_capture_to_planes( 298 raw_cap, props) 299 raw_r_means.append(r[_RAW_STATS_XY, _RAW_STATS_XY] * req_res_ratio) 300 raw_gr_means.append(gr[_RAW_STATS_XY, _RAW_STATS_XY] * req_res_ratio) 301 raw_gb_means.append(gb[_RAW_STATS_XY, _RAW_STATS_XY] * req_res_ratio) 302 raw_b_means.append(b[_RAW_STATS_XY, _RAW_STATS_XY] * req_res_ratio) 303 304 # Test number of points per 2x gain 305 m *= pow(2, 1.0/_NUM_PTS_2X_GAIN) 306 307 # Loosen threshold for devices with wider exposure range 308 if m >= _WIDE_EXP_RANGE_THRESH: 309 thresh_max_level_diff = _THRESH_MAX_LEVEL_DIFF_WIDE_RANGE 310 311 # Draw plots and check data 312 if raw_avlb and debug: 313 plot_raw_means('RAW data', mults, raw_r_means, raw_gr_means, raw_gb_means, 314 raw_b_means, test_name_with_path) 315 for ch, color in enumerate(['R', 'Gr', 'Gb', 'B']): 316 values = [raw_r_means, raw_gr_means, raw_gb_means, raw_b_means][ch] 317 check_line_fit(color, mults, values, thresh_max_level_diff) 318 319 plot_rgb_means(f'RGB (1x: iso={s}, exp={e})', mults, 320 r_means, g_means, b_means, test_name_with_path) 321 for ch, color in enumerate(['R', 'G', 'B']): 322 values = [r_means, g_means, b_means][ch] 323 check_line_fit(color, mults, values, thresh_max_level_diff) 324 325if __name__ == '__main__': 326 test_runner.main() 327