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_NUM_STEPS = 10 31_ZOOM_MIN_THRESH = 2.0 32_THRESHOLD_MAX_RMS_DIFF_CROPPED_RAW_USE_CASE = 0.03 33_NAME = os.path.splitext(os.path.basename(__file__))[0] 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 props = cam.get_camera_properties() 46 props = cam.override_with_hidden_physical_camera_props(props) 47 name_with_log_path = os.path.join(self.log_path, _NAME) 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(z_max >= z_min * _ZOOM_MIN_THRESH) 61 z_list = np.arange(z_min, z_max, float(z_max - z_min) / (_NUM_STEPS - 1)) 62 z_list = np.append(z_list, z_max) 63 64 a = props['android.sensor.info.activeArraySize'] 65 aw, ah = a['right'] - a['left'], a['bottom'] - a['top'] 66 67 # Capture a RAW frame without any zoom 68 imgs = {} 69 cam.do_3a() 70 req = capture_request_utils.auto_capture_request() 71 cap_raw_full = cam.do_capture(req, cam.CAP_RAW) 72 rgb_full_img = image_processing_utils.convert_capture_to_rgb_image( 73 cap_raw_full, props=props) 74 image_processing_utils.write_image( 75 rgb_full_img, f'{name_with_log_path}_raw_full.jpg') 76 imgs['raw_full'] = rgb_full_img 77 78 # Capture RAW images with different zoom ratios with stream use case 79 # CROPPED_RAW set 80 for _, z in enumerate(z_list): 81 req['android.control.zoomRatio'] = z 82 cap_zoomed_raw = cam.do_capture(req, cam.CAP_CROPPED_RAW) 83 rgb_zoomed_raw = image_processing_utils.convert_capture_to_rgb_image( 84 cap_zoomed_raw, props=props) 85 # Dump zoomed in RAW image 86 img_name = f'{name_with_log_path}_zoomed_raw_{z:.2f}.jpg' 87 image_processing_utils.write_image(rgb_zoomed_raw, img_name) 88 size_raw = [cap_zoomed_raw['width'], cap_zoomed_raw['height']] 89 logging.debug('Finding center circle for zoom %f: size [%d x %d],' 90 ' (min zoom %f)', z, cap_zoomed_raw['width'], 91 cap_zoomed_raw['height'], z_list[0]) 92 meta = cap_zoomed_raw['metadata'] 93 result_raw_crop_region = meta['android.scaler.rawCropRegion'] 94 rl = result_raw_crop_region['left'] 95 rt = result_raw_crop_region['top'] 96 # Make sure that scale factor for width and height scaling is the same. 97 rw = result_raw_crop_region['right'] - rl 98 rh = result_raw_crop_region['bottom'] - rt 99 logging.debug('RAW_CROP_REGION reported for zoom %f: [%d %d %d %d]', 100 z, rl, rt, rw, rh) 101 # Effective zoom ratio. May not be == z since its possible the HAL 102 # wasn't able to crop RAW. 103 effective_zoom_ratio = aw / rw 104 inv_scale_factor = rw / aw 105 if aw / rw != ah / rh: 106 raise AssertionError('RAW_CROP_REGION width and height aspect ratio' 107 f' != active array AR, region size: {rw} x {rh} ' 108 f' active array size: {aw} x {ah}') 109 # Find the center circle in img 110 circle = zoom_capture_utils.get_center_circle( 111 rgb_zoomed_raw, img_name, size_raw, effective_zoom_ratio, 112 z_list[0], debug=True) 113 # Zoom is too large to find center circle, break out 114 if circle is None: 115 break 116 117 xnorm = rl / aw 118 ynorm = rt / ah 119 wnorm = rw / aw 120 hnorm = rh / ah 121 logging.debug('Image patch norm for zoom %.2f: [%.2f %.2f %.2f %.2f]', 122 z, xnorm, ynorm, wnorm, hnorm) 123 # Crop the full FoV RAW to result_raw_crop_region 124 rgb_full_cropped = image_processing_utils.get_image_patch( 125 rgb_full_img, xnorm, ynorm, wnorm, hnorm) 126 127 # Downscale the zoomed-in RAW image returned by the camera sub-system 128 rgb_zoomed_downscale = cv2.resize( 129 rgb_zoomed_raw, None, fx=inv_scale_factor, fy=inv_scale_factor) 130 131 # Debug dump images being rms compared 132 img_name_downscaled = f'{name_with_log_path}_downscale_raw_{z:.2f}.jpg' 133 image_processing_utils.write_image( 134 rgb_zoomed_downscale, img_name_downscaled) 135 136 img_name_cropped = f'{name_with_log_path}_full_cropped_raw_{z:.2f}.jpg' 137 image_processing_utils.write_image(rgb_full_cropped, img_name_cropped) 138 139 rms_diff = image_processing_utils.compute_image_rms_difference_3d( 140 rgb_zoomed_downscale, rgb_full_cropped) 141 msg = f'RMS diff for CROPPED_RAW use case: {rms_diff:.4f}' 142 logging.debug('%s', msg) 143 if rms_diff >= _THRESHOLD_MAX_RMS_DIFF_CROPPED_RAW_USE_CASE: 144 raise AssertionError(f'{_NAME} failed! test_log.DEBUG has errors') 145 146 147if __name__ == '__main__': 148 test_runner.main() 149