1# Copyright 2015 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 android.noiseReduction.mode applied for reprocessing reqs."""
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
30import target_exposure_utils
31
32_COLORS = ('R', 'G', 'B')
33_NAME = os.path.splitext(os.path.basename(__file__))[0]
34_NR_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'MIN': 3, 'ZSL': 4}
35_NR_MODES_LIST = tuple(_NR_MODES.values())
36_NUM_FRAMES = 4
37_PATCH_H = 0.1  # center 10%
38_PATCH_W = 0.1
39_PATCH_X = 0.5 - _PATCH_W/2
40_PATCH_Y = 0.5 - _PATCH_H/2
41_SNR_ATOL = 3  # unit in dB
42
43
44def calc_rgb_snr(cap, frame, nr_mode, name_with_log_path):
45  """Calculate the RGB SNRs from a capture center patch.
46
47  Args:
48    cap: Camera capture object.
49    frame: Integer frame number.
50    nr_mode: Integer noise reduction mode index.
51    name_with_log_path: Test name with path for storage
52
53  Returns:
54    RGB SNRs.
55  """
56  img = image_processing_utils.decompress_jpeg_to_rgb_image(cap)
57  if frame == 0:  # save 1st frame
58    image_processing_utils.write_image(
59        img, f'{name_with_log_path}_high_gain_nr={nr_mode}_fmt=jpg.jpg')
60  patch = image_processing_utils.get_image_patch(
61      img, _PATCH_X, _PATCH_Y, _PATCH_W, _PATCH_H)
62  return image_processing_utils.compute_image_snrs(patch)
63
64
65def create_plot(snrs, reprocess_format, name_with_log_path):
66  """create plot from data.
67
68  Args:
69    snrs: RGB SNR data from NR_MODES captures.
70    reprocess_format: String of 'yuv' or 'private'.
71    name_with_log_path: Test name with path for storage.
72  """
73  pylab.figure(reprocess_format)
74  for ch, color in enumerate(_COLORS):
75    pylab.plot(_NR_MODES_LIST, snrs[ch], f'-{color.lower()}o')
76  pylab.title(f'{_NAME} ({reprocess_format})')
77  pylab.xlabel(f'{str(_NR_MODES)[1:-1]}')  # strip '{' '}' off string
78  pylab.ylabel('SNR (dB)')
79  pylab.xticks(_NR_MODES_LIST)
80  matplotlib.pyplot.savefig(
81      f'{name_with_log_path}_plot_{reprocess_format}_SNRs.png')
82
83
84class ReprocessNoiseReductionTest(its_base_test.ItsBaseTest):
85  """Test android.noiseReduction.mode is applied for reprocessing requests.
86
87  Uses JPEG captures for the reprocessing as YUV captures are not available.
88  Uses high analog gain to ensure the captured images are noisy.
89
90  Determines which reprocessing formats are available among 'yuv' and 'private'.
91  For each reprocessing format:
92    Captures in supported reprocessed modes.
93    Averages _NUM_FRAMES to account for frame-to-frame variation.
94    Logs min/max of captures for debug if gross outlier.
95    Noise reduction (NR) modes:
96      OFF, FAST, High Quality (HQ), Minimal (MIN), and zero shutter lag (ZSL)
97
98    Proper behavior:
99      FAST >= OFF, HQ >= FAST, HQ >> OFF
100      if MIN mode supported: MIN >= OFF, HQ >= MIN, ZSL ~ MIN
101      else: ZSL ~ OFF
102  """
103
104  def test_reprocess_noise_reduction(self):
105    logging.debug('Starting %s', _NAME)
106    logging.debug('NR_MODES: %s', str(_NR_MODES))
107    with its_session_utils.ItsSession(
108        device_id=self.dut.serial,
109        camera_id=self.camera_id,
110        hidden_physical_id=self.hidden_physical_id) as cam:
111      props = cam.get_camera_properties()
112      props = cam.override_with_hidden_physical_camera_props(props)
113      camera_properties_utils.skip_unless(
114          camera_properties_utils.compute_target_exposure(props) and
115          camera_properties_utils.per_frame_control(props) and
116          camera_properties_utils.noise_reduction_mode(props, 0) and
117          (camera_properties_utils.yuv_reprocess(props) or
118           camera_properties_utils.private_reprocess(props)))
119      log_path = self.log_path
120      name_with_log_path = os.path.join(log_path, _NAME)
121
122      # Load chart for scene.
123      its_session_utils.load_scene(
124          cam, props, self.scene, self.tablet,
125          its_session_utils.CHART_DISTANCE_NO_SCALING)
126
127      # If reprocessing is supported, ZSL NR mode must be available.
128      if not camera_properties_utils.noise_reduction_mode(
129          props, _NR_MODES['ZSL']):
130        raise KeyError('Reprocessing supported, so ZSL must be supported.')
131
132      reprocess_formats = []
133      if camera_properties_utils.yuv_reprocess(props):
134        reprocess_formats.append('yuv')
135      if camera_properties_utils.private_reprocess(props):
136        reprocess_formats.append('private')
137
138      size = capture_request_utils.get_available_output_sizes('jpg', props)[0]
139      out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'}
140      for reprocess_format in reprocess_formats:
141        logging.debug('Reprocess format: %s', reprocess_format)
142        # List of variances for R, G, B.
143        snrs = [[], [], []]
144        nr_modes_reported = []
145
146        # Capture for each mode.
147        exp, sens = target_exposure_utils.get_target_exposure_combos(
148            log_path, cam)['maxSensitivity']
149        for nr_mode in _NR_MODES_LIST:
150          # Skip unavailable modes
151          if not camera_properties_utils.noise_reduction_mode(props, nr_mode):
152            nr_modes_reported.append(nr_mode)
153            for ch, _ in enumerate(_COLORS):
154              snrs[ch].append(0)
155            continue
156
157          # Create req, do caps and calc center SNRs.
158          rgb_snr_list = []
159          nr_modes_reported.append(nr_mode)
160          req = capture_request_utils.manual_capture_request(sens, exp)
161          req['android.noiseReduction.mode'] = nr_mode
162          caps = cam.do_capture(
163              [req]*_NUM_FRAMES, out_surface, reprocess_format)
164          for i in range(_NUM_FRAMES):
165            rgb_snr_list.append(calc_rgb_snr(caps[i]['data'], i, nr_mode,
166                                             name_with_log_path))
167
168          r_snrs = [rgb[0] for rgb in rgb_snr_list]
169          g_snrs = [rgb[1] for rgb in rgb_snr_list]
170          b_snrs = [rgb[2] for rgb in rgb_snr_list]
171          rgb_avg_snrs = [np.mean(r_snrs), np.mean(g_snrs), np.mean(b_snrs)]
172          for ch, x_snrs in enumerate([r_snrs, g_snrs, b_snrs]):
173            snrs[ch].append(rgb_avg_snrs[ch])
174            logging.debug(
175                'NR mode %d %s SNR avg: %.2f min: %.2f, max: %.2f', nr_mode,
176                _COLORS[ch], rgb_avg_snrs[ch], min(x_snrs), max(x_snrs))
177
178        # Plot data.
179        create_plot(snrs, reprocess_format, name_with_log_path)
180
181        # Assert proper behavior.
182        if nr_modes_reported != list(_NR_MODES_LIST):
183          raise KeyError('Reported modes: '
184                         f'{nr_modes_reported}. Expected: {_NR_MODES_LIST}.')
185        for j, _ in enumerate(_COLORS):
186          # OFF < FAST + ATOL
187          if snrs[j][_NR_MODES['OFF']] >= snrs[j][_NR_MODES['FAST']]+_SNR_ATOL:
188            raise AssertionError(f'FAST: {snrs[j][_NR_MODES["FAST"]]:.2f}, '
189                                 f'OFF: {snrs[j][_NR_MODES["OFF"]]:.2f}, '
190                                 f'ATOL: {_SNR_ATOL}')
191
192          # FAST < HQ + ATOL
193          if snrs[j][_NR_MODES['FAST']] >= snrs[j][_NR_MODES['HQ']]+_SNR_ATOL:
194            raise AssertionError(f'HQ: {snrs[j][_NR_MODES["HQ"]]:.2f}, '
195                                 f'FAST: {snrs[j][_NR_MODES["FAST"]]:.2f}, '
196                                 f'ATOL: {_SNR_ATOL}')
197
198          # HQ > OFF
199          if snrs[j][_NR_MODES['HQ']] <= snrs[j][_NR_MODES['OFF']]:
200            raise AssertionError(f'HQ: {snrs[j][_NR_MODES["HQ"]]:.2f}, '
201                                 f'OFF: {snrs[j][_NR_MODES["OFF"]]:.2f}')
202
203          if camera_properties_utils.noise_reduction_mode(
204              props, _NR_MODES['MIN']):
205            # OFF < MIN + ATOL
206            if snrs[j][_NR_MODES['OFF']] >= snrs[j][_NR_MODES['MIN']]+_SNR_ATOL:
207              raise AssertionError(f'MIN: {snrs[j][_NR_MODES["MIN"]]:.2f}, '
208                                   f'OFF: {snrs[j][_NR_MODES["OFF"]]:.2f}, '
209                                   f'ATOL: {_SNR_ATOL}')
210
211            # MIN < HQ + ATOL
212            if snrs[j][_NR_MODES['MIN']] >= snrs[j][_NR_MODES['HQ']]+_SNR_ATOL:
213              raise AssertionError(f'MIN: {snrs[j][_NR_MODES["MIN"]]:.2f}, '
214                                   f'HQ: {snrs[j][_NR_MODES["HQ"]]:.2f}, '
215                                   f'ATOL: {_SNR_ATOL}')
216
217            # ZSL ~ MIN
218            if not math.isclose(
219                snrs[j][_NR_MODES['ZSL']], snrs[j][_NR_MODES['MIN']],
220                abs_tol=_SNR_ATOL):
221              raise AssertionError(f'ZSL: {snrs[j][_NR_MODES["ZSL"]]:.2f}, '
222                                   f'MIN: {snrs[j][_NR_MODES["MIN"]]:.2f}, '
223                                   f'ATOL: {_SNR_ATOL}')
224          else:
225            # ZSL ~ OFF
226            if not math.isclose(
227                snrs[j][_NR_MODES['ZSL']], snrs[j][_NR_MODES['OFF']],
228                abs_tol=_SNR_ATOL):
229              raise AssertionError(f'ZSL: {snrs[j][_NR_MODES["ZSL"]]:.2f}, '
230                                   f'OFF: {snrs[j][_NR_MODES["OFF"]]:.2f}, '
231                                   f'ATOL: {_SNR_ATOL}')
232
233if __name__ == '__main__':
234  test_runner.main()
235
236