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 15 import os.path 16 17 import its.caps 18 import its.device 19 import its.image 20 import its.objects 21 import its.target 22 import matplotlib 23 from matplotlib import pylab 24 import numpy 25 26 IMG_STATS_GRID = 9 # find used to find the center 11.11% 27 NAME = os.path.basename(__file__).split('.')[0] 28 THRESHOLD_MAX_OUTLIER_DIFF = 0.1 29 THRESHOLD_MIN_LEVEL = 0.1 30 THRESHOLD_MAX_LEVEL = 0.9 31 THRESHOLD_MAX_LEVEL_DIFF = 0.045 32 THRESHOLD_MAX_LEVEL_DIFF_WIDE_RANGE = 0.06 33 THRESH_ROUND_DOWN_GAIN = 0.1 34 THRESH_ROUND_DOWN_EXP = 0.03 35 THRESH_ROUND_DOWN_EXP0 = 1.00 # tol at 0ms exp; theoretical limit @ 4-line exp 36 THRESH_EXP_KNEE = 6E6 # exposures less than knee have relaxed tol 37 38 39 def get_raw_active_array_size(props): 40 """Return the active array w, h from props.""" 41 aaw = (props['android.sensor.info.preCorrectionActiveArraySize']['right'] - 42 props['android.sensor.info.preCorrectionActiveArraySize']['left']) 43 aah = (props['android.sensor.info.preCorrectionActiveArraySize']['bottom'] - 44 props['android.sensor.info.preCorrectionActiveArraySize']['top']) 45 return aaw, aah 46 47 48 def main(): 49 """Test that a constant exposure is seen as ISO and exposure time vary. 50 51 Take a series of shots that have ISO and exposure time chosen to balance 52 each other; result should be the same brightness, but over the sequence 53 the images should get noisier. 54 """ 55 mults = [] 56 r_means = [] 57 g_means = [] 58 b_means = [] 59 raw_r_means = [] 60 raw_gr_means = [] 61 raw_gb_means = [] 62 raw_b_means = [] 63 threshold_max_level_diff = THRESHOLD_MAX_LEVEL_DIFF 64 65 with its.device.ItsSession() as cam: 66 props = cam.get_camera_properties() 67 props = cam.override_with_hidden_physical_camera_props(props) 68 its.caps.skip_unless(its.caps.compute_target_exposure(props)) 69 sync_latency = its.caps.sync_latency(props) 70 process_raw = its.caps.raw16(props) and its.caps.manual_sensor(props) 71 debug = its.caps.debug_mode() 72 largest_yuv = its.objects.get_largest_yuv_format(props) 73 if debug: 74 fmt = largest_yuv 75 else: 76 match_ar = (largest_yuv['width'], largest_yuv['height']) 77 fmt = its.objects.get_smallest_yuv_format(props, match_ar=match_ar) 78 79 e, s = its.target.get_target_exposure_combos(cam)['minSensitivity'] 80 s_e_product = s*e 81 expt_range = props['android.sensor.info.exposureTimeRange'] 82 sens_range = props['android.sensor.info.sensitivityRange'] 83 84 m = 1.0 85 while s*m < sens_range[1] and e/m > expt_range[0]: 86 mults.append(m) 87 s_test = round(s*m) 88 e_test = s_e_product / s_test 89 print 'Testing s:', s_test, 'e:', e_test 90 req = its.objects.manual_capture_request( 91 s_test, e_test, 0.0, True, props) 92 cap = its.device.do_capture_with_latency( 93 cam, req, sync_latency, fmt) 94 s_res = cap['metadata']['android.sensor.sensitivity'] 95 e_res = cap['metadata']['android.sensor.exposureTime'] 96 # determine exposure tolerance based on exposure time 97 if e_test >= THRESH_EXP_KNEE: 98 thresh_round_down_exp = THRESH_ROUND_DOWN_EXP 99 else: 100 thresh_round_down_exp = ( 101 THRESH_ROUND_DOWN_EXP + 102 (THRESH_ROUND_DOWN_EXP0 - THRESH_ROUND_DOWN_EXP) * 103 (THRESH_EXP_KNEE - e_test) / THRESH_EXP_KNEE) 104 s_msg = 's_write: %d, s_read: %d, TOL=%.f%%' % ( 105 s_test, s_res, THRESH_ROUND_DOWN_GAIN*100) 106 e_msg = 'e_write: %.3fms, e_read: %.3fms, TOL=%.f%%' % ( 107 e_test/1.0E6, e_res/1.0E6, thresh_round_down_exp*100) 108 assert 0 <= s_test - s_res < s_test * THRESH_ROUND_DOWN_GAIN, s_msg 109 assert 0 <= e_test - e_res < e_test * thresh_round_down_exp, e_msg 110 s_e_product_res = s_res * e_res 111 request_result_ratio = s_e_product / s_e_product_res 112 print 'Capture result s:', s_res, 'e:', e_res 113 img = its.image.convert_capture_to_rgb_image(cap) 114 its.image.write_image(img, '%s_mult=%3.2f.jpg' % (NAME, m)) 115 tile = its.image.get_image_patch(img, 0.45, 0.45, 0.1, 0.1) 116 rgb_means = its.image.compute_image_means(tile) 117 # Adjust for the difference between request and result 118 r_means.append(rgb_means[0] * request_result_ratio) 119 g_means.append(rgb_means[1] * request_result_ratio) 120 b_means.append(rgb_means[2] * request_result_ratio) 121 # do same in RAW space if possible 122 if process_raw and debug: 123 aaw, aah = get_raw_active_array_size(props) 124 fmt_raw = {'format': 'rawStats', 125 'gridWidth': aaw/IMG_STATS_GRID, 126 'gridHeight': aah/IMG_STATS_GRID} 127 raw_cap = its.device.do_capture_with_latency( 128 cam, req, sync_latency, fmt_raw) 129 r, gr, gb, b = its.image.convert_capture_to_planes( 130 raw_cap, props) 131 raw_r_means.append(r[IMG_STATS_GRID/2, IMG_STATS_GRID/2] 132 * request_result_ratio) 133 raw_gr_means.append(gr[IMG_STATS_GRID/2, IMG_STATS_GRID/2] 134 * request_result_ratio) 135 raw_gb_means.append(gb[IMG_STATS_GRID/2, IMG_STATS_GRID/2] 136 * request_result_ratio) 137 raw_b_means.append(b[IMG_STATS_GRID/2, IMG_STATS_GRID/2] 138 * request_result_ratio) 139 # Test 3 steps per 2x gain 140 m *= pow(2, 1.0 / 3) 141 142 # Allow more threshold for devices with wider exposure range 143 if m >= 64.0: 144 threshold_max_level_diff = THRESHOLD_MAX_LEVEL_DIFF_WIDE_RANGE 145 146 # Draw plots 147 pylab.figure('rgb data') 148 pylab.plot(mults, r_means, 'ro-') 149 pylab.plot(mults, g_means, 'go-') 150 pylab.plot(mults, b_means, 'bo-') 151 pylab.title(NAME + 'RGB Data') 152 pylab.xlabel('Gain Multiplier') 153 pylab.ylabel('Normalized RGB Plane Avg') 154 pylab.ylim([0, 1]) 155 matplotlib.pyplot.savefig('%s_plot_means.png' % (NAME)) 156 157 if process_raw and debug: 158 pylab.figure('raw data') 159 pylab.plot(mults, raw_r_means, 'ro-', label='R') 160 pylab.plot(mults, raw_gr_means, 'go-', label='GR') 161 pylab.plot(mults, raw_gb_means, 'ko-', label='GB') 162 pylab.plot(mults, raw_b_means, 'bo-', label='B') 163 pylab.title(NAME + 'RAW Data') 164 pylab.xlabel('Gain Multiplier') 165 pylab.ylabel('Normalized RAW Plane Avg') 166 pylab.ylim([0, 1]) 167 pylab.legend(numpoints=1) 168 matplotlib.pyplot.savefig('%s_plot_raw_means.png' % (NAME)) 169 170 # Check for linearity. Verify sample pixel mean values are close to each 171 # other. Also ensure that the images aren't clamped to 0 or 1 172 # (which would make them look like flat lines). 173 for chan in xrange(3): 174 values = [r_means, g_means, b_means][chan] 175 m, b = numpy.polyfit(mults, values, 1).tolist() 176 max_val = max(values) 177 min_val = min(values) 178 max_diff = max_val - min_val 179 print 'Channel %d line fit (y = mx+b): m = %f, b = %f' % (chan, m, b) 180 print 'Channel max %f min %f diff %f' % (max_val, min_val, max_diff) 181 assert max_diff < threshold_max_level_diff 182 assert b > THRESHOLD_MIN_LEVEL and b < THRESHOLD_MAX_LEVEL 183 for v in values: 184 assert v > THRESHOLD_MIN_LEVEL and v < THRESHOLD_MAX_LEVEL 185 assert abs(v - b) < THRESHOLD_MAX_OUTLIER_DIFF 186 if process_raw and debug: 187 for chan in xrange(4): 188 values = [raw_r_means, raw_gr_means, raw_gb_means, 189 raw_b_means][chan] 190 m, b = numpy.polyfit(mults, values, 1).tolist() 191 max_val = max(values) 192 min_val = min(values) 193 max_diff = max_val - min_val 194 print 'Channel %d line fit (y = mx+b): m = %f, b = %f' % (chan, 195 m, b) 196 print 'Channel max %f min %f diff %f' % (max_val, min_val, max_diff) 197 assert max_diff < threshold_max_level_diff 198 assert b > THRESHOLD_MIN_LEVEL and b < THRESHOLD_MAX_LEVEL 199 for v in values: 200 assert v > THRESHOLD_MIN_LEVEL and v < THRESHOLD_MAX_LEVEL 201 assert abs(v - b) < THRESHOLD_MAX_OUTLIER_DIFF 202 203 if __name__ == '__main__': 204 main() 205