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