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