xref: /aosp_15_r20/cts/apps/CameraITS/tests/scene6/test_low_latency_zoom.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1# Copyright 2023 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 zoom ratio scales circle sizes correctly if settings override zoom is set."""
15
16
17import logging
18import math
19import os.path
20
21import its_base_test
22import camera_properties_utils
23import capture_request_utils
24import image_processing_utils
25import its_session_utils
26import opencv_processing_utils
27import zoom_capture_utils
28import cv2
29from mobly import test_runner
30import numpy as np
31
32
33_CONTINUOUS_PICTURE_MODE = 4  # continuous picture AF mode
34_NAME = os.path.splitext(os.path.basename(__file__))[0]
35_NUM_STEPS = 10
36_SMOOTH_ZOOM_STEP = 1.1  # [1.0, 1.1] as a reference smooth zoom step
37
38
39class LowLatencyZoomTest(its_base_test.ItsBaseTest):
40  """Test the camera low latency zoom behavior.
41
42  On supported devices, set control.settingsOverride to ZOOM
43  to enable low latency zoom and do a burst capture of N frames.
44
45  Make sure that the zoomRatio in the capture result is reflected
46  in the captured image.
47
48  If the device's firstApiLevel is V, make sure the zoom steps are
49  small and logarithmic to simulate a smooth zoom experience.
50  """
51
52  def test_low_latency_zoom(self):
53    with its_session_utils.ItsSession(
54        device_id=self.dut.serial,
55        camera_id=self.camera_id,
56        hidden_physical_id=self.hidden_physical_id) as cam:
57      props = cam.get_camera_properties()
58      props = cam.override_with_hidden_physical_camera_props(props)
59      camera_properties_utils.skip_unless(
60          camera_properties_utils.zoom_ratio_range(props) and
61          camera_properties_utils.low_latency_zoom(props))
62
63      # Load chart for scene
64      its_session_utils.load_scene(
65          cam, props, self.scene, self.tablet, self.chart_distance)
66
67      # Determine test zoom range
68      z_range = props['android.control.zoomRatioRange']
69      debug = self.debug_mode
70      z_min, z_max = float(z_range[0]), float(z_range[1])
71      camera_properties_utils.skip_unless(
72          z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH)
73      z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min)
74      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
75      if first_api_level <= its_session_utils.ANDROID14_API_LEVEL:
76        z_list = np.arange(z_min, z_max, (z_max - z_min) / (_NUM_STEPS - 1))
77      else:
78        # Here since we're trying to follow a log scale for moving through
79        # zoom steps from min to max we determine smooth_zoom_num_steps from
80        # the following: z_min*(SMOOTH_ZOOM_STEP^x)  = z_max. If we solve for
81        # x, we get the equation below. As an example, if z_min was 1.0
82        # and z_max was 5.0, we would go through our list of zooms tested
83        # [1.0, 1.1,  1.21,  1.331...]
84        smooth_zoom_num_steps = (
85            (math.log(z_max) - math.log(z_min)) / math.log(_SMOOTH_ZOOM_STEP))
86        z_list_logarithmic = np.arange(
87            math.log(z_min), math.log(z_max),
88            (math.log(z_max) - math.log(z_min)) / smooth_zoom_num_steps
89        )
90        z_list = [math.exp(z) for z in z_list_logarithmic]
91      z_list = np.append(z_list, z_max)
92      logging.debug('Testing zoom range: %s', str(z_list))
93
94      # set TOLs based on camera and test rig params
95      if camera_properties_utils.logical_multi_camera(props):
96        test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size(
97            cam, props, self.chart_distance, debug)
98      else:
99        test_tols = {}
100        fls = props['android.lens.info.availableFocalLengths']
101        for fl in fls:
102          test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL,
103                           zoom_capture_utils.OFFSET_RTOL)
104        yuv_size = capture_request_utils.get_largest_format('yuv', props)
105        size = [yuv_size['width'], yuv_size['height']]
106      logging.debug('capture size: %s', str(size))
107      logging.debug('test TOLs: %s', str(test_tols))
108
109      # do auto captures over zoom range and find ArUco markers with cv2
110      img_name_stem = f'{os.path.join(self.log_path, _NAME)}'
111      logging.debug('Using auto capture request')
112      fmt = 'yuv'
113      cam.do_3a(
114          zoom_ratio=z_min,
115          out_surfaces={
116              'format': fmt,
117              'width': size[0],
118              'height': size[1]
119          },
120          repeat_request=None,
121      )
122      test_failed = False
123      test_data = []
124      all_aruco_ids = []
125      all_aruco_corners = []
126      images = []
127      reqs = []
128      req = capture_request_utils.auto_capture_request()
129      req['android.control.settingsOverride'] = (
130          camera_properties_utils.SETTINGS_OVERRIDE_ZOOM
131      )
132      req['android.control.enableZsl'] = False
133      if not camera_properties_utils.fixed_focus(props):
134        req['android.control.afMode'] = _CONTINUOUS_PICTURE_MODE
135      for z in z_list:
136        logging.debug('zoom ratio: %.2f', z)
137        req_for_zoom = req.copy()
138        req_for_zoom['android.control.zoomRatio'] = z
139        reqs.append(req_for_zoom)
140
141      # take captures at different zoom ratios
142      caps = cam.do_capture(
143          reqs, {'format': fmt, 'width': size[0], 'height': size[1]},
144          reuse_session=True)
145
146      # Check low latency zoom outputs match result metadata
147      for i, cap in enumerate(caps):
148        z_result = cap['metadata']['android.control.zoomRatio']
149        af_state = cap['metadata']['android.control.afState']
150        logging.debug('Result[%d]: zoom ratio %.2f, afState %d',
151                      i, z_result, af_state)
152        img = image_processing_utils.convert_capture_to_rgb_image(
153            cap, props=props)
154        img_name = f'{img_name_stem}_{fmt}_{i}_{round(z_result, 2)}.jpg'
155        image_processing_utils.write_image(img, img_name)
156        img_bgr = cv2.cvtColor(
157            image_processing_utils.convert_image_to_uint8(img),
158            cv2.COLOR_RGB2BGR)
159
160        # determine radius tolerance of capture
161        cap_fl = cap['metadata']['android.lens.focalLength']
162        cap_physical_id = (
163            cap['metadata']['android.logicalMultiCamera.activePhysicalId']
164        )
165        radius_tol, offset_tol = test_tols[cap_fl]
166
167        # Find the center ArUco marker in img and check if it's cropped
168        corners, ids, _ = opencv_processing_utils.find_aruco_markers(
169            img_bgr,
170            f'{img_name_stem}_{fmt}_{i}_{z_result:.2f}_ArUco.jpg',
171            aruco_marker_count=1,
172            force_greyscale=True,
173        )
174
175        all_aruco_corners.append([corner[0] for corner in corners])
176        all_aruco_ids.append([id[0] for id in ids])
177        images.append(img_bgr)
178        test_data.append(
179            zoom_capture_utils.ZoomTestData(
180                result_zoom=z_result,
181                radius_tol=radius_tol,
182                offset_tol=offset_tol,
183                focal_length=cap_fl,
184                physical_id=cap_physical_id,
185            )
186        )
187
188      # Find ArUco markers in all captures and update test data
189      zoom_capture_utils.update_zoom_test_data_with_shared_aruco_marker(
190          test_data, all_aruco_ids, all_aruco_corners, size)
191      # Mark ArUco marker center and image center
192      opencv_processing_utils.mark_zoom_images(
193          images, test_data, f'{img_name_stem}_{fmt}')
194      # Since we are zooming in, settings_override may change the minimum zoom
195      # value in the result metadata.
196      # This is because zoom values like: [1., 2., 3., ..., 10.] may be applied
197      # as: [4., 4., 4., .... 9., 10., 10.].
198      # If we were zooming out, we would need to change the z_max.
199      z_min = test_data[0].result_zoom
200
201      if not zoom_capture_utils.verify_zoom_results(
202          test_data, size, z_max, z_min):
203        test_failed = True
204
205    if test_failed:
206      raise AssertionError(f'{_NAME} failed! Check test_log.DEBUG for errors')
207
208if __name__ == '__main__':
209  test_runner.main()
210