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"""Validate aspect ratio, crop and FoV vs format."""
15
16
17import logging
18import math
19import os.path
20from mobly import test_runner
21import numpy as np
22
23import cv2
24import its_base_test
25import camera_properties_utils
26import capture_request_utils
27import image_processing_utils
28import its_session_utils
29import opencv_processing_utils
30
31_ANDROID11_API_LEVEL = 30
32_CIRCLE_COLOR = 0  # [0: black, 255: white].
33_CIRCLE_MIN_AREA = 0.01  # 1% of image size.
34_FOV_PERCENT_RTOL = 0.15  # Relative tolerance on circle FoV % to expected.
35_LARGE_SIZE = 2000  # Size of a large image (compared against max(w, h)).
36_NAME = os.path.splitext(os.path.basename(__file__))[0]
37_PREVIEW_SIZE = (1920, 1080)
38_THRESH_AR_L = 0.02  # Aspect ratio test threshold of large images.
39_THRESH_AR_S = 0.075  # Aspect ratio test threshold of mini images.
40_THRESH_CROP_L = 0.02  # Crop test threshold of large images.
41_THRESH_CROP_S = 0.075  # Crop test threshold of mini images.
42_THRESH_MIN_PIXEL = 4  # Crop test allowed offset.
43
44# Before API level 30, only resolutions with the following listed aspect ratio
45# are checked. Device launched after API level 30 will need to pass the test
46# for all advertised resolutions. Device launched before API level 30 just
47# needs to pass the test for all resolutions within these aspect ratios.
48_AR_CHECKED_PRE_API_30 = ('4:3', '16:9', '18:9')
49_AR_DIFF_ATOL = 0.01
50
51
52def _check_skip_conditions(first_api_level, props):
53  """Check the skip conditions based on first API level."""
54  if first_api_level < _ANDROID11_API_LEVEL:  # Original constraint.
55    camera_properties_utils.skip_unless(camera_properties_utils.read_3a(props))
56  else:  # Loosen from read_3a to enable LIMITED coverage.
57    camera_properties_utils.skip_unless(
58        camera_properties_utils.ae_lock(props) and
59        camera_properties_utils.awb_lock(props))
60
61
62def _check_basic_correctness(cap, fmt_iter, w_iter, h_iter):
63  """Check the capture for basic correctness."""
64  if cap['format'] != fmt_iter:
65    raise AssertionError
66  if cap['width'] != w_iter:
67    raise AssertionError
68  if cap['height'] != h_iter:
69    raise AssertionError
70
71
72def _create_format_list():
73  """Create format list for multiple capture objects.
74
75  Do multi-capture of 'iter' and 'cmpr'. Iterate through all the available
76  sizes of 'iter', and only use the size specified for 'cmpr'.
77  The 'cmpr' capture is only used so that we have multiple capture target
78  instead of just one, which should help catching more potential issues.
79  The test doesn't look into the output of 'cmpr' images at all.
80  The 'iter_max' or 'cmpr_size' key defines the maximal size being iterated
81  or selected for the 'iter' and 'cmpr' stream accordingly. None means no
82  upper bound is specified.
83
84  Args:
85    None
86
87  Returns:
88    format_list
89  """
90  format_list = []
91  format_list.append({'iter': 'yuv', 'iter_max': None,
92                      'cmpr': 'yuv', 'cmpr_size': _PREVIEW_SIZE})
93  format_list.append({'iter': 'yuv', 'iter_max': _PREVIEW_SIZE,
94                      'cmpr': 'jpeg', 'cmpr_size': None})
95  format_list.append({'iter': 'yuv', 'iter_max': _PREVIEW_SIZE,
96                      'cmpr': 'raw', 'cmpr_size': None})
97  format_list.append({'iter': 'jpeg', 'iter_max': None,
98                      'cmpr': 'raw', 'cmpr_size': None})
99  format_list.append({'iter': 'jpeg', 'iter_max': None,
100                      'cmpr': 'yuv', 'cmpr_size': _PREVIEW_SIZE})
101  return format_list
102
103
104def _print_failed_test_results(failed_ar, failed_fov, failed_crop,
105                               first_api_level, level_3):
106  """Print failed test results."""
107  if failed_ar:
108    logging.error('Aspect ratio test summary')
109    logging.error('Images failed in the aspect ratio test:')
110    logging.error('Aspect ratio value: width / height')
111    for fa in failed_ar:
112      logging.error('%s', fa)
113
114  if failed_fov:
115    logging.error('FoV test summary')
116    logging.error('Images failed in the FoV test:')
117    for fov in failed_fov:
118      logging.error('%s', str(fov))
119
120  if failed_crop:
121    logging.error('Crop test summary')
122    logging.error('Images failed in the crop test:')
123    logging.error('Circle center (H x V) relative to the image center.')
124    for fc in failed_crop:
125      logging.error('%s', fc)
126  if failed_ar:
127    raise RuntimeError
128  if failed_fov:
129    raise RuntimeError
130  if first_api_level > _ANDROID11_API_LEVEL:
131    if failed_crop:  # failed_crop = [] if run_crop_test = False.
132      raise RuntimeError
133  else:
134    if failed_crop and level_3:
135      raise RuntimeError
136
137
138def _is_checked_aspect_ratio(first_api_level, w, h):
139  """Determine if format aspect ratio is a checked on based of first_API."""
140  if first_api_level >= _ANDROID11_API_LEVEL:
141    return True
142
143  for ar_check in _AR_CHECKED_PRE_API_30:
144    match_ar_list = [float(x) for x in ar_check.split(':')]
145    match_ar = match_ar_list[0] / match_ar_list[1]
146    if np.isclose(float(w) / h, match_ar, atol=_AR_DIFF_ATOL):
147      return True
148
149  return False
150
151
152def _calc_expected_circle_image_ratio(ref_fov, img_w, img_h):
153  """Determine the circle image area ratio in percentage for a given image size.
154
155  Cropping happens either horizontally or vertically. In both cases crop results
156  in the visble area reduced by a ratio r (r < 1) and the circle will in turn
157  occupy ref_pct/r (percent) on the target image size.
158
159  Args:
160    ref_fov: dict with {fmt, % coverage, w, h, circle_w, circle_h}
161    img_w: the image width
162    img_h: the image height
163
164  Returns:
165    chk_percent: the expected circle image area ratio in percentage
166  """
167  ar_ref = ref_fov['w'] / ref_fov['h']
168  ar_target = img_w / img_h
169
170  r = ar_ref / ar_target
171  if r < 1.0:
172    r = 1.0 / r
173  return ref_fov['percent'] * r
174
175
176def _find_raw_fov_reference(cam, req, props, log_path):
177  """Determine the circle coverage of the image in RAW reference image.
178
179  Captures a full-frame RAW and uses its aspect ratio and circle center
180  location as ground truth for the other jpeg or yuv images.
181
182  The intrinsics and distortion coefficients are meant for full-sized RAW,
183  so convert_capture_to_rgb_image returns a 2x downsampled version, so resizes
184  RGB back to full size.
185
186  If the device supports lens distortion correction, applies the coefficients on
187  the RAW image so it can be compared to YUV/JPEG outputs which are subject
188  to the same correction via ISP.
189
190  Finds circle size and location for reference values in calculations for other
191  formats.
192
193  Args:
194    cam: camera object
195    req: camera request
196    props: camera properties
197    log_path: location to save data
198
199  Returns:
200    ref_fov: dict with [fmt, % coverage, w, h, circle_w, circle_h]
201    cc_ct_gt: circle center position relative to the center of image.
202    aspect_ratio_gt: aspect ratio of the detected circle in float.
203  """
204  logging.debug('Creating references for fov_coverage from RAW')
205  out_surface = {'format': 'raw'}
206  cap_raw = cam.do_capture(req, out_surface)
207  logging.debug('Captured RAW %dx%d', cap_raw['width'], cap_raw['height'])
208  img_raw = image_processing_utils.convert_capture_to_rgb_image(
209      cap_raw, props=props)
210  # Resize back up to full scale.
211  img_raw = cv2.resize(img_raw, (0, 0), fx=2.0, fy=2.0)
212
213  if (camera_properties_utils.distortion_correction(props) and
214      camera_properties_utils.intrinsic_calibration(props)):
215    logging.debug('Applying intrinsic calibration and distortion params')
216    fd = float(cap_raw['metadata']['android.lens.focalLength'])
217    k = camera_properties_utils.get_intrinsic_calibration(props, True, fd)
218    opencv_dist = camera_properties_utils.get_distortion_matrix(props)
219    img_raw = cv2.undistort(img_raw, k, opencv_dist)
220
221  # Get image size.
222  size_raw = img_raw.shape
223  w_raw = size_raw[1]
224  h_raw = size_raw[0]
225  img_name = '%s_%s_w%d_h%d.png' % (
226      os.path.join(log_path, _NAME), 'raw', w_raw, h_raw)
227  image_processing_utils.write_image(img_raw, img_name, True)
228
229  # Find circle.
230  img_raw *= 255  # cv2 needs images between [0,255].
231  circle_raw = opencv_processing_utils.find_circle(
232      img_raw, img_name, _CIRCLE_MIN_AREA, _CIRCLE_COLOR)
233  opencv_processing_utils.append_circle_center_to_img(circle_raw, img_raw,
234                                                      img_name)
235
236  # Determine final return values.
237  aspect_ratio_gt = circle_raw['w'] / circle_raw['h']
238  cc_ct_gt = {'hori': circle_raw['x_offset'], 'vert': circle_raw['y_offset']}
239  raw_fov_percent = _calc_circle_image_ratio(circle_raw['r'], w_raw, h_raw)
240  ref_fov = {}
241  ref_fov['fmt'] = 'RAW'
242  ref_fov['percent'] = raw_fov_percent
243  ref_fov['w'] = w_raw
244  ref_fov['h'] = h_raw
245  ref_fov['circle_w'] = circle_raw['w']
246  ref_fov['circle_h'] = circle_raw['h']
247  logging.debug('Using RAW reference: %s', str(ref_fov))
248  return ref_fov, cc_ct_gt, aspect_ratio_gt
249
250
251def _find_jpeg_fov_reference(cam, req, props, log_path):
252  """Determine the circle coverage of the image in JPEG reference image.
253
254  Similar to _find_raw_fov_reference() and used when RAW is not available.
255
256  Args:
257    cam: camera object
258    req: camera request
259    props: camera properties
260    log_path: location to save data
261
262  Returns:
263    ref_fov: dict with [fmt, % coverage, w, h, circle_w, circle_h]
264    cc_ct_gt: circle center position relative to the center of image.
265  """
266  ref_fov = {}
267  fmt = capture_request_utils.get_largest_jpeg_format(props)
268  # Capture and determine circle area in image.
269  cap = cam.do_capture(req, fmt)
270  w = cap['width']
271  h = cap['height']
272
273  img = image_processing_utils.convert_capture_to_rgb_image(cap, props)
274  img *= 255  # cv2 works with [0,255] images.
275  logging.debug('Captured JPEG %dx%d', w, h)
276  img_name = '%s_jpeg_w%d_h%d.png' % (os.path.join(log_path, _NAME), w, h)
277  circle_jpg = opencv_processing_utils.find_circle(
278      img, img_name, _CIRCLE_MIN_AREA, _CIRCLE_COLOR)
279  opencv_processing_utils.append_circle_center_to_img(circle_jpg, img,
280                                                      img_name)
281
282  # Determine final return values.
283  cc_ct_gt = {'hori': circle_jpg['x_offset'], 'vert': circle_jpg['y_offset']}
284  fov_percent = _calc_circle_image_ratio(circle_jpg['r'], w, h)
285  ref_fov = {}
286  ref_fov['fmt'] = 'JPEG'
287  ref_fov['percent'] = fov_percent
288  ref_fov['w'] = w
289  ref_fov['h'] = h
290  ref_fov['circle_w'] = circle_jpg['w']
291  ref_fov['circle_h'] = circle_jpg['h']
292  logging.debug('Using JPEG reference: %s', str(ref_fov))
293  return ref_fov, cc_ct_gt
294
295
296def _calc_circle_image_ratio(radius, img_w, img_h):
297  """Calculate the percent of area the input circle covers in input image.
298
299  Args:
300    radius: radius of circle
301    img_w: int width of image
302    img_h: int height of image
303  Returns:
304    fov_percent: float % of image covered by circle
305  """
306  return 100 * math.pi * math.pow(radius, 2) / (img_w * img_h)
307
308
309def _check_fov(circle, ref_fov, w, h, first_api_level):
310  """Check the FoV for correct size."""
311  fov_percent = _calc_circle_image_ratio(circle['r'], w, h)
312  chk_percent = _calc_expected_circle_image_ratio(ref_fov, w, h)
313  chk_enabled = _is_checked_aspect_ratio(first_api_level, w, h)
314  if chk_enabled and not np.isclose(fov_percent, chk_percent,
315                                    rtol=_FOV_PERCENT_RTOL):
316    e_msg = 'FoV %%: %.2f, Ref FoV %%: %.2f, ' % (fov_percent, chk_percent)
317    e_msg += 'TOL=%.f%%, img: %dx%d, ref: %dx%d' % (
318        _FOV_PERCENT_RTOL*100, w, h, ref_fov['w'], ref_fov['h'])
319    return e_msg
320
321
322def _check_ar(circle, ar_gt, w, h, fmt_iter, fmt_cmpr):
323  """Check the aspect ratio of the circle.
324
325  size is the larger of w or h.
326  if size >= LARGE_SIZE: use THRESH_AR_L
327  elif size == 0 (extreme case): THRESH_AR_S
328  elif 0 < image size < LARGE_SIZE: scale between THRESH_AR_S & THRESH_AR_L
329
330  Args:
331    circle: dict with circle parameters
332    ar_gt: aspect ratio ground truth to compare against
333    w: width of image
334    h: height of image
335    fmt_iter: format of primary capture
336    fmt_cmpr: format of secondary capture
337
338  Returns:
339    error string if check fails
340  """
341  thresh_ar = max(_THRESH_AR_L, _THRESH_AR_S +
342                  max(w, h) * (_THRESH_AR_L-_THRESH_AR_S) / _LARGE_SIZE)
343  ar = circle['w'] / circle['h']
344  if not np.isclose(ar, ar_gt, atol=thresh_ar):
345    e_msg = (f'{fmt_iter} with {fmt_cmpr} {w}x{h}: aspect_ratio {ar:.3f}, '
346             f'thresh {thresh_ar:.3f}')
347    return e_msg
348
349
350def _check_crop(circle, cc_gt, w, h, fmt_iter, fmt_cmpr, crop_thresh_factor):
351  """Check cropping.
352
353  if size >= LARGE_SIZE: use thresh_crop_l
354  elif size == 0 (extreme case): thresh_crop_s
355  elif 0 < size < LARGE_SIZE: scale between thresh_crop_s & thresh_crop_l
356  Also allow at least THRESH_MIN_PIXEL to prevent threshold being too tight
357  for very small circle.
358
359  Args:
360    circle: dict of circle values
361    cc_gt: circle center {'hori', 'vert'}  ground truth (ref'd to img center)
362    w: width of image
363    h: height of image
364    fmt_iter: format of primary capture
365    fmt_cmpr: format of secondary capture
366    crop_thresh_factor: scaling factor for crop thresholds
367
368  Returns:
369    error string if check fails
370  """
371  thresh_crop_l = _THRESH_CROP_L * crop_thresh_factor
372  thresh_crop_s = _THRESH_CROP_S * crop_thresh_factor
373  thresh_crop_hori = max(
374      [thresh_crop_l,
375       thresh_crop_s + w * (thresh_crop_l - thresh_crop_s) / _LARGE_SIZE,
376       _THRESH_MIN_PIXEL / circle['w']])
377  thresh_crop_vert = max(
378      [thresh_crop_l,
379       thresh_crop_s + h * (thresh_crop_l - thresh_crop_s) / _LARGE_SIZE,
380       _THRESH_MIN_PIXEL / circle['h']])
381
382  if (not np.isclose(circle['x_offset'], cc_gt['hori'],
383                     atol=thresh_crop_hori) or
384      not np.isclose(circle['y_offset'], cc_gt['vert'],
385                     atol=thresh_crop_vert)):
386    valid_x_range = (cc_gt['hori'] - thresh_crop_hori,
387                     cc_gt['hori'] + thresh_crop_hori)
388    valid_y_range = (cc_gt['vert'] - thresh_crop_vert,
389                     cc_gt['vert'] + thresh_crop_vert)
390    e_msg = (f'{fmt_iter} with {fmt_cmpr} {w}x{h} '
391             f"offset X {circle['x_offset']:.3f}, Y {circle['y_offset']:.3f}, "
392             f'valid X range: {valid_x_range[0]:.3f} ~ {valid_x_range[1]:.3f}, '
393             f'valid Y range: {valid_y_range[0]:.3f} ~ {valid_y_range[1]:.3f}')
394    return e_msg
395
396
397class AspectRatioAndCropTest(its_base_test.ItsBaseTest):
398  """Test aspect ratio/field of view/cropping for each tested fmt combinations.
399
400  This test checks for:
401    1. Aspect ratio: images are not stretched
402    2. Crop: center of images is not shifted
403    3. FOV: images cropped to keep maximum possible FOV with only 1 dimension
404       (horizontal or veritical) cropped.
405
406  Aspect ratio and FOV test runs on level3, full and limited devices.
407  Crop test only runs on level3 and full devices.
408
409  The test chart is a black circle inside a black square. When raw capture is
410  available, set the height vs. width ratio of the circle in the full-frame
411  raw as ground truth. In an ideal setup such ratio should be very close to
412  1.0, but here we just use the value derived from full resolution RAW as
413  ground truth to account for the possibility that the chart is not well
414  positioned to be precisely parallel to image sensor plane.
415  The test then compares the ground truth ratio with the same ratio measured
416  on images captued using different stream combinations of varying formats
417  ('jpeg' and 'yuv') and resolutions.
418  If raw capture is unavailable, a full resolution JPEG image is used to setup
419  ground truth. In this case, the ground truth aspect ratio is defined as 1.0
420  and it is the tester's responsibility to make sure the test chart is
421  properly positioned so the detected circles indeed have aspect ratio close
422  to 1.0 assuming no bugs causing image stretched.
423
424  The aspect ratio test checks the aspect ratio of the detected circle and
425  it will fail if the aspect ratio differs too much from the ground truth
426  aspect ratio mentioned above.
427
428  The FOV test examines the ratio between the detected circle area and the
429  image size. When the aspect ratio of the test image is the same as the
430  ground truth image, the ratio should be very close to the ground truth
431  value. When the aspect ratio is different, the difference is factored in
432  per the expectation of the Camera2 API specification, which mandates the
433  FOV reduction from full sensor area must only occur in one dimension:
434  horizontally or vertically, and never both. For example, let's say a sensor
435  has a 16:10 full sensor FOV. For all 16:10 output images there should be no
436  FOV reduction on them. For 16:9 output images the FOV should be vertically
437  cropped by 9/10. For 4:3 output images the FOV should be cropped
438  horizontally instead and the ratio (r) can be calculated as follows:
439      (16 * r) / 10 = 4 / 3 => r = 40 / 48 = 0.8333
440  Say the circle is covering x percent of the 16:10 sensor on the full 16:10
441  FOV, and assume the circle in the center will never be cut in any output
442  sizes (this can be achieved by picking the right size and position of the
443  test circle), the from above cropping expectation we can derive on a 16:9
444  output image the circle will cover (x / 0.9) percent of the 16:9 image; on
445  a 4:3 output image the circle will cover (x / 0.8333) percent of the 4:3
446  image.
447
448  The crop test checks that the center of any output image remains aligned
449  with center of sensor's active area, no matter what kind of cropping or
450  scaling is applied. The test verifies that by checking the relative vector
451  from the image center to the center of detected circle remains unchanged.
452  The relative part is normalized by the detected circle size to account for
453  scaling effect.
454  """
455
456  def test_aspect_ratio_and_crop(self):
457    logging.debug('Starting %s', _NAME)
458    failed_ar = []  # Streams failed the aspect ratio test.
459    failed_crop = []  # Streams failed the crop test.
460    failed_fov = []  # Streams that fail FoV test.
461    format_list = _create_format_list()
462
463    with its_session_utils.ItsSession(
464        device_id=self.dut.serial,
465        camera_id=self.camera_id,
466        hidden_physical_id=self.hidden_physical_id) as cam:
467      props = cam.get_camera_properties()
468      fls_logical = props['android.lens.info.availableFocalLengths']
469      logging.debug('logical available focal lengths: %s', str(fls_logical))
470      props = cam.override_with_hidden_physical_camera_props(props)
471      fls_physical = props['android.lens.info.availableFocalLengths']
472      logging.debug('physical available focal lengths: %s', str(fls_physical))
473      log_path = self.log_path
474
475      # Check SKIP conditions.
476      first_api_level = its_session_utils.get_first_api_level(self.dut.serial)
477      _check_skip_conditions(first_api_level, props)
478
479      # Load chart for scene.
480      its_session_utils.load_scene(
481          cam, props, self.scene, self.tablet, self.chart_distance)
482
483      # Determine camera capabilities.
484      full_or_better = camera_properties_utils.full_or_better(props)
485      level3 = camera_properties_utils.level3(props)
486      raw_avlb = camera_properties_utils.raw16(props)
487      debug = self.debug_mode
488
489      # Converge 3A.
490      cam.do_3a()
491      req = capture_request_utils.auto_capture_request()
492
493      # If raw is available and main camera, use it as ground truth.
494      if raw_avlb and (fls_physical == fls_logical):
495        ref_fov, cc_ct_gt, aspect_ratio_gt = _find_raw_fov_reference(
496            cam, req, props, log_path)
497      else:
498        aspect_ratio_gt = 1.0  # Ground truth circle width/height ratio.
499        ref_fov, cc_ct_gt = _find_jpeg_fov_reference(cam, req, props, log_path)
500
501      run_crop_test = full_or_better and raw_avlb
502      if run_crop_test:
503        # Normalize the circle size to 1/4 of the image size, so that
504        # circle size won't affect the crop test result
505        crop_thresh_factor = ((min(ref_fov['w'], ref_fov['h']) / 4.0) /
506                              max(ref_fov['circle_w'], ref_fov['circle_h']))
507      else:
508        logging.debug('Crop test skipped')
509
510      # Take pictures of each settings with all the image sizes available.
511      for fmt in format_list:
512        fmt_iter = fmt['iter']
513        fmt_cmpr = fmt['cmpr']
514        # Get the size of 'cmpr'.
515        sizes = capture_request_utils.get_available_output_sizes(
516            fmt_cmpr, props, fmt['cmpr_size'])
517        if not sizes:  # Device might not support RAW.
518          continue
519        w_cmpr, h_cmpr = sizes[0][0], sizes[0][1]
520        for size_iter in capture_request_utils.get_available_output_sizes(
521            fmt_iter, props, fmt['iter_max']):
522          w_iter, h_iter = size_iter[0], size_iter[1]
523          # Skip same format/size combination: ITS doesn't handle that properly.
524          if w_iter*h_iter == w_cmpr*h_cmpr and fmt_iter == fmt_cmpr:
525            continue
526          out_surface = [{'width': w_iter, 'height': h_iter,
527                          'format': fmt_iter}]
528          out_surface.append({'width': w_cmpr, 'height': h_cmpr,
529                              'format': fmt_cmpr})
530
531          cap = cam.do_capture(req, out_surface)[0]
532          _check_basic_correctness(cap, fmt_iter, w_iter, h_iter)
533          logging.debug('Captured %s with %s %dx%d. Compared size: %dx%d',
534                        fmt_iter, fmt_cmpr, w_iter, h_iter, w_cmpr, h_cmpr)
535          img = image_processing_utils.convert_capture_to_rgb_image(cap)
536          img *= 255  # cv2 uses [0, 255].
537          img_name = '%s_%s_with_%s_w%d_h%d.png' % (
538              os.path.join(log_path, _NAME), fmt_iter, fmt_cmpr, w_iter, h_iter)
539          circle = opencv_processing_utils.find_circle(
540              img, img_name, _CIRCLE_MIN_AREA, _CIRCLE_COLOR)
541          if debug:
542            opencv_processing_utils.append_circle_center_to_img(circle, img,
543                                                                img_name)
544
545          # Check pass/fail for fov coverage for all fmts in AR_CHECKED
546          img /= 255  # image_processing_utils uses [0, 1].
547          fov_chk_msg = _check_fov(circle, ref_fov, w_iter, h_iter,
548                                   first_api_level)
549          if fov_chk_msg:
550            failed_fov.append(fov_chk_msg)
551            image_processing_utils.write_image(img, img_name, True)
552
553          # Check pass/fail for aspect ratio.
554          ar_chk_msg = _check_ar(
555              circle, aspect_ratio_gt, w_iter, h_iter, fmt_iter, fmt_cmpr)
556          if ar_chk_msg:
557            failed_ar.append(ar_chk_msg)
558            image_processing_utils.write_image(img, img_name, True)
559
560          # Check pass/fail for crop.
561          if run_crop_test:
562            crop_chk_msg = _check_crop(circle, cc_ct_gt, w_iter, h_iter,
563                                       fmt_iter, fmt_cmpr, crop_thresh_factor)
564            if crop_chk_msg:
565              failed_crop.append(crop_chk_msg)
566              image_processing_utils.write_image(img, img_name, True)
567
568      # Print any failed test results.
569      _print_failed_test_results(failed_ar, failed_fov, failed_crop,
570                                 first_api_level, level3)
571
572if __name__ == '__main__':
573  test_runner.main()
574