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