1*b7c941bbSAndroid Build Coastguard Worker# Copyright 2023 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"""Utility functions for zoom capture. 15*b7c941bbSAndroid Build Coastguard Worker""" 16*b7c941bbSAndroid Build Coastguard Worker 17*b7c941bbSAndroid Build Coastguard Workerfrom collections.abc import Iterable 18*b7c941bbSAndroid Build Coastguard Workerimport dataclasses 19*b7c941bbSAndroid Build Coastguard Workerimport logging 20*b7c941bbSAndroid Build Coastguard Workerimport math 21*b7c941bbSAndroid Build Coastguard Workerfrom typing import Optional 22*b7c941bbSAndroid Build Coastguard Workerimport cv2 23*b7c941bbSAndroid Build Coastguard Workerfrom matplotlib import animation 24*b7c941bbSAndroid Build Coastguard Workerfrom matplotlib import ticker 25*b7c941bbSAndroid Build Coastguard Workerimport matplotlib.pyplot as plt 26*b7c941bbSAndroid Build Coastguard Workerimport numpy 27*b7c941bbSAndroid Build Coastguard Workerfrom PIL import Image 28*b7c941bbSAndroid Build Coastguard Worker 29*b7c941bbSAndroid Build Coastguard Workerimport camera_properties_utils 30*b7c941bbSAndroid Build Coastguard Workerimport capture_request_utils 31*b7c941bbSAndroid Build Coastguard Workerimport image_processing_utils 32*b7c941bbSAndroid Build Coastguard Workerimport opencv_processing_utils 33*b7c941bbSAndroid Build Coastguard Worker 34*b7c941bbSAndroid Build Coastguard Worker_CIRCLE_COLOR = 0 # [0: black, 255: white] 35*b7c941bbSAndroid Build Coastguard Worker_CIRCLE_AR_RTOL = 0.15 # contour width vs height (aspect ratio) 36*b7c941bbSAndroid Build Coastguard Worker_SMOOTH_ZOOM_OFFSET_MONOTONICITY_ATOL = 25 # number of pixels 37*b7c941bbSAndroid Build Coastguard Worker_CIRCLISH_RTOL = 0.05 # contour area vs ideal circle area pi*((w+h)/4)**2 38*b7c941bbSAndroid Build Coastguard Worker_CONTOUR_AREA_LOGGING_THRESH = 0.8 # logging tol to cut down spam in log file 39*b7c941bbSAndroid Build Coastguard Worker_CV2_LINE_THICKNESS = 3 # line thickness for drawing on images 40*b7c941bbSAndroid Build Coastguard Worker_CV2_RED = (255, 0, 0) # color in cv2 to draw lines 41*b7c941bbSAndroid Build Coastguard Worker_MIN_AREA_RATIO = 0.00013 # Found empirically with partners 42*b7c941bbSAndroid Build Coastguard Worker_MIN_CIRCLE_PTS = 25 43*b7c941bbSAndroid Build Coastguard Worker_MIN_FOCUS_DIST_TOL = 0.80 # allow charts a little closer than min 44*b7c941bbSAndroid Build Coastguard Worker_OFFSET_ATOL = 10 # number of pixels 45*b7c941bbSAndroid Build Coastguard Worker_OFFSET_PLOT_FPS = 2 46*b7c941bbSAndroid Build Coastguard Worker_OFFSET_PLOT_INTERVAL = 400 # delay between frames in milliseconds. 47*b7c941bbSAndroid Build Coastguard Worker_OFFSET_RTOL_MIN_FD = 0.30 48*b7c941bbSAndroid Build Coastguard Worker_RADIUS_RTOL_MIN_FD = 0.15 49*b7c941bbSAndroid Build Coastguard Worker 50*b7c941bbSAndroid Build Coastguard WorkerDEFAULT_FOV_RATIO = 1 # ratio of sub camera's fov over logical camera's fov 51*b7c941bbSAndroid Build Coastguard WorkerJPEG_STR = 'jpg' 52*b7c941bbSAndroid Build Coastguard WorkerOFFSET_RTOL = 0.15 53*b7c941bbSAndroid Build Coastguard WorkerOFFSET_RTOL_SMOOTH_ZOOM = 0.5 # generous RTOL paired with other offset checks 54*b7c941bbSAndroid Build Coastguard WorkerPREFERRED_BASE_ZOOM_RATIO = 1 # Preferred base image for zoom data verification 55*b7c941bbSAndroid Build Coastguard WorkerPREFERRED_BASE_ZOOM_RATIO_RTOL = 0.1 56*b7c941bbSAndroid Build Coastguard WorkerPRV_Z_RTOL = 0.02 # 2% variation of zoom ratio between request and result 57*b7c941bbSAndroid Build Coastguard WorkerRADIUS_RTOL = 0.15 58*b7c941bbSAndroid Build Coastguard WorkerZOOM_MAX_THRESH = 9.0 # TODO: b/368666244 - reduce marker size and use 10.0 59*b7c941bbSAndroid Build Coastguard WorkerZOOM_MIN_THRESH = 2.0 60*b7c941bbSAndroid Build Coastguard WorkerZOOM_RTOL = 0.01 # variation of zoom ratio due to floating point 61*b7c941bbSAndroid Build Coastguard Worker 62*b7c941bbSAndroid Build Coastguard Worker 63*b7c941bbSAndroid Build Coastguard Worker@dataclasses.dataclass 64*b7c941bbSAndroid Build Coastguard Workerclass ZoomTestData: 65*b7c941bbSAndroid Build Coastguard Worker """Class to store zoom-related metadata for a capture.""" 66*b7c941bbSAndroid Build Coastguard Worker result_zoom: float 67*b7c941bbSAndroid Build Coastguard Worker radius_tol: float 68*b7c941bbSAndroid Build Coastguard Worker offset_tol: float 69*b7c941bbSAndroid Build Coastguard Worker focal_length: Optional[float] = None 70*b7c941bbSAndroid Build Coastguard Worker # (x, y) coordinates of ArUco marker corners in clockwise order from top left. 71*b7c941bbSAndroid Build Coastguard Worker aruco_corners: Optional[Iterable[float]] = None 72*b7c941bbSAndroid Build Coastguard Worker aruco_offset: Optional[float] = None 73*b7c941bbSAndroid Build Coastguard Worker physical_id: int = dataclasses.field(default=None) 74*b7c941bbSAndroid Build Coastguard Worker 75*b7c941bbSAndroid Build Coastguard Worker 76*b7c941bbSAndroid Build Coastguard Workerdef get_test_tols_and_cap_size(cam, props, chart_distance, debug): 77*b7c941bbSAndroid Build Coastguard Worker """Determine the tolerance per camera based on test rig and camera params. 78*b7c941bbSAndroid Build Coastguard Worker 79*b7c941bbSAndroid Build Coastguard Worker Cameras are pre-filtered to only include supportable cameras. 80*b7c941bbSAndroid Build Coastguard Worker Supportable cameras are: YUV(RGB) 81*b7c941bbSAndroid Build Coastguard Worker 82*b7c941bbSAndroid Build Coastguard Worker Args: 83*b7c941bbSAndroid Build Coastguard Worker cam: camera object 84*b7c941bbSAndroid Build Coastguard Worker props: dict; physical camera properties dictionary 85*b7c941bbSAndroid Build Coastguard Worker chart_distance: float; distance to chart in cm 86*b7c941bbSAndroid Build Coastguard Worker debug: boolean; log additional data 87*b7c941bbSAndroid Build Coastguard Worker 88*b7c941bbSAndroid Build Coastguard Worker Returns: 89*b7c941bbSAndroid Build Coastguard Worker dict of TOLs with camera focal length as key 90*b7c941bbSAndroid Build Coastguard Worker largest common size across all cameras 91*b7c941bbSAndroid Build Coastguard Worker """ 92*b7c941bbSAndroid Build Coastguard Worker ids = camera_properties_utils.logical_multi_camera_physical_ids(props) 93*b7c941bbSAndroid Build Coastguard Worker physical_props = {} 94*b7c941bbSAndroid Build Coastguard Worker physical_ids = [] 95*b7c941bbSAndroid Build Coastguard Worker for i in ids: 96*b7c941bbSAndroid Build Coastguard Worker physical_props[i] = cam.get_camera_properties_by_id(i) 97*b7c941bbSAndroid Build Coastguard Worker # find YUV capable physical cameras 98*b7c941bbSAndroid Build Coastguard Worker if camera_properties_utils.backward_compatible(physical_props[i]): 99*b7c941bbSAndroid Build Coastguard Worker physical_ids.append(i) 100*b7c941bbSAndroid Build Coastguard Worker 101*b7c941bbSAndroid Build Coastguard Worker # find physical camera focal lengths that work well with rig 102*b7c941bbSAndroid Build Coastguard Worker chart_distance_m = abs(chart_distance)/100 # convert CM to M 103*b7c941bbSAndroid Build Coastguard Worker test_tols = {} 104*b7c941bbSAndroid Build Coastguard Worker test_yuv_sizes = [] 105*b7c941bbSAndroid Build Coastguard Worker for i in physical_ids: 106*b7c941bbSAndroid Build Coastguard Worker yuv_sizes = capture_request_utils.get_available_output_sizes( 107*b7c941bbSAndroid Build Coastguard Worker 'yuv', physical_props[i]) 108*b7c941bbSAndroid Build Coastguard Worker test_yuv_sizes.append(yuv_sizes) 109*b7c941bbSAndroid Build Coastguard Worker if debug: 110*b7c941bbSAndroid Build Coastguard Worker logging.debug('cam[%s] yuv sizes: %s', i, str(yuv_sizes)) 111*b7c941bbSAndroid Build Coastguard Worker 112*b7c941bbSAndroid Build Coastguard Worker # determine if minimum focus distance is less than rig depth 113*b7c941bbSAndroid Build Coastguard Worker min_fd = physical_props[i]['android.lens.info.minimumFocusDistance'] 114*b7c941bbSAndroid Build Coastguard Worker for fl in physical_props[i]['android.lens.info.availableFocalLengths']: 115*b7c941bbSAndroid Build Coastguard Worker logging.debug('cam[%s] min_fd: %.3f (diopters), fl: %.2f', i, min_fd, fl) 116*b7c941bbSAndroid Build Coastguard Worker if (math.isclose(min_fd, 0.0, rel_tol=1E-6) or # fixed focus 117*b7c941bbSAndroid Build Coastguard Worker (1.0/min_fd < chart_distance_m*_MIN_FOCUS_DIST_TOL)): 118*b7c941bbSAndroid Build Coastguard Worker test_tols[fl] = (RADIUS_RTOL, OFFSET_RTOL) 119*b7c941bbSAndroid Build Coastguard Worker else: 120*b7c941bbSAndroid Build Coastguard Worker test_tols[fl] = (_RADIUS_RTOL_MIN_FD, _OFFSET_RTOL_MIN_FD) 121*b7c941bbSAndroid Build Coastguard Worker logging.debug('loosening RTOL for cam[%s]: ' 122*b7c941bbSAndroid Build Coastguard Worker 'min focus distance too large.', i) 123*b7c941bbSAndroid Build Coastguard Worker # find intersection of formats for max common format 124*b7c941bbSAndroid Build Coastguard Worker common_sizes = list(set.intersection(*[set(list) for list in test_yuv_sizes])) 125*b7c941bbSAndroid Build Coastguard Worker if debug: 126*b7c941bbSAndroid Build Coastguard Worker logging.debug('common_fmt: %s', max(common_sizes)) 127*b7c941bbSAndroid Build Coastguard Worker 128*b7c941bbSAndroid Build Coastguard Worker return test_tols, max(common_sizes) 129*b7c941bbSAndroid Build Coastguard Worker 130*b7c941bbSAndroid Build Coastguard Worker 131*b7c941bbSAndroid Build Coastguard Workerdef find_center_circle( 132*b7c941bbSAndroid Build Coastguard Worker img, img_name, size, zoom_ratio, min_zoom_ratio, 133*b7c941bbSAndroid Build Coastguard Worker expected_color=_CIRCLE_COLOR, circle_ar_rtol=_CIRCLE_AR_RTOL, 134*b7c941bbSAndroid Build Coastguard Worker circlish_rtol=_CIRCLISH_RTOL, min_circle_pts=_MIN_CIRCLE_PTS, 135*b7c941bbSAndroid Build Coastguard Worker fov_ratio=DEFAULT_FOV_RATIO, debug=False, draw_color=_CV2_RED, 136*b7c941bbSAndroid Build Coastguard Worker write_img=True): 137*b7c941bbSAndroid Build Coastguard Worker """Find circle closest to image center for scene with multiple circles. 138*b7c941bbSAndroid Build Coastguard Worker 139*b7c941bbSAndroid Build Coastguard Worker Finds all contours in the image. Rejects those too small and not enough 140*b7c941bbSAndroid Build Coastguard Worker points to qualify as a circle. The remaining contours must have center 141*b7c941bbSAndroid Build Coastguard Worker point of color=color and are sorted based on distance from the center 142*b7c941bbSAndroid Build Coastguard Worker of the image. The contour closest to the center of the image is returned. 143*b7c941bbSAndroid Build Coastguard Worker If circle is not found due to zoom ratio being larger than ZOOM_MAX_THRESH 144*b7c941bbSAndroid Build Coastguard Worker or the circle being cropped, None is returned. 145*b7c941bbSAndroid Build Coastguard Worker 146*b7c941bbSAndroid Build Coastguard Worker Note: hierarchy is not used as the hierarchy for black circles changes 147*b7c941bbSAndroid Build Coastguard Worker as the zoom level changes. 148*b7c941bbSAndroid Build Coastguard Worker 149*b7c941bbSAndroid Build Coastguard Worker Args: 150*b7c941bbSAndroid Build Coastguard Worker img: numpy img array with pixel values in [0,255] 151*b7c941bbSAndroid Build Coastguard Worker img_name: str file name for saved image 152*b7c941bbSAndroid Build Coastguard Worker size: [width, height] of the image 153*b7c941bbSAndroid Build Coastguard Worker zoom_ratio: zoom_ratio for the particular capture 154*b7c941bbSAndroid Build Coastguard Worker min_zoom_ratio: min_zoom_ratio supported by the camera device 155*b7c941bbSAndroid Build Coastguard Worker expected_color: int 0 --> black, 255 --> white 156*b7c941bbSAndroid Build Coastguard Worker circle_ar_rtol: float aspect ratio relative tolerance 157*b7c941bbSAndroid Build Coastguard Worker circlish_rtol: float contour area vs ideal circle area pi*((w+h)/4)**2 158*b7c941bbSAndroid Build Coastguard Worker min_circle_pts: int minimum number of points to define a circle 159*b7c941bbSAndroid Build Coastguard Worker fov_ratio: ratio of sub camera over logical camera's field of view 160*b7c941bbSAndroid Build Coastguard Worker debug: bool to save extra data 161*b7c941bbSAndroid Build Coastguard Worker draw_color: cv2 color in RGB to draw circle and circle center on the image 162*b7c941bbSAndroid Build Coastguard Worker write_img: bool: True - save image with circle and center 163*b7c941bbSAndroid Build Coastguard Worker False - don't save image. 164*b7c941bbSAndroid Build Coastguard Worker 165*b7c941bbSAndroid Build Coastguard Worker Returns: 166*b7c941bbSAndroid Build Coastguard Worker circle: [center_x, center_y, radius] 167*b7c941bbSAndroid Build Coastguard Worker """ 168*b7c941bbSAndroid Build Coastguard Worker 169*b7c941bbSAndroid Build Coastguard Worker width, height = size 170*b7c941bbSAndroid Build Coastguard Worker min_area = ( 171*b7c941bbSAndroid Build Coastguard Worker _MIN_AREA_RATIO * width * height * zoom_ratio * zoom_ratio * fov_ratio) 172*b7c941bbSAndroid Build Coastguard Worker 173*b7c941bbSAndroid Build Coastguard Worker # create a copy of image to avoid modification on the original image since 174*b7c941bbSAndroid Build Coastguard Worker # image_processing_utils.convert_image_to_uint8 uses mutable np array methods 175*b7c941bbSAndroid Build Coastguard Worker if debug: 176*b7c941bbSAndroid Build Coastguard Worker img = numpy.ndarray.copy(img) 177*b7c941bbSAndroid Build Coastguard Worker 178*b7c941bbSAndroid Build Coastguard Worker # convert [0, 1] image to [0, 255] and cast as uint8 179*b7c941bbSAndroid Build Coastguard Worker if img.dtype != numpy.uint8: 180*b7c941bbSAndroid Build Coastguard Worker img = image_processing_utils.convert_image_to_uint8(img) 181*b7c941bbSAndroid Build Coastguard Worker 182*b7c941bbSAndroid Build Coastguard Worker # gray scale & otsu threshold to binarize the image 183*b7c941bbSAndroid Build Coastguard Worker gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 184*b7c941bbSAndroid Build Coastguard Worker _, img_bw = cv2.threshold( 185*b7c941bbSAndroid Build Coastguard Worker numpy.uint8(gray), 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) 186*b7c941bbSAndroid Build Coastguard Worker 187*b7c941bbSAndroid Build Coastguard Worker # use OpenCV to find contours (connected components) 188*b7c941bbSAndroid Build Coastguard Worker contours = opencv_processing_utils.find_all_contours(255-img_bw) 189*b7c941bbSAndroid Build Coastguard Worker 190*b7c941bbSAndroid Build Coastguard Worker # write copy of image for debug purposes 191*b7c941bbSAndroid Build Coastguard Worker if debug: 192*b7c941bbSAndroid Build Coastguard Worker img_copy_name = img_name.split('.')[0] + '_copy.jpg' 193*b7c941bbSAndroid Build Coastguard Worker Image.fromarray((img_bw).astype(numpy.uint8)).save(img_copy_name) 194*b7c941bbSAndroid Build Coastguard Worker 195*b7c941bbSAndroid Build Coastguard Worker # check contours and find the best circle candidates 196*b7c941bbSAndroid Build Coastguard Worker circles = [] 197*b7c941bbSAndroid Build Coastguard Worker img_ctr = [gray.shape[1] // 2, gray.shape[0] // 2] 198*b7c941bbSAndroid Build Coastguard Worker logging.debug('img center x,y: %d, %d', img_ctr[0], img_ctr[1]) 199*b7c941bbSAndroid Build Coastguard Worker logging.debug('min area: %d, min circle pts: %d', min_area, min_circle_pts) 200*b7c941bbSAndroid Build Coastguard Worker logging.debug('circlish_rtol: %.3f', circlish_rtol) 201*b7c941bbSAndroid Build Coastguard Worker 202*b7c941bbSAndroid Build Coastguard Worker for contour in contours: 203*b7c941bbSAndroid Build Coastguard Worker area = cv2.contourArea(contour) 204*b7c941bbSAndroid Build Coastguard Worker if area > min_area * _CONTOUR_AREA_LOGGING_THRESH: # skip tiny contours 205*b7c941bbSAndroid Build Coastguard Worker logging.debug('area: %d, min_area: %d, num_pts: %d, min_circle_pts: %d', 206*b7c941bbSAndroid Build Coastguard Worker area, min_area, len(contour), min_circle_pts) 207*b7c941bbSAndroid Build Coastguard Worker if area > min_area and len(contour) >= min_circle_pts: 208*b7c941bbSAndroid Build Coastguard Worker shape = opencv_processing_utils.component_shape(contour) 209*b7c941bbSAndroid Build Coastguard Worker radius = (shape['width'] + shape['height']) / 4 210*b7c941bbSAndroid Build Coastguard Worker circle_color = img_bw[shape['cty']][shape['ctx']] 211*b7c941bbSAndroid Build Coastguard Worker circlish = round((math.pi * radius**2) / area, 4) 212*b7c941bbSAndroid Build Coastguard Worker logging.debug('color: %s, circlish: %.2f, WxH: %dx%d', 213*b7c941bbSAndroid Build Coastguard Worker circle_color, circlish, shape['width'], shape['height']) 214*b7c941bbSAndroid Build Coastguard Worker if (circle_color == expected_color and 215*b7c941bbSAndroid Build Coastguard Worker math.isclose(1, circlish, rel_tol=circlish_rtol) and 216*b7c941bbSAndroid Build Coastguard Worker math.isclose(shape['width'], shape['height'], 217*b7c941bbSAndroid Build Coastguard Worker rel_tol=circle_ar_rtol)): 218*b7c941bbSAndroid Build Coastguard Worker logging.debug('circle found: r: %.2f, area: %.2f\n', radius, area) 219*b7c941bbSAndroid Build Coastguard Worker circles.append([shape['ctx'], shape['cty'], radius, circlish, area]) 220*b7c941bbSAndroid Build Coastguard Worker else: 221*b7c941bbSAndroid Build Coastguard Worker logging.debug('circle rejected: bad color, circlish or aspect ratio\n') 222*b7c941bbSAndroid Build Coastguard Worker 223*b7c941bbSAndroid Build Coastguard Worker if not circles: 224*b7c941bbSAndroid Build Coastguard Worker zoom_ratio_value = zoom_ratio / min_zoom_ratio 225*b7c941bbSAndroid Build Coastguard Worker if zoom_ratio_value >= ZOOM_MAX_THRESH: 226*b7c941bbSAndroid Build Coastguard Worker logging.debug('No circle was detected, but zoom %.2f exceeds' 227*b7c941bbSAndroid Build Coastguard Worker ' maximum zoom threshold', zoom_ratio_value) 228*b7c941bbSAndroid Build Coastguard Worker return None 229*b7c941bbSAndroid Build Coastguard Worker else: 230*b7c941bbSAndroid Build Coastguard Worker raise AssertionError( 231*b7c941bbSAndroid Build Coastguard Worker 'No circle detected for zoom ratio <= ' 232*b7c941bbSAndroid Build Coastguard Worker f'{ZOOM_MAX_THRESH}. ' 233*b7c941bbSAndroid Build Coastguard Worker 'Take pictures according to instructions carefully!') 234*b7c941bbSAndroid Build Coastguard Worker else: 235*b7c941bbSAndroid Build Coastguard Worker logging.debug('num of circles found: %s', len(circles)) 236*b7c941bbSAndroid Build Coastguard Worker 237*b7c941bbSAndroid Build Coastguard Worker if debug: 238*b7c941bbSAndroid Build Coastguard Worker logging.debug('circles [x, y, r, pi*r**2/area, area]: %s', str(circles)) 239*b7c941bbSAndroid Build Coastguard Worker 240*b7c941bbSAndroid Build Coastguard Worker # find circle closest to center 241*b7c941bbSAndroid Build Coastguard Worker circle = min( 242*b7c941bbSAndroid Build Coastguard Worker circles, key=lambda x: math.hypot(x[0] - img_ctr[0], x[1] - img_ctr[1])) 243*b7c941bbSAndroid Build Coastguard Worker 244*b7c941bbSAndroid Build Coastguard Worker # check if circle is cropped because of zoom factor 245*b7c941bbSAndroid Build Coastguard Worker if opencv_processing_utils.is_circle_cropped(circle, size): 246*b7c941bbSAndroid Build Coastguard Worker logging.debug('zoom %.2f is too large! Skip further captures', zoom_ratio) 247*b7c941bbSAndroid Build Coastguard Worker return None 248*b7c941bbSAndroid Build Coastguard Worker 249*b7c941bbSAndroid Build Coastguard Worker # mark image center 250*b7c941bbSAndroid Build Coastguard Worker size = gray.shape 251*b7c941bbSAndroid Build Coastguard Worker m_x, m_y = size[1] // 2, size[0] // 2 252*b7c941bbSAndroid Build Coastguard Worker marker_size = _CV2_LINE_THICKNESS * 10 253*b7c941bbSAndroid Build Coastguard Worker cv2.drawMarker(img, (m_x, m_y), draw_color, markerType=cv2.MARKER_CROSS, 254*b7c941bbSAndroid Build Coastguard Worker markerSize=marker_size, thickness=_CV2_LINE_THICKNESS) 255*b7c941bbSAndroid Build Coastguard Worker 256*b7c941bbSAndroid Build Coastguard Worker # add circle to saved image 257*b7c941bbSAndroid Build Coastguard Worker center_i = (int(round(circle[0], 0)), int(round(circle[1], 0))) 258*b7c941bbSAndroid Build Coastguard Worker radius_i = int(round(circle[2], 0)) 259*b7c941bbSAndroid Build Coastguard Worker cv2.circle(img, center_i, radius_i, draw_color, _CV2_LINE_THICKNESS) 260*b7c941bbSAndroid Build Coastguard Worker if write_img: 261*b7c941bbSAndroid Build Coastguard Worker image_processing_utils.write_image(img / 255.0, img_name) 262*b7c941bbSAndroid Build Coastguard Worker 263*b7c941bbSAndroid Build Coastguard Worker return circle 264*b7c941bbSAndroid Build Coastguard Worker 265*b7c941bbSAndroid Build Coastguard Worker 266*b7c941bbSAndroid Build Coastguard Workerdef preview_zoom_data_to_string(test_data): 267*b7c941bbSAndroid Build Coastguard Worker """Returns formatted string from test_data. 268*b7c941bbSAndroid Build Coastguard Worker 269*b7c941bbSAndroid Build Coastguard Worker Floats are capped at 2 floating points. 270*b7c941bbSAndroid Build Coastguard Worker 271*b7c941bbSAndroid Build Coastguard Worker Args: 272*b7c941bbSAndroid Build Coastguard Worker test_data: ZoomTestData with relevant test data. 273*b7c941bbSAndroid Build Coastguard Worker 274*b7c941bbSAndroid Build Coastguard Worker Returns: 275*b7c941bbSAndroid Build Coastguard Worker Formatted String 276*b7c941bbSAndroid Build Coastguard Worker """ 277*b7c941bbSAndroid Build Coastguard Worker output = [] 278*b7c941bbSAndroid Build Coastguard Worker for key, value in dataclasses.asdict(test_data).items(): 279*b7c941bbSAndroid Build Coastguard Worker if isinstance(value, float): 280*b7c941bbSAndroid Build Coastguard Worker output.append(f'{key}: {value:.2f}') 281*b7c941bbSAndroid Build Coastguard Worker elif isinstance(value, list): 282*b7c941bbSAndroid Build Coastguard Worker output.append( 283*b7c941bbSAndroid Build Coastguard Worker f"{key}: [{', '.join([f'{item:.2f}' for item in value])}]") 284*b7c941bbSAndroid Build Coastguard Worker else: 285*b7c941bbSAndroid Build Coastguard Worker output.append(f'{key}: {value}') 286*b7c941bbSAndroid Build Coastguard Worker 287*b7c941bbSAndroid Build Coastguard Worker return ', '.join(output) 288*b7c941bbSAndroid Build Coastguard Worker 289*b7c941bbSAndroid Build Coastguard Worker 290*b7c941bbSAndroid Build Coastguard Workerdef _get_aruco_marker_x_y_offset(aruco_corners, size): 291*b7c941bbSAndroid Build Coastguard Worker """Get the x and y distances from the ArUco marker to the image center. 292*b7c941bbSAndroid Build Coastguard Worker 293*b7c941bbSAndroid Build Coastguard Worker Args: 294*b7c941bbSAndroid Build Coastguard Worker aruco_corners: list of 4 Iterables, each tuple is a (x, y) coordinate of a 295*b7c941bbSAndroid Build Coastguard Worker corner. 296*b7c941bbSAndroid Build Coastguard Worker size: Iterable; the width and height of the images. 297*b7c941bbSAndroid Build Coastguard Worker Returns: 298*b7c941bbSAndroid Build Coastguard Worker The x and y distances from the ArUco marker to the center of the image. 299*b7c941bbSAndroid Build Coastguard Worker """ 300*b7c941bbSAndroid Build Coastguard Worker aruco_marker_x, aruco_marker_y = opencv_processing_utils.get_aruco_center( 301*b7c941bbSAndroid Build Coastguard Worker aruco_corners) 302*b7c941bbSAndroid Build Coastguard Worker return aruco_marker_x - size[0] // 2, aruco_marker_y - size[1] // 2 303*b7c941bbSAndroid Build Coastguard Worker 304*b7c941bbSAndroid Build Coastguard Worker 305*b7c941bbSAndroid Build Coastguard Workerdef _get_aruco_marker_offset(aruco_corners, size): 306*b7c941bbSAndroid Build Coastguard Worker """Get the distance from the chosen ArUco marker to the center of the image. 307*b7c941bbSAndroid Build Coastguard Worker 308*b7c941bbSAndroid Build Coastguard Worker Args: 309*b7c941bbSAndroid Build Coastguard Worker aruco_corners: list of 4 Iterables, each tuple is a (x, y) coordinate of a 310*b7c941bbSAndroid Build Coastguard Worker corner. 311*b7c941bbSAndroid Build Coastguard Worker size: Iterable; the width and height of the images. 312*b7c941bbSAndroid Build Coastguard Worker Returns: 313*b7c941bbSAndroid Build Coastguard Worker The distance from the ArUco marker to the center of the image. 314*b7c941bbSAndroid Build Coastguard Worker """ 315*b7c941bbSAndroid Build Coastguard Worker return math.hypot(*_get_aruco_marker_x_y_offset(aruco_corners, size)) 316*b7c941bbSAndroid Build Coastguard Worker 317*b7c941bbSAndroid Build Coastguard Worker 318*b7c941bbSAndroid Build Coastguard Workerdef _get_shortest_focal_length(props): 319*b7c941bbSAndroid Build Coastguard Worker """Return the first available focal length from properties.""" 320*b7c941bbSAndroid Build Coastguard Worker return props['android.lens.info.availableFocalLengths'][0] 321*b7c941bbSAndroid Build Coastguard Worker 322*b7c941bbSAndroid Build Coastguard Worker 323*b7c941bbSAndroid Build Coastguard Workerdef _get_average_offset(shared_id, aruco_ids, aruco_corners, size): 324*b7c941bbSAndroid Build Coastguard Worker """Get the average offset a given marker to the image center. 325*b7c941bbSAndroid Build Coastguard Worker 326*b7c941bbSAndroid Build Coastguard Worker Args: 327*b7c941bbSAndroid Build Coastguard Worker shared_id: ID of the given marker to find the average offset. 328*b7c941bbSAndroid Build Coastguard Worker aruco_ids: nested Iterables of ArUco marker IDs. 329*b7c941bbSAndroid Build Coastguard Worker aruco_corners: nested Iterables of ArUco marker corners. 330*b7c941bbSAndroid Build Coastguard Worker size: size of the image to calculate image center. 331*b7c941bbSAndroid Build Coastguard Worker Returns: 332*b7c941bbSAndroid Build Coastguard Worker The average offset from the given marker to the image center. 333*b7c941bbSAndroid Build Coastguard Worker """ 334*b7c941bbSAndroid Build Coastguard Worker offsets = [] 335*b7c941bbSAndroid Build Coastguard Worker for ids, corners in zip(aruco_ids, aruco_corners): 336*b7c941bbSAndroid Build Coastguard Worker offsets.append( 337*b7c941bbSAndroid Build Coastguard Worker _get_average_offset_from_single_capture( 338*b7c941bbSAndroid Build Coastguard Worker shared_id, ids, corners, size)) 339*b7c941bbSAndroid Build Coastguard Worker return numpy.mean(offsets) 340*b7c941bbSAndroid Build Coastguard Worker 341*b7c941bbSAndroid Build Coastguard Worker 342*b7c941bbSAndroid Build Coastguard Workerdef _get_average_offset_from_single_capture( 343*b7c941bbSAndroid Build Coastguard Worker shared_id, ids, corners, size): 344*b7c941bbSAndroid Build Coastguard Worker """Get the average offset a given marker to a known image's center. 345*b7c941bbSAndroid Build Coastguard Worker 346*b7c941bbSAndroid Build Coastguard Worker Args: 347*b7c941bbSAndroid Build Coastguard Worker shared_id: ID of the given marker to find the average offset. 348*b7c941bbSAndroid Build Coastguard Worker ids: Iterable of ArUco marker IDs for single capture test data. 349*b7c941bbSAndroid Build Coastguard Worker corners: Iterable of ArUco marker corners for single capture test data. 350*b7c941bbSAndroid Build Coastguard Worker size: size of the image to calculate image center. 351*b7c941bbSAndroid Build Coastguard Worker Returns: 352*b7c941bbSAndroid Build Coastguard Worker The average offset from the given marker to the image center. 353*b7c941bbSAndroid Build Coastguard Worker """ 354*b7c941bbSAndroid Build Coastguard Worker corresponding_corners = corners[numpy.where(ids == shared_id)[0][0]] 355*b7c941bbSAndroid Build Coastguard Worker return _get_aruco_marker_offset(corresponding_corners, size) 356*b7c941bbSAndroid Build Coastguard Worker 357*b7c941bbSAndroid Build Coastguard Worker 358*b7c941bbSAndroid Build Coastguard Workerdef _are_values_non_decreasing(values, abs_tol=0): 359*b7c941bbSAndroid Build Coastguard Worker """Returns True if any values are not decreasing with absolute tolerance.""" 360*b7c941bbSAndroid Build Coastguard Worker return all(x < y + abs_tol for x, y in zip(values, values[1:])) 361*b7c941bbSAndroid Build Coastguard Worker 362*b7c941bbSAndroid Build Coastguard Worker 363*b7c941bbSAndroid Build Coastguard Workerdef _are_values_non_increasing(values, abs_tol=0): 364*b7c941bbSAndroid Build Coastguard Worker """Returns True if any values are not increasing with absolute tolerance.""" 365*b7c941bbSAndroid Build Coastguard Worker return all(x > y - abs_tol for x, y in zip(values, values[1:])) 366*b7c941bbSAndroid Build Coastguard Worker 367*b7c941bbSAndroid Build Coastguard Worker 368*b7c941bbSAndroid Build Coastguard Workerdef _verify_offset_monotonicity(offsets): 369*b7c941bbSAndroid Build Coastguard Worker """Returns if values continuously increase or decrease with tolerance.""" 370*b7c941bbSAndroid Build Coastguard Worker return ( 371*b7c941bbSAndroid Build Coastguard Worker _are_values_non_decreasing( 372*b7c941bbSAndroid Build Coastguard Worker offsets, _SMOOTH_ZOOM_OFFSET_MONOTONICITY_ATOL) or 373*b7c941bbSAndroid Build Coastguard Worker _are_values_non_increasing( 374*b7c941bbSAndroid Build Coastguard Worker offsets, _SMOOTH_ZOOM_OFFSET_MONOTONICITY_ATOL) 375*b7c941bbSAndroid Build Coastguard Worker ) 376*b7c941bbSAndroid Build Coastguard Worker 377*b7c941bbSAndroid Build Coastguard Worker 378*b7c941bbSAndroid Build Coastguard Workerdef update_zoom_test_data_with_shared_aruco_marker( 379*b7c941bbSAndroid Build Coastguard Worker test_data, aruco_ids, aruco_corners, size): 380*b7c941bbSAndroid Build Coastguard Worker """Update test_data in place with a shared ArUco marker if available. 381*b7c941bbSAndroid Build Coastguard Worker 382*b7c941bbSAndroid Build Coastguard Worker Iterates through the list of aruco_ids and aruco_corners to find the shared 383*b7c941bbSAndroid Build Coastguard Worker ArUco marker that is closest to the center across all captures. If found, 384*b7c941bbSAndroid Build Coastguard Worker updates the test_data with the shared marker and its offset from the 385*b7c941bbSAndroid Build Coastguard Worker image center. 386*b7c941bbSAndroid Build Coastguard Worker 387*b7c941bbSAndroid Build Coastguard Worker Args: 388*b7c941bbSAndroid Build Coastguard Worker test_data: list of ZoomTestData. 389*b7c941bbSAndroid Build Coastguard Worker aruco_ids: nested Iterables of ArUco marker IDs. 390*b7c941bbSAndroid Build Coastguard Worker aruco_corners: nested Iterables of ArUco marker corners. 391*b7c941bbSAndroid Build Coastguard Worker size: Iterable; the width and height of the images. 392*b7c941bbSAndroid Build Coastguard Worker """ 393*b7c941bbSAndroid Build Coastguard Worker shared_ids = set(list(aruco_ids[0])) 394*b7c941bbSAndroid Build Coastguard Worker for ids in aruco_ids[1:]: 395*b7c941bbSAndroid Build Coastguard Worker shared_ids.intersection_update(list(ids)) 396*b7c941bbSAndroid Build Coastguard Worker # Choose closest shared marker to center of transition image if possible. 397*b7c941bbSAndroid Build Coastguard Worker if shared_ids: 398*b7c941bbSAndroid Build Coastguard Worker for i, (ids, corners) in enumerate(zip(aruco_ids, aruco_corners)): 399*b7c941bbSAndroid Build Coastguard Worker if test_data[i].physical_id != test_data[0].physical_id: 400*b7c941bbSAndroid Build Coastguard Worker transition_aruco_ids = ids 401*b7c941bbSAndroid Build Coastguard Worker transition_aruco_corners = corners 402*b7c941bbSAndroid Build Coastguard Worker shared_id = min( 403*b7c941bbSAndroid Build Coastguard Worker shared_ids, 404*b7c941bbSAndroid Build Coastguard Worker key=lambda i: _get_average_offset_from_single_capture( 405*b7c941bbSAndroid Build Coastguard Worker i, transition_aruco_ids, transition_aruco_corners, size) 406*b7c941bbSAndroid Build Coastguard Worker ) 407*b7c941bbSAndroid Build Coastguard Worker break 408*b7c941bbSAndroid Build Coastguard Worker else: 409*b7c941bbSAndroid Build Coastguard Worker shared_id = min( 410*b7c941bbSAndroid Build Coastguard Worker shared_ids, 411*b7c941bbSAndroid Build Coastguard Worker key=lambda i: _get_average_offset(i, aruco_ids, aruco_corners, size) 412*b7c941bbSAndroid Build Coastguard Worker ) 413*b7c941bbSAndroid Build Coastguard Worker else: 414*b7c941bbSAndroid Build Coastguard Worker raise AssertionError('No shared ArUco marker found across all captures.') 415*b7c941bbSAndroid Build Coastguard Worker logging.debug('Using shared aruco ID %d', shared_id) 416*b7c941bbSAndroid Build Coastguard Worker for i, (ids, corners) in enumerate(zip(aruco_ids, aruco_corners)): 417*b7c941bbSAndroid Build Coastguard Worker index = numpy.where(ids == shared_id)[0][0] 418*b7c941bbSAndroid Build Coastguard Worker corresponding_corners = corners[index] 419*b7c941bbSAndroid Build Coastguard Worker logging.debug('Corners of shared ID: %s', corresponding_corners) 420*b7c941bbSAndroid Build Coastguard Worker test_data[i].aruco_corners = corresponding_corners 421*b7c941bbSAndroid Build Coastguard Worker test_data[i].aruco_offset = ( 422*b7c941bbSAndroid Build Coastguard Worker _get_aruco_marker_offset( 423*b7c941bbSAndroid Build Coastguard Worker corresponding_corners, size 424*b7c941bbSAndroid Build Coastguard Worker ) 425*b7c941bbSAndroid Build Coastguard Worker ) 426*b7c941bbSAndroid Build Coastguard Worker 427*b7c941bbSAndroid Build Coastguard Worker 428*b7c941bbSAndroid Build Coastguard Workerdef verify_zoom_results(test_data, size, z_max, z_min, 429*b7c941bbSAndroid Build Coastguard Worker offset_plot_name_stem=None): 430*b7c941bbSAndroid Build Coastguard Worker """Verify that the output images' zoom level reflects the correct zoom ratios. 431*b7c941bbSAndroid Build Coastguard Worker 432*b7c941bbSAndroid Build Coastguard Worker This test verifies that the center and radius of the circles in the output 433*b7c941bbSAndroid Build Coastguard Worker images reflects the zoom ratios being set. The larger the zoom ratio, the 434*b7c941bbSAndroid Build Coastguard Worker larger the circle. And the distance from the center of the circle to the 435*b7c941bbSAndroid Build Coastguard Worker center of the image is proportional to the zoom ratio as well. 436*b7c941bbSAndroid Build Coastguard Worker 437*b7c941bbSAndroid Build Coastguard Worker Args: 438*b7c941bbSAndroid Build Coastguard Worker test_data: Iterable[ZoomTestData] 439*b7c941bbSAndroid Build Coastguard Worker size: array; the width and height of the images 440*b7c941bbSAndroid Build Coastguard Worker z_max: float; the maximum zoom ratio being tested 441*b7c941bbSAndroid Build Coastguard Worker z_min: float; the minimum zoom ratio being tested 442*b7c941bbSAndroid Build Coastguard Worker offset_plot_name_stem: Optional[str]; log path and name of the offset plot 443*b7c941bbSAndroid Build Coastguard Worker 444*b7c941bbSAndroid Build Coastguard Worker Returns: 445*b7c941bbSAndroid Build Coastguard Worker Boolean whether the test passes (True) or not (False) 446*b7c941bbSAndroid Build Coastguard Worker """ 447*b7c941bbSAndroid Build Coastguard Worker # assert some range is tested before circles get too big 448*b7c941bbSAndroid Build Coastguard Worker test_success = True 449*b7c941bbSAndroid Build Coastguard Worker 450*b7c941bbSAndroid Build Coastguard Worker zoom_max_thresh = ZOOM_MAX_THRESH 451*b7c941bbSAndroid Build Coastguard Worker z_max_ratio = z_max / z_min 452*b7c941bbSAndroid Build Coastguard Worker if z_max_ratio < ZOOM_MAX_THRESH: 453*b7c941bbSAndroid Build Coastguard Worker zoom_max_thresh = z_max_ratio 454*b7c941bbSAndroid Build Coastguard Worker 455*b7c941bbSAndroid Build Coastguard Worker # handle capture orders like [1, 0.5, 1.5, 2...] 456*b7c941bbSAndroid Build Coastguard Worker test_data_zoom_values = [v.result_zoom for v in test_data] 457*b7c941bbSAndroid Build Coastguard Worker test_data_max_z = max(test_data_zoom_values) / min(test_data_zoom_values) 458*b7c941bbSAndroid Build Coastguard Worker logging.debug('test zoom ratio max: %.2f vs threshold %.2f', 459*b7c941bbSAndroid Build Coastguard Worker test_data_max_z, zoom_max_thresh) 460*b7c941bbSAndroid Build Coastguard Worker if not math.isclose( 461*b7c941bbSAndroid Build Coastguard Worker test_data_max_z, zoom_max_thresh, rel_tol=ZOOM_RTOL): 462*b7c941bbSAndroid Build Coastguard Worker test_success = False 463*b7c941bbSAndroid Build Coastguard Worker e_msg = (f'Max zoom ratio tested: {test_data_max_z:.4f}, ' 464*b7c941bbSAndroid Build Coastguard Worker f'range advertised min: {z_min}, max: {z_max} ' 465*b7c941bbSAndroid Build Coastguard Worker f'THRESH: {zoom_max_thresh + ZOOM_RTOL}') 466*b7c941bbSAndroid Build Coastguard Worker logging.error(e_msg) 467*b7c941bbSAndroid Build Coastguard Worker return test_success and verify_zoom_data( 468*b7c941bbSAndroid Build Coastguard Worker test_data, size, offset_plot_name_stem=offset_plot_name_stem) 469*b7c941bbSAndroid Build Coastguard Worker 470*b7c941bbSAndroid Build Coastguard Worker 471*b7c941bbSAndroid Build Coastguard Workerdef verify_zoom_data( 472*b7c941bbSAndroid Build Coastguard Worker test_data, size, 473*b7c941bbSAndroid Build Coastguard Worker plot_name_stem=None, offset_plot_name_stem=None, 474*b7c941bbSAndroid Build Coastguard Worker number_of_cameras_to_test=0): 475*b7c941bbSAndroid Build Coastguard Worker """Verify that the output images' zoom level reflects the correct zoom ratios. 476*b7c941bbSAndroid Build Coastguard Worker 477*b7c941bbSAndroid Build Coastguard Worker This test verifies that the center and side length of the ArUco markers in 478*b7c941bbSAndroid Build Coastguard Worker the output images reflects the zoom ratios being set. ArUco marker side length 479*b7c941bbSAndroid Build Coastguard Worker should increase proportionally to the zoom ratio. The distance from the 480*b7c941bbSAndroid Build Coastguard Worker center of the ArUco marker to the center of the image (offset) should either 481*b7c941bbSAndroid Build Coastguard Worker change proportionally to the zoom ratio, or decrease/increase toward the 482*b7c941bbSAndroid Build Coastguard Worker offset of the first capture using the upcoming physical camera, if there is 483*b7c941bbSAndroid Build Coastguard Worker a camera switch. 484*b7c941bbSAndroid Build Coastguard Worker 485*b7c941bbSAndroid Build Coastguard Worker Args: 486*b7c941bbSAndroid Build Coastguard Worker test_data: Iterable[ZoomTestData] 487*b7c941bbSAndroid Build Coastguard Worker size: array; the width and height of the images 488*b7c941bbSAndroid Build Coastguard Worker plot_name_stem: Optional[str]; log path and name of the plot 489*b7c941bbSAndroid Build Coastguard Worker offset_plot_name_stem: Optional[str]; log path and name of the offset plot 490*b7c941bbSAndroid Build Coastguard Worker number_of_cameras_to_test: [Optional][int]; minimum cameras in ZoomTestData 491*b7c941bbSAndroid Build Coastguard Worker 492*b7c941bbSAndroid Build Coastguard Worker Returns: 493*b7c941bbSAndroid Build Coastguard Worker Boolean whether the test passes (True) or not (False) 494*b7c941bbSAndroid Build Coastguard Worker """ 495*b7c941bbSAndroid Build Coastguard Worker range_success = True 496*b7c941bbSAndroid Build Coastguard Worker 497*b7c941bbSAndroid Build Coastguard Worker # assert that multiple cameras were tested where applicable 498*b7c941bbSAndroid Build Coastguard Worker ids_tested = set([v.physical_id for v in test_data]) 499*b7c941bbSAndroid Build Coastguard Worker if len(ids_tested) < number_of_cameras_to_test: 500*b7c941bbSAndroid Build Coastguard Worker range_success = False 501*b7c941bbSAndroid Build Coastguard Worker logging.error('Expected at least %d physical cameras tested, ' 502*b7c941bbSAndroid Build Coastguard Worker 'found IDs: %s', number_of_cameras_to_test, ids_tested) 503*b7c941bbSAndroid Build Coastguard Worker 504*b7c941bbSAndroid Build Coastguard Worker side_success = True 505*b7c941bbSAndroid Build Coastguard Worker offset_success = True 506*b7c941bbSAndroid Build Coastguard Worker used_smooth_offset = False 507*b7c941bbSAndroid Build Coastguard Worker 508*b7c941bbSAndroid Build Coastguard Worker # initialize relative size w/ zoom[0] for diff zoom ratio checks 509*b7c941bbSAndroid Build Coastguard Worker side_0 = opencv_processing_utils.get_aruco_marker_side_length( 510*b7c941bbSAndroid Build Coastguard Worker test_data[0].aruco_corners) 511*b7c941bbSAndroid Build Coastguard Worker z_0 = float(test_data[0].result_zoom) 512*b7c941bbSAndroid Build Coastguard Worker 513*b7c941bbSAndroid Build Coastguard Worker # use 1x ~ 1.1x data as base image if available 514*b7c941bbSAndroid Build Coastguard Worker if z_0 < PREFERRED_BASE_ZOOM_RATIO: 515*b7c941bbSAndroid Build Coastguard Worker for data in test_data: 516*b7c941bbSAndroid Build Coastguard Worker if (data.result_zoom >= PREFERRED_BASE_ZOOM_RATIO and 517*b7c941bbSAndroid Build Coastguard Worker math.isclose(data.result_zoom, PREFERRED_BASE_ZOOM_RATIO, 518*b7c941bbSAndroid Build Coastguard Worker rel_tol=PREFERRED_BASE_ZOOM_RATIO_RTOL)): 519*b7c941bbSAndroid Build Coastguard Worker side_0 = opencv_processing_utils.get_aruco_marker_side_length( 520*b7c941bbSAndroid Build Coastguard Worker data.aruco_corners) 521*b7c941bbSAndroid Build Coastguard Worker z_0 = float(data.result_zoom) 522*b7c941bbSAndroid Build Coastguard Worker break 523*b7c941bbSAndroid Build Coastguard Worker logging.debug('z_0: %.3f, side_0: %.3f', z_0, side_0) 524*b7c941bbSAndroid Build Coastguard Worker if plot_name_stem: 525*b7c941bbSAndroid Build Coastguard Worker frame_numbers = [] 526*b7c941bbSAndroid Build Coastguard Worker z_variations = [] 527*b7c941bbSAndroid Build Coastguard Worker rel_variations = [] 528*b7c941bbSAndroid Build Coastguard Worker radius_tols = [] 529*b7c941bbSAndroid Build Coastguard Worker max_rel_variation = None 530*b7c941bbSAndroid Build Coastguard Worker max_rel_variation_zoom = None 531*b7c941bbSAndroid Build Coastguard Worker offset_x_values = [] 532*b7c941bbSAndroid Build Coastguard Worker offset_y_values = [] 533*b7c941bbSAndroid Build Coastguard Worker hypots = [] 534*b7c941bbSAndroid Build Coastguard Worker 535*b7c941bbSAndroid Build Coastguard Worker id_to_next_offset = {} 536*b7c941bbSAndroid Build Coastguard Worker offsets_while_transitioning = [] 537*b7c941bbSAndroid Build Coastguard Worker previous_id = test_data[0].physical_id 538*b7c941bbSAndroid Build Coastguard Worker # First pass to get transition points 539*b7c941bbSAndroid Build Coastguard Worker for i, data in enumerate(test_data): 540*b7c941bbSAndroid Build Coastguard Worker if i == 0: 541*b7c941bbSAndroid Build Coastguard Worker continue 542*b7c941bbSAndroid Build Coastguard Worker if test_data[i-1].physical_id != data.physical_id: 543*b7c941bbSAndroid Build Coastguard Worker id_to_next_offset[previous_id] = data.aruco_offset 544*b7c941bbSAndroid Build Coastguard Worker previous_id = data.physical_id 545*b7c941bbSAndroid Build Coastguard Worker 546*b7c941bbSAndroid Build Coastguard Worker initial_offset = test_data[0].aruco_offset 547*b7c941bbSAndroid Build Coastguard Worker initial_zoom = test_data[0].result_zoom 548*b7c941bbSAndroid Build Coastguard Worker # Second pass to check offset correctness 549*b7c941bbSAndroid Build Coastguard Worker for i, data in enumerate(test_data): 550*b7c941bbSAndroid Build Coastguard Worker logging.debug(' ') # add blank line between frames 551*b7c941bbSAndroid Build Coastguard Worker logging.debug('Frame# %d {%s}', i, preview_zoom_data_to_string(data)) 552*b7c941bbSAndroid Build Coastguard Worker logging.debug('Zoom: %.2f, physical ID: %s', 553*b7c941bbSAndroid Build Coastguard Worker data.result_zoom, data.physical_id) 554*b7c941bbSAndroid Build Coastguard Worker offset_x, offset_y = _get_aruco_marker_x_y_offset(data.aruco_corners, size) 555*b7c941bbSAndroid Build Coastguard Worker offset_x_values.append(offset_x) 556*b7c941bbSAndroid Build Coastguard Worker offset_y_values.append(offset_y) 557*b7c941bbSAndroid Build Coastguard Worker z_ratio = data.result_zoom / z_0 558*b7c941bbSAndroid Build Coastguard Worker 559*b7c941bbSAndroid Build Coastguard Worker # check relative size against zoom[0] 560*b7c941bbSAndroid Build Coastguard Worker current_side = opencv_processing_utils.get_aruco_marker_side_length( 561*b7c941bbSAndroid Build Coastguard Worker data.aruco_corners) 562*b7c941bbSAndroid Build Coastguard Worker side_ratio = current_side / side_0 563*b7c941bbSAndroid Build Coastguard Worker 564*b7c941bbSAndroid Build Coastguard Worker # Calculate variations 565*b7c941bbSAndroid Build Coastguard Worker z_variation = z_ratio - side_ratio 566*b7c941bbSAndroid Build Coastguard Worker relative_variation = abs(z_variation) / max(abs(z_ratio), abs(side_ratio)) 567*b7c941bbSAndroid Build Coastguard Worker 568*b7c941bbSAndroid Build Coastguard Worker # Store values for plotting 569*b7c941bbSAndroid Build Coastguard Worker if plot_name_stem: 570*b7c941bbSAndroid Build Coastguard Worker frame_numbers.append(i) 571*b7c941bbSAndroid Build Coastguard Worker z_variations.append(z_variation) 572*b7c941bbSAndroid Build Coastguard Worker rel_variations.append(relative_variation) 573*b7c941bbSAndroid Build Coastguard Worker radius_tols.append(data.radius_tol) 574*b7c941bbSAndroid Build Coastguard Worker if max_rel_variation is None or relative_variation > max_rel_variation: 575*b7c941bbSAndroid Build Coastguard Worker max_rel_variation = relative_variation 576*b7c941bbSAndroid Build Coastguard Worker max_rel_variation_zoom = data.result_zoom 577*b7c941bbSAndroid Build Coastguard Worker 578*b7c941bbSAndroid Build Coastguard Worker logging.debug('r ratio req: %.3f, measured: %.3f', 579*b7c941bbSAndroid Build Coastguard Worker z_ratio, side_ratio) 580*b7c941bbSAndroid Build Coastguard Worker msg = ( 581*b7c941bbSAndroid Build Coastguard Worker f'{i} Marker side ratio: result({data.result_zoom:.3f}/{z_0:.3f}):' 582*b7c941bbSAndroid Build Coastguard Worker f' {z_ratio:.3f}, marker({current_side:.3f}/{side_0:.3f}):' 583*b7c941bbSAndroid Build Coastguard Worker f' {side_ratio:.3f}, RTOL: {data.radius_tol}' 584*b7c941bbSAndroid Build Coastguard Worker ) 585*b7c941bbSAndroid Build Coastguard Worker if not math.isclose(z_ratio, side_ratio, rel_tol=data.radius_tol): 586*b7c941bbSAndroid Build Coastguard Worker side_success = False 587*b7c941bbSAndroid Build Coastguard Worker logging.error(msg) 588*b7c941bbSAndroid Build Coastguard Worker else: 589*b7c941bbSAndroid Build Coastguard Worker logging.debug(msg) 590*b7c941bbSAndroid Build Coastguard Worker 591*b7c941bbSAndroid Build Coastguard Worker # check relative offset against init vals w/ no focal length change 592*b7c941bbSAndroid Build Coastguard Worker # set init values for first capture or change in physical cam focal length 593*b7c941bbSAndroid Build Coastguard Worker hypots.append(data.aruco_offset) 594*b7c941bbSAndroid Build Coastguard Worker if i == 0: 595*b7c941bbSAndroid Build Coastguard Worker continue 596*b7c941bbSAndroid Build Coastguard Worker if test_data[i-1].physical_id != data.physical_id: 597*b7c941bbSAndroid Build Coastguard Worker initial_zoom = float(data.result_zoom) 598*b7c941bbSAndroid Build Coastguard Worker initial_offset = data.aruco_offset 599*b7c941bbSAndroid Build Coastguard Worker logging.debug('offset_hypot_init: %.3f', initial_offset) 600*b7c941bbSAndroid Build Coastguard Worker d_msg = (f'-- init {i} zoom: {data.result_zoom:.2f}, ' 601*b7c941bbSAndroid Build Coastguard Worker f'offset init: {initial_offset:.1f}, ' 602*b7c941bbSAndroid Build Coastguard Worker f'zoom: {z_ratio:.1f} ') 603*b7c941bbSAndroid Build Coastguard Worker logging.debug(d_msg) 604*b7c941bbSAndroid Build Coastguard Worker if offsets_while_transitioning: 605*b7c941bbSAndroid Build Coastguard Worker logging.debug('Offsets while transitioning: %s', 606*b7c941bbSAndroid Build Coastguard Worker offsets_while_transitioning) 607*b7c941bbSAndroid Build Coastguard Worker if used_smooth_offset and not _verify_offset_monotonicity( 608*b7c941bbSAndroid Build Coastguard Worker offsets_while_transitioning): 609*b7c941bbSAndroid Build Coastguard Worker logging.error('Offsets %s are not monotonic', 610*b7c941bbSAndroid Build Coastguard Worker offsets_while_transitioning) 611*b7c941bbSAndroid Build Coastguard Worker offset_success = False 612*b7c941bbSAndroid Build Coastguard Worker offsets_while_transitioning.clear() 613*b7c941bbSAndroid Build Coastguard Worker else: 614*b7c941bbSAndroid Build Coastguard Worker offsets_while_transitioning.append(data.aruco_offset) 615*b7c941bbSAndroid Build Coastguard Worker z_ratio = data.result_zoom / initial_zoom 616*b7c941bbSAndroid Build Coastguard Worker offset_hypot_rel = data.aruco_offset / z_ratio 617*b7c941bbSAndroid Build Coastguard Worker logging.debug('offset_hypot_rel: %.3f', offset_hypot_rel) 618*b7c941bbSAndroid Build Coastguard Worker rel_tol = data.offset_tol 619*b7c941bbSAndroid Build Coastguard Worker if not math.isclose(initial_offset, offset_hypot_rel, 620*b7c941bbSAndroid Build Coastguard Worker rel_tol=rel_tol, abs_tol=_OFFSET_ATOL): 621*b7c941bbSAndroid Build Coastguard Worker w_msg = ('Original offset check failed. ' 622*b7c941bbSAndroid Build Coastguard Worker f'{i} zoom: {data.result_zoom:.2f}, ' 623*b7c941bbSAndroid Build Coastguard Worker f'offset init: {initial_offset:.4f}, ' 624*b7c941bbSAndroid Build Coastguard Worker f'offset rel: {offset_hypot_rel:.4f}, ' 625*b7c941bbSAndroid Build Coastguard Worker f'Zoom: {z_ratio:.1f}, ' 626*b7c941bbSAndroid Build Coastguard Worker f'RTOL: {rel_tol}, ATOL: {_OFFSET_ATOL}') 627*b7c941bbSAndroid Build Coastguard Worker logging.warning(w_msg) 628*b7c941bbSAndroid Build Coastguard Worker used_smooth_offset = True 629*b7c941bbSAndroid Build Coastguard Worker if data.physical_id not in id_to_next_offset: 630*b7c941bbSAndroid Build Coastguard Worker offset_success = False 631*b7c941bbSAndroid Build Coastguard Worker logging.error('No physical camera is available to explain ' 632*b7c941bbSAndroid Build Coastguard Worker 'offset changes!') 633*b7c941bbSAndroid Build Coastguard Worker else: 634*b7c941bbSAndroid Build Coastguard Worker next_initial_offset = id_to_next_offset[data.physical_id] 635*b7c941bbSAndroid Build Coastguard Worker if not math.isclose(next_initial_offset, data.aruco_offset, 636*b7c941bbSAndroid Build Coastguard Worker rel_tol=OFFSET_RTOL_SMOOTH_ZOOM, 637*b7c941bbSAndroid Build Coastguard Worker abs_tol=_OFFSET_ATOL): 638*b7c941bbSAndroid Build Coastguard Worker offset_success = False 639*b7c941bbSAndroid Build Coastguard Worker e_msg = ('Current offset did not match upcoming physical camera! ' 640*b7c941bbSAndroid Build Coastguard Worker f'{i} zoom: {data.result_zoom:.2f}, ' 641*b7c941bbSAndroid Build Coastguard Worker f'next initial offset: {next_initial_offset:.1f}, ' 642*b7c941bbSAndroid Build Coastguard Worker f'current offset: {data.aruco_offset:.1f}, ' 643*b7c941bbSAndroid Build Coastguard Worker f'RTOL: {OFFSET_RTOL_SMOOTH_ZOOM}, ATOL: {_OFFSET_ATOL}') 644*b7c941bbSAndroid Build Coastguard Worker logging.error(e_msg) 645*b7c941bbSAndroid Build Coastguard Worker else: 646*b7c941bbSAndroid Build Coastguard Worker logging.debug('Successfully matched current offset with upcoming ' 647*b7c941bbSAndroid Build Coastguard Worker 'physical camera offset') 648*b7c941bbSAndroid Build Coastguard Worker if offset_success: 649*b7c941bbSAndroid Build Coastguard Worker d_msg = (f'{i} zoom: {data.result_zoom:.2f}, ' 650*b7c941bbSAndroid Build Coastguard Worker f'offset init: {initial_offset:.1f}, ' 651*b7c941bbSAndroid Build Coastguard Worker f'offset rel: {offset_hypot_rel:.1f}, ' 652*b7c941bbSAndroid Build Coastguard Worker f'offset dist: {data.aruco_offset:.1f}, ' 653*b7c941bbSAndroid Build Coastguard Worker f'Zoom: {z_ratio:.1f}, ' 654*b7c941bbSAndroid Build Coastguard Worker f'RTOL: {rel_tol}, ATOL: {_OFFSET_ATOL}') 655*b7c941bbSAndroid Build Coastguard Worker logging.debug(d_msg) 656*b7c941bbSAndroid Build Coastguard Worker 657*b7c941bbSAndroid Build Coastguard Worker if plot_name_stem: 658*b7c941bbSAndroid Build Coastguard Worker plot_name = plot_name_stem.split('/')[-1].split('.')[0] 659*b7c941bbSAndroid Build Coastguard Worker # Don't change print to logging. Used for KPI. 660*b7c941bbSAndroid Build Coastguard Worker print(f'{plot_name}_max_rel_variation: ', max_rel_variation) 661*b7c941bbSAndroid Build Coastguard Worker print(f'{plot_name}_max_rel_variation_zoom: ', max_rel_variation_zoom) 662*b7c941bbSAndroid Build Coastguard Worker 663*b7c941bbSAndroid Build Coastguard Worker # Calculate RMS values 664*b7c941bbSAndroid Build Coastguard Worker rms_z_variations = numpy.sqrt(numpy.mean(numpy.square(z_variations))) 665*b7c941bbSAndroid Build Coastguard Worker rms_rel_variations = numpy.sqrt(numpy.mean(numpy.square(rel_variations))) 666*b7c941bbSAndroid Build Coastguard Worker 667*b7c941bbSAndroid Build Coastguard Worker # Print RMS values 668*b7c941bbSAndroid Build Coastguard Worker print(f'{plot_name}_rms_z_variations: ', rms_z_variations) 669*b7c941bbSAndroid Build Coastguard Worker print(f'{plot_name}_rms_rel_variations: ', rms_rel_variations) 670*b7c941bbSAndroid Build Coastguard Worker 671*b7c941bbSAndroid Build Coastguard Worker plot_variation(frame_numbers, z_variations, None, 672*b7c941bbSAndroid Build Coastguard Worker f'{plot_name_stem}_variations.png', 'Zoom Variation') 673*b7c941bbSAndroid Build Coastguard Worker plot_variation(frame_numbers, rel_variations, radius_tols, 674*b7c941bbSAndroid Build Coastguard Worker f'{plot_name_stem}_relative.png', 'Relative Variation') 675*b7c941bbSAndroid Build Coastguard Worker 676*b7c941bbSAndroid Build Coastguard Worker if offset_plot_name_stem: 677*b7c941bbSAndroid Build Coastguard Worker plot_offset_trajectory( 678*b7c941bbSAndroid Build Coastguard Worker [d.result_zoom for d in test_data], 679*b7c941bbSAndroid Build Coastguard Worker offset_x_values, 680*b7c941bbSAndroid Build Coastguard Worker offset_y_values, 681*b7c941bbSAndroid Build Coastguard Worker hypots, 682*b7c941bbSAndroid Build Coastguard Worker f'{offset_plot_name_stem}_offset_trajectory.gif' # GIF animation 683*b7c941bbSAndroid Build Coastguard Worker ) 684*b7c941bbSAndroid Build Coastguard Worker 685*b7c941bbSAndroid Build Coastguard Worker return range_success and side_success and offset_success 686*b7c941bbSAndroid Build Coastguard Worker 687*b7c941bbSAndroid Build Coastguard Worker 688*b7c941bbSAndroid Build Coastguard Workerdef verify_preview_zoom_results(test_data, size, z_max, z_min, z_step_size, 689*b7c941bbSAndroid Build Coastguard Worker plot_name_stem): 690*b7c941bbSAndroid Build Coastguard Worker """Verify that the output images' zoom level reflects the correct zoom ratios. 691*b7c941bbSAndroid Build Coastguard Worker 692*b7c941bbSAndroid Build Coastguard Worker This test verifies that the center and radius of the circles in the output 693*b7c941bbSAndroid Build Coastguard Worker images reflects the zoom ratios being set. The larger the zoom ratio, the 694*b7c941bbSAndroid Build Coastguard Worker larger the circle. And the distance from the center of the circle to the 695*b7c941bbSAndroid Build Coastguard Worker center of the image is proportional to the zoom ratio as well. Verifies 696*b7c941bbSAndroid Build Coastguard Worker that circles are detected throughout the zoom range. 697*b7c941bbSAndroid Build Coastguard Worker 698*b7c941bbSAndroid Build Coastguard Worker Args: 699*b7c941bbSAndroid Build Coastguard Worker test_data: Iterable[ZoomTestData] 700*b7c941bbSAndroid Build Coastguard Worker size: array; the width and height of the images 701*b7c941bbSAndroid Build Coastguard Worker z_max: float; the maximum zoom ratio being tested 702*b7c941bbSAndroid Build Coastguard Worker z_min: float; the minimum zoom ratio being tested 703*b7c941bbSAndroid Build Coastguard Worker z_step_size: float; zoom step size to zoom from z_min to z_max 704*b7c941bbSAndroid Build Coastguard Worker plot_name_stem: str; log path and name of the plot 705*b7c941bbSAndroid Build Coastguard Worker 706*b7c941bbSAndroid Build Coastguard Worker Returns: 707*b7c941bbSAndroid Build Coastguard Worker Boolean whether the test passes (True) or not (False) 708*b7c941bbSAndroid Build Coastguard Worker """ 709*b7c941bbSAndroid Build Coastguard Worker test_success = True 710*b7c941bbSAndroid Build Coastguard Worker 711*b7c941bbSAndroid Build Coastguard Worker test_data_zoom_values = [v.result_zoom for v in test_data] 712*b7c941bbSAndroid Build Coastguard Worker results_z_max = max(test_data_zoom_values) 713*b7c941bbSAndroid Build Coastguard Worker results_z_min = min(test_data_zoom_values) 714*b7c941bbSAndroid Build Coastguard Worker logging.debug('capture result: min zoom: %.2f vs max zoom: %.2f', 715*b7c941bbSAndroid Build Coastguard Worker results_z_min, results_z_max) 716*b7c941bbSAndroid Build Coastguard Worker 717*b7c941bbSAndroid Build Coastguard Worker # check if max zoom in capture result close to requested zoom range 718*b7c941bbSAndroid Build Coastguard Worker if (math.isclose(results_z_max, z_max, rel_tol=PRV_Z_RTOL) or 719*b7c941bbSAndroid Build Coastguard Worker math.isclose(results_z_max, z_max - z_step_size, rel_tol=PRV_Z_RTOL)): 720*b7c941bbSAndroid Build Coastguard Worker d_msg = (f'results_z_max = {results_z_max:.2f} is close to requested ' 721*b7c941bbSAndroid Build Coastguard Worker f'z_max = {z_max:.2f} or z_max-step = {z_max-z_step_size:.2f} ' 722*b7c941bbSAndroid Build Coastguard Worker f'by {PRV_Z_RTOL:.2f} Tol') 723*b7c941bbSAndroid Build Coastguard Worker logging.debug(d_msg) 724*b7c941bbSAndroid Build Coastguard Worker else: 725*b7c941bbSAndroid Build Coastguard Worker test_success = False 726*b7c941bbSAndroid Build Coastguard Worker e_msg = (f'Max zoom ratio {results_z_max:.4f} in capture results ' 727*b7c941bbSAndroid Build Coastguard Worker f'not close to {z_max:.2f} and ' 728*b7c941bbSAndroid Build Coastguard Worker f'z_max-step = {z_max-z_step_size:.2f} by {PRV_Z_RTOL:.2f} ' 729*b7c941bbSAndroid Build Coastguard Worker f'tolerance.') 730*b7c941bbSAndroid Build Coastguard Worker logging.error(e_msg) 731*b7c941bbSAndroid Build Coastguard Worker 732*b7c941bbSAndroid Build Coastguard Worker if math.isclose(results_z_min, z_min, rel_tol=PRV_Z_RTOL): 733*b7c941bbSAndroid Build Coastguard Worker d_msg = (f'results_z_min = {results_z_min:.2f} is close to requested ' 734*b7c941bbSAndroid Build Coastguard Worker f'z_min = {z_min:.2f} by {PRV_Z_RTOL:.2f} Tol') 735*b7c941bbSAndroid Build Coastguard Worker logging.debug(d_msg) 736*b7c941bbSAndroid Build Coastguard Worker else: 737*b7c941bbSAndroid Build Coastguard Worker test_success = False 738*b7c941bbSAndroid Build Coastguard Worker e_msg = (f'Min zoom ratio {results_z_min:.4f} in capture results ' 739*b7c941bbSAndroid Build Coastguard Worker f'not close to {z_min:.2f} by {PRV_Z_RTOL:.2f} tolerance.') 740*b7c941bbSAndroid Build Coastguard Worker logging.error(e_msg) 741*b7c941bbSAndroid Build Coastguard Worker 742*b7c941bbSAndroid Build Coastguard Worker return test_success and verify_zoom_data( 743*b7c941bbSAndroid Build Coastguard Worker test_data, size, plot_name_stem=plot_name_stem) 744*b7c941bbSAndroid Build Coastguard Worker 745*b7c941bbSAndroid Build Coastguard Worker 746*b7c941bbSAndroid Build Coastguard Workerdef get_preview_zoom_params(zoom_range, steps): 747*b7c941bbSAndroid Build Coastguard Worker """Returns zoom min, max, step_size based on zoom range and steps. 748*b7c941bbSAndroid Build Coastguard Worker 749*b7c941bbSAndroid Build Coastguard Worker Determine zoom min, max, step_size based on zoom range, steps. 750*b7c941bbSAndroid Build Coastguard Worker Zoom max is capped due to current ITS box size limitation. 751*b7c941bbSAndroid Build Coastguard Worker 752*b7c941bbSAndroid Build Coastguard Worker Args: 753*b7c941bbSAndroid Build Coastguard Worker zoom_range: [float,float]; Camera's zoom range 754*b7c941bbSAndroid Build Coastguard Worker steps: int; number of steps 755*b7c941bbSAndroid Build Coastguard Worker 756*b7c941bbSAndroid Build Coastguard Worker Returns: 757*b7c941bbSAndroid Build Coastguard Worker zoom_min: minimum zoom 758*b7c941bbSAndroid Build Coastguard Worker zoom_max: maximum zoom 759*b7c941bbSAndroid Build Coastguard Worker zoom_step_size: size of zoom steps 760*b7c941bbSAndroid Build Coastguard Worker """ 761*b7c941bbSAndroid Build Coastguard Worker # Determine test zoom range 762*b7c941bbSAndroid Build Coastguard Worker logging.debug('z_range = %s', str(zoom_range)) 763*b7c941bbSAndroid Build Coastguard Worker zoom_min, zoom_max = float(zoom_range[0]), float(zoom_range[1]) 764*b7c941bbSAndroid Build Coastguard Worker zoom_max = min(zoom_max, ZOOM_MAX_THRESH * zoom_min) 765*b7c941bbSAndroid Build Coastguard Worker 766*b7c941bbSAndroid Build Coastguard Worker zoom_step_size = (zoom_max-zoom_min) / (steps-1) 767*b7c941bbSAndroid Build Coastguard Worker logging.debug('zoomRatioRange = %s z_min = %f z_max = %f z_stepSize = %f', 768*b7c941bbSAndroid Build Coastguard Worker str(zoom_range), zoom_min, zoom_max, zoom_step_size) 769*b7c941bbSAndroid Build Coastguard Worker 770*b7c941bbSAndroid Build Coastguard Worker return zoom_min, zoom_max, zoom_step_size 771*b7c941bbSAndroid Build Coastguard Worker 772*b7c941bbSAndroid Build Coastguard Worker 773*b7c941bbSAndroid Build Coastguard Workerdef plot_variation(frame_numbers, variations, tolerances, plot_name, ylabel): 774*b7c941bbSAndroid Build Coastguard Worker """Plots a variation against frame numbers with corresponding tolerances. 775*b7c941bbSAndroid Build Coastguard Worker 776*b7c941bbSAndroid Build Coastguard Worker Args: 777*b7c941bbSAndroid Build Coastguard Worker frame_numbers: List of frame numbers. 778*b7c941bbSAndroid Build Coastguard Worker variations: List of variations. 779*b7c941bbSAndroid Build Coastguard Worker tolerances: List of tolerances corresponding to each variation. 780*b7c941bbSAndroid Build Coastguard Worker plot_name: Name for the plot file. 781*b7c941bbSAndroid Build Coastguard Worker ylabel: Label for the y-axis. 782*b7c941bbSAndroid Build Coastguard Worker """ 783*b7c941bbSAndroid Build Coastguard Worker 784*b7c941bbSAndroid Build Coastguard Worker plt.figure(figsize=(40, 10)) 785*b7c941bbSAndroid Build Coastguard Worker 786*b7c941bbSAndroid Build Coastguard Worker plt.scatter(frame_numbers, variations, marker='o', linestyle='-', 787*b7c941bbSAndroid Build Coastguard Worker color='blue', label=ylabel) 788*b7c941bbSAndroid Build Coastguard Worker 789*b7c941bbSAndroid Build Coastguard Worker if tolerances: 790*b7c941bbSAndroid Build Coastguard Worker plt.plot(frame_numbers, tolerances, linestyle='--', color='red', 791*b7c941bbSAndroid Build Coastguard Worker label='Tolerance') 792*b7c941bbSAndroid Build Coastguard Worker 793*b7c941bbSAndroid Build Coastguard Worker plt.xlabel('Frame Number', fontsize=12) 794*b7c941bbSAndroid Build Coastguard Worker plt.ylabel(ylabel, fontsize=12) 795*b7c941bbSAndroid Build Coastguard Worker plt.title(f'{ylabel} vs. Frame Number', fontsize=14) 796*b7c941bbSAndroid Build Coastguard Worker 797*b7c941bbSAndroid Build Coastguard Worker plt.legend() 798*b7c941bbSAndroid Build Coastguard Worker 799*b7c941bbSAndroid Build Coastguard Worker plt.grid(axis='y', linestyle='--') 800*b7c941bbSAndroid Build Coastguard Worker plt.savefig(plot_name) 801*b7c941bbSAndroid Build Coastguard Worker plt.close() 802*b7c941bbSAndroid Build Coastguard Worker 803*b7c941bbSAndroid Build Coastguard Worker 804*b7c941bbSAndroid Build Coastguard Workerdef plot_offset_trajectory( 805*b7c941bbSAndroid Build Coastguard Worker zooms, x_offsets, y_offsets, hypots, plot_name): 806*b7c941bbSAndroid Build Coastguard Worker """Plot an animation describing offset drift for each zoom ratio. 807*b7c941bbSAndroid Build Coastguard Worker 808*b7c941bbSAndroid Build Coastguard Worker Args: 809*b7c941bbSAndroid Build Coastguard Worker zooms: Iterable[float]; zoom ratios corresponding to each offset. 810*b7c941bbSAndroid Build Coastguard Worker x_offsets: Iterable[float]; x-axis offsets. 811*b7c941bbSAndroid Build Coastguard Worker y_offsets: Iterable[float]; y-axis offsets. 812*b7c941bbSAndroid Build Coastguard Worker hypots: Iterable[float]; offset hypotenuses (distances from image center). 813*b7c941bbSAndroid Build Coastguard Worker plot_name: Plot name with path to save the plot. 814*b7c941bbSAndroid Build Coastguard Worker """ 815*b7c941bbSAndroid Build Coastguard Worker fig, (ax1, ax2) = plt.subplots(1, 2, constrained_layout=True) 816*b7c941bbSAndroid Build Coastguard Worker fig.suptitle('Zoom Offset Trajectory') 817*b7c941bbSAndroid Build Coastguard Worker scatter = ax1.scatter([], [], c='blue', marker='o') 818*b7c941bbSAndroid Build Coastguard Worker line, = ax1.plot([], [], c='blue', linestyle='dashed') 819*b7c941bbSAndroid Build Coastguard Worker 820*b7c941bbSAndroid Build Coastguard Worker # Preset axes limits, since data is added frame by frame (no initial data). 821*b7c941bbSAndroid Build Coastguard Worker ax1.set_xlim(min(x_offsets), max(x_offsets), auto=True) 822*b7c941bbSAndroid Build Coastguard Worker ax1.set_ylim(min(y_offsets), max(y_offsets), auto=True) 823*b7c941bbSAndroid Build Coastguard Worker 824*b7c941bbSAndroid Build Coastguard Worker ax1.set_title('Offset (x, y) by Zoom Ratio') 825*b7c941bbSAndroid Build Coastguard Worker ax1.set_xlabel('x') 826*b7c941bbSAndroid Build Coastguard Worker ax1.set_ylabel('y') 827*b7c941bbSAndroid Build Coastguard Worker 828*b7c941bbSAndroid Build Coastguard Worker # Function to animate each frame. Each frame corresponds to a capture/zoom. 829*b7c941bbSAndroid Build Coastguard Worker def animate(i): 830*b7c941bbSAndroid Build Coastguard Worker scatter.set_offsets((x_offsets[i], y_offsets[i])) 831*b7c941bbSAndroid Build Coastguard Worker line.set_data(x_offsets[:i+1], y_offsets[:i+1]) 832*b7c941bbSAndroid Build Coastguard Worker ax1.set_title(f'Zoom: {zooms[i]:.3f}') 833*b7c941bbSAndroid Build Coastguard Worker return scatter, line 834*b7c941bbSAndroid Build Coastguard Worker 835*b7c941bbSAndroid Build Coastguard Worker ani = animation.FuncAnimation( 836*b7c941bbSAndroid Build Coastguard Worker fig, animate, repeat=True, frames=len(hypots), 837*b7c941bbSAndroid Build Coastguard Worker interval=_OFFSET_PLOT_INTERVAL 838*b7c941bbSAndroid Build Coastguard Worker ) 839*b7c941bbSAndroid Build Coastguard Worker 840*b7c941bbSAndroid Build Coastguard Worker ax2.xaxis.set_major_locator(ticker.MultipleLocator(1)) # ticker every 1.0x. 841*b7c941bbSAndroid Build Coastguard Worker ax2.plot(zooms, hypots, '-bo') 842*b7c941bbSAndroid Build Coastguard Worker ax2.set_title('Offset Distance vs. Zoom Ratio') 843*b7c941bbSAndroid Build Coastguard Worker ax2.set_xlabel('Zoom Ratio') 844*b7c941bbSAndroid Build Coastguard Worker ax2.set_ylabel('Offset (pixels)') 845*b7c941bbSAndroid Build Coastguard Worker 846*b7c941bbSAndroid Build Coastguard Worker writer = animation.PillowWriter(fps=_OFFSET_PLOT_FPS) 847*b7c941bbSAndroid Build Coastguard Worker ani.save(plot_name, writer=writer) 848