Home | History | Annotate | Download | only in scene4
      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