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"""Verify zoom ratio scales circle sizes correctly if settings override zoom is set.""" 15 16 17import logging 18import math 19import os.path 20 21import its_base_test 22import camera_properties_utils 23import capture_request_utils 24import image_processing_utils 25import its_session_utils 26import opencv_processing_utils 27import zoom_capture_utils 28import cv2 29from mobly import test_runner 30import numpy as np 31 32 33_CONTINUOUS_PICTURE_MODE = 4 # continuous picture AF mode 34_NAME = os.path.splitext(os.path.basename(__file__))[0] 35_NUM_STEPS = 10 36_SMOOTH_ZOOM_STEP = 1.1 # [1.0, 1.1] as a reference smooth zoom step 37 38 39class LowLatencyZoomTest(its_base_test.ItsBaseTest): 40 """Test the camera low latency zoom behavior. 41 42 On supported devices, set control.settingsOverride to ZOOM 43 to enable low latency zoom and do a burst capture of N frames. 44 45 Make sure that the zoomRatio in the capture result is reflected 46 in the captured image. 47 48 If the device's firstApiLevel is V, make sure the zoom steps are 49 small and logarithmic to simulate a smooth zoom experience. 50 """ 51 52 def test_low_latency_zoom(self): 53 with its_session_utils.ItsSession( 54 device_id=self.dut.serial, 55 camera_id=self.camera_id, 56 hidden_physical_id=self.hidden_physical_id) as cam: 57 props = cam.get_camera_properties() 58 props = cam.override_with_hidden_physical_camera_props(props) 59 camera_properties_utils.skip_unless( 60 camera_properties_utils.zoom_ratio_range(props) and 61 camera_properties_utils.low_latency_zoom(props)) 62 63 # Load chart for scene 64 its_session_utils.load_scene( 65 cam, props, self.scene, self.tablet, self.chart_distance) 66 67 # Determine test zoom range 68 z_range = props['android.control.zoomRatioRange'] 69 debug = self.debug_mode 70 z_min, z_max = float(z_range[0]), float(z_range[1]) 71 camera_properties_utils.skip_unless( 72 z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH) 73 z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min) 74 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 75 if first_api_level <= its_session_utils.ANDROID14_API_LEVEL: 76 z_list = np.arange(z_min, z_max, (z_max - z_min) / (_NUM_STEPS - 1)) 77 else: 78 # Here since we're trying to follow a log scale for moving through 79 # zoom steps from min to max we determine smooth_zoom_num_steps from 80 # the following: z_min*(SMOOTH_ZOOM_STEP^x) = z_max. If we solve for 81 # x, we get the equation below. As an example, if z_min was 1.0 82 # and z_max was 5.0, we would go through our list of zooms tested 83 # [1.0, 1.1, 1.21, 1.331...] 84 smooth_zoom_num_steps = ( 85 (math.log(z_max) - math.log(z_min)) / math.log(_SMOOTH_ZOOM_STEP)) 86 z_list_logarithmic = np.arange( 87 math.log(z_min), math.log(z_max), 88 (math.log(z_max) - math.log(z_min)) / smooth_zoom_num_steps 89 ) 90 z_list = [math.exp(z) for z in z_list_logarithmic] 91 z_list = np.append(z_list, z_max) 92 logging.debug('Testing zoom range: %s', str(z_list)) 93 94 # set TOLs based on camera and test rig params 95 if camera_properties_utils.logical_multi_camera(props): 96 test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size( 97 cam, props, self.chart_distance, debug) 98 else: 99 test_tols = {} 100 fls = props['android.lens.info.availableFocalLengths'] 101 for fl in fls: 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 auto captures over zoom range and find ArUco markers with cv2 110 img_name_stem = f'{os.path.join(self.log_path, _NAME)}' 111 logging.debug('Using auto capture request') 112 fmt = 'yuv' 113 cam.do_3a( 114 zoom_ratio=z_min, 115 out_surfaces={ 116 'format': fmt, 117 'width': size[0], 118 'height': size[1] 119 }, 120 repeat_request=None, 121 ) 122 test_failed = False 123 test_data = [] 124 all_aruco_ids = [] 125 all_aruco_corners = [] 126 images = [] 127 reqs = [] 128 req = capture_request_utils.auto_capture_request() 129 req['android.control.settingsOverride'] = ( 130 camera_properties_utils.SETTINGS_OVERRIDE_ZOOM 131 ) 132 req['android.control.enableZsl'] = False 133 if not camera_properties_utils.fixed_focus(props): 134 req['android.control.afMode'] = _CONTINUOUS_PICTURE_MODE 135 for z in z_list: 136 logging.debug('zoom ratio: %.2f', z) 137 req_for_zoom = req.copy() 138 req_for_zoom['android.control.zoomRatio'] = z 139 reqs.append(req_for_zoom) 140 141 # take captures at different zoom ratios 142 caps = cam.do_capture( 143 reqs, {'format': fmt, 'width': size[0], 'height': size[1]}, 144 reuse_session=True) 145 146 # Check low latency zoom outputs match result metadata 147 for i, cap in enumerate(caps): 148 z_result = cap['metadata']['android.control.zoomRatio'] 149 af_state = cap['metadata']['android.control.afState'] 150 logging.debug('Result[%d]: zoom ratio %.2f, afState %d', 151 i, z_result, af_state) 152 img = image_processing_utils.convert_capture_to_rgb_image( 153 cap, props=props) 154 img_name = f'{img_name_stem}_{fmt}_{i}_{round(z_result, 2)}.jpg' 155 image_processing_utils.write_image(img, img_name) 156 img_bgr = cv2.cvtColor( 157 image_processing_utils.convert_image_to_uint8(img), 158 cv2.COLOR_RGB2BGR) 159 160 # determine radius tolerance of capture 161 cap_fl = cap['metadata']['android.lens.focalLength'] 162 cap_physical_id = ( 163 cap['metadata']['android.logicalMultiCamera.activePhysicalId'] 164 ) 165 radius_tol, offset_tol = test_tols[cap_fl] 166 167 # Find the center ArUco marker in img and check if it's cropped 168 corners, ids, _ = opencv_processing_utils.find_aruco_markers( 169 img_bgr, 170 f'{img_name_stem}_{fmt}_{i}_{z_result:.2f}_ArUco.jpg', 171 aruco_marker_count=1, 172 force_greyscale=True, 173 ) 174 175 all_aruco_corners.append([corner[0] for corner in corners]) 176 all_aruco_ids.append([id[0] for id in ids]) 177 images.append(img_bgr) 178 test_data.append( 179 zoom_capture_utils.ZoomTestData( 180 result_zoom=z_result, 181 radius_tol=radius_tol, 182 offset_tol=offset_tol, 183 focal_length=cap_fl, 184 physical_id=cap_physical_id, 185 ) 186 ) 187 188 # Find ArUco markers in all captures and update test data 189 zoom_capture_utils.update_zoom_test_data_with_shared_aruco_marker( 190 test_data, all_aruco_ids, all_aruco_corners, size) 191 # Mark ArUco marker center and image center 192 opencv_processing_utils.mark_zoom_images( 193 images, test_data, f'{img_name_stem}_{fmt}') 194 # Since we are zooming in, settings_override may change the minimum zoom 195 # value in the result metadata. 196 # This is because zoom values like: [1., 2., 3., ..., 10.] may be applied 197 # as: [4., 4., 4., .... 9., 10., 10.]. 198 # If we were zooming out, we would need to change the z_max. 199 z_min = test_data[0].result_zoom 200 201 if not zoom_capture_utils.verify_zoom_results( 202 test_data, size, z_max, z_min): 203 test_failed = True 204 205 if test_failed: 206 raise AssertionError(f'{_NAME} failed! Check test_log.DEBUG for errors') 207 208if __name__ == '__main__': 209 test_runner.main() 210