• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 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 math
16import os.path
17import cv2
18
19import its.caps
20import its.cv2image
21import its.device
22import its.image
23import its.objects
24
25import numpy as np
26
27CIRCLE_COLOR = 0  # [0: black, 255: white]
28CIRCLE_TOL = 0.05  # contour area vs ideal circle area pi*((w+h)/4)**2
29LINE_COLOR = (255, 0, 0)  # red
30LINE_THICKNESS = 5
31MIN_AREA_RATIO = 0.00015  # based on 2000/(4000x3000) pixels
32MIN_CIRCLE_PTS = 25
33NAME = os.path.basename(__file__).split('.')[0]
34NUM_STEPS = 10
35OFFSET_RTOL = 0.10
36RADIUS_RTOL = 0.10
37ZOOM_MAX_THRESH = 10.0
38ZOOM_MIN_THRESH = 2.0
39
40
41def distance((x, y)):
42    return math.sqrt(x**2 + y**2)
43
44
45def circle_cropped(circle, size):
46    """Determine if a circle is cropped by edge of img.
47
48    Args:
49        circle:         list; [x, y, radius] of circle
50        size:           tuple; [x, y] size of img
51
52    Returns:
53        Boolean True if selected circle is cropped
54    """
55
56    cropped = False
57    circle_x, circle_y = circle[0], circle[1]
58    circle_r = circle[2]
59    x_min, x_max = circle_x - circle_r, circle_x + circle_r
60    y_min, y_max = circle_y - circle_r, circle_y + circle_r
61    if x_min < 0 or y_min < 0 or x_max > size[0] or y_max > size[1]:
62        cropped = True
63    return cropped
64
65
66def find_center_circle(img, name, color, min_area, debug):
67    """Find the circle closest to the center of the image.
68
69    Finds all contours in the image. Rejects those too small and not enough
70    points to qualify as a circle. The remaining contours must have center
71    point of color=color and are sorted based on distance from the center
72    of the image. The contour closest to the center of the image is returned.
73
74    Note: hierarchy is not used as the hierarchy for black circles changes
75    as the zoom level changes.
76
77    Args:
78        img:            numpy img array with pixel values in [0,255].
79        name:           str; file name
80        color:          int; 0: black, 255: white
81        min_area:       int; minimum area of circles to screen out
82        debug:          bool; save extra data
83
84    Returns:
85        circle:         [center_x, center_y, radius]
86    """
87
88    # gray scale & otsu threshold to binarize the image
89    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
90    _, img_bw = cv2.threshold(np.uint8(gray), 0, 255,
91                              cv2.THRESH_BINARY + cv2.THRESH_OTSU)
92
93    # use OpenCV to find contours (connected components)
94    cv2_version = cv2.__version__
95    if cv2_version.startswith('3.'): # OpenCV 3.x
96        _, contours, _ = cv2.findContours(
97                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
98    else: # OpenCV 2.x and 4.x
99        contours, _ = cv2.findContours(
100                255-img_bw, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
101
102    # check contours and find the best circle candidates
103    circles = []
104    img_ctr = [gray.shape[1]/2, gray.shape[0]/2]
105    for contour in contours:
106        area = cv2.contourArea(contour)
107        if area > min_area and len(contour) >= MIN_CIRCLE_PTS:
108            shape = its.cv2image.component_shape(contour)
109            radius = (shape['width'] + shape['height']) / 4
110            colour = img_bw[shape['cty']][shape['ctx']]
111            circlish = round((math.pi * radius**2) / area, 4)
112            if colour == color and (1-CIRCLE_TOL <= circlish <= 1+CIRCLE_TOL):
113                circles.append([shape['ctx'], shape['cty'], radius, circlish,
114                                area])
115
116    if debug:
117        circles.sort(key=lambda x: abs(x[3]-1.0))  # sort for best circles
118        print 'circles [x, y, r, pi*r**2/area, area]:', circles
119
120    # find circle closest to center
121    circles.sort(key=lambda x: distance((x[0]-img_ctr[0], x[1]-img_ctr[1])))
122    circle = circles[0]
123
124    # mark image center
125    size = gray.shape
126    m_x, m_y = size[1]/2, size[0]/2
127    marker_size = LINE_THICKNESS * 10
128    if cv2_version.startswith('2.4.'):
129        cv2.line(img, (m_x-marker_size/2, m_y), (m_x+marker_size/2, m_y),
130                 LINE_COLOR, LINE_THICKNESS)
131        cv2.line(img, (m_x, m_y-marker_size/2), (m_x, m_y+marker_size/2),
132                 LINE_COLOR, LINE_THICKNESS)
133    elif cv2_version.startswith('3.2.'):
134        cv2.drawMarker(img, (m_x, m_y), LINE_COLOR,
135                       markerType=cv2.MARKER_CROSS,
136                       markerSize=marker_size,
137                       thickness=LINE_THICKNESS)
138
139    # add circle to saved image
140    center_i = (int(round(circle[0], 0)), int(round(circle[1], 0)))
141    radius_i = int(round(circle[2], 0))
142    cv2.circle(img, center_i, radius_i, LINE_COLOR, LINE_THICKNESS)
143    its.image.write_image(img/255.0, name)
144
145    if not circles:
146        print 'No circle was detected. Please take pictures according',
147        print 'to instruction carefully!\n'
148        assert False
149
150    return [circle[0], circle[1], circle[2]]
151
152
153def main():
154    """Test the camera zoom behavior."""
155
156    z_test_list = []
157    fls = []
158    circles = []
159    with its.device.ItsSession() as cam:
160        props = cam.get_camera_properties()
161        its.caps.skip_unless(its.caps.zoom_ratio_range(props))
162
163        z_range = props['android.control.zoomRatioRange']
164        print 'testing zoomRatioRange:', z_range
165        yuv_size = its.objects.get_largest_yuv_format(props)
166        size = [yuv_size['width'], yuv_size['height']]
167        debug = its.caps.debug_mode()
168
169        z_min, z_max = float(z_range[0]), float(z_range[1])
170        its.caps.skip_unless(z_max >= z_min*ZOOM_MIN_THRESH)
171        z_list = np.arange(z_min, z_max, float(z_max-z_min)/(NUM_STEPS-1))
172        z_list = np.append(z_list, z_max)
173
174        # do captures over zoom range
175        req = its.objects.auto_capture_request()
176        for i, z in enumerate(z_list):
177            print 'zoom ratio: %.2f' % z
178            req['android.control.zoomRatio'] = z
179            cap = cam.do_capture(req, cam.CAP_YUV)
180            img = its.image.convert_capture_to_rgb_image(cap, props=props)
181
182            # convert to [0, 255] images with unsigned integer
183            img *= 255
184            img = img.astype(np.uint8)
185
186            # Find the circles in img
187            circle = find_center_circle(
188                    img, '%s_%s.jpg' % (NAME, round(z, 2)), CIRCLE_COLOR,
189                    min_area=MIN_AREA_RATIO*size[0]*size[1]*z*z, debug=debug)
190            if circle_cropped(circle, size):
191                print 'zoom %.2f is too large! Skip further captures' % z
192                break
193            circles.append(circle)
194            z_test_list.append(z)
195            fls.append(cap['metadata']['android.lens.focalLength'])
196
197    # assert some range is tested before circles get too big
198    zoom_max_thresh = ZOOM_MAX_THRESH
199    if z_max < ZOOM_MAX_THRESH:
200        zoom_max_thresh = z_max
201    msg = 'Max zoom level tested: %d, THRESH: %d' % (
202            z_test_list[-1], zoom_max_thresh)
203    assert z_test_list[-1] >= zoom_max_thresh, msg
204
205    # initialize relative size w/ zoom[0] for diff zoom ratio checks
206    radius_0 = float(circles[0][2])
207    z_0 = float(z_test_list[0])
208
209    for i, z in enumerate(z_test_list):
210        print '\nZoom: %.2f, fl: %.2f' % (z, fls[i])
211        offset_abs = ((circles[i][0] - size[0]/2), (circles[i][1] - size[1]/2))
212        print 'Circle r: %.1f, center offset x, y: %d, %d' % (
213                circles[i][2], offset_abs[0], offset_abs[1])
214        z_ratio = z / z_0
215
216        # check relative size against zoom[0]
217        radius_ratio = circles[i][2]/radius_0
218        print 'radius_ratio: %.3f' % radius_ratio
219        msg = 'zoom: %.2f, radius ratio: %.2f, RTOL: %.2f' % (
220                z_ratio, radius_ratio, RADIUS_RTOL)
221        assert np.isclose(z_ratio, radius_ratio, rtol=RADIUS_RTOL), msg
222
223        # check relative offset against init vals w/ no focal length change
224        if i == 0 or fls[i-1] != fls[i]:  # set init values
225            z_init = float(z_test_list[i])
226            offset_init = (circles[i][0] - size[0] / 2,
227                           circles[i][1] - size[1] / 2)
228        else:  # check
229            z_ratio = z / z_init
230            offset_rel = (distance(offset_abs) / z_ratio /
231                          distance(offset_init))
232            print 'offset_rel: %.3f' % offset_rel
233            msg = 'zoom: %.2f, offset(rel): %.2f, RTOL: %.2f' % (
234                    z, offset_rel, OFFSET_RTOL)
235            assert np.isclose(offset_rel, 1.0, rtol=OFFSET_RTOL), msg
236
237
238if __name__ == '__main__':
239    main()
240