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