• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015 The Android Open Source Project (lint as: python2)
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 math
16import os.path
17import cv2
18import its.caps
19import its.cv2image
20import its.device
21import its.image
22import its.objects
23import numpy as np
24
25FOV_PERCENT_RTOL = 0.15  # Relative tolerance on circle FoV % to expected
26LARGE_SIZE = 2000   # Define the size of a large image (compare against max(w,h))
27NAME = os.path.basename(__file__).split(".")[0]
28NUM_DISTORT_PARAMS = 5
29THRESH_L_AR = 0.02  # aspect ratio test threshold of large images
30THRESH_XS_AR = 0.075  # aspect ratio test threshold of mini images
31THRESH_L_CP = 0.02  # Crop test threshold of large images
32THRESH_XS_CP = 0.075  # Crop test threshold of mini images
33THRESH_MIN_PIXEL = 4  # Crop test allowed offset
34PREVIEW_SIZE = (1920, 1080)  # preview size
35
36# Before API level 30, only resolutions with the following listed aspect ratio
37# are checked. Device launched after API level 30 will need to pass the test
38# for all advertised resolutions. Device launched before API level 30 just
39# needs to pass the test for all resolutions within these aspect ratios.
40AR_CHECKED_PRE_API_30 = ["4:3", "16:9", "18:9"]
41AR_DIFF_ATOL = 0.01
42
43
44def print_failed_test_results(failed_ar, failed_fov, failed_crop):
45    """Print failed test results."""
46    if failed_ar:
47        print "\nAspect ratio test summary"
48        print "Images failed in the aspect ratio test:"
49        print "Aspect ratio value: width / height"
50        for fa in failed_ar:
51            print "%s with %s %dx%d: %.3f;" % (
52                    fa["fmt_iter"], fa["fmt_cmpr"],
53                    fa["w"], fa["h"], fa["ar"]),
54            print "valid range: %.3f ~ %.3f" % (
55                    fa["valid_range"][0], fa["valid_range"][1])
56
57    if failed_fov:
58        print "\nFoV test summary"
59        print "Images failed in the FoV test:"
60        for fov in failed_fov:
61            print fov
62
63    if failed_crop:
64        print "\nCrop test summary"
65        print "Images failed in the crop test:"
66        print "Circle center position, (horizontal x vertical), listed",
67        print "below is relative to the image center."
68        for fc in failed_crop:
69            print "%s with %s %dx%d: %.3f x %.3f;" % (
70                    fc["fmt_iter"], fc["fmt_cmpr"], fc["w"], fc["h"],
71                    fc["ct_hori"], fc["ct_vert"]),
72            print "valid horizontal range: %.3f ~ %.3f;" % (
73                    fc["valid_range_h"][0], fc["valid_range_h"][1]),
74            print "valid vertical range: %.3f ~ %.3f" % (
75                    fc["valid_range_v"][0], fc["valid_range_v"][1])
76
77
78def is_checked_aspect_ratio(first_api_level, w, h):
79    if first_api_level >= 30:
80        return True
81
82    for ar_check in AR_CHECKED_PRE_API_30:
83        match_ar_list = [float(x) for x in ar_check.split(":")]
84        match_ar = match_ar_list[0] / match_ar_list[1]
85        if np.isclose(float(w)/h, match_ar, atol=AR_DIFF_ATOL):
86            return True
87
88    return False
89
90def calc_expected_circle_image_ratio(ref_fov, img_w, img_h):
91    """Determine the circle image area ratio in percentage for a given image size.
92
93    Args:
94        ref_fov:    dict with [fmt, % coverage, w, h, circle_w, circle_h]
95        img_w:      the image width
96        img_h:      the image height
97
98    Returns:
99        chk_percent: the expected circle image area ratio in percentage
100    """
101
102    ar_ref = float(ref_fov["w"]) / ref_fov["h"]
103    ar_target = float(img_w) / img_h
104    # The cropping will happen either horizontally or vertically.
105    # In both case a crop results in the visble area reduce by a ratio r (r < 1.0)
106    # and the circle will in turn occupy ref_pct / r (percent) on the target
107    # image size.
108    r = ar_ref / ar_target
109    if r < 1.0:
110        r = 1.0 / r
111    return ref_fov["percent"] * r
112
113
114def find_raw_fov_reference(cam, req, props, debug):
115    """Determine the circle coverage of the image in RAW reference image.
116
117    Args:
118        cam:        camera object
119        req:        camera request
120        props:      camera properties
121        debug:      perform debug dump or not
122
123    Returns:
124        ref_fov:         dict with [fmt, % coverage, w, h, circle_w, circle_h]
125        cc_ct_gt:        circle center position relative to the center of image.
126        aspect_ratio_gt: aspect ratio of the detected circle in float.
127    """
128
129    # Capture full-frame raw. Use its aspect ratio and circle center
130    # location as ground truth for the other jpeg or yuv images.
131    print "Creating references for fov_coverage from RAW"
132    out_surface = {"format": "raw"}
133    cap_raw = cam.do_capture(req, out_surface)
134    print "Captured %s %dx%d" % ("raw", cap_raw["width"],
135                                 cap_raw["height"])
136    img_raw = its.image.convert_capture_to_rgb_image(cap_raw,
137                                                     props=props)
138
139    # The intrinsics and distortion coefficients are meant for full
140    # size RAW, but convert_capture_to_rgb_image returns a 2x downsampled
141    # version, so resize back to full size here.
142    img_raw = cv2.resize(img_raw, (0, 0), fx=2.0, fy=2.0)
143
144    # If the device supports lens distortion correction, apply the
145    # coefficients on the RAW image so it can be compared to YUV/JPEG
146    # outputs which are subject to the same correction via ISP.
147    if its.caps.distortion_correction(props):
148        # Intrinsic cal is of format: [f_x, f_y, c_x, c_y, s]
149        # [f_x, f_y] is the horizontal and vertical focal lengths,
150        # [c_x, c_y] is the position of the optical axis,
151        # and s is skew of sensor plane vs lens plane.
152        print "Applying intrinsic calibration and distortion params"
153        ical = np.array(props["android.lens.intrinsicCalibration"])
154        msg = "Cannot include lens distortion without intrinsic cal!"
155        assert len(ical) == 5, msg
156        sensor_h = props["android.sensor.info.physicalSize"]["height"]
157        sensor_w = props["android.sensor.info.physicalSize"]["width"]
158        pixel_h = props["android.sensor.info.pixelArraySize"]["height"]
159        pixel_w = props["android.sensor.info.pixelArraySize"]["width"]
160        fd = float(cap_raw["metadata"]["android.lens.focalLength"])
161        fd_w_pix = pixel_w * fd / sensor_w
162        fd_h_pix = pixel_h * fd / sensor_h
163        # transformation matrix
164        # k = [[f_x, s, c_x],
165        #      [0, f_y, c_y],
166        #      [0,   0,   1]]
167        k = np.array([[ical[0], ical[4], ical[2]],
168                      [0, ical[1], ical[3]],
169                      [0, 0, 1]])
170        print "k:", k
171        e_msg = "fd_w(pixels): %.2f\tcal[0](pixels): %.2f\tTOL=20%%" % (
172                fd_w_pix, ical[0])
173        assert np.isclose(fd_w_pix, ical[0], rtol=0.20), e_msg
174        e_msg = "fd_h(pixels): %.2f\tcal[1](pixels): %.2f\tTOL=20%%" % (
175                fd_h_pix, ical[0])
176        assert np.isclose(fd_h_pix, ical[1], rtol=0.20), e_msg
177
178        # distortion
179        rad_dist = props["android.lens.distortion"]
180        print "android.lens.distortion:", rad_dist
181        e_msg = "%s param(s) found. %d expected." % (len(rad_dist),
182                                                     NUM_DISTORT_PARAMS)
183        assert len(rad_dist) == NUM_DISTORT_PARAMS, e_msg
184        opencv_dist = np.array([rad_dist[0], rad_dist[1],
185                                rad_dist[3], rad_dist[4],
186                                rad_dist[2]])
187        print "dist:", opencv_dist
188        img_raw = cv2.undistort(img_raw, k, opencv_dist)
189    size_raw = img_raw.shape
190    w_raw = size_raw[1]
191    h_raw = size_raw[0]
192    img_name = "%s_%s_w%d_h%d.png" % (NAME, "raw", w_raw, h_raw)
193    its.image.write_image(img_raw, img_name, True)
194    aspect_ratio_gt, cc_ct_gt, circle_size_raw = measure_aspect_ratio(
195            img_raw, img_name, True, debug)
196    raw_fov_percent = calc_circle_image_ratio(
197            circle_size_raw[0], circle_size_raw[1], w_raw, h_raw)
198    ref_fov = {}
199    ref_fov["fmt"] = "RAW"
200    ref_fov["percent"] = raw_fov_percent
201    ref_fov["w"] = w_raw
202    ref_fov["h"] = h_raw
203    ref_fov["circle_w"] = circle_size_raw[0]
204    ref_fov["circle_h"] = circle_size_raw[1]
205    print "Using RAW reference:", ref_fov
206    return ref_fov, cc_ct_gt, aspect_ratio_gt
207
208
209def find_jpeg_fov_reference(cam, req, props):
210    """Determine the circle coverage of the image in JPEG reference image.
211
212    Args:
213        cam:        camera object
214        req:        camera request
215        props:      camera properties
216
217    Returns:
218        ref_fov:    dict with [fmt, % coverage, w, h, circle_w, circle_h]
219        cc_ct_gt:   circle center position relative to the center of image.
220    """
221    ref_fov = {}
222    fmt = its.objects.get_largest_jpeg_format(props)
223    # capture and determine circle area in image
224    cap = cam.do_capture(req, fmt)
225    w = cap["width"]
226    h = cap["height"]
227
228    img = its.image.convert_capture_to_rgb_image(cap, props=props)
229    print "Captured JPEG %dx%d" % (w, h)
230    img_name = "%s_jpeg_w%d_h%d.png" % (NAME, w, h)
231    # Set debug to True to save the reference image
232    _, cc_ct_gt, circle_size = measure_aspect_ratio(img, img_name, False, debug=True)
233    fov_percent = calc_circle_image_ratio(circle_size[0], circle_size[1], w, h)
234    ref_fov["fmt"] = "JPEG"
235    ref_fov["percent"] = fov_percent
236    ref_fov["w"] = w
237    ref_fov["h"] = h
238    ref_fov["circle_w"] = circle_size[0]
239    ref_fov["circle_h"] = circle_size[1]
240    print "Using JPEG reference:", ref_fov
241    return ref_fov, cc_ct_gt
242
243
244def calc_circle_image_ratio(circle_w, circle_h, image_w, image_h):
245    """Calculate the percent of area the input circle covers in input image.
246
247    Args:
248        circle_w (int):      width of circle
249        circle_h (int):      height of circle
250        image_w (int):       width of image
251        image_h (int):       height of image
252    Returns:
253        fov_percent (float): % of image covered by circle
254    """
255    circle_area = math.pi * math.pow(np.mean([circle_w, circle_h])/2.0, 2)
256    image_area = image_w * image_h
257    fov_percent = 100*circle_area/image_area
258    return fov_percent
259
260
261def measure_aspect_ratio(img, img_name, raw_avlb, debug):
262    """Measure the aspect ratio of the black circle in the test image.
263
264    Args:
265        img: Numpy float image array in RGB, with pixel values in [0,1].
266        img_name: string with image info of format and size.
267        raw_avlb: True: raw capture is available; False: raw capture is not
268             available.
269        debug: boolean for whether in debug mode.
270    Returns:
271        aspect_ratio: aspect ratio number in float.
272        cc_ct: circle center position relative to the center of image.
273        (circle_w, circle_h): tuple of the circle size
274    """
275    size = img.shape
276    img *= 255
277    # Gray image
278    img_gray = 0.299*img[:, :, 2] + 0.587*img[:, :, 1] + 0.114*img[:, :, 0]
279
280    # otsu threshold to binarize the image
281    _, img_bw = cv2.threshold(np.uint8(img_gray), 0, 255,
282                              cv2.THRESH_BINARY + cv2.THRESH_OTSU)
283
284    # connected component
285    cv2_version = cv2.__version__
286    if cv2_version.startswith('3.'): # OpenCV 3.x
287        _, contours, hierarchy = cv2.findContours(
288                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
289    else: # OpenCV 2.x and 4.x
290        contours, hierarchy = cv2.findContours(
291                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
292
293    # Check each component and find the black circle
294    min_cmpt = size[0] * size[1] * 0.005
295    max_cmpt = size[0] * size[1] * 0.35
296    num_circle = 0
297    aspect_ratio = 0
298    for ct, hrch in zip(contours, hierarchy[0]):
299        # The radius of the circle is 1/3 of the length of the square, meaning
300        # around 1/3 of the area of the square
301        # Parental component should exist and the area is acceptable.
302        # The coutour of a circle should have at least 5 points
303        child_area = cv2.contourArea(ct)
304        if (hrch[3] == -1 or child_area < min_cmpt or child_area > max_cmpt
305                    or len(ct) < 15):
306            continue
307        # Check the shapes of current component and its parent
308        child_shape = its.cv2image.component_shape(ct)
309        parent = hrch[3]
310        prt_shape = its.cv2image.component_shape(contours[parent])
311        prt_area = cv2.contourArea(contours[parent])
312        dist_x = abs(child_shape["ctx"]-prt_shape["ctx"])
313        dist_y = abs(child_shape["cty"]-prt_shape["cty"])
314        # 1. 0.56*Parent"s width < Child"s width < 0.76*Parent"s width.
315        # 2. 0.56*Parent"s height < Child"s height < 0.76*Parent"s height.
316        # 3. Child"s width > 0.1*Image width
317        # 4. Child"s height > 0.1*Image height
318        # 5. 0.25*Parent"s area < Child"s area < 0.45*Parent"s area
319        # 6. Child == 0, and Parent == 255
320        # 7. Center of Child and center of parent should overlap
321        if (prt_shape["width"] * 0.56 < child_shape["width"]
322                    < prt_shape["width"] * 0.76
323                    and prt_shape["height"] * 0.56 < child_shape["height"]
324                    < prt_shape["height"] * 0.76
325                    and child_shape["width"] > 0.1 * size[1]
326                    and child_shape["height"] > 0.1 * size[0]
327                    and 0.30 * prt_area < child_area < 0.50 * prt_area
328                    and img_bw[child_shape["cty"]][child_shape["ctx"]] == 0
329                    and img_bw[child_shape["top"]][child_shape["left"]] == 255
330                    and dist_x < 0.1 * child_shape["width"]
331                    and dist_y < 0.1 * child_shape["height"]):
332            # If raw capture is not available, check the camera is placed right
333            # in front of the test page:
334            # 1. Distances between parent and child horizontally on both side,0
335            #    dist_left and dist_right, should be close.
336            # 2. Distances between parent and child vertically on both side,
337            #    dist_top and dist_bottom, should be close.
338            if not raw_avlb:
339                dist_left = child_shape["left"] - prt_shape["left"]
340                dist_right = prt_shape["right"] - child_shape["right"]
341                dist_top = child_shape["top"] - prt_shape["top"]
342                dist_bottom = prt_shape["bottom"] - child_shape["bottom"]
343                if (abs(dist_left-dist_right) > 0.05 * child_shape["width"]
344                            or abs(dist_top-dist_bottom) > 0.05 * child_shape["height"]):
345                    continue
346            # Calculate aspect ratio
347            aspect_ratio = float(child_shape["width"]) / child_shape["height"]
348            circle_ctx = child_shape["ctx"]
349            circle_cty = child_shape["cty"]
350            circle_w = float(child_shape["width"])
351            circle_h = float(child_shape["height"])
352            cc_ct = {"hori": float(child_shape["ctx"]-size[1]/2) / circle_w,
353                     "vert": float(child_shape["cty"]-size[0]/2) / circle_h}
354            num_circle += 1
355            # If more than one circle found, break
356            if num_circle == 2:
357                break
358
359    if num_circle == 0:
360        its.image.write_image(img/255, img_name, True)
361        print "No black circle was detected. Please take pictures according",
362        print "to instruction carefully!\n"
363        assert num_circle == 1
364
365    if num_circle > 1:
366        its.image.write_image(img/255, img_name, True)
367        print "More than one black circle was detected. Background of scene",
368        print "may be too complex.\n"
369        assert num_circle == 1
370
371    # draw circle center and image center, and save the image
372    line_width = max(1, max(size)/500)
373    move_text_dist = line_width * 3
374    cv2.line(img, (circle_ctx, circle_cty), (size[1]/2, size[0]/2),
375             (255, 0, 0), line_width)
376    if circle_cty > size[0]/2:
377        move_text_down_circle = 4
378        move_text_down_image = -1
379    else:
380        move_text_down_circle = -1
381        move_text_down_image = 4
382    if circle_ctx > size[1]/2:
383        move_text_right_circle = 2
384        move_text_right_image = -1
385    else:
386        move_text_right_circle = -1
387        move_text_right_image = 2
388    # circle center
389    text_circle_x = move_text_dist * move_text_right_circle + circle_ctx
390    text_circle_y = move_text_dist * move_text_down_circle + circle_cty
391    cv2.circle(img, (circle_ctx, circle_cty), line_width*2, (255, 0, 0), -1)
392    cv2.putText(img, "circle center", (text_circle_x, text_circle_y),
393                cv2.FONT_HERSHEY_SIMPLEX, line_width/2.0, (255, 0, 0),
394                line_width)
395    # image center
396    text_imgct_x = move_text_dist * move_text_right_image + size[1]/2
397    text_imgct_y = move_text_dist * move_text_down_image + size[0]/2
398    cv2.circle(img, (size[1]/2, size[0]/2), line_width*2, (255, 0, 0), -1)
399    cv2.putText(img, "image center", (text_imgct_x, text_imgct_y),
400                cv2.FONT_HERSHEY_SIMPLEX, line_width/2.0, (255, 0, 0),
401                line_width)
402    if debug:
403        its.image.write_image(img/255, img_name, True)
404
405    print "Aspect ratio: %.3f" % aspect_ratio
406    print "Circle center position wrt to image center:",
407    print "%.3fx%.3f" % (cc_ct["vert"], cc_ct["hori"])
408    return aspect_ratio, cc_ct, (circle_w, circle_h)
409
410
411def main():
412    """Test aspect ratio/field of view (FOV)/cropping for each tested formats combinations.
413
414    This test checks for:
415      1. Aspect ratio: images are not stretched
416      2. Crop: center of images is always center of the image sensor no matter
417         how the image is cropped from sensor's full FOV
418      3. FOV: images are always cropped to keep the maximum possible FOV with
419         only one dimension (horizontal or veritical) cropped.
420
421    Aspect ratio and FOV test runs on level3, full and limited devices.
422    Crop test only runs on full and level3 devices.
423
424    The test chart is a black circle inside a black square. When raw capture is
425    available, set the height vs. width ratio of the circle in the full-frame
426    raw as ground truth. In an ideal setup such ratio should be very close to
427    1.0, but here we just use the value derived from full resolution RAW as
428    ground truth to account for the possiblity that the chart is not well
429    positioned to be precisely parallel to image sensor plane.
430    The test then compare the ground truth ratio with the same ratio measured
431    on images captued using different stream combinations of varying formats
432    ("jpeg" and "yuv") and resolutions.
433    If raw capture is unavailable, a full resolution JPEG image is used to setup
434    ground truth. In this case, the ground truth aspect ratio is defined as 1.0
435    and it is the tester's responsibility to make sure the test chart is
436    properly positioned so the detected circles indeed have aspect ratio close
437    to 1.0 assuming no bugs causing image stretched.
438
439    The aspect ratio test checks the aspect ratio of the detected circle and
440    it will fail if the aspect ratio differs too much from the ground truth
441    aspect ratio mentioned above.
442
443    The FOV test examines the ratio between the detected circle area and the
444    image size. When the aspect ratio of the test image is the same as the
445    ground truth image, the ratio should be very close to the ground truth
446    value. When the aspect ratio is different, the difference is factored in
447    per the expectation of the Camera2 API specification, which mandates the
448    FOV reduction from full sensor area must only occur in one dimension:
449    horizontally or vertically, and never both. For example, let's say a sensor
450    has a 16:10 full sensor FOV. For all 16:10 output images there should be no
451    FOV reduction on them. For 16:9 output images the FOV should be vertically
452    cropped by 9/10. For 4:3 output images the FOV should be cropped
453    horizontally instead and the ratio (r) can be calculated as follows:
454        (16 * r) / 10 = 4 / 3 => r = 40 / 48 = 0.8333
455    Say the circle is covering x percent of the 16:10 sensor on the full 16:10
456    FOV, and assume the circle in the center will never be cut in any output
457    sizes (this can be achieved by picking the right size and position of the
458    test circle), the from above cropping expectation we can derive on a 16:9
459    output image the circle will cover (x / 0.9) percent of the 16:9 image; on
460    a 4:3 output image the circle will cover (x / 0.8333) percent of the 4:3
461    image.
462
463    The crop test checks that the center of any output image remains aligned
464    with center of sensor's active area, no matter what kind of cropping or
465    scaling is applied. The test verified that by checking the relative vector
466    from the image center to the center of detected circle remains unchanged.
467    The relative part is normalized by the detected circle size to account for
468    scaling effect.
469    """
470    aspect_ratio_gt = 1.0  # Ground truth circle width/height ratio.
471                           # If full resolution RAW is available as reference
472                           # then this will be updated to the value measured on
473                           # the RAW image. Otherwise a full resolution JPEG
474                           # will be used as reference and this value will be
475                           # 1.0.
476    failed_ar = []  # streams failed the aspect ratio test
477    failed_crop = []  # streams failed the crop test
478    failed_fov = []  # streams that fail FoV test
479    format_list = []  # format list for multiple capture objects.
480
481    # Do multi-capture of "iter" and "cmpr". Iterate through all the
482    # available sizes of "iter", and only use the size specified for "cmpr"
483    # The "cmpr" capture is only used so that we have multiple capture target
484    # instead of just one, which should help catching more potential issues.
485    # The test doesn't look into the output of "cmpr" images at all.
486    # The "iter_max" or "cmpr_size" key defines the maximal size being iterated
487    # or selected for the "iter" and "cmpr" stream accordingly. None means no
488    # upper bound is specified.
489    format_list.append({"iter": "yuv", "iter_max": None,
490                        "cmpr": "yuv", "cmpr_size": PREVIEW_SIZE})
491    format_list.append({"iter": "yuv", "iter_max": PREVIEW_SIZE,
492                        "cmpr": "jpeg", "cmpr_size": None})
493    format_list.append({"iter": "yuv", "iter_max": PREVIEW_SIZE,
494                        "cmpr": "raw", "cmpr_size": None})
495    format_list.append({"iter": "jpeg", "iter_max": None,
496                        "cmpr": "raw", "cmpr_size": None})
497    format_list.append({"iter": "jpeg", "iter_max": None,
498                        "cmpr": "yuv", "cmpr_size": PREVIEW_SIZE})
499    ref_fov = {}  # Reference frame's FOV related information
500                  # If RAW is available a full resolution RAW frame will be used
501                  # as reference frame; otherwise the highest resolution JPEG is used.
502    with its.device.ItsSession() as cam:
503        props = cam.get_camera_properties()
504        fls_logical = props['android.lens.info.availableFocalLengths']
505        print 'logical available focal lengths: %s', str(fls_logical)
506        props = cam.override_with_hidden_physical_camera_props(props)
507        fls_physical = props['android.lens.info.availableFocalLengths']
508        print 'physical available focal lengths: %s', str(fls_physical)
509        # determine skip conditions
510        first_api = its.device.get_first_api_level(its.device.get_device_id())
511        if first_api < 30:  # original constraint
512            its.caps.skip_unless(its.caps.read_3a(props))
513        else:  # loosen from read_3a to enable LIMITED coverage
514            its.caps.skip_unless(its.caps.ae_lock(props) and
515                                 its.caps.awb_lock(props))
516        # determine capabilities
517        full_device = its.caps.full_or_better(props)
518        limited_device = its.caps.limited(props)
519        its.caps.skip_unless(full_device or limited_device)
520        level3_device = its.caps.level3(props)
521        raw_avlb = its.caps.raw16(props)
522        run_crop_test = (level3_device or full_device) and raw_avlb
523        if not run_crop_test:
524            print "Crop test skipped"
525        debug = its.caps.debug_mode()
526        # Converge 3A
527        cam.do_3a()
528        req = its.objects.auto_capture_request()
529
530        # If raw is available and main camera, use it as ground truth.
531        if raw_avlb and (fls_physical == fls_logical):
532            ref_fov, cc_ct_gt, aspect_ratio_gt = find_raw_fov_reference(cam, req, props, debug)
533        else:
534            ref_fov, cc_ct_gt = find_jpeg_fov_reference(cam, req, props)
535
536        if run_crop_test:
537            # Normalize the circle size to 1/4 of the image size, so that
538            # circle size won't affect the crop test result
539            factor_cp_thres = ((min(ref_fov["w"], ref_fov["h"])/4.0) /
540                               max(ref_fov["circle_w"], ref_fov["circle_h"]))
541            thres_l_cp_test = THRESH_L_CP * factor_cp_thres
542            thres_xs_cp_test = THRESH_XS_CP * factor_cp_thres
543
544        # Take pictures of each settings with all the image sizes available.
545        for fmt in format_list:
546            fmt_iter = fmt["iter"]
547            fmt_cmpr = fmt["cmpr"]
548            dual_target = fmt_cmpr is not "none"
549            # Get the size of "cmpr"
550            if dual_target:
551                sizes = its.objects.get_available_output_sizes(
552                        fmt_cmpr, props, fmt["cmpr_size"])
553                if not sizes:  # device might not support RAW
554                    continue
555                size_cmpr = sizes[0]
556            for size_iter in its.objects.get_available_output_sizes(
557                    fmt_iter, props, fmt["iter_max"]):
558                w_iter = size_iter[0]
559                h_iter = size_iter[1]
560                # Skip testing same format/size combination
561                # ITS does not handle that properly now
562                if (dual_target
563                            and w_iter*h_iter == size_cmpr[0]*size_cmpr[1]
564                            and fmt_iter == fmt_cmpr):
565                    continue
566                out_surface = [{"width": w_iter,
567                                "height": h_iter,
568                                "format": fmt_iter}]
569                if dual_target:
570                    out_surface.append({"width": size_cmpr[0],
571                                        "height": size_cmpr[1],
572                                        "format": fmt_cmpr})
573                cap = cam.do_capture(req, out_surface)
574                if dual_target:
575                    frm_iter = cap[0]
576                else:
577                    frm_iter = cap
578                assert frm_iter["format"] == fmt_iter
579                assert frm_iter["width"] == w_iter
580                assert frm_iter["height"] == h_iter
581                print "Captured %s with %s %dx%d. Compared size: %dx%d" % (
582                        fmt_iter, fmt_cmpr, w_iter, h_iter, size_cmpr[0],
583                        size_cmpr[1])
584                img = its.image.convert_capture_to_rgb_image(frm_iter)
585                img_name = "%s_%s_with_%s_w%d_h%d.png" % (NAME,
586                                                          fmt_iter, fmt_cmpr,
587                                                          w_iter, h_iter)
588                aspect_ratio, cc_ct, (cc_w, cc_h) = measure_aspect_ratio(
589                        img, img_name, raw_avlb, debug)
590                # check fov coverage for all fmts in AR_CHECKED
591                fov_percent = calc_circle_image_ratio(
592                        cc_w, cc_h, w_iter, h_iter)
593                chk_percent = calc_expected_circle_image_ratio(ref_fov, w_iter, h_iter)
594                chk_enabled = is_checked_aspect_ratio(first_api, w_iter, h_iter)
595                if chk_enabled and not np.isclose(fov_percent, chk_percent,
596                                                  rtol=FOV_PERCENT_RTOL):
597                    msg = "FoV %%: %.2f, Ref FoV %%: %.2f, " % (
598                            fov_percent, chk_percent)
599                    msg += "TOL=%.f%%, img: %dx%d, ref: %dx%d" % (
600                            FOV_PERCENT_RTOL*100, w_iter, h_iter,
601                            ref_fov["w"], ref_fov["h"])
602                    failed_fov.append(msg)
603                    its.image.write_image(img/255, img_name, True)
604
605                # check pass/fail for aspect ratio
606                # image size: the larger one of image width and height
607                # image size >= LARGE_SIZE: use THRESH_L_AR
608                # image size == 0 (extreme case): THRESH_XS_AR
609                # 0 < image size < LARGE_SIZE: scale between THRESH_XS_AR
610                # and THRESH_L_AR
611                thres_ar_test = max(
612                        THRESH_L_AR, THRESH_XS_AR + max(w_iter, h_iter) *
613                        (THRESH_L_AR-THRESH_XS_AR)/LARGE_SIZE)
614                thres_range_ar = (aspect_ratio_gt-thres_ar_test,
615                                  aspect_ratio_gt+thres_ar_test)
616                if (aspect_ratio < thres_range_ar[0] or
617                            aspect_ratio > thres_range_ar[1]):
618                    failed_ar.append({"fmt_iter": fmt_iter,
619                                      "fmt_cmpr": fmt_cmpr,
620                                      "w": w_iter, "h": h_iter,
621                                      "ar": aspect_ratio,
622                                      "valid_range": thres_range_ar})
623                    its.image.write_image(img/255, img_name, True)
624
625                # check pass/fail for crop
626                if run_crop_test:
627                    # image size >= LARGE_SIZE: use thres_l_cp_test
628                    # image size == 0 (extreme case): thres_xs_cp_test
629                    # 0 < image size < LARGE_SIZE: scale between
630                    # thres_xs_cp_test and thres_l_cp_test
631                    # Also, allow at least THRESH_MIN_PIXEL off to
632                    # prevent threshold being too tight for very
633                    # small circle
634                    thres_hori_cp_test = max(
635                            thres_l_cp_test, thres_xs_cp_test + w_iter *
636                            (thres_l_cp_test-thres_xs_cp_test)/LARGE_SIZE)
637                    min_threshold_h = THRESH_MIN_PIXEL / cc_w
638                    thres_hori_cp_test = max(thres_hori_cp_test,
639                                             min_threshold_h)
640                    thres_range_h_cp = (cc_ct_gt["hori"]-thres_hori_cp_test,
641                                        cc_ct_gt["hori"]+thres_hori_cp_test)
642                    thres_vert_cp_test = max(
643                            thres_l_cp_test, thres_xs_cp_test + h_iter *
644                            (thres_l_cp_test-thres_xs_cp_test)/LARGE_SIZE)
645                    min_threshold_v = THRESH_MIN_PIXEL / cc_h
646                    thres_vert_cp_test = max(thres_vert_cp_test,
647                                             min_threshold_v)
648                    thres_range_v_cp = (cc_ct_gt["vert"]-thres_vert_cp_test,
649                                        cc_ct_gt["vert"]+thres_vert_cp_test)
650                    if (cc_ct["hori"] < thres_range_h_cp[0]
651                                or cc_ct["hori"] > thres_range_h_cp[1]
652                                or cc_ct["vert"] < thres_range_v_cp[0]
653                                or cc_ct["vert"] > thres_range_v_cp[1]):
654                        failed_crop.append({"fmt_iter": fmt_iter,
655                                            "fmt_cmpr": fmt_cmpr,
656                                            "w": w_iter, "h": h_iter,
657                                            "ct_hori": cc_ct["hori"],
658                                            "ct_vert": cc_ct["vert"],
659                                            "valid_range_h": thres_range_h_cp,
660                                            "valid_range_v": thres_range_v_cp})
661                        its.image.write_image(img/255, img_name, True)
662
663        # Print failed any test results
664        print_failed_test_results(failed_ar, failed_fov, failed_crop)
665        assert not failed_ar
666        assert not failed_fov
667        if level3_device:
668            assert not failed_crop
669
670
671if __name__ == "__main__":
672    main()
673