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