xref: /aosp_15_r20/cts/apps/CameraITS/utils/opencv_processing_utils.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1*b7c941bbSAndroid Build Coastguard Worker# Copyright 2016 The Android Open Source Project
2*b7c941bbSAndroid Build Coastguard Worker#
3*b7c941bbSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*b7c941bbSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*b7c941bbSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*b7c941bbSAndroid Build Coastguard Worker#
7*b7c941bbSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*b7c941bbSAndroid Build Coastguard Worker#
9*b7c941bbSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*b7c941bbSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*b7c941bbSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*b7c941bbSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*b7c941bbSAndroid Build Coastguard Worker# limitations under the License.
14*b7c941bbSAndroid Build Coastguard Worker"""Image processing utilities using openCV."""
15*b7c941bbSAndroid Build Coastguard Worker
16*b7c941bbSAndroid Build Coastguard Worker
17*b7c941bbSAndroid Build Coastguard Workerimport logging
18*b7c941bbSAndroid Build Coastguard Workerimport math
19*b7c941bbSAndroid Build Coastguard Workerimport os
20*b7c941bbSAndroid Build Coastguard Workerimport pathlib
21*b7c941bbSAndroid Build Coastguard Workerimport cv2
22*b7c941bbSAndroid Build Coastguard Workerimport numpy
23*b7c941bbSAndroid Build Coastguard Workerimport scipy.spatial
24*b7c941bbSAndroid Build Coastguard Worker
25*b7c941bbSAndroid Build Coastguard Workerimport camera_properties_utils
26*b7c941bbSAndroid Build Coastguard Workerimport capture_request_utils
27*b7c941bbSAndroid Build Coastguard Workerimport error_util
28*b7c941bbSAndroid Build Coastguard Workerimport image_processing_utils
29*b7c941bbSAndroid Build Coastguard Worker
30*b7c941bbSAndroid Build Coastguard WorkerAE_AWB_METER_WEIGHT = 1000  # 1 - 1000 with 1000 the highest
31*b7c941bbSAndroid Build Coastguard WorkerANGLE_CHECK_TOL = 1  # degrees
32*b7c941bbSAndroid Build Coastguard WorkerANGLE_NUM_MIN = 10  # Minimum number of angles for find_angle() to be valid
33*b7c941bbSAndroid Build Coastguard WorkerARUCO_DETECTOR_ATTRIBUTE_NAME = 'ArucoDetector'
34*b7c941bbSAndroid Build Coastguard WorkerARUCO_CORNER_COUNT = 4  # total of 4 corners to a aruco marker
35*b7c941bbSAndroid Build Coastguard Worker
36*b7c941bbSAndroid Build Coastguard WorkerTEST_IMG_DIR = os.path.join(os.environ['CAMERA_ITS_TOP'], 'test_images')
37*b7c941bbSAndroid Build Coastguard WorkerCH_FULL_SCALE = 255
38*b7c941bbSAndroid Build Coastguard WorkerCHART_FILE = os.path.join(TEST_IMG_DIR, 'ISO12233.png')
39*b7c941bbSAndroid Build Coastguard WorkerCHART_HEIGHT_31CM = 13.5  # cm height of chart for 31cm distance chart
40*b7c941bbSAndroid Build Coastguard WorkerCHART_HEIGHT_22CM = 9.5  # cm height of chart for 22cm distance chart
41*b7c941bbSAndroid Build Coastguard WorkerCHART_DISTANCE_90CM = 90.0  # cm
42*b7c941bbSAndroid Build Coastguard WorkerCHART_DISTANCE_31CM = 31.0  # cm
43*b7c941bbSAndroid Build Coastguard WorkerCHART_DISTANCE_22CM = 22.0  # cm
44*b7c941bbSAndroid Build Coastguard WorkerCHART_SCALE_RTOL = 0.1
45*b7c941bbSAndroid Build Coastguard WorkerCHART_SCALE_START = 0.65
46*b7c941bbSAndroid Build Coastguard WorkerCHART_SCALE_STOP = 1.35
47*b7c941bbSAndroid Build Coastguard WorkerCHART_SCALE_STEP = 0.025
48*b7c941bbSAndroid Build Coastguard Worker
49*b7c941bbSAndroid Build Coastguard WorkerCIRCLE_AR_ATOL = 0.1  # circle aspect ratio tolerance
50*b7c941bbSAndroid Build Coastguard WorkerCIRCLISH_ATOL = 0.10  # contour area vs ideal circle area & aspect ratio TOL
51*b7c941bbSAndroid Build Coastguard WorkerCIRCLISH_LOW_RES_ATOL = 0.15  # loosen for low res images
52*b7c941bbSAndroid Build Coastguard WorkerCIRCLE_MIN_PTS = 20
53*b7c941bbSAndroid Build Coastguard WorkerCIRCLE_RADIUS_NUMPTS_THRESH = 2  # contour num_pts/radius: empirically ~3x
54*b7c941bbSAndroid Build Coastguard WorkerCIRCLE_COLOR_ATOL = 0.05  # circle color fill tolerance
55*b7c941bbSAndroid Build Coastguard WorkerCIRCLE_LOCATION_VARIATION_RTOL = 0.05  # tolerance to remove similar circles
56*b7c941bbSAndroid Build Coastguard Worker
57*b7c941bbSAndroid Build Coastguard WorkerCV2_CONTRAST_ALPHA = 1.25  # contrast
58*b7c941bbSAndroid Build Coastguard WorkerCV2_CONTRAST_BETA = 0  # brightness
59*b7c941bbSAndroid Build Coastguard WorkerCV2_THESHOLD_LOWER_BLACK = 0
60*b7c941bbSAndroid Build Coastguard WorkerCV2_LINE_THICKNESS = 3  # line thickness for drawing on images
61*b7c941bbSAndroid Build Coastguard WorkerCV2_BLACK = (0, 0, 0)
62*b7c941bbSAndroid Build Coastguard WorkerCV2_BLUE = (0, 0, 255)
63*b7c941bbSAndroid Build Coastguard WorkerCV2_RED = (255, 0, 0)  # color in cv2 to draw lines
64*b7c941bbSAndroid Build Coastguard WorkerCV2_RED_NORM = tuple(numpy.array(CV2_RED) / 255)
65*b7c941bbSAndroid Build Coastguard WorkerCV2_GREEN = (0, 255, 0)
66*b7c941bbSAndroid Build Coastguard WorkerCV2_GREEN_NORM = tuple(numpy.array(CV2_GREEN) / 255)
67*b7c941bbSAndroid Build Coastguard WorkerCV2_WHITE = (255, 255, 255)
68*b7c941bbSAndroid Build Coastguard WorkerCV2_YELLOW = (255, 255, 0)
69*b7c941bbSAndroid Build Coastguard WorkerCV2_THRESHOLD_BLOCK_SIZE = 11
70*b7c941bbSAndroid Build Coastguard WorkerCV2_THRESHOLD_CONSTANT = 2
71*b7c941bbSAndroid Build Coastguard WorkerCV2_ZOOM_MARKER_SIZE = 30
72*b7c941bbSAndroid Build Coastguard WorkerCV2_ZOOM_MARKER_THICKNESS = 3
73*b7c941bbSAndroid Build Coastguard Worker
74*b7c941bbSAndroid Build Coastguard WorkerCV2_HOME_DIRECTORY = os.path.dirname(cv2.__file__)
75*b7c941bbSAndroid Build Coastguard WorkerCV2_ALTERNATE_DIRECTORY = pathlib.Path(CV2_HOME_DIRECTORY).parents[3]
76*b7c941bbSAndroid Build Coastguard WorkerHAARCASCADE_FILE_NAME = 'haarcascade_frontalface_default.xml'
77*b7c941bbSAndroid Build Coastguard Worker
78*b7c941bbSAndroid Build Coastguard WorkerFACES_ALIGNED_MIN_NUM = 2
79*b7c941bbSAndroid Build Coastguard WorkerFACE_CENTER_MATCH_TOL_X = 10  # 10 pixels or ~1.5% in 640x480 image
80*b7c941bbSAndroid Build Coastguard WorkerFACE_CENTER_MATCH_TOL_Y = 20  # 20 pixels or ~4% in 640x480 image
81*b7c941bbSAndroid Build Coastguard WorkerFACE_CENTER_MIN_LOGGING_DIST = 50
82*b7c941bbSAndroid Build Coastguard WorkerFACE_MIN_CENTER_DELTA = 15
83*b7c941bbSAndroid Build Coastguard Worker
84*b7c941bbSAndroid Build Coastguard WorkerFOV_THRESH_TELE25 = 25
85*b7c941bbSAndroid Build Coastguard WorkerFOV_THRESH_TELE40 = 40
86*b7c941bbSAndroid Build Coastguard WorkerFOV_THRESH_TELE = 60
87*b7c941bbSAndroid Build Coastguard WorkerFOV_THRESH_UW = 90
88*b7c941bbSAndroid Build Coastguard Worker
89*b7c941bbSAndroid Build Coastguard WorkerIMAGE_ROTATION_THRESHOLD = 40  # rotation by 20 pixels
90*b7c941bbSAndroid Build Coastguard Worker
91*b7c941bbSAndroid Build Coastguard WorkerLOW_RES_IMG_THRESH = 320 * 240
92*b7c941bbSAndroid Build Coastguard Worker
93*b7c941bbSAndroid Build Coastguard WorkerNUM_AE_AWB_REGIONS = 4
94*b7c941bbSAndroid Build Coastguard Worker
95*b7c941bbSAndroid Build Coastguard WorkerOPT_VALUE_THRESH = 0.5  # Max opt value is ~0.8
96*b7c941bbSAndroid Build Coastguard Worker
97*b7c941bbSAndroid Build Coastguard WorkerSCALE_CHART_33_PERCENT = 0.33
98*b7c941bbSAndroid Build Coastguard WorkerSCALE_CHART_67_PERCENT = 0.67
99*b7c941bbSAndroid Build Coastguard WorkerSCALE_WIDE_IN_22CM_RIG = 0.67
100*b7c941bbSAndroid Build Coastguard WorkerSCALE_TELE_IN_22CM_RIG = 0.5
101*b7c941bbSAndroid Build Coastguard WorkerSCALE_TELE_IN_31CM_RIG = 0.67
102*b7c941bbSAndroid Build Coastguard WorkerSCALE_TELE40_IN_22CM_RIG = 0.33
103*b7c941bbSAndroid Build Coastguard WorkerSCALE_TELE40_IN_31CM_RIG = 0.5
104*b7c941bbSAndroid Build Coastguard WorkerSCALE_TELE25_IN_31CM_RIG = 0.33
105*b7c941bbSAndroid Build Coastguard Worker
106*b7c941bbSAndroid Build Coastguard WorkerSQUARE_AREA_MIN_REL = 0.05  # Minimum size for square relative to image area
107*b7c941bbSAndroid Build Coastguard WorkerSQUARE_CROP_MARGIN = 0  # Set to aid detection of QR codes
108*b7c941bbSAndroid Build Coastguard WorkerSQUARE_TOL = 0.05  # Square W vs H mismatch RTOL
109*b7c941bbSAndroid Build Coastguard WorkerSQUARISH_RTOL = 0.10
110*b7c941bbSAndroid Build Coastguard WorkerSQUARISH_AR_RTOL = 0.10
111*b7c941bbSAndroid Build Coastguard Worker
112*b7c941bbSAndroid Build Coastguard WorkerVGA_HEIGHT = 480
113*b7c941bbSAndroid Build Coastguard WorkerVGA_WIDTH = 640
114*b7c941bbSAndroid Build Coastguard Worker
115*b7c941bbSAndroid Build Coastguard Worker
116*b7c941bbSAndroid Build Coastguard Workerdef convert_to_y(img, color_order='RGB'):
117*b7c941bbSAndroid Build Coastguard Worker  """Returns a Y image from a uint8 RGB or BGR ordered image.
118*b7c941bbSAndroid Build Coastguard Worker
119*b7c941bbSAndroid Build Coastguard Worker  Args:
120*b7c941bbSAndroid Build Coastguard Worker    img: a uint8 openCV image.
121*b7c941bbSAndroid Build Coastguard Worker    color_order: str; 'RGB' or 'BGR' to signify color plane order.
122*b7c941bbSAndroid Build Coastguard Worker
123*b7c941bbSAndroid Build Coastguard Worker  Returns:
124*b7c941bbSAndroid Build Coastguard Worker    The Y plane of the input img.
125*b7c941bbSAndroid Build Coastguard Worker  """
126*b7c941bbSAndroid Build Coastguard Worker  if img.dtype != 'uint8':
127*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'Incorrect input type: {img.dtype}! Expected: uint8')
128*b7c941bbSAndroid Build Coastguard Worker  if color_order == 'RGB':
129*b7c941bbSAndroid Build Coastguard Worker    y, _, _ = cv2.split(cv2.cvtColor(img, cv2.COLOR_RGB2YUV))
130*b7c941bbSAndroid Build Coastguard Worker  elif color_order == 'BGR':
131*b7c941bbSAndroid Build Coastguard Worker    y, _, _ = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2YUV))
132*b7c941bbSAndroid Build Coastguard Worker  else:
133*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'Undefined color order: {color_order}!')
134*b7c941bbSAndroid Build Coastguard Worker  return y
135*b7c941bbSAndroid Build Coastguard Worker
136*b7c941bbSAndroid Build Coastguard Worker
137*b7c941bbSAndroid Build Coastguard Workerdef binarize_image(img_gray):
138*b7c941bbSAndroid Build Coastguard Worker  """Returns a binarized image based on cv2 thresholds.
139*b7c941bbSAndroid Build Coastguard Worker
140*b7c941bbSAndroid Build Coastguard Worker  Args:
141*b7c941bbSAndroid Build Coastguard Worker    img_gray: A grayscale openCV image.
142*b7c941bbSAndroid Build Coastguard Worker  Returns:
143*b7c941bbSAndroid Build Coastguard Worker    An openCV image binarized to 0 (black) and 255 (white).
144*b7c941bbSAndroid Build Coastguard Worker  """
145*b7c941bbSAndroid Build Coastguard Worker  _, img_bw = cv2.threshold(numpy.uint8(img_gray), 0, 255,
146*b7c941bbSAndroid Build Coastguard Worker                            cv2.THRESH_BINARY + cv2.THRESH_OTSU)
147*b7c941bbSAndroid Build Coastguard Worker  return img_bw
148*b7c941bbSAndroid Build Coastguard Worker
149*b7c941bbSAndroid Build Coastguard Worker
150*b7c941bbSAndroid Build Coastguard Workerdef _load_opencv_haarcascade_file():
151*b7c941bbSAndroid Build Coastguard Worker  """Return Haar Cascade file for face detection."""
152*b7c941bbSAndroid Build Coastguard Worker  for cv2_directory in (CV2_HOME_DIRECTORY, CV2_ALTERNATE_DIRECTORY,):
153*b7c941bbSAndroid Build Coastguard Worker    for path, _, files in os.walk(cv2_directory):
154*b7c941bbSAndroid Build Coastguard Worker      if HAARCASCADE_FILE_NAME in files:
155*b7c941bbSAndroid Build Coastguard Worker        haarcascade_file = os.path.join(path, HAARCASCADE_FILE_NAME)
156*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Haar Cascade file location: %s', haarcascade_file)
157*b7c941bbSAndroid Build Coastguard Worker        return haarcascade_file
158*b7c941bbSAndroid Build Coastguard Worker  raise error_util.CameraItsError('haarcascade_frontalface_default.xml was '
159*b7c941bbSAndroid Build Coastguard Worker                                  f'not found in {CV2_HOME_DIRECTORY} '
160*b7c941bbSAndroid Build Coastguard Worker                                  f'or {CV2_ALTERNATE_DIRECTORY}')
161*b7c941bbSAndroid Build Coastguard Worker
162*b7c941bbSAndroid Build Coastguard Worker
163*b7c941bbSAndroid Build Coastguard Workerdef find_opencv_faces(img, scale_factor, min_neighbors):
164*b7c941bbSAndroid Build Coastguard Worker  """Finds face rectangles with openCV.
165*b7c941bbSAndroid Build Coastguard Worker
166*b7c941bbSAndroid Build Coastguard Worker  Args:
167*b7c941bbSAndroid Build Coastguard Worker    img: numpy array; 3-D RBG image with [0,1] values
168*b7c941bbSAndroid Build Coastguard Worker    scale_factor: float, specifies how much image size is reduced at each scale
169*b7c941bbSAndroid Build Coastguard Worker    min_neighbors: int, specifies minimum number of neighbors to keep rectangle
170*b7c941bbSAndroid Build Coastguard Worker  Returns:
171*b7c941bbSAndroid Build Coastguard Worker    List of rectangles with faces
172*b7c941bbSAndroid Build Coastguard Worker  """
173*b7c941bbSAndroid Build Coastguard Worker  # prep opencv
174*b7c941bbSAndroid Build Coastguard Worker  opencv_haarcascade_file = _load_opencv_haarcascade_file()
175*b7c941bbSAndroid Build Coastguard Worker  face_cascade = cv2.CascadeClassifier(opencv_haarcascade_file)
176*b7c941bbSAndroid Build Coastguard Worker  img_uint8 = image_processing_utils.convert_image_to_uint8(img)
177*b7c941bbSAndroid Build Coastguard Worker  img_gray = cv2.cvtColor(img_uint8, cv2.COLOR_RGB2GRAY)
178*b7c941bbSAndroid Build Coastguard Worker
179*b7c941bbSAndroid Build Coastguard Worker  # find face rectangles with opencv
180*b7c941bbSAndroid Build Coastguard Worker  faces_opencv = face_cascade.detectMultiScale(
181*b7c941bbSAndroid Build Coastguard Worker      img_gray, scale_factor, min_neighbors)
182*b7c941bbSAndroid Build Coastguard Worker  logging.debug('%s', str(faces_opencv))
183*b7c941bbSAndroid Build Coastguard Worker  return faces_opencv
184*b7c941bbSAndroid Build Coastguard Worker
185*b7c941bbSAndroid Build Coastguard Worker
186*b7c941bbSAndroid Build Coastguard Workerdef find_all_contours(img):
187*b7c941bbSAndroid Build Coastguard Worker  cv2_version = cv2.__version__
188*b7c941bbSAndroid Build Coastguard Worker  if cv2_version.startswith('3.'):  # OpenCV 3.x
189*b7c941bbSAndroid Build Coastguard Worker    _, contours, _ = cv2.findContours(img, cv2.RETR_TREE,
190*b7c941bbSAndroid Build Coastguard Worker                                      cv2.CHAIN_APPROX_SIMPLE)
191*b7c941bbSAndroid Build Coastguard Worker  else:  # OpenCV 2.x and 4.x
192*b7c941bbSAndroid Build Coastguard Worker    contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
193*b7c941bbSAndroid Build Coastguard Worker  return contours
194*b7c941bbSAndroid Build Coastguard Worker
195*b7c941bbSAndroid Build Coastguard Worker
196*b7c941bbSAndroid Build Coastguard Workerdef calc_chart_scaling(chart_distance, camera_fov):
197*b7c941bbSAndroid Build Coastguard Worker  """Returns charts scaling factor.
198*b7c941bbSAndroid Build Coastguard Worker
199*b7c941bbSAndroid Build Coastguard Worker  Args:
200*b7c941bbSAndroid Build Coastguard Worker   chart_distance: float; distance in cm from camera of displayed chart
201*b7c941bbSAndroid Build Coastguard Worker   camera_fov: float; camera field of view.
202*b7c941bbSAndroid Build Coastguard Worker
203*b7c941bbSAndroid Build Coastguard Worker  Returns:
204*b7c941bbSAndroid Build Coastguard Worker   chart_scaling: float; scaling factor for chart
205*b7c941bbSAndroid Build Coastguard Worker  """
206*b7c941bbSAndroid Build Coastguard Worker  chart_scaling = 1.0
207*b7c941bbSAndroid Build Coastguard Worker  fov = float(camera_fov)
208*b7c941bbSAndroid Build Coastguard Worker  is_chart_distance_22cm = math.isclose(
209*b7c941bbSAndroid Build Coastguard Worker      chart_distance, CHART_DISTANCE_22CM, rel_tol=CHART_SCALE_RTOL)
210*b7c941bbSAndroid Build Coastguard Worker  is_chart_distance_31cm = math.isclose(
211*b7c941bbSAndroid Build Coastguard Worker      chart_distance, CHART_DISTANCE_31CM, rel_tol=CHART_SCALE_RTOL)
212*b7c941bbSAndroid Build Coastguard Worker  is_chart_distance_90cm = math.isclose(
213*b7c941bbSAndroid Build Coastguard Worker      chart_distance, CHART_DISTANCE_90CM, rel_tol=CHART_SCALE_RTOL)
214*b7c941bbSAndroid Build Coastguard Worker
215*b7c941bbSAndroid Build Coastguard Worker  if FOV_THRESH_TELE < fov < FOV_THRESH_UW and is_chart_distance_22cm:
216*b7c941bbSAndroid Build Coastguard Worker    chart_scaling = SCALE_WIDE_IN_22CM_RIG
217*b7c941bbSAndroid Build Coastguard Worker  elif FOV_THRESH_TELE40 < fov <= FOV_THRESH_TELE and is_chart_distance_22cm:
218*b7c941bbSAndroid Build Coastguard Worker    chart_scaling = SCALE_TELE_IN_22CM_RIG
219*b7c941bbSAndroid Build Coastguard Worker  elif fov <= FOV_THRESH_TELE40 and is_chart_distance_22cm:
220*b7c941bbSAndroid Build Coastguard Worker    chart_scaling = SCALE_TELE40_IN_22CM_RIG
221*b7c941bbSAndroid Build Coastguard Worker  elif fov <= FOV_THRESH_TELE25 and is_chart_distance_31cm:
222*b7c941bbSAndroid Build Coastguard Worker    chart_scaling = SCALE_TELE25_IN_31CM_RIG
223*b7c941bbSAndroid Build Coastguard Worker  elif fov <= FOV_THRESH_TELE40 and is_chart_distance_31cm:
224*b7c941bbSAndroid Build Coastguard Worker    chart_scaling = SCALE_TELE40_IN_31CM_RIG
225*b7c941bbSAndroid Build Coastguard Worker  elif fov <= FOV_THRESH_TELE40 and is_chart_distance_90cm:
226*b7c941bbSAndroid Build Coastguard Worker    chart_scaling = SCALE_CHART_67_PERCENT
227*b7c941bbSAndroid Build Coastguard Worker  elif fov <= FOV_THRESH_TELE and is_chart_distance_31cm:
228*b7c941bbSAndroid Build Coastguard Worker    chart_scaling = SCALE_TELE_IN_31CM_RIG
229*b7c941bbSAndroid Build Coastguard Worker  elif chart_distance > CHART_DISTANCE_31CM:
230*b7c941bbSAndroid Build Coastguard Worker    chart_scaling = SCALE_CHART_33_PERCENT
231*b7c941bbSAndroid Build Coastguard Worker  return chart_scaling
232*b7c941bbSAndroid Build Coastguard Worker
233*b7c941bbSAndroid Build Coastguard Worker
234*b7c941bbSAndroid Build Coastguard Workerdef scale_img(img, scale=1.0):
235*b7c941bbSAndroid Build Coastguard Worker  """Scale image based on a real number scale factor."""
236*b7c941bbSAndroid Build Coastguard Worker  dim = (int(img.shape[1] * scale), int(img.shape[0] * scale))
237*b7c941bbSAndroid Build Coastguard Worker  return cv2.resize(img.copy(), dim, interpolation=cv2.INTER_AREA)
238*b7c941bbSAndroid Build Coastguard Worker
239*b7c941bbSAndroid Build Coastguard Worker
240*b7c941bbSAndroid Build Coastguard Workerclass Chart(object):
241*b7c941bbSAndroid Build Coastguard Worker  """Definition for chart object.
242*b7c941bbSAndroid Build Coastguard Worker
243*b7c941bbSAndroid Build Coastguard Worker  Defines PNG reference file, chart, size, distance and scaling range.
244*b7c941bbSAndroid Build Coastguard Worker  """
245*b7c941bbSAndroid Build Coastguard Worker
246*b7c941bbSAndroid Build Coastguard Worker  def __init__(
247*b7c941bbSAndroid Build Coastguard Worker      self,
248*b7c941bbSAndroid Build Coastguard Worker      cam,
249*b7c941bbSAndroid Build Coastguard Worker      props,
250*b7c941bbSAndroid Build Coastguard Worker      log_path,
251*b7c941bbSAndroid Build Coastguard Worker      chart_file=None,
252*b7c941bbSAndroid Build Coastguard Worker      height=None,
253*b7c941bbSAndroid Build Coastguard Worker      distance=None,
254*b7c941bbSAndroid Build Coastguard Worker      scale_start=None,
255*b7c941bbSAndroid Build Coastguard Worker      scale_stop=None,
256*b7c941bbSAndroid Build Coastguard Worker      scale_step=None,
257*b7c941bbSAndroid Build Coastguard Worker      rotation=None):
258*b7c941bbSAndroid Build Coastguard Worker    """Initial constructor for class.
259*b7c941bbSAndroid Build Coastguard Worker
260*b7c941bbSAndroid Build Coastguard Worker    Args:
261*b7c941bbSAndroid Build Coastguard Worker     cam: open ITS session
262*b7c941bbSAndroid Build Coastguard Worker     props: camera properties object
263*b7c941bbSAndroid Build Coastguard Worker     log_path: log path to store the captured images.
264*b7c941bbSAndroid Build Coastguard Worker     chart_file: str; absolute path to png file of chart
265*b7c941bbSAndroid Build Coastguard Worker     height: float; height in cm of displayed chart
266*b7c941bbSAndroid Build Coastguard Worker     distance: float; distance in cm from camera of displayed chart
267*b7c941bbSAndroid Build Coastguard Worker     scale_start: float; start value for scaling for chart search
268*b7c941bbSAndroid Build Coastguard Worker     scale_stop: float; stop value for scaling for chart search
269*b7c941bbSAndroid Build Coastguard Worker     scale_step: float; step value for scaling for chart search
270*b7c941bbSAndroid Build Coastguard Worker     rotation: clockwise rotation in degrees (multiple of 90) or None
271*b7c941bbSAndroid Build Coastguard Worker    """
272*b7c941bbSAndroid Build Coastguard Worker    self._file = chart_file or CHART_FILE
273*b7c941bbSAndroid Build Coastguard Worker    if math.isclose(
274*b7c941bbSAndroid Build Coastguard Worker        distance, CHART_DISTANCE_31CM, rel_tol=CHART_SCALE_RTOL):
275*b7c941bbSAndroid Build Coastguard Worker      self._height = height or CHART_HEIGHT_31CM
276*b7c941bbSAndroid Build Coastguard Worker      self._distance = distance
277*b7c941bbSAndroid Build Coastguard Worker    else:
278*b7c941bbSAndroid Build Coastguard Worker      self._height = height or CHART_HEIGHT_22CM
279*b7c941bbSAndroid Build Coastguard Worker      self._distance = CHART_DISTANCE_22CM
280*b7c941bbSAndroid Build Coastguard Worker    self._scale_start = scale_start or CHART_SCALE_START
281*b7c941bbSAndroid Build Coastguard Worker    self._scale_stop = scale_stop or CHART_SCALE_STOP
282*b7c941bbSAndroid Build Coastguard Worker    self._scale_step = scale_step or CHART_SCALE_STEP
283*b7c941bbSAndroid Build Coastguard Worker    self.opt_val = None
284*b7c941bbSAndroid Build Coastguard Worker    self.locate(cam, props, log_path, rotation)
285*b7c941bbSAndroid Build Coastguard Worker
286*b7c941bbSAndroid Build Coastguard Worker  def _set_scale_factors_to_one(self):
287*b7c941bbSAndroid Build Coastguard Worker    """Set scale factors to 1.0 for skipped tests."""
288*b7c941bbSAndroid Build Coastguard Worker    self.wnorm = 1.0
289*b7c941bbSAndroid Build Coastguard Worker    self.hnorm = 1.0
290*b7c941bbSAndroid Build Coastguard Worker    self.xnorm = 0.0
291*b7c941bbSAndroid Build Coastguard Worker    self.ynorm = 0.0
292*b7c941bbSAndroid Build Coastguard Worker    self.scale = 1.0
293*b7c941bbSAndroid Build Coastguard Worker
294*b7c941bbSAndroid Build Coastguard Worker  def _calc_scale_factors(self, cam, props, fmt, log_path, rotation):
295*b7c941bbSAndroid Build Coastguard Worker    """Take an image with s, e, & fd to find the chart location.
296*b7c941bbSAndroid Build Coastguard Worker
297*b7c941bbSAndroid Build Coastguard Worker    Args:
298*b7c941bbSAndroid Build Coastguard Worker     cam: An open its session.
299*b7c941bbSAndroid Build Coastguard Worker     props: Properties of cam
300*b7c941bbSAndroid Build Coastguard Worker     fmt: Image format for the capture
301*b7c941bbSAndroid Build Coastguard Worker     log_path: log path to save the captured images.
302*b7c941bbSAndroid Build Coastguard Worker     rotation: clockwise rotation of template in degrees (multiple of 90) or
303*b7c941bbSAndroid Build Coastguard Worker       None
304*b7c941bbSAndroid Build Coastguard Worker
305*b7c941bbSAndroid Build Coastguard Worker    Returns:
306*b7c941bbSAndroid Build Coastguard Worker      template: numpy array; chart template for locator
307*b7c941bbSAndroid Build Coastguard Worker      img_3a: numpy array; RGB image for chart location
308*b7c941bbSAndroid Build Coastguard Worker      scale_factor: float; scaling factor for chart search
309*b7c941bbSAndroid Build Coastguard Worker    """
310*b7c941bbSAndroid Build Coastguard Worker    req = capture_request_utils.auto_capture_request()
311*b7c941bbSAndroid Build Coastguard Worker    cap_chart = capture_request_utils.stationary_lens_capture(cam, req, fmt)
312*b7c941bbSAndroid Build Coastguard Worker    img_3a = image_processing_utils.convert_capture_to_rgb_image(
313*b7c941bbSAndroid Build Coastguard Worker        cap_chart, props)
314*b7c941bbSAndroid Build Coastguard Worker    img_3a = image_processing_utils.rotate_img_per_argv(img_3a)
315*b7c941bbSAndroid Build Coastguard Worker    af_scene_name = os.path.join(log_path, 'af_scene.jpg')
316*b7c941bbSAndroid Build Coastguard Worker    image_processing_utils.write_image(img_3a, af_scene_name)
317*b7c941bbSAndroid Build Coastguard Worker    template = cv2.imread(self._file, cv2.IMREAD_ANYDEPTH)
318*b7c941bbSAndroid Build Coastguard Worker    if rotation is not None:
319*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Rotating template by %d degrees', rotation)
320*b7c941bbSAndroid Build Coastguard Worker      template = numpy.rot90(template, k=rotation / 90)
321*b7c941bbSAndroid Build Coastguard Worker    focal_l = cap_chart['metadata']['android.lens.focalLength']
322*b7c941bbSAndroid Build Coastguard Worker    pixel_pitch = (
323*b7c941bbSAndroid Build Coastguard Worker        props['android.sensor.info.physicalSize']['height'] / img_3a.shape[0])
324*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Chart distance: %.2fcm', self._distance)
325*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Chart height: %.2fcm', self._height)
326*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Focal length: %.2fmm', focal_l)
327*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Pixel pitch: %.2fum', pixel_pitch * 1E3)
328*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Template width: %dpixels', template.shape[1])
329*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Template height: %dpixels', template.shape[0])
330*b7c941bbSAndroid Build Coastguard Worker    chart_pixel_h = self._height * focal_l / (self._distance * pixel_pitch)
331*b7c941bbSAndroid Build Coastguard Worker    scale_factor = template.shape[0] / chart_pixel_h
332*b7c941bbSAndroid Build Coastguard Worker    if rotation == 90 or rotation == 270:
333*b7c941bbSAndroid Build Coastguard Worker      # With the landscape to portrait override turned on, the width and height
334*b7c941bbSAndroid Build Coastguard Worker      # of the active array, normally w x h, will be h x (w * (h/w)^2). Reduce
335*b7c941bbSAndroid Build Coastguard Worker      # the applied scaling by the same factor to compensate for this, because
336*b7c941bbSAndroid Build Coastguard Worker      # the chart will take up more of the scene. Assume w > h, since this is
337*b7c941bbSAndroid Build Coastguard Worker      # meant for landscape sensors.
338*b7c941bbSAndroid Build Coastguard Worker      rotate_physical_aspect = (
339*b7c941bbSAndroid Build Coastguard Worker          props['android.sensor.info.physicalSize']['height'] /
340*b7c941bbSAndroid Build Coastguard Worker          props['android.sensor.info.physicalSize']['width'])
341*b7c941bbSAndroid Build Coastguard Worker      scale_factor *= rotate_physical_aspect ** 2
342*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Chart/image scale factor = %.2f', scale_factor)
343*b7c941bbSAndroid Build Coastguard Worker    return template, img_3a, scale_factor
344*b7c941bbSAndroid Build Coastguard Worker
345*b7c941bbSAndroid Build Coastguard Worker  def locate(self, cam, props, log_path, rotation):
346*b7c941bbSAndroid Build Coastguard Worker    """Find the chart in the image, and append location to chart object.
347*b7c941bbSAndroid Build Coastguard Worker
348*b7c941bbSAndroid Build Coastguard Worker    Args:
349*b7c941bbSAndroid Build Coastguard Worker      cam: Open its session.
350*b7c941bbSAndroid Build Coastguard Worker      props: Camera properties object.
351*b7c941bbSAndroid Build Coastguard Worker      log_path: log path to store the captured images.
352*b7c941bbSAndroid Build Coastguard Worker      rotation: clockwise rotation of template in degrees (multiple of 90) or
353*b7c941bbSAndroid Build Coastguard Worker        None
354*b7c941bbSAndroid Build Coastguard Worker
355*b7c941bbSAndroid Build Coastguard Worker    The values appended are:
356*b7c941bbSAndroid Build Coastguard Worker    xnorm: float; [0, 1] left loc of chart in scene
357*b7c941bbSAndroid Build Coastguard Worker    ynorm: float; [0, 1] top loc of chart in scene
358*b7c941bbSAndroid Build Coastguard Worker    wnorm: float; [0, 1] width of chart in scene
359*b7c941bbSAndroid Build Coastguard Worker    hnorm: float; [0, 1] height of chart in scene
360*b7c941bbSAndroid Build Coastguard Worker    scale: float; scale factor to extract chart
361*b7c941bbSAndroid Build Coastguard Worker    opt_val: float; The normalized match optimization value [0, 1]
362*b7c941bbSAndroid Build Coastguard Worker    """
363*b7c941bbSAndroid Build Coastguard Worker    fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
364*b7c941bbSAndroid Build Coastguard Worker    cam.do_3a()
365*b7c941bbSAndroid Build Coastguard Worker    chart, scene, s_factor = self._calc_scale_factors(cam, props, fmt, log_path,
366*b7c941bbSAndroid Build Coastguard Worker                                                      rotation)
367*b7c941bbSAndroid Build Coastguard Worker    scale_start = self._scale_start * s_factor
368*b7c941bbSAndroid Build Coastguard Worker    scale_stop = self._scale_stop * s_factor
369*b7c941bbSAndroid Build Coastguard Worker    scale_step = self._scale_step * s_factor
370*b7c941bbSAndroid Build Coastguard Worker    offset = scale_step / 2
371*b7c941bbSAndroid Build Coastguard Worker    self.scale = s_factor
372*b7c941bbSAndroid Build Coastguard Worker    logging.debug('scale start: %.3f, stop: %.3f, step: %.3f',
373*b7c941bbSAndroid Build Coastguard Worker                  scale_start, scale_stop, scale_step)
374*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Used offset of %.3f to include stop value.', offset)
375*b7c941bbSAndroid Build Coastguard Worker    max_match = []
376*b7c941bbSAndroid Build Coastguard Worker    # convert [0.0, 1.0] image to [0, 255] and then grayscale
377*b7c941bbSAndroid Build Coastguard Worker    scene_uint8 = image_processing_utils.convert_image_to_uint8(scene)
378*b7c941bbSAndroid Build Coastguard Worker    scene_gray = image_processing_utils.convert_rgb_to_grayscale(scene_uint8)
379*b7c941bbSAndroid Build Coastguard Worker
380*b7c941bbSAndroid Build Coastguard Worker    # find scene
381*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Finding chart in scene...')
382*b7c941bbSAndroid Build Coastguard Worker    for scale in numpy.arange(scale_start, scale_stop + offset, scale_step):
383*b7c941bbSAndroid Build Coastguard Worker      scene_scaled = scale_img(scene_gray, scale)
384*b7c941bbSAndroid Build Coastguard Worker      if (scene_scaled.shape[0] < chart.shape[0] or
385*b7c941bbSAndroid Build Coastguard Worker          scene_scaled.shape[1] < chart.shape[1]):
386*b7c941bbSAndroid Build Coastguard Worker        logging.debug(
387*b7c941bbSAndroid Build Coastguard Worker            'Skipped scale %.3f. scene_scaled shape: %s, chart shape: %s',
388*b7c941bbSAndroid Build Coastguard Worker            scale, scene_scaled.shape, chart.shape)
389*b7c941bbSAndroid Build Coastguard Worker        continue
390*b7c941bbSAndroid Build Coastguard Worker      result = cv2.matchTemplate(scene_scaled, chart, cv2.TM_CCOEFF_NORMED)
391*b7c941bbSAndroid Build Coastguard Worker      _, opt_val, _, top_left_scaled = cv2.minMaxLoc(result)
392*b7c941bbSAndroid Build Coastguard Worker      logging.debug(' scale factor: %.3f, opt val: %.3f', scale, opt_val)
393*b7c941bbSAndroid Build Coastguard Worker      max_match.append((opt_val, scale, top_left_scaled))
394*b7c941bbSAndroid Build Coastguard Worker
395*b7c941bbSAndroid Build Coastguard Worker    # determine if optimization results are valid
396*b7c941bbSAndroid Build Coastguard Worker    opt_values = [x[0] for x in max_match]
397*b7c941bbSAndroid Build Coastguard Worker    if not opt_values or max(opt_values) < OPT_VALUE_THRESH:
398*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError(
399*b7c941bbSAndroid Build Coastguard Worker          'Unable to find chart in scene!\n'
400*b7c941bbSAndroid Build Coastguard Worker          'Check camera distance and self-reported '
401*b7c941bbSAndroid Build Coastguard Worker          'pixel pitch, focal length and hyperfocal distance.')
402*b7c941bbSAndroid Build Coastguard Worker    else:
403*b7c941bbSAndroid Build Coastguard Worker      # find max and draw bbox
404*b7c941bbSAndroid Build Coastguard Worker      matched_scale_and_loc = max(max_match, key=lambda x: x[0])
405*b7c941bbSAndroid Build Coastguard Worker      self.opt_val = matched_scale_and_loc[0]
406*b7c941bbSAndroid Build Coastguard Worker      self.scale = matched_scale_and_loc[1]
407*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Optimum scale factor: %.3f', self.scale)
408*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Opt val: %.3f', self.opt_val)
409*b7c941bbSAndroid Build Coastguard Worker      top_left_scaled = matched_scale_and_loc[2]
410*b7c941bbSAndroid Build Coastguard Worker      logging.debug('top_left_scaled: %d, %d', top_left_scaled[0],
411*b7c941bbSAndroid Build Coastguard Worker                    top_left_scaled[1])
412*b7c941bbSAndroid Build Coastguard Worker      h, w = chart.shape
413*b7c941bbSAndroid Build Coastguard Worker      bottom_right_scaled = (top_left_scaled[0] + w, top_left_scaled[1] + h)
414*b7c941bbSAndroid Build Coastguard Worker      logging.debug('bottom_right_scaled: %d, %d', bottom_right_scaled[0],
415*b7c941bbSAndroid Build Coastguard Worker                    bottom_right_scaled[1])
416*b7c941bbSAndroid Build Coastguard Worker      top_left = ((top_left_scaled[0] // self.scale),
417*b7c941bbSAndroid Build Coastguard Worker                  (top_left_scaled[1] // self.scale))
418*b7c941bbSAndroid Build Coastguard Worker      bottom_right = ((bottom_right_scaled[0] // self.scale),
419*b7c941bbSAndroid Build Coastguard Worker                      (bottom_right_scaled[1] // self.scale))
420*b7c941bbSAndroid Build Coastguard Worker      self.wnorm = ((bottom_right[0]) - top_left[0]) / scene.shape[1]
421*b7c941bbSAndroid Build Coastguard Worker      self.hnorm = ((bottom_right[1]) - top_left[1]) / scene.shape[0]
422*b7c941bbSAndroid Build Coastguard Worker      self.xnorm = (top_left[0]) / scene.shape[1]
423*b7c941bbSAndroid Build Coastguard Worker      self.ynorm = (top_left[1]) / scene.shape[0]
424*b7c941bbSAndroid Build Coastguard Worker      patch = image_processing_utils.get_image_patch(
425*b7c941bbSAndroid Build Coastguard Worker          scene_uint8, self.xnorm, self.ynorm, self.wnorm, self.hnorm) / 255
426*b7c941bbSAndroid Build Coastguard Worker      image_processing_utils.write_image(
427*b7c941bbSAndroid Build Coastguard Worker          patch, os.path.join(log_path, 'template_scene.jpg'))
428*b7c941bbSAndroid Build Coastguard Worker
429*b7c941bbSAndroid Build Coastguard Worker
430*b7c941bbSAndroid Build Coastguard Workerdef component_shape(contour):
431*b7c941bbSAndroid Build Coastguard Worker  """Measure the shape of a connected component.
432*b7c941bbSAndroid Build Coastguard Worker
433*b7c941bbSAndroid Build Coastguard Worker  Args:
434*b7c941bbSAndroid Build Coastguard Worker    contour: return from cv2.findContours. A list of pixel coordinates of
435*b7c941bbSAndroid Build Coastguard Worker    the contour.
436*b7c941bbSAndroid Build Coastguard Worker
437*b7c941bbSAndroid Build Coastguard Worker  Returns:
438*b7c941bbSAndroid Build Coastguard Worker    The most left, right, top, bottom pixel location, height, width, and
439*b7c941bbSAndroid Build Coastguard Worker    the center pixel location of the contour.
440*b7c941bbSAndroid Build Coastguard Worker  """
441*b7c941bbSAndroid Build Coastguard Worker  shape = {'left': numpy.inf, 'right': 0, 'top': numpy.inf, 'bottom': 0,
442*b7c941bbSAndroid Build Coastguard Worker           'width': 0, 'height': 0, 'ctx': 0, 'cty': 0}
443*b7c941bbSAndroid Build Coastguard Worker  for pt in contour:
444*b7c941bbSAndroid Build Coastguard Worker    if pt[0][0] < shape['left']:
445*b7c941bbSAndroid Build Coastguard Worker      shape['left'] = pt[0][0]
446*b7c941bbSAndroid Build Coastguard Worker    if pt[0][0] > shape['right']:
447*b7c941bbSAndroid Build Coastguard Worker      shape['right'] = pt[0][0]
448*b7c941bbSAndroid Build Coastguard Worker    if pt[0][1] < shape['top']:
449*b7c941bbSAndroid Build Coastguard Worker      shape['top'] = pt[0][1]
450*b7c941bbSAndroid Build Coastguard Worker    if pt[0][1] > shape['bottom']:
451*b7c941bbSAndroid Build Coastguard Worker      shape['bottom'] = pt[0][1]
452*b7c941bbSAndroid Build Coastguard Worker  shape['width'] = shape['right'] - shape['left'] + 1
453*b7c941bbSAndroid Build Coastguard Worker  shape['height'] = shape['bottom'] - shape['top'] + 1
454*b7c941bbSAndroid Build Coastguard Worker  shape['ctx'] = (shape['left'] + shape['right']) // 2
455*b7c941bbSAndroid Build Coastguard Worker  shape['cty'] = (shape['top'] + shape['bottom']) // 2
456*b7c941bbSAndroid Build Coastguard Worker  return shape
457*b7c941bbSAndroid Build Coastguard Worker
458*b7c941bbSAndroid Build Coastguard Worker
459*b7c941bbSAndroid Build Coastguard Workerdef find_circle_fill_metric(shape, img_bw, color):
460*b7c941bbSAndroid Build Coastguard Worker  """Find the proportion of points matching a desired color on a shape's axes.
461*b7c941bbSAndroid Build Coastguard Worker
462*b7c941bbSAndroid Build Coastguard Worker  Args:
463*b7c941bbSAndroid Build Coastguard Worker    shape: dictionary returned by component_shape(...)
464*b7c941bbSAndroid Build Coastguard Worker    img_bw: binarized numpy image array
465*b7c941bbSAndroid Build Coastguard Worker    color: int of [0 or 255] 0 is black, 255 is white
466*b7c941bbSAndroid Build Coastguard Worker  Returns:
467*b7c941bbSAndroid Build Coastguard Worker    float: number of x, y axis points matching color / total x, y axis points
468*b7c941bbSAndroid Build Coastguard Worker  """
469*b7c941bbSAndroid Build Coastguard Worker  matching = 0
470*b7c941bbSAndroid Build Coastguard Worker  total = 0
471*b7c941bbSAndroid Build Coastguard Worker  for y in range(shape['top'], shape['bottom']):
472*b7c941bbSAndroid Build Coastguard Worker    total += 1
473*b7c941bbSAndroid Build Coastguard Worker    matching += 1 if img_bw[y][shape['ctx']] == color else 0
474*b7c941bbSAndroid Build Coastguard Worker  for x in range(shape['left'], shape['right']):
475*b7c941bbSAndroid Build Coastguard Worker    total += 1
476*b7c941bbSAndroid Build Coastguard Worker    matching += 1 if img_bw[shape['cty']][x] == color else 0
477*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Found %d matching points out of %d', matching, total)
478*b7c941bbSAndroid Build Coastguard Worker  return matching / total
479*b7c941bbSAndroid Build Coastguard Worker
480*b7c941bbSAndroid Build Coastguard Worker
481*b7c941bbSAndroid Build Coastguard Workerdef find_circle(img, img_name, min_area, color, use_adaptive_threshold=False):
482*b7c941bbSAndroid Build Coastguard Worker  """Find the circle in the test image.
483*b7c941bbSAndroid Build Coastguard Worker
484*b7c941bbSAndroid Build Coastguard Worker  Args:
485*b7c941bbSAndroid Build Coastguard Worker    img: numpy image array in RGB, with pixel values in [0,255].
486*b7c941bbSAndroid Build Coastguard Worker    img_name: string with image info of format and size.
487*b7c941bbSAndroid Build Coastguard Worker    min_area: float of minimum area of circle to find
488*b7c941bbSAndroid Build Coastguard Worker    color: int of [0 or 255] 0 is black, 255 is white
489*b7c941bbSAndroid Build Coastguard Worker    use_adaptive_threshold: True if binarization should use adaptive threshold.
490*b7c941bbSAndroid Build Coastguard Worker
491*b7c941bbSAndroid Build Coastguard Worker  Returns:
492*b7c941bbSAndroid Build Coastguard Worker    circle = {'x', 'y', 'r', 'w', 'h', 'x_offset', 'y_offset'}
493*b7c941bbSAndroid Build Coastguard Worker  """
494*b7c941bbSAndroid Build Coastguard Worker  circle = {}
495*b7c941bbSAndroid Build Coastguard Worker  img_size = img.shape
496*b7c941bbSAndroid Build Coastguard Worker  if img_size[0]*img_size[1] >= LOW_RES_IMG_THRESH:
497*b7c941bbSAndroid Build Coastguard Worker    circlish_atol = CIRCLISH_ATOL
498*b7c941bbSAndroid Build Coastguard Worker  else:
499*b7c941bbSAndroid Build Coastguard Worker    circlish_atol = CIRCLISH_LOW_RES_ATOL
500*b7c941bbSAndroid Build Coastguard Worker
501*b7c941bbSAndroid Build Coastguard Worker  # convert to gray-scale image and binarize using adaptive/global threshold
502*b7c941bbSAndroid Build Coastguard Worker  if use_adaptive_threshold:
503*b7c941bbSAndroid Build Coastguard Worker    img_gray = cv2.cvtColor(img.astype(numpy.uint8), cv2.COLOR_BGR2GRAY)
504*b7c941bbSAndroid Build Coastguard Worker    img_bw = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
505*b7c941bbSAndroid Build Coastguard Worker                                   cv2.THRESH_BINARY, CV2_THRESHOLD_BLOCK_SIZE,
506*b7c941bbSAndroid Build Coastguard Worker                                   CV2_THRESHOLD_CONSTANT)
507*b7c941bbSAndroid Build Coastguard Worker  else:
508*b7c941bbSAndroid Build Coastguard Worker    img_gray = image_processing_utils.convert_rgb_to_grayscale(img)
509*b7c941bbSAndroid Build Coastguard Worker    img_bw = binarize_image(img_gray)
510*b7c941bbSAndroid Build Coastguard Worker
511*b7c941bbSAndroid Build Coastguard Worker  # find contours
512*b7c941bbSAndroid Build Coastguard Worker  contours = find_all_contours(255-img_bw)
513*b7c941bbSAndroid Build Coastguard Worker
514*b7c941bbSAndroid Build Coastguard Worker  # Check each contour and find the circle bigger than min_area
515*b7c941bbSAndroid Build Coastguard Worker  num_circles = 0
516*b7c941bbSAndroid Build Coastguard Worker  circle_contours = []
517*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Initial number of contours: %d', len(contours))
518*b7c941bbSAndroid Build Coastguard Worker  min_circle_area = min_area * img_size[0] * img_size[1]
519*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Screening out circles w/ radius < %.1f (pixels) or %d pts.',
520*b7c941bbSAndroid Build Coastguard Worker                math.sqrt(min_circle_area / math.pi), CIRCLE_MIN_PTS)
521*b7c941bbSAndroid Build Coastguard Worker  for contour in contours:
522*b7c941bbSAndroid Build Coastguard Worker    area = cv2.contourArea(contour)
523*b7c941bbSAndroid Build Coastguard Worker    num_pts = len(contour)
524*b7c941bbSAndroid Build Coastguard Worker    if (area > min_circle_area and num_pts >= CIRCLE_MIN_PTS):
525*b7c941bbSAndroid Build Coastguard Worker      shape = component_shape(contour)
526*b7c941bbSAndroid Build Coastguard Worker      radius = (shape['width'] + shape['height']) / 4
527*b7c941bbSAndroid Build Coastguard Worker      colour = img_bw[shape['cty']][shape['ctx']]
528*b7c941bbSAndroid Build Coastguard Worker      circlish = (math.pi * radius**2) / area
529*b7c941bbSAndroid Build Coastguard Worker      aspect_ratio = shape['width'] / shape['height']
530*b7c941bbSAndroid Build Coastguard Worker      fill = find_circle_fill_metric(shape, img_bw, color)
531*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Potential circle found. radius: %.2f, color: %d, '
532*b7c941bbSAndroid Build Coastguard Worker                    'circlish: %.3f, ar: %.3f, pts: %d, fill metric: %.3f',
533*b7c941bbSAndroid Build Coastguard Worker                    radius, colour, circlish, aspect_ratio, num_pts, fill)
534*b7c941bbSAndroid Build Coastguard Worker      if (colour == color and
535*b7c941bbSAndroid Build Coastguard Worker          math.isclose(1.0, circlish, abs_tol=circlish_atol) and
536*b7c941bbSAndroid Build Coastguard Worker          math.isclose(1.0, aspect_ratio, abs_tol=CIRCLE_AR_ATOL) and
537*b7c941bbSAndroid Build Coastguard Worker          num_pts/radius >= CIRCLE_RADIUS_NUMPTS_THRESH and
538*b7c941bbSAndroid Build Coastguard Worker          math.isclose(1.0, fill, abs_tol=CIRCLE_COLOR_ATOL)):
539*b7c941bbSAndroid Build Coastguard Worker        radii = [
540*b7c941bbSAndroid Build Coastguard Worker            image_processing_utils.distance(
541*b7c941bbSAndroid Build Coastguard Worker                (shape['ctx'], shape['cty']), numpy.squeeze(point))
542*b7c941bbSAndroid Build Coastguard Worker            for point in contour
543*b7c941bbSAndroid Build Coastguard Worker        ]
544*b7c941bbSAndroid Build Coastguard Worker        minimum_radius, maximum_radius = min(radii), max(radii)
545*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Minimum radius: %.2f, maximum radius: %.2f',
546*b7c941bbSAndroid Build Coastguard Worker                      minimum_radius, maximum_radius)
547*b7c941bbSAndroid Build Coastguard Worker        if circle:
548*b7c941bbSAndroid Build Coastguard Worker          old_circle_center = (circle['x'], circle['y'])
549*b7c941bbSAndroid Build Coastguard Worker          new_circle_center = (shape['ctx'], shape['cty'])
550*b7c941bbSAndroid Build Coastguard Worker          # Based on image height
551*b7c941bbSAndroid Build Coastguard Worker          center_distance_atol = img_size[0]*CIRCLE_LOCATION_VARIATION_RTOL
552*b7c941bbSAndroid Build Coastguard Worker          if math.isclose(
553*b7c941bbSAndroid Build Coastguard Worker              image_processing_utils.distance(
554*b7c941bbSAndroid Build Coastguard Worker                  old_circle_center, new_circle_center),
555*b7c941bbSAndroid Build Coastguard Worker              0,
556*b7c941bbSAndroid Build Coastguard Worker              abs_tol=center_distance_atol
557*b7c941bbSAndroid Build Coastguard Worker          ) and maximum_radius - minimum_radius < circle['radius_spread']:
558*b7c941bbSAndroid Build Coastguard Worker            logging.debug('Replacing the previously found circle. '
559*b7c941bbSAndroid Build Coastguard Worker                          'Circle located at %s has a smaller radius spread '
560*b7c941bbSAndroid Build Coastguard Worker                          'than the previously found circle at %s. '
561*b7c941bbSAndroid Build Coastguard Worker                          'Current radius spread: %.2f, '
562*b7c941bbSAndroid Build Coastguard Worker                          'previous radius spread: %.2f',
563*b7c941bbSAndroid Build Coastguard Worker                          new_circle_center, old_circle_center,
564*b7c941bbSAndroid Build Coastguard Worker                          maximum_radius - minimum_radius,
565*b7c941bbSAndroid Build Coastguard Worker                          circle['radius_spread'])
566*b7c941bbSAndroid Build Coastguard Worker            circle_contours.pop()
567*b7c941bbSAndroid Build Coastguard Worker            circle = {}
568*b7c941bbSAndroid Build Coastguard Worker            num_circles -= 1
569*b7c941bbSAndroid Build Coastguard Worker        circle_contours.append(contour)
570*b7c941bbSAndroid Build Coastguard Worker
571*b7c941bbSAndroid Build Coastguard Worker        # Populate circle dictionary
572*b7c941bbSAndroid Build Coastguard Worker        circle['x'] = shape['ctx']
573*b7c941bbSAndroid Build Coastguard Worker        circle['y'] = shape['cty']
574*b7c941bbSAndroid Build Coastguard Worker        circle['r'] = (shape['width'] + shape['height']) / 4
575*b7c941bbSAndroid Build Coastguard Worker        circle['w'] = float(shape['width'])
576*b7c941bbSAndroid Build Coastguard Worker        circle['h'] = float(shape['height'])
577*b7c941bbSAndroid Build Coastguard Worker        circle['x_offset'] = (shape['ctx'] - img_size[1]//2) / circle['w']
578*b7c941bbSAndroid Build Coastguard Worker        circle['y_offset'] = (shape['cty'] - img_size[0]//2) / circle['h']
579*b7c941bbSAndroid Build Coastguard Worker        circle['radius_spread'] = maximum_radius - minimum_radius
580*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Num pts: %d', num_pts)
581*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Aspect ratio: %.3f', aspect_ratio)
582*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Circlish value: %.3f', circlish)
583*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Location: %.1f x %.1f', circle['x'], circle['y'])
584*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Radius: %.3f', circle['r'])
585*b7c941bbSAndroid Build Coastguard Worker        logging.debug('Circle center position wrt to image center: %.3fx%.3f',
586*b7c941bbSAndroid Build Coastguard Worker                      circle['x_offset'], circle['y_offset'])
587*b7c941bbSAndroid Build Coastguard Worker        num_circles += 1
588*b7c941bbSAndroid Build Coastguard Worker        # if more than one circle found, break
589*b7c941bbSAndroid Build Coastguard Worker        if num_circles == 2:
590*b7c941bbSAndroid Build Coastguard Worker          break
591*b7c941bbSAndroid Build Coastguard Worker
592*b7c941bbSAndroid Build Coastguard Worker  if num_circles == 0:
593*b7c941bbSAndroid Build Coastguard Worker    image_processing_utils.write_image(img/255, img_name, True)
594*b7c941bbSAndroid Build Coastguard Worker    if not use_adaptive_threshold:
595*b7c941bbSAndroid Build Coastguard Worker      return find_circle(
596*b7c941bbSAndroid Build Coastguard Worker          img, img_name, min_area, color, use_adaptive_threshold=True)
597*b7c941bbSAndroid Build Coastguard Worker    else:
598*b7c941bbSAndroid Build Coastguard Worker      raise AssertionError('No circle detected. '
599*b7c941bbSAndroid Build Coastguard Worker                           'Please take pictures according to instructions.')
600*b7c941bbSAndroid Build Coastguard Worker
601*b7c941bbSAndroid Build Coastguard Worker  if num_circles > 1:
602*b7c941bbSAndroid Build Coastguard Worker    image_processing_utils.write_image(img/255, img_name, True)
603*b7c941bbSAndroid Build Coastguard Worker    cv2.drawContours(img, circle_contours, -1, CV2_RED,
604*b7c941bbSAndroid Build Coastguard Worker                     CV2_LINE_THICKNESS)
605*b7c941bbSAndroid Build Coastguard Worker    img_name_parts = img_name.split('.')
606*b7c941bbSAndroid Build Coastguard Worker    image_processing_utils.write_image(
607*b7c941bbSAndroid Build Coastguard Worker        img/255, f'{img_name_parts[0]}_contours.{img_name_parts[1]}', True)
608*b7c941bbSAndroid Build Coastguard Worker    if not use_adaptive_threshold:
609*b7c941bbSAndroid Build Coastguard Worker      return find_circle(
610*b7c941bbSAndroid Build Coastguard Worker          img, img_name, min_area, color, use_adaptive_threshold=True)
611*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('More than 1 circle detected. '
612*b7c941bbSAndroid Build Coastguard Worker                         'Background of scene may be too complex.')
613*b7c941bbSAndroid Build Coastguard Worker
614*b7c941bbSAndroid Build Coastguard Worker  return circle
615*b7c941bbSAndroid Build Coastguard Worker
616*b7c941bbSAndroid Build Coastguard Worker
617*b7c941bbSAndroid Build Coastguard Workerdef append_circle_center_to_img(circle, img, img_name, save_img=True):
618*b7c941bbSAndroid Build Coastguard Worker  """Append circle center and image center to image and save image.
619*b7c941bbSAndroid Build Coastguard Worker
620*b7c941bbSAndroid Build Coastguard Worker  Draws line from circle center to image center and then labels end-points.
621*b7c941bbSAndroid Build Coastguard Worker  Adjusts text positioning depending on circle center wrt image center.
622*b7c941bbSAndroid Build Coastguard Worker  Moves text position left/right half of up/down movement for visual aesthetics.
623*b7c941bbSAndroid Build Coastguard Worker
624*b7c941bbSAndroid Build Coastguard Worker  Args:
625*b7c941bbSAndroid Build Coastguard Worker    circle: dict with circle location vals.
626*b7c941bbSAndroid Build Coastguard Worker    img: numpy float image array in RGB, with pixel values in [0,255].
627*b7c941bbSAndroid Build Coastguard Worker    img_name: string with image info of format and size.
628*b7c941bbSAndroid Build Coastguard Worker    save_img: optional boolean to not save image
629*b7c941bbSAndroid Build Coastguard Worker  """
630*b7c941bbSAndroid Build Coastguard Worker  line_width_scaling_factor = 500
631*b7c941bbSAndroid Build Coastguard Worker  text_move_scaling_factor = 3
632*b7c941bbSAndroid Build Coastguard Worker  img_size = img.shape
633*b7c941bbSAndroid Build Coastguard Worker  img_center_x = img_size[1]//2
634*b7c941bbSAndroid Build Coastguard Worker  img_center_y = img_size[0]//2
635*b7c941bbSAndroid Build Coastguard Worker
636*b7c941bbSAndroid Build Coastguard Worker  # draw line from circle to image center
637*b7c941bbSAndroid Build Coastguard Worker  line_width = int(max(1, max(img_size)//line_width_scaling_factor))
638*b7c941bbSAndroid Build Coastguard Worker  font_size = line_width // 2
639*b7c941bbSAndroid Build Coastguard Worker  move_text_dist = line_width * text_move_scaling_factor
640*b7c941bbSAndroid Build Coastguard Worker  cv2.line(img, (circle['x'], circle['y']), (img_center_x, img_center_y),
641*b7c941bbSAndroid Build Coastguard Worker           CV2_RED, line_width)
642*b7c941bbSAndroid Build Coastguard Worker
643*b7c941bbSAndroid Build Coastguard Worker  # adjust text location
644*b7c941bbSAndroid Build Coastguard Worker  move_text_right_circle = -1
645*b7c941bbSAndroid Build Coastguard Worker  move_text_right_image = 2
646*b7c941bbSAndroid Build Coastguard Worker  if circle['x'] > img_center_x:
647*b7c941bbSAndroid Build Coastguard Worker    move_text_right_circle = 2
648*b7c941bbSAndroid Build Coastguard Worker    move_text_right_image = -1
649*b7c941bbSAndroid Build Coastguard Worker
650*b7c941bbSAndroid Build Coastguard Worker  move_text_down_circle = -1
651*b7c941bbSAndroid Build Coastguard Worker  move_text_down_image = 4
652*b7c941bbSAndroid Build Coastguard Worker  if circle['y'] > img_center_y:
653*b7c941bbSAndroid Build Coastguard Worker    move_text_down_circle = 4
654*b7c941bbSAndroid Build Coastguard Worker    move_text_down_image = -1
655*b7c941bbSAndroid Build Coastguard Worker
656*b7c941bbSAndroid Build Coastguard Worker  # add circles to end points and label
657*b7c941bbSAndroid Build Coastguard Worker  radius_pt = line_width * 2  # makes a dot 2x line width
658*b7c941bbSAndroid Build Coastguard Worker  filled_pt = -1  # cv2 value for a filled circle
659*b7c941bbSAndroid Build Coastguard Worker  # circle center
660*b7c941bbSAndroid Build Coastguard Worker  cv2.circle(img, (circle['x'], circle['y']), radius_pt, CV2_RED, filled_pt)
661*b7c941bbSAndroid Build Coastguard Worker  text_circle_x = move_text_dist * move_text_right_circle + circle['x']
662*b7c941bbSAndroid Build Coastguard Worker  text_circle_y = move_text_dist * move_text_down_circle + circle['y']
663*b7c941bbSAndroid Build Coastguard Worker  cv2.putText(img, 'circle center', (text_circle_x, text_circle_y),
664*b7c941bbSAndroid Build Coastguard Worker              cv2.FONT_HERSHEY_SIMPLEX, font_size, CV2_RED, line_width)
665*b7c941bbSAndroid Build Coastguard Worker  # image center
666*b7c941bbSAndroid Build Coastguard Worker  cv2.circle(img, (img_center_x, img_center_y), radius_pt, CV2_RED, filled_pt)
667*b7c941bbSAndroid Build Coastguard Worker  text_imgct_x = move_text_dist * move_text_right_image + img_center_x
668*b7c941bbSAndroid Build Coastguard Worker  text_imgct_y = move_text_dist * move_text_down_image + img_center_y
669*b7c941bbSAndroid Build Coastguard Worker  cv2.putText(img, 'image center', (text_imgct_x, text_imgct_y),
670*b7c941bbSAndroid Build Coastguard Worker              cv2.FONT_HERSHEY_SIMPLEX, font_size, CV2_RED, line_width)
671*b7c941bbSAndroid Build Coastguard Worker  if save_img:
672*b7c941bbSAndroid Build Coastguard Worker    image_processing_utils.write_image(img/255, img_name, True)  # [0, 1] values
673*b7c941bbSAndroid Build Coastguard Worker
674*b7c941bbSAndroid Build Coastguard Worker
675*b7c941bbSAndroid Build Coastguard Workerdef is_circle_cropped(circle, size):
676*b7c941bbSAndroid Build Coastguard Worker  """Determine if a circle is cropped by edge of image.
677*b7c941bbSAndroid Build Coastguard Worker
678*b7c941bbSAndroid Build Coastguard Worker  Args:
679*b7c941bbSAndroid Build Coastguard Worker    circle: list [x, y, radius] of circle
680*b7c941bbSAndroid Build Coastguard Worker    size: tuple (x, y) of size of img
681*b7c941bbSAndroid Build Coastguard Worker
682*b7c941bbSAndroid Build Coastguard Worker  Returns:
683*b7c941bbSAndroid Build Coastguard Worker    Boolean True if selected circle is cropped
684*b7c941bbSAndroid Build Coastguard Worker  """
685*b7c941bbSAndroid Build Coastguard Worker
686*b7c941bbSAndroid Build Coastguard Worker  cropped = False
687*b7c941bbSAndroid Build Coastguard Worker  circle_x, circle_y = circle[0], circle[1]
688*b7c941bbSAndroid Build Coastguard Worker  circle_r = circle[2]
689*b7c941bbSAndroid Build Coastguard Worker  x_min, x_max = circle_x - circle_r, circle_x + circle_r
690*b7c941bbSAndroid Build Coastguard Worker  y_min, y_max = circle_y - circle_r, circle_y + circle_r
691*b7c941bbSAndroid Build Coastguard Worker  if x_min < 0 or y_min < 0 or x_max > size[0] or y_max > size[1]:
692*b7c941bbSAndroid Build Coastguard Worker    cropped = True
693*b7c941bbSAndroid Build Coastguard Worker  return cropped
694*b7c941bbSAndroid Build Coastguard Worker
695*b7c941bbSAndroid Build Coastguard Worker
696*b7c941bbSAndroid Build Coastguard Workerdef find_white_square(img, min_area):
697*b7c941bbSAndroid Build Coastguard Worker  """Find the white square in the test image.
698*b7c941bbSAndroid Build Coastguard Worker
699*b7c941bbSAndroid Build Coastguard Worker  Args:
700*b7c941bbSAndroid Build Coastguard Worker    img: numpy image array in RGB, with pixel values in [0,255].
701*b7c941bbSAndroid Build Coastguard Worker    min_area: float of minimum area of circle to find
702*b7c941bbSAndroid Build Coastguard Worker
703*b7c941bbSAndroid Build Coastguard Worker  Returns:
704*b7c941bbSAndroid Build Coastguard Worker    square = {'left', 'right', 'top', 'bottom', 'width', 'height'}
705*b7c941bbSAndroid Build Coastguard Worker  """
706*b7c941bbSAndroid Build Coastguard Worker  square = {}
707*b7c941bbSAndroid Build Coastguard Worker  num_squares = 0
708*b7c941bbSAndroid Build Coastguard Worker  img_size = img.shape
709*b7c941bbSAndroid Build Coastguard Worker
710*b7c941bbSAndroid Build Coastguard Worker  # convert to gray-scale image
711*b7c941bbSAndroid Build Coastguard Worker  img_gray = image_processing_utils.convert_rgb_to_grayscale(img)
712*b7c941bbSAndroid Build Coastguard Worker
713*b7c941bbSAndroid Build Coastguard Worker  # otsu threshold to binarize the image
714*b7c941bbSAndroid Build Coastguard Worker  img_bw = binarize_image(img_gray)
715*b7c941bbSAndroid Build Coastguard Worker
716*b7c941bbSAndroid Build Coastguard Worker  # find contours
717*b7c941bbSAndroid Build Coastguard Worker  contours = find_all_contours(img_bw)
718*b7c941bbSAndroid Build Coastguard Worker
719*b7c941bbSAndroid Build Coastguard Worker  # Check each contour and find the square bigger than min_area
720*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Initial number of contours: %d', len(contours))
721*b7c941bbSAndroid Build Coastguard Worker  min_area = img_size[0]*img_size[1]*min_area
722*b7c941bbSAndroid Build Coastguard Worker  logging.debug('min_area: %.3f', min_area)
723*b7c941bbSAndroid Build Coastguard Worker  for contour in contours:
724*b7c941bbSAndroid Build Coastguard Worker    area = cv2.contourArea(contour)
725*b7c941bbSAndroid Build Coastguard Worker    num_pts = len(contour)
726*b7c941bbSAndroid Build Coastguard Worker    if (area > min_area and num_pts >= 4):
727*b7c941bbSAndroid Build Coastguard Worker      shape = component_shape(contour)
728*b7c941bbSAndroid Build Coastguard Worker      squarish = (shape['width'] * shape['height']) / area
729*b7c941bbSAndroid Build Coastguard Worker      aspect_ratio = shape['width'] / shape['height']
730*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Potential square found. squarish: %.3f, ar: %.3f, pts: %d',
731*b7c941bbSAndroid Build Coastguard Worker                    squarish, aspect_ratio, num_pts)
732*b7c941bbSAndroid Build Coastguard Worker      if (math.isclose(1.0, squarish, abs_tol=SQUARISH_RTOL) and
733*b7c941bbSAndroid Build Coastguard Worker          math.isclose(1.0, aspect_ratio, abs_tol=SQUARISH_AR_RTOL)):
734*b7c941bbSAndroid Build Coastguard Worker        # Populate square dictionary
735*b7c941bbSAndroid Build Coastguard Worker        angle = cv2.minAreaRect(contour)[-1]
736*b7c941bbSAndroid Build Coastguard Worker        if angle < -45:
737*b7c941bbSAndroid Build Coastguard Worker          angle += 90
738*b7c941bbSAndroid Build Coastguard Worker        square['angle'] = angle
739*b7c941bbSAndroid Build Coastguard Worker        square['left'] = shape['left'] - SQUARE_CROP_MARGIN
740*b7c941bbSAndroid Build Coastguard Worker        square['right'] = shape['right'] + SQUARE_CROP_MARGIN
741*b7c941bbSAndroid Build Coastguard Worker        square['top'] = shape['top'] - SQUARE_CROP_MARGIN
742*b7c941bbSAndroid Build Coastguard Worker        square['bottom'] = shape['bottom'] + SQUARE_CROP_MARGIN
743*b7c941bbSAndroid Build Coastguard Worker        square['w'] = shape['width'] + 2*SQUARE_CROP_MARGIN
744*b7c941bbSAndroid Build Coastguard Worker        square['h'] = shape['height'] + 2*SQUARE_CROP_MARGIN
745*b7c941bbSAndroid Build Coastguard Worker        num_squares += 1
746*b7c941bbSAndroid Build Coastguard Worker
747*b7c941bbSAndroid Build Coastguard Worker  if num_squares == 0:
748*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('No white square detected. '
749*b7c941bbSAndroid Build Coastguard Worker                         'Please take pictures according to instructions.')
750*b7c941bbSAndroid Build Coastguard Worker  if num_squares > 1:
751*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('More than 1 white square detected. '
752*b7c941bbSAndroid Build Coastguard Worker                         'Background of scene may be too complex.')
753*b7c941bbSAndroid Build Coastguard Worker  return square
754*b7c941bbSAndroid Build Coastguard Worker
755*b7c941bbSAndroid Build Coastguard Worker
756*b7c941bbSAndroid Build Coastguard Workerdef get_angle(input_img):
757*b7c941bbSAndroid Build Coastguard Worker  """Computes anglular inclination of chessboard in input_img.
758*b7c941bbSAndroid Build Coastguard Worker
759*b7c941bbSAndroid Build Coastguard Worker  Args:
760*b7c941bbSAndroid Build Coastguard Worker    input_img (2D numpy.ndarray): Grayscale image stored as a 2D numpy array.
761*b7c941bbSAndroid Build Coastguard Worker  Returns:
762*b7c941bbSAndroid Build Coastguard Worker    Median angle of squares in degrees identified in the image.
763*b7c941bbSAndroid Build Coastguard Worker
764*b7c941bbSAndroid Build Coastguard Worker  Angle estimation algorithm description:
765*b7c941bbSAndroid Build Coastguard Worker    Input: 2D grayscale image of chessboard.
766*b7c941bbSAndroid Build Coastguard Worker    Output: Angle of rotation of chessboard perpendicular to
767*b7c941bbSAndroid Build Coastguard Worker            chessboard. Assumes chessboard and camera are parallel to
768*b7c941bbSAndroid Build Coastguard Worker            each other.
769*b7c941bbSAndroid Build Coastguard Worker
770*b7c941bbSAndroid Build Coastguard Worker    1) Use adaptive threshold to make image binary
771*b7c941bbSAndroid Build Coastguard Worker    2) Find countours
772*b7c941bbSAndroid Build Coastguard Worker    3) Filter out small contours
773*b7c941bbSAndroid Build Coastguard Worker    4) Filter out all non-square contours
774*b7c941bbSAndroid Build Coastguard Worker    5) Compute most common square shape.
775*b7c941bbSAndroid Build Coastguard Worker        The assumption here is that the most common square instances are the
776*b7c941bbSAndroid Build Coastguard Worker        chessboard squares. We've shown that with our current tuning, we can
777*b7c941bbSAndroid Build Coastguard Worker        robustly identify the squares on the sensor fusion chessboard.
778*b7c941bbSAndroid Build Coastguard Worker    6) Return median angle of most common square shape.
779*b7c941bbSAndroid Build Coastguard Worker
780*b7c941bbSAndroid Build Coastguard Worker  USAGE NOTE: This function has been tuned to work for the chessboard used in
781*b7c941bbSAndroid Build Coastguard Worker  the sensor_fusion tests. See images in test_images/rotated_chessboard/ for
782*b7c941bbSAndroid Build Coastguard Worker  sample captures. If this function is used with other chessboards, it may not
783*b7c941bbSAndroid Build Coastguard Worker  work as expected.
784*b7c941bbSAndroid Build Coastguard Worker  """
785*b7c941bbSAndroid Build Coastguard Worker  # Tuning parameters
786*b7c941bbSAndroid Build Coastguard Worker  square_area_min = (float)(input_img.shape[1] * SQUARE_AREA_MIN_REL)
787*b7c941bbSAndroid Build Coastguard Worker
788*b7c941bbSAndroid Build Coastguard Worker  # Creates copy of image to avoid modifying original.
789*b7c941bbSAndroid Build Coastguard Worker  img = numpy.array(input_img, copy=True)
790*b7c941bbSAndroid Build Coastguard Worker
791*b7c941bbSAndroid Build Coastguard Worker  # Scale pixel values from 0-1 to 0-255
792*b7c941bbSAndroid Build Coastguard Worker  img_uint8 = image_processing_utils.convert_image_to_uint8(img)
793*b7c941bbSAndroid Build Coastguard Worker  img_thresh = cv2.adaptiveThreshold(
794*b7c941bbSAndroid Build Coastguard Worker      img_uint8, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 201, 2)
795*b7c941bbSAndroid Build Coastguard Worker
796*b7c941bbSAndroid Build Coastguard Worker  # Find all contours.
797*b7c941bbSAndroid Build Coastguard Worker  contours = find_all_contours(img_thresh)
798*b7c941bbSAndroid Build Coastguard Worker
799*b7c941bbSAndroid Build Coastguard Worker  # Filter contours to squares only.
800*b7c941bbSAndroid Build Coastguard Worker  square_contours = []
801*b7c941bbSAndroid Build Coastguard Worker  for contour in contours:
802*b7c941bbSAndroid Build Coastguard Worker    rect = cv2.minAreaRect(contour)
803*b7c941bbSAndroid Build Coastguard Worker    _, (width, height), angle = rect
804*b7c941bbSAndroid Build Coastguard Worker
805*b7c941bbSAndroid Build Coastguard Worker    # Skip non-squares
806*b7c941bbSAndroid Build Coastguard Worker    if not math.isclose(width, height, rel_tol=SQUARE_TOL):
807*b7c941bbSAndroid Build Coastguard Worker      continue
808*b7c941bbSAndroid Build Coastguard Worker
809*b7c941bbSAndroid Build Coastguard Worker    # Remove very small contours: usually just tiny dots due to noise.
810*b7c941bbSAndroid Build Coastguard Worker    area = cv2.contourArea(contour)
811*b7c941bbSAndroid Build Coastguard Worker    if area < square_area_min:
812*b7c941bbSAndroid Build Coastguard Worker      continue
813*b7c941bbSAndroid Build Coastguard Worker
814*b7c941bbSAndroid Build Coastguard Worker    square_contours.append(contour)
815*b7c941bbSAndroid Build Coastguard Worker
816*b7c941bbSAndroid Build Coastguard Worker  areas = []
817*b7c941bbSAndroid Build Coastguard Worker  for contour in square_contours:
818*b7c941bbSAndroid Build Coastguard Worker    area = cv2.contourArea(contour)
819*b7c941bbSAndroid Build Coastguard Worker    areas.append(area)
820*b7c941bbSAndroid Build Coastguard Worker
821*b7c941bbSAndroid Build Coastguard Worker  median_area = numpy.median(areas)
822*b7c941bbSAndroid Build Coastguard Worker
823*b7c941bbSAndroid Build Coastguard Worker  filtered_squares = []
824*b7c941bbSAndroid Build Coastguard Worker  filtered_angles = []
825*b7c941bbSAndroid Build Coastguard Worker  for square in square_contours:
826*b7c941bbSAndroid Build Coastguard Worker    area = cv2.contourArea(square)
827*b7c941bbSAndroid Build Coastguard Worker    if not math.isclose(area, median_area, rel_tol=SQUARE_TOL):
828*b7c941bbSAndroid Build Coastguard Worker      continue
829*b7c941bbSAndroid Build Coastguard Worker
830*b7c941bbSAndroid Build Coastguard Worker    filtered_squares.append(square)
831*b7c941bbSAndroid Build Coastguard Worker    _, (width, height), angle = cv2.minAreaRect(square)
832*b7c941bbSAndroid Build Coastguard Worker    filtered_angles.append(angle)
833*b7c941bbSAndroid Build Coastguard Worker
834*b7c941bbSAndroid Build Coastguard Worker  if len(filtered_angles) < ANGLE_NUM_MIN:
835*b7c941bbSAndroid Build Coastguard Worker    logging.debug(
836*b7c941bbSAndroid Build Coastguard Worker        'A frame had too few angles to be processed. '
837*b7c941bbSAndroid Build Coastguard Worker        'Num of angles: %d, MIN: %d', len(filtered_angles), ANGLE_NUM_MIN)
838*b7c941bbSAndroid Build Coastguard Worker    return None
839*b7c941bbSAndroid Build Coastguard Worker
840*b7c941bbSAndroid Build Coastguard Worker  return numpy.median(filtered_angles)
841*b7c941bbSAndroid Build Coastguard Worker
842*b7c941bbSAndroid Build Coastguard Worker
843*b7c941bbSAndroid Build Coastguard Workerdef correct_faces_for_crop(faces, img, crop):
844*b7c941bbSAndroid Build Coastguard Worker  """Correct face rectangles for sensor crop.
845*b7c941bbSAndroid Build Coastguard Worker
846*b7c941bbSAndroid Build Coastguard Worker  Args:
847*b7c941bbSAndroid Build Coastguard Worker    faces: list of dicts with face information relative to sensor's
848*b7c941bbSAndroid Build Coastguard Worker      aspect ratio
849*b7c941bbSAndroid Build Coastguard Worker    img: np image array
850*b7c941bbSAndroid Build Coastguard Worker    crop: dict of crop region size with 'top', 'right', 'left', 'bottom'
851*b7c941bbSAndroid Build Coastguard Worker      as keys to desired region of the sensor to read out
852*b7c941bbSAndroid Build Coastguard Worker  Returns:
853*b7c941bbSAndroid Build Coastguard Worker    list of face locations (left, right, top, bottom) corrected
854*b7c941bbSAndroid Build Coastguard Worker  """
855*b7c941bbSAndroid Build Coastguard Worker  faces_corrected = []
856*b7c941bbSAndroid Build Coastguard Worker  crop_w = crop['right'] - crop['left']
857*b7c941bbSAndroid Build Coastguard Worker  crop_h = crop['bottom'] - crop['top']
858*b7c941bbSAndroid Build Coastguard Worker  logging.debug('crop region: %s', str(crop))
859*b7c941bbSAndroid Build Coastguard Worker  img_w, img_h = img.shape[1], img.shape[0]
860*b7c941bbSAndroid Build Coastguard Worker  crop_aspect_ratio = crop_w / crop_h
861*b7c941bbSAndroid Build Coastguard Worker  img_aspect_ratio = img_w / img_h
862*b7c941bbSAndroid Build Coastguard Worker  for rect in [face['bounds'] for face in faces]:
863*b7c941bbSAndroid Build Coastguard Worker    logging.debug('rect: %s', str(rect))
864*b7c941bbSAndroid Build Coastguard Worker    if crop_aspect_ratio >= img_aspect_ratio:
865*b7c941bbSAndroid Build Coastguard Worker      # Sensor width is being cropped, so we need to adjust the horizontal
866*b7c941bbSAndroid Build Coastguard Worker      # coordinates of the face rectangles to account for the crop.
867*b7c941bbSAndroid Build Coastguard Worker      # Since we are converting from sensor coordinates to image coordinates
868*b7c941bbSAndroid Build Coastguard Worker      img_crop_h_ratio = img_h / crop_h
869*b7c941bbSAndroid Build Coastguard Worker      scaled_crop_w = crop_w * img_crop_h_ratio
870*b7c941bbSAndroid Build Coastguard Worker      excess_w = (img_w - scaled_crop_w) / 2
871*b7c941bbSAndroid Build Coastguard Worker      left = int(
872*b7c941bbSAndroid Build Coastguard Worker          round((rect['left'] - crop['left']) * img_crop_h_ratio + excess_w))
873*b7c941bbSAndroid Build Coastguard Worker      right = int(
874*b7c941bbSAndroid Build Coastguard Worker          round((rect['right'] - crop['left']) * img_crop_h_ratio + excess_w))
875*b7c941bbSAndroid Build Coastguard Worker      top = int(round((rect['top'] - crop['top']) * img_crop_h_ratio))
876*b7c941bbSAndroid Build Coastguard Worker      bottom = int(round((rect['bottom'] - crop['top']) * img_crop_h_ratio))
877*b7c941bbSAndroid Build Coastguard Worker    else:
878*b7c941bbSAndroid Build Coastguard Worker      # Sensor height is being cropped, so we need to adjust the vertical
879*b7c941bbSAndroid Build Coastguard Worker      # coordinates of the face rectangles to account for the crop.
880*b7c941bbSAndroid Build Coastguard Worker      img_crop_w_ratio = img_w / crop_w
881*b7c941bbSAndroid Build Coastguard Worker      scaled_crop_h = crop_h * img_crop_w_ratio
882*b7c941bbSAndroid Build Coastguard Worker      excess_w = (img_h - scaled_crop_h) / 2
883*b7c941bbSAndroid Build Coastguard Worker      left = int(round((rect['left'] - crop['left']) * img_crop_w_ratio))
884*b7c941bbSAndroid Build Coastguard Worker      right = int(round((rect['right'] - crop['left']) * img_crop_w_ratio))
885*b7c941bbSAndroid Build Coastguard Worker      top = int(
886*b7c941bbSAndroid Build Coastguard Worker          round((rect['top'] - crop['top']) * img_crop_w_ratio + excess_w))
887*b7c941bbSAndroid Build Coastguard Worker      bottom = int(
888*b7c941bbSAndroid Build Coastguard Worker          round((rect['bottom'] - crop['top']) * img_crop_w_ratio + excess_w))
889*b7c941bbSAndroid Build Coastguard Worker    faces_corrected.append([left, right, top, bottom])
890*b7c941bbSAndroid Build Coastguard Worker  logging.debug('faces_corrected: %s', str(faces_corrected))
891*b7c941bbSAndroid Build Coastguard Worker  return faces_corrected
892*b7c941bbSAndroid Build Coastguard Worker
893*b7c941bbSAndroid Build Coastguard Worker
894*b7c941bbSAndroid Build Coastguard Workerdef eliminate_duplicate_centers(coordinates_list):
895*b7c941bbSAndroid Build Coastguard Worker  """Checks center coordinates of OpenCV's face rectangles.
896*b7c941bbSAndroid Build Coastguard Worker
897*b7c941bbSAndroid Build Coastguard Worker  Method makes sure that the list of face rectangles' centers do not
898*b7c941bbSAndroid Build Coastguard Worker  contain duplicates from the same face
899*b7c941bbSAndroid Build Coastguard Worker
900*b7c941bbSAndroid Build Coastguard Worker  Args:
901*b7c941bbSAndroid Build Coastguard Worker    coordinates_list: list; coordinates of face rectangles' centers
902*b7c941bbSAndroid Build Coastguard Worker  Returns:
903*b7c941bbSAndroid Build Coastguard Worker    non_duplicate_list: list; coordinates of face rectangles' centers
904*b7c941bbSAndroid Build Coastguard Worker    without duplicates on the same face
905*b7c941bbSAndroid Build Coastguard Worker  """
906*b7c941bbSAndroid Build Coastguard Worker  output = set()
907*b7c941bbSAndroid Build Coastguard Worker
908*b7c941bbSAndroid Build Coastguard Worker  for _, xy1 in enumerate(coordinates_list):
909*b7c941bbSAndroid Build Coastguard Worker    for _, xy2 in enumerate(coordinates_list):
910*b7c941bbSAndroid Build Coastguard Worker      if scipy.spatial.distance.euclidean(xy1, xy2) < FACE_MIN_CENTER_DELTA:
911*b7c941bbSAndroid Build Coastguard Worker        continue
912*b7c941bbSAndroid Build Coastguard Worker      if xy1 not in output:
913*b7c941bbSAndroid Build Coastguard Worker        output.add(xy1)
914*b7c941bbSAndroid Build Coastguard Worker      else:
915*b7c941bbSAndroid Build Coastguard Worker        output.add(xy2)
916*b7c941bbSAndroid Build Coastguard Worker  return list(output)
917*b7c941bbSAndroid Build Coastguard Worker
918*b7c941bbSAndroid Build Coastguard Worker
919*b7c941bbSAndroid Build Coastguard Workerdef match_face_locations(faces_cropped, faces_opencv, img, img_name):
920*b7c941bbSAndroid Build Coastguard Worker  """Assert face locations between two methods.
921*b7c941bbSAndroid Build Coastguard Worker
922*b7c941bbSAndroid Build Coastguard Worker  Method determines if center of opencv face boxes is within face detection
923*b7c941bbSAndroid Build Coastguard Worker  face boxes. Using math.hypot to measure the distance between the centers,
924*b7c941bbSAndroid Build Coastguard Worker  as math.dist is not available for python versions before 3.8.
925*b7c941bbSAndroid Build Coastguard Worker
926*b7c941bbSAndroid Build Coastguard Worker  Args:
927*b7c941bbSAndroid Build Coastguard Worker    faces_cropped: list of lists with (l, r, t, b) for each face.
928*b7c941bbSAndroid Build Coastguard Worker    faces_opencv: list of lists with (x, y, w, h) for each face.
929*b7c941bbSAndroid Build Coastguard Worker    img: numpy [0, 1] image array
930*b7c941bbSAndroid Build Coastguard Worker    img_name: text string with path to image file
931*b7c941bbSAndroid Build Coastguard Worker  """
932*b7c941bbSAndroid Build Coastguard Worker  # turn faces_opencv into list of center locations
933*b7c941bbSAndroid Build Coastguard Worker  faces_opencv_center = [(x+w//2, y+h//2) for (x, y, w, h) in faces_opencv]
934*b7c941bbSAndroid Build Coastguard Worker  cropped_faces_centers = [
935*b7c941bbSAndroid Build Coastguard Worker      ((l+r)//2, (t+b)//2) for (l, r, t, b) in faces_cropped]
936*b7c941bbSAndroid Build Coastguard Worker  faces_opencv_center.sort(key=lambda t: [t[1], t[0]])
937*b7c941bbSAndroid Build Coastguard Worker  cropped_faces_centers.sort(key=lambda t: [t[1], t[0]])
938*b7c941bbSAndroid Build Coastguard Worker  logging.debug('cropped face centers: %s', str(cropped_faces_centers))
939*b7c941bbSAndroid Build Coastguard Worker  logging.debug('opencv face center: %s', str(faces_opencv_center))
940*b7c941bbSAndroid Build Coastguard Worker  faces_opencv_centers = []
941*b7c941bbSAndroid Build Coastguard Worker  num_centers_aligned = 0
942*b7c941bbSAndroid Build Coastguard Worker
943*b7c941bbSAndroid Build Coastguard Worker  # eliminate duplicate openCV face rectangles' centers the same face
944*b7c941bbSAndroid Build Coastguard Worker  faces_opencv_centers = eliminate_duplicate_centers(faces_opencv_center)
945*b7c941bbSAndroid Build Coastguard Worker  logging.debug('opencv face centers: %s', str(faces_opencv_centers))
946*b7c941bbSAndroid Build Coastguard Worker
947*b7c941bbSAndroid Build Coastguard Worker  for (x, y) in faces_opencv_centers:
948*b7c941bbSAndroid Build Coastguard Worker    for (x1, y1) in cropped_faces_centers:
949*b7c941bbSAndroid Build Coastguard Worker      centers_dist = math.hypot(x-x1, y-y1)
950*b7c941bbSAndroid Build Coastguard Worker      if centers_dist < FACE_CENTER_MIN_LOGGING_DIST:
951*b7c941bbSAndroid Build Coastguard Worker        logging.debug('centers_dist: %.3f', centers_dist)
952*b7c941bbSAndroid Build Coastguard Worker      if (abs(x-x1) < FACE_CENTER_MATCH_TOL_X and
953*b7c941bbSAndroid Build Coastguard Worker          abs(y-y1) < FACE_CENTER_MATCH_TOL_Y):
954*b7c941bbSAndroid Build Coastguard Worker        num_centers_aligned += 1
955*b7c941bbSAndroid Build Coastguard Worker
956*b7c941bbSAndroid Build Coastguard Worker  # If test failed, save image with green AND OpenCV red rectangles
957*b7c941bbSAndroid Build Coastguard Worker  image_processing_utils.write_image(img, img_name)
958*b7c941bbSAndroid Build Coastguard Worker  if num_centers_aligned < FACES_ALIGNED_MIN_NUM:
959*b7c941bbSAndroid Build Coastguard Worker    for (x, y, w, h) in faces_opencv:
960*b7c941bbSAndroid Build Coastguard Worker      cv2.rectangle(img, (x, y), (x+w, y+h), CV2_RED_NORM, 2)
961*b7c941bbSAndroid Build Coastguard Worker      image_processing_utils.write_image(img, img_name)
962*b7c941bbSAndroid Build Coastguard Worker      logging.debug('centered: %s', str(num_centers_aligned))
963*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError(f'Face rectangles in wrong location(s)!. '
964*b7c941bbSAndroid Build Coastguard Worker                         f'Found {num_centers_aligned} rectangles near cropped '
965*b7c941bbSAndroid Build Coastguard Worker                         f'face centers, expected {FACES_ALIGNED_MIN_NUM}')
966*b7c941bbSAndroid Build Coastguard Worker
967*b7c941bbSAndroid Build Coastguard Worker
968*b7c941bbSAndroid Build Coastguard Workerdef draw_green_boxes_around_faces(img, faces_cropped, img_name):
969*b7c941bbSAndroid Build Coastguard Worker  """Correct face rectangles for sensor crop.
970*b7c941bbSAndroid Build Coastguard Worker
971*b7c941bbSAndroid Build Coastguard Worker  Args:
972*b7c941bbSAndroid Build Coastguard Worker    img: numpy [0, 1] image array
973*b7c941bbSAndroid Build Coastguard Worker    faces_cropped: list of lists with (l, r, t, b) for each face
974*b7c941bbSAndroid Build Coastguard Worker    img_name: text string with path to image file
975*b7c941bbSAndroid Build Coastguard Worker  Returns:
976*b7c941bbSAndroid Build Coastguard Worker    image with green rectangles
977*b7c941bbSAndroid Build Coastguard Worker  """
978*b7c941bbSAndroid Build Coastguard Worker  # draw boxes around faces in green and save image
979*b7c941bbSAndroid Build Coastguard Worker  for (l, r, t, b) in faces_cropped:
980*b7c941bbSAndroid Build Coastguard Worker    cv2.rectangle(img, (l, t), (r, b), CV2_GREEN_NORM, 2)
981*b7c941bbSAndroid Build Coastguard Worker  image_processing_utils.write_image(img, img_name)
982*b7c941bbSAndroid Build Coastguard Worker
983*b7c941bbSAndroid Build Coastguard Worker
984*b7c941bbSAndroid Build Coastguard Workerdef version_agnostic_detect_markers(image):
985*b7c941bbSAndroid Build Coastguard Worker  """Detects ArUco markers with compatibility across cv2 versions.
986*b7c941bbSAndroid Build Coastguard Worker
987*b7c941bbSAndroid Build Coastguard Worker  Args:
988*b7c941bbSAndroid Build Coastguard Worker    image: numpy image in BGR channel order with ArUco markers to be detected.
989*b7c941bbSAndroid Build Coastguard Worker  Returns:
990*b7c941bbSAndroid Build Coastguard Worker    corners: list of detected corners.
991*b7c941bbSAndroid Build Coastguard Worker    ids: list of int ids for each ArUco markers in the input_img.
992*b7c941bbSAndroid Build Coastguard Worker    rejected_params: list of rejected corners.
993*b7c941bbSAndroid Build Coastguard Worker  """
994*b7c941bbSAndroid Build Coastguard Worker  # ArUco markers used are 4x4
995*b7c941bbSAndroid Build Coastguard Worker  aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_100)
996*b7c941bbSAndroid Build Coastguard Worker  parameters = cv2.aruco.DetectorParameters()
997*b7c941bbSAndroid Build Coastguard Worker  aruco_detector = None
998*b7c941bbSAndroid Build Coastguard Worker  if hasattr(cv2.aruco, ARUCO_DETECTOR_ATTRIBUTE_NAME):
999*b7c941bbSAndroid Build Coastguard Worker    aruco_detector = cv2.aruco.ArucoDetector(aruco_dict, parameters)
1000*b7c941bbSAndroid Build Coastguard Worker  # Use ArucoDetector object if available, else fall back to detectMarkers()
1001*b7c941bbSAndroid Build Coastguard Worker  if aruco_detector is not None:
1002*b7c941bbSAndroid Build Coastguard Worker    return aruco_detector.detectMarkers(image)
1003*b7c941bbSAndroid Build Coastguard Worker  else:
1004*b7c941bbSAndroid Build Coastguard Worker    return cv2.aruco.detectMarkers(
1005*b7c941bbSAndroid Build Coastguard Worker        image, aruco_dict, parameters=parameters
1006*b7c941bbSAndroid Build Coastguard Worker    )
1007*b7c941bbSAndroid Build Coastguard Worker
1008*b7c941bbSAndroid Build Coastguard Worker
1009*b7c941bbSAndroid Build Coastguard Workerdef find_aruco_markers(
1010*b7c941bbSAndroid Build Coastguard Worker    input_img, output_img_path, aruco_marker_count=ARUCO_CORNER_COUNT,
1011*b7c941bbSAndroid Build Coastguard Worker    force_greyscale=False):
1012*b7c941bbSAndroid Build Coastguard Worker  """Detects ArUco markers in the input_img.
1013*b7c941bbSAndroid Build Coastguard Worker
1014*b7c941bbSAndroid Build Coastguard Worker  Finds ArUco markers in the input_img and draws the contours
1015*b7c941bbSAndroid Build Coastguard Worker  around them.
1016*b7c941bbSAndroid Build Coastguard Worker  Args:
1017*b7c941bbSAndroid Build Coastguard Worker    input_img: input img in numpy array with ArUco markers
1018*b7c941bbSAndroid Build Coastguard Worker      to be detected
1019*b7c941bbSAndroid Build Coastguard Worker    output_img_path: path of the image to be saved with contours
1020*b7c941bbSAndroid Build Coastguard Worker      around the markers detected
1021*b7c941bbSAndroid Build Coastguard Worker    aruco_marker_count: optional int for minimum markers to expect.
1022*b7c941bbSAndroid Build Coastguard Worker    force_greyscale: optional bool to force greyscale detection, even if enough
1023*b7c941bbSAndroid Build Coastguard Worker      markers are detected.
1024*b7c941bbSAndroid Build Coastguard Worker  Returns:
1025*b7c941bbSAndroid Build Coastguard Worker    corners: list of detected corners
1026*b7c941bbSAndroid Build Coastguard Worker    ids: list of int ids for each ArUco markers in the input_img
1027*b7c941bbSAndroid Build Coastguard Worker    rejected_params: list of rejected corners
1028*b7c941bbSAndroid Build Coastguard Worker  """
1029*b7c941bbSAndroid Build Coastguard Worker  corners, ids, rejected_params = version_agnostic_detect_markers(input_img)
1030*b7c941bbSAndroid Build Coastguard Worker  # Early return if sufficient markers found and greyscale detection not needed
1031*b7c941bbSAndroid Build Coastguard Worker  if ids is not None and len(ids) >= aruco_marker_count and not force_greyscale:
1032*b7c941bbSAndroid Build Coastguard Worker    logging.debug('All ArUco markers detected.')
1033*b7c941bbSAndroid Build Coastguard Worker    cv2.aruco.drawDetectedMarkers(input_img, corners, ids)
1034*b7c941bbSAndroid Build Coastguard Worker    image_processing_utils.write_image(input_img / 255, output_img_path)
1035*b7c941bbSAndroid Build Coastguard Worker    return corners, ids, rejected_params
1036*b7c941bbSAndroid Build Coastguard Worker  # Try with high-contrast greyscale if needed
1037*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Trying ArUco marker detection with greyscale image.')
1038*b7c941bbSAndroid Build Coastguard Worker  bw_img = convert_image_to_high_contrast_black_white(input_img)
1039*b7c941bbSAndroid Build Coastguard Worker  corners, ids, rejected_params = version_agnostic_detect_markers(bw_img)
1040*b7c941bbSAndroid Build Coastguard Worker  if ids is not None and len(ids) >= aruco_marker_count:
1041*b7c941bbSAndroid Build Coastguard Worker    logging.debug('All ArUco markers detected with greyscale image.')
1042*b7c941bbSAndroid Build Coastguard Worker  # Handle case where no markers are found
1043*b7c941bbSAndroid Build Coastguard Worker  if ids is None:
1044*b7c941bbSAndroid Build Coastguard Worker    image_processing_utils.write_image(input_img/255, output_img_path)
1045*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('ArUco markers not detected.')
1046*b7c941bbSAndroid Build Coastguard Worker  # Log and save results
1047*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Number of ArUco markers detected w/ greyscale: %d', len(ids))
1048*b7c941bbSAndroid Build Coastguard Worker  logging.debug('IDs of the ArUco markers detected: %s', ids)
1049*b7c941bbSAndroid Build Coastguard Worker  logging.debug('Corners of the ArUco markers detected: %s', corners)
1050*b7c941bbSAndroid Build Coastguard Worker  cv2.aruco.drawDetectedMarkers(bw_img, corners, ids)
1051*b7c941bbSAndroid Build Coastguard Worker  image_processing_utils.write_image(bw_img / 255, output_img_path)
1052*b7c941bbSAndroid Build Coastguard Worker  return corners, ids, rejected_params
1053*b7c941bbSAndroid Build Coastguard Worker
1054*b7c941bbSAndroid Build Coastguard Worker
1055*b7c941bbSAndroid Build Coastguard Workerdef get_patch_from_aruco_markers(
1056*b7c941bbSAndroid Build Coastguard Worker    input_img, aruco_marker_corners, aruco_marker_ids):
1057*b7c941bbSAndroid Build Coastguard Worker  """Returns the rectangle patch from the aruco marker corners.
1058*b7c941bbSAndroid Build Coastguard Worker
1059*b7c941bbSAndroid Build Coastguard Worker  Note: Refer to image used in scene7 for ArUco markers location.
1060*b7c941bbSAndroid Build Coastguard Worker
1061*b7c941bbSAndroid Build Coastguard Worker  Args:
1062*b7c941bbSAndroid Build Coastguard Worker    input_img: input img in numpy array with ArUco markers
1063*b7c941bbSAndroid Build Coastguard Worker      to be detected
1064*b7c941bbSAndroid Build Coastguard Worker    aruco_marker_corners: array of aruco marker corner coordinates detected by
1065*b7c941bbSAndroid Build Coastguard Worker      opencv_processing_utils.find_aruco_markers
1066*b7c941bbSAndroid Build Coastguard Worker    aruco_marker_ids: array of ids of aruco markers detected by
1067*b7c941bbSAndroid Build Coastguard Worker      opencv_processing_utils.find_aruco_markers
1068*b7c941bbSAndroid Build Coastguard Worker  Returns:
1069*b7c941bbSAndroid Build Coastguard Worker    Numpy float image array of the rectangle patch
1070*b7c941bbSAndroid Build Coastguard Worker  """
1071*b7c941bbSAndroid Build Coastguard Worker  outer_rect_coordinates = {}
1072*b7c941bbSAndroid Build Coastguard Worker  for corner, marker_id in zip(aruco_marker_corners, aruco_marker_ids):
1073*b7c941bbSAndroid Build Coastguard Worker    corner = corner.reshape(4, 2)  # opencv returns 3D array
1074*b7c941bbSAndroid Build Coastguard Worker    index = marker_id[0]
1075*b7c941bbSAndroid Build Coastguard Worker    # Roll the array 4x to align with the coordinates of the corner adjacent
1076*b7c941bbSAndroid Build Coastguard Worker    # to the corner of the rectangle
1077*b7c941bbSAndroid Build Coastguard Worker    # Marker id: 0 => index 2 coordinates
1078*b7c941bbSAndroid Build Coastguard Worker    # Marker id: 1 => index 3 coordinates
1079*b7c941bbSAndroid Build Coastguard Worker    # Marker id: 2 => index 0 coordinates
1080*b7c941bbSAndroid Build Coastguard Worker    # Marker id: 3 => index 1 coordinates
1081*b7c941bbSAndroid Build Coastguard Worker    corner = numpy.roll(corner, 4)
1082*b7c941bbSAndroid Build Coastguard Worker
1083*b7c941bbSAndroid Build Coastguard Worker    outer_rect_coordinates[index] = tuple(corner[index])
1084*b7c941bbSAndroid Build Coastguard Worker
1085*b7c941bbSAndroid Build Coastguard Worker  red_corner = tuple(map(int, outer_rect_coordinates[0]))
1086*b7c941bbSAndroid Build Coastguard Worker  green_corner = tuple(map(int, outer_rect_coordinates[1]))
1087*b7c941bbSAndroid Build Coastguard Worker  gray_corner = tuple(map(int, outer_rect_coordinates[2]))
1088*b7c941bbSAndroid Build Coastguard Worker  blue_corner = tuple(map(int, outer_rect_coordinates[3]))
1089*b7c941bbSAndroid Build Coastguard Worker
1090*b7c941bbSAndroid Build Coastguard Worker  logging.debug('red_corner: %s', red_corner)
1091*b7c941bbSAndroid Build Coastguard Worker  logging.debug('blue_corner: %s', blue_corner)
1092*b7c941bbSAndroid Build Coastguard Worker  logging.debug('green_corner: %s', green_corner)
1093*b7c941bbSAndroid Build Coastguard Worker  logging.debug('gray_corner: %s', gray_corner)
1094*b7c941bbSAndroid Build Coastguard Worker  # Ensure that the image is not rotated
1095*b7c941bbSAndroid Build Coastguard Worker  blue_gray_y_diff = abs(gray_corner[1] - blue_corner[1])
1096*b7c941bbSAndroid Build Coastguard Worker  red_green_y_diff = abs(green_corner[1] - red_corner[1])
1097*b7c941bbSAndroid Build Coastguard Worker
1098*b7c941bbSAndroid Build Coastguard Worker  if ((blue_gray_y_diff > IMAGE_ROTATION_THRESHOLD) or
1099*b7c941bbSAndroid Build Coastguard Worker      (red_green_y_diff > IMAGE_ROTATION_THRESHOLD)):
1100*b7c941bbSAndroid Build Coastguard Worker    raise AssertionError('Image rotation is not within the threshold. '
1101*b7c941bbSAndroid Build Coastguard Worker                         f'Actual blue_gray_y_diff: {blue_gray_y_diff}, '
1102*b7c941bbSAndroid Build Coastguard Worker                         f'red_green_y_diff: {red_green_y_diff} '
1103*b7c941bbSAndroid Build Coastguard Worker                         f'Expected {IMAGE_ROTATION_THRESHOLD}')
1104*b7c941bbSAndroid Build Coastguard Worker  cv2.rectangle(input_img, red_corner, gray_corner,
1105*b7c941bbSAndroid Build Coastguard Worker                CV2_RED_NORM, CV2_LINE_THICKNESS)
1106*b7c941bbSAndroid Build Coastguard Worker  return input_img[red_corner[1]:gray_corner[1],
1107*b7c941bbSAndroid Build Coastguard Worker                   red_corner[0]:gray_corner[0]].copy()
1108*b7c941bbSAndroid Build Coastguard Worker
1109*b7c941bbSAndroid Build Coastguard Worker
1110*b7c941bbSAndroid Build Coastguard Workerdef get_chart_boundary_from_aruco_markers(
1111*b7c941bbSAndroid Build Coastguard Worker    aruco_marker_corners, aruco_marker_ids, input_img, output_img_path):
1112*b7c941bbSAndroid Build Coastguard Worker  """Returns top left and bottom right coordinates from the aruco markers.
1113*b7c941bbSAndroid Build Coastguard Worker
1114*b7c941bbSAndroid Build Coastguard Worker  Note: Refer to image used in scene8 for ArUco markers location.
1115*b7c941bbSAndroid Build Coastguard Worker
1116*b7c941bbSAndroid Build Coastguard Worker  Args:
1117*b7c941bbSAndroid Build Coastguard Worker    aruco_marker_corners: array of aruco marker corner coordinates detected by
1118*b7c941bbSAndroid Build Coastguard Worker      opencv_processing_utils.find_aruco_markers.
1119*b7c941bbSAndroid Build Coastguard Worker    aruco_marker_ids: array of ids of aruco markers detected by
1120*b7c941bbSAndroid Build Coastguard Worker      opencv_processing_utils.find_aruco_markers.
1121*b7c941bbSAndroid Build Coastguard Worker    input_img: 3D RGB numpy [0, 255] uint8; input image.
1122*b7c941bbSAndroid Build Coastguard Worker    output_img_path: string; output image path.
1123*b7c941bbSAndroid Build Coastguard Worker  Returns:
1124*b7c941bbSAndroid Build Coastguard Worker    top_left: tuple; aruco marker corner coordinates in pixel.
1125*b7c941bbSAndroid Build Coastguard Worker    top_right: tuple; aruco marker corner coordinates in pixel.
1126*b7c941bbSAndroid Build Coastguard Worker    bottom_right: tuple; aruco marker corner coordinates in pixel.
1127*b7c941bbSAndroid Build Coastguard Worker    bottom_left: tuple; aruco marker corner coordinates in pixel.
1128*b7c941bbSAndroid Build Coastguard Worker  """
1129*b7c941bbSAndroid Build Coastguard Worker  outer_rect_coordinates = {}
1130*b7c941bbSAndroid Build Coastguard Worker  for corner, marker_id in zip(aruco_marker_corners, aruco_marker_ids):
1131*b7c941bbSAndroid Build Coastguard Worker    corner = corner.reshape(4, 2)  # reshape opencv 3D array to 4x2
1132*b7c941bbSAndroid Build Coastguard Worker    index = marker_id[0]
1133*b7c941bbSAndroid Build Coastguard Worker    corner = numpy.roll(corner, ARUCO_CORNER_COUNT)
1134*b7c941bbSAndroid Build Coastguard Worker    outer_rect_coordinates[index] = tuple(corner[index])
1135*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Corners: %s', corner)
1136*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Index: %s', index)
1137*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Outer rect coordinates: %s', outer_rect_coordinates[index])
1138*b7c941bbSAndroid Build Coastguard Worker  top_left = tuple(map(int, outer_rect_coordinates[0]))
1139*b7c941bbSAndroid Build Coastguard Worker  top_right = tuple(map(int, outer_rect_coordinates[1]))
1140*b7c941bbSAndroid Build Coastguard Worker  bottom_right = tuple(map(int, outer_rect_coordinates[2]))
1141*b7c941bbSAndroid Build Coastguard Worker  bottom_left = tuple(map(int, outer_rect_coordinates[3]))
1142*b7c941bbSAndroid Build Coastguard Worker
1143*b7c941bbSAndroid Build Coastguard Worker  # Outline metering rectangles with corresponding colors
1144*b7c941bbSAndroid Build Coastguard Worker  rect_w = round((bottom_right[0] - top_left[0])/NUM_AE_AWB_REGIONS)
1145*b7c941bbSAndroid Build Coastguard Worker  top_x, top_y = top_left[0], top_left[1]
1146*b7c941bbSAndroid Build Coastguard Worker  bottom_x, bottom_y = bottom_left[0], bottom_left[1]
1147*b7c941bbSAndroid Build Coastguard Worker  cv2.rectangle(
1148*b7c941bbSAndroid Build Coastguard Worker      input_img,
1149*b7c941bbSAndroid Build Coastguard Worker      (top_x, top_y), (bottom_x + rect_w, bottom_y),
1150*b7c941bbSAndroid Build Coastguard Worker      CV2_BLUE, CV2_LINE_THICKNESS)
1151*b7c941bbSAndroid Build Coastguard Worker  cv2.rectangle(
1152*b7c941bbSAndroid Build Coastguard Worker      input_img,
1153*b7c941bbSAndroid Build Coastguard Worker      (top_x + rect_w, top_y), (bottom_x + rect_w * 2, bottom_y),
1154*b7c941bbSAndroid Build Coastguard Worker      CV2_WHITE, CV2_LINE_THICKNESS)
1155*b7c941bbSAndroid Build Coastguard Worker  cv2.rectangle(
1156*b7c941bbSAndroid Build Coastguard Worker      input_img,
1157*b7c941bbSAndroid Build Coastguard Worker      (top_x + rect_w * 2, top_y), (bottom_x + rect_w * 3, bottom_y),
1158*b7c941bbSAndroid Build Coastguard Worker      CV2_BLACK, CV2_LINE_THICKNESS)
1159*b7c941bbSAndroid Build Coastguard Worker  cv2.rectangle(
1160*b7c941bbSAndroid Build Coastguard Worker      input_img,
1161*b7c941bbSAndroid Build Coastguard Worker      (top_x + rect_w * 3, top_y), bottom_right,
1162*b7c941bbSAndroid Build Coastguard Worker      CV2_YELLOW, CV2_LINE_THICKNESS)
1163*b7c941bbSAndroid Build Coastguard Worker  image_processing_utils.write_image(input_img/255, output_img_path)
1164*b7c941bbSAndroid Build Coastguard Worker  logging.debug('ArUco marker top_left: %s', top_left)
1165*b7c941bbSAndroid Build Coastguard Worker  logging.debug('ArUco marker bottom_right: %s', bottom_right)
1166*b7c941bbSAndroid Build Coastguard Worker  return top_left, top_right, bottom_right, bottom_left
1167*b7c941bbSAndroid Build Coastguard Worker
1168*b7c941bbSAndroid Build Coastguard Worker
1169*b7c941bbSAndroid Build Coastguard Workerdef get_aruco_center(corners):
1170*b7c941bbSAndroid Build Coastguard Worker  """Get the center of an ArUco marker defined by its four corners.
1171*b7c941bbSAndroid Build Coastguard Worker
1172*b7c941bbSAndroid Build Coastguard Worker  Args:
1173*b7c941bbSAndroid Build Coastguard Worker    corners: list of 4 Iterables, each Iterable is a (x, y) corner coordinate.
1174*b7c941bbSAndroid Build Coastguard Worker  Returns:
1175*b7c941bbSAndroid Build Coastguard Worker    x, y: the x, y coordinates of the center of the ArUco marker.
1176*b7c941bbSAndroid Build Coastguard Worker  """
1177*b7c941bbSAndroid Build Coastguard Worker  x = (corners[0][0] + corners[2][0]) // 2  # mean of top left x, bottom right x
1178*b7c941bbSAndroid Build Coastguard Worker  y = (corners[1][1] + corners[3][1]) // 2  # mean of top right y, bottom left y
1179*b7c941bbSAndroid Build Coastguard Worker  return x, y
1180*b7c941bbSAndroid Build Coastguard Worker
1181*b7c941bbSAndroid Build Coastguard Worker
1182*b7c941bbSAndroid Build Coastguard Workerdef get_aruco_marker_side_length(corners):
1183*b7c941bbSAndroid Build Coastguard Worker  """Get the side length of an ArUco marker defined by its four corners.
1184*b7c941bbSAndroid Build Coastguard Worker
1185*b7c941bbSAndroid Build Coastguard Worker  This method uses the x-distance from the top left corner to the
1186*b7c941bbSAndroid Build Coastguard Worker  bottom right corner and the y-distance from the top right corner to the
1187*b7c941bbSAndroid Build Coastguard Worker  bottom left corner to calculate the side length of the ArUco marker.
1188*b7c941bbSAndroid Build Coastguard Worker
1189*b7c941bbSAndroid Build Coastguard Worker  Args:
1190*b7c941bbSAndroid Build Coastguard Worker    corners: list of 4 Iterables, each Iterable is a (x, y) corner coordinate.
1191*b7c941bbSAndroid Build Coastguard Worker  Returns:
1192*b7c941bbSAndroid Build Coastguard Worker    The side length of the ArUco marker.
1193*b7c941bbSAndroid Build Coastguard Worker  """
1194*b7c941bbSAndroid Build Coastguard Worker  return math.sqrt(
1195*b7c941bbSAndroid Build Coastguard Worker      (corners[2][0] - corners[0][0]) * (corners[3][1] - corners[1][1])
1196*b7c941bbSAndroid Build Coastguard Worker  )
1197*b7c941bbSAndroid Build Coastguard Worker
1198*b7c941bbSAndroid Build Coastguard Worker
1199*b7c941bbSAndroid Build Coastguard Workerdef _mark_aruco_image(img, data):
1200*b7c941bbSAndroid Build Coastguard Worker  """Return marked image with ArUco marker center and image center.
1201*b7c941bbSAndroid Build Coastguard Worker
1202*b7c941bbSAndroid Build Coastguard Worker  Args:
1203*b7c941bbSAndroid Build Coastguard Worker    img: NumPy image in BGR channel order.
1204*b7c941bbSAndroid Build Coastguard Worker    data: zoom_capture_utils.ZoomTestData corresponding to the image.
1205*b7c941bbSAndroid Build Coastguard Worker  """
1206*b7c941bbSAndroid Build Coastguard Worker  center_x, center_y = get_aruco_center(
1207*b7c941bbSAndroid Build Coastguard Worker      data.aruco_corners)
1208*b7c941bbSAndroid Build Coastguard Worker  # Mark ArUco marker center
1209*b7c941bbSAndroid Build Coastguard Worker  img = cv2.drawMarker(
1210*b7c941bbSAndroid Build Coastguard Worker      img, (int(center_x), int(center_y)),
1211*b7c941bbSAndroid Build Coastguard Worker      color=CV2_GREEN, markerType=cv2.MARKER_TILTED_CROSS,
1212*b7c941bbSAndroid Build Coastguard Worker      markerSize=CV2_ZOOM_MARKER_SIZE, thickness=CV2_ZOOM_MARKER_THICKNESS)
1213*b7c941bbSAndroid Build Coastguard Worker  # Mark ArUco marker edges
1214*b7c941bbSAndroid Build Coastguard Worker  # TODO: b/369852004 - make side length discrepancies more visible
1215*b7c941bbSAndroid Build Coastguard Worker  for line_start, line_end in zip(
1216*b7c941bbSAndroid Build Coastguard Worker      data.aruco_corners,
1217*b7c941bbSAndroid Build Coastguard Worker      numpy.vstack((data.aruco_corners[1:], data.aruco_corners[0]))):
1218*b7c941bbSAndroid Build Coastguard Worker    img = cv2.line(
1219*b7c941bbSAndroid Build Coastguard Worker        img,
1220*b7c941bbSAndroid Build Coastguard Worker        (int(line_start[0]), int(line_start[1])),
1221*b7c941bbSAndroid Build Coastguard Worker        (int(line_end[0]), int(line_end[1])),
1222*b7c941bbSAndroid Build Coastguard Worker        color=CV2_BLUE,
1223*b7c941bbSAndroid Build Coastguard Worker        thickness=CV2_ZOOM_MARKER_THICKNESS)
1224*b7c941bbSAndroid Build Coastguard Worker  # Mark image center
1225*b7c941bbSAndroid Build Coastguard Worker  m_x, m_y = img.shape[1] // 2, img.shape[0] // 2
1226*b7c941bbSAndroid Build Coastguard Worker  img = cv2.drawMarker(img, (m_x, m_y),
1227*b7c941bbSAndroid Build Coastguard Worker                       color=CV2_BLUE, markerType=cv2.MARKER_CROSS,
1228*b7c941bbSAndroid Build Coastguard Worker                       markerSize=CV2_ZOOM_MARKER_SIZE,
1229*b7c941bbSAndroid Build Coastguard Worker                       thickness=CV2_ZOOM_MARKER_THICKNESS)
1230*b7c941bbSAndroid Build Coastguard Worker  return img
1231*b7c941bbSAndroid Build Coastguard Worker
1232*b7c941bbSAndroid Build Coastguard Worker
1233*b7c941bbSAndroid Build Coastguard Workerdef mark_zoom_images(images, test_data, img_name_stem):
1234*b7c941bbSAndroid Build Coastguard Worker  """Mark chosen ArUco marker's center and center of image for all test images.
1235*b7c941bbSAndroid Build Coastguard Worker
1236*b7c941bbSAndroid Build Coastguard Worker  Args:
1237*b7c941bbSAndroid Build Coastguard Worker    images: BGR images in uint8, [0, 255] format.
1238*b7c941bbSAndroid Build Coastguard Worker    test_data: Iterable[zoom_capture_utils.ZoomTestData].
1239*b7c941bbSAndroid Build Coastguard Worker    img_name_stem: str, beginning of path to save data.
1240*b7c941bbSAndroid Build Coastguard Worker  """
1241*b7c941bbSAndroid Build Coastguard Worker  for img, data in zip(images, test_data):
1242*b7c941bbSAndroid Build Coastguard Worker    img = _mark_aruco_image(img, data)
1243*b7c941bbSAndroid Build Coastguard Worker    img_name = (f'{img_name_stem}_{data.result_zoom:.2f}_marked.jpg')
1244*b7c941bbSAndroid Build Coastguard Worker    cv2.imwrite(img_name, img)
1245*b7c941bbSAndroid Build Coastguard Worker
1246*b7c941bbSAndroid Build Coastguard Worker
1247*b7c941bbSAndroid Build Coastguard Workerdef mark_zoom_images_to_video(out, image_paths, test_data):
1248*b7c941bbSAndroid Build Coastguard Worker  """Mark chosen ArUco marker's center and image center, then write to video.
1249*b7c941bbSAndroid Build Coastguard Worker
1250*b7c941bbSAndroid Build Coastguard Worker  Args:
1251*b7c941bbSAndroid Build Coastguard Worker    out: VideoWriter to write frames to.
1252*b7c941bbSAndroid Build Coastguard Worker    image_paths: Iterable[str] of images paths of the frames
1253*b7c941bbSAndroid Build Coastguard Worker    test_data: Iterable[zoom_capture_utils.ZoomTestData].
1254*b7c941bbSAndroid Build Coastguard Worker  """
1255*b7c941bbSAndroid Build Coastguard Worker  for image_path, data in zip(image_paths, test_data):
1256*b7c941bbSAndroid Build Coastguard Worker    img = cv2.imread(image_path)
1257*b7c941bbSAndroid Build Coastguard Worker    img = _mark_aruco_image(img, data)
1258*b7c941bbSAndroid Build Coastguard Worker    out.write(img)
1259*b7c941bbSAndroid Build Coastguard Worker
1260*b7c941bbSAndroid Build Coastguard Worker
1261*b7c941bbSAndroid Build Coastguard Workerdef define_metering_rectangle_values(
1262*b7c941bbSAndroid Build Coastguard Worker    props, top_left, top_right, bottom_right, bottom_left, w, h):
1263*b7c941bbSAndroid Build Coastguard Worker  """Find normalized values of coordinates and return 4 metering rects.
1264*b7c941bbSAndroid Build Coastguard Worker
1265*b7c941bbSAndroid Build Coastguard Worker  Args:
1266*b7c941bbSAndroid Build Coastguard Worker    props: dict; camera properties object.
1267*b7c941bbSAndroid Build Coastguard Worker    top_left: coordinates; defined by aruco markers for targeted image.
1268*b7c941bbSAndroid Build Coastguard Worker    top_right: coordinates; defined by aruco markers for targeted image.
1269*b7c941bbSAndroid Build Coastguard Worker    bottom_right: coordinates; defined by aruco markers for targeted image.
1270*b7c941bbSAndroid Build Coastguard Worker    bottom_left: coordinates; defined by aruco markers for targeted image.
1271*b7c941bbSAndroid Build Coastguard Worker    w: int; active array width in pixels.
1272*b7c941bbSAndroid Build Coastguard Worker    h: int; active array height in pixels.
1273*b7c941bbSAndroid Build Coastguard Worker  Returns:
1274*b7c941bbSAndroid Build Coastguard Worker    meter_rects: 4 metering rectangles made of (x, y, width, height, weight).
1275*b7c941bbSAndroid Build Coastguard Worker      x, y are the top left coordinate of the metering rectangle.
1276*b7c941bbSAndroid Build Coastguard Worker  """
1277*b7c941bbSAndroid Build Coastguard Worker  # If testing front camera, mirror coordinates either left/right or up/down
1278*b7c941bbSAndroid Build Coastguard Worker  # Preview are flipped on device's natural orientation
1279*b7c941bbSAndroid Build Coastguard Worker  # For sensor orientation 90 or 270, it is up or down
1280*b7c941bbSAndroid Build Coastguard Worker  # For sensor orientation 0 or 180, it is left or right
1281*b7c941bbSAndroid Build Coastguard Worker  if (props['android.lens.facing'] ==
1282*b7c941bbSAndroid Build Coastguard Worker      camera_properties_utils.LENS_FACING['FRONT']):
1283*b7c941bbSAndroid Build Coastguard Worker    if props['android.sensor.orientation'] in (90, 270):
1284*b7c941bbSAndroid Build Coastguard Worker      tl_coordinates = (bottom_left[0], h - bottom_left[1])
1285*b7c941bbSAndroid Build Coastguard Worker      br_coordinates = (top_right[0], h - top_right[1])
1286*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Found sensor orientation %d, flipping up down',
1287*b7c941bbSAndroid Build Coastguard Worker                    props['android.sensor.orientation'])
1288*b7c941bbSAndroid Build Coastguard Worker    else:
1289*b7c941bbSAndroid Build Coastguard Worker      tl_coordinates = (w - top_right[0], top_right[1])
1290*b7c941bbSAndroid Build Coastguard Worker      br_coordinates = (w - bottom_left[0], bottom_left[1])
1291*b7c941bbSAndroid Build Coastguard Worker      logging.debug('Found sensor orientation %d, flipping left right',
1292*b7c941bbSAndroid Build Coastguard Worker                    props['android.sensor.orientation'])
1293*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Mirrored top-left coordinates: %s', tl_coordinates)
1294*b7c941bbSAndroid Build Coastguard Worker    logging.debug('Mirrored bottom-right coordinates: %s', br_coordinates)
1295*b7c941bbSAndroid Build Coastguard Worker  else:
1296*b7c941bbSAndroid Build Coastguard Worker    tl_coordinates, br_coordinates = top_left, bottom_right
1297*b7c941bbSAndroid Build Coastguard Worker
1298*b7c941bbSAndroid Build Coastguard Worker  # Normalize coordinates' values to construct metering rectangles
1299*b7c941bbSAndroid Build Coastguard Worker  meter_rects = []
1300*b7c941bbSAndroid Build Coastguard Worker  tl_normalized_x = tl_coordinates[0] / w
1301*b7c941bbSAndroid Build Coastguard Worker  tl_normalized_y = tl_coordinates[1] / h
1302*b7c941bbSAndroid Build Coastguard Worker  br_normalized_x = br_coordinates[0] / w
1303*b7c941bbSAndroid Build Coastguard Worker  br_normalized_y = br_coordinates[1] / h
1304*b7c941bbSAndroid Build Coastguard Worker  rect_w = round((br_normalized_x - tl_normalized_x) / NUM_AE_AWB_REGIONS, 2)
1305*b7c941bbSAndroid Build Coastguard Worker  rect_h = round(br_normalized_y - tl_normalized_y, 2)
1306*b7c941bbSAndroid Build Coastguard Worker  for i in range(NUM_AE_AWB_REGIONS):
1307*b7c941bbSAndroid Build Coastguard Worker    x = round(tl_normalized_x + (rect_w * i), 2)
1308*b7c941bbSAndroid Build Coastguard Worker    y = round(tl_normalized_y, 2)
1309*b7c941bbSAndroid Build Coastguard Worker    meter_rect = [x, y, rect_w, rect_h, AE_AWB_METER_WEIGHT]
1310*b7c941bbSAndroid Build Coastguard Worker    meter_rects.append(meter_rect)
1311*b7c941bbSAndroid Build Coastguard Worker  logging.debug('metering rects: %s', meter_rects)
1312*b7c941bbSAndroid Build Coastguard Worker  return meter_rects
1313*b7c941bbSAndroid Build Coastguard Worker
1314*b7c941bbSAndroid Build Coastguard Worker
1315*b7c941bbSAndroid Build Coastguard Workerdef convert_image_to_high_contrast_black_white(
1316*b7c941bbSAndroid Build Coastguard Worker    img, contrast=CV2_CONTRAST_ALPHA, brightness=CV2_CONTRAST_BETA):
1317*b7c941bbSAndroid Build Coastguard Worker  """Convert capture to high contrast black and white image.
1318*b7c941bbSAndroid Build Coastguard Worker
1319*b7c941bbSAndroid Build Coastguard Worker  Args:
1320*b7c941bbSAndroid Build Coastguard Worker    img: numpy array of image.
1321*b7c941bbSAndroid Build Coastguard Worker    contrast: gain parameter between the value of 0 to 3.
1322*b7c941bbSAndroid Build Coastguard Worker    brightness: bias parameter between the value of 1 to 100.
1323*b7c941bbSAndroid Build Coastguard Worker  Returns:
1324*b7c941bbSAndroid Build Coastguard Worker    high_contrast_img: high contrast black and white image.
1325*b7c941bbSAndroid Build Coastguard Worker  """
1326*b7c941bbSAndroid Build Coastguard Worker  copy_img = numpy.ndarray.copy(img)
1327*b7c941bbSAndroid Build Coastguard Worker  uint8_img = image_processing_utils.convert_image_to_uint8(copy_img)
1328*b7c941bbSAndroid Build Coastguard Worker  gray_img = convert_to_y(uint8_img)
1329*b7c941bbSAndroid Build Coastguard Worker  img_bw = cv2.convertScaleAbs(
1330*b7c941bbSAndroid Build Coastguard Worker      gray_img, alpha=contrast, beta=brightness)
1331*b7c941bbSAndroid Build Coastguard Worker  _, high_contrast_img = cv2.threshold(
1332*b7c941bbSAndroid Build Coastguard Worker      numpy.uint8(img_bw), CV2_THESHOLD_LOWER_BLACK, CH_FULL_SCALE,
1333*b7c941bbSAndroid Build Coastguard Worker      cv2.THRESH_BINARY + cv2.THRESH_OTSU
1334*b7c941bbSAndroid Build Coastguard Worker  )
1335*b7c941bbSAndroid Build Coastguard Worker  high_contrast_img = numpy.expand_dims(
1336*b7c941bbSAndroid Build Coastguard Worker      (CH_FULL_SCALE - high_contrast_img), axis=2)
1337*b7c941bbSAndroid Build Coastguard Worker  return high_contrast_img
1338