• 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"""Utility functions for zoom capture.
15"""
16
17import logging
18import math
19
20import camera_properties_utils
21import capture_request_utils
22import image_processing_utils
23import numpy as np
24import opencv_processing_utils
25
26_CIRCLE_COLOR = 0  # [0: black, 255: white]
27_CIRCLE_AR_RTOL = 0.15  # contour width vs height (aspect ratio)
28_CIRCLISH_RTOL = 0.05  # contour area vs ideal circle area pi*((w+h)/4)**2
29_MIN_AREA_RATIO = 0.00013  # Found empirically with partners
30_MIN_CIRCLE_PTS = 25
31_MIN_FOCUS_DIST_TOL = 0.80  # allow charts a little closer than min
32_OFFSET_ATOL = 10  # number of pixels
33_OFFSET_RTOL_MIN_FD = 0.30
34_RADIUS_RTOL_MIN_FD = 0.15
35OFFSET_RTOL = 0.15
36RADIUS_RTOL = 0.10
37ZOOM_MAX_THRESH = 10.0
38
39
40def get_test_tols_and_cap_size(cam, props, chart_distance, debug):
41  """Determine the tolerance per camera based on test rig and camera params.
42
43  Cameras are pre-filtered to only include supportable cameras.
44  Supportable cameras are: YUV(RGB)
45
46  Args:
47    cam: camera object
48    props: dict; physical camera properties dictionary
49    chart_distance: float; distance to chart in cm
50    debug: boolean; log additional data
51
52  Returns:
53    dict of TOLs with camera focal length as key
54    largest common size across all cameras
55  """
56  ids = camera_properties_utils.logical_multi_camera_physical_ids(props)
57  physical_props = {}
58  physical_ids = []
59  for i in ids:
60    physical_props[i] = cam.get_camera_properties_by_id(i)
61    # find YUV capable physical cameras
62    if camera_properties_utils.backward_compatible(physical_props[i]):
63      physical_ids.append(i)
64
65  # find physical camera focal lengths that work well with rig
66  chart_distance_m = abs(chart_distance)/100  # convert CM to M
67  test_tols = {}
68  test_yuv_sizes = []
69  for i in physical_ids:
70    yuv_sizes = capture_request_utils.get_available_output_sizes(
71        'yuv', physical_props[i])
72    test_yuv_sizes.append(yuv_sizes)
73    if debug:
74      logging.debug('cam[%s] yuv sizes: %s', i, str(yuv_sizes))
75
76    # determine if minimum focus distance is less than rig depth
77    min_fd = physical_props[i]['android.lens.info.minimumFocusDistance']
78    for fl in physical_props[i]['android.lens.info.availableFocalLengths']:
79      logging.debug('cam[%s] min_fd: %.3f (diopters), fl: %.2f', i, min_fd, fl)
80      if (math.isclose(min_fd, 0.0, rel_tol=1E-6) or  # fixed focus
81          (1.0/min_fd < chart_distance_m*_MIN_FOCUS_DIST_TOL)):
82        test_tols[fl] = (RADIUS_RTOL, OFFSET_RTOL)
83      else:
84        test_tols[fl] = (_RADIUS_RTOL_MIN_FD, _OFFSET_RTOL_MIN_FD)
85        logging.debug('loosening RTOL for cam[%s]: '
86                      'min focus distance too large.', i)
87  # find intersection of formats for max common format
88  common_sizes = list(set.intersection(*[set(list) for list in test_yuv_sizes]))
89  if debug:
90    logging.debug('common_fmt: %s', max(common_sizes))
91
92  return test_tols, max(common_sizes)
93
94
95def get_center_circle(img, img_name, size, zoom_ratio, min_zoom_ratio, debug):
96  """Find circle closest to image center for scene with multiple circles.
97
98  If circle is not found due to zoom ratio being larger than ZOOM_MAX_THRESH
99  or the circle being cropped, None is returned.
100
101  Args:
102    img: numpy img array with pixel values in [0,255].
103    img_name: str file name for saved image
104    size: width, height of the image
105    zoom_ratio: zoom_ratio for the particular capture
106    min_zoom_ratio: min_zoom_ratio supported by the camera device
107    debug: boolean to save extra data
108
109  Returns:
110    circle: [center_x, center_y, radius] if found, else None
111  """
112  # Create a copy since convert_image_to_uint8 uses mutable np array methods
113  imgc = np.copy(img)
114  # convert [0, 1] image to [0, 255] and cast as uint8
115  imgc = image_processing_utils.convert_image_to_uint8(imgc)
116
117  # Find the center circle in img
118  try:
119    circle = opencv_processing_utils.find_center_circle(
120        imgc, img_name, _CIRCLE_COLOR, circle_ar_rtol=_CIRCLE_AR_RTOL,
121        circlish_rtol=_CIRCLISH_RTOL,
122        min_area=_MIN_AREA_RATIO * size[0] * size[1] * zoom_ratio * zoom_ratio,
123        min_circle_pts=_MIN_CIRCLE_PTS, debug=debug)
124    if opencv_processing_utils.is_circle_cropped(circle, size):
125      logging.debug('zoom %.2f is too large! Skip further captures', zoom_ratio)
126      return None
127  except AssertionError as e:
128    if zoom_ratio / min_zoom_ratio >= ZOOM_MAX_THRESH:
129      return None
130    else:
131      raise AssertionError(
132          'No circle detected for zoom ratio <= '
133          f'{ZOOM_MAX_THRESH}. '
134          'Take pictures according to instructions carefully!') from e
135  return circle
136
137
138def verify_zoom_results(test_data, size, z_max, z_min):
139  """Verify that the output images' zoom level reflects the correct zoom ratios.
140
141  This test verifies that the center and radius of the circles in the output
142  images reflects the zoom ratios being set. The larger the zoom ratio, the
143  larger the circle. And the distance from the center of the circle to the
144  center of the image is proportional to the zoom ratio as well.
145
146  Args:
147    test_data: dict; contains the detected circles for each zoom value
148    size: array; the width and height of the images
149    z_max: float; the maximum zoom ratio being tested
150    z_min: float; the minimum zoom ratio being tested
151
152  Returns:
153    Boolean whether the test passes (True) or not (False)
154  """
155  # assert some range is tested before circles get too big
156  test_failed = False
157  zoom_max_thresh = ZOOM_MAX_THRESH
158  z_max_ratio = z_max / z_min
159  if z_max_ratio < ZOOM_MAX_THRESH:
160    zoom_max_thresh = z_max_ratio
161  test_data_max_z = (test_data[max(test_data.keys())]['z'] /
162                     test_data[min(test_data.keys())]['z'])
163  logging.debug('test zoom ratio max: %.2f', test_data_max_z)
164  if test_data_max_z < zoom_max_thresh:
165    test_failed = True
166    e_msg = (f'Max zoom ratio tested: {test_data_max_z:.4f}, '
167             f'range advertised min: {z_min}, max: {z_max} '
168             f'THRESH: {zoom_max_thresh}')
169    logging.error(e_msg)
170
171  # initialize relative size w/ zoom[0] for diff zoom ratio checks
172  radius_0 = float(test_data[0]['circle'][2])
173  z_0 = float(test_data[0]['z'])
174
175  for i, data in test_data.items():
176    logging.debug('Zoom: %.2f, fl: %.2f', data['z'], data['fl'])
177    offset_xy = [(data['circle'][0] - size[0] // 2),
178                 (data['circle'][1] - size[1] // 2)]
179    logging.debug('Circle r: %.1f, center offset x, y: %d, %d',
180                  data['circle'][2], offset_xy[0], offset_xy[1])
181    z_ratio = data['z'] / z_0
182
183    # check relative size against zoom[0]
184    radius_ratio = data['circle'][2] / radius_0
185    logging.debug('r ratio req: %.3f, measured: %.3f',
186                  z_ratio, radius_ratio)
187    if not math.isclose(z_ratio, radius_ratio, rel_tol=data['r_tol']):
188      test_failed = True
189      e_msg = (f"Circle radius in capture taken at {z_0:.2f} "
190               "was expected to increase in capture taken at "
191               f"{data['z']:.2f} by {data['z']:.2f}/{z_0:.2f}="
192               f"{z_ratio:.2f}, but it increased by "
193               f"{radius_ratio:.2f}. RTOL: {data['r_tol']}")
194      logging.error(e_msg)
195
196    # check relative offset against init vals w/ no focal length change
197    if i == 0 or test_data[i-1]['fl'] != data['fl']:  # set init values
198      z_init = float(data['z'])
199      offset_hypot_init = math.hypot(offset_xy[0], offset_xy[1])
200      logging.debug('offset_hypot_init: %.3f', offset_hypot_init)
201    else:  # check
202      z_ratio = data['z'] / z_init
203      offset_hypot_rel = math.hypot(offset_xy[0], offset_xy[1]) / z_ratio
204      logging.debug('offset_hypot_rel: %.3f', offset_hypot_rel)
205
206      rel_tol = data['o_tol']
207      if not math.isclose(offset_hypot_init, offset_hypot_rel,
208                          rel_tol=rel_tol, abs_tol=_OFFSET_ATOL):
209        test_failed = True
210        e_msg = (f"zoom: {data['z']:.2f}, "
211                 f'offset init: {offset_hypot_init:.4f}, '
212                 f'offset rel: {offset_hypot_rel:.4f}, '
213                 f'RTOL: {rel_tol}, ATOL: {_OFFSET_ATOL}')
214        logging.error(e_msg)
215
216  return not test_failed
217
218