1# Copyright 2014 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"""Verifies android.scaler.cropRegion param works.""" 15 16 17import logging 18import os.path 19 20from mobly import test_runner 21import numpy as np 22 23import its_base_test 24import camera_properties_utils 25import capture_request_utils 26import image_processing_utils 27import its_session_utils 28import target_exposure_utils 29 30# 5 regions specified in normalized (x, y, w, h) coords. 31_CROP_REGIONS = [(0.0, 0.0, 0.5, 0.5), # top-left 32 (0.5, 0.0, 0.5, 0.5), # top-right 33 (0.0, 0.5, 0.5, 0.5), # bottom-left 34 (0.5, 0.5, 0.5, 0.5), # bottom-right 35 (0.25, 0.25, 0.5, 0.5)] # center 36_MIN_DIGITAL_ZOOM_THRESH = 2 37_NAME = os.path.splitext(os.path.basename(__file__))[0] 38 39 40class CropRegionsTest(its_base_test.ItsBaseTest): 41 """Test that crop regions works.""" 42 43 def test_crop_regions(self): 44 logging.debug('Starting %s', _NAME) 45 with its_session_utils.ItsSession( 46 device_id=self.dut.serial, 47 camera_id=self.camera_id, 48 hidden_physical_id=self.hidden_physical_id) as cam: 49 props = cam.get_camera_properties() 50 props = cam.override_with_hidden_physical_camera_props(props) 51 log_path = self.log_path 52 name_with_log_path = os.path.join(log_path, _NAME) 53 54 # check SKIP conditions 55 camera_properties_utils.skip_unless( 56 camera_properties_utils.compute_target_exposure(props) and 57 camera_properties_utils.freeform_crop(props) and 58 camera_properties_utils.per_frame_control(props)) 59 60 # Load chart for scene 61 its_session_utils.load_scene( 62 cam, props, self.scene, self.tablet, 63 its_session_utils.CHART_DISTANCE_NO_SCALING) 64 65 a = props['android.sensor.info.activeArraySize'] 66 ax, ay = a['left'], a['top'] 67 aw, ah = a['right'] - a['left'], a['bottom'] - a['top'] 68 e, s = target_exposure_utils.get_target_exposure_combos( 69 log_path, cam)['minSensitivity'] 70 logging.debug('Active sensor region (%d,%d %dx%d)', ax, ay, aw, ah) 71 72 # Uses a 2x digital zoom. 73 max_digital_zoom = capture_request_utils.get_max_digital_zoom(props) 74 if max_digital_zoom < _MIN_DIGITAL_ZOOM_THRESH: 75 raise AssertionError(f'Max digital zoom: {max_digital_zoom}, ' 76 f'THRESH: {_MIN_DIGITAL_ZOOM_THRESH}') 77 78 # Capture a full frame. 79 req = capture_request_utils.manual_capture_request(s, e) 80 cap_full = cam.do_capture(req) 81 img_full = image_processing_utils.convert_capture_to_rgb_image(cap_full) 82 wfull, hfull = cap_full['width'], cap_full['height'] 83 image_processing_utils.write_image( 84 img_full, f'{name_with_log_path}_full_{wfull}x{hfull}.jpg') 85 86 # Capture a burst of crop region frames. 87 # Note that each region is 1/2x1/2 of the full frame, and is digitally 88 # zoomed into the full size output image, so must be downscaled (below) 89 # by 2x when compared to a tile of the full image. 90 reqs = [] 91 for x, y, w, h in _CROP_REGIONS: 92 req = capture_request_utils.manual_capture_request(s, e) 93 req['android.scaler.cropRegion'] = { 94 'top': int(ah * y), 95 'left': int(aw * x), 96 'right': int(aw * (x + w)), 97 'bottom': int(ah * (y + h))} 98 reqs.append(req) 99 caps_regions = cam.do_capture(reqs) 100 match_failed = False 101 e_msg = [] 102 for i, cap in enumerate(caps_regions): 103 a = cap['metadata']['android.scaler.cropRegion'] 104 ax, ay = a['left'], a['top'] 105 aw, ah = a['right'] - a['left'], a['bottom'] - a['top'] 106 107 # Match this crop image against each of the five regions of 108 # the full image, to find the best match (which should be 109 # the region that corresponds to this crop image). 110 img_crop = image_processing_utils.convert_capture_to_rgb_image(cap) 111 img_crop = image_processing_utils.downscale_image(img_crop, 2) 112 image_processing_utils.write_image( 113 img_crop, f'{name_with_log_path}_crop{i}.jpg') 114 min_diff = None 115 min_diff_region = None 116 for j, (x, y, w, h) in enumerate(_CROP_REGIONS): 117 tile_full = image_processing_utils.get_image_patch( 118 img_full, x, y, w, h) 119 wtest = min(tile_full.shape[1], aw) 120 htest = min(tile_full.shape[0], ah) 121 tile_full = tile_full[0:htest:, 0:wtest:, ::] 122 tile_crop = img_crop[0:htest:, 0:wtest:, ::] 123 image_processing_utils.write_image( 124 tile_full, f'{name_with_log_path}_fullregion{j}.jpg') 125 diff = np.fabs(tile_full - tile_crop).mean() 126 if min_diff is None or diff < min_diff: 127 min_diff = diff 128 min_diff_region = j 129 if i != min_diff_region: 130 match_failed = True 131 e_msg.append(f'i != min_diff_region. i: {i}, ' 132 f'min_diff_region: {min_diff_region}. ') 133 logging.debug('Crop image %d (%d,%d %dx%d) best match with region %d', 134 i, ax, ay, aw, ah, min_diff_region) 135 136 if match_failed: 137 raise AssertionError(f'Match failed: {e_msg}') 138 139if __name__ == '__main__': 140 test_runner.main() 141