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"""Test the camera in-sensor zoom behavior.""" 15 16import logging 17import os.path 18 19import camera_properties_utils 20import capture_request_utils 21import cv2 22import image_processing_utils 23import its_base_test 24import its_session_utils 25import opencv_processing_utils 26import zoom_capture_utils 27 28from mobly import test_runner 29import numpy as np 30 31_NAME = os.path.splitext(os.path.basename(__file__))[0] 32_NUM_STEPS = 10 33_THRESHOLD_MAX_RMS_DIFF_CROPPED_RAW_USE_CASE = 0.06 34 35 36class InSensorZoomTest(its_base_test.ItsBaseTest): 37 38 """Use case CROPPED_RAW: verify that CaptureResult.RAW_CROP_REGION matches cropped RAW image.""" 39 40 def test_in_sensor_zoom(self): 41 with its_session_utils.ItsSession( 42 device_id=self.dut.serial, 43 camera_id=self.camera_id, 44 hidden_physical_id=self.hidden_physical_id) as cam: 45 logical_props = cam.get_camera_properties() 46 props = cam.override_with_hidden_physical_camera_props(logical_props) 47 name_with_log_path = os.path.join(self.log_path, _NAME) 48 49 # Skip the test if CROPPED_RAW is not present in stream use cases 50 camera_properties_utils.skip_unless( 51 camera_properties_utils.cropped_raw_stream_use_case(props)) 52 53 # Load chart for scene 54 its_session_utils.load_scene( 55 cam, props, self.scene, self.tablet, self.chart_distance) 56 57 z_range = props['android.control.zoomRatioRange'] 58 logging.debug('In sensor zoom: testing zoomRatioRange: %s', str(z_range)) 59 60 z_min, z_max = float(z_range[0]), float(z_range[1]) 61 camera_properties_utils.skip_unless( 62 z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH) 63 z_list = np.arange(z_min, z_max, float(z_max - z_min) / (_NUM_STEPS - 1)) 64 z_list = np.append(z_list, z_max) 65 66 a = props['android.sensor.info.activeArraySize'] 67 aw, ah = a['right'] - a['left'], a['bottom'] - a['top'] 68 69 # Capture a RAW frame without any zoom 70 raw_size = capture_request_utils.get_available_output_sizes( 71 'raw', props)[0] 72 output_surfaces = [{'format': 'raw', 73 'width': raw_size[0], 74 'height': raw_size[1]}] 75 if self.hidden_physical_id: 76 output_surfaces[0].update({'physicalCamera': self.hidden_physical_id}) 77 imgs = {} 78 cam.do_3a(out_surfaces=output_surfaces) 79 req = capture_request_utils.auto_capture_request() 80 req['android.statistics.lensShadingMapMode'] = ( 81 image_processing_utils.LENS_SHADING_MAP_ON) 82 cap_raw_full = cam.do_capture( 83 req, 84 output_surfaces, 85 reuse_session=True) 86 rgb_full_img = image_processing_utils.convert_raw_capture_to_rgb_image( 87 cap_raw_full, props, 'raw', name_with_log_path) 88 image_processing_utils.write_image( 89 rgb_full_img, f'{name_with_log_path}_raw_full.jpg') 90 imgs['raw_full'] = rgb_full_img 91 output_surfaces[0].update( 92 {'useCase': its_session_utils.USE_CASE_CROPPED_RAW} 93 ) 94 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 95 reuse_session = False 96 # Capture RAW images with different zoom ratios with stream use case 97 # CROPPED_RAW set 98 for _, z in enumerate(z_list): 99 req['android.control.zoomRatio'] = z 100 if first_api_level >= its_session_utils.ANDROID15_API_LEVEL: 101 cam.do_3a(out_surfaces=output_surfaces) 102 reuse_session = True 103 cap_zoomed_raw = cam.do_capture( 104 req, 105 output_surfaces, 106 reuse_session=reuse_session) 107 rgb_zoomed_raw = ( 108 image_processing_utils.convert_raw_capture_to_rgb_image( 109 cap_zoomed_raw, props, 'raw', name_with_log_path)) 110 # Dump zoomed in RAW image 111 img_name = f'{name_with_log_path}_zoomed_raw_{z:.2f}.jpg' 112 image_processing_utils.write_image(rgb_zoomed_raw, img_name) 113 logging.debug('Finding ArUco markers for zoom %f: size [%d x %d],' 114 ' (min zoom %f)', z, cap_zoomed_raw['width'], 115 cap_zoomed_raw['height'], z_list[0]) 116 meta = cap_zoomed_raw['metadata'] 117 result_raw_crop_region = meta['android.scaler.rawCropRegion'] 118 rl = result_raw_crop_region['left'] 119 rt = result_raw_crop_region['top'] 120 # Make sure that scale factor for width and height scaling is the same. 121 rw = result_raw_crop_region['right'] - rl 122 rh = result_raw_crop_region['bottom'] - rt 123 logging.debug('RAW_CROP_REGION reported for zoom %f: [%d %d %d %d]', 124 z, rl, rt, rw, rh) 125 inv_scale_factor = rw / aw 126 if aw / rw != ah / rh: 127 raise AssertionError('RAW_CROP_REGION width and height aspect ratio' 128 f' != active array AR, region size: {rw} x {rh}' 129 f' active array size: {aw} x {ah}') 130 # Find any ArUco marker in img 131 try: 132 opencv_processing_utils.find_aruco_markers( 133 image_processing_utils.convert_image_to_uint8(rgb_zoomed_raw), 134 f'{name_with_log_path}_zoomed_raw_{z:.2f}_ArUco.jpg', 135 aruco_marker_count=1 136 ) 137 except AssertionError as e: 138 logging.debug('Could not find ArUco marker at zoom ratio %.2f: %s', 139 z, e) 140 break 141 142 xnorm = rl / aw 143 ynorm = rt / ah 144 wnorm = rw / aw 145 hnorm = rh / ah 146 logging.debug('Image patch norm for zoom %.2f: [%.2f %.2f %.2f %.2f]', 147 z, xnorm, ynorm, wnorm, hnorm) 148 # Crop the full FoV RAW to result_raw_crop_region 149 rgb_full_cropped = image_processing_utils.get_image_patch( 150 rgb_full_img, xnorm, ynorm, wnorm, hnorm) 151 152 # Downscale the zoomed-in RAW image returned by the camera sub-system 153 rgb_zoomed_downscale = cv2.resize( 154 rgb_zoomed_raw, None, fx=inv_scale_factor, fy=inv_scale_factor) 155 156 # Debug dump images being rms compared 157 img_name_downscaled = f'{name_with_log_path}_downscale_raw_{z:.2f}.jpg' 158 image_processing_utils.write_image( 159 rgb_zoomed_downscale, img_name_downscaled) 160 161 img_name_cropped = f'{name_with_log_path}_full_cropped_raw_{z:.2f}.jpg' 162 image_processing_utils.write_image(rgb_full_cropped, img_name_cropped) 163 164 rms_diff = image_processing_utils.compute_image_rms_difference_3d( 165 rgb_zoomed_downscale, rgb_full_cropped) 166 msg = f'RMS diff for CROPPED_RAW use case: {rms_diff:.4f}' 167 logging.debug('%s', msg) 168 if rms_diff >= _THRESHOLD_MAX_RMS_DIFF_CROPPED_RAW_USE_CASE: 169 raise AssertionError( 170 f'RMS diff {rms_diff:.4f} of downscaled cropped RAW & full, ' 171 f'ATOL: {_THRESHOLD_MAX_RMS_DIFF_CROPPED_RAW_USE_CASE}') 172 173 174if __name__ == '__main__': 175 test_runner.main() 176