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"""Verifies the DNG RAW model parameters are correct.""" 15 16 17import logging 18import math 19import os.path 20import matplotlib 21from matplotlib import pylab 22from mobly import test_runner 23 24import its_base_test 25import camera_properties_utils 26import capture_request_utils 27import image_processing_utils 28import its_session_utils 29 30 31_BAYER_COLORS = ('R', 'GR', 'GB', 'B') 32_NAME = os.path.splitext(os.path.basename(__file__))[0] 33_NUM_STEPS = 4 34_PATCH_H = 0.02 # center 2% 35_PATCH_W = 0.02 36_PATCH_X = 0.5 - _PATCH_W/2 37_PATCH_Y = 0.5 - _PATCH_H/2 38_VAR_ATOL_THRESH = 0.0012 # absolute variance delta threshold 39_VAR_RTOL_THRESH = 0.2 # relative variance delta threshold 40 41 42class DngNoiseModelTest(its_base_test.ItsBaseTest): 43 """Verify that the DNG raw model parameters are correct. 44 45 Pass if the difference between expected and computed variances is small, 46 defined as being within an absolute variance delta or relative variance 47 delta of the expected variance, whichever is larger. This is to allow the 48 test to pass in the presence of some randomness (since this test is 49 measuring noise of a small patch) and some imperfect scene conditions 50 (since ITS doesn't require a perfectly uniformly lit scene). 51 """ 52 53 def test_dng_noise_model(self): 54 logging.debug('Starting %s', _NAME) 55 with its_session_utils.ItsSession( 56 device_id=self.dut.serial, 57 camera_id=self.camera_id, 58 hidden_physical_id=self.hidden_physical_id) as cam: 59 props = cam.get_camera_properties() 60 props = cam.override_with_hidden_physical_camera_props(props) 61 name_with_log_path = os.path.join(self.log_path, _NAME) 62 63 # check SKIP conditions 64 camera_properties_utils.skip_unless( 65 camera_properties_utils.raw(props) and 66 camera_properties_utils.raw16(props) and 67 camera_properties_utils.manual_sensor(props) and 68 camera_properties_utils.per_frame_control(props) and 69 not camera_properties_utils.mono_camera(props)) 70 71 # Load chart for scene 72 its_session_utils.load_scene( 73 cam, props, self.scene, self.tablet, 74 its_session_utils.CHART_DISTANCE_NO_SCALING) 75 76 # Expose for the scene with min sensitivity 77 white_level = float(props['android.sensor.info.whiteLevel']) 78 cfa_idxs = image_processing_utils.get_canonical_cfa_order(props) 79 sens_min, _ = props['android.sensor.info.sensitivityRange'] 80 sens_max_ana = props['android.sensor.maxAnalogSensitivity'] 81 sens_step = (sens_max_ana - sens_min) // _NUM_STEPS 82 s_ae, e_ae, _, _, _ = cam.do_3a(get_results=True, do_af=False) 83 # Focus at zero to intentionally blur the scene as much as possible. 84 f_dist = 0.0 85 s_e_prod = s_ae * e_ae 86 sensitivities = range(sens_min, sens_max_ana+1, sens_step) 87 88 var_exp = [[], [], [], []] 89 var_meas = [[], [], [], []] 90 sens_valid = [] 91 for sens in sensitivities: 92 # Capture a raw frame with the desired sensitivity 93 exp = int(s_e_prod / float(sens)) 94 req = capture_request_utils.manual_capture_request(sens, exp, f_dist) 95 cap = cam.do_capture(req, cam.CAP_RAW) 96 planes = image_processing_utils.convert_capture_to_planes(cap, props) 97 s_read = cap['metadata']['android.sensor.sensitivity'] 98 logging.debug('iso_write: %d, iso_read: %d', sens, s_read) 99 if self.debug_mode: 100 img = image_processing_utils.convert_capture_to_rgb_image( 101 cap, props=props) 102 image_processing_utils.write_image( 103 img, f'{name_with_log_path}_{sens}.jpg') 104 105 # Test each raw color channel (R, GR, GB, B) 106 noise_profile = cap['metadata']['android.sensor.noiseProfile'] 107 if len(noise_profile) != len(_BAYER_COLORS): 108 raise AssertionError( 109 f'noise_profile wrong length! {len(noise_profile)}') 110 for i, ch in enumerate(_BAYER_COLORS): 111 # Get the noise model parameters for this channel of this shot. 112 s, o = noise_profile[cfa_idxs[i]] 113 114 # Use a very small patch to ensure gross uniformity (i.e. so 115 # non-uniform lighting or vignetting doesn't affect the variance 116 # calculation) 117 black_level = image_processing_utils.get_black_level( 118 i, props, cap['metadata']) 119 level_range = white_level - black_level 120 plane = image_processing_utils.get_image_patch( 121 planes[i], _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H) 122 patch_raw = plane * white_level 123 patch_norm = ((patch_raw - black_level) / level_range) 124 125 # exit if distribution is clipped at 0, otherwise continue 126 mean_img_ch = patch_norm.mean() 127 var_model = s * mean_img_ch + o 128 # This computation is suspicious because if the data were clipped, 129 # the mean and standard deviation could be affected in a way that 130 # affects this check. However, empirically, the mean and standard 131 # deviation change more slowly than the clipping point itself does, 132 # so the check remains correct even after the signal starts to clip. 133 mean_minus_3sigma = mean_img_ch - math.sqrt(var_model) * 3 134 if mean_minus_3sigma < 0: 135 if mean_minus_3sigma >= 0: 136 raise AssertionError( 137 'Pixel distribution crosses 0. Likely black level over-clips.' 138 f' Linear model is not valid. mean: {mean_img_ch:.3e},' 139 f' var: {var_model:.3e}, u-3s: {mean_minus_3sigma:.3e}') 140 else: 141 var = image_processing_utils.compute_image_variances(patch_norm)[0] 142 var_meas[i].append(var) 143 var_exp[i].append(var_model) 144 abs_diff = abs(var - var_model) 145 logging.debug('%s mean: %.3f, var: %.3e, var_model: %.3e', 146 ch, mean_img_ch, var, var_model) 147 if var_model: 148 rel_diff = abs_diff / var_model 149 else: 150 raise AssertionError(f'{ch} model variance = 0!') 151 logging.debug('abs_diff: %.5f, rel_diff: %.3f', abs_diff, rel_diff) 152 sens_valid.append(sens) 153 154 # plot data and models 155 pylab.figure(_NAME) 156 for i, ch in enumerate(_BAYER_COLORS): 157 pylab.plot(sens_valid, var_exp[i], 'rgkb'[i], label=ch+' expected') 158 pylab.plot(sens_valid, var_meas[i], 'rgkb'[i]+'.--', label=ch+' measured') 159 pylab.title(_NAME) 160 pylab.xlabel('Sensitivity') 161 pylab.ylabel('Center patch variance') 162 pylab.ticklabel_format(axis='y', style='sci', scilimits=(-6, -6)) 163 pylab.legend(loc=2) 164 matplotlib.pyplot.savefig(f'{name_with_log_path}_plot.png') 165 166 # PASS/FAIL check 167 for i, ch in enumerate(_BAYER_COLORS): 168 var_diffs = [abs(var_meas[i][j] - var_exp[i][j]) 169 for j in range(len(sens_valid))] 170 logging.debug('%s variance diffs: %s', ch, str(var_diffs)) 171 for j, diff in enumerate(var_diffs): 172 thresh = max(_VAR_ATOL_THRESH, _VAR_RTOL_THRESH*var_exp[i][j]) 173 if diff > thresh: 174 raise AssertionError(f'var diff: {diff:.5f}, thresh: {thresh:.4f}') 175 176if __name__ == '__main__': 177 test_runner.main() 178