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