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