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"]  # 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.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
70def 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
86def 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
117def 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
134def 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
437def 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
587def 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
616if __name__ == "__main__":
617    main()
618