1# Copyright 2020 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."""
15
16
17import logging
18import os.path
19
20import camera_properties_utils
21import capture_request_utils
22import image_processing_utils
23import its_base_test
24import its_session_utils
25from mobly import test_runner
26import numpy as np
27import zoom_capture_utils
28
29_CIRCLISH_RTOL = 0.05  # contour area vs ideal circle area pi*((w+h)/4)**2
30_NAME = os.path.splitext(os.path.basename(__file__))[0]
31_NUM_STEPS = 10
32_TEST_FORMATS = ['yuv']  # list so can be appended for newer Android versions
33_TEST_REQUIRED_MPC = 33
34
35
36class ZoomTest(its_base_test.ItsBaseTest):
37  """Test the camera zoom behavior."""
38
39  def test_zoom(self):
40    with its_session_utils.ItsSession(
41        device_id=self.dut.serial,
42        camera_id=self.camera_id,
43        hidden_physical_id=self.hidden_physical_id) as cam:
44      props = cam.get_camera_properties()
45      props = cam.override_with_hidden_physical_camera_props(props)
46      camera_properties_utils.skip_unless(
47          camera_properties_utils.zoom_ratio_range(props))
48
49      # Load chart for scene
50      its_session_utils.load_scene(
51          cam, props, self.scene, self.tablet, self.chart_distance)
52
53      # Determine test zoom range
54      z_range = props['android.control.zoomRatioRange']
55      debug = self.debug_mode
56      z_min, z_max = float(z_range[0]), float(z_range[1])
57      camera_properties_utils.skip_unless(
58          z_max >= z_min * zoom_capture_utils.ZOOM_MIN_THRESH)
59      z_max = min(z_max, zoom_capture_utils.ZOOM_MAX_THRESH * z_min)
60      z_list = np.arange(z_min, z_max, (z_max - z_min) / (_NUM_STEPS - 1))
61      z_list = np.append(z_list, z_max)
62      if z_min != 1:
63        z_list = np.insert(z_list, 0, 1)  # make first (reference) zoom 1x
64      logging.debug('Testing zoom range: %s', str(z_list))
65
66      # Check media performance class
67      media_performance_class = its_session_utils.get_media_performance_class(
68          self.dut.serial)
69      if (media_performance_class >= _TEST_REQUIRED_MPC and
70          cam.is_primary_camera() and
71          cam.has_ultrawide_camera(facing=props['android.lens.facing']) and
72          int(z_min) >= 1):
73        raise AssertionError(
74            f'With primary camera {self.camera_id}, '
75            f'MPC >= {_TEST_REQUIRED_MPC}, and '
76            'an ultrawide camera facing in the same direction as the primary, '
77            'zoom_ratio minimum must be less than 1.0. '
78            f'Found media performance class {media_performance_class} '
79            f'and minimum zoom {z_min}.')
80
81      # set TOLs based on camera and test rig params
82      if camera_properties_utils.logical_multi_camera(props):
83        test_tols, size = zoom_capture_utils.get_test_tols_and_cap_size(
84            cam, props, self.chart_distance, debug)
85      else:
86        test_tols = {}
87        fls = props['android.lens.info.availableFocalLengths']
88        for fl in fls:
89          test_tols[fl] = (zoom_capture_utils.RADIUS_RTOL,
90                           zoom_capture_utils.OFFSET_RTOL)
91        yuv_size = capture_request_utils.get_largest_yuv_format(props)
92        size = [yuv_size['width'], yuv_size['height']]
93      logging.debug('capture size: %s', str(size))
94      logging.debug('test TOLs: %s', str(test_tols))
95
96      # determine first API level and test_formats to test
97      test_formats = _TEST_FORMATS
98      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
99      if first_api_level >= its_session_utils.ANDROID14_API_LEVEL:
100        test_formats.append(zoom_capture_utils.JPEG_STR)
101
102      # do captures over zoom range and find circles with cv2
103      img_name_stem = f'{os.path.join(self.log_path, _NAME)}'
104      req = capture_request_utils.auto_capture_request()
105      test_failed = False
106      for fmt in test_formats:
107        logging.debug('testing %s format', fmt)
108        test_data = []
109        for z in z_list:
110          req['android.control.zoomRatio'] = z
111          logging.debug('zoom ratio: %.3f', z)
112          cam.do_3a(
113              zoom_ratio=z,
114              out_surfaces={
115                  'format': fmt,
116                  'width': size[0],
117                  'height': size[1]
118              },
119              repeat_request=None,
120          )
121          cap = cam.do_capture(
122              req, {'format': fmt, 'width': size[0], 'height': size[1]},
123              reuse_session=True)
124
125          img = image_processing_utils.convert_capture_to_rgb_image(
126              cap, props=props)
127          img_name = (f'{img_name_stem}_{fmt}_{round(z, 2)}.'
128                      f'{zoom_capture_utils.JPEG_STR}')
129          image_processing_utils.write_image(img, img_name)
130
131          # determine radius tolerance of capture
132          cap_fl = cap['metadata']['android.lens.focalLength']
133          radius_tol, offset_tol = test_tols.get(
134              cap_fl,
135              (zoom_capture_utils.RADIUS_RTOL, zoom_capture_utils.OFFSET_RTOL)
136          )
137
138          # Scale circlish RTOL for low zoom ratios
139          if z < 1:
140            circlish_rtol = _CIRCLISH_RTOL / z
141          else:
142            circlish_rtol = _CIRCLISH_RTOL
143
144          # Find the center circle in img and check if it's cropped
145          circle = zoom_capture_utils.find_center_circle(
146              img, img_name, size, z, z_list[0], circlish_rtol=circlish_rtol,
147              debug=debug)
148
149          # Zoom is too large to find center circle
150          if circle is None:
151            break
152          test_data.append(
153              zoom_capture_utils.ZoomTestData(
154                  result_zoom=z,
155                  circle=circle,
156                  radius_tol=radius_tol,
157                  offset_tol=offset_tol,
158                  focal_length=cap_fl
159              )
160          )
161
162        if not zoom_capture_utils.verify_zoom_results(
163            test_data, size, z_max, z_min):
164          test_failed = True
165
166    if test_failed:
167      raise AssertionError(f'{_NAME} failed! Check test_log.DEBUG for errors')
168
169if __name__ == '__main__':
170  test_runner.main()
171