• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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