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 session characteristics zoom.""" 15 16import logging 17import os 18 19import cv2 20from mobly import test_runner 21import numpy as np 22 23import its_base_test 24import camera_properties_utils 25import capture_request_utils 26import image_processing_utils 27import its_session_utils 28import opencv_processing_utils 29import zoom_capture_utils 30 31_FPS_30_60 = (30, 60) 32_FPS_SELECTION_ATOL = 0.01 33_FPS_ATOL = 0.8 34_MAX_FPS_INDEX = 1 35_MAX_STREAM_COUNT = 2 36_NAME = os.path.splitext(os.path.basename(__file__))[0] 37_SEC_TO_NSEC = 1_000_000_000 38 39 40class SessionCharacteristicsZoomTest(its_base_test.ItsBaseTest): 41 """Tests camera capture session specific zoom behavior. 42 43 The combination of camera features tested by this function are: 44 - Preview stabilization 45 - Target FPS range 46 - HLG 10-bit HDR 47 """ 48 49 def test_session_characteristics_zoom(self): 50 with its_session_utils.ItsSession( 51 device_id=self.dut.serial, 52 camera_id=self.camera_id) as cam: 53 54 # Skip if the device doesn't support feature combination query 55 props = cam.get_camera_properties() 56 feature_combination_query_version = props.get( 57 'android.info.sessionConfigurationQueryVersion') 58 if not feature_combination_query_version: 59 feature_combination_query_version = ( 60 its_session_utils.ANDROID14_API_LEVEL 61 ) 62 camera_properties_utils.skip_unless( 63 feature_combination_query_version >= 64 its_session_utils.ANDROID15_API_LEVEL) 65 66 # Raise error if not FRONT or REAR facing camera 67 camera_properties_utils.check_front_or_rear_camera(props) 68 69 # Load chart for scene 70 its_session_utils.load_scene( 71 cam, props, self.scene, self.tablet, self.chart_distance) 72 73 # set TOLs based on camera and test rig params 74 debug = self.debug_mode 75 if camera_properties_utils.logical_multi_camera(props): 76 test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size( 77 cam, props, self.chart_distance, debug) 78 else: 79 test_tols = {} 80 fls = props['android.lens.info.availableFocalLengths'] 81 for fl in fls: 82 test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL, 83 zoom_capture_utils.OFFSET_RTOL) 84 yuv_size = capture_request_utils.get_largest_format('yuv', props) 85 size = [yuv_size['width'], yuv_size['height']] 86 logging.debug('capture size: %s', size) 87 logging.debug('test TOLs: %s', test_tols) 88 89 # List of queryable stream combinations 90 combinations_str, combinations = cam.get_queryable_stream_combinations() 91 logging.debug('Queryable stream combinations: %s', combinations_str) 92 93 # Stabilization modes. Make sure to test ON first. 94 stabilization_params = [] 95 stabilization_modes = props[ 96 'android.control.availableVideoStabilizationModes'] 97 if (camera_properties_utils.STABILIZATION_MODE_PREVIEW in 98 stabilization_modes): 99 stabilization_params.append( 100 camera_properties_utils.STABILIZATION_MODE_PREVIEW) 101 stabilization_params.append( 102 camera_properties_utils.STABILIZATION_MODE_OFF) 103 logging.debug('stabilization modes: %s', stabilization_params) 104 105 configs = props['android.scaler.streamConfigurationMap'][ 106 'availableStreamConfigurations'] 107 fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props) 108 109 test_failures = [] 110 for stream_combination in combinations: 111 streams_name = stream_combination['name'] 112 min_frame_duration = 0 113 configured_streams = [] 114 skip = False 115 116 # Only supports combinations of up to 2 streams 117 if len(stream_combination['combination']) > _MAX_STREAM_COUNT: 118 raise AssertionError( 119 f'stream combination cannot exceed {_MAX_STREAM_COUNT} streams.') 120 121 # Skip if combinations contains only 1 stream, which is preview 122 if len(stream_combination['combination']) == 1: 123 continue 124 125 for i, stream in enumerate(stream_combination['combination']): 126 fmt = None 127 size = [int(e) for e in stream['size'].split('x')] 128 if stream['format'] == its_session_utils.PRIVATE_FORMAT: 129 fmt = capture_request_utils.FMT_CODE_PRIV 130 elif stream['format'] == 'jpeg': 131 fmt = capture_request_utils.FMT_CODE_JPEG 132 elif stream['format'] == its_session_utils.JPEG_R_FMT_STR: 133 fmt = capture_request_utils.FMT_CODE_JPEG_R 134 elif stream['format'] == 'yuv': 135 fmt = capture_request_utils.FMT_CODE_YUV 136 137 # Assume first stream is always a preview stream with priv format 138 if i == 0 and fmt != capture_request_utils.FMT_CODE_PRIV: 139 raise AssertionError( 140 'first stream in the combination must be priv format preview.') 141 142 # Second stream must be jpeg or yuv for zoom test. If not, skip 143 if (i == 1 and fmt != capture_request_utils.FMT_CODE_JPEG and 144 fmt != capture_request_utils.FMT_CODE_JPEG_R and 145 fmt != capture_request_utils.FMT_CODE_YUV): 146 logging.debug( 147 'second stream format %s is not yuv/jpeg/jpeg_r. Skip', 148 stream['format']) 149 skip = True 150 break 151 152 # Skip if size and format are not supported by the device. 153 config = [x for x in configs if 154 x['format'] == fmt and 155 x['width'] == size[0] and 156 x['height'] == size[1]] 157 if not config: 158 logging.debug( 159 'stream combination %s not supported. Skip', streams_name) 160 skip = True 161 break 162 163 min_frame_duration = max( 164 config[0]['minFrameDuration'], min_frame_duration) 165 logging.debug( 166 'format is %s, min_frame_duration is %d}', 167 stream['format'], config[0]['minFrameDuration']) 168 configured_streams.append( 169 {'format': stream['format'], 'width': size[0], 'height': size[1]}) 170 171 if skip: 172 continue 173 174 # FPS ranges 175 max_achievable_fps = _SEC_TO_NSEC / min_frame_duration 176 fps_params = [fps for fps in fps_ranges if ( 177 fps[_MAX_FPS_INDEX] in _FPS_30_60 and 178 max_achievable_fps >= fps[_MAX_FPS_INDEX] - _FPS_SELECTION_ATOL)] 179 180 for fps_range in fps_params: 181 # HLG10. Make sure to test ON first. 182 hlg10_params = [] 183 if camera_properties_utils.dynamic_range_ten_bit(props): 184 hlg10_params.append(True) 185 hlg10_params.append(False) 186 187 features_tested = [] # feature combinations already tested 188 for hlg10 in hlg10_params: 189 # Construct output surfaces 190 output_surfaces = [] 191 for configured_stream in configured_streams: 192 hlg10_stream = (hlg10 and configured_stream['format'] == 193 its_session_utils.PRIVATE_FORMAT) 194 output_surfaces.append({'format': configured_stream['format'], 195 'width': configured_stream['width'], 196 'height': configured_stream['height'], 197 'hlg10': hlg10_stream}) 198 199 for stabilize in stabilization_params: 200 settings = { 201 'android.control.videoStabilizationMode': stabilize, 202 'android.control.aeTargetFpsRange': fps_range, 203 } 204 combination_name = (f'streams_{streams_name}_hlg10_{hlg10}' 205 f'_stabilization_{stabilize}_fps_range_' 206 f'_{fps_range[0]}_{fps_range[1]}') 207 logging.debug('combination name: %s', combination_name) 208 209 # Is the feature combination supported? 210 if not cam.is_stream_combination_supported( 211 output_surfaces, settings): 212 logging.debug('%s not supported', combination_name) 213 break 214 215 # If a superset of features are already tested, skip. 216 # pylint: disable=line-too-long 217 is_stabilized = ( 218 stabilize == camera_properties_utils.STABILIZATION_MODE_PREVIEW 219 ) 220 skip_test = its_session_utils.check_and_update_features_tested( 221 features_tested, hlg10, is_stabilized) 222 if skip_test: 223 continue 224 225 # Get zoom ratio range 226 session_props = cam.get_session_properties( 227 output_surfaces, settings) 228 z_range = session_props.get('android.control.zoomRatioRange') 229 230 debug = self.debug_mode 231 z_min, z_max = float(z_range[0]), float(z_range[1]) 232 camera_properties_utils.skip_unless( 233 z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH) 234 z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min) 235 z_list = [z_min, z_max] 236 if z_min != 1: 237 z_list = np.insert(z_list, 0, 1) # make reference zoom 1x 238 logging.debug('Testing zoom range: %s', z_list) 239 240 # do captures over zoom range and find ArUco markers with cv2 241 img_name_stem = f'{os.path.join(self.log_path, _NAME)}' 242 req = capture_request_utils.auto_capture_request() 243 244 test_data = [] 245 all_aruco_ids = [] 246 all_aruco_corners = [] 247 images = [] 248 fmt_str = configured_streams[1]['format'] 249 for i, z in enumerate(z_list): 250 req['android.control.zoomRatio'] = z 251 logging.debug('zoom ratio: %.3f', z) 252 cam.do_3a( 253 zoom_ratio=z, 254 out_surfaces=output_surfaces, 255 repeat_request=None, 256 first_surface_for_3a=True 257 ) 258 cap = cam.do_capture( 259 req, output_surfaces, 260 reuse_session=True, 261 first_surface_for_3a=True) 262 263 img = image_processing_utils.convert_capture_to_rgb_image( 264 cap, props=props) 265 img_name = (f'{img_name_stem}_{combination_name}_{fmt_str}' 266 f'_{z:.2f}.{zoom_capture_utils.JPEG_STR}') 267 image_processing_utils.write_image(img, img_name) 268 269 # determine radius tolerance of capture 270 cap_fl = cap['metadata']['android.lens.focalLength'] 271 cap_physical_id = ( 272 cap['metadata'][ 273 'android.logicalMultiCamera.activePhysicalId'] 274 ) 275 result_zoom = float( 276 cap['metadata']['android.control.zoomRatio']) 277 radius_tol, offset_tol = test_tols.get( 278 cap_fl, 279 (zoom_capture_utils.RADIUS_RTOL, 280 zoom_capture_utils.OFFSET_RTOL) 281 ) 282 283 # Find ArUco markers 284 bgr_img = cv2.cvtColor( 285 image_processing_utils.convert_image_to_uint8(img), 286 cv2.COLOR_RGB2BGR 287 ) 288 try: 289 corners, ids, _ = opencv_processing_utils.find_aruco_markers( 290 bgr_img, 291 (f'{img_name_stem}_{z:.2f}_' 292 f'ArUco.{zoom_capture_utils.JPEG_STR}'), 293 aruco_marker_count=1 294 ) 295 except AssertionError as e: 296 logging.debug( 297 'Could not find ArUco marker at zoom ratio %.2f: %s', 298 z, e 299 ) 300 break 301 all_aruco_corners.append([corner[0] for corner in corners]) 302 all_aruco_ids.append([id[0] for id in ids]) 303 images.append(bgr_img) 304 305 test_data.append( 306 zoom_capture_utils.ZoomTestData( 307 result_zoom=result_zoom, 308 radius_tol=radius_tol, 309 offset_tol=offset_tol, 310 focal_length=cap_fl, 311 physical_id=cap_physical_id 312 ) 313 ) 314 315 # Find ArUco markers in all captures and update test data 316 zoom_capture_utils.update_zoom_test_data_with_shared_aruco_marker( 317 test_data, all_aruco_ids, all_aruco_corners, size) 318 # Mark ArUco marker center and image center 319 opencv_processing_utils.mark_zoom_images( 320 images, test_data, img_name_stem) 321 if not zoom_capture_utils.verify_zoom_results( 322 test_data, size, z_max, z_min): 323 failure_msg = ( 324 f'{combination_name}: failed! ' 325 'Check test_log.DEBUG for errors') 326 test_failures.append(failure_msg) 327 328 if test_failures: 329 raise AssertionError(test_failures) 330 331if __name__ == '__main__': 332 test_runner.main() 333