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