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 copy 18import logging 19import math 20import os 21from mobly import test_runner 22import numpy as np 23 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 32_FRAME_ATOL_MS = 10 33_LENS_INTRINSIC_CAL_FX_IDX = 0 34_LENS_INTRINSIC_CAL_FY_IDX = 1 35_LENS_INTRINSIC_CAL_RTOL = 0.001 36_MIN_AF_FD_RTOL = 0.2 # AF value must 20% larger than min_fd 37# Min focus distance should be within 10% of chart distance to 38# skip the LENS_INTRINSIC_CALIBRATION check 39_MIN_FD_THRESHOLD = 0.1 40_NAME = os.path.splitext(os.path.basename(__file__))[0] 41_NUM_FRAMES_PER_FD = 12 42_POSITION_RTOL = 0.10 # 10% 43_SHARPNESS_RTOL = 0.10 # 10% 44_START_FRAME = 1 # start on second frame 45_VGA_WIDTH, _VGA_HEIGHT = 640, 480 46 47 48def take_caps_and_determine_sharpness( 49 cam, props, fmt, gain, exp, af_fd, chart, log_path): 50 """Return fd, sharpness, lens state of the output images. 51 52 Args: 53 cam: An open device session. 54 props: Properties of cam 55 fmt: dict; capture format 56 gain: Sensitivity for the request as defined in android.sensor.sensitivity 57 exp: Exposure time for the request as defined in 58 android.sensor.exposureTime 59 af_fd: Focus distance for the request as defined in 60 android.lens.focusDistance 61 chart: Object that contains chart information 62 log_path: log_path to save the captured image 63 64 Returns: 65 Object containing reported sharpness of the output image, keyed by 66 the following string: 67 'sharpness' 68 """ 69 70 # initialize variables and take data sets 71 data_set = {} 72 white_level = int(props['android.sensor.info.whiteLevel']) 73 min_fd = props['android.lens.info.minimumFocusDistance'] 74 fds = [af_fd] * _NUM_FRAMES_PER_FD + [min_fd] * _NUM_FRAMES_PER_FD 75 reqs = [] 76 for i, fd in enumerate(fds): 77 reqs.append(capture_request_utils.manual_capture_request(gain, exp)) 78 reqs[i]['android.lens.focusDistance'] = fd 79 caps = cam.do_capture(reqs, fmt) 80 caps = caps[_START_FRAME:] 81 for i, cap in enumerate(caps): 82 data = {'fd': fds[i+_START_FRAME]} 83 data['frame_num'] = i + _START_FRAME 84 data['loc'] = cap['metadata']['android.lens.focusDistance'] 85 data['lens_moving'] = (cap['metadata']['android.lens.state'] 86 == 1) 87 data['lens_intrinsic_calibration'] = ( 88 cap['metadata']['android.lens.intrinsicCalibration']) 89 timestamp = cap['metadata']['android.sensor.timestamp'] * 1E-6 90 if i == 0: 91 timestamp_init = timestamp 92 timestamp -= timestamp_init 93 data['timestamp'] = timestamp 94 y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props) 95 chart.img = image_processing_utils.normalize_img( 96 image_processing_utils.get_image_patch( 97 y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm)) 98 image_processing_utils.write_image( 99 chart.img, f'{os.path.join(log_path, _NAME)}_i={i}.jpg') 100 data['sharpness'] = ( 101 white_level * image_processing_utils.compute_image_sharpness(chart.img)) 102 data_set[i+_START_FRAME] = data 103 return data_set 104 105 106class LensMovementReportingTest(its_base_test.ItsBaseTest): 107 """Test if focus distance is properly reported. 108 109 Do unit step of focus distance and check sharpness correlates. 110 """ 111 112 def test_lens_movement_reporting(self): 113 with its_session_utils.ItsSession( 114 device_id=self.dut.serial, 115 camera_id=self.camera_id, 116 hidden_physical_id=self.hidden_physical_id) as cam: 117 props = cam.get_camera_properties() 118 props = cam.override_with_hidden_physical_camera_props(props) 119 120 # Check skip conditions 121 camera_properties_utils.skip_unless( 122 not camera_properties_utils.fixed_focus(props) and 123 camera_properties_utils.read_3a(props) and 124 camera_properties_utils.lens_approx_calibrated(props)) 125 lens_calibrated = camera_properties_utils.lens_calibrated(props) 126 logging.debug('lens_calibrated: %d', lens_calibrated) 127 min_focus_distance = props['android.lens.info.minimumFocusDistance'] 128 129 # Load scene 130 its_session_utils.load_scene( 131 cam, props, self.scene, self.tablet, self.chart_distance) 132 133 # Initialize chart class and locate chart in scene 134 chart = opencv_processing_utils.Chart( 135 cam, props, self.log_path, distance=self.chart_distance) 136 137 # Get proper sensitivity, exposure time, and focus distance with 3A. 138 mono_camera = camera_properties_utils.mono_camera(props) 139 s, e, _, _, af_fd = cam.do_3a(get_results=True, mono_camera=mono_camera) 140 141 # Get sharpness for each focal distance 142 fmt = {'format': 'yuv', 'width': _VGA_WIDTH, 'height': _VGA_HEIGHT} 143 frame_data = take_caps_and_determine_sharpness( 144 cam, props, fmt, s, e, af_fd, chart, self.log_path) 145 for k in sorted(frame_data): 146 logging.debug( 147 'i: %d\tfd: %.3f\tdiopters: %.3f \tsharpness: %.1f \t' 148 'lens_state: %d \ttimestamp: %.1fms\t cal: %s', 149 frame_data[k]['frame_num'], frame_data[k]['fd'], 150 frame_data[k]['loc'], frame_data[k]['sharpness'], 151 frame_data[k]['lens_moving'], frame_data[k]['timestamp'], 152 np.around(frame_data[k]['lens_intrinsic_calibration'], 2)) 153 154 # Assert frames are consecutive 155 frame_diffs = np.gradient([v['timestamp'] for v in frame_data.values()]) 156 delta_diffs = np.amax(frame_diffs) - np.amin(frame_diffs) 157 if not math.isclose(delta_diffs, 0, abs_tol=_FRAME_ATOL_MS): 158 raise AssertionError(f'Timestamp gradient(ms): {delta_diffs:.1f}, ' 159 f'ATOL: {_FRAME_ATOL_MS}') 160 161 # Remove data when lens is moving 162 frame_data_non_moving = copy.deepcopy(frame_data) 163 for k in sorted(frame_data_non_moving): 164 if frame_data_non_moving[k]['lens_moving']: 165 del frame_data_non_moving[k] 166 167 # Split data into min_fd and af data for processing 168 data_min_fd = {} 169 data_af_fd = {} 170 for k in sorted(frame_data_non_moving): 171 if frame_data_non_moving[k]['fd'] == props[ 172 'android.lens.info.minimumFocusDistance']: 173 data_min_fd[k] = frame_data_non_moving[k] 174 if frame_data_non_moving[k]['fd'] == af_fd: 175 data_af_fd[k] = frame_data_non_moving[k] 176 177 logging.debug('Assert reported locs are close for af_fd captures') 178 min_loc = min([v['loc'] for v in data_af_fd.values()]) 179 max_loc = max([v['loc'] for v in data_af_fd.values()]) 180 if not math.isclose(min_loc, max_loc, rel_tol=_POSITION_RTOL): 181 raise AssertionError(f'af_fd[loc] min: {min_loc:.3f}, max: ' 182 f'{max_loc:.3f}, RTOL: {_POSITION_RTOL}') 183 184 logging.debug('Assert reported sharpness is close at af_fd') 185 min_sharp = min([v['sharpness'] for v in data_af_fd.values()]) 186 max_sharp = max([v['sharpness'] for v in data_af_fd.values()]) 187 if not math.isclose(min_sharp, max_sharp, rel_tol=_SHARPNESS_RTOL): 188 raise AssertionError(f'af_fd[sharpness] min: {min_sharp:.3f}, ' 189 f'max: {max_sharp:.3f}, RTOL: {_SHARPNESS_RTOL}') 190 191 logging.debug('Assert reported loc is close to assign loc for af_fd') 192 first_key = min(data_af_fd.keys()) # find 1st non-moving frame 193 loc = data_af_fd[first_key]['loc'] 194 fd = data_af_fd[first_key]['fd'] 195 if not math.isclose(loc, fd, rel_tol=_POSITION_RTOL): 196 raise AssertionError(f'af_fd[loc]: {loc:.3f}, af_fd[fd]: {fd:.3f}, ' 197 f'RTOL: {_POSITION_RTOL}') 198 199 logging.debug('Assert reported locs are close for min_fd captures') 200 min_loc = min([v['loc'] for v in data_min_fd.values()]) 201 max_loc = max([v['loc'] for v in data_min_fd.values()]) 202 if not math.isclose(min_loc, max_loc, rel_tol=_POSITION_RTOL): 203 raise AssertionError(f'min_fd[loc] min: {min_loc:.3f}, max: ' 204 f'{max_loc:.3f}, RTOL: {_POSITION_RTOL}') 205 206 logging.debug('Assert reported sharpness is close at min_fd') 207 min_sharp = min([v['sharpness'] for v in data_min_fd.values()]) 208 max_sharp = max([v['sharpness'] for v in data_min_fd.values()]) 209 if not math.isclose(min_sharp, max_sharp, rel_tol=_SHARPNESS_RTOL): 210 raise AssertionError(f'min_fd[sharpness] min: {min_sharp:.3f}, ' 211 f'max: {max_sharp:.3f}, RTOL: {_SHARPNESS_RTOL}') 212 213 logging.debug('Assert reported loc is close to assigned loc for min_fd') 214 last_key = max(data_min_fd.keys()) # find last (non-moving) frame 215 loc = data_min_fd[last_key]['loc'] 216 fd = data_min_fd[last_key]['fd'] 217 if not math.isclose(loc, fd, rel_tol=_POSITION_RTOL): 218 raise AssertionError(f'min_fd[loc]: {loc:.3f}, min_fd[fd]: {fd:.3f}, ' 219 f'RTOL: {_POSITION_RTOL}') 220 221 logging.debug('Assert AF focus distance > minimum focus distance') 222 min_fd = data_min_fd[last_key]['fd'] 223 if af_fd > min_fd * (1 + _MIN_AF_FD_RTOL): 224 raise AssertionError(f'AF focus distance > min focus distance! af: ' 225 f'{af_fd}, min: {min_fd}, RTOL: {_MIN_AF_FD_RTOL}') 226 227 # Check LENS_INTRINSIC_CALIBRATION 228 # Check min focus distance is within 10% of chart distance 229 skip_check = False 230 # Convert min_focus_distance from diopters to cm 231 min_focus_distance_cm = (1/min_focus_distance) * 100 232 if math.isclose(min_focus_distance_cm, self.chart_distance, 233 rel_tol=_MIN_FD_THRESHOLD): 234 skip_check = True 235 if (its_session_utils.get_first_api_level(self.dut.serial) >= 236 its_session_utils.ANDROID15_API_LEVEL and 237 camera_properties_utils.intrinsic_calibration(props) and 238 not skip_check): 239 logging.debug('Assert LENS_INTRINSIC_CALIBRATION changes with lens ' 240 'location on non-moving frames.') 241 last_af_frame_cal = data_af_fd[max(data_af_fd.keys())][ 242 'lens_intrinsic_calibration'] 243 first_min_frame_cal = data_min_fd[min(data_min_fd.keys())][ 244 'lens_intrinsic_calibration'] 245 logging.debug('Last AF frame cal: %s', last_af_frame_cal) 246 logging.debug('1st min_fd frame cal: %s', first_min_frame_cal) 247 if (math.isclose(first_min_frame_cal[_LENS_INTRINSIC_CAL_FX_IDX], 248 last_af_frame_cal[_LENS_INTRINSIC_CAL_FX_IDX], 249 rel_tol=_LENS_INTRINSIC_CAL_RTOL) and 250 math.isclose(first_min_frame_cal[_LENS_INTRINSIC_CAL_FY_IDX], 251 last_af_frame_cal[_LENS_INTRINSIC_CAL_FY_IDX], 252 rel_tol=_LENS_INTRINSIC_CAL_RTOL)): 253 raise AssertionError( 254 'LENS_INTRINSIC_CALIBRAION[f_x, f_y] not changing with lens ' 255 f'movement! AF lens location: {last_af_frame_cal}, ' 256 f'min fd lens location: {first_min_frame_cal}, ' 257 f'RTOL: {_LENS_INTRINSIC_CAL_RTOL}') 258 259if __name__ == '__main__': 260 test_runner.main() 261