xref: /aosp_15_r20/cts/apps/CameraITS/tests/scene6/test_preview_zoom.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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 preview zoom ratio scales ArUco marker sizes correctly."""
15
16import logging
17import os.path
18import subprocess
19
20import cv2
21from mobly import test_runner
22
23import its_base_test
24import camera_properties_utils
25import its_session_utils
26import opencv_processing_utils
27import preview_processing_utils
28import video_processing_utils
29import zoom_capture_utils
30
31
32_CRF = 23
33_CV2_RED = (0, 0, 255)  # color (B, G, R) in cv2 to draw lines
34_FPS = 30
35_MP4V = 'mp4v'
36_NAME = os.path.splitext(os.path.basename(__file__))[0]
37_NUM_STEPS = 50
38
39
40def compress_video(input_filename, output_filename, crf=_CRF):
41  """Compresses the given video using ffmpeg."""
42
43  ffmpeg_cmd = [
44      'ffmpeg',
45      '-i', input_filename,   # Input file
46      '-c:v', 'libx264',      # Use H.264 codec
47      '-crf', str(crf),       # Set Constant Rate Factor (adjust for quality)
48      '-preset', 'medium',    # Encoding speed/compression balance
49      '-c:a', 'copy',         # Copy audio stream without re-encoding
50      output_filename         # Output file
51  ]
52
53  with open(os.devnull, 'w') as devnull:
54    subprocess.run(ffmpeg_cmd, stdout=devnull,
55                   stderr=subprocess.STDOUT, check=False)
56
57
58class PreviewZoomTest(its_base_test.ItsBaseTest):
59  """Verify zoom ratio of preview frames matches values in TotalCaptureResult."""
60
61  def test_preview_zoom(self):
62    log_path = self.log_path
63    video_processing_utils.log_ffmpeg_version()
64
65    with its_session_utils.ItsSession(
66        device_id=self.dut.serial,
67        camera_id=self.camera_id,
68        hidden_physical_id=self.hidden_physical_id) as cam:
69
70      debug = self.debug_mode
71
72      props = cam.get_camera_properties()
73      props = cam.override_with_hidden_physical_camera_props(props)
74      camera_properties_utils.skip_unless(
75          camera_properties_utils.zoom_ratio_range(props))
76
77      # Load chart for scene
78      its_session_utils.load_scene(
79          cam, props, self.scene, self.tablet, self.chart_distance)
80
81      # Raise error if not FRONT or REAR facing camera
82      camera_properties_utils.check_front_or_rear_camera(props)
83
84      # set TOLs based on camera and test rig params
85      if camera_properties_utils.logical_multi_camera(props):
86        test_tols, _ = zoom_capture_utils.get_test_tols_and_cap_size(
87            cam, props, self.chart_distance, debug)
88      else:
89        test_tols = {}
90        fls = props['android.lens.info.availableFocalLengths']
91        for fl in fls:
92          test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL,
93                           zoom_capture_utils.OFFSET_RTOL)
94      logging.debug('Threshold levels to be used for testing: %s', test_tols)
95
96      # get max preview size
97      preview_size = preview_processing_utils.get_max_preview_test_size(
98          cam, self.camera_id)
99      size = [int(x) for x in preview_size.split('x')]
100      logging.debug('preview_size = %s', preview_size)
101      logging.debug('size = %s', size)
102
103      # Determine test zoom range and step size
104      z_range = props['android.control.zoomRatioRange']
105      logging.debug('z_range = %s', str(z_range))
106      z_min, z_max, z_step_size = zoom_capture_utils.get_preview_zoom_params(
107          z_range, _NUM_STEPS)
108      camera_properties_utils.skip_unless(
109          z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH)
110
111      # recording preview
112      capture_results, file_list = (
113          preview_processing_utils.preview_over_zoom_range(
114              self.dut, cam, preview_size, z_min, z_max, z_step_size, log_path)
115      )
116
117      test_data = []
118      test_data_index = 0
119      all_aruco_ids = []
120      all_aruco_corners = []
121      img_paths = []
122      # Initialize video writer
123      fourcc = cv2.VideoWriter_fourcc(*_MP4V)
124      uncompressed_video = os.path.join(log_path,
125                                        'output_frames_uncompressed.mp4')
126      out = cv2.VideoWriter(uncompressed_video, fourcc, _FPS,
127                            (size[0], size[1]))
128
129      physical_ids = set()
130      for capture_result, img_name in zip(capture_results, file_list):
131        z = float(capture_result['android.control.zoomRatio'])
132        if camera_properties_utils.logical_multi_camera(props):
133          phy_id = capture_result['android.logicalMultiCamera.activePhysicalId']
134        else:
135          phy_id = None
136        if phy_id:
137          physical_ids.add(phy_id)
138        logging.debug('Physical IDs: %s', physical_ids)
139
140        # read image
141        img_bgr = cv2.imread(os.path.join(log_path, img_name))
142
143        # add path to image name
144        img_path = f'{os.path.join(self.log_path, img_name)}'
145
146        # determine radius tolerance of capture
147        cap_fl = capture_result['android.lens.focalLength']
148        radius_tol, offset_tol = test_tols.get(
149            cap_fl,
150            (zoom_capture_utils.RADIUS_RTOL, zoom_capture_utils.OFFSET_RTOL)
151        )
152
153        # Find ArUco markers
154        # TODO: b/370585114 - improve test time and skip saving debug images.
155        try:
156          corners, ids, _ = opencv_processing_utils.find_aruco_markers(
157              img_bgr,
158              (f'{os.path.join(log_path, img_name)}_{z:.2f}_'
159               f'ArUco.{zoom_capture_utils.JPEG_STR}'),
160              aruco_marker_count=1
161          )
162        except AssertionError as e:
163          logging.debug('Could not find ArUco marker at zoom ratio %.2f: %s',
164                        z, e)
165          break
166
167        all_aruco_corners.append([corner[0] for corner in corners])
168        all_aruco_ids.append([id[0] for id in ids])
169        img_paths.append(img_path)
170
171        test_data.append(
172            zoom_capture_utils.ZoomTestData(
173                result_zoom=z,
174                radius_tol=radius_tol,
175                offset_tol=offset_tol,
176                focal_length=cap_fl,
177                physical_id=phy_id
178            )
179        )
180
181        logging.debug('test_data[%d] = %s', test_data_index,
182                      test_data[test_data_index])
183        test_data_index = test_data_index + 1
184
185      # Find ArUco markers in all captures and update test data
186      zoom_capture_utils.update_zoom_test_data_with_shared_aruco_marker(
187          test_data, all_aruco_ids, all_aruco_corners, size)
188      # Mark ArUco marker center and image center
189      opencv_processing_utils.mark_zoom_images_to_video(
190          out, img_paths, test_data)
191
192      out.release()
193      # Remove png files
194      for path in img_paths:
195        its_session_utils.remove_file(path)
196
197      # --- Compress Video ---
198      compressed_video = os.path.join(log_path, 'output_frames.mp4')
199      compress_video(uncompressed_video, compressed_video)
200
201      os.remove(uncompressed_video)
202
203      plot_name_stem = f'{os.path.join(log_path, _NAME)}'
204      # TODO: b/369852004 - decrease TOL for test_preview_zoom
205      if not zoom_capture_utils.verify_preview_zoom_results(
206          test_data, size, z_max, z_min, z_step_size, plot_name_stem):
207        first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
208        failure_msg = f'{_NAME} failed! Check test_log.DEBUG for errors'
209        if first_api_level >= its_session_utils.ANDROID15_API_LEVEL:
210          raise AssertionError(failure_msg)
211        else:
212          raise AssertionError(f'{its_session_utils.NOT_YET_MANDATED_MESSAGE}'
213                               f'\n\n{failure_msg}')
214
215if __name__ == '__main__':
216  test_runner.main()
217