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 os.path 19 20import camera_properties_utils 21import capture_request_utils 22import image_processing_utils 23import its_base_test 24import its_session_utils 25from mobly import test_runner 26import numpy as np 27import opencv_processing_utils 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_ZOOM_MIN_THRESH = 2.0 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 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(z_max >= z_min * _ZOOM_MIN_THRESH) 72 z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min) 73 z_list = np.arange(z_min, z_max, (z_max - z_min) / (_NUM_STEPS - 1)) 74 z_list = np.append(z_list, z_max) 75 logging.debug('Testing zoom range: %s', str(z_list)) 76 77 # set TOLs based on camera and test rig params 78 if camera_properties_utils.logical_multi_camera(props): 79 test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size( 80 cam, props, self.chart_distance, debug) 81 else: 82 test_tols = {} 83 fls = props['android.lens.info.availableFocalLengths'] 84 for fl in fls: 85 test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL, 86 zoom_capture_utils.OFFSET_RTOL) 87 yuv_size = capture_request_utils.get_largest_yuv_format(props) 88 size = [yuv_size['width'], yuv_size['height']] 89 logging.debug('capture size: %s', str(size)) 90 logging.debug('test TOLs: %s', str(test_tols)) 91 92 # do auto captures over zoom range and find circles with cv2 93 img_name_stem = f'{os.path.join(self.log_path, _NAME)}' 94 logging.debug('Using auto capture request') 95 cam.do_3a(zoom_ratio=z_min) 96 test_failed = False 97 fmt = 'yuv' 98 test_data = {} 99 reqs = [] 100 req = capture_request_utils.auto_capture_request() 101 req['android.control.settingsOverride'] = ( 102 camera_properties_utils.SETTINGS_OVERRIDE_ZOOM 103 ) 104 req['android.control.enableZsl'] = False 105 if not camera_properties_utils.fixed_focus(props): 106 req['android.control.afMode'] = _CONTINUOUS_PICTURE_MODE 107 for z in z_list: 108 logging.debug('zoom ratio: %.2f', z) 109 req_for_zoom = req.copy() 110 req_for_zoom['android.control.zoomRatio'] = z 111 reqs.append(req_for_zoom) 112 113 # take captures at different zoom ratios 114 caps = cam.do_capture( 115 reqs, {'format': fmt, 'width': size[0], 'height': size[1]}) 116 117 # Check low latency zoom outputs match result metadata 118 for i, cap in enumerate(caps): 119 z_result = cap['metadata']['android.control.zoomRatio'] 120 af_state = cap['metadata']['android.control.afState'] 121 scaled_zoom = min(z_list[i], z_result) 122 logging.debug('Result[%d]: zoom ratio %.2f, afState %d', 123 i, z_result, af_state) 124 img = image_processing_utils.convert_capture_to_rgb_image( 125 cap, props=props) 126 img_name = f'{img_name_stem}_{fmt}_{i}_{round(z_result, 2)}.jpg' 127 image_processing_utils.write_image(img, img_name) 128 129 # determine radius tolerance of capture 130 cap_fl = cap['metadata']['android.lens.focalLength'] 131 radius_tol, offset_tol = test_tols[cap_fl] 132 133 # convert [0, 1] image to [0, 255] and cast as uint8 134 img = image_processing_utils.convert_image_to_uint8(img) 135 136 # Find the center circle in img 137 try: 138 circle = opencv_processing_utils.find_center_circle( 139 img, img_name, _CIRCLE_COLOR, circle_ar_rtol=_CIRCLE_AR_RTOL, 140 circlish_rtol=_CIRCLISH_RTOL, 141 min_area=_MIN_AREA_RATIO*size[0]*size[1]*scaled_zoom*scaled_zoom, 142 min_circle_pts=_MIN_CIRCLE_PTS, debug=debug) 143 if opencv_processing_utils.is_circle_cropped(circle, size): 144 logging.debug('zoom %.2f is too large! Skip further captures', 145 z_result) 146 break 147 except AssertionError as e: 148 if z_result/z_list[0] >= zoom_capture_utils.ZOOM_MAX_THRESH: 149 break 150 else: 151 raise AssertionError( 152 'No circle detected for zoom ratio <= ' 153 f'{zoom_capture_utils.ZOOM_MAX_THRESH}. ' 154 'Take pictures according to instructions carefully!') from e 155 156 test_data[i] = {'z': z_result, 'circle': circle, 'r_tol': radius_tol, 157 'o_tol': offset_tol, 'fl': cap_fl} 158 159 # Since we are zooming in, settings_override may change the minimum zoom 160 # value in the result metadata. 161 # This is because zoom values like: [1., 2., 3., ..., 10.] may be applied 162 # as: [4., 4., 4., .... 9., 10., 10.]. 163 # If we were zooming out, we would need to change the z_max. 164 z_min = test_data[min(test_data.keys())]['z'] 165 166 if not zoom_capture_utils.verify_zoom_results( 167 test_data, size, z_max, z_min): 168 test_failed = True 169 170 if test_failed: 171 raise AssertionError(f'{_NAME} failed! Check test_log.DEBUG for errors') 172 173if __name__ == '__main__': 174 test_runner.main() 175