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