1# Copyright 2023 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 U and V are not swapped during reprocessing."""
15
16
17import logging
18import os.path
19from mobly import test_runner
20
21import its_base_test
22import camera_properties_utils
23import capture_request_utils
24import image_processing_utils
25import its_session_utils
26
27_NAME = os.path.splitext(os.path.basename(__file__))[0]
28_NR_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'MIN': 3, 'ZSL': 4}
29_EDGE_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'ZSL': 3}
30
31
32def calc_rgb_uv_swap(cap_no_re, cap_re, re_mode, name_with_log_path,
33                     reprocess_format, reprocess_type):
34  """Determine the likelihood of a UV swap due to reprocessing.
35
36  Args:
37    cap_no_re: Camera capture object without reprocessing.
38    cap_re: Camera capture object with reprocessing.
39    re_mode: Integer reprocess mode index.
40    name_with_log_path: Test name with path for storage.
41    reprocess_format: The reprocess format string.
42    reprocess_type: The type of reprocessing as a string (e.g. "nr", "ee").
43
44  Returns:
45    A tuple of sums of absolute differences for the swapped and non-swapped
46    comparisons.
47  """
48  suffix = f'{reprocess_type}={re_mode}_reprocess_fmt='
49  suffix += f'{reprocess_format}_fmt=jpg.jpg'
50  img_no_re = image_processing_utils.decompress_jpeg_to_yuv_image(cap_no_re)
51  image_processing_utils.write_image(
52      img_no_re, f'{name_with_log_path}_no_{suffix}', is_yuv=True)
53
54  img_re = image_processing_utils.decompress_jpeg_to_yuv_image(cap_re)
55  image_processing_utils.write_image(
56      img_re, f'{name_with_log_path}_{suffix}', is_yuv=True)
57
58  # Generate a UV-swapped copy of the reprocessed image.
59  img_re_swap = img_re[:, :, [0, 2, 1]]
60
61  # Calculate the sums of absolute difference with and without the U and V
62  # channels swapped
63  sad_swap = image_processing_utils.compute_image_sad(img_no_re, img_re_swap)
64  sad_no_swap = image_processing_utils.compute_image_sad(img_no_re, img_re)
65
66  return (sad_swap, sad_no_swap)
67
68
69class ReprocessUvSwapTest(its_base_test.ItsBaseTest):
70  """Test for UV swap during reprocessing requests.
71
72  Uses JPEG captures as the output format.
73
74  Determines which reprocessing formats are available among 'yuv' and 'private'.
75  For each reprocessing format:
76    Captures without reprocessing.
77    Captures in supported reprocessed modes.
78    Calculates the SAD (Sum of Absolute Differences) between the two, with
79      and without UV swap.
80    If the SAD is smaller when U and V are swapped, fails the test.
81    Noise reduction (NR) modes:
82      OFF, FAST, High Quality (HQ), Minimal (MIN), and zero shutter lag (ZSL)
83
84    Proper behavior:
85      The U and V planes should not be swapped.
86  """
87
88  def test_reprocess_noise_reduction(self):
89    logging.debug('Starting %s:test_reprocess_noise_reduction', _NAME)
90    logging.debug('NR_MODES: %s', str(_NR_MODES))
91
92    with its_session_utils.ItsSession(
93        device_id=self.dut.serial,
94        camera_id=self.camera_id,
95        hidden_physical_id=self.hidden_physical_id) as cam:
96      props = cam.get_camera_properties()
97      props = cam.override_with_hidden_physical_camera_props(props)
98      camera_properties_utils.skip_unless(
99          camera_properties_utils.compute_target_exposure(props) and
100          camera_properties_utils.per_frame_control(props) and
101          camera_properties_utils.noise_reduction_mode(props, 0) and
102          (camera_properties_utils.yuv_reprocess(props) or
103           camera_properties_utils.private_reprocess(props)))
104      log_path = self.log_path
105      name_with_log_path = os.path.join(log_path, _NAME)
106
107      # Load chart for scene
108      its_session_utils.load_scene(
109          cam, props, self.scene, self.tablet, self.chart_distance)
110
111      # If reprocessing is supported, ZSL NR mode must be available
112      if not camera_properties_utils.noise_reduction_mode(
113          props, _NR_MODES['ZSL']):
114        raise KeyError('Reprocessing supported, so ZSL must be supported.')
115
116      reprocess_formats = camera_properties_utils.get_reprocess_formats(props)
117
118      # Capture for each available reprocess format
119      uv_swaps = {}
120      size = capture_request_utils.get_available_output_sizes('jpg', props)[0]
121      out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'}
122      for reprocess_format in reprocess_formats:
123        logging.debug('Reprocess format: %s', reprocess_format)
124        uv_swaps[reprocess_format] = {}
125
126        # Capture for each mode
127        for nr_mode in tuple(_NR_MODES.values()):
128          # Skip unavailable modes
129          if not camera_properties_utils.noise_reduction_mode(props, nr_mode):
130            uv_swaps[reprocess_format][nr_mode] = (0, 0)
131            continue
132
133          # Create req, do caps and determine UV swap likelihood
134          req = capture_request_utils.auto_capture_request()
135          req['android.noiseReduction.mode'] = nr_mode
136          cam.do_3a()
137          caps_no_nr = cam.do_capture([req], out_surface)
138          caps = cam.do_capture([req], out_surface, reprocess_format)
139
140          sad_swap, sad_no_swap = calc_rgb_uv_swap(
141              caps_no_nr[0]['data'], caps[0]['data'], nr_mode,
142              name_with_log_path, reprocess_format, 'nr')
143          uv_swaps[reprocess_format][nr_mode] = (sad_swap, sad_no_swap)
144
145      # Fail all instances where swapping the U and V channels results in an
146      # image which is closer to the non-reprocessed capture.
147      num_fail = 0
148      num_tests = 0
149      for reprocess_format, nr_mode_dict in uv_swaps.items():
150        for nr_mode, (sad_swap, sad_no_swap) in nr_mode_dict.items():
151          num_tests += 1
152          if sad_swap < sad_no_swap:
153            num_fail += 1
154            logging.error('REPROCESS_FMT: %s, '
155                          'NR_MODE: %d, '
156                          'SAD_SWAP: %.2f, '
157                          'SAD_NO_SWAP: %.2f',
158                          reprocess_format, nr_mode, sad_swap, sad_no_swap)
159
160      if num_fail > 0:
161        raise AssertionError(f'Number of fails: {num_fail} / {num_tests}')
162
163  def test_reprocess_edge_enhancement(self):
164    logging.debug('Starting %s:test_reprocess_edge_enhancement', _NAME)
165    logging.debug('EDGE_MODES: %s', str(_EDGE_MODES))
166
167    with its_session_utils.ItsSession(
168        device_id=self.dut.serial,
169        camera_id=self.camera_id,
170        hidden_physical_id=self.hidden_physical_id) as cam:
171      props = cam.get_camera_properties()
172      props = cam.override_with_hidden_physical_camera_props(props)
173      camera_properties_utils.skip_unless(
174          camera_properties_utils.read_3a(props) and
175          camera_properties_utils.per_frame_control(props) and
176          camera_properties_utils.edge_mode(props, 0) and
177          (camera_properties_utils.yuv_reprocess(props) or
178           camera_properties_utils.private_reprocess(props)))
179      log_path = self.log_path
180      name_with_log_path = os.path.join(log_path, _NAME)
181
182      # Load chart for scene
183      its_session_utils.load_scene(
184          cam, props, self.scene, self.tablet, self.chart_distance)
185
186      # If reprocessing is supported, ZSL EE mode must be available
187      if not camera_properties_utils.edge_mode(props, _EDGE_MODES['ZSL']):
188        raise KeyError('Reprocessing supported, so ZSL must be supported.')
189
190      reprocess_formats = camera_properties_utils.get_reprocess_formats(props)
191
192      # Capture for each available reprocess format
193      uv_swaps = {}
194      size = capture_request_utils.get_available_output_sizes('jpg', props)[0]
195      out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'}
196      for reprocess_format in reprocess_formats:
197        logging.debug('Reprocess format: %s', reprocess_format)
198        uv_swaps[reprocess_format] = {}
199
200        # Capture for each mode
201        for edge_mode in tuple(_EDGE_MODES.values()):
202          # Skip unavailable modes
203          if not camera_properties_utils.edge_mode(props, edge_mode):
204            uv_swaps[reprocess_format][edge_mode] = (0, 0)
205            continue
206
207          # Create req, do caps and determine UV swap likelihood
208          req = capture_request_utils.auto_capture_request()
209          req['android.edge.mode'] = edge_mode
210          cam.do_3a()
211          caps_no_ee = cam.do_capture([req], out_surface)
212          caps = cam.do_capture([req], out_surface, reprocess_format)
213
214          sad_swap, sad_no_swap = calc_rgb_uv_swap(
215              caps_no_ee[0]['data'], caps[0]['data'], edge_mode,
216              name_with_log_path, reprocess_format, 'ee')
217          uv_swaps[reprocess_format][edge_mode] = (sad_swap, sad_no_swap)
218
219      # Fail all instances where swapping the U and V channels results in an
220      # image which is closer to the non-reprocessed capture.
221      num_fail = 0
222      num_tests = 0
223      for reprocess_format, edge_mode_dict in uv_swaps.items():
224        for edge_mode, (sad_swap, sad_no_swap) in edge_mode_dict.items():
225          num_tests += 1
226          if sad_swap < sad_no_swap:
227            num_fail += 1
228            logging.error('REPROCESS_FMT: %s, '
229                          'EDGE_MODE: %d, '
230                          'SAD_SWAP: %.2f, '
231                          'SAD_NO_SWAP: %.2f',
232                          reprocess_format, edge_mode, sad_swap, sad_no_swap)
233
234      if num_fail > 0:
235        raise AssertionError(f'Number of fails: {num_fail} / {num_tests}')
236
237  def test_reprocess_jpeg(self):
238    logging.debug('Starting %s:test_reprocess_jpeg', _NAME)
239
240    with its_session_utils.ItsSession(
241        device_id=self.dut.serial,
242        camera_id=self.camera_id,
243        hidden_physical_id=self.hidden_physical_id) as cam:
244      props = cam.get_camera_properties()
245      props = cam.override_with_hidden_physical_camera_props(props)
246      camera_properties_utils.skip_unless(
247          camera_properties_utils.per_frame_control(props) and
248          camera_properties_utils.jpeg_orientation(props) and
249          (camera_properties_utils.yuv_reprocess(props) or
250           camera_properties_utils.private_reprocess(props)))
251      log_path = self.log_path
252      name_with_log_path = os.path.join(log_path, _NAME)
253      applied_orientation = 90
254
255      # Load chart for scene
256      its_session_utils.load_scene(
257          cam, props, self.scene, self.tablet, self.chart_distance)
258
259      reprocess_formats = camera_properties_utils.get_reprocess_formats(props)
260
261      # Capture for each available reprocess format
262      uv_swaps = {}
263      size = capture_request_utils.get_available_output_sizes('jpg', props)[0]
264      out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'}
265      for reprocess_format in reprocess_formats:
266        logging.debug('Reprocess format: %s', reprocess_format)
267
268        # Create req, do caps and determine UV swap likelihood
269        req = capture_request_utils.auto_capture_request()
270        req['android.jpeg.orientation'] = applied_orientation
271        cam.do_3a()
272        caps_no_jpeg = cam.do_capture([req], out_surface)
273        caps = cam.do_capture([req], out_surface, reprocess_format)
274
275        sad_swap, sad_no_swap = calc_rgb_uv_swap(
276            caps_no_jpeg[0]['data'], caps[0]['data'], applied_orientation,
277            name_with_log_path, reprocess_format, 'orientation')
278        uv_swaps[reprocess_format] = (sad_swap, sad_no_swap)
279
280      # Fail all instances where swapping the U and V channels results in an
281      # image which is closer to the non-reprocessed capture.
282      num_fail = 0
283      num_tests = 0
284      for reprocess_format, (sad_swap, sad_no_swap) in uv_swaps.items():
285        num_tests += 1
286        if sad_swap < sad_no_swap:
287          num_fail += 1
288          logging.error('REPROCESS_FMT: %s, '
289                        'ORIENTATION: %d, '
290                        'SAD_SWAP: %.2f, '
291                        'SAD_NO_SWAP: %.2f',
292                        reprocess_format, applied_orientation, sad_swap,
293                        sad_no_swap)
294
295      if num_fail > 0:
296        raise AssertionError(f'Number of fails: {num_fail} / {num_tests}')
297
298if __name__ == '__main__':
299  test_runner.main()
300
301