1# Copyright 2019 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 15import os.path 16 17import its.caps 18import its.device 19import its.image 20import its.objects 21 22import matplotlib 23from matplotlib import pylab 24import numpy as np 25 26AE_STATE_CONVERGED = 2 27AE_STATE_FLASH_REQUIRED = 4 28DELTA_GAIN_THRESH = 0.03 # >3% gain change --> luma change in same dir 29DELTA_LUMA_THRESH = 0.03 # 3% frame-to-frame noise test_burst_sameness_manual 30DELTA_NO_GAIN_THRESH = 0.01 # <1% gain change --> min luma change 31LSC_TOL = 0.005 # allow <0.5% change in lens shading correction 32NAME = os.path.basename(__file__).split('.')[0] 33NUM_CAPS = 1 34NUM_FRAMES = 30 35VALID_STABLE_LUMA_MIN = 0.1 36VALID_STABLE_LUMA_MAX = 0.9 37 38 39def lsc_unchanged(lsc_avlb, lsc, idx): 40 """Determine if lens shading correction unchanged. 41 42 Args: 43 lsc_avlb: bool; True if lens shading correction available 44 lsc: list; lens shading correction matrix 45 idx: int; frame index 46 Returns: 47 boolean 48 """ 49 if lsc_avlb: 50 diff = list((np.array(lsc[idx]) - np.array(lsc[idx-1])) / 51 np.array(lsc[idx-1])) 52 diff = map(abs, diff) 53 max_abs_diff = max(diff) 54 if max_abs_diff > LSC_TOL: 55 print ' max abs(LSC) change:', round(max_abs_diff, 4) 56 return False 57 else: 58 return True 59 else: 60 return True 61 62 63def tonemap_unchanged(raw_cap, tonemap_g, idx): 64 """Determine if tonemap unchanged. 65 66 Args: 67 raw_cap: bool; True if RAW capture 68 tonemap_g: list; green tonemap 69 idx: int; frame index 70 Returns: 71 boolean 72 """ 73 if not raw_cap: 74 return tonemap_g[idx-1] == tonemap_g[idx] 75 else: 76 return True 77 78 79def is_awb_af_stable(cap_info, i): 80 awb_gains_0 = cap_info[i-1]['awb_gains'] 81 awb_gains_1 = cap_info[i]['awb_gains'] 82 ccm_0 = cap_info[i-1]['ccm'] 83 ccm_1 = cap_info[i]['ccm'] 84 fd_0 = cap_info[i-1]['fd'] 85 fd_1 = cap_info[i]['fd'] 86 87 return (np.allclose(awb_gains_0, awb_gains_1, rtol=0.01) and 88 ccm_0 == ccm_1 and np.isclose(fd_0, fd_1, rtol=0.01)) 89 90 91def main(): 92 """Tests PER_FRAME_CONTROL properties for auto capture requests. 93 94 If debug is required, MANUAL_POSTPROCESSING capability is implied 95 since its.caps.read_3a is valid for test. Debug can performed with 96 a defined tonemap curve: 97 req['android.tonemap.mode'] = 0 98 gamma = sum([[i/63.0,math.pow(i/63.0,1/2.2)] for i in xrange(64)],[]) 99 req['android.tonemap.curve'] = { 100 'red': gamma, 'green': gamma, 'blue': gamma} 101 """ 102 103 with its.device.ItsSession() as cam: 104 props = cam.get_camera_properties() 105 its.caps.skip_unless(its.caps.per_frame_control(props) and 106 its.caps.read_3a(props)) 107 debug = its.caps.debug_mode() 108 raw_avlb = its.caps.raw16(props) 109 largest_yuv = its.objects.get_largest_yuv_format(props) 110 match_ar = (largest_yuv['width'], largest_yuv['height']) 111 fmts = [its.objects.get_smallest_yuv_format(props, match_ar=match_ar)] 112 if raw_avlb and debug: 113 fmts.insert(0, cam.CAP_RAW) 114 115 failed = [] 116 for f, fmt in enumerate(fmts): 117 print 'fmt:', fmt['format'] 118 cam.do_3a() 119 req = its.objects.auto_capture_request() 120 cap_info = {} 121 ae_states = [] 122 lumas = [] 123 total_gains = [] 124 tonemap_g = [] 125 lsc = [] 126 num_caps = NUM_CAPS 127 num_frames = NUM_FRAMES 128 raw_cap = f == 0 and raw_avlb and debug 129 lsc_avlb = its.caps.lsc_map(props) and not raw_cap 130 print 'lens shading correction available:', lsc_avlb 131 if lsc_avlb: 132 req['android.statistics.lensShadingMapMode'] = 1 133 name_suffix = 'YUV' 134 if raw_cap: 135 name_suffix = 'RAW' 136 # break up caps if RAW to reduce load 137 num_caps = NUM_CAPS * 6 138 num_frames = NUM_FRAMES / 6 139 for j in range(num_caps): 140 caps = cam.do_capture([req]*num_frames, fmt) 141 for i, cap in enumerate(caps): 142 frame = {} 143 idx = i + j * num_frames 144 print '=========== frame %d ==========' % idx 145 # RAW --> GR, YUV --> Y plane 146 if raw_cap: 147 plane = its.image.convert_capture_to_planes( 148 cap, props=props)[1] 149 else: 150 plane = its.image.convert_capture_to_planes(cap)[0] 151 tile = its.image.get_image_patch( 152 plane, 0.45, 0.45, 0.1, 0.1) 153 luma = its.image.compute_image_means(tile)[0] 154 ae_state = cap['metadata']['android.control.aeState'] 155 iso = cap['metadata']['android.sensor.sensitivity'] 156 isp_gain = cap['metadata']['android.control.postRawSensitivityBoost'] 157 exp_time = cap['metadata']['android.sensor.exposureTime'] 158 total_gain = iso * isp_gain / 100.0 * exp_time / 1000000.0 159 if raw_cap: 160 total_gain = iso * exp_time / 1000000.0 161 awb_state = cap['metadata']['android.control.awbState'] 162 frame['awb_gains'] = cap['metadata']['android.colorCorrection.gains'] 163 frame['ccm'] = cap['metadata']['android.colorCorrection.transform'] 164 frame['fd'] = cap['metadata']['android.lens.focusDistance'] 165 166 # Convert CCM from rational to float, as numpy arrays. 167 awb_ccm = np.array(its.objects.rational_to_float(frame['ccm'])).reshape(3, 3) 168 169 print 'AE: %d ISO: %d ISP_sen: %d exp(ns): %d tot_gain: %f' % ( 170 ae_state, iso, isp_gain, exp_time, total_gain), 171 print 'luma: %f' % luma 172 print 'fd: %f' % frame['fd'] 173 print 'AWB state: %d, AWB gains: %s\n AWB matrix: %s' % ( 174 awb_state, str(frame['awb_gains']), 175 str(awb_ccm)) 176 if not raw_cap: 177 tonemap = cap['metadata']['android.tonemap.curve'] 178 tonemap_g.append(tonemap['green']) 179 print 'G tonemap curve:', tonemap_g[idx] 180 if lsc_avlb: 181 lsc.append(cap['metadata']['android.statistics.lensShadingCorrectionMap']['map']) 182 183 img = its.image.convert_capture_to_rgb_image( 184 cap, props=props) 185 its.image.write_image(img, '%s_frame_%s_%d.jpg' % ( 186 NAME, name_suffix, idx)) 187 cap_info[idx] = frame 188 ae_states.append(ae_state) 189 lumas.append(luma) 190 total_gains.append(total_gain) 191 192 norm_gains = [x/max(total_gains)*max(lumas) for x in total_gains] 193 pylab.figure(name_suffix) 194 pylab.plot(range(len(lumas)), lumas, '-g.', 195 label='Center patch brightness') 196 pylab.plot(range(len(norm_gains)), norm_gains, '-r.', 197 label='Metadata AE setting product') 198 pylab.title(NAME + ' ' + name_suffix) 199 pylab.xlabel('frame index') 200 201 # expand y axis for low delta results 202 ymin = min(norm_gains + lumas) 203 ymax = max(norm_gains + lumas) 204 yavg = (ymax + ymin)/2.0 205 if ymax-ymin < 3*DELTA_LUMA_THRESH: 206 ymin = round(yavg - 1.5*DELTA_LUMA_THRESH, 3) 207 ymax = round(yavg + 1.5*DELTA_LUMA_THRESH, 3) 208 pylab.ylim(ymin, ymax) 209 pylab.legend() 210 matplotlib.pyplot.savefig('%s_plot_%s.png' % (NAME, name_suffix)) 211 212 print '\nfmt:', fmt['format'] 213 for i in range(1, num_caps*num_frames): 214 if is_awb_af_stable(cap_info, i): 215 prev_total_gain = total_gains[i-1] 216 total_gain = total_gains[i] 217 delta_gain = total_gain - prev_total_gain 218 prev_luma = lumas[i-1] 219 luma = lumas[i] 220 delta_luma = luma - prev_luma 221 delta_gain_rel = delta_gain / prev_total_gain 222 delta_luma_rel = delta_luma / prev_luma 223 # luma and total_gain should change in same direction 224 msg = '%s: frame %d: gain %.1f -> %.1f (%.1f%%), ' % ( 225 fmt['format'], i, prev_total_gain, total_gain, 226 delta_gain_rel*100) 227 msg += 'luma %f -> %f (%.2f%%) GAIN/LUMA OPPOSITE DIR' % ( 228 prev_luma, luma, delta_luma_rel*100) 229 # Threshold change to trigger check. Small delta_gain might 230 # not be enough to generate a reliable delta_luma to 231 # overcome frame-to-frame variation. 232 if (tonemap_unchanged(raw_cap, tonemap_g, i) and 233 lsc_unchanged(lsc_avlb, lsc, i)): 234 if abs(delta_gain_rel) > DELTA_GAIN_THRESH: 235 print ' frame %d: %.2f%% delta gain,' % ( 236 i, delta_gain_rel*100), 237 print '%.2f%% delta luma' % (delta_luma_rel*100) 238 if delta_gain * delta_luma < 0.0: 239 failed.append(msg) 240 elif abs(delta_gain_rel) < DELTA_NO_GAIN_THRESH: 241 print ' frame %d: <|%.1f%%| delta gain,' % ( 242 i, DELTA_NO_GAIN_THRESH*100), 243 print '%.2f%% delta luma' % (delta_luma_rel*100) 244 msg = '%s: ' % fmt['format'] 245 msg += 'frame %d: gain %.1f -> %.1f (%.1f%%), ' % ( 246 i, prev_total_gain, total_gain, 247 delta_gain_rel*100) 248 msg += 'luma %f -> %f (%.1f%%) ' % ( 249 prev_luma, luma, delta_luma_rel*100) 250 msg += '<|%.1f%%| GAIN, >|%.f%%| LUMA DELTA' % ( 251 DELTA_NO_GAIN_THRESH*100, DELTA_LUMA_THRESH*100) 252 if abs(delta_luma_rel) > DELTA_LUMA_THRESH: 253 failed.append(msg) 254 else: 255 print ' frame %d: %.1f%% delta gain,' % ( 256 i, delta_gain_rel*100), 257 print '%.2f%% delta luma' % (delta_luma_rel*100) 258 else: 259 print ' frame %d -> %d: tonemap' % (i-1, i), 260 print 'or lens shading correction changed' 261 else: 262 print ' frame %d -> %d: AWB/AF changed' % (i-1, i) 263 264 for i in range(len(lumas)): 265 luma = lumas[i] 266 ae_state = ae_states[i] 267 if (ae_state == AE_STATE_CONVERGED or 268 ae_state == AE_STATE_FLASH_REQUIRED): 269 msg = '%s: frame %d AE converged ' % (fmt['format'], i) 270 msg += 'luma %f. valid range: (%f, %f)' % ( 271 luma, VALID_STABLE_LUMA_MIN, VALID_STABLE_LUMA_MAX) 272 if VALID_STABLE_LUMA_MIN > luma > VALID_STABLE_LUMA_MAX: 273 failed.append(msg) 274 if failed: 275 print '\nError summary' 276 for fail in failed: 277 print fail 278 assert not failed 279 280if __name__ == '__main__': 281 main() 282