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 RAW sensitivity burst."""
15
16
17import logging
18import os.path
19import matplotlib
20from matplotlib import pylab
21from mobly import test_runner
22import numpy as np
23
24import its_base_test
25import camera_properties_utils
26import capture_request_utils
27import image_processing_utils
28import its_session_utils
29
30_GR_PLANE_IDX = 1  # GR plane index in RGGB data
31_IMG_STATS_GRID = 9  # find used to find the center 11.11%
32_NAME = os.path.splitext(os.path.basename(__file__))[0]
33_NUM_FRAMES = 4
34_NUM_STEPS = 5
35_VAR_THRESH = 1.01  # each shot must be 1% noisier than previous
36
37
38def define_raw_stats_fmt(props):
39  """Defines the format using camera props active array width and height."""
40  aax = props['android.sensor.info.preCorrectionActiveArraySize']['left']
41  aay = props['android.sensor.info.preCorrectionActiveArraySize']['top']
42  aaw = props['android.sensor.info.preCorrectionActiveArraySize']['right'] - aax
43  aah = props[
44      'android.sensor.info.preCorrectionActiveArraySize']['bottom'] - aay
45
46  return {'format': 'rawStats',
47          'gridWidth': aaw // _IMG_STATS_GRID,
48          'gridHeight': aah // _IMG_STATS_GRID}
49
50
51class RawBurstSensitivityTest(its_base_test.ItsBaseTest):
52  """Captures a set of RAW images with increasing sensitivity & measures noise.
53
54  Sensitivity range (gain) is determined from camera properties and limited to
55  the analog sensitivity range as captures are RAW only in a burst. Digital
56  sensitivity range from props['android.sensor.info.sensitivityRange'] is not
57  used.
58
59  Uses RawStats capture format to speed up processing. RawStats defines a grid
60  over the RAW image and returns average and variance of requested areas.
61  white_level is found from camera to normalize variance values from RawStats.
62
63  Noise (image variance) of center patch should increase with increasing
64  sensitivity.
65  """
66
67  def test_raw_burst_sensitivity(self):
68    with its_session_utils.ItsSession(
69        device_id=self.dut.serial,
70        camera_id=self.camera_id,
71        hidden_physical_id=self.hidden_physical_id) as cam:
72      props = cam.get_camera_properties()
73      props = cam.override_with_hidden_physical_camera_props(props)
74      camera_properties_utils.skip_unless(
75          camera_properties_utils.raw16(props) and
76          camera_properties_utils.manual_sensor(props) and
77          camera_properties_utils.read_3a(props) and
78          camera_properties_utils.per_frame_control(props) and
79          not camera_properties_utils.mono_camera(props))
80      name_with_log_path = os.path.join(self.log_path, _NAME)
81
82      # Load chart for scene
83      its_session_utils.load_scene(
84          cam, props, self.scene, self.tablet,
85          its_session_utils.CHART_DISTANCE_NO_SCALING)
86
87      # Find sensitivity range and create capture requests
88      sens_min, _ = props['android.sensor.info.sensitivityRange']
89      sens_max = props['android.sensor.maxAnalogSensitivity']
90      sens_step = (sens_max - sens_min) // _NUM_STEPS
91      # Intentionally blur images for noise measurements
92      sens_ae, exp_ae, _, _, _ = cam.do_3a(do_af=False, get_results=True)
93      sens_exp_prod = sens_ae * exp_ae
94      reqs = []
95      settings = []
96      for sens in range(sens_min, sens_max, sens_step):
97        exp = int(sens_exp_prod / float(sens))
98        req = capture_request_utils.manual_capture_request(sens, exp, 0)
99        for i in range(_NUM_FRAMES):
100          reqs.append(req)
101          settings.append((sens, exp))
102
103      # Get rawStats capture format
104      fmt = define_raw_stats_fmt(props)
105
106      # Do captures
107      caps = cam.do_capture(reqs, fmt)
108
109      # Find white_level for RawStats normalization & CFA order
110      white_level = float(props['android.sensor.info.whiteLevel'])
111      cfa_idxs = image_processing_utils.get_canonical_cfa_order(props)
112
113      # Extract variances from each shot
114      variances = []
115      for i, cap in enumerate(caps):
116        sens, exp = settings[i]
117        _, var_image = image_processing_utils.unpack_rawstats_capture(cap)
118        var = var_image[_IMG_STATS_GRID//2, _IMG_STATS_GRID//2,
119                        cfa_idxs[_GR_PLANE_IDX]]/white_level**2
120        variances.append(var)
121        logging.debug('s=%d, e=%d, var=%e', sens, exp, var)
122
123      # Create a plot
124      x = range(len(variances))
125      pylab.figure(_NAME)
126      pylab.plot(x, variances, '-ro')
127      pylab.xticks(x)
128      pylab.ticklabel_format(style='sci', axis='y', scilimits=(-6, -6))
129      pylab.xlabel('Setting Combination')
130      pylab.ylabel('Image Center Patch Variance')
131      pylab.title(_NAME)
132      matplotlib.pyplot.savefig(
133          f'{name_with_log_path}_variances.png')
134
135      # Find average variance at each step
136      vars_step_means = []
137      for i in range(_NUM_STEPS):
138        vars_step = []
139        for j in range(_NUM_FRAMES):
140          vars_step.append(variances[_NUM_FRAMES * i + j])
141        vars_step_means.append(np.mean(vars_step))
142      logging.debug('averaged variances: %s', vars_step_means)
143
144      # Assert each set of shots is noisier than previous and save img on FAIL
145      for variance_idx, variance in enumerate(vars_step_means[:-1]):
146        if variance >= vars_step_means[variance_idx+1] / _VAR_THRESH:
147          image_processing_utils.capture_scene_image(
148              cam, props, name_with_log_path
149          )
150          raise AssertionError(
151              f'variances [i]: {variances[variance_idx]:.5f}, '
152              f'[i+1]: {variances[variance_idx+1]:.5f}, THRESH: {_VAR_THRESH}'
153          )
154
155if __name__ == '__main__':
156  test_runner.main()
157