1# Copyright 2024 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"""Verify zoom ratio scales ArUco marker sizes correctly for the TELE camera.""" 15 16 17import logging 18import math 19import os.path 20 21import camera_properties_utils 22import capture_request_utils 23import image_processing_utils 24import opencv_processing_utils 25import its_base_test 26import its_session_utils 27import cv2 28from mobly import test_runner 29import numpy as np 30import zoom_capture_utils 31 32_NAME = os.path.splitext(os.path.basename(__file__))[0] 33_NUMBER_OF_CAMERAS_TO_TEST = 2 # WIDE and TELE 34_NUM_STEPS_PER_SECTION = 10 35# YUV only to improve marker detection, JPEG is tested in test_zoom 36_TEST_FORMATS = ('yuv',) 37# Empirically found zoom ratio for main cameras without custom offset behavior 38_WIDE_ZOOM_RATIO_MIN = 2.2 39# Empirically found zoom ratio for TELE cameras 40_TELE_TRANSITION_ZOOM_RATIO = 5.0 41_ZOOM_RATIO_REQUEST_RESULT_DIFF_RTOL = 0.1 42 43 44class ZoomTestTELE(its_base_test.ItsBaseTest): 45 """Test the camera zoom behavior for the TELE camera, if available.""" 46 47 def test_zoom_tele(self): 48 with its_session_utils.ItsSession( 49 device_id=self.dut.serial, 50 camera_id=self.camera_id, 51 # Use logical camera for captures. Physical ID only for result tracking 52 hidden_physical_id=None) as cam: 53 camera_properties_utils.skip_unless(self.hidden_physical_id is not None) 54 props = cam.get_camera_properties() 55 physical_props = cam.get_camera_properties_by_id(self.hidden_physical_id) 56 physical_fov = float(cam.calc_camera_fov(physical_props)) 57 is_tele = physical_fov < opencv_processing_utils.FOV_THRESH_TELE 58 camera_properties_utils.skip_unless( 59 camera_properties_utils.zoom_ratio_range(props) and 60 is_tele) 61 62 # Load chart for scene 63 its_session_utils.load_scene( 64 cam, props, self.scene, self.tablet, 65 # Ensure markers are large enough by loading unscaled chart 66 its_session_utils.CHART_DISTANCE_NO_SCALING) 67 68 # Determine test zoom range 69 z_range = props['android.control.zoomRatioRange'] 70 debug = self.debug_mode 71 z_min, z_max = float(z_range[0]), float(z_range[1]) 72 camera_properties_utils.skip_unless( 73 z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH) 74 z_min = max(z_min, _WIDE_ZOOM_RATIO_MIN) # Force W 75 tele_transition_zoom_ratio = min(z_max, _TELE_TRANSITION_ZOOM_RATIO) 76 # Increase data near transition ratio 77 transition_z_list = np.arange( 78 z_min, 79 tele_transition_zoom_ratio, 80 (tele_transition_zoom_ratio - z_min) / (_NUM_STEPS_PER_SECTION - 1) 81 ) 82 tele_z_list = np.array([]) 83 if z_max > tele_transition_zoom_ratio: 84 tele_z_list = np.arange( 85 tele_transition_zoom_ratio, 86 z_max, 87 (z_max - tele_transition_zoom_ratio) / (_NUM_STEPS_PER_SECTION - 1) 88 ) 89 z_list = np.unique(np.concatenate((transition_z_list, tele_z_list))) 90 z_list = np.append(z_list, z_max) 91 logging.debug('Testing zoom range: %s', str(z_list)) 92 93 # Set TOLs based on camera and test rig params 94 if camera_properties_utils.logical_multi_camera(props): 95 test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size( 96 cam, props, self.chart_distance, debug) 97 else: 98 test_tols = {} 99 focal_lengths = props['android.lens.info.availableFocalLengths'] 100 logging.debug('focal lengths: %s', focal_lengths) 101 for fl in focal_lengths: 102 test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL, 103 zoom_capture_utils.OFFSET_RTOL) 104 yuv_size = capture_request_utils.get_largest_format('yuv', props) 105 size = [yuv_size['width'], yuv_size['height']] 106 logging.debug('capture size: %s', str(size)) 107 logging.debug('test TOLs: %s', str(test_tols)) 108 109 # Do captures over zoom range and find ArUco markers with cv2 110 img_name_stem = f'{os.path.join(self.log_path, _NAME)}' 111 req = capture_request_utils.auto_capture_request() 112 test_failed = False 113 114 for fmt in _TEST_FORMATS: 115 logging.debug('testing %s format', fmt) 116 test_data = [] 117 all_aruco_ids = [] 118 all_aruco_corners = [] 119 images = [] 120 found_markers = False 121 for z in z_list: 122 req['android.control.zoomRatio'] = z 123 logging.debug('zoom ratio: %.3f', z) 124 cam.do_3a( 125 zoom_ratio=z, 126 out_surfaces={ 127 'format': fmt, 128 'width': size[0], 129 'height': size[1] 130 }, 131 repeat_request=None, 132 ) 133 cap = cam.do_capture( 134 req, {'format': fmt, 'width': size[0], 'height': size[1]}, 135 reuse_session=True) 136 cap_physical_id = ( 137 cap['metadata']['android.logicalMultiCamera.activePhysicalId'] 138 ) 139 cap_zoom_ratio = float(cap['metadata']['android.control.zoomRatio']) 140 if not math.isclose(cap_zoom_ratio, z, 141 rel_tol=_ZOOM_RATIO_REQUEST_RESULT_DIFF_RTOL): 142 raise AssertionError( 143 'Request and result zoom ratios too different! ' 144 f'Request zoom ratio: {z}. ' 145 f'Result zoom ratio: {cap_zoom_ratio}. ', 146 f'RTOL: {_ZOOM_RATIO_REQUEST_RESULT_DIFF_RTOL}' 147 ) 148 img = image_processing_utils.convert_capture_to_rgb_image( 149 cap, props=props) 150 img_name = (f'{img_name_stem}_{fmt}_{z:.2f}.' 151 f'{zoom_capture_utils.JPEG_STR}') 152 image_processing_utils.write_image(img, img_name) 153 154 # Determine radius tolerance of capture 155 cap_fl = cap['metadata']['android.lens.focalLength'] 156 radius_tol, offset_tol = test_tols.get( 157 cap_fl, 158 (zoom_capture_utils.RADIUS_RTOL, zoom_capture_utils.OFFSET_RTOL) 159 ) 160 161 # Find ArUco markers 162 bgr_img = cv2.cvtColor( 163 image_processing_utils.convert_image_to_uint8(img), 164 cv2.COLOR_RGB2BGR 165 ) 166 try: 167 corners, ids, _ = opencv_processing_utils.find_aruco_markers( 168 bgr_img, 169 (f'{img_name_stem}_{fmt}_{z:.2f}_' 170 f'ArUco.{zoom_capture_utils.JPEG_STR}'), 171 aruco_marker_count=1, 172 force_greyscale=True # Maximize number of markers detected 173 ) 174 found_markers = True 175 except AssertionError as e: 176 logging.debug('Could not find ArUco marker at zoom ratio %.2f: %s', 177 z, e) 178 if found_markers: 179 logging.debug('No more ArUco markers found at zoom %.2f', z) 180 break 181 else: 182 logging.debug('Still no ArUco markers found at zoom %.2f', z) 183 continue 184 all_aruco_corners.append([corner[0] for corner in corners]) 185 all_aruco_ids.append([id[0] for id in ids]) 186 images.append(bgr_img) 187 188 test_data.append( 189 zoom_capture_utils.ZoomTestData( 190 result_zoom=cap_zoom_ratio, 191 radius_tol=radius_tol, 192 offset_tol=offset_tol, 193 focal_length=cap_fl, 194 physical_id=cap_physical_id, 195 ) 196 ) 197 198 # Find ArUco markers in all captures and update test data 199 zoom_capture_utils.update_zoom_test_data_with_shared_aruco_marker( 200 test_data, all_aruco_ids, all_aruco_corners, size) 201 test_artifacts_name_stem = f'{img_name_stem}_{fmt}' 202 # Mark ArUco marker center and image center 203 opencv_processing_utils.mark_zoom_images( 204 images, test_data, test_artifacts_name_stem) 205 206 if not zoom_capture_utils.verify_zoom_data( 207 test_data, size, 208 offset_plot_name_stem=test_artifacts_name_stem, 209 number_of_cameras_to_test=_NUMBER_OF_CAMERAS_TO_TEST): 210 test_failed = True 211 212 if test_failed: 213 raise AssertionError(f'{_NAME} failed! Check test_log.DEBUG for errors') 214 215if __name__ == '__main__': 216 test_runner.main() 217