• 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
31BAYER_LIST = ['R', 'GR', 'GB', 'B']
32NAME = os.path.splitext(os.path.basename(__file__))[0]
33NUM_STEPS = 4
34PATCH_H = 0.02  # center 2%
35PATCH_W = 0.02
36PATCH_X = 0.5 - PATCH_W/2
37PATCH_Y = 0.5 - PATCH_H/2
38VAR_ATOL_THRESH = 0.0012  # absolute variance delta threshold
39VAR_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      log_path = self.log_path
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, self.chart_distance)
74
75      # Expose for the scene with min sensitivity
76      white_level = float(props['android.sensor.info.whiteLevel'])
77      cfa_idxs = image_processing_utils.get_canonical_cfa_order(props)
78      sens_min, _ = props['android.sensor.info.sensitivityRange']
79      sens_max_ana = props['android.sensor.maxAnalogSensitivity']
80      sens_step = (sens_max_ana - sens_min) // NUM_STEPS
81      s_ae, e_ae, _, _, _ = cam.do_3a(get_results=True, do_af=False)
82      # Focus at zero to intentionally blur the scene as much as possible.
83      f_dist = 0.0
84      s_e_prod = s_ae * e_ae
85      sensitivities = range(sens_min, sens_max_ana+1, sens_step)
86
87      var_exp = [[], [], [], []]
88      var_meas = [[], [], [], []]
89      sens_valid = []
90      for sens in sensitivities:
91        # Capture a raw frame with the desired sensitivity
92        exp = int(s_e_prod / float(sens))
93        req = capture_request_utils.manual_capture_request(sens, exp, f_dist)
94        cap = cam.do_capture(req, cam.CAP_RAW)
95        planes = image_processing_utils.convert_capture_to_planes(cap, props)
96        s_read = cap['metadata']['android.sensor.sensitivity']
97        logging.debug('iso_write: %d, iso_read: %d', sens, s_read)
98        if self.debug_mode:
99          img = image_processing_utils.convert_capture_to_rgb_image(
100              cap, props=props)
101          image_processing_utils.write_image(
102              img, '%s_%d.jpg' % (os.path.join(log_path, NAME), sens))
103
104        # Test each raw color channel (R, GR, GB, B)
105        noise_profile = cap['metadata']['android.sensor.noiseProfile']
106        assert len(noise_profile) == len(BAYER_LIST)
107        for i, ch in enumerate(BAYER_LIST):
108          # Get the noise model parameters for this channel of this shot.
109          s, o = noise_profile[cfa_idxs[i]]
110
111          # Use a very small patch to ensure gross uniformity (i.e. so
112          # non-uniform lighting or vignetting doesn't affect the variance
113          # calculation)
114          black_level = image_processing_utils.get_black_level(
115              i, props, cap['metadata'])
116          level_range = white_level - black_level
117          plane = image_processing_utils.get_image_patch(
118              planes[i], PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
119          patch_raw = plane * white_level
120          patch_norm = ((patch_raw - black_level) / level_range)
121
122          # exit if distribution is clipped at 0, otherwise continue
123          mean_img_ch = patch_norm.mean()
124          var_model = s * mean_img_ch + o
125          # This computation is suspicious because if the data were clipped,
126          # the mean and standard deviation could be affected in a way that
127          # affects this check. However, empirically, the mean and standard
128          # deviation change more slowly than the clipping point itself does,
129          # so the check remains correct even after the signal starts to clip.
130          mean_minus_3sigma = mean_img_ch - math.sqrt(var_model) * 3
131          if mean_minus_3sigma < 0:
132            e_msg = 'Pixel distribution crosses 0. Likely black level '
133            e_msg += 'over-clips. Linear model is not valid. '
134            e_msg += 'mean: %.3e, var: %.3e, u-3s: %.3e' % (
135                mean_img_ch, var_model, mean_minus_3sigma)
136            assert mean_minus_3sigma < 0, e_msg
137          else:
138            var = image_processing_utils.compute_image_variances(patch_norm)[0]
139            var_meas[i].append(var)
140            var_exp[i].append(var_model)
141            abs_diff = abs(var - var_model)
142            logging.debug('%s mean: %.3f, var: %.3e, var_model: %.3e',
143                          ch, mean_img_ch, var, var_model)
144            if var_model:
145              rel_diff = abs_diff / var_model
146            else:
147              raise AssertionError(f'{ch} model variance = 0!')
148            logging.debug('abs_diff: %.5f, rel_diff: %.3f', abs_diff, rel_diff)
149        sens_valid.append(sens)
150
151    # plot data and models
152    pylab.figure(NAME)
153    for i, ch in enumerate(BAYER_LIST):
154      pylab.plot(sens_valid, var_exp[i], 'rgkb'[i], label=ch+' expected')
155      pylab.plot(sens_valid, var_meas[i], 'rgkb'[i]+'.--', label=ch+' measured')
156    pylab.title(NAME)
157    pylab.xlabel('Sensitivity')
158    pylab.ylabel('Center patch variance')
159    pylab.ticklabel_format(axis='y', style='sci', scilimits=(-6, -6))
160    pylab.legend(loc=2)
161    matplotlib.pyplot.savefig('%s_plot.png' % os.path.join(log_path, NAME))
162
163    # PASS/FAIL check
164    for i, ch in enumerate(BAYER_LIST):
165      var_diffs = [abs(var_meas[i][j] - var_exp[i][j])
166                   for j in range(len(sens_valid))]
167      logging.debug('%s variance diffs: %s', ch, str(var_diffs))
168      for j, diff in enumerate(var_diffs):
169        thresh = max(VAR_ATOL_THRESH, VAR_RTOL_THRESH*var_exp[i][j])
170        assert diff <= thresh, 'var diff: %.5f, thresh: %.4f' % (diff, thresh)
171
172if __name__ == '__main__':
173  test_runner.main()
174