| # Copyright 2015 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Validate aspect ratio, crop and FoV vs format.""" |
| |
| |
| import logging |
| import math |
| import os.path |
| from mobly import test_runner |
| |
| import its_base_test |
| import camera_properties_utils |
| import capture_request_utils |
| import image_fov_utils |
| import image_processing_utils |
| import its_session_utils |
| import opencv_processing_utils |
| |
| _ANDROID11_API_LEVEL = 30 |
| _NAME = os.path.splitext(os.path.basename(__file__))[0] |
| _SIZE_PREVIEW = (1920, 1080) |
| _SIZE_PREVIEW_4x3 = (1440, 1080) |
| _SIZE_VGA = (640, 480) |
| _SIZES_COMMON = ( |
| (1920, 1080), |
| (1440, 1080), |
| (1280, 720), |
| (960, 720), |
| (640, 480), |
| ) |
| |
| |
| # Before API level 30, only resolutions with the following listed aspect ratio |
| # are checked. Device launched after API level 30 will need to pass the test |
| # for all advertised resolutions. Device launched before API level 30 just |
| # needs to pass the test for all resolutions within these aspect ratios. |
| _AR_CHECKED_PRE_API_30 = ('4:3', '16:9', '18:9') |
| _AR_DIFF_ATOL = 0.01 |
| # If RAW reference capture aspect ratio is ~4:3 or ~16:9, use JPEG, else RAW |
| _AR_FOR_JPEG_REFERENCE = (4/3, 16/9) |
| |
| |
| def _check_skip_conditions(first_api_level, props): |
| """Check the skip conditions based on first API level.""" |
| if first_api_level < _ANDROID11_API_LEVEL: # Original constraint. |
| camera_properties_utils.skip_unless(camera_properties_utils.read_3a(props)) |
| else: # Loosen from read_3a to enable LIMITED coverage. |
| camera_properties_utils.skip_unless( |
| camera_properties_utils.ae_lock(props) and |
| camera_properties_utils.awb_lock(props)) |
| |
| |
| def _check_basic_correctness(cap, fmt_iter, w_iter, h_iter): |
| """Check the capture for basic correctness.""" |
| if cap['format'] != fmt_iter: |
| raise AssertionError |
| if cap['width'] != w_iter: |
| raise AssertionError |
| if cap['height'] != h_iter: |
| raise AssertionError |
| |
| |
| def _create_format_list(): |
| """Create format list for multiple capture objects. |
| |
| Do multi-capture of 'iter' and 'cmpr'. Iterate through all the available |
| sizes of 'iter', and only use the size specified for 'cmpr'. |
| The 'cmpr' capture is only used so that we have multiple capture target |
| instead of just one, which should help catching more potential issues. |
| The test doesn't look into the output of 'cmpr' images at all. |
| The 'iter_max' or 'cmpr_size' key defines the maximal size being iterated |
| or selected for the 'iter' and 'cmpr' stream accordingly. None means no |
| upper bound is specified. |
| |
| Args: |
| None |
| |
| Returns: |
| format_list |
| """ |
| format_list = [] |
| format_list.append({'iter': 'jpeg_r', 'iter_max': None, |
| 'cmpr': 'priv', 'cmpr_size': _SIZE_PREVIEW}) |
| format_list.append({'iter': 'yuv', 'iter_max': None, |
| 'cmpr': 'yuv', 'cmpr_size': _SIZE_PREVIEW}) |
| format_list.append({'iter': 'yuv', 'iter_max': _SIZE_PREVIEW, |
| 'cmpr': 'jpeg', 'cmpr_size': None}) |
| format_list.append({'iter': 'yuv', 'iter_max': _SIZE_PREVIEW, |
| 'cmpr': 'raw', 'cmpr_size': None}) |
| format_list.append({'iter': 'jpeg', 'iter_max': None, |
| 'cmpr': 'raw', 'cmpr_size': None}) |
| format_list.append({'iter': 'jpeg', 'iter_max': None, |
| 'cmpr': 'yuv', 'cmpr_size': _SIZE_PREVIEW}) |
| format_list.append({'iter': 'yuv', 'iter_max': None, |
| 'cmpr': 'priv', 'cmpr_size': _SIZE_PREVIEW}) |
| format_list.append({'iter': 'yuv', 'iter_max': None, |
| 'cmpr': 'priv', 'cmpr_size': _SIZE_PREVIEW_4x3}) |
| format_list.append({'iter': 'yuv', 'iter_max': _SIZE_VGA, |
| 'cmpr': 'priv', 'cmpr_size': _SIZE_PREVIEW, |
| 'third': 'yuv', 'third_size': _SIZE_PREVIEW}) |
| return format_list |
| |
| |
| def _print_failed_test_results(failed_ar, failed_fov, failed_crop, |
| first_api_level, level_3): |
| """Print failed test results.""" |
| if failed_ar: |
| logging.error('Aspect ratio test summary') |
| logging.error('Images failed in the aspect ratio test:') |
| logging.error('Aspect ratio value: width / height') |
| for fa in failed_ar: |
| logging.error('%s', fa) |
| |
| if failed_fov: |
| logging.error('FoV test summary') |
| logging.error('Images failed in the FoV test:') |
| for fov in failed_fov: |
| logging.error('%s', str(fov)) |
| |
| if failed_crop: |
| logging.error('Crop test summary') |
| logging.error('Images failed in the crop test:') |
| logging.error('Circle center (H x V) relative to the image center.') |
| for fc in failed_crop: |
| logging.error('%s', fc) |
| if failed_ar: |
| raise RuntimeError |
| if failed_fov: |
| raise RuntimeError |
| if first_api_level > _ANDROID11_API_LEVEL: |
| if failed_crop: # failed_crop = [] if run_crop_test = False. |
| raise RuntimeError |
| else: |
| if failed_crop and level_3: |
| raise RuntimeError |
| |
| |
| def _is_checked_aspect_ratio(first_api_level, w, h): |
| """Determine if format aspect ratio is a checked on based of first_API.""" |
| if first_api_level >= _ANDROID11_API_LEVEL: |
| return True |
| |
| for ar_check in _AR_CHECKED_PRE_API_30: |
| match_ar_list = [float(x) for x in ar_check.split(':')] |
| match_ar = match_ar_list[0] / match_ar_list[1] |
| if math.isclose(w / h, match_ar, abs_tol=_AR_DIFF_ATOL): |
| return True |
| |
| return False |
| |
| |
| class AspectRatioAndCropTest(its_base_test.ItsBaseTest): |
| """Test aspect ratio/field of view/cropping for each tested fmt combinations. |
| |
| This test checks for: |
| 1. Aspect ratio: images are not stretched |
| 2. Crop: center of images is not shifted |
| 3. FOV: images cropped to keep maximum possible FOV with only 1 dimension |
| (horizontal or veritical) cropped. |
| |
| Aspect ratio and FOV test runs on level3, full and limited devices. |
| Crop test only runs on level3 and full devices. |
| |
| The test chart is a black circle inside a black square. When raw capture is |
| available, set the height vs. width ratio of the circle in the full-frame |
| raw as ground truth. In an ideal setup such ratio should be very close to |
| 1.0, but here we just use the value derived from full resolution RAW as |
| ground truth to account for the possibility that the chart is not well |
| positioned to be precisely parallel to image sensor plane. |
| The test then compares the ground truth ratio with the same ratio measured |
| on images captured using different stream combinations of varying formats |
| ('jpeg' and 'yuv') and resolutions. |
| If raw capture is unavailable, a full resolution JPEG image is used to setup |
| ground truth. In this case, the ground truth aspect ratio is defined as 1.0 |
| and it is the tester's responsibility to make sure the test chart is |
| properly positioned so the detected circles indeed have aspect ratio close |
| to 1.0 assuming no bugs causing image stretched. |
| |
| The aspect ratio test checks the aspect ratio of the detected circle and |
| it will fail if the aspect ratio differs too much from the ground truth |
| aspect ratio mentioned above. |
| |
| The FOV test examines the ratio between the detected circle area and the |
| image size. When the aspect ratio of the test image is the same as the |
| ground truth image, the ratio should be very close to the ground truth |
| value. When the aspect ratio is different, the difference is factored in |
| per the expectation of the Camera2 API specification, which mandates the |
| FOV reduction from full sensor area must only occur in one dimension: |
| horizontally or vertically, and never both. For example, let's say a sensor |
| has a 16:10 full sensor FOV. For all 16:10 output images there should be no |
| FOV reduction on them. For 16:9 output images the FOV should be vertically |
| cropped by 9/10. For 4:3 output images the FOV should be cropped |
| horizontally instead and the ratio (r) can be calculated as follows: |
| (16 * r) / 10 = 4 / 3 => r = 40 / 48 = 0.8333 |
| Say the circle is covering x percent of the 16:10 sensor on the full 16:10 |
| FOV, and assume the circle in the center will never be cut in any output |
| sizes (this can be achieved by picking the right size and position of the |
| test circle), the from above cropping expectation we can derive on a 16:9 |
| output image the circle will cover (x / 0.9) percent of the 16:9 image; on |
| a 4:3 output image the circle will cover (x / 0.8333) percent of the 4:3 |
| image. |
| |
| The crop test checks that the center of any output image remains aligned |
| with center of sensor's active area, no matter what kind of cropping or |
| scaling is applied. The test verifies that by checking the relative vector |
| from the image center to the center of detected circle remains unchanged. |
| The relative part is normalized by the detected circle size to account for |
| scaling effect. |
| """ |
| |
| def test_aspect_ratio_and_crop(self): |
| logging.debug('Starting %s', _NAME) |
| failed_ar = [] # Streams failed the aspect ratio test. |
| failed_crop = [] # Streams failed the crop test. |
| failed_fov = [] # Streams that fail FoV test. |
| format_list = _create_format_list() |
| |
| with its_session_utils.ItsSession( |
| device_id=self.dut.serial, |
| camera_id=self.camera_id, |
| hidden_physical_id=self.hidden_physical_id) as cam: |
| props = cam.get_camera_properties() |
| fls_logical = props['android.lens.info.availableFocalLengths'] |
| logging.debug('logical available focal lengths: %s', str(fls_logical)) |
| props = cam.override_with_hidden_physical_camera_props(props) |
| fls_physical = props['android.lens.info.availableFocalLengths'] |
| logging.debug('physical available focal lengths: %s', str(fls_physical)) |
| logging.debug('minimum focus distance (diopters): %.2f', |
| props['android.lens.info.minimumFocusDistance']) |
| name_with_log_path = os.path.join(self.log_path, _NAME) |
| if self.hidden_physical_id: |
| logging.debug('Testing camera: %s.%s', |
| self.camera_id, self.hidden_physical_id) |
| |
| # Check SKIP conditions. |
| first_api_level = its_session_utils.get_first_api_level(self.dut.serial) |
| _check_skip_conditions(first_api_level, props) |
| |
| # Load chart for scene. |
| its_session_utils.load_scene( |
| cam, props, self.scene, self.tablet, self.chart_distance) |
| |
| # Determine camera capabilities. |
| full_or_better = camera_properties_utils.full_or_better(props) |
| level3 = camera_properties_utils.level3(props) |
| raw_avlb = camera_properties_utils.raw16(props) |
| |
| # Converge 3A. |
| if camera_properties_utils.manual_sensor(props): |
| logging.debug('Manual sensor, using manual capture request') |
| s, e, _, _, f_d = cam.do_3a(get_results=True) |
| req = capture_request_utils.manual_capture_request( |
| s, e, f_distance=f_d) |
| else: |
| logging.debug('Using auto capture request') |
| cam.do_3a() |
| req = capture_request_utils.auto_capture_request() |
| |
| # For main camera: if RAW available, use it as ground truth, else JPEG |
| # For physical sub-camera: if RAW available, only use if not 4:3 or 16:9 |
| use_raw_fov = False |
| if raw_avlb: |
| pixel_array_w = props['android.sensor.info.pixelArraySize']['width'] |
| pixel_array_h = props['android.sensor.info.pixelArraySize']['height'] |
| logging.debug('Pixel array size: %dx%d', pixel_array_w, pixel_array_h) |
| raw_aspect_ratio = pixel_array_w / pixel_array_h |
| use_raw_fov = ( |
| fls_physical == fls_logical or not |
| any(math.isclose(raw_aspect_ratio, jpeg_ar, abs_tol=_AR_DIFF_ATOL) |
| for jpeg_ar in _AR_FOR_JPEG_REFERENCE) |
| ) |
| |
| ref_fov, cc_ct_gt, aspect_ratio_gt = ( |
| image_fov_utils.find_fov_reference( |
| cam, req, props, use_raw_fov, name_with_log_path)) |
| |
| run_crop_test = full_or_better and raw_avlb |
| if run_crop_test: |
| # Normalize the circle size to 1/4 of the image size, so that |
| # circle size won't affect the crop test result |
| crop_thresh_factor = ((min(ref_fov['w'], ref_fov['h']) / 4.0) / |
| max(ref_fov['circle_w'], ref_fov['circle_h'])) |
| else: |
| logging.debug('Crop test skipped') |
| |
| # Take pictures of each settings with all the image sizes available. |
| for fmt in format_list: |
| fmt_iter = fmt['iter'] |
| fmt_cmpr = fmt['cmpr'] |
| # Get the size of 'cmpr'. |
| sizes = capture_request_utils.get_available_output_sizes( |
| fmt_cmpr, props, fmt['cmpr_size']) |
| if not sizes: # Device might not support RAW. |
| continue |
| w_cmpr, h_cmpr = sizes[0][0], sizes[0][1] |
| # Get the size of third stream if defined. |
| if 'third' in fmt.keys(): |
| sizes_third = capture_request_utils.get_available_output_sizes( |
| fmt_cmpr, props, fmt['third_size']) |
| test_sizes = capture_request_utils.get_available_output_sizes( |
| fmt_iter, props, fmt['iter_max']) |
| if fmt_cmpr == its_session_utils.PRIVATE_FORMAT: |
| test_sizes = [size for size in test_sizes if size in _SIZES_COMMON] |
| for size_iter in test_sizes: |
| w_iter, h_iter = size_iter[0], size_iter[1] |
| # Skip same format/size combination: ITS doesn't handle that properly. |
| if w_iter*h_iter == w_cmpr*h_cmpr and fmt_iter == fmt_cmpr: |
| continue |
| out_surface = [{'width': w_iter, 'height': h_iter, |
| 'format': fmt_iter}] |
| out_surface.append({'width': w_cmpr, 'height': h_cmpr, |
| 'format': fmt_cmpr}) |
| if 'third' in fmt.keys(): |
| out_surface.append({'width': sizes_third[0][0], |
| 'height': sizes_third[0][1], |
| 'format': fmt['third']}) |
| if cam.is_stream_combination_supported(out_surface): |
| cap = cam.do_capture(req, out_surface)[0] |
| _check_basic_correctness(cap, fmt_iter, w_iter, h_iter) |
| logging.debug('Captured %s with %s %dx%d. Compared size: %dx%d', |
| fmt_iter, fmt_cmpr, w_iter, h_iter, w_cmpr, h_cmpr) |
| img = image_processing_utils.convert_capture_to_rgb_image(cap) |
| img *= 255 # cv2 uses [0, 255]. |
| img_name = f'{name_with_log_path}_{fmt_iter}_with_{fmt_cmpr}_w{w_iter}_h{h_iter}.png' |
| circle = opencv_processing_utils.find_circle( |
| img, img_name, image_fov_utils.CIRCLE_MIN_AREA, |
| image_fov_utils.CIRCLE_COLOR) |
| opencv_processing_utils.append_circle_center_to_img( |
| circle, img, img_name, save_img=False) # imgs saved on FAILs |
| |
| # Check pass/fail for fov coverage for all fmts in AR_CHECKED |
| img /= 255 # image_processing_utils uses [0, 1]. |
| if _is_checked_aspect_ratio(first_api_level, w_iter, h_iter): |
| fov_chk_msg = image_fov_utils.check_fov( |
| circle, ref_fov, w_iter, h_iter) |
| if fov_chk_msg: |
| failed_fov.append(fov_chk_msg) |
| image_processing_utils.write_image(img, img_name, True) |
| |
| # Check pass/fail for aspect ratio. |
| ar_chk_msg = image_fov_utils.check_ar( |
| circle, aspect_ratio_gt, w_iter, h_iter, |
| f'{fmt_iter} with {fmt_cmpr}') |
| if ar_chk_msg: |
| failed_ar.append(ar_chk_msg) |
| image_processing_utils.write_image(img, img_name, True) |
| |
| # Check pass/fail for crop. |
| if run_crop_test: |
| crop_chk_msg = image_fov_utils.check_crop( |
| circle, cc_ct_gt, w_iter, h_iter, |
| f'{fmt_iter} with {fmt_cmpr}', crop_thresh_factor) |
| if crop_chk_msg: |
| failed_crop.append(crop_chk_msg) |
| image_processing_utils.write_image(img, img_name, True) |
| else: |
| continue |
| |
| # Print any failed test results. |
| _print_failed_test_results(failed_ar, failed_fov, failed_crop, |
| first_api_level, level3) |
| |
| if __name__ == '__main__': |
| test_runner.main() |