1# Copyright 2020 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.""" 15 16 17import logging 18import math 19import os.path 20 21import camera_properties_utils 22import capture_request_utils 23import image_processing_utils 24import its_base_test 25import its_session_utils 26import opencv_processing_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_NUM_STEPS = 10 34_TEST_FORMATS = ['yuv'] # list so can be appended for newer Android versions 35_TEST_REQUIRED_MPC = 33 36_SINGLE_CAMERA_NUMBER_OF_CAMERAS_TO_TEST = 1 37_ULTRAWIDE_NUMBER_OF_CAMERAS_TO_TEST = 2 # UW and W 38# Wider zoom ratio range will be tested by test_zoom_tele 39_WIDE_ZOOM_RATIO_MAX = 2.2 40_ZOOM_RATIO_REQUEST_RESULT_DIFF_RTOL = 0.1 41 42 43class ZoomTest(its_base_test.ItsBaseTest): 44 """Test the camera zoom behavior.""" 45 46 def test_zoom(self): 47 with its_session_utils.ItsSession( 48 device_id=self.dut.serial, 49 camera_id=self.camera_id, 50 hidden_physical_id=self.hidden_physical_id) as cam: 51 props = cam.get_camera_properties() 52 props = cam.override_with_hidden_physical_camera_props(props) 53 camera_properties_utils.skip_unless( 54 camera_properties_utils.zoom_ratio_range(props)) 55 56 # Load chart for scene 57 its_session_utils.load_scene( 58 cam, props, self.scene, self.tablet, self.chart_distance) 59 60 # Determine test zoom range 61 z_range = props['android.control.zoomRatioRange'] 62 debug = self.debug_mode 63 z_min, z_max = float(z_range[0]), float(z_range[1]) 64 camera_properties_utils.skip_unless( 65 z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH) 66 z_max = min(z_max, _WIDE_ZOOM_RATIO_MAX) 67 z_list = np.arange(z_min, z_max, (z_max - z_min) / (_NUM_STEPS - 1)) 68 z_list = np.append(z_list, z_max) 69 logging.debug('Testing zoom range: %s', str(z_list)) 70 71 # Check media performance class 72 media_performance_class = its_session_utils.get_media_performance_class( 73 self.dut.serial) 74 ultrawide_camera_found = cam.has_ultrawide_camera( 75 facing=props['android.lens.facing']) 76 if (media_performance_class >= _TEST_REQUIRED_MPC and 77 cam.is_primary_camera() and 78 ultrawide_camera_found and 79 int(z_min) >= 1): 80 raise AssertionError( 81 f'With primary camera {self.camera_id}, ' 82 f'MPC >= {_TEST_REQUIRED_MPC}, and ' 83 'an ultrawide camera facing in the same direction as the primary, ' 84 'zoom_ratio minimum must be less than 1.0. ' 85 f'Found media performance class {media_performance_class} ' 86 f'and minimum zoom {z_min}.') 87 88 # set TOLs based on camera and test rig params 89 if camera_properties_utils.logical_multi_camera(props): 90 test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size( 91 cam, props, self.chart_distance, debug) 92 else: 93 test_tols = {} 94 fls = props['android.lens.info.availableFocalLengths'] 95 for fl in fls: 96 test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL, 97 zoom_capture_utils.OFFSET_RTOL) 98 yuv_size = capture_request_utils.get_largest_format('yuv', props) 99 size = [yuv_size['width'], yuv_size['height']] 100 logging.debug('capture size: %s', str(size)) 101 logging.debug('test TOLs: %s', str(test_tols)) 102 103 # determine first API level and test_formats to test 104 test_formats = _TEST_FORMATS 105 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 106 if first_api_level >= its_session_utils.ANDROID14_API_LEVEL: 107 test_formats.append(zoom_capture_utils.JPEG_STR) 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 for fmt in test_formats: 114 logging.debug('testing %s format', fmt) 115 test_data = [] 116 all_aruco_ids = [] 117 all_aruco_corners = [] 118 images = [] 119 physical_ids = set() 120 for z in z_list: 121 req['android.control.zoomRatio'] = z 122 logging.debug('zoom ratio: %.3f', z) 123 cam.do_3a( 124 zoom_ratio=z, 125 out_surfaces={ 126 'format': fmt, 127 'width': size[0], 128 'height': size[1] 129 }, 130 repeat_request=None, 131 ) 132 cap = cam.do_capture( 133 req, {'format': fmt, 'width': size[0], 'height': size[1]}, 134 reuse_session=True) 135 cap_physical_id = ( 136 cap['metadata']['android.logicalMultiCamera.activePhysicalId'] 137 ) 138 cap_zoom_ratio = float(cap['metadata']['android.control.zoomRatio']) 139 if not math.isclose(cap_zoom_ratio, z, 140 rel_tol=_ZOOM_RATIO_REQUEST_RESULT_DIFF_RTOL): 141 raise AssertionError( 142 'Request and result zoom ratios too different! ' 143 f'Request zoom ratio: {z}. ' 144 f'Result zoom ratio: {cap_zoom_ratio}. ', 145 f'RTOL: {_ZOOM_RATIO_REQUEST_RESULT_DIFF_RTOL}' 146 ) 147 148 physical_ids.add(cap_physical_id) 149 logging.debug('Physical IDs: %s', physical_ids) 150 151 img = image_processing_utils.convert_capture_to_rgb_image( 152 cap, props=props) 153 img_name = (f'{img_name_stem}_{fmt}_{round(z, 2)}.' 154 f'{zoom_capture_utils.JPEG_STR}') 155 image_processing_utils.write_image(img, img_name) 156 157 # determine radius tolerance of capture 158 cap_fl = cap['metadata']['android.lens.focalLength'] 159 radius_tol, offset_tol = test_tols.get( 160 cap_fl, 161 (zoom_capture_utils.RADIUS_RTOL, zoom_capture_utils.OFFSET_RTOL) 162 ) 163 164 # Find ArUco markers 165 bgr_img = cv2.cvtColor( 166 image_processing_utils.convert_image_to_uint8(img), 167 cv2.COLOR_RGB2BGR 168 ) 169 try: 170 corners, ids, _ = opencv_processing_utils.find_aruco_markers( 171 bgr_img, 172 (f'{img_name_stem}_{fmt}_{z:.2f}_' 173 f'ArUco.{zoom_capture_utils.JPEG_STR}'), 174 aruco_marker_count=1, 175 force_greyscale=True # Maximize number of markers detected 176 ) 177 except AssertionError as e: 178 logging.debug('Could not find ArUco marker at zoom ratio %.2f: %s', 179 z, e) 180 break 181 all_aruco_corners.append([corner[0] for corner in corners]) 182 all_aruco_ids.append([id[0] for id in ids]) 183 images.append(bgr_img) 184 185 test_data.append( 186 zoom_capture_utils.ZoomTestData( 187 result_zoom=cap_zoom_ratio, 188 radius_tol=radius_tol, 189 offset_tol=offset_tol, 190 focal_length=cap_fl, 191 physical_id=cap_physical_id, 192 ) 193 ) 194 195 # Find ArUco markers in all captures and update test data 196 zoom_capture_utils.update_zoom_test_data_with_shared_aruco_marker( 197 test_data, all_aruco_ids, all_aruco_corners, size) 198 # Mark ArUco marker center and image center 199 opencv_processing_utils.mark_zoom_images( 200 images, test_data, f'{img_name_stem}_{fmt}') 201 202 number_of_cameras_to_test = ( 203 _ULTRAWIDE_NUMBER_OF_CAMERAS_TO_TEST 204 if ultrawide_camera_found 205 else _SINGLE_CAMERA_NUMBER_OF_CAMERAS_TO_TEST 206 ) 207 if not zoom_capture_utils.verify_zoom_data( 208 test_data, size, 209 offset_plot_name_stem=f'{img_name_stem}_{fmt}', 210 number_of_cameras_to_test=number_of_cameras_to_test): 211 test_failed = True 212 213 if test_failed: 214 raise AssertionError(f'{_NAME} failed! Check test_log.DEBUG for errors') 215 216if __name__ == '__main__': 217 test_runner.main() 218