1# Copyright 2022 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 preview is stable during phone movement.""" 15 16import logging 17import os 18 19from mobly import test_runner 20 21import its_base_test 22import camera_properties_utils 23import its_session_utils 24import preview_processing_utils 25import video_processing_utils 26 27_NAME = os.path.splitext(os.path.basename(__file__))[0] 28# 1080P with 16:9 aspect ratio, 720P and VGA resolutions 29_TARGET_PREVIEW_SIZES = ('1920x1080', '1280x720', '640x480') 30_TEST_REQUIRED_MPC_FRONT = 34 31_TEST_REQUIRED_MPC_REAR = 33 32_ZOOM_RATIO_UW = 0.9 33_ZOOM_RATIO_W = 1.0 34 35 36def _get_preview_sizes(cam, camera_id): 37 """Determine preview sizes to test based on DUT's supported sizes. 38 39 Targeting 1080P (16:9 ratio), 720P and VGA. 40 41 Args: 42 cam: ItsSession camera object. 43 camera_id: str; unique identifier assigned to each camera. 44 Returns: 45 preview sizes to test. 46 """ 47 preview_sizes_to_test = cam.get_supported_preview_sizes(camera_id) 48 preview_sizes_to_test = [size for size in preview_sizes_to_test 49 if size in _TARGET_PREVIEW_SIZES] 50 logging.debug('Preview sizes to test: %s', preview_sizes_to_test) 51 return preview_sizes_to_test 52 53 54class PreviewStabilizationTest(its_base_test.ItsBaseTest): 55 """Tests if preview is stabilized. 56 57 Camera is moved in sensor fusion rig on an arc of 15 degrees. 58 Speed is set to mimic hand movement (and not be too fast). 59 Preview is captured after rotation rig starts moving, and the 60 gyroscope data is dumped. 61 62 The recorded preview is processed to dump all of the frames to 63 PNG files. Camera movement is extracted from frames by determining 64 max angle of deflection in video movement vs max angle of deflection 65 in gyroscope movement. Test is a PASS if rotation is reduced in video. 66 """ 67 68 def test_preview_stabilization(self): 69 rot_rig = {} 70 log_path = self.log_path 71 72 with its_session_utils.ItsSession( 73 device_id=self.dut.serial, 74 camera_id=self.camera_id, 75 hidden_physical_id=self.hidden_physical_id) as cam: 76 77 props = cam.get_camera_properties() 78 props = cam.override_with_hidden_physical_camera_props(props) 79 first_api_level = its_session_utils.get_first_api_level(self.dut.serial) 80 camera_properties_utils.skip_unless( 81 first_api_level >= its_session_utils.ANDROID13_API_LEVEL, 82 'First API level should be {} or higher. Found {}.'.format( 83 its_session_utils.ANDROID13_API_LEVEL, first_api_level)) 84 85 supported_stabilization_modes = props[ 86 'android.control.availableVideoStabilizationModes' 87 ] 88 89 # Check media performance class 90 should_run = (supported_stabilization_modes is not None and 91 camera_properties_utils.STABILIZATION_MODE_PREVIEW in 92 supported_stabilization_modes) 93 media_performance_class = its_session_utils.get_media_performance_class( 94 self.dut.serial) 95 if (props['android.lens.facing'] == 96 camera_properties_utils.LENS_FACING['FRONT']): 97 if (media_performance_class >= _TEST_REQUIRED_MPC_FRONT 98 and not should_run): 99 its_session_utils.raise_mpc_assertion_error( 100 _TEST_REQUIRED_MPC_FRONT, _NAME, media_performance_class) 101 else: 102 if (media_performance_class >= _TEST_REQUIRED_MPC_REAR 103 and not should_run): 104 its_session_utils.raise_mpc_assertion_error( 105 _TEST_REQUIRED_MPC_REAR, _NAME, media_performance_class) 106 107 camera_properties_utils.skip_unless(should_run) 108 109 # Log ffmpeg version being used 110 video_processing_utils.log_ffmpeg_version() 111 112 # Raise error if not FRONT or REAR facing camera 113 facing = props['android.lens.facing'] 114 camera_properties_utils.check_front_or_rear_camera(props) 115 116 # Check zoom range 117 zoom_range = props['android.control.zoomRatioRange'] 118 logging.debug('zoomRatioRange: %s', str(zoom_range)) 119 120 # If device doesn't support UW, only test W 121 # If device's UW's zoom ratio is bigger than 0.9x, use that value 122 test_zoom_ratios = [_ZOOM_RATIO_W] 123 if (zoom_range[0] < _ZOOM_RATIO_W and 124 first_api_level >= its_session_utils.ANDROID15_API_LEVEL): 125 test_zoom_ratios.append(max(_ZOOM_RATIO_UW, zoom_range[0])) 126 127 # Initialize rotation rig 128 rot_rig['cntl'] = self.rotator_cntl 129 rot_rig['ch'] = self.rotator_ch 130 if rot_rig['cntl'].lower() != 'arduino': 131 raise AssertionError( 132 f'You must use the arduino controller for {_NAME}.') 133 134 # Determine preview sizes to test 135 preview_sizes_to_test = _get_preview_sizes(cam, self.camera_id) 136 137 # Preview recording with camera movement 138 stabilization_result = {} 139 for preview_size in preview_sizes_to_test: 140 for zoom_ratio in test_zoom_ratios: 141 recording_obj = preview_processing_utils.collect_data( 142 cam, self.tablet_device, preview_size, 143 stabilize=True, rot_rig=rot_rig, zoom_ratio=zoom_ratio) 144 145 # Get gyro events 146 logging.debug('Reading out inertial sensor events') 147 gyro_events = cam.get_sensor_events()['gyro'] 148 logging.debug('Number of gyro samples %d', len(gyro_events)) 149 150 # Grab the video from the save location on DUT 151 self.dut.adb.pull([recording_obj['recordedOutputPath'], log_path]) 152 153 # Verify stabilization was applied to preview stream 154 stabilization_result[preview_size] = ( 155 preview_processing_utils.verify_preview_stabilization( 156 recording_obj, gyro_events, _NAME, log_path, facing, 157 zoom_ratio) 158 ) 159 160 # Assert PASS/FAIL criteria 161 test_failures = [] 162 for _, result_per_size in stabilization_result.items(): 163 if result_per_size['failure'] is not None: 164 test_failures.append(result_per_size['failure']) 165 166 if test_failures: 167 raise AssertionError(test_failures) 168 169 170if __name__ == '__main__': 171 test_runner.main() 172