• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 camera_properties_utils
22import capture_request_utils
23import image_processing_utils
24import its_base_test
25import its_session_utils
26from mobly import test_runner
27import numpy as np
28import zoom_capture_utils
29
30
31_CIRCLE_COLOR = 0  # [0: black, 255: white]
32_CIRCLE_AR_RTOL = 0.15  # contour width vs height (aspect ratio)
33_CIRCLISH_RTOL = 0.05  # contour area vs ideal circle area pi*((w+h)/4)**2
34_CONTINUOUS_PICTURE_MODE = 4  # continuous picture AF mode
35_MIN_AREA_RATIO = 0.00015  # based on 2000/(4000x3000) pixels
36_MIN_CIRCLE_PTS = 25
37_NAME = os.path.splitext(os.path.basename(__file__))[0]
38_NUM_STEPS = 10
39_SMOOTH_ZOOM_STEP = 1.1  # [1.0, 1.1] as a reference smooth zoom step
40
41
42class LowLatencyZoomTest(its_base_test.ItsBaseTest):
43  """Test the camera low latency zoom behavior.
44
45  On supported devices, set control.settingsOverride to ZOOM
46  to enable low latency zoom and do a burst capture of N frames.
47
48  Make sure that the zoomRatio in the capture result is reflected
49  in the captured image.
50
51  If the device's firstApiLevel is V, make sure the zoom steps are
52  small and logarithmic to simulate a smooth zoom experience.
53  """
54
55  def test_low_latency_zoom(self):
56    with its_session_utils.ItsSession(
57        device_id=self.dut.serial,
58        camera_id=self.camera_id,
59        hidden_physical_id=self.hidden_physical_id) as cam:
60      props = cam.get_camera_properties()
61      props = cam.override_with_hidden_physical_camera_props(props)
62      camera_properties_utils.skip_unless(
63          camera_properties_utils.zoom_ratio_range(props) and
64          camera_properties_utils.low_latency_zoom(props))
65
66      # Load chart for scene
67      its_session_utils.load_scene(
68          cam, props, self.scene, self.tablet, self.chart_distance)
69
70      # Determine test zoom range
71      z_range = props['android.control.zoomRatioRange']
72      debug = self.debug_mode
73      z_min, z_max = float(z_range[0]), float(z_range[1])
74      camera_properties_utils.skip_unless(
75          z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH)
76      z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min)
77      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
78      if first_api_level <= its_session_utils.ANDROID14_API_LEVEL:
79        z_list = np.arange(z_min, z_max, (z_max - z_min) / (_NUM_STEPS - 1))
80      else:
81        # Here since we're trying to follow a log scale for moving through
82        # zoom steps from min to max we determine smooth_zoom_num_steps from
83        # the following: z_min*(SMOOTH_ZOOM_STEP^x)  = z_max. If we solve for
84        # x, we get the equation below. As an example, if z_min was 1.0
85        # and z_max was 5.0, we would go through our list of zooms tested
86        # [1.0, 1.1,  1.21,  1.331...]
87        smooth_zoom_num_steps = (
88            (math.log(z_max) - math.log(z_min)) / math.log(_SMOOTH_ZOOM_STEP))
89        z_list_logarithmic = np.arange(
90            math.log(z_min), math.log(z_max),
91            (math.log(z_max) - math.log(z_min)) / smooth_zoom_num_steps
92        )
93        z_list = [math.exp(z) for z in z_list_logarithmic]
94      z_list = np.append(z_list, z_max)
95      logging.debug('Testing zoom range: %s', str(z_list))
96
97      # set TOLs based on camera and test rig params
98      if camera_properties_utils.logical_multi_camera(props):
99        test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size(
100            cam, props, self.chart_distance, debug)
101      else:
102        test_tols = {}
103        fls = props['android.lens.info.availableFocalLengths']
104        for fl in fls:
105          test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL,
106                           zoom_capture_utils.OFFSET_RTOL)
107        yuv_size = capture_request_utils.get_largest_yuv_format(props)
108        size = [yuv_size['width'], yuv_size['height']]
109      logging.debug('capture size: %s', str(size))
110      logging.debug('test TOLs: %s', str(test_tols))
111
112      # do auto captures over zoom range and find circles with cv2
113      img_name_stem = f'{os.path.join(self.log_path, _NAME)}'
114      logging.debug('Using auto capture request')
115      fmt = 'yuv'
116      cam.do_3a(
117          zoom_ratio=z_min,
118          out_surfaces={
119              'format': fmt,
120              'width': size[0],
121              'height': size[1]
122          },
123          repeat_request=None,
124      )
125      test_failed = False
126      test_data = []
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        scaled_zoom = min(z_list[i], z_result)
151        logging.debug('Result[%d]: zoom ratio %.2f, afState %d',
152                      i, z_result, af_state)
153        img = image_processing_utils.convert_capture_to_rgb_image(
154            cap, props=props)
155        img_name = f'{img_name_stem}_{fmt}_{i}_{round(z_result, 2)}.jpg'
156        image_processing_utils.write_image(img, img_name)
157
158        # determine radius tolerance of capture
159        cap_fl = cap['metadata']['android.lens.focalLength']
160        radius_tol, offset_tol = test_tols[cap_fl]
161
162        # Find the center circle in img and check if it's cropped
163        circle = zoom_capture_utils.find_center_circle(
164            img, img_name, size, scaled_zoom, z_min, debug=debug)
165
166        test_data.append(
167            zoom_capture_utils.ZoomTestData(
168                result_zoom=z_result,
169                circle=circle,
170                radius_tol=radius_tol,
171                offset_tol=offset_tol,
172                focal_length=cap_fl
173            )
174        )
175
176      # Since we are zooming in, settings_override may change the minimum zoom
177      # value in the result metadata.
178      # This is because zoom values like: [1., 2., 3., ..., 10.] may be applied
179      # as: [4., 4., 4., .... 9., 10., 10.].
180      # If we were zooming out, we would need to change the z_max.
181      z_min = test_data[0].result_zoom
182
183      if not zoom_capture_utils.verify_zoom_results(
184          test_data, size, z_max, z_min):
185        test_failed = True
186
187    if test_failed:
188      raise AssertionError(f'{_NAME} failed! Check test_log.DEBUG for errors')
189
190if __name__ == '__main__':
191  test_runner.main()
192