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 camera_properties_utils 22import capture_request_utils 23import image_processing_utils 24import its_base_test 25import its_session_utils 26from mobly import test_runner 27import numpy as np 28import zoom_capture_utils 29 30 31_CIRCLE_COLOR = 0 # [0: black, 255: white] 32_CIRCLE_AR_RTOL = 0.15 # contour width vs height (aspect ratio) 33_CIRCLISH_RTOL = 0.05 # contour area vs ideal circle area pi*((w+h)/4)**2 34_CONTINUOUS_PICTURE_MODE = 4 # continuous picture AF mode 35_MIN_AREA_RATIO = 0.00015 # based on 2000/(4000x3000) pixels 36_MIN_CIRCLE_PTS = 25 37_NAME = os.path.splitext(os.path.basename(__file__))[0] 38_NUM_STEPS = 10 39_SMOOTH_ZOOM_STEP = 1.1 # [1.0, 1.1] as a reference smooth zoom step 40 41 42class LowLatencyZoomTest(its_base_test.ItsBaseTest): 43 """Test the camera low latency zoom behavior. 44 45 On supported devices, set control.settingsOverride to ZOOM 46 to enable low latency zoom and do a burst capture of N frames. 47 48 Make sure that the zoomRatio in the capture result is reflected 49 in the captured image. 50 51 If the device's firstApiLevel is V, make sure the zoom steps are 52 small and logarithmic to simulate a smooth zoom experience. 53 """ 54 55 def test_low_latency_zoom(self): 56 with its_session_utils.ItsSession( 57 device_id=self.dut.serial, 58 camera_id=self.camera_id, 59 hidden_physical_id=self.hidden_physical_id) as cam: 60 props = cam.get_camera_properties() 61 props = cam.override_with_hidden_physical_camera_props(props) 62 camera_properties_utils.skip_unless( 63 camera_properties_utils.zoom_ratio_range(props) and 64 camera_properties_utils.low_latency_zoom(props)) 65 66 # Load chart for scene 67 its_session_utils.load_scene( 68 cam, props, self.scene, self.tablet, self.chart_distance) 69 70 # Determine test zoom range 71 z_range = props['android.control.zoomRatioRange'] 72 debug = self.debug_mode 73 z_min, z_max = float(z_range[0]), float(z_range[1]) 74 camera_properties_utils.skip_unless( 75 z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH) 76 z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min) 77 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 78 if first_api_level <= its_session_utils.ANDROID14_API_LEVEL: 79 z_list = np.arange(z_min, z_max, (z_max - z_min) / (_NUM_STEPS - 1)) 80 else: 81 # Here since we're trying to follow a log scale for moving through 82 # zoom steps from min to max we determine smooth_zoom_num_steps from 83 # the following: z_min*(SMOOTH_ZOOM_STEP^x) = z_max. If we solve for 84 # x, we get the equation below. As an example, if z_min was 1.0 85 # and z_max was 5.0, we would go through our list of zooms tested 86 # [1.0, 1.1, 1.21, 1.331...] 87 smooth_zoom_num_steps = ( 88 (math.log(z_max) - math.log(z_min)) / math.log(_SMOOTH_ZOOM_STEP)) 89 z_list_logarithmic = np.arange( 90 math.log(z_min), math.log(z_max), 91 (math.log(z_max) - math.log(z_min)) / smooth_zoom_num_steps 92 ) 93 z_list = [math.exp(z) for z in z_list_logarithmic] 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 fls = props['android.lens.info.availableFocalLengths'] 104 for fl in fls: 105 test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL, 106 zoom_capture_utils.OFFSET_RTOL) 107 yuv_size = capture_request_utils.get_largest_yuv_format(props) 108 size = [yuv_size['width'], yuv_size['height']] 109 logging.debug('capture size: %s', str(size)) 110 logging.debug('test TOLs: %s', str(test_tols)) 111 112 # do auto captures over zoom range and find circles with cv2 113 img_name_stem = f'{os.path.join(self.log_path, _NAME)}' 114 logging.debug('Using auto capture request') 115 fmt = 'yuv' 116 cam.do_3a( 117 zoom_ratio=z_min, 118 out_surfaces={ 119 'format': fmt, 120 'width': size[0], 121 'height': size[1] 122 }, 123 repeat_request=None, 124 ) 125 test_failed = False 126 test_data = [] 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 scaled_zoom = min(z_list[i], z_result) 151 logging.debug('Result[%d]: zoom ratio %.2f, afState %d', 152 i, z_result, af_state) 153 img = image_processing_utils.convert_capture_to_rgb_image( 154 cap, props=props) 155 img_name = f'{img_name_stem}_{fmt}_{i}_{round(z_result, 2)}.jpg' 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[cap_fl] 161 162 # Find the center circle in img and check if it's cropped 163 circle = zoom_capture_utils.find_center_circle( 164 img, img_name, size, scaled_zoom, z_min, debug=debug) 165 166 test_data.append( 167 zoom_capture_utils.ZoomTestData( 168 result_zoom=z_result, 169 circle=circle, 170 radius_tol=radius_tol, 171 offset_tol=offset_tol, 172 focal_length=cap_fl 173 ) 174 ) 175 176 # Since we are zooming in, settings_override may change the minimum zoom 177 # value in the result metadata. 178 # This is because zoom values like: [1., 2., 3., ..., 10.] may be applied 179 # as: [4., 4., 4., .... 9., 10., 10.]. 180 # If we were zooming out, we would need to change the z_max. 181 z_min = test_data[0].result_zoom 182 183 if not zoom_capture_utils.verify_zoom_results( 184 test_data, size, z_max, z_min): 185 test_failed = True 186 187 if test_failed: 188 raise AssertionError(f'{_NAME} failed! Check test_log.DEBUG for errors') 189 190if __name__ == '__main__': 191 test_runner.main() 192