• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""Ensure that FoV reduction with Preview Stabilization is within spec."""
15
16import logging
17import math
18import os
19
20from mobly import test_runner
21
22import its_base_test
23import camera_properties_utils
24import image_fov_utils
25import image_processing_utils
26import its_session_utils
27import opencv_processing_utils
28import video_processing_utils
29
30_PREVIEW_STABILIZATION_MODE_PREVIEW = 2
31_VIDEO_DURATION = 3  # seconds
32
33_MAX_STABILIZED_RADIUS_RATIO = 1.25  # An FOV reduction of 20% corresponds to an
34                                     # increase in lengths of 25%. So the
35                                     # stabilized circle's radius can be at most
36                                     # 1.25 times that of an unstabilized circle
37_MAX_STABILIZED_RADIUS_ATOL = 1  # 1 pixel tol for radii inaccuracy
38_ROUNDESS_DELTA_THRESHOLD = 0.05
39
40_MAX_CENTER_THRESHOLD_PERCENT = 0.075
41_MAX_AREA = 1920 * 1440  # max mandatory preview stream resolution
42_MIN_CENTER_THRESHOLD_PERCENT = 0.03
43_MIN_AREA = 176 * 144  # assume QCIF to be min preview size
44
45
46def _collect_data(cam, preview_size, stabilize):
47  """Capture a preview video from the device.
48
49  Captures camera preview frames from the passed device.
50
51  Args:
52    cam: camera object
53    preview_size: str; preview resolution. ex. '1920x1080'
54    stabilize: boolean; whether the preview should be stabilized or not
55
56  Returns:
57    recording object as described by cam.do_preview_recording
58  """
59
60  recording_obj = cam.do_preview_recording(preview_size, _VIDEO_DURATION,
61                                           stabilize)
62  logging.debug('Recorded output path: %s', recording_obj['recordedOutputPath'])
63  logging.debug('Tested quality: %s', recording_obj['quality'])
64
65  return recording_obj
66
67
68def _point_distance(p1_x, p1_y, p2_x, p2_y):
69  """Calculates the euclidean distance between two points.
70
71  Args:
72    p1_x: x coordinate of the first point
73    p1_y: y coordinate of the first point
74    p2_x: x coordinate of the second point
75    p2_y: y coordinate of the second point
76
77  Returns:
78    Euclidean distance between two points
79  """
80  return math.sqrt(pow(p1_x - p2_x, 2) + pow(p1_y - p2_y, 2))
81
82
83def _calculate_center_offset_threshold(image_size):
84  """Calculates appropriate center offset threshold.
85
86  This function calculates a viable threshold that centers of two circles can be
87  offset by for a given image size. The threshold percent is linearly
88  interpolated between _MIN_CENTER_THRESHOLD_PERCENT and
89  _MAX_CENTER_THRESHOLD_PERCENT according to the image size passed.
90
91  Args:
92    image_size: pair; size of the image for which threshold has to be
93                calculated. ex. (1920, 1080)
94
95  Returns:
96    threshold value ratio between which the circle centers can differ
97  """
98
99  img_area = image_size[0] * image_size[1]
100
101  normalized_area = (img_area - _MIN_AREA) / (_MAX_AREA - _MIN_AREA)
102
103  if normalized_area > 1 or normalized_area < 0:
104    raise AssertionError(f'normalized area > 1 or < 0! '
105                         f'image_size[0]: {image_size[0]}, '
106                         f'image_size[1]: {image_size[1]}, '
107                         f'normalized_area: {normalized_area}')
108
109  # Threshold should be larger for images with smaller resolution
110  normalized_threshold_percent = ((1 - normalized_area) *
111                                  (_MAX_CENTER_THRESHOLD_PERCENT -
112                                   _MIN_CENTER_THRESHOLD_PERCENT))
113
114  return normalized_threshold_percent + _MIN_CENTER_THRESHOLD_PERCENT
115
116
117class PreviewStabilizationFoVTest(its_base_test.ItsBaseTest):
118  """Tests if stabilized preview FoV is within spec.
119
120  The test captures two videos, one with preview stabilization on, and another
121  with preview stabilization off. A representative frame is selected from each
122  video, and analyzed to ensure that the FoV changes in the two videos are
123  within spec.
124
125  Specifically, the test checks for the following parameters with and without
126  preview stabilization:
127    - The circle roundness remains about constant
128    - The center of the circle remains relatively stable
129    - The size of circle changes no more that 20% i.e. the FOV changes at most
130      20%
131  """
132
133  def test_preview_stabilization_fov(self):
134    log_path = self.log_path
135
136    with its_session_utils.ItsSession(
137        device_id=self.dut.serial,
138        camera_id=self.camera_id,
139        hidden_physical_id=self.hidden_physical_id) as cam:
140
141      props = cam.get_camera_properties()
142      props = cam.override_with_hidden_physical_camera_props(props)
143
144      # Load scene.
145      its_session_utils.load_scene(cam, props, self.scene,
146                                   self.tablet, self.chart_distance)
147
148      # Check skip condition
149      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
150      camera_properties_utils.skip_unless(
151          first_api_level >= its_session_utils.ANDROID13_API_LEVEL,
152          'First API level should be {} or higher. Found {}.'.format(
153              its_session_utils.ANDROID13_API_LEVEL, first_api_level))
154
155      # Log ffmpeg version being used
156      video_processing_utils.log_ffmpeg_version()
157
158      supported_stabilization_modes = props[
159          'android.control.availableVideoStabilizationModes'
160      ]
161
162      camera_properties_utils.skip_unless(
163          supported_stabilization_modes is not None
164          and _PREVIEW_STABILIZATION_MODE_PREVIEW
165          in supported_stabilization_modes,
166          'Preview Stabilization not supported',
167      )
168
169      # Raise error if not FRONT or REAR facing camera
170      facing = props['android.lens.facing']
171      if (facing != camera_properties_utils.LENS_FACING_BACK
172          and facing != camera_properties_utils.LENS_FACING_FRONT):
173        raise AssertionError('Unknown lens facing: {facing}.')
174
175      # List of preview resolutions to test
176      supported_preview_sizes = cam.get_supported_preview_sizes(self.camera_id)
177      for size in video_processing_utils.LOW_RESOLUTION_SIZES:
178        if size in supported_preview_sizes:
179          supported_preview_sizes.remove(size)
180      logging.debug('Supported preview resolutions: %s',
181                    supported_preview_sizes)
182
183      test_failures = []
184
185      for preview_size in supported_preview_sizes:
186
187        # recording with stabilization off
188        ustab_rec_obj = _collect_data(cam, preview_size, False)
189        # recording with stabilization on
190        stab_rec_obj = _collect_data(cam, preview_size, True)
191
192        # Grab the unstabilized video from DUT
193        self.dut.adb.pull([ustab_rec_obj['recordedOutputPath'], log_path])
194        ustab_file_name = (ustab_rec_obj['recordedOutputPath'].split('/')[-1])
195        logging.debug('ustab_file_name: %s', ustab_file_name)
196
197        # Grab the stabilized video from DUT
198        self.dut.adb.pull([stab_rec_obj['recordedOutputPath'], log_path])
199        stab_file_name = (stab_rec_obj['recordedOutputPath'].split('/')[-1])
200        logging.debug('stab_file_name: %s', stab_file_name)
201
202        # Get all frames from the videos
203        ustab_file_list = video_processing_utils.extract_key_frames_from_video(
204            log_path, ustab_file_name)
205        logging.debug('Number of unstabilized iframes %d', len(ustab_file_list))
206
207        stab_file_list = video_processing_utils.extract_key_frames_from_video(
208            log_path, stab_file_name)
209        logging.debug('Number of stabilized iframes %d', len(stab_file_list))
210
211        # Extract last key frame to test from each video
212        ustab_frame = os.path.join(log_path,
213                                   video_processing_utils
214                                   .get_key_frame_to_process(ustab_file_list))
215        logging.debug('unstabilized frame: %s', ustab_frame)
216        stab_frame = os.path.join(log_path,
217                                  video_processing_utils
218                                  .get_key_frame_to_process(stab_file_list))
219        logging.debug('stabilized frame: %s', stab_frame)
220
221        # Convert to numpy matrix for analysis
222        ustab_np_image = image_processing_utils.convert_image_to_numpy_array(
223            ustab_frame)
224        logging.debug('unstabilized frame size: %s', ustab_np_image.shape)
225        stab_np_image = image_processing_utils.convert_image_to_numpy_array(
226            stab_frame)
227        logging.debug('stabilized frame size: %s', stab_np_image.shape)
228
229        image_size = stab_np_image.shape
230
231        # Get circles to compare
232        ustab_circle = opencv_processing_utils.find_circle(
233            ustab_np_image,
234            ustab_frame,
235            image_fov_utils.CIRCLE_MIN_AREA,
236            image_fov_utils.CIRCLE_COLOR)
237
238        stab_circle = opencv_processing_utils.find_circle(
239            stab_np_image,
240            stab_frame,
241            image_fov_utils.CIRCLE_MIN_AREA,
242            image_fov_utils.CIRCLE_COLOR)
243
244        failure_string = ''
245
246        # Ensure the circles are equally round w/ and w/o stabilization
247        ustab_roundness = ustab_circle['w'] / ustab_circle['h']
248        logging.debug('unstabilized roundness: %f', ustab_roundness)
249        stab_roundness = stab_circle['w'] / stab_circle['h']
250        logging.debug('stabilized roundness: %f', stab_roundness)
251
252        roundness_diff = abs(stab_roundness - ustab_roundness)
253        if roundness_diff > _ROUNDESS_DELTA_THRESHOLD:
254          failure_string += (f'Circle roundness changed too much: '
255                             f'unstabilized ratio: {ustab_roundness}, '
256                             f'stabilized ratio: {stab_roundness}, '
257                             f'Expected ratio difference <= '
258                             f'{_ROUNDESS_DELTA_THRESHOLD}, '
259                             f'actual ratio difference: {roundness_diff}. ')
260
261        # Distance between centers, x_offset and y_offset are relative to the
262        # radius of the circle, so they're normalized. Not pixel values.
263        unstab_center = (ustab_circle['x_offset'], ustab_circle['y_offset'])
264        logging.debug('unstabilized center: %s', unstab_center)
265        stab_center = (stab_circle['x_offset'], stab_circle['y_offset'])
266        logging.debug('stabilized center: %s', stab_center)
267
268        dist_centers = _point_distance(unstab_center[0], unstab_center[1],
269                                       stab_center[0], stab_center[1])
270        center_offset_threshold = _calculate_center_offset_threshold(image_size)
271        if dist_centers > center_offset_threshold:
272          failure_string += (f'Circle moved too much: '
273                             f'unstabilized center: ('
274                             f'{unstab_center[0]}, {unstab_center[1]}), '
275                             f'stabilized center: '
276                             f'({stab_center[0]}, {stab_center[1]}), '
277                             f'expected distance < {center_offset_threshold}, '
278                             f'actual_distance {dist_centers}. ')
279
280        # ensure radius of stabilized frame is within 120% of radius within
281        # unstabilized frame
282        ustab_radius = ustab_circle['r']
283        logging.debug('unstabilized radius: %f', ustab_radius)
284        stab_radius = stab_circle['r']
285        logging.debug('stabilized radius: %f', stab_radius)
286
287        max_stab_radius = (ustab_radius * _MAX_STABILIZED_RADIUS_RATIO +
288                           _MAX_STABILIZED_RADIUS_ATOL)
289        if stab_radius > max_stab_radius:
290          failure_string += (f'Too much FoV reduction: '
291                             f'unstabilized radius: {ustab_radius}, '
292                             f'stabilized radius: {stab_radius}, '
293                             f'expected max stabilized radius: '
294                             f'{max_stab_radius}. ')
295
296        if failure_string:
297          failure_string = f'{preview_size} fails FoV test. ' + failure_string
298          test_failures.append(failure_string)
299
300      if test_failures:
301        raise AssertionError(test_failures)
302
303
304if __name__ == '__main__':
305  test_runner.main()
306
307