xref: /aosp_15_r20/cts/apps/CameraITS/tests/scene6/test_session_characteristics_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 session characteristics zoom."""
15
16import logging
17import os
18
19import cv2
20from mobly import test_runner
21import numpy as np
22
23import its_base_test
24import camera_properties_utils
25import capture_request_utils
26import image_processing_utils
27import its_session_utils
28import opencv_processing_utils
29import zoom_capture_utils
30
31_FPS_30_60 = (30, 60)
32_FPS_SELECTION_ATOL = 0.01
33_FPS_ATOL = 0.8
34_MAX_FPS_INDEX = 1
35_MAX_STREAM_COUNT = 2
36_NAME = os.path.splitext(os.path.basename(__file__))[0]
37_SEC_TO_NSEC = 1_000_000_000
38
39
40class SessionCharacteristicsZoomTest(its_base_test.ItsBaseTest):
41  """Tests camera capture session specific zoom behavior.
42
43  The combination of camera features tested by this function are:
44  - Preview stabilization
45  - Target FPS range
46  - HLG 10-bit HDR
47  """
48
49  def test_session_characteristics_zoom(self):
50    with its_session_utils.ItsSession(
51        device_id=self.dut.serial,
52        camera_id=self.camera_id) as cam:
53
54      # Skip if the device doesn't support feature combination query
55      props = cam.get_camera_properties()
56      feature_combination_query_version = props.get(
57          'android.info.sessionConfigurationQueryVersion')
58      if not feature_combination_query_version:
59        feature_combination_query_version = (
60            its_session_utils.ANDROID14_API_LEVEL
61        )
62      camera_properties_utils.skip_unless(
63          feature_combination_query_version >=
64          its_session_utils.ANDROID15_API_LEVEL)
65
66      # Raise error if not FRONT or REAR facing camera
67      camera_properties_utils.check_front_or_rear_camera(props)
68
69      # Load chart for scene
70      its_session_utils.load_scene(
71          cam, props, self.scene, self.tablet, self.chart_distance)
72
73      # set TOLs based on camera and test rig params
74      debug = self.debug_mode
75      if camera_properties_utils.logical_multi_camera(props):
76        test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size(
77            cam, props, self.chart_distance, debug)
78      else:
79        test_tols = {}
80        fls = props['android.lens.info.availableFocalLengths']
81        for fl in fls:
82          test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL,
83                           zoom_capture_utils.OFFSET_RTOL)
84        yuv_size = capture_request_utils.get_largest_format('yuv', props)
85        size = [yuv_size['width'], yuv_size['height']]
86      logging.debug('capture size: %s', size)
87      logging.debug('test TOLs: %s', test_tols)
88
89      # List of queryable stream combinations
90      combinations_str, combinations = cam.get_queryable_stream_combinations()
91      logging.debug('Queryable stream combinations: %s', combinations_str)
92
93      # Stabilization modes. Make sure to test ON first.
94      stabilization_params = []
95      stabilization_modes = props[
96          'android.control.availableVideoStabilizationModes']
97      if (camera_properties_utils.STABILIZATION_MODE_PREVIEW in
98          stabilization_modes):
99        stabilization_params.append(
100            camera_properties_utils.STABILIZATION_MODE_PREVIEW)
101      stabilization_params.append(
102          camera_properties_utils.STABILIZATION_MODE_OFF)
103      logging.debug('stabilization modes: %s', stabilization_params)
104
105      configs = props['android.scaler.streamConfigurationMap'][
106          'availableStreamConfigurations']
107      fps_ranges = camera_properties_utils.get_ae_target_fps_ranges(props)
108
109      test_failures = []
110      for stream_combination in combinations:
111        streams_name = stream_combination['name']
112        min_frame_duration = 0
113        configured_streams = []
114        skip = False
115
116        # Only supports combinations of up to 2 streams
117        if len(stream_combination['combination']) > _MAX_STREAM_COUNT:
118          raise AssertionError(
119              f'stream combination cannot exceed {_MAX_STREAM_COUNT} streams.')
120
121        # Skip if combinations contains only 1 stream, which is preview
122        if len(stream_combination['combination']) == 1:
123          continue
124
125        for i, stream in enumerate(stream_combination['combination']):
126          fmt = None
127          size = [int(e) for e in stream['size'].split('x')]
128          if stream['format'] == its_session_utils.PRIVATE_FORMAT:
129            fmt = capture_request_utils.FMT_CODE_PRIV
130          elif stream['format'] == 'jpeg':
131            fmt = capture_request_utils.FMT_CODE_JPEG
132          elif stream['format'] == its_session_utils.JPEG_R_FMT_STR:
133            fmt = capture_request_utils.FMT_CODE_JPEG_R
134          elif stream['format'] == 'yuv':
135            fmt = capture_request_utils.FMT_CODE_YUV
136
137          # Assume first stream is always a preview stream with priv format
138          if i == 0 and fmt != capture_request_utils.FMT_CODE_PRIV:
139            raise AssertionError(
140                'first stream in the combination must be priv format preview.')
141
142          # Second stream must be jpeg or yuv for zoom test. If not, skip
143          if (i == 1 and fmt != capture_request_utils.FMT_CODE_JPEG and
144              fmt != capture_request_utils.FMT_CODE_JPEG_R and
145              fmt != capture_request_utils.FMT_CODE_YUV):
146            logging.debug(
147                'second stream format %s is not yuv/jpeg/jpeg_r. Skip',
148                stream['format'])
149            skip = True
150            break
151
152          # Skip if size and format are not supported by the device.
153          config = [x for x in configs if
154                    x['format'] == fmt and
155                    x['width'] == size[0] and
156                    x['height'] == size[1]]
157          if not config:
158            logging.debug(
159                'stream combination %s not supported. Skip', streams_name)
160            skip = True
161            break
162
163          min_frame_duration = max(
164              config[0]['minFrameDuration'], min_frame_duration)
165          logging.debug(
166              'format is %s, min_frame_duration is %d}',
167              stream['format'], config[0]['minFrameDuration'])
168          configured_streams.append(
169              {'format': stream['format'], 'width': size[0], 'height': size[1]})
170
171        if skip:
172          continue
173
174        # FPS ranges
175        max_achievable_fps = _SEC_TO_NSEC / min_frame_duration
176        fps_params = [fps for fps in fps_ranges if (
177            fps[_MAX_FPS_INDEX] in _FPS_30_60 and
178            max_achievable_fps >= fps[_MAX_FPS_INDEX] - _FPS_SELECTION_ATOL)]
179
180        for fps_range in fps_params:
181          # HLG10. Make sure to test ON first.
182          hlg10_params = []
183          if camera_properties_utils.dynamic_range_ten_bit(props):
184            hlg10_params.append(True)
185          hlg10_params.append(False)
186
187          features_tested = []  # feature combinations already tested
188          for hlg10 in hlg10_params:
189            # Construct output surfaces
190            output_surfaces = []
191            for configured_stream in configured_streams:
192              hlg10_stream = (hlg10 and configured_stream['format'] ==
193                              its_session_utils.PRIVATE_FORMAT)
194              output_surfaces.append({'format': configured_stream['format'],
195                                      'width': configured_stream['width'],
196                                      'height': configured_stream['height'],
197                                      'hlg10': hlg10_stream})
198
199            for stabilize in stabilization_params:
200              settings = {
201                  'android.control.videoStabilizationMode': stabilize,
202                  'android.control.aeTargetFpsRange': fps_range,
203              }
204              combination_name = (f'streams_{streams_name}_hlg10_{hlg10}'
205                                  f'_stabilization_{stabilize}_fps_range_'
206                                  f'_{fps_range[0]}_{fps_range[1]}')
207              logging.debug('combination name: %s', combination_name)
208
209              # Is the feature combination supported?
210              if not cam.is_stream_combination_supported(
211                  output_surfaces, settings):
212                logging.debug('%s not supported', combination_name)
213                break
214
215              # If a superset of features are already tested, skip.
216              # pylint: disable=line-too-long
217              is_stabilized = (
218                  stabilize == camera_properties_utils.STABILIZATION_MODE_PREVIEW
219              )
220              skip_test = its_session_utils.check_and_update_features_tested(
221                  features_tested, hlg10, is_stabilized)
222              if skip_test:
223                continue
224
225              # Get zoom ratio range
226              session_props = cam.get_session_properties(
227                  output_surfaces, settings)
228              z_range = session_props.get('android.control.zoomRatioRange')
229
230              debug = self.debug_mode
231              z_min, z_max = float(z_range[0]), float(z_range[1])
232              camera_properties_utils.skip_unless(
233                  z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH)
234              z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min)
235              z_list = [z_min, z_max]
236              if z_min != 1:
237                z_list = np.insert(z_list, 0, 1)  # make reference zoom 1x
238              logging.debug('Testing zoom range: %s', z_list)
239
240              # do captures over zoom range and find ArUco markers with cv2
241              img_name_stem = f'{os.path.join(self.log_path, _NAME)}'
242              req = capture_request_utils.auto_capture_request()
243
244              test_data = []
245              all_aruco_ids = []
246              all_aruco_corners = []
247              images = []
248              fmt_str = configured_streams[1]['format']
249              for i, z in enumerate(z_list):
250                req['android.control.zoomRatio'] = z
251                logging.debug('zoom ratio: %.3f', z)
252                cam.do_3a(
253                    zoom_ratio=z,
254                    out_surfaces=output_surfaces,
255                    repeat_request=None,
256                    first_surface_for_3a=True
257                )
258                cap = cam.do_capture(
259                    req, output_surfaces,
260                    reuse_session=True,
261                    first_surface_for_3a=True)
262
263                img = image_processing_utils.convert_capture_to_rgb_image(
264                    cap, props=props)
265                img_name = (f'{img_name_stem}_{combination_name}_{fmt_str}'
266                            f'_{z:.2f}.{zoom_capture_utils.JPEG_STR}')
267                image_processing_utils.write_image(img, img_name)
268
269                # determine radius tolerance of capture
270                cap_fl = cap['metadata']['android.lens.focalLength']
271                cap_physical_id = (
272                    cap['metadata'][
273                        'android.logicalMultiCamera.activePhysicalId']
274                )
275                result_zoom = float(
276                    cap['metadata']['android.control.zoomRatio'])
277                radius_tol, offset_tol = test_tols.get(
278                    cap_fl,
279                    (zoom_capture_utils.RADIUS_RTOL,
280                     zoom_capture_utils.OFFSET_RTOL)
281                )
282
283                # Find ArUco markers
284                bgr_img = cv2.cvtColor(
285                    image_processing_utils.convert_image_to_uint8(img),
286                    cv2.COLOR_RGB2BGR
287                )
288                try:
289                  corners, ids, _ = opencv_processing_utils.find_aruco_markers(
290                      bgr_img,
291                      (f'{img_name_stem}_{z:.2f}_'
292                       f'ArUco.{zoom_capture_utils.JPEG_STR}'),
293                      aruco_marker_count=1
294                  )
295                except AssertionError as e:
296                  logging.debug(
297                      'Could not find ArUco marker at zoom ratio %.2f: %s',
298                      z, e
299                  )
300                  break
301                all_aruco_corners.append([corner[0] for corner in corners])
302                all_aruco_ids.append([id[0] for id in ids])
303                images.append(bgr_img)
304
305                test_data.append(
306                    zoom_capture_utils.ZoomTestData(
307                        result_zoom=result_zoom,
308                        radius_tol=radius_tol,
309                        offset_tol=offset_tol,
310                        focal_length=cap_fl,
311                        physical_id=cap_physical_id
312                    )
313                )
314
315              # Find ArUco markers in all captures and update test data
316              zoom_capture_utils.update_zoom_test_data_with_shared_aruco_marker(
317                  test_data, all_aruco_ids, all_aruco_corners, size)
318              # Mark ArUco marker center and image center
319              opencv_processing_utils.mark_zoom_images(
320                  images, test_data, img_name_stem)
321              if not zoom_capture_utils.verify_zoom_results(
322                  test_data, size, z_max, z_min):
323                failure_msg = (
324                    f'{combination_name}: failed! '
325                    'Check test_log.DEBUG for errors')
326                test_failures.append(failure_msg)
327
328      if test_failures:
329        raise AssertionError(test_failures)
330
331if __name__ == '__main__':
332  test_runner.main()
333