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