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