| # Copyright 2023 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Verifies U and V are not swapped during reprocessing.""" |
| |
| |
| import logging |
| import os.path |
| from mobly import test_runner |
| |
| import its_base_test |
| import camera_properties_utils |
| import capture_request_utils |
| import image_processing_utils |
| import its_session_utils |
| |
| _NAME = os.path.splitext(os.path.basename(__file__))[0] |
| _NR_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'MIN': 3, 'ZSL': 4} |
| _EDGE_MODES = {'OFF': 0, 'FAST': 1, 'HQ': 2, 'ZSL': 3} |
| |
| |
| def calc_rgb_uv_swap(cap_no_re, cap_re, re_mode, name_with_log_path, |
| reprocess_format, reprocess_type): |
| """Determine the likelihood of a UV swap due to reprocessing. |
| |
| Args: |
| cap_no_re: Camera capture object without reprocessing. |
| cap_re: Camera capture object with reprocessing. |
| re_mode: Integer reprocess mode index. |
| name_with_log_path: Test name with path for storage. |
| reprocess_format: The reprocess format string. |
| reprocess_type: The type of reprocessing as a string (e.g. "nr", "ee"). |
| |
| Returns: |
| A tuple of sums of absolute differences for the swapped and non-swapped |
| comparisons. |
| """ |
| suffix = f'{reprocess_type}={re_mode}_reprocess_fmt=' |
| suffix += f'{reprocess_format}_fmt=jpg.jpg' |
| img_no_re = image_processing_utils.decompress_jpeg_to_yuv_image(cap_no_re) |
| image_processing_utils.write_image( |
| img_no_re, f'{name_with_log_path}_no_{suffix}', is_yuv=True) |
| |
| img_re = image_processing_utils.decompress_jpeg_to_yuv_image(cap_re) |
| image_processing_utils.write_image( |
| img_re, f'{name_with_log_path}_{suffix}', is_yuv=True) |
| |
| # Generate a UV-swapped copy of the reprocessed image. |
| img_re_swap = img_re[:, :, [0, 2, 1]] |
| |
| # Calculate the sums of absolute difference with and without the U and V |
| # channels swapped |
| sad_swap = image_processing_utils.compute_image_sad(img_no_re, img_re_swap) |
| sad_no_swap = image_processing_utils.compute_image_sad(img_no_re, img_re) |
| |
| return (sad_swap, sad_no_swap) |
| |
| |
| class ReprocessUvSwapTest(its_base_test.ItsBaseTest): |
| """Test for UV swap during reprocessing requests. |
| |
| Uses JPEG captures as the output format. |
| |
| Determines which reprocessing formats are available among 'yuv' and 'private'. |
| For each reprocessing format: |
| Captures without reprocessing. |
| Captures in supported reprocessed modes. |
| Calculates the SAD (Sum of Absolute Differences) between the two, with |
| and without UV swap. |
| If the SAD is smaller when U and V are swapped, fails the test. |
| Noise reduction (NR) modes: |
| OFF, FAST, High Quality (HQ), Minimal (MIN), and zero shutter lag (ZSL) |
| |
| Proper behavior: |
| The U and V planes should not be swapped. |
| """ |
| |
| def test_reprocess_noise_reduction(self): |
| logging.debug('Starting %s:test_reprocess_noise_reduction', _NAME) |
| logging.debug('NR_MODES: %s', str(_NR_MODES)) |
| |
| with its_session_utils.ItsSession( |
| device_id=self.dut.serial, |
| camera_id=self.camera_id, |
| hidden_physical_id=self.hidden_physical_id) as cam: |
| props = cam.get_camera_properties() |
| props = cam.override_with_hidden_physical_camera_props(props) |
| camera_properties_utils.skip_unless( |
| camera_properties_utils.compute_target_exposure(props) and |
| camera_properties_utils.per_frame_control(props) and |
| camera_properties_utils.noise_reduction_mode(props, 0) and |
| (camera_properties_utils.yuv_reprocess(props) or |
| camera_properties_utils.private_reprocess(props))) |
| log_path = self.log_path |
| name_with_log_path = os.path.join(log_path, _NAME) |
| |
| # Load chart for scene |
| its_session_utils.load_scene( |
| cam, props, self.scene, self.tablet, self.chart_distance) |
| |
| # If reprocessing is supported, ZSL NR mode must be available |
| if not camera_properties_utils.noise_reduction_mode( |
| props, _NR_MODES['ZSL']): |
| raise KeyError('Reprocessing supported, so ZSL must be supported.') |
| |
| reprocess_formats = camera_properties_utils.get_reprocess_formats(props) |
| |
| # Capture for each available reprocess format |
| uv_swaps = {} |
| size = capture_request_utils.get_available_output_sizes('jpg', props)[0] |
| out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'} |
| for reprocess_format in reprocess_formats: |
| logging.debug('Reprocess format: %s', reprocess_format) |
| uv_swaps[reprocess_format] = {} |
| |
| # Capture for each mode |
| for nr_mode in tuple(_NR_MODES.values()): |
| # Skip unavailable modes |
| if not camera_properties_utils.noise_reduction_mode(props, nr_mode): |
| uv_swaps[reprocess_format][nr_mode] = (0, 0) |
| continue |
| |
| # Create req, do caps and determine UV swap likelihood |
| req = capture_request_utils.auto_capture_request() |
| req['android.noiseReduction.mode'] = nr_mode |
| cam.do_3a() |
| caps_no_nr = cam.do_capture([req], out_surface) |
| caps = cam.do_capture([req], out_surface, reprocess_format) |
| |
| sad_swap, sad_no_swap = calc_rgb_uv_swap( |
| caps_no_nr[0]['data'], caps[0]['data'], nr_mode, |
| name_with_log_path, reprocess_format, 'nr') |
| uv_swaps[reprocess_format][nr_mode] = (sad_swap, sad_no_swap) |
| |
| # Fail all instances where swapping the U and V channels results in an |
| # image which is closer to the non-reprocessed capture. |
| num_fail = 0 |
| num_tests = 0 |
| for reprocess_format, nr_mode_dict in uv_swaps.items(): |
| for nr_mode, (sad_swap, sad_no_swap) in nr_mode_dict.items(): |
| num_tests += 1 |
| if sad_swap < sad_no_swap: |
| num_fail += 1 |
| logging.error('REPROCESS_FMT: %s, ' |
| 'NR_MODE: %d, ' |
| 'SAD_SWAP: %.2f, ' |
| 'SAD_NO_SWAP: %.2f', |
| reprocess_format, nr_mode, sad_swap, sad_no_swap) |
| |
| if num_fail > 0: |
| raise AssertionError(f'Number of fails: {num_fail} / {num_tests}') |
| |
| def test_reprocess_edge_enhancement(self): |
| logging.debug('Starting %s:test_reprocess_edge_enhancement', _NAME) |
| logging.debug('EDGE_MODES: %s', str(_EDGE_MODES)) |
| |
| with its_session_utils.ItsSession( |
| device_id=self.dut.serial, |
| camera_id=self.camera_id, |
| hidden_physical_id=self.hidden_physical_id) as cam: |
| props = cam.get_camera_properties() |
| props = cam.override_with_hidden_physical_camera_props(props) |
| camera_properties_utils.skip_unless( |
| camera_properties_utils.read_3a(props) and |
| camera_properties_utils.per_frame_control(props) and |
| camera_properties_utils.edge_mode(props, 0) and |
| (camera_properties_utils.yuv_reprocess(props) or |
| camera_properties_utils.private_reprocess(props))) |
| log_path = self.log_path |
| name_with_log_path = os.path.join(log_path, _NAME) |
| |
| # Load chart for scene |
| its_session_utils.load_scene( |
| cam, props, self.scene, self.tablet, self.chart_distance) |
| |
| # If reprocessing is supported, ZSL EE mode must be available |
| if not camera_properties_utils.edge_mode(props, _EDGE_MODES['ZSL']): |
| raise KeyError('Reprocessing supported, so ZSL must be supported.') |
| |
| reprocess_formats = camera_properties_utils.get_reprocess_formats(props) |
| |
| # Capture for each available reprocess format |
| uv_swaps = {} |
| size = capture_request_utils.get_available_output_sizes('jpg', props)[0] |
| out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'} |
| for reprocess_format in reprocess_formats: |
| logging.debug('Reprocess format: %s', reprocess_format) |
| uv_swaps[reprocess_format] = {} |
| |
| # Capture for each mode |
| for edge_mode in tuple(_EDGE_MODES.values()): |
| # Skip unavailable modes |
| if not camera_properties_utils.edge_mode(props, edge_mode): |
| uv_swaps[reprocess_format][edge_mode] = (0, 0) |
| continue |
| |
| # Create req, do caps and determine UV swap likelihood |
| req = capture_request_utils.auto_capture_request() |
| req['android.edge.mode'] = edge_mode |
| cam.do_3a() |
| caps_no_ee = cam.do_capture([req], out_surface) |
| caps = cam.do_capture([req], out_surface, reprocess_format) |
| |
| sad_swap, sad_no_swap = calc_rgb_uv_swap( |
| caps_no_ee[0]['data'], caps[0]['data'], edge_mode, |
| name_with_log_path, reprocess_format, 'ee') |
| uv_swaps[reprocess_format][edge_mode] = (sad_swap, sad_no_swap) |
| |
| # Fail all instances where swapping the U and V channels results in an |
| # image which is closer to the non-reprocessed capture. |
| num_fail = 0 |
| num_tests = 0 |
| for reprocess_format, edge_mode_dict in uv_swaps.items(): |
| for edge_mode, (sad_swap, sad_no_swap) in edge_mode_dict.items(): |
| num_tests += 1 |
| if sad_swap < sad_no_swap: |
| num_fail += 1 |
| logging.error('REPROCESS_FMT: %s, ' |
| 'EDGE_MODE: %d, ' |
| 'SAD_SWAP: %.2f, ' |
| 'SAD_NO_SWAP: %.2f', |
| reprocess_format, edge_mode, sad_swap, sad_no_swap) |
| |
| if num_fail > 0: |
| raise AssertionError(f'Number of fails: {num_fail} / {num_tests}') |
| |
| def test_reprocess_jpeg(self): |
| logging.debug('Starting %s:test_reprocess_jpeg', _NAME) |
| |
| with its_session_utils.ItsSession( |
| device_id=self.dut.serial, |
| camera_id=self.camera_id, |
| hidden_physical_id=self.hidden_physical_id) as cam: |
| props = cam.get_camera_properties() |
| props = cam.override_with_hidden_physical_camera_props(props) |
| camera_properties_utils.skip_unless( |
| camera_properties_utils.per_frame_control(props) and |
| camera_properties_utils.jpeg_orientation(props) and |
| (camera_properties_utils.yuv_reprocess(props) or |
| camera_properties_utils.private_reprocess(props))) |
| log_path = self.log_path |
| name_with_log_path = os.path.join(log_path, _NAME) |
| applied_orientation = 90 |
| |
| # Load chart for scene |
| its_session_utils.load_scene( |
| cam, props, self.scene, self.tablet, self.chart_distance) |
| |
| reprocess_formats = camera_properties_utils.get_reprocess_formats(props) |
| |
| # Capture for each available reprocess format |
| uv_swaps = {} |
| size = capture_request_utils.get_available_output_sizes('jpg', props)[0] |
| out_surface = {'width': size[0], 'height': size[1], 'format': 'jpg'} |
| for reprocess_format in reprocess_formats: |
| logging.debug('Reprocess format: %s', reprocess_format) |
| |
| # Create req, do caps and determine UV swap likelihood |
| req = capture_request_utils.auto_capture_request() |
| req['android.jpeg.orientation'] = applied_orientation |
| cam.do_3a() |
| caps_no_jpeg = cam.do_capture([req], out_surface) |
| caps = cam.do_capture([req], out_surface, reprocess_format) |
| |
| sad_swap, sad_no_swap = calc_rgb_uv_swap( |
| caps_no_jpeg[0]['data'], caps[0]['data'], applied_orientation, |
| name_with_log_path, reprocess_format, 'orientation') |
| uv_swaps[reprocess_format] = (sad_swap, sad_no_swap) |
| |
| # Fail all instances where swapping the U and V channels results in an |
| # image which is closer to the non-reprocessed capture. |
| num_fail = 0 |
| num_tests = 0 |
| for reprocess_format, (sad_swap, sad_no_swap) in uv_swaps.items(): |
| num_tests += 1 |
| if sad_swap < sad_no_swap: |
| num_fail += 1 |
| logging.error('REPROCESS_FMT: %s, ' |
| 'ORIENTATION: %d, ' |
| 'SAD_SWAP: %.2f, ' |
| 'SAD_NO_SWAP: %.2f', |
| reprocess_format, applied_orientation, sad_swap, |
| sad_no_swap) |
| |
| if num_fail > 0: |
| raise AssertionError(f'Number of fails: {num_fail} / {num_tests}') |
| |
| if __name__ == '__main__': |
| test_runner.main() |
| |