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 15import math 16import os.path 17import its.caps 18import its.device 19import its.image 20import its.objects 21import matplotlib 22from matplotlib import pylab 23 24NAME = os.path.basename(__file__).split('.')[0] 25BAYER_LIST = ['R', 'GR', 'GB', 'B'] 26DIFF_THRESH = 0.0012 # absolute variance delta threshold 27FRAC_THRESH = 0.2 # relative variance delta threshold 28NUM_STEPS = 4 29SENS_TOL = 0.97 # specification is <= 3% 30 31 32def main(): 33 """Verify that the DNG raw model parameters are correct.""" 34 35 # Pass if the difference between expected and computed variances is small, 36 # defined as being within an absolute variance delta or relative variance 37 # delta of the expected variance, whichever is larger. This is to allow the 38 # test to pass in the presence of some randomness (since this test is 39 # measuring noise of a small patch) and some imperfect scene conditions 40 # (since ITS doesn't require a perfectly uniformly lit scene). 41 42 with its.device.ItsSession() as cam: 43 props = cam.get_camera_properties() 44 props = cam.override_with_hidden_physical_camera_props(props) 45 its.caps.skip_unless( 46 its.caps.raw(props) and 47 its.caps.raw16(props) and 48 its.caps.manual_sensor(props) and 49 its.caps.read_3a(props) and 50 its.caps.per_frame_control(props) and 51 not its.caps.mono_camera(props)) 52 53 white_level = float(props['android.sensor.info.whiteLevel']) 54 cfa_idxs = its.image.get_canonical_cfa_order(props) 55 56 # Expose for the scene with min sensitivity 57 sens_min, _ = props['android.sensor.info.sensitivityRange'] 58 sens_max_ana = props['android.sensor.maxAnalogSensitivity'] 59 sens_step = (sens_max_ana - sens_min) / NUM_STEPS 60 s_ae, e_ae, _, _, _ = cam.do_3a(get_results=True, do_af=False) 61 s_e_prod = s_ae * e_ae 62 # Focus at zero to intentionally blur the scene as much as possible. 63 f_dist = 0.0 64 sensitivities = range(sens_min, sens_max_ana+1, sens_step) 65 66 var_expected = [[], [], [], []] 67 var_measured = [[], [], [], []] 68 sens_valid = [] 69 for sens in sensitivities: 70 # Capture a raw frame with the desired sensitivity 71 exp = int(s_e_prod / float(sens)) 72 req = its.objects.manual_capture_request(sens, exp, f_dist) 73 cap = cam.do_capture(req, cam.CAP_RAW) 74 planes = its.image.convert_capture_to_planes(cap, props) 75 s_read = cap['metadata']['android.sensor.sensitivity'] 76 print 'iso_write: %d, iso_read: %d' % (sens, s_read) 77 78 # Test each raw color channel (R, GR, GB, B) 79 noise_profile = cap['metadata']['android.sensor.noiseProfile'] 80 assert len(noise_profile) == len(BAYER_LIST) 81 for i in range(len(BAYER_LIST)): 82 print BAYER_LIST[i], 83 # Get the noise model parameters for this channel of this shot. 84 ch = cfa_idxs[i] 85 s, o = noise_profile[ch] 86 87 # Use a very small patch to ensure gross uniformity (i.e. so 88 # non-uniform lighting or vignetting doesn't affect the variance 89 # calculation) 90 black_level = its.image.get_black_level(i, props, 91 cap['metadata']) 92 level_range = white_level - black_level 93 plane = its.image.get_image_patch(planes[i], 0.49, 0.49, 94 0.02, 0.02) 95 tile_raw = plane * white_level 96 tile_norm = ((tile_raw - black_level) / level_range) 97 98 # exit if distribution is clipped at 0, otherwise continue 99 mean_img_ch = tile_norm.mean() 100 var_model = s * mean_img_ch + o 101 # This computation is a suspicious because if the data were 102 # clipped, the mean and standard deviation could be affected 103 # in a way that affects this check. However, empirically, 104 # the mean and standard deviation change more slowly than the 105 # clipping point itself does, so the check remains correct 106 # even after the signal starts to clip. 107 mean_minus_3sigma = mean_img_ch - math.sqrt(var_model) * 3 108 if mean_minus_3sigma < 0: 109 e_msg = '\nPixel distribution crosses 0.\n' 110 e_msg += 'Likely black level over-clips.\n' 111 e_msg += 'Linear model is not valid.\n' 112 e_msg += 'mean: %.3e, var: %.3e, u-3s: %.3e' % ( 113 mean_img_ch, var_model, mean_minus_3sigma) 114 assert 0, e_msg 115 else: 116 print 'mean:', mean_img_ch, 117 var_measured[i].append( 118 its.image.compute_image_variances(tile_norm)[0]) 119 print 'var:', var_measured[i][-1], 120 var_expected[i].append(var_model) 121 print 'var_model:', var_expected[i][-1] 122 print '' 123 sens_valid.append(sens) 124 125 # plot data and models 126 for i, ch in enumerate(BAYER_LIST): 127 pylab.plot(sens_valid, var_expected[i], 'rgkb'[i], 128 label=ch+' expected') 129 pylab.plot(sens_valid, var_measured[i], 'rgkb'[i]+'.--', 130 label=ch+' measured') 131 pylab.xlabel('Sensitivity') 132 pylab.ylabel('Center patch variance') 133 pylab.legend(loc=2) 134 matplotlib.pyplot.savefig('%s_plot.png' % NAME) 135 136 # PASS/FAIL check 137 for i, ch in enumerate(BAYER_LIST): 138 diffs = [abs(var_measured[i][j] - var_expected[i][j]) 139 for j in range(len(sens_valid))] 140 print 'Diffs (%s):'%(ch), diffs 141 for j, diff in enumerate(diffs): 142 thresh = max(DIFF_THRESH, FRAC_THRESH*var_expected[i][j]) 143 assert diff <= thresh, 'diff: %.5f, thresh: %.4f' % (diff, thresh) 144 145if __name__ == '__main__': 146 main() 147