1 # Copyright 2015 The Android Open Source Project 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 import math 16 import os.path 17 import cv2 18 import its.caps 19 import its.device 20 import its.image 21 import its.objects 22 import numpy as np 23 24 FMT_ATOL = 0.01 # Absolute tolerance on format ratio 25 AR_CHECKED = ["4:3", "16:9"] # Aspect ratios checked 26 FOV_PERCENT_RTOL = 0.15 # Relative tolerance on circle FoV % to expected 27 LARGE_SIZE = 2000 # Define the size of a large image 28 NAME = os.path.basename(__file__).split(".")[0] 29 NUM_DISTORT_PARAMS = 5 30 THRESH_L_AR = 0.02 # aspect ratio test threshold of large images 31 THRESH_XS_AR = 0.075 # aspect ratio test threshold of mini images 32 THRESH_L_CP = 0.02 # Crop test threshold of large images 33 THRESH_XS_CP = 0.075 # Crop test threshold of mini images 34 THRESH_MIN_PIXEL = 4 # Crop test allowed offset 35 PREVIEW_SIZE = (1920, 1080) # preview size 36 37 38 def convert_ar_to_float(ar_string): 39 """Convert aspect ratio string into float. 40 41 Args: 42 ar_string: "4:3" or "16:9" 43 Returns: 44 float(ar_string) 45 """ 46 ar_list = [float(x) for x in ar_string.split(":")] 47 return ar_list[0] / ar_list[1] 48 49 50 def determine_sensor_aspect_ratio(props): 51 """Determine the aspect ratio of the sensor. 52 53 Args: 54 props: camera properties 55 Returns: 56 matched entry in AR_CHECKED 57 """ 58 match_ar = None 59 sensor_size = props["android.sensor.info.activeArraySize"] 60 sensor_ar = (float(abs(sensor_size["right"] - sensor_size["left"])) / 61 abs(sensor_size["bottom"] - sensor_size["top"])) 62 for ar_string in AR_CHECKED: 63 if np.isclose(sensor_ar, convert_ar_to_float(ar_string), atol=FMT_ATOL): 64 match_ar = ar_string 65 if not match_ar: 66 print "Error: no aspect ratio match with sensor parameters!" 67 return match_ar 68 69 70 def aspect_ratio_scale_factors(camera_ar_string): 71 """Determine scale factors for each aspect ratio to correct cropping. 72 73 Args: 74 camera_ar_string: camera aspect ratio that is the baseline 75 Returns: 76 dict of correction ratios with AR_CHECKED values as keys 77 """ 78 ar_scaling = {} 79 camera_ar = convert_ar_to_float(camera_ar_string) 80 for ar_string in AR_CHECKED: 81 ar = convert_ar_to_float(ar_string) 82 ar_scaling[ar_string] = ar / camera_ar 83 return ar_scaling 84 85 86 def find_yuv_fov_reference(cam, req, props): 87 """Determine the circle coverage of the image in YUV reference image. 88 89 Args: 90 cam: camera object 91 req: camera request 92 props: camera properties 93 94 Returns: 95 ref_fov: dict with [fmt, % coverage, w, h] 96 """ 97 ref_fov = {} 98 ar = determine_sensor_aspect_ratio(props) 99 match_ar = [float(x) for x in ar.split(":")] 100 fmt = its.objects.get_largest_yuv_format(props, match_ar=match_ar) 101 cap = cam.do_capture(req, fmt) 102 w = cap["width"] 103 h = cap["height"] 104 img = its.image.convert_capture_to_rgb_image(cap, props=props) 105 print "Captured %s %dx%d" % ("yuv", w, h) 106 img_name = "%s_%s_w%d_h%d.png" % (NAME, "yuv", w, h) 107 _, _, circle_size = measure_aspect_ratio(img, False, img_name, True) 108 fov_percent = calc_circle_image_ratio(circle_size[1], circle_size[0], w, h) 109 ref_fov["fmt"] = ar 110 ref_fov["percent"] = fov_percent 111 ref_fov["w"] = w 112 ref_fov["h"] = h 113 print "Using YUV reference:", ref_fov 114 return ref_fov 115 116 117 def calc_circle_image_ratio(circle_w, circle_h, image_w, image_h): 118 """Calculate the circle coverage of the image. 119 120 Args: 121 circle_w (int): width of circle 122 circle_h (int): height of circle 123 image_w (int): width of image 124 image_h (int): height of image 125 Returns: 126 fov_percent (float): % of image covered by circle 127 """ 128 circle_area = math.pi * math.pow(np.mean([circle_w, circle_h])/2.0, 2) 129 image_area = image_w * image_h 130 fov_percent = 100*circle_area/image_area 131 return fov_percent 132 133 134 def main(): 135 """Test aspect ratio & check if images are cropped correctly for each fmt. 136 137 Aspect ratio test runs on level3, full and limited devices. Crop test only 138 runs on full and level3 devices. 139 The test image is a black circle inside a black square. When raw capture is 140 available, set the height vs. width ratio of the circle in the full-frame 141 raw as ground truth. Then compare with images of request combinations of 142 different formats ("jpeg" and "yuv") and sizes. 143 If raw capture is unavailable, take a picture of the test image right in 144 front to eliminate shooting angle effect. the height vs. width ratio for 145 the circle should be close to 1. Considering shooting position error, aspect 146 ratio greater than 1+THRESH_*_AR or less than 1-THRESH_*_AR will FAIL. 147 """ 148 aspect_ratio_gt = 1 # ground truth 149 failed_ar = [] # streams failed the aspect ration test 150 failed_crop = [] # streams failed the crop test 151 format_list = [] # format list for multiple capture objects. 152 # Do multi-capture of "iter" and "cmpr". Iterate through all the 153 # available sizes of "iter", and only use the size specified for "cmpr" 154 # Do single-capture to cover untouched sizes in multi-capture when needed. 155 format_list.append({"iter": "yuv", "iter_max": None, 156 "cmpr": "yuv", "cmpr_size": PREVIEW_SIZE}) 157 format_list.append({"iter": "yuv", "iter_max": PREVIEW_SIZE, 158 "cmpr": "jpeg", "cmpr_size": None}) 159 format_list.append({"iter": "yuv", "iter_max": PREVIEW_SIZE, 160 "cmpr": "raw", "cmpr_size": None}) 161 format_list.append({"iter": "jpeg", "iter_max": None, 162 "cmpr": "raw", "cmpr_size": None}) 163 format_list.append({"iter": "jpeg", "iter_max": None, 164 "cmpr": "yuv", "cmpr_size": PREVIEW_SIZE}) 165 ref_fov = {} 166 with its.device.ItsSession() as cam: 167 props = cam.get_camera_properties() 168 its.caps.skip_unless(its.caps.read_3a(props)) 169 full_device = its.caps.full_or_better(props) 170 limited_device = its.caps.limited(props) 171 its.caps.skip_unless(full_device or limited_device) 172 level3_device = its.caps.level3(props) 173 raw_avlb = its.caps.raw16(props) 174 mono_camera = its.caps.mono_camera(props) 175 run_crop_test = (level3_device or full_device) and raw_avlb 176 if not run_crop_test: 177 print "Crop test skipped" 178 debug = its.caps.debug_mode() 179 # Converge 3A and get the estimates. 180 sens, exp, gains, xform, focus = cam.do_3a(get_results=True, 181 lock_ae=True, lock_awb=True, 182 mono_camera=mono_camera) 183 print "AE sensitivity %d, exposure %dms" % (sens, exp / 1000000.0) 184 print "AWB gains", gains 185 print "AWB transform", xform 186 print "AF distance", focus 187 req = its.objects.manual_capture_request( 188 sens, exp, focus, True, props) 189 xform_rat = its.objects.float_to_rational(xform) 190 req["android.colorCorrection.gains"] = gains 191 req["android.colorCorrection.transform"] = xform_rat 192 193 # If raw capture is available, use it as ground truth. 194 if raw_avlb: 195 # Capture full-frame raw. Use its aspect ratio and circle center 196 # location as ground truth for the other jepg or yuv images. 197 print "Creating references for fov_coverage from RAW" 198 out_surface = {"format": "raw"} 199 cap_raw = cam.do_capture(req, out_surface) 200 print "Captured %s %dx%d" % ("raw", cap_raw["width"], 201 cap_raw["height"]) 202 img_raw = its.image.convert_capture_to_rgb_image(cap_raw, 203 props=props) 204 if its.caps.distortion_correction(props): 205 # The intrinsics and distortion coefficients are meant for full 206 # size RAW. Resize back to full size here. 207 img_raw = cv2.resize(img_raw, (0,0), fx=2.0, fy=2.0) 208 # Intrinsic cal is of format: [f_x, f_y, c_x, c_y, s] 209 # [f_x, f_y] is the horizontal and vertical focal lengths, 210 # [c_x, c_y] is the position of the optical axis, 211 # and s is skew of sensor plane vs lens plane. 212 print "Applying intrinsic calibration and distortion params" 213 ical = np.array(props["android.lens.intrinsicCalibration"]) 214 msg = "Cannot include lens distortion without intrinsic cal!" 215 assert len(ical) == 5, msg 216 sensor_h = props["android.sensor.info.physicalSize"]["height"] 217 sensor_w = props["android.sensor.info.physicalSize"]["width"] 218 pixel_h = props["android.sensor.info.pixelArraySize"]["height"] 219 pixel_w = props["android.sensor.info.pixelArraySize"]["width"] 220 fd = float(props["android.lens.info.availableFocalLengths"][0]) 221 fd_w_pix = pixel_w * fd / sensor_w 222 fd_h_pix = pixel_h * fd / sensor_h 223 # transformation matrix 224 # k = [[f_x, s, c_x], 225 # [0, f_y, c_y], 226 # [0, 0, 1]] 227 k = np.array([[ical[0], ical[4], ical[2]], 228 [0, ical[1], ical[3]], 229 [0, 0, 1]]) 230 print "k:", k 231 e_msg = "fd_w(pixels): %.2f\tcal[0](pixels): %.2f\tTOL=20%%" % ( 232 fd_w_pix, ical[0]) 233 assert np.isclose(fd_w_pix, ical[0], rtol=0.20), e_msg 234 e_msg = "fd_h(pixels): %.2f\tcal[1](pixels): %.2f\tTOL=20%%" % ( 235 fd_h_pix, ical[0]) 236 assert np.isclose(fd_h_pix, ical[1], rtol=0.20), e_msg 237 238 # distortion 239 rad_dist = props["android.lens.distortion"] 240 print "android.lens.distortion:", rad_dist 241 e_msg = "%s param(s) found. %d expected." % (len(rad_dist), 242 NUM_DISTORT_PARAMS) 243 assert len(rad_dist) == NUM_DISTORT_PARAMS, e_msg 244 opencv_dist = np.array([rad_dist[0], rad_dist[1], 245 rad_dist[3], rad_dist[4], 246 rad_dist[2]]) 247 print "dist:", opencv_dist 248 img_raw = cv2.undistort(img_raw, k, opencv_dist) 249 size_raw = img_raw.shape 250 w_raw = size_raw[1] 251 h_raw = size_raw[0] 252 img_name = "%s_%s_w%d_h%d.png" % (NAME, "raw", w_raw, h_raw) 253 aspect_ratio_gt, cc_ct_gt, circle_size_raw = measure_aspect_ratio( 254 img_raw, raw_avlb, img_name, debug) 255 raw_fov_percent = calc_circle_image_ratio( 256 circle_size_raw[1], circle_size_raw[0], w_raw, h_raw) 257 # Normalize the circle size to 1/4 of the image size, so that 258 # circle size won't affect the crop test result 259 factor_cp_thres = (min(size_raw[0:1])/4.0) / max(circle_size_raw) 260 thres_l_cp_test = THRESH_L_CP * factor_cp_thres 261 thres_xs_cp_test = THRESH_XS_CP * factor_cp_thres 262 ref_fov["fmt"] = determine_sensor_aspect_ratio(props) 263 ref_fov["percent"] = raw_fov_percent 264 ref_fov["w"] = w_raw 265 ref_fov["h"] = h_raw 266 print "Using RAW reference:", ref_fov 267 else: 268 ref_fov = find_yuv_fov_reference(cam, req, props) 269 270 # Determine scaling factors for AR calculations 271 ar_scaling = aspect_ratio_scale_factors(ref_fov["fmt"]) 272 273 # Take pictures of each settings with all the image sizes available. 274 for fmt in format_list: 275 fmt_iter = fmt["iter"] 276 fmt_cmpr = fmt["cmpr"] 277 dual_target = fmt_cmpr is not "none" 278 # Get the size of "cmpr" 279 if dual_target: 280 sizes = its.objects.get_available_output_sizes( 281 fmt_cmpr, props, fmt["cmpr_size"]) 282 if not sizes: # device might not support RAW 283 continue 284 size_cmpr = sizes[0] 285 for size_iter in its.objects.get_available_output_sizes( 286 fmt_iter, props, fmt["iter_max"]): 287 w_iter = size_iter[0] 288 h_iter = size_iter[1] 289 # Skip testing same format/size combination 290 # ITS does not handle that properly now 291 if (dual_target and w_iter == size_cmpr[0] 292 and h_iter == size_cmpr[1] 293 and fmt_iter == fmt_cmpr): 294 continue 295 out_surface = [{"width": w_iter, 296 "height": h_iter, 297 "format": fmt_iter}] 298 if dual_target: 299 out_surface.append({"width": size_cmpr[0], 300 "height": size_cmpr[1], 301 "format": fmt_cmpr}) 302 cap = cam.do_capture(req, out_surface) 303 if dual_target: 304 frm_iter = cap[0] 305 else: 306 frm_iter = cap 307 assert frm_iter["format"] == fmt_iter 308 assert frm_iter["width"] == w_iter 309 assert frm_iter["height"] == h_iter 310 print "Captured %s with %s %dx%d. Compared size: %dx%d" % ( 311 fmt_iter, fmt_cmpr, w_iter, h_iter, size_cmpr[0], 312 size_cmpr[1]) 313 img = its.image.convert_capture_to_rgb_image(frm_iter) 314 if its.caps.distortion_correction(props) and raw_avlb: 315 w_scale = float(w_iter)/w_raw 316 h_scale = float(h_iter)/h_raw 317 k_scale = np.array([[ical[0]*w_scale, ical[4], 318 ical[2]*w_scale], 319 [0, ical[1]*h_scale, ical[3]*h_scale], 320 [0, 0, 1]]) 321 print "k_scale:", k_scale 322 img = cv2.undistort(img, k_scale, opencv_dist) 323 img_name = "%s_%s_with_%s_w%d_h%d.png" % (NAME, 324 fmt_iter, fmt_cmpr, 325 w_iter, h_iter) 326 aspect_ratio, cc_ct, (cc_w, cc_h) = measure_aspect_ratio( 327 img, raw_avlb, img_name, debug) 328 # check fov coverage for all fmts in AR_CHECKED 329 fov_percent = calc_circle_image_ratio( 330 cc_w, cc_h, w_iter, h_iter) 331 for ar_check in AR_CHECKED: 332 match_ar_list = [float(x) for x in ar_check.split(":")] 333 match_ar = match_ar_list[0] / match_ar_list[1] 334 if np.isclose(float(w_iter)/h_iter, match_ar, 335 atol=FMT_ATOL): 336 # scale check value based on aspect ratio 337 chk_percent = ref_fov["percent"] * ar_scaling[ar_check] 338 339 msg = "FoV %%: %.2f, Ref FoV %%: %.2f, TOL=%.f%%, " % ( 340 fov_percent, chk_percent, 341 FOV_PERCENT_RTOL*100) 342 msg += "img: %dx%d, ref: %dx%d" % (w_iter, h_iter, 343 ref_fov["w"], 344 ref_fov["h"]) 345 assert np.isclose(fov_percent, chk_percent, 346 rtol=FOV_PERCENT_RTOL), msg 347 # check pass/fail for aspect ratio 348 # image size >= LARGE_SIZE: use THRESH_L_AR 349 # image size == 0 (extreme case): THRESH_XS_AR 350 # 0 < image size < LARGE_SIZE: scale between THRESH_XS_AR 351 # and THRESH_L_AR 352 thres_ar_test = max( 353 THRESH_L_AR, THRESH_XS_AR + max(w_iter, h_iter) * 354 (THRESH_L_AR-THRESH_XS_AR)/LARGE_SIZE) 355 thres_range_ar = (aspect_ratio_gt-thres_ar_test, 356 aspect_ratio_gt+thres_ar_test) 357 if (aspect_ratio < thres_range_ar[0] or 358 aspect_ratio > thres_range_ar[1]): 359 failed_ar.append({"fmt_iter": fmt_iter, 360 "fmt_cmpr": fmt_cmpr, 361 "w": w_iter, "h": h_iter, 362 "ar": aspect_ratio, 363 "valid_range": thres_range_ar}) 364 its.image.write_image(img/255, img_name, True) 365 366 # check pass/fail for crop 367 if run_crop_test: 368 # image size >= LARGE_SIZE: use thres_l_cp_test 369 # image size == 0 (extreme case): thres_xs_cp_test 370 # 0 < image size < LARGE_SIZE: scale between 371 # thres_xs_cp_test and thres_l_cp_test 372 # Also, allow at least THRESH_MIN_PIXEL off to 373 # prevent threshold being too tight for very 374 # small circle 375 thres_hori_cp_test = max( 376 thres_l_cp_test, thres_xs_cp_test + w_iter * 377 (thres_l_cp_test-thres_xs_cp_test)/LARGE_SIZE) 378 min_threshold_h = THRESH_MIN_PIXEL / cc_w 379 thres_hori_cp_test = max(thres_hori_cp_test, 380 min_threshold_h) 381 thres_range_h_cp = (cc_ct_gt["hori"]-thres_hori_cp_test, 382 cc_ct_gt["hori"]+thres_hori_cp_test) 383 thres_vert_cp_test = max( 384 thres_l_cp_test, thres_xs_cp_test + h_iter * 385 (thres_l_cp_test-thres_xs_cp_test)/LARGE_SIZE) 386 min_threshold_v = THRESH_MIN_PIXEL / cc_h 387 thres_vert_cp_test = max(thres_vert_cp_test, 388 min_threshold_v) 389 thres_range_v_cp = (cc_ct_gt["vert"]-thres_vert_cp_test, 390 cc_ct_gt["vert"]+thres_vert_cp_test) 391 if (cc_ct["hori"] < thres_range_h_cp[0] 392 or cc_ct["hori"] > thres_range_h_cp[1] 393 or cc_ct["vert"] < thres_range_v_cp[0] 394 or cc_ct["vert"] > thres_range_v_cp[1]): 395 failed_crop.append({"fmt_iter": fmt_iter, 396 "fmt_cmpr": fmt_cmpr, 397 "w": w_iter, "h": h_iter, 398 "ct_hori": cc_ct["hori"], 399 "ct_vert": cc_ct["vert"], 400 "valid_range_h": thres_range_h_cp, 401 "valid_range_v": thres_range_v_cp}) 402 its.image.write_image(img/255, img_name, True) 403 404 # Print aspect ratio test results 405 failed_image_number_for_aspect_ratio_test = len(failed_ar) 406 if failed_image_number_for_aspect_ratio_test > 0: 407 print "\nAspect ratio test summary" 408 print "Images failed in the aspect ratio test:" 409 print "Aspect ratio value: width / height" 410 for fa in failed_ar: 411 print "%s with %s %dx%d: %.3f;" % (fa["fmt_iter"], fa["fmt_cmpr"], 412 fa["w"], fa["h"], fa["ar"]), 413 print "valid range: %.3f ~ %.3f" % (fa["valid_range"][0], 414 fa["valid_range"][1]) 415 416 # Print crop test results 417 failed_image_number_for_crop_test = len(failed_crop) 418 if failed_image_number_for_crop_test > 0: 419 print "\nCrop test summary" 420 print "Images failed in the crop test:" 421 print "Circle center position, (horizontal x vertical), listed", 422 print "below is relative to the image center." 423 for fc in failed_crop: 424 print "%s with %s %dx%d: %.3f x %.3f;" % ( 425 fc["fmt_iter"], fc["fmt_cmpr"], fc["w"], fc["h"], 426 fc["ct_hori"], fc["ct_vert"]), 427 print "valid horizontal range: %.3f ~ %.3f;" % ( 428 fc["valid_range_h"][0], fc["valid_range_h"][1]), 429 print "valid vertical range: %.3f ~ %.3f" % ( 430 fc["valid_range_v"][0], fc["valid_range_v"][1]) 431 432 assert failed_image_number_for_aspect_ratio_test == 0 433 if level3_device: 434 assert failed_image_number_for_crop_test == 0 435 436 437 def measure_aspect_ratio(img, raw_avlb, img_name, debug): 438 """Measure the aspect ratio of the black circle in the test image. 439 440 Args: 441 img: Numpy float image array in RGB, with pixel values in [0,1]. 442 raw_avlb: True: raw capture is available; False: raw capture is not 443 available. 444 img_name: string with image info of format and size. 445 debug: boolean for whether in debug mode. 446 Returns: 447 aspect_ratio: aspect ratio number in float. 448 cc_ct: circle center position relative to the center of image. 449 (circle_w, circle_h): tuple of the circle size 450 """ 451 size = img.shape 452 img *= 255 453 # Gray image 454 img_gray = 0.299*img[:, :, 2] + 0.587*img[:, :, 1] + 0.114*img[:, :, 0] 455 456 # otsu threshold to binarize the image 457 _, img_bw = cv2.threshold(np.uint8(img_gray), 0, 255, 458 cv2.THRESH_BINARY + cv2.THRESH_OTSU) 459 460 # connected component 461 cv2_version = cv2.__version__ 462 if cv2_version.startswith("2.4."): 463 contours, hierarchy = cv2.findContours(255-img_bw, cv2.RETR_TREE, 464 cv2.CHAIN_APPROX_SIMPLE) 465 elif cv2_version.startswith("3.2."): 466 _, contours, hierarchy = cv2.findContours(255-img_bw, cv2.RETR_TREE, 467 cv2.CHAIN_APPROX_SIMPLE) 468 469 # Check each component and find the black circle 470 min_cmpt = size[0] * size[1] * 0.005 471 max_cmpt = size[0] * size[1] * 0.35 472 num_circle = 0 473 aspect_ratio = 0 474 for ct, hrch in zip(contours, hierarchy[0]): 475 # The radius of the circle is 1/3 of the length of the square, meaning 476 # around 1/3 of the area of the square 477 # Parental component should exist and the area is acceptable. 478 # The coutour of a circle should have at least 5 points 479 child_area = cv2.contourArea(ct) 480 if (hrch[3] == -1 or child_area < min_cmpt or child_area > max_cmpt 481 or len(ct) < 15): 482 continue 483 # Check the shapes of current component and its parent 484 child_shape = component_shape(ct) 485 parent = hrch[3] 486 prt_shape = component_shape(contours[parent]) 487 prt_area = cv2.contourArea(contours[parent]) 488 dist_x = abs(child_shape["ctx"]-prt_shape["ctx"]) 489 dist_y = abs(child_shape["cty"]-prt_shape["cty"]) 490 # 1. 0.56*Parent"s width < Child"s width < 0.76*Parent"s width. 491 # 2. 0.56*Parent"s height < Child"s height < 0.76*Parent"s height. 492 # 3. Child"s width > 0.1*Image width 493 # 4. Child"s height > 0.1*Image height 494 # 5. 0.25*Parent"s area < Child"s area < 0.45*Parent"s area 495 # 6. Child is a black, and Parent is white 496 # 7. Center of Child and center of parent should overlap 497 if (prt_shape["width"] * 0.56 < child_shape["width"] 498 < prt_shape["width"] * 0.76 499 and prt_shape["height"] * 0.56 < child_shape["height"] 500 < prt_shape["height"] * 0.76 501 and child_shape["width"] > 0.1 * size[1] 502 and child_shape["height"] > 0.1 * size[0] 503 and 0.30 * prt_area < child_area < 0.50 * prt_area 504 and img_bw[child_shape["cty"]][child_shape["ctx"]] == 0 505 and img_bw[child_shape["top"]][child_shape["left"]] == 255 506 and dist_x < 0.1 * child_shape["width"] 507 and dist_y < 0.1 * child_shape["height"]): 508 # If raw capture is not available, check the camera is placed right 509 # in front of the test page: 510 # 1. Distances between parent and child horizontally on both side,0 511 # dist_left and dist_right, should be close. 512 # 2. Distances between parent and child vertically on both side, 513 # dist_top and dist_bottom, should be close. 514 if not raw_avlb: 515 dist_left = child_shape["left"] - prt_shape["left"] 516 dist_right = prt_shape["right"] - child_shape["right"] 517 dist_top = child_shape["top"] - prt_shape["top"] 518 dist_bottom = prt_shape["bottom"] - child_shape["bottom"] 519 if (abs(dist_left-dist_right) > 0.05 * child_shape["width"] 520 or abs(dist_top-dist_bottom) > 0.05 * child_shape["height"]): 521 continue 522 # Calculate aspect ratio 523 aspect_ratio = float(child_shape["width"]) / child_shape["height"] 524 circle_ctx = child_shape["ctx"] 525 circle_cty = child_shape["cty"] 526 circle_w = float(child_shape["width"]) 527 circle_h = float(child_shape["height"]) 528 cc_ct = {"hori": float(child_shape["ctx"]-size[1]/2) / circle_w, 529 "vert": float(child_shape["cty"]-size[0]/2) / circle_h} 530 num_circle += 1 531 # If more than one circle found, break 532 if num_circle == 2: 533 break 534 535 if num_circle == 0: 536 its.image.write_image(img/255, img_name, True) 537 print "No black circle was detected. Please take pictures according", 538 print "to instruction carefully!\n" 539 assert num_circle == 1 540 541 if num_circle > 1: 542 its.image.write_image(img/255, img_name, True) 543 print "More than one black circle was detected. Background of scene", 544 print "may be too complex.\n" 545 assert num_circle == 1 546 547 # draw circle center and image center, and save the image 548 line_width = max(1, max(size)/500) 549 move_text_dist = line_width * 3 550 cv2.line(img, (circle_ctx, circle_cty), (size[1]/2, size[0]/2), 551 (255, 0, 0), line_width) 552 if circle_cty > size[0]/2: 553 move_text_down_circle = 4 554 move_text_down_image = -1 555 else: 556 move_text_down_circle = -1 557 move_text_down_image = 4 558 if circle_ctx > size[1]/2: 559 move_text_right_circle = 2 560 move_text_right_image = -1 561 else: 562 move_text_right_circle = -1 563 move_text_right_image = 2 564 # circle center 565 text_circle_x = move_text_dist * move_text_right_circle + circle_ctx 566 text_circle_y = move_text_dist * move_text_down_circle + circle_cty 567 cv2.circle(img, (circle_ctx, circle_cty), line_width*2, (255, 0, 0), -1) 568 cv2.putText(img, "circle center", (text_circle_x, text_circle_y), 569 cv2.FONT_HERSHEY_SIMPLEX, line_width/2.0, (255, 0, 0), 570 line_width) 571 # image center 572 text_imgct_x = move_text_dist * move_text_right_image + size[1]/2 573 text_imgct_y = move_text_dist * move_text_down_image + size[0]/2 574 cv2.circle(img, (size[1]/2, size[0]/2), line_width*2, (255, 0, 0), -1) 575 cv2.putText(img, "image center", (text_imgct_x, text_imgct_y), 576 cv2.FONT_HERSHEY_SIMPLEX, line_width/2.0, (255, 0, 0), 577 line_width) 578 if debug: 579 its.image.write_image(img/255, img_name, True) 580 581 print "Aspect ratio: %.3f" % aspect_ratio 582 print "Circle center position wrt to image center:", 583 print "%.3fx%.3f" % (cc_ct["vert"], cc_ct["hori"]) 584 return aspect_ratio, cc_ct, (circle_w, circle_h) 585 586 587 def component_shape(contour): 588 """Measure the shape for a connected component in the aspect ratio test. 589 590 Args: 591 contour: return from cv2.findContours. A list of pixel coordinates of 592 the contour. 593 594 Returns: 595 The most left, right, top, bottom pixel location, height, width, and 596 the center pixel location of the contour. 597 """ 598 shape = {"left": np.inf, "right": 0, "top": np.inf, "bottom": 0, 599 "width": 0, "height": 0, "ctx": 0, "cty": 0} 600 for pt in contour: 601 if pt[0][0] < shape["left"]: 602 shape["left"] = pt[0][0] 603 if pt[0][0] > shape["right"]: 604 shape["right"] = pt[0][0] 605 if pt[0][1] < shape["top"]: 606 shape["top"] = pt[0][1] 607 if pt[0][1] > shape["bottom"]: 608 shape["bottom"] = pt[0][1] 609 shape["width"] = shape["right"] - shape["left"] + 1 610 shape["height"] = shape["bottom"] - shape["top"] + 1 611 shape["ctx"] = (shape["left"]+shape["right"])/2 612 shape["cty"] = (shape["top"]+shape["bottom"])/2 613 return shape 614 615 616 if __name__ == "__main__": 617 main() 618