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