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