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