• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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