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