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
15import os
16
17import its.caps
18import its.cv2image
19import its.device
20import its.image
21import its.objects
22import numpy as np
23
24NUM_IMGS = 12
25FRAME_TIME_TOL = 10  # ms
26SHARPNESS_TOL = 0.10  # percentage
27POSITION_TOL = 0.10  # percentage
28VGA_WIDTH = 640
29VGA_HEIGHT = 480
30NAME = os.path.basename(__file__).split('.')[0]
31
32
33def test_lens_movement_reporting(cam, props, fmt, gain, exp, af_fd, chart):
34    """Return fd, sharpness, lens state of the output images.
35
36    Args:
37        cam: An open device session.
38        props: Properties of cam
39        fmt: dict; capture format
40        gain: Sensitivity for the 3A request as defined in
41            android.sensor.sensitivity
42        exp: Exposure time for the 3A request as defined in
43            android.sensor.exposureTime
44        af_fd: Focus distance for the 3A request as defined in
45            android.lens.focusDistance
46        chart: Object that contains chart information
47
48    Returns:
49        Object containing reported sharpness of the output image, keyed by
50        the following string:
51            'sharpness'
52    """
53
54    # initialize variables and take data sets
55    data_set = {}
56    white_level = int(props['android.sensor.info.whiteLevel'])
57    min_fd = props['android.lens.info.minimumFocusDistance']
58    fds = [af_fd, min_fd]
59    fds = sorted(fds * NUM_IMGS)
60    reqs = []
61    for i, fd in enumerate(fds):
62        reqs.append(its.objects.manual_capture_request(gain, exp))
63        reqs[i]['android.lens.focusDistance'] = fd
64    caps = cam.do_capture(reqs, fmt)
65    for i, cap in enumerate(caps):
66        data = {'fd': fds[i]}
67        data['loc'] = cap['metadata']['android.lens.focusDistance']
68        data['lens_moving'] = (cap['metadata']['android.lens.state']
69                               == 1)
70        timestamp = cap['metadata']['android.sensor.timestamp']
71        if i == 0:
72            timestamp_init = timestamp
73        timestamp -= timestamp_init
74        timestamp *= 1E-6
75        data['timestamp'] = timestamp
76        print ' focus distance (diopters): %.3f' % data['fd']
77        print ' current lens location (diopters): %.3f' % data['loc']
78        print ' lens moving %r' % data['lens_moving']
79        y, _, _ = its.image.convert_capture_to_planes(cap, props)
80        y = its.image.rotate_img_per_argv(y)
81        chart.img = its.image.normalize_img(its.image.get_image_patch(
82                y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
83        its.image.write_image(chart.img, '%s_i=%d_chart.jpg' % (NAME, i))
84        data['sharpness'] = white_level*its.image.compute_image_sharpness(
85                chart.img)
86        print 'Chart sharpness: %.1f\n' % data['sharpness']
87        data_set[i] = data
88    return data_set
89
90
91def main():
92    """Test if focus distance is properly reported.
93
94    Capture images at a variety of focus locations.
95    """
96
97    print '\nStarting test_lens_movement_reporting.py'
98    # check skip conditions
99    with its.device.ItsSession() as cam:
100        props = cam.get_camera_properties()
101        its.caps.skip_unless(not its.caps.fixed_focus(props))
102        its.caps.skip_unless(its.caps.read_3a(props) and
103                             its.caps.lens_approx_calibrated(props))
104    # initialize chart class
105    chart = its.cv2image.Chart()
106
107    with its.device.ItsSession() as cam:
108        mono_camera = its.caps.mono_camera(props)
109        min_fd = props['android.lens.info.minimumFocusDistance']
110        fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
111
112        # Get proper sensitivity, exposure time, and focus distance with 3A.
113        s, e, _, _, fd = cam.do_3a(get_results=True, mono_camera=mono_camera)
114
115        # Get sharpness for each focal distance
116        d = test_lens_movement_reporting(cam, props, fmt, s, e, fd, chart)
117        for k in sorted(d):
118            print ('i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
119                   'sharpness: %.1f  \tlens_moving: %r \t'
120                   'timestamp: %.1fms' % (k, d[k]['fd'], d[k]['loc'],
121                                          d[k]['sharpness'],
122                                          d[k]['lens_moving'],
123                                          d[k]['timestamp']))
124
125        # assert frames are consecutive
126        print 'Asserting frames are consecutive'
127        times = [v['timestamp'] for v in d.itervalues()]
128        diffs = np.gradient(times)
129        assert np.isclose(np.amax(diffs)-np.amax(diffs), 0, atol=FRAME_TIME_TOL)
130
131        # remove data when lens is moving
132        for k in sorted(d):
133            if d[k]['lens_moving']:
134                del d[k]
135
136        # split data into min_fd and af data for processing
137        d_min_fd = {}
138        d_af_fd = {}
139        for k in sorted(d):
140            if d[k]['fd'] == min_fd:
141                d_min_fd[k] = d[k]
142            if d[k]['fd'] == fd:
143                d_af_fd[k] = d[k]
144
145        # assert reported locations are close at af_fd
146        print 'Asserting lens location of af_fd data'
147        min_loc = min([v['loc'] for v in d_af_fd.itervalues()])
148        max_loc = max([v['loc'] for v in d_af_fd.itervalues()])
149        assert np.isclose(min_loc, max_loc, rtol=POSITION_TOL)
150        # assert reported sharpness is close at af_fd
151        print 'Asserting sharpness of af_fd data'
152        min_sharp = min([v['sharpness'] for v in d_af_fd.itervalues()])
153        max_sharp = max([v['sharpness'] for v in d_af_fd.itervalues()])
154        assert np.isclose(min_sharp, max_sharp, rtol=SHARPNESS_TOL)
155        # assert reported location is close to assign location for af_fd
156        print 'Asserting lens location close to assigned fd for af_fd data'
157        first_key = min(d_af_fd.keys())  # finds 1st non-moving frame
158        assert np.isclose(d_af_fd[first_key]['loc'], d_af_fd[first_key]['fd'],
159                          rtol=POSITION_TOL)
160
161        # assert reported location is close for min_fd captures
162        print 'Asserting lens location similar min_fd data'
163        min_loc = min([v['loc'] for v in d_min_fd.itervalues()])
164        max_loc = max([v['loc'] for v in d_min_fd.itervalues()])
165        assert np.isclose(min_loc, max_loc, rtol=POSITION_TOL)
166        # assert reported sharpness is close at min_fd
167        print 'Asserting sharpness of min_fd data'
168        min_sharp = min([v['sharpness'] for v in d_min_fd.itervalues()])
169        max_sharp = max([v['sharpness'] for v in d_min_fd.itervalues()])
170        assert np.isclose(min_sharp, max_sharp, rtol=SHARPNESS_TOL)
171        # assert reported location is close to assign location for min_fd
172        print 'Asserting lens location close to assigned fd for min_fd data'
173        assert np.isclose(d_min_fd[NUM_IMGS*2-1]['loc'],
174                          d_min_fd[NUM_IMGS*2-1]['fd'], rtol=POSITION_TOL)
175
176
177if __name__ == '__main__':
178    main()
179