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 15 import matplotlib 16 matplotlib.use('Agg') 17 18 import its.error 19 from matplotlib import pylab 20 import sys 21 from PIL import Image 22 import numpy 23 import math 24 import unittest 25 import cStringIO 26 import scipy.stats 27 import copy 28 import cv2 29 import os 30 31 def 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 36 class 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 167 class __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 196 if __name__ == '__main__': 197 unittest.main() 198