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_TRYS = 2
25NUM_STEPS = 6
26SHARPNESS_TOL = 0.1
27POSITION_TOL = 0.1
28FRAME_TIME_TOL = 10  # ms
29VGA_WIDTH = 640
30VGA_HEIGHT = 480
31NAME = os.path.basename(__file__).split('.')[0]
32
33
34def test_lens_position(cam, props, fmt, sensitivity, exp, chart):
35    """Return fd, sharpness, lens state of the output images.
36
37    Args:
38        cam: An open device session.
39        props: Properties of cam
40        fmt: dict; capture format
41        sensitivity: Sensitivity for the 3A request as defined in
42            android.sensor.sensitivity
43        exp: Exposure time for the 3A request as defined in
44            android.sensor.exposureTime
45        chart: Object with chart properties
46
47    Returns:
48        Dictionary of results for different focal distance captures
49        with static lens positions and moving lens positions
50        d_static, d_moving
51    """
52
53    # initialize variables and take data sets
54    data_static = {}
55    data_moving = {}
56    white_level = int(props['android.sensor.info.whiteLevel'])
57    min_fd = props['android.lens.info.minimumFocusDistance']
58    hyperfocal = props['android.lens.info.hyperfocalDistance']
59    fds_f = np.arange(hyperfocal, min_fd, (min_fd-hyperfocal)/(NUM_STEPS-1))
60    fds_f = np.append(fds_f, min_fd)
61    fds_f = fds_f.tolist()
62    fds_b = list(reversed(fds_f))
63    fds_fb = list(fds_f)
64    fds_fb.extend(fds_b)  # forward and back
65    # take static data set
66    for i, fd in enumerate(fds_fb):
67        req = its.objects.manual_capture_request(sensitivity, exp)
68        req['android.lens.focusDistance'] = fd
69        cap = its.image.stationary_lens_cap(cam, req, fmt)
70        data = {'fd': fds_fb[i]}
71        data['loc'] = cap['metadata']['android.lens.focusDistance']
72        print ' focus distance (diopters): %.3f' % data['fd']
73        print ' current lens location (diopters): %.3f' % data['loc']
74        y, _, _ = its.image.convert_capture_to_planes(cap, props)
75        chart.img = its.image.normalize_img(its.image.get_image_patch(
76                y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
77        its.image.write_image(chart.img, '%s_stat_i=%d_chart.jpg' % (NAME, i))
78        data['sharpness'] = white_level*its.image.compute_image_sharpness(
79                chart.img)
80        print 'Chart sharpness: %.1f\n' % data['sharpness']
81        data_static[i] = data
82    # take moving data set
83    reqs = []
84    for i, fd in enumerate(fds_f):
85        reqs.append(its.objects.manual_capture_request(sensitivity, exp))
86        reqs[i]['android.lens.focusDistance'] = fd
87    caps = cam.do_capture(reqs, fmt)
88    for i, cap in enumerate(caps):
89        data = {'fd': fds_f[i]}
90        data['loc'] = cap['metadata']['android.lens.focusDistance']
91        data['lens_moving'] = (cap['metadata']['android.lens.state']
92                               == 1)
93        timestamp = cap['metadata']['android.sensor.timestamp'] * 1E-6
94        if i == 0:
95            timestamp_init = timestamp
96        timestamp -= timestamp_init
97        data['timestamp'] = timestamp
98        print ' focus distance (diopters): %.3f' % data['fd']
99        print ' current lens location (diopters): %.3f' % data['loc']
100        y, _, _ = its.image.convert_capture_to_planes(cap, props)
101        y = its.image.rotate_img_per_argv(y)
102        chart.img = its.image.normalize_img(its.image.get_image_patch(
103                y, chart.xnorm, chart.ynorm, chart.wnorm, chart.hnorm))
104        its.image.write_image(chart.img, '%s_move_i=%d_chart.jpg' % (NAME, i))
105        data['sharpness'] = white_level*its.image.compute_image_sharpness(
106                chart.img)
107        print 'Chart sharpness: %.1f\n' % data['sharpness']
108        data_moving[i] = data
109    return data_static, data_moving
110
111
112def main():
113    """Test if focus position is properly reported for moving lenses."""
114    print '\nStarting test_lens_position.py'
115    # check skip conditions
116    with its.device.ItsSession() as cam:
117        props = cam.get_camera_properties()
118        its.caps.skip_unless(not its.caps.fixed_focus(props))
119        its.caps.skip_unless(its.caps.read_3a(props) and
120                             its.caps.lens_calibrated(props))
121    # initialize chart class
122    chart = its.cv2image.Chart()
123
124    with its.device.ItsSession() as cam:
125        mono_camera = its.caps.mono_camera(props)
126        fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT}
127
128        # Get proper sensitivity and exposure time with 3A
129        s, e, _, _, _ = cam.do_3a(get_results=True, mono_camera=mono_camera)
130
131        # Get sharpness for each focal distance
132        d_stat, d_move = test_lens_position(cam, props, fmt, s, e, chart)
133        print 'Lens stationary'
134        for k in sorted(d_stat):
135            print ('i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
136                   'sharpness: %.1f' % (k, d_stat[k]['fd'],
137                                        d_stat[k]['loc'],
138                                        d_stat[k]['sharpness']))
139        print 'Lens moving'
140        for k in sorted(d_move):
141            print ('i: %d\tfd: %.3f\tlens location (diopters): %.3f \t'
142                   'sharpness: %.1f  \tlens_moving: %r \t'
143                   'timestamp: %.1fms' % (k, d_move[k]['fd'],
144                                          d_move[k]['loc'],
145                                          d_move[k]['sharpness'],
146                                          d_move[k]['lens_moving'],
147                                          d_move[k]['timestamp']))
148
149        # assert static reported location/sharpness is close
150        print 'Asserting static lens locations/sharpness are similar'
151        for i in range(len(d_stat)/2):
152            j = 2 * NUM_STEPS - 1 - i
153            rw_msg = 'fd_write: %.3f, fd_read: %.3f, RTOL: %.2f' % (
154                    d_stat[i]['fd'], d_stat[i]['loc'], POSITION_TOL)
155            fr_msg = 'loc_fwd: %.3f, loc_rev: %.3f, RTOL: %.2f' % (
156                    d_stat[i]['loc'], d_stat[j]['loc'], POSITION_TOL)
157            s_msg = 'sharpness_fwd: %.3f, sharpness_rev: %.3f, RTOL: %.2f' % (
158                    d_stat[i]['sharpness'], d_stat[j]['sharpness'],
159                    SHARPNESS_TOL)
160            assert np.isclose(d_stat[i]['loc'], d_stat[i]['fd'],
161                              rtol=POSITION_TOL), rw_msg
162            assert np.isclose(d_stat[i]['loc'], d_stat[j]['loc'],
163                              rtol=POSITION_TOL), fr_msg
164            assert np.isclose(d_stat[i]['sharpness'], d_stat[j]['sharpness'],
165                              rtol=SHARPNESS_TOL), s_msg
166        # assert moving frames approximately consecutive with even distribution
167        print 'Asserting moving frames are consecutive'
168        times = [v['timestamp'] for v in d_move.itervalues()]
169        diffs = np.gradient(times)
170        assert np.isclose(np.amin(diffs), np.amax(diffs), atol=FRAME_TIME_TOL)
171        # assert reported location/sharpness is correct in moving frames
172        print 'Asserting moving lens locations/sharpness are similar'
173        for i in range(len(d_move)):
174            m_msg = 'static: %.3f, moving: %.3f, RTOL: %.2f' % (
175                    d_stat[i]['loc'], d_move[i]['loc'], POSITION_TOL)
176            assert np.isclose(d_stat[i]['loc'], d_move[i]['loc'],
177                              rtol=POSITION_TOL), m_msg
178            if d_move[i]['lens_moving'] and i > 0:
179                if d_stat[i]['sharpness'] > d_stat[i-1]['sharpness']:
180                    assert (d_stat[i]['sharpness']*(1.0+SHARPNESS_TOL) >
181                            d_move[i]['sharpness'] >
182                            d_stat[i-1]['sharpness']*(1.0-SHARPNESS_TOL))
183                else:
184                    assert (d_stat[i-1]['sharpness']*(1.0+SHARPNESS_TOL) >
185                            d_move[i]['sharpness'] >
186                            d_stat[i]['sharpness']*(1.0-SHARPNESS_TOL))
187            elif not d_move[i]['lens_moving']:
188                assert np.isclose(
189                        d_stat[i]['sharpness'], d_move[i]['sharpness'],
190                        rtol=SHARPNESS_TOL)
191            else:
192                raise its.error.Error('Lens is moving at frame 0!')
193
194if __name__ == '__main__':
195    main()
196
197