1# Copyright 2016 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.lens.state when lens is moving.""" 15 16 17import logging 18import os 19from mobly import test_runner 20import numpy as np 21 22import its_base_test 23import camera_properties_utils 24import capture_request_utils 25import image_processing_utils 26import its_session_utils 27import opencv_processing_utils 28 29 30FRAME_ATOL_MS = 10 31MIN_AF_FD_TOL = 1.2 # AF value must < 1.2*min 32NAME = os.path.splitext(os.path.basename(__file__))[0] 33NUM_FRAMES_PER_FD = 12 34POSITION_RTOL = 0.10 # 10% 35SHARPNESS_RTOL = 0.10 # 10% 36START_FRAME = 1 # start on second frame 37VGA_WIDTH, VGA_HEIGHT = 640, 480 38 39 40def take_caps_and_determine_sharpness( 41 cam, props, fmt, gain, exp, af_fd, chart, log_path): 42 """Return fd, sharpness, lens state of the output images. 43 44 Args: 45 cam: An open device session. 46 props: Properties of cam 47 fmt: dict; capture format 48 gain: Sensitivity for the request as defined in android.sensor.sensitivity 49 exp: Exposure time for the request as defined in 50 android.sensor.exposureTime 51 af_fd: Focus distance for the request as defined in 52 android.lens.focusDistance 53 chart: Object that contains chart information 54 log_path: log_path to save the captured image 55 56 Returns: 57 Object containing reported sharpness of the output image, keyed by 58 the following string: 59 'sharpness' 60 """ 61 62 # initialize variables and take data sets 63 data_set = {} 64 white_level = int(props['android.sensor.info.whiteLevel']) 65 min_fd = props['android.lens.info.minimumFocusDistance'] 66 fds = [af_fd] * NUM_FRAMES_PER_FD + [min_fd] * NUM_FRAMES_PER_FD 67 reqs = [] 68 for i, fd in enumerate(fds): 69 reqs.append(capture_request_utils.manual_capture_request(gain, exp)) 70 reqs[i]['android.lens.focusDistance'] = fd 71 caps = cam.do_capture(reqs, fmt) 72 caps = caps[START_FRAME:] 73 for i, cap in enumerate(caps): 74 data = {'fd': fds[i+START_FRAME]} 75 data['loc'] = cap['metadata']['android.lens.focusDistance'] 76 data['lens_moving'] = (cap['metadata']['android.lens.state'] 77 == 1) 78 timestamp = cap['metadata']['android.sensor.timestamp'] * 1E-6 79 if i == 0: 80 timestamp_init = timestamp 81 timestamp -= timestamp_init 82 data['timestamp'] = timestamp 83 y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props) 84 chart.img = image_processing_utils.normalize_img( 85 image_processing_utils.get_image_patch( 86 y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm)) 87 img_name = '%s_i=%d.jpg' % (os.path.join(log_path, NAME), i) 88 image_processing_utils.write_image(chart.img, img_name) 89 data['sharpness'] = ( 90 white_level * image_processing_utils.compute_image_sharpness(chart.img)) 91 data_set[i] = data 92 return data_set 93 94 95class LensMovementReportingTest(its_base_test.ItsBaseTest): 96 """Test if focus distance is properly reported. 97 98 Do unit step of focus distance and check sharpness correlates. 99 """ 100 101 def test_lens_movement_reporting(self): 102 logging.debug('Starting %s', NAME) 103 104 with its_session_utils.ItsSession( 105 device_id=self.dut.serial, 106 camera_id=self.camera_id, 107 hidden_physical_id=self.hidden_physical_id) as cam: 108 props = cam.get_camera_properties() 109 props = cam.override_with_hidden_physical_camera_props(props) 110 111 # Check skip conditions 112 camera_properties_utils.skip_unless( 113 not camera_properties_utils.fixed_focus(props) and 114 camera_properties_utils.read_3a(props) and 115 camera_properties_utils.lens_approx_calibrated(props)) 116 117 # Calculate camera_fov and load scaled image on tablet. 118 its_session_utils.load_scene( 119 cam, props, self.scene, self.tablet, self.chart_distance) 120 121 # Initialize chart class and locate chart in scene 122 chart = opencv_processing_utils.Chart(cam, props, self.log_path) 123 124 # Get proper sensitivity, exposure time, and focus distance with 3A. 125 mono_camera = camera_properties_utils.mono_camera(props) 126 s, e, _, _, af_fd = cam.do_3a(get_results=True, mono_camera=mono_camera) 127 128 # Get sharpness for each focal distance 129 fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT} 130 d = take_caps_and_determine_sharpness( 131 cam, props, fmt, s, e, af_fd, chart, self.log_path) 132 for k in sorted(d): 133 logging.debug( 134 'i: %d\tfd: %.3f\tlens location (diopters): %.3f \t' 135 'sharpness: %.1f \tlens_moving: %r \t' 136 'timestamp: %.1fms', k, d[k]['fd'], d[k]['loc'], d[k]['sharpness'], 137 d[k]['lens_moving'], d[k]['timestamp']) 138 139 # Assert frames are consecutive 140 frame_diffs = np.gradient([v['timestamp'] for v in d.values()]) 141 delta_diffs = np.amax(frame_diffs) - np.amin(frame_diffs) 142 if not np.isclose(delta_diffs, 0, atol=FRAME_ATOL_MS): 143 raise AssertionError(f'Timestamp gradient(ms): {delta_diffs:.1f}, ' 144 f'ATOL: {FRAME_ATOL_MS}') 145 146 # Remove data when lens is moving 147 for k in sorted(d): 148 if d[k]['lens_moving']: 149 del d[k] 150 151 # Split data into min_fd and af data for processing 152 d_min_fd = {} 153 d_af_fd = {} 154 for k in sorted(d): 155 if d[k]['fd'] == props['android.lens.info.minimumFocusDistance']: 156 d_min_fd[k] = d[k] 157 if d[k]['fd'] == af_fd: 158 d_af_fd[k] = d[k] 159 160 logging.debug('Assert reported locs are close for af_fd captures') 161 min_loc = min([v['loc'] for v in d_af_fd.values()]) 162 max_loc = max([v['loc'] for v in d_af_fd.values()]) 163 if not np.isclose(min_loc, max_loc, rtol=POSITION_RTOL): 164 raise AssertionError(f'af_fd[loc] min: {min_loc:.3f}, max: ' 165 f'{max_loc:.3f}, RTOL: {POSITION_RTOL}') 166 167 logging.debug('Assert reported sharpness is close at af_fd') 168 min_sharp = min([v['sharpness'] for v in d_af_fd.values()]) 169 max_sharp = max([v['sharpness'] for v in d_af_fd.values()]) 170 if not np.isclose(min_sharp, max_sharp, rtol=SHARPNESS_RTOL): 171 raise AssertionError(f'af_fd[sharpness] min: {min_sharp:.3f}, ' 172 f'max: {max_sharp:.3f}, RTOL: {SHARPNESS_RTOL}') 173 174 logging.debug('Assert reported loc is close to assign loc for af_fd') 175 first_key = min(d_af_fd.keys()) # find 1st non-moving frame 176 loc = d_af_fd[first_key]['loc'] 177 fd = d_af_fd[first_key]['fd'] 178 if not np.isclose(loc, fd, rtol=POSITION_RTOL): 179 raise AssertionError(f'af_fd[loc]: {loc:.3f}, af_fd[fd]: {fd:.3f}, ' 180 f'RTOL: {POSITION_RTOL}') 181 182 logging.debug('Assert reported locs are close for min_fd captures') 183 min_loc = min([v['loc'] for v in d_min_fd.values()]) 184 max_loc = max([v['loc'] for v in d_min_fd.values()]) 185 if not np.isclose(min_loc, max_loc, rtol=POSITION_RTOL): 186 raise AssertionError(f'min_fd[loc] min: {min_loc:.3f}, max: ' 187 f'{max_loc:.3f}, RTOL: {POSITION_RTOL}') 188 189 logging.debug('Assert reported sharpness is close at min_fd') 190 min_sharp = min([v['sharpness'] for v in d_min_fd.values()]) 191 max_sharp = max([v['sharpness'] for v in d_min_fd.values()]) 192 if not np.isclose(min_sharp, max_sharp, rtol=SHARPNESS_RTOL): 193 raise AssertionError(f'min_fd[sharpness] min: {min_sharp:.3f}, ' 194 f'max: {max_sharp:.3f}, RTOL: {SHARPNESS_RTOL}') 195 196 logging.debug('Assert reported loc is close to assigned loc for min_fd') 197 last_key = max(d_min_fd.keys()) # find last (non-moving) frame 198 loc = d_min_fd[last_key]['loc'] 199 fd = d_min_fd[last_key]['fd'] 200 if not np.isclose(loc, fd, rtol=POSITION_RTOL): 201 raise AssertionError(f'min_fd[loc]: {loc:.3f}, min_fd[fd]: {fd:.3f}, ' 202 f'RTOL: {POSITION_RTOL}') 203 204 logging.debug('Assert AF focus distance > minimum focus distance') 205 min_fd = d_min_fd[last_key]['fd'] 206 if af_fd > min_fd * MIN_AF_FD_TOL: 207 raise AssertionError(f'AF focus distance > min focus distance! af: ' 208 f'{af_fd}, min: {min_fd}, TOL: {MIN_AF_FD_TOL}') 209 210if __name__ == '__main__': 211 test_runner.main() 212