• 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        if len(noise_profile) != len(BAYER_LIST):
107          raise AssertionError(
108              f'noise_profile wrong length! {len(noise_profile)}')
109        for i, ch in enumerate(BAYER_LIST):
110          # Get the noise model parameters for this channel of this shot.
111          s, o = noise_profile[cfa_idxs[i]]
112
113          # Use a very small patch to ensure gross uniformity (i.e. so
114          # non-uniform lighting or vignetting doesn't affect the variance
115          # calculation)
116          black_level = image_processing_utils.get_black_level(
117              i, props, cap['metadata'])
118          level_range = white_level - black_level
119          plane = image_processing_utils.get_image_patch(
120              planes[i], PATCH_X, PATCH_Y, PATCH_W, PATCH_H)
121          patch_raw = plane * white_level
122          patch_norm = ((patch_raw - black_level) / level_range)
123
124          # exit if distribution is clipped at 0, otherwise continue
125          mean_img_ch = patch_norm.mean()
126          var_model = s * mean_img_ch + o
127          # This computation is suspicious because if the data were clipped,
128          # the mean and standard deviation could be affected in a way that
129          # affects this check. However, empirically, the mean and standard
130          # deviation change more slowly than the clipping point itself does,
131          # so the check remains correct even after the signal starts to clip.
132          mean_minus_3sigma = mean_img_ch - math.sqrt(var_model) * 3
133          if mean_minus_3sigma < 0:
134            if mean_minus_3sigma >= 0:
135              raise AssertionError(
136                  'Pixel distribution crosses 0. Likely black level over-clips.'
137                  f' Linear model is not valid. mean: {mean_img_ch:.3e},'
138                  f' var: {var_model:.3e}, u-3s: {mean_minus_3sigma:.3e}')
139          else:
140            var = image_processing_utils.compute_image_variances(patch_norm)[0]
141            var_meas[i].append(var)
142            var_exp[i].append(var_model)
143            abs_diff = abs(var - var_model)
144            logging.debug('%s mean: %.3f, var: %.3e, var_model: %.3e',
145                          ch, mean_img_ch, var, var_model)
146            if var_model:
147              rel_diff = abs_diff / var_model
148            else:
149              raise AssertionError(f'{ch} model variance = 0!')
150            logging.debug('abs_diff: %.5f, rel_diff: %.3f', abs_diff, rel_diff)
151        sens_valid.append(sens)
152
153    # plot data and models
154    pylab.figure(NAME)
155    for i, ch in enumerate(BAYER_LIST):
156      pylab.plot(sens_valid, var_exp[i], 'rgkb'[i], label=ch+' expected')
157      pylab.plot(sens_valid, var_meas[i], 'rgkb'[i]+'.--', label=ch+' measured')
158    pylab.title(NAME)
159    pylab.xlabel('Sensitivity')
160    pylab.ylabel('Center patch variance')
161    pylab.ticklabel_format(axis='y', style='sci', scilimits=(-6, -6))
162    pylab.legend(loc=2)
163    matplotlib.pyplot.savefig('%s_plot.png' % os.path.join(log_path, NAME))
164
165    # PASS/FAIL check
166    for i, ch in enumerate(BAYER_LIST):
167      var_diffs = [abs(var_meas[i][j] - var_exp[i][j])
168                   for j in range(len(sens_valid))]
169      logging.debug('%s variance diffs: %s', ch, str(var_diffs))
170      for j, diff in enumerate(var_diffs):
171        thresh = max(VAR_ATOL_THRESH, VAR_RTOL_THRESH*var_exp[i][j])
172        if diff > thresh:
173          raise AssertionError(f'var diff: {diff:.5f}, thresh: {thresh:.4f}')
174
175if __name__ == '__main__':
176  test_runner.main()
177