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 matplotlib
16matplotlib.use('Agg')
17
18import its.error
19from matplotlib import pylab
20import sys
21from PIL import Image
22import numpy
23import math
24import unittest
25import cStringIO
26import scipy.stats
27import copy
28import cv2
29import os
30
31def scale_img(img, scale=1.0):
32    """Scale and image based on a real number scale factor."""
33    dim = (int(img.shape[1]*scale), int(img.shape[0]*scale))
34    return cv2.resize(img.copy(), dim, interpolation=cv2.INTER_AREA)
35
36class Chart(object):
37    """Definition for chart object.
38
39    Defines PNG reference file, chart size and distance, and scaling range.
40    """
41
42    def __init__(self, chart_file, height, distance, scale_start, scale_stop,
43                 scale_step):
44        """Initial constructor for class.
45
46        Args:
47            chart_file:     str; absolute path to png file of chart
48            height:         float; height in cm of displayed chart
49            distance:       float; distance in cm from camera of displayed chart
50            scale_start:    float; start value for scaling for chart search
51            scale_stop:     float; stop value for scaling for chart search
52            scale_step:     float; step value for scaling for chart search
53        """
54        self._file = chart_file
55        self._height = height
56        self._distance = distance
57        self._scale_start = scale_start
58        self._scale_stop = scale_stop
59        self._scale_step = scale_step
60
61    def _calc_scale_factors(self, cam, props, fmt, s, e, fd):
62        """Take an image with s, e, & fd to find the chart location.
63
64        Args:
65            cam:            An open device session.
66            props:          Properties of cam
67            fmt:            Image format for the capture
68            s:              Sensitivity for the AF request as defined in
69                            android.sensor.sensitivity
70            e:              Exposure time for the AF request as defined in
71                            android.sensor.exposureTime
72            fd:             float; autofocus lens position
73        Returns:
74            template:       numpy array; chart template for locator
75            img_3a:         numpy array; RGB image for chart location
76            scale_factor:   float; scaling factor for chart search
77        """
78        req = its.objects.manual_capture_request(s, e)
79        req['android.lens.focusDistance'] = fd
80        cap_chart = its.image.stationary_lens_cap(cam, req, fmt)
81        img_3a = its.image.convert_capture_to_rgb_image(cap_chart, props)
82        img_3a = its.image.flip_mirror_img_per_argv(img_3a)
83        its.image.write_image(img_3a, 'af_scene.jpg')
84        template = cv2.imread(self._file, cv2.IMREAD_ANYDEPTH)
85        focal_l = cap_chart['metadata']['android.lens.focalLength']
86        pixel_pitch = (props['android.sensor.info.physicalSize']['height'] /
87                       img_3a.shape[0])
88        print ' Chart distance: %.2fcm' % self._distance
89        print ' Chart height: %.2fcm' % self._height
90        print ' Focal length: %.2fmm' % focal_l
91        print ' Pixel pitch: %.2fum' % (pixel_pitch*1E3)
92        print ' Template height: %dpixels' % template.shape[0]
93        chart_pixel_h = self._height * focal_l / (self._distance * pixel_pitch)
94        scale_factor = template.shape[0] / chart_pixel_h
95        print 'Chart/image scale factor = %.2f' % scale_factor
96        return template, img_3a, scale_factor
97
98    def locate(self, cam, props, fmt, s, e, fd):
99        """Find the chart in the image.
100
101        Args:
102            cam:            An open device session
103            props:          Properties of cam
104            fmt:            Image format for the capture
105            s:              Sensitivity for the AF request as defined in
106                            android.sensor.sensitivity
107            e:              Exposure time for the AF request as defined in
108                            android.sensor.exposureTime
109            fd:             float; autofocus lens position
110
111        Returns:
112            xnorm:          float; [0, 1] left loc of chart in scene
113            ynorm:          float; [0, 1] top loc of chart in scene
114            wnorm:          float; [0, 1] width of chart in scene
115            hnorm:          float; [0, 1] height of chart in scene
116        """
117        chart, scene, s_factor = self._calc_scale_factors(cam, props, fmt,
118                                                          s, e, fd)
119        scale_start = self._scale_start * s_factor
120        scale_stop = self._scale_stop * s_factor
121        scale_step = self._scale_step * s_factor
122        max_match = []
123        # check for normalized image
124        if numpy.amax(scene) <= 1.0:
125            scene = (scene * 255.0).astype(numpy.uint8)
126        if len(scene.shape) == 2:
127            scene_gray = scene.copy()
128        elif len(scene.shape) == 3:
129            if scene.shape[2] == 1:
130                scene_gray = scene[:, :, 0]
131            else:
132                scene_gray = cv2.cvtColor(scene.copy(), cv2.COLOR_RGB2GRAY)
133        print 'Finding chart in scene...'
134        for scale in numpy.arange(scale_start, scale_stop, scale_step):
135            scene_scaled = scale_img(scene_gray, scale)
136            result = cv2.matchTemplate(scene_scaled, chart, cv2.TM_CCOEFF)
137            _, opt_val, _, top_left_scaled = cv2.minMaxLoc(result)
138            # print out scale and match
139            print ' scale factor: %.3f, opt val: %.f' % (scale, opt_val)
140            max_match.append((opt_val, top_left_scaled))
141
142        # determine if optimization results are valid
143        opt_values = [x[0] for x in max_match]
144        if 2.0*min(opt_values) > max(opt_values):
145            estring = ('Unable to find chart in scene!\n'
146                       'Check camera distance and self-reported '
147                       'pixel pitch, focal length and hyperfocal distance.')
148            raise its.error.Error(estring)
149        # find max and draw bbox
150        match_index = max_match.index(max(max_match, key=lambda x: x[0]))
151        scale = scale_start + scale_step * match_index
152        print 'Optimum scale factor: %.3f' %  scale
153        top_left_scaled = max_match[match_index][1]
154        h, w = chart.shape
155        bottom_right_scaled = (top_left_scaled[0] + w, top_left_scaled[1] + h)
156        top_left = (int(top_left_scaled[0]/scale),
157                    int(top_left_scaled[1]/scale))
158        bottom_right = (int(bottom_right_scaled[0]/scale),
159                        int(bottom_right_scaled[1]/scale))
160        wnorm = float((bottom_right[0]) - top_left[0]) / scene.shape[1]
161        hnorm = float((bottom_right[1]) - top_left[1]) / scene.shape[0]
162        xnorm = float(top_left[0]) / scene.shape[1]
163        ynorm = float(top_left[1]) / scene.shape[0]
164        return xnorm, ynorm, wnorm, hnorm
165
166
167class __UnitTest(unittest.TestCase):
168    """Run a suite of unit tests on this module.
169    """
170
171    def test_compute_image_sharpness(self):
172        """Unit test for compute_img_sharpness.
173
174        Test by using PNG of ISO12233 chart and blurring intentionally.
175        'sharpness' should drop off by sqrt(2) for 2x blur of image.
176
177        We do one level of blur as PNG image is not perfect.
178        """
179        yuv_full_scale = 1023.0
180        chart_file = os.path.join(os.environ['CAMERA_ITS_TOP'], 'pymodules',
181                                  'its', 'test_images', 'ISO12233.png')
182        chart = cv2.imread(chart_file, cv2.IMREAD_ANYDEPTH)
183        white_level = numpy.amax(chart).astype(float)
184        sharpness = {}
185        for j in [2, 4, 8]:
186            blur = cv2.blur(chart, (j, j))
187            blur = blur[:, :, numpy.newaxis]
188            sharpness[j] = (yuv_full_scale *
189                    its.image.compute_image_sharpness(blur / white_level))
190        self.assertTrue(numpy.isclose(sharpness[2]/sharpness[4],
191                                      numpy.sqrt(2), atol=0.1))
192        self.assertTrue(numpy.isclose(sharpness[4]/sharpness[8],
193                                      numpy.sqrt(2), atol=0.1))
194
195
196if __name__ == '__main__':
197    unittest.main()
198