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