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 sensitivities on RAW images."""
15
16
17import logging
18import math
19import os.path
20import matplotlib
21from matplotlib import pylab
22from mobly import test_runner
23import numpy as np
24
25import its_base_test
26import camera_properties_utils
27import capture_request_utils
28import image_processing_utils
29import its_session_utils
30
31_BLACK_LEVEL_RTOL = 0.005  # 0.5%
32_GR_PLANE_IDX = 1  # GR plane index in RGGB data
33_IMG_STATS_GRID = 9  # Center 11.11%
34_NAME = os.path.splitext(os.path.basename(__file__))[0]
35_NUM_FRAMES = 4
36_NUM_SENS_STEPS = 5
37_VAR_THRESH = 1.01  # Each shot must be 1% noisier than previous
38
39
40def define_raw_stats_fmt(props):
41  """Define format with active array width and height."""
42  aaw = (props['android.sensor.info.preCorrectionActiveArraySize']['right'] -
43         props['android.sensor.info.preCorrectionActiveArraySize']['left'])
44  aah = (props['android.sensor.info.preCorrectionActiveArraySize']['bottom'] -
45         props['android.sensor.info.preCorrectionActiveArraySize']['top'])
46  logging.debug('Active array W,H: %d,%d', aaw, aah)
47  return {'format': 'rawStats',
48          'gridWidth': aaw // _IMG_STATS_GRID,
49          'gridHeight': aah // _IMG_STATS_GRID}
50
51
52class RawSensitivityTest(its_base_test.ItsBaseTest):
53  """Capture a set of raw images with increasing gains and measure the noise."""
54
55  def test_raw_sensitivity(self):
56    logging.debug('Starting %s', _NAME)
57    with its_session_utils.ItsSession(
58        device_id=self.dut.serial,
59        camera_id=self.camera_id,
60        hidden_physical_id=self.hidden_physical_id) as cam:
61      props = cam.get_camera_properties()
62      props = cam.override_with_hidden_physical_camera_props(props)
63      camera_properties_utils.skip_unless(
64          camera_properties_utils.raw16(props) and
65          camera_properties_utils.manual_sensor(props) and
66          camera_properties_utils.read_3a(props) and
67          camera_properties_utils.per_frame_control(props) and
68          not camera_properties_utils.mono_camera(props))
69      name_with_log_path = os.path.join(self.log_path, _NAME)
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      sens_min, _ = props['android.sensor.info.sensitivityRange']
78      # Digital gains might not be visible on RAW data
79      sens_max = props['android.sensor.maxAnalogSensitivity']
80      sens_step = (sens_max - sens_min) // _NUM_SENS_STEPS
81
82      # Intentionally blur images for noise measurements
83      s_ae, e_ae, _, _, _ = cam.do_3a(do_af=False, get_results=True)
84      s_e_prod = s_ae * e_ae
85
86      # Get property constants for captures
87      cfa_idxs = image_processing_utils.get_canonical_cfa_order(props)
88      black_levels = image_processing_utils.get_black_levels(props)
89      white_level = props['android.sensor.info.whiteLevel']
90
91      sensitivities = list(range(sens_min, sens_max, sens_step))
92      variances = []
93      for s in sensitivities:
94        e = int(s_e_prod / float(s))
95        req = capture_request_utils.manual_capture_request(s, e, 0)
96
97        # Capture in rawStats to reduce test run time
98        fmt = define_raw_stats_fmt(props)
99        caps = cam.do_capture([req]*_NUM_FRAMES, fmt)
100        image_processing_utils.assert_capture_width_and_height(
101            caps[0], _IMG_STATS_GRID, _IMG_STATS_GRID
102        )
103
104        # Measure mean & variance
105        for i, cap in enumerate(caps):
106          mean_img, var_img = image_processing_utils.unpack_rawstats_capture(
107              cap
108          )
109          mean = mean_img[_IMG_STATS_GRID//2, _IMG_STATS_GRID//2,
110                          cfa_idxs[_GR_PLANE_IDX]]
111          var = var_img[_IMG_STATS_GRID//2, _IMG_STATS_GRID//2,
112                        cfa_idxs[_GR_PLANE_IDX]]/white_level**2
113          logging.debug('cap: %d, mean: %.2f, var: %e', i, mean, var)
114          variances.append(var)
115
116      # Flag dark images
117      if math.isclose(mean, max(black_levels), rel_tol=_BLACK_LEVEL_RTOL):
118        raise AssertionError(f'Images are too dark! Center mean: {mean:.2f}')
119
120      # Create plot
121      sensitivities = np.repeat(sensitivities, _NUM_FRAMES)
122      pylab.figure(_NAME)
123      pylab.plot(sensitivities, variances, '-ro')
124      pylab.xticks(sensitivities)
125      pylab.xlabel('Sensitivities')
126      pylab.ylabel('Image Center Patch Variance')
127      pylab.ticklabel_format(axis='y', style='sci', scilimits=(-6, -6))
128      pylab.title(_NAME)
129      matplotlib.pyplot.savefig(f'{name_with_log_path}_variances.png')
130
131      # Find average variance at each step
132      vars_step_means = []
133      for i in range(_NUM_SENS_STEPS):
134        vars_step = []
135        for j in range(_NUM_FRAMES):
136          vars_step.append(variances[_NUM_FRAMES * i + j])
137        vars_step_means.append(np.mean(vars_step))
138      logging.debug('averaged variances: %s', vars_step_means)
139
140      # Assert each set of shots is noisier than previous and save img on FAIL
141      for variance_idx, variance in enumerate(vars_step_means[:-1]):
142        if variance >= vars_step_means[variance_idx+1] / _VAR_THRESH:
143          image_processing_utils.capture_scene_image(
144              cam, props, name_with_log_path
145          )
146          raise AssertionError(
147              f'variances [i]: {variances[variance_idx]:.5f}, '
148              f'[i+1]: {variances[variance_idx+1]:.5f}, THRESH: {_VAR_THRESH}'
149          )
150
151if __name__ == '__main__':
152  test_runner.main()
153