• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 its.image
16import its.caps
17import its.device
18import its.objects
19import os.path
20import cv2
21import numpy as np
22
23
24def main():
25    """ Test aspect ratio and check if images are cropped correctly under each
26    output size
27    Aspect ratio test runs on level3, full and limited devices. Crop test only
28    runs on full and level3 devices.
29    The test image is a black circle inside a black square. When raw capture is
30    available, set the height vs. width ratio of the circle in the full-frame
31    raw as ground truth. Then compare with images of request combinations of
32    different formats ("jpeg" and "yuv") and sizes.
33    If raw capture is unavailable, take a picture of the test image right in
34    front to eliminate shooting angle effect. the height vs. width ratio for
35    the circle should be close to 1. Considering shooting position error, aspect
36    ratio greater than 1.05 or smaller than 0.95 will fail the test.
37    """
38    NAME = os.path.basename(__file__).split(".")[0]
39    LARGE_SIZE = 2000   # Define the size of a large image
40    # pass/fail threshold of large size images for aspect ratio test
41    THRES_L_AR_TEST = 0.02
42    # pass/fail threshold of mini size images for aspect ratio test
43    THRES_XS_AR_TEST = 0.05
44    # pass/fail threshold of large size images for crop test
45    THRES_L_CP_TEST = 0.02
46    # pass/fail threshold of mini size images for crop test
47    THRES_XS_CP_TEST = 0.05
48    # Crop test will allow at least THRES_MIN_PIXEL offset
49    THRES_MIN_PIXEL = 4
50    PREVIEW_SIZE = (1920, 1080) # preview size
51    aspect_ratio_gt = 1  # ground truth
52    failed_ar = []  # streams failed the aspect ration test
53    failed_crop = [] # streams failed the crop test
54    format_list = [] # format list for multiple capture objects.
55    # Do multi-capture of "iter" and "cmpr". Iterate through all the
56    # available sizes of "iter", and only use the size specified for "cmpr"
57    # Do single-capture to cover untouched sizes in multi-capture when needed.
58    format_list.append({"iter": "yuv", "iter_max": None,
59                        "cmpr": "yuv", "cmpr_size": PREVIEW_SIZE})
60    format_list.append({"iter": "yuv", "iter_max": PREVIEW_SIZE,
61                        "cmpr": "jpeg", "cmpr_size": None})
62    format_list.append({"iter": "yuv", "iter_max": PREVIEW_SIZE,
63                        "cmpr": "raw", "cmpr_size": None})
64    format_list.append({"iter": "jpeg", "iter_max": None,
65                        "cmpr": "raw", "cmpr_size": None})
66    format_list.append({"iter": "jpeg", "iter_max": None,
67                        "cmpr": "yuv", "cmpr_size": PREVIEW_SIZE})
68    with its.device.ItsSession() as cam:
69        props = cam.get_camera_properties()
70        # Todo: test for radial distortion enabled devices has not yet been
71        # implemented
72        its.caps.skip_unless(not its.caps.radial_distortion_correction(props))
73        full_device = its.caps.full_or_better(props)
74        limited_device = its.caps.limited(props)
75        its.caps.skip_unless(full_device or limited_device)
76        level3_device = its.caps.level3(props)
77        raw_avlb = its.caps.raw16(props)
78        run_crop_test = (level3_device or full_device) and raw_avlb
79        if not run_crop_test:
80            print "Crop test skipped"
81        # Converge 3A and get the estimates.
82        sens, exp, gains, xform, focus = cam.do_3a(get_results=True,
83                                                   lock_ae=True, lock_awb=True)
84        print "AE sensitivity %d, exposure %dms" % (sens, exp / 1000000.0)
85        print "AWB gains", gains
86        print "AWB transform", xform
87        print "AF distance", focus
88        req = its.objects.manual_capture_request(
89                sens, exp, focus, True, props)
90        xform_rat = its.objects.float_to_rational(xform)
91        req["android.colorCorrection.gains"] = gains
92        req["android.colorCorrection.transform"] = xform_rat
93
94        # If raw capture is available, use it as ground truth.
95        if raw_avlb:
96            # Capture full-frame raw. Use its aspect ratio and circle center
97            # location as ground truth for the other jepg or yuv images.
98            out_surface = {"format": "raw"}
99            cap_raw = cam.do_capture(req, out_surface)
100            print "Captured %s %dx%d" % ("raw", cap_raw["width"],
101                                         cap_raw["height"])
102            img_raw = its.image.convert_capture_to_rgb_image(cap_raw,
103                                                             props=props)
104            size_raw = img_raw.shape
105            img_name = "%s_%s_w%d_h%d.png" \
106                       % (NAME, "raw", size_raw[1], size_raw[0])
107            aspect_ratio_gt, cc_ct_gt, circle_size_raw = measure_aspect_ratio(
108                                                         img_raw, 1, img_name)
109            # Normalize the circle size to 1/4 of the image size, so that
110            # circle size won"t affect the crop test result
111            factor_cp_thres = (min(size_raw[0:1])/4.0) / max(circle_size_raw)
112            thres_l_cp_test = THRES_L_CP_TEST * factor_cp_thres
113            thres_xs_cp_test = THRES_XS_CP_TEST * factor_cp_thres
114
115        # Take pictures of each settings with all the image sizes available.
116        for fmt in format_list:
117            fmt_iter = fmt["iter"]
118            fmt_cmpr = fmt["cmpr"]
119            dual_target = fmt_cmpr is not "none"
120            # Get the size of "cmpr"
121            if dual_target:
122                sizes = its.objects.get_available_output_sizes(
123                        fmt_cmpr, props, fmt["cmpr_size"])
124                if len(sizes) == 0: # device might not support RAW
125                    continue
126                size_cmpr = sizes[0]
127            for size_iter in its.objects.get_available_output_sizes(
128                    fmt_iter, props, fmt["iter_max"]):
129                w_iter = size_iter[0]
130                h_iter = size_iter[1]
131                # Skip testing same format/size combination
132                # ITS does not handle that properly now
133                if dual_target and \
134                        w_iter == size_cmpr[0] and \
135                        h_iter == size_cmpr[1] and \
136                        fmt_iter == fmt_cmpr:
137                    continue
138                out_surface = [{"width": w_iter,
139                                "height": h_iter,
140                                "format": fmt_iter}]
141                if dual_target:
142                    out_surface.append({"width": size_cmpr[0],
143                                        "height": size_cmpr[1],
144                                        "format": fmt_cmpr})
145                cap = cam.do_capture(req, out_surface)
146                if dual_target:
147                    frm_iter = cap[0]
148                else:
149                    frm_iter = cap
150                assert (frm_iter["format"] == fmt_iter)
151                assert (frm_iter["width"] == w_iter)
152                assert (frm_iter["height"] == h_iter)
153                print "Captured %s with %s %dx%d" \
154                        % (fmt_iter, fmt_cmpr, w_iter, h_iter)
155                img = its.image.convert_capture_to_rgb_image(frm_iter)
156                img_name = "%s_%s_with_%s_w%d_h%d.png" \
157                           % (NAME, fmt_iter, fmt_cmpr, w_iter, h_iter)
158                aspect_ratio, cc_ct, (cc_w, cc_h) = \
159                        measure_aspect_ratio(img, raw_avlb, img_name)
160                # check pass/fail for aspect ratio
161                # image size >= LARGE_SIZE: use THRES_L_AR_TEST
162                # image size == 0 (extreme case): THRES_XS_AR_TEST
163                # 0 < image size < LARGE_SIZE: scale between THRES_XS_AR_TEST
164                # and THRES_L_AR_TEST
165                thres_ar_test = max(THRES_L_AR_TEST,
166                        THRES_XS_AR_TEST + max(w_iter, h_iter) *
167                        (THRES_L_AR_TEST-THRES_XS_AR_TEST)/LARGE_SIZE)
168                thres_range_ar = (aspect_ratio_gt-thres_ar_test,
169                                  aspect_ratio_gt+thres_ar_test)
170                if aspect_ratio < thres_range_ar[0] \
171                        or aspect_ratio > thres_range_ar[1]:
172                    failed_ar.append({"fmt_iter": fmt_iter,
173                                      "fmt_cmpr": fmt_cmpr,
174                                      "w": w_iter, "h": h_iter,
175                                      "ar": aspect_ratio,
176                                      "valid_range": thres_range_ar})
177
178                # check pass/fail for crop
179                if run_crop_test:
180                    # image size >= LARGE_SIZE: use thres_l_cp_test
181                    # image size == 0 (extreme case): thres_xs_cp_test
182                    # 0 < image size < LARGE_SIZE: scale between
183                    # thres_xs_cp_test and thres_l_cp_test
184                    # Also, allow at least THRES_MIN_PIXEL off to
185                    # prevent threshold being too tight for very
186                    # small circle
187                    thres_hori_cp_test = max(thres_l_cp_test,
188                            thres_xs_cp_test + w_iter *
189                            (thres_l_cp_test-thres_xs_cp_test)/LARGE_SIZE)
190                    min_threshold_h = THRES_MIN_PIXEL / cc_w
191                    thres_hori_cp_test = max(thres_hori_cp_test,
192                            min_threshold_h)
193                    thres_range_h_cp = (cc_ct_gt["hori"]-thres_hori_cp_test,
194                                        cc_ct_gt["hori"]+thres_hori_cp_test)
195                    thres_vert_cp_test = max(thres_l_cp_test,
196                            thres_xs_cp_test + h_iter *
197                            (thres_l_cp_test-thres_xs_cp_test)/LARGE_SIZE)
198                    min_threshold_v = THRES_MIN_PIXEL / cc_h
199                    thres_vert_cp_test = max(thres_vert_cp_test,
200                            min_threshold_v)
201                    thres_range_v_cp = (cc_ct_gt["vert"]-thres_vert_cp_test,
202                                        cc_ct_gt["vert"]+thres_vert_cp_test)
203                    if cc_ct["hori"] < thres_range_h_cp[0] \
204                            or cc_ct["hori"] > thres_range_h_cp[1] \
205                            or cc_ct["vert"] < thres_range_v_cp[0] \
206                            or cc_ct["vert"] > thres_range_v_cp[1]:
207                        failed_crop.append({"fmt_iter": fmt_iter,
208                                            "fmt_cmpr": fmt_cmpr,
209                                            "w": w_iter, "h": h_iter,
210                                            "ct_hori": cc_ct["hori"],
211                                            "ct_vert": cc_ct["vert"],
212                                            "valid_range_h": thres_range_h_cp,
213                                            "valid_range_v": thres_range_v_cp})
214
215        # Print aspect ratio test results
216        failed_image_number_for_aspect_ratio_test = len(failed_ar)
217        if failed_image_number_for_aspect_ratio_test > 0:
218            print "\nAspect ratio test summary"
219            print "Images failed in the aspect ratio test:"
220            print "Aspect ratio value: width / height"
221        for fa in failed_ar:
222            print "%s with %s %dx%d: %.3f; valid range: %.3f ~ %.3f" % \
223                  (fa["fmt_iter"], fa["fmt_cmpr"], fa["w"], fa["h"], fa["ar"],
224                   fa["valid_range"][0], fa["valid_range"][1])
225
226        # Print crop test results
227        failed_image_number_for_crop_test = len(failed_crop)
228        if failed_image_number_for_crop_test > 0:
229            print "\nCrop test summary"
230            print "Images failed in the crop test:"
231            print "Circle center position, (horizontal x vertical), listed " \
232                  "below is relative to the image center."
233        for fc in failed_crop:
234            print "%s with %s %dx%d: %.3f x %.3f; " \
235                    "valid horizontal range: %.3f ~ %.3f; " \
236                    "valid vertical range: %.3f ~ %.3f" \
237                    % (fc["fmt_iter"], fc["fmt_cmpr"], fc["w"], fc["h"],
238                    fc["ct_hori"], fc["ct_vert"], fc["valid_range_h"][0],
239                    fc["valid_range_h"][1], fc["valid_range_v"][0],
240                    fc["valid_range_v"][1])
241
242        assert (failed_image_number_for_aspect_ratio_test == 0)
243        if level3_device:
244            assert (failed_image_number_for_crop_test == 0)
245
246
247def measure_aspect_ratio(img, raw_avlb, img_name):
248    """ Measure the aspect ratio of the black circle in the test image.
249
250    Args:
251        img: Numpy float image array in RGB, with pixel values in [0,1].
252        raw_avlb: True: raw capture is available; False: raw capture is not
253             available.
254        img_name: string with image info of format and size.
255    Returns:
256        aspect_ratio: aspect ratio number in float.
257        cc_ct: circle center position relative to the center of image.
258        (circle_w, circle_h): tuple of the circle size
259    """
260    size = img.shape
261    img = img * 255
262    # Gray image
263    img_gray = 0.299 * img[:,:,2] + 0.587 * img[:,:,1] + 0.114 * img[:,:,0]
264
265    # otsu threshold to binarize the image
266    ret3, img_bw = cv2.threshold(np.uint8(img_gray), 0, 255,
267            cv2.THRESH_BINARY + cv2.THRESH_OTSU)
268
269    # connected component
270    contours, hierarchy = cv2.findContours(255-img_bw, cv2.RETR_TREE,
271            cv2.CHAIN_APPROX_SIMPLE)
272
273    # Check each component and find the black circle
274    min_cmpt = size[0] * size[1] * 0.005
275    max_cmpt = size[0] * size[1] * 0.35
276    num_circle = 0
277    aspect_ratio = 0
278    for ct, hrch in zip(contours, hierarchy[0]):
279        # The radius of the circle is 1/3 of the length of the square, meaning
280        # around 1/3 of the area of the square
281        # Parental component should exist and the area is acceptable.
282        # The coutour of a circle should have at least 5 points
283        child_area = cv2.contourArea(ct)
284        if hrch[3] == -1 or child_area < min_cmpt or child_area > max_cmpt or \
285                len(ct) < 15:
286            continue
287        # Check the shapes of current component and its parent
288        child_shape = component_shape(ct)
289        parent = hrch[3]
290        prt_shape = component_shape(contours[parent])
291        prt_area = cv2.contourArea(contours[parent])
292        dist_x = abs(child_shape["ctx"]-prt_shape["ctx"])
293        dist_y = abs(child_shape["cty"]-prt_shape["cty"])
294        # 1. 0.56*Parent"s width < Child"s width < 0.76*Parent"s width.
295        # 2. 0.56*Parent"s height < Child"s height < 0.76*Parent"s height.
296        # 3. Child"s width > 0.1*Image width
297        # 4. Child"s height > 0.1*Image height
298        # 5. 0.25*Parent"s area < Child"s area < 0.45*Parent"s area
299        # 6. Child is a black, and Parent is white
300        # 7. Center of Child and center of parent should overlap
301        if prt_shape["width"] * 0.56 < child_shape["width"] \
302                < prt_shape["width"] * 0.76 \
303                and prt_shape["height"] * 0.56 < child_shape["height"] \
304                < prt_shape["height"] * 0.76 \
305                and child_shape["width"] > 0.1 * size[1] \
306                and child_shape["height"] > 0.1 * size[0] \
307                and 0.30 * prt_area < child_area < 0.50 * prt_area \
308                and img_bw[child_shape["cty"]][child_shape["ctx"]] == 0 \
309                and img_bw[child_shape["top"]][child_shape["left"]] == 255 \
310                and dist_x < 0.1 * child_shape["width"] \
311                and dist_y < 0.1 * child_shape["height"]:
312            # If raw capture is not available, check the camera is placed right
313            # in front of the test page:
314            # 1. Distances between parent and child horizontally on both side,0
315            #    dist_left and dist_right, should be close.
316            # 2. Distances between parent and child vertically on both side,
317            #    dist_top and dist_bottom, should be close.
318            if not raw_avlb:
319                dist_left = child_shape["left"] - prt_shape["left"]
320                dist_right = prt_shape["right"] - child_shape["right"]
321                dist_top = child_shape["top"] - prt_shape["top"]
322                dist_bottom = prt_shape["bottom"] - child_shape["bottom"]
323                if abs(dist_left-dist_right) > 0.05 * child_shape["width"] or \
324                        abs(dist_top-dist_bottom) > \
325                        0.05 * child_shape["height"]:
326                    continue
327            # Calculate aspect ratio
328            aspect_ratio = float(child_shape["width"]) / \
329                           float(child_shape["height"])
330            circle_ctx = child_shape["ctx"]
331            circle_cty = child_shape["cty"]
332            circle_w = float(child_shape["width"])
333            circle_h = float(child_shape["height"])
334            cc_ct = {"hori": float(child_shape["ctx"]-size[1]/2) / circle_w,
335                     "vert": float(child_shape["cty"]-size[0]/2) / circle_h}
336            num_circle += 1
337            # If more than one circle found, break
338            if num_circle == 2:
339                break
340
341    if num_circle == 0:
342        its.image.write_image(img/255, img_name, True)
343        print "No black circle was detected. Please take pictures according " \
344              "to instruction carefully!\n"
345        assert (num_circle == 1)
346
347    if num_circle > 1:
348        its.image.write_image(img/255, img_name, True)
349        print "More than one black circle was detected. Background of scene " \
350              "may be too complex.\n"
351        assert (num_circle == 1)
352
353    # draw circle center and image center, and save the image
354    line_width = max(1, max(size)/500)
355    move_text_dist = line_width * 3
356    cv2.line(img, (circle_ctx, circle_cty), (size[1]/2, size[0]/2),
357             (255, 0, 0), line_width)
358    if circle_cty > size[0]/2:
359        move_text_down_circle = 4
360        move_text_down_image = -1
361    else:
362        move_text_down_circle = -1
363        move_text_down_image = 4
364    if circle_ctx > size[1]/2:
365        move_text_right_circle = 2
366        move_text_right_image = -1
367    else:
368        move_text_right_circle = -1
369        move_text_right_image = 2
370    # circle center
371    text_circle_x = move_text_dist * move_text_right_circle + circle_ctx
372    text_circle_y = move_text_dist * move_text_down_circle + circle_cty
373    cv2.circle(img, (circle_ctx, circle_cty), line_width*2, (255, 0, 0), -1)
374    cv2.putText(img, "circle center", (text_circle_x, text_circle_y),
375                cv2.FONT_HERSHEY_SIMPLEX, line_width/2.0, (255, 0, 0),
376                line_width)
377    # image center
378    text_imgct_x = move_text_dist * move_text_right_image + size[1]/2
379    text_imgct_y = move_text_dist * move_text_down_image + size[0]/2
380    cv2.circle(img, (size[1]/2, size[0]/2), line_width*2, (255, 0, 0), -1)
381    cv2.putText(img, "image center", (text_imgct_x, text_imgct_y),
382                cv2.FONT_HERSHEY_SIMPLEX, line_width/2.0, (255, 0, 0),
383                line_width)
384    its.image.write_image(img/255, img_name, True)
385
386    print "Aspect ratio: %.3f" % aspect_ratio
387    print "Circle center position regarding to image center: %.3fx%.3f" % \
388            (cc_ct["vert"], cc_ct["hori"])
389    return aspect_ratio, cc_ct, (circle_w, circle_h)
390
391
392def component_shape(contour):
393    """ Measure the shape for a connected component in the aspect ratio test
394
395    Args:
396        contour: return from cv2.findContours. A list of pixel coordinates of
397        the contour.
398
399    Returns:
400        The most left, right, top, bottom pixel location, height, width, and
401        the center pixel location of the contour.
402    """
403    shape = {"left": np.inf, "right": 0, "top": np.inf, "bottom": 0,
404             "width": 0, "height": 0, "ctx": 0, "cty": 0}
405    for pt in contour:
406        if pt[0][0] < shape["left"]:
407            shape["left"] = pt[0][0]
408        if pt[0][0] > shape["right"]:
409            shape["right"] = pt[0][0]
410        if pt[0][1] < shape["top"]:
411            shape["top"] = pt[0][1]
412        if pt[0][1] > shape["bottom"]:
413            shape["bottom"] = pt[0][1]
414    shape["width"] = shape["right"] - shape["left"] + 1
415    shape["height"] = shape["bottom"] - shape["top"] + 1
416    shape["ctx"] = (shape["left"]+shape["right"])/2
417    shape["cty"] = (shape["top"]+shape["bottom"])/2
418    return shape
419
420
421if __name__ == "__main__":
422    main()
423