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 feature combinations for stabilization, 10-bit, and frame rate.""" 15 16import logging 17import os 18 19from mobly import test_runner 20 21import its_base_test 22import camera_properties_utils 23import capture_request_utils 24import its_session_utils 25import preview_processing_utils 26import video_processing_utils 27 28_BIT_HLG10 = 0x01 # bit 1 for feature mask 29_BIT_STABILIZATION = 0x02 # bit 2 for feature mask 30_FPS_30_60 = (30, 60) 31_FPS_SELECTION_ATOL = 0.01 32_FPS_ATOL_CODEC = 1.2 33_FPS_ATOL_METADATA = 0.8 34 35_NAME = os.path.splitext(os.path.basename(__file__))[0] 36_SEC_TO_NSEC = 1_000_000_000 37 38 39class FeatureCombinationTest(its_base_test.ItsBaseTest): 40 """Tests camera feature combinations. 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 Camera is moved in sensor fusion rig on an arc of 15 degrees. 48 Speed is set to mimic hand movement (and not be too fast). 49 Preview is captured after rotation rig starts moving and the 50 gyroscope data is dumped. 51 52 Preview stabilization: 53 The recorded preview is processed to dump all of the frames to 54 PNG files. Camera movement is extracted from frames by determining 55 max angle of deflection in video movement vs max angle of deflection 56 in gyroscope movement. Test is a PASS if rotation is reduced in video. 57 58 Target FPS range: 59 The recorded preview has the expected fps range. For example, 60 if [60, 60] is set as targetFpsRange, the camera device is expected to 61 produce 60fps preview/video. 62 63 HLG 10-bit HDR: 64 The recorded preview has valid 10-bit HLG outputs. 65 """ 66 67 def test_feature_combination(self): 68 rot_rig = {} 69 log_path = self.log_path 70 71 with its_session_utils.ItsSession( 72 device_id=self.dut.serial, 73 camera_id=self.camera_id) as cam: 74 75 # Skip if the device doesn't support feature combination query 76 props = cam.get_camera_properties() 77 feature_combination_query_version = props.get( 78 'android.info.sessionConfigurationQueryVersion') 79 if not feature_combination_query_version: 80 feature_combination_query_version = ( 81 its_session_utils.ANDROID14_API_LEVEL 82 ) 83 should_run = (feature_combination_query_version >= 84 its_session_utils.ANDROID15_API_LEVEL) 85 camera_properties_utils.skip_unless(should_run) 86 87 # Log ffmpeg version being used 88 video_processing_utils.log_ffmpeg_version() 89 90 # Raise error if not FRONT or REAR facing camera 91 facing = props['android.lens.facing'] 92 camera_properties_utils.check_front_or_rear_camera(props) 93 94 # Initialize rotation rig 95 rot_rig['cntl'] = self.rotator_cntl 96 rot_rig['ch'] = self.rotator_ch 97 if rot_rig['cntl'].lower() != 'arduino': 98 raise AssertionError( 99 f'You must use the arduino controller for {_NAME}.') 100 101 # List of queryable stream combinations 102 combinations_str, combinations = cam.get_queryable_stream_combinations() 103 logging.debug('Queryable stream combinations: %s', combinations_str) 104 105 # Stabilization modes. Make sure to test ON first. 106 stabilization_params = [] 107 stabilization_modes = props[ 108 'android.control.availableVideoStabilizationModes'] 109 if (camera_properties_utils.STABILIZATION_MODE_PREVIEW in 110 stabilization_modes): 111 stabilization_params.append( 112 camera_properties_utils.STABILIZATION_MODE_PREVIEW) 113 stabilization_params.append( 114 camera_properties_utils.STABILIZATION_MODE_OFF 115 ) 116 logging.debug('stabilization modes: %s', stabilization_params) 117 118 configs = props['android.scaler.streamConfigurationMap'][ 119 'availableStreamConfigurations'] 120 fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props) 121 122 test_failures = [] 123 for stream_combination in combinations: 124 streams_name = stream_combination['name'] 125 min_frame_duration = 0 126 configured_streams = [] 127 skip = False 128 if (stream_combination['combination'][0]['format'] != 129 its_session_utils.PRIVATE_FORMAT): 130 raise AssertionError( 131 f'First stream for {streams_name} must be PRIV') 132 preview_size = stream_combination['combination'][0]['size'] 133 for stream in stream_combination['combination']: 134 fmt = None 135 size = [int(e) for e in stream['size'].split('x')] 136 if stream['format'] == its_session_utils.PRIVATE_FORMAT: 137 fmt = capture_request_utils.FMT_CODE_PRIV 138 elif stream['format'] == 'jpeg': 139 fmt = capture_request_utils.FMT_CODE_JPEG 140 elif stream['format'] == its_session_utils.JPEG_R_FMT_STR: 141 fmt = capture_request_utils.FMT_CODE_JPEG_R 142 config = [x for x in configs if 143 x['format'] == fmt and 144 x['width'] == size[0] and 145 x['height'] == size[1]] 146 if not config: 147 logging.debug( 148 'stream combination %s not supported. Skip', streams_name) 149 skip = True 150 break 151 152 min_frame_duration = max( 153 config[0]['minFrameDuration'], min_frame_duration) 154 logging.debug( 155 'format is %s, min_frame_duration is %d}', 156 stream['format'], config[0]['minFrameDuration']) 157 configured_streams.append( 158 {'format': stream['format'], 'width': size[0], 'height': size[1]}) 159 160 if skip: 161 continue 162 163 # Fps ranges 164 max_achievable_fps = _SEC_TO_NSEC / min_frame_duration 165 fps_params = [fps for fps in fps_ranges if ( 166 fps[1] in _FPS_30_60 and 167 max_achievable_fps >= fps[1] - _FPS_SELECTION_ATOL)] 168 169 for fps_range in fps_params: 170 # HLG10. Make sure to test ON first. 171 hlg10_params = [] 172 if cam.is_hlg10_recording_supported_for_size_and_fps( 173 preview_size, fps_range[1]): 174 hlg10_params.append(True) 175 hlg10_params.append(False) 176 177 features_tested = [] # feature combinations already tested 178 for hlg10 in hlg10_params: 179 # Construct output surfaces 180 output_surfaces = [] 181 for configured_stream in configured_streams: 182 hlg10_stream = (configured_stream['format'] == 183 its_session_utils.PRIVATE_FORMAT and hlg10) 184 output_surfaces.append({'format': configured_stream['format'], 185 'width': configured_stream['width'], 186 'height': configured_stream['height'], 187 'hlg10': hlg10_stream}) 188 189 for stabilize in stabilization_params: 190 settings = { 191 'android.control.videoStabilizationMode': stabilize, 192 'android.control.aeTargetFpsRange': fps_range, 193 } 194 combination_name = (f'(streams: {streams_name}, hlg10: {hlg10}, ' 195 f'stabilization: {stabilize}, fps_range: ' 196 f'[{fps_range[0]}, {fps_range[1]}])') 197 logging.debug('combination name: %s', combination_name) 198 199 # Is the feature combination supported? 200 supported = cam.is_stream_combination_supported( 201 output_surfaces, settings) 202 if not supported: 203 logging.debug('%s not supported', combination_name) 204 break 205 206 is_stabilized = False 207 if (stabilize == 208 camera_properties_utils.STABILIZATION_MODE_PREVIEW): 209 is_stabilized = True 210 211 # If a superset of features are already tested, skip. 212 skip_test = its_session_utils.check_and_update_features_tested( 213 features_tested, hlg10, is_stabilized) 214 if skip_test: 215 continue 216 217 recording_obj = ( 218 preview_processing_utils.collect_data_with_surfaces( 219 cam, self.tablet_device, output_surfaces, is_stabilized, 220 rot_rig=rot_rig, fps_range=fps_range)) 221 222 if is_stabilized: 223 # Get gyro events 224 logging.debug('Reading out inertial sensor events') 225 gyro_events = cam.get_sensor_events()['gyro'] 226 logging.debug('Number of gyro samples %d', len(gyro_events)) 227 228 # Grab the video from the file location on DUT 229 self.dut.adb.pull([recording_obj['recordedOutputPath'], log_path]) 230 231 # Verify FPS by inspecting the video clip 232 preview_file_name = ( 233 recording_obj['recordedOutputPath'].split('/')[-1]) 234 preview_file_name_with_path = os.path.join( 235 self.log_path, preview_file_name) 236 average_frame_rate_codec = ( 237 video_processing_utils.get_average_frame_rate( 238 preview_file_name_with_path)) 239 logging.debug('Average codec frame rate for %s is %f', combination_name, 240 average_frame_rate_codec) 241 if (average_frame_rate_codec > fps_range[1] + _FPS_ATOL_CODEC or 242 average_frame_rate_codec < fps_range[0] - _FPS_ATOL_CODEC): 243 failure_msg = ( 244 f'{combination_name}: Average video clip frame rate ' 245 f'{average_frame_rate_codec} exceeding the allowed range of ' 246 f'({fps_range[0]}-{_FPS_ATOL_CODEC}, ' 247 f'{fps_range[1]}+{_FPS_ATOL_CODEC})') 248 test_failures.append(failure_msg) 249 250 # Verify FPS by inspecting the result metadata 251 capture_results = recording_obj['captureMetadata']; 252 assert len(capture_results) > 1 253 last_t = capture_results[-1]['android.sensor.timestamp']; 254 first_t = capture_results[0]['android.sensor.timestamp']; 255 average_frame_duration = (last_t - first_t) / (len(capture_results) - 1) 256 average_frame_rate_metadata = _SEC_TO_NSEC / average_frame_duration 257 logging.debug('Average metadata frame rate for %s is %f', combination_name, 258 average_frame_rate_metadata) 259 if (average_frame_rate_metadata > fps_range[1] + _FPS_ATOL_METADATA or 260 average_frame_rate_metadata < fps_range[0] - _FPS_ATOL_METADATA): 261 failure_msg = ( 262 f'{combination_name}: Average frame rate ' 263 f'{average_frame_rate_metadata} exceeding the allowed range of ' 264 f'({fps_range[0]}-{_FPS_ATOL_METADATA}, {fps_range[1]}+{_FPS_ATOL_METADATA})') 265 test_failures.append(failure_msg) 266 267 # Verify video stabilization 268 if is_stabilized: 269 stabilization_result = ( 270 preview_processing_utils.verify_preview_stabilization( 271 recording_obj, gyro_events, _NAME, log_path, facing)) 272 if stabilization_result['failure'] is not None: 273 failure_msg = (combination_name + ': ' + 274 stabilization_result['failure']) 275 test_failures.append(failure_msg) 276 277 # Verify color space 278 color_space = video_processing_utils.get_video_colorspace( 279 self.log_path, preview_file_name_with_path) 280 if (hlg10 and 281 video_processing_utils.COLORSPACE_HDR not in color_space): 282 failure_msg = ( 283 f'{combination_name}: video color space {color_space} ' 284 'is missing COLORSPACE_HDR') 285 test_failures.append(failure_msg) 286 287 # Assert PASS/FAIL criteria 288 if test_failures: 289 raise AssertionError(test_failures) 290 291if __name__ == '__main__': 292 test_runner.main() 293