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.focusDistance for lens moving and stationary.""" 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 error_util 26import image_processing_utils 27import its_session_utils 28import opencv_processing_utils 29 30FRAME_ATOL_MS = 10 # ms 31LENS_MOVING_STATE = 1 32NAME = os.path.splitext(os.path.basename(__file__))[0] 33NSEC_TO_MSEC = 1.0E-6 34NUM_TRYS = 2 35NUM_STEPS = 6 36POSITION_RTOL = 0.1 37SHARPNESS_RTOL = 0.1 38VGA_W, VGA_H = 640, 480 39 40 41def assert_static_frames_behavior(d_stat): 42 """Assert locations/sharpness are correct in static frames.""" 43 logging.debug('Asserting static lens locations/sharpness are similar') 44 for i in range(len(d_stat) // 2): 45 j = 2 * NUM_STEPS - 1 - i 46 rw_msg = 'fd_write: %.3f, fd_read: %.3f, RTOL: %.2f' % ( 47 d_stat[i]['fd'], d_stat[i]['loc'], POSITION_RTOL) 48 fr_msg = 'loc_fwd[%d]: %.3f, loc_rev[%d]: %.3f, RTOL: %.2f' % ( 49 i, d_stat[i]['loc'], j, d_stat[j]['loc'], POSITION_RTOL) 50 s_msg = 'sharpness_fwd: %.3f, sharpness_rev: %.3f, RTOL: %.2f' % ( 51 d_stat[i]['sharpness'], d_stat[j]['sharpness'], SHARPNESS_RTOL) 52 assert np.isclose(d_stat[i]['loc'], d_stat[i]['fd'], 53 rtol=POSITION_RTOL), rw_msg 54 assert np.isclose(d_stat[i]['loc'], d_stat[j]['loc'], 55 rtol=POSITION_RTOL), fr_msg 56 assert np.isclose(d_stat[i]['sharpness'], d_stat[j]['sharpness'], 57 rtol=SHARPNESS_RTOL), s_msg 58 59 60def assert_moving_frames_behavior(d_move, d_stat): 61 """Assert locations/sharpness are correct for consecutive moving frames.""" 62 logging.debug('Asserting moving frames are consecutive') 63 times = [v['timestamp'] for v in d_move.values()] 64 diffs = np.gradient(times) 65 assert np.isclose(np.amin(diffs), np.amax(diffs), 66 atol=FRAME_ATOL_MS), 'ATOL(ms): %.1f' % FRAME_ATOL_MS 67 68 logging.debug('Asserting moving lens locations/sharpness are similar') 69 for i in range(len(d_move)): 70 e_msg = 'static: %.3f, moving: %.3f, RTOL: %.2f' % ( 71 d_stat[i]['loc'], d_move[i]['loc'], POSITION_RTOL) 72 assert np.isclose(d_stat[i]['loc'], d_move[i]['loc'], 73 rtol=POSITION_RTOL), e_msg 74 if d_move[i]['lens_moving'] and i > 0: 75 e_msg = '%d sharpness[stat]: %.2f ' % (i-1, d_stat[i-1]['sharpness']) 76 e_msg += '%d sharpness[stat]: %.2f, [move]: %.2f, RTOL: %.1f' % ( 77 i, d_stat[i]['sharpness'], d_move[i]['sharpness'], SHARPNESS_RTOL) 78 if d_stat[i]['sharpness'] > d_stat[i-1]['sharpness']: 79 assert (d_stat[i]['sharpness'] * (1.0 + SHARPNESS_RTOL) > 80 d_move[i]['sharpness'] > d_stat[i-1]['sharpness'] * 81 (1.0 - SHARPNESS_RTOL)), e_msg 82 else: 83 assert (d_stat[i-1]['sharpness'] * (1.0 + SHARPNESS_RTOL) > 84 d_move[i]['sharpness'] > d_stat[i]['sharpness'] * 85 (1.0 - SHARPNESS_RTOL)), e_msg 86 elif not d_move[i]['lens_moving']: 87 e_msg = '%d sharpness[stat]: %.2f, [move]: %.2f, RTOL: %.1f' % ( 88 i, d_stat[i]['sharpness'], d_move[i]['sharpness'], SHARPNESS_RTOL) 89 assert np.isclose(d_stat[i]['sharpness'], d_move[i]['sharpness'], 90 rtol=SHARPNESS_RTOL), e_msg 91 else: 92 raise error_util.Error('Lens is moving at frame 0!') 93 94 95def take_caps_and_return_data(cam, props, fmt, sens, exp, chart, log_path): 96 """Return fd, sharpness, lens state of the output images. 97 98 Args: 99 cam: An open device session 100 props: Properties of cam 101 fmt: Dict for capture format 102 sens: Sensitivity for 3A request as defined in android.sensor.sensitivity 103 exp: Exposure time for 3A request as defined in android.sensor.exposureTime 104 chart: Object with chart properties 105 log_path: Location to save images 106 107 Returns: 108 Dictionary of results for different focal distance captures with static 109 lens positions and moving lens positions: d_static, d_moving 110 """ 111 112 # initialize variables and take data sets 113 data_static = {} 114 data_moving = {} 115 white_level = int(props['android.sensor.info.whiteLevel']) 116 min_fd = props['android.lens.info.minimumFocusDistance'] 117 hyperfocal = props['android.lens.info.hyperfocalDistance'] 118 # create forward + back list of focal distances 119 fds_f = np.arange(hyperfocal, min_fd, (min_fd-hyperfocal)/(NUM_STEPS-1)) 120 fds_f = np.append(fds_f, min_fd) 121 fds_fb = list(fds_f) + list(reversed(fds_f)) 122 123 # take static data set 124 for i, fd in enumerate(fds_fb): 125 req = capture_request_utils.manual_capture_request(sens, exp) 126 req['android.lens.focusDistance'] = fd 127 cap = image_processing_utils.stationary_lens_cap(cam, req, fmt) 128 data = {'fd': fds_fb[i]} 129 data['loc'] = cap['metadata']['android.lens.focusDistance'] 130 y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props) 131 chart.img = image_processing_utils.normalize_img( 132 image_processing_utils.get_image_patch(y, chart.xnorm, chart.ynorm, 133 chart.wnorm, chart.hnorm)) 134 image_processing_utils.write_image(chart.img, '%s_stat_i=%d_chart.jpg' % ( 135 os.path.join(log_path, NAME), i)) 136 data['sharpness'] = white_level*image_processing_utils.compute_image_sharpness( 137 chart.img) 138 data_static[i] = data 139 140 # take moving data set 141 reqs = [] 142 for i, fd in enumerate(fds_f): 143 reqs.append(capture_request_utils.manual_capture_request(sens, exp)) 144 reqs[i]['android.lens.focusDistance'] = fd 145 caps = cam.do_capture(reqs, fmt) 146 for i, cap in enumerate(caps): 147 data = {'fd': fds_f[i]} 148 data['loc'] = cap['metadata']['android.lens.focusDistance'] 149 data['lens_moving'] = ( 150 cap['metadata']['android.lens.state'] == LENS_MOVING_STATE) 151 timestamp = cap['metadata']['android.sensor.timestamp'] * NSEC_TO_MSEC 152 if i == 0: 153 timestamp_init = timestamp 154 timestamp -= timestamp_init 155 data['timestamp'] = timestamp 156 y, _, _ = image_processing_utils.convert_capture_to_planes(cap, props) 157 y = image_processing_utils.rotate_img_per_argv(y) 158 chart.img = image_processing_utils.normalize_img( 159 image_processing_utils.get_image_patch( 160 y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm)) 161 image_processing_utils.write_image(chart.img, '%s_move_i=%d_chart.jpg' % ( 162 os.path.join(log_path, NAME), i)) 163 data['sharpness'] = ( 164 white_level * image_processing_utils.compute_image_sharpness(chart.img)) 165 data_moving[i] = data 166 return data_static, data_moving 167 168 169class LensPositionReportingTest(its_base_test.ItsBaseTest): 170 """Test if focus position is properly reported for moving lenses.""" 171 172 def test_lens_position_reporting(self): 173 logging.debug('Starting %s', NAME) 174 with its_session_utils.ItsSession( 175 device_id=self.dut.serial, 176 camera_id=self.camera_id, 177 hidden_physical_id=self.hidden_physical_id) as cam: 178 chart_loc_arg = self.chart_loc_arg 179 props = cam.get_camera_properties() 180 props = cam.override_with_hidden_physical_camera_props(props) 181 log_path = self.log_path 182 183 # Check skip conditions 184 camera_properties_utils.skip_unless( 185 not camera_properties_utils.fixed_focus(props) and 186 camera_properties_utils.read_3a(props) and 187 camera_properties_utils.lens_calibrated(props)) 188 189 # Calculate camera_fov and load scaled image on tablet. 190 its_session_utils.load_scene(cam, props, self.scene, self.tablet, 191 self.chart_distance) 192 193 # Initialize chart class and locate chart in scene 194 chart = opencv_processing_utils.Chart( 195 cam, props, self.log_path, chart_loc=chart_loc_arg) 196 197 # Initialize capture format 198 fmt = {'format': 'yuv', 'width': VGA_W, 'height': VGA_H} 199 200 # Get proper sensitivity and exposure time with 3A 201 mono_camera = camera_properties_utils.mono_camera(props) 202 s, e, _, _, _ = cam.do_3a(get_results=True, mono_camera=mono_camera) 203 204 # Take caps and get sharpness for each focal distance 205 d_stat, d_move = take_caps_and_return_data( 206 cam, props, fmt, s, e, chart, log_path) 207 208 # Summarize info for log file and easier debug 209 logging.debug('Lens stationary') 210 for k in sorted(d_stat): 211 logging.debug( 212 'i: %d\tfd: %.3f\tlens location (diopters): %.3f \t' 213 'sharpness: %.1f', k, d_stat[k]['fd'], d_stat[k]['loc'], 214 d_stat[k]['sharpness']) 215 logging.debug('Lens moving') 216 for k in sorted(d_move): 217 logging.debug( 218 'i: %d\tfd: %.3f\tlens location (diopters): %.3f \t' 219 'sharpness: %.1f \tlens_moving: %r \t' 220 'timestamp: %.1fms', k, d_move[k]['fd'], d_move[k]['loc'], 221 d_move[k]['sharpness'], d_move[k]['lens_moving'], 222 d_move[k]['timestamp']) 223 224 # assert reported location/sharpness is correct in static frames 225 assert_static_frames_behavior(d_stat) 226 227 # assert reported location/sharpness is correct in moving frames 228 assert_moving_frames_behavior(d_move, d_stat) 229 230 231if __name__ == '__main__': 232 test_runner.main() 233