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