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