Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 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 | |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 15 | import os |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 16 | import unittest |
| 17 | |
| 18 | import cv2 |
Clemenz Portmann | 96bed40 | 2017-12-15 09:59:45 -0800 | [diff] [blame] | 19 | import its.caps |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 20 | import its.device |
| 21 | import its.error |
Will Guedes | 0f11cfb | 2018-04-09 08:47:31 -0500 | [diff] [blame] | 22 | import its.image |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 23 | import numpy |
| 24 | |
| 25 | VGA_HEIGHT = 480 |
| 26 | VGA_WIDTH = 640 |
| 27 | |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 28 | |
| 29 | def scale_img(img, scale=1.0): |
| 30 | """Scale and image based on a real number scale factor.""" |
| 31 | dim = (int(img.shape[1]*scale), int(img.shape[0]*scale)) |
| 32 | return cv2.resize(img.copy(), dim, interpolation=cv2.INTER_AREA) |
| 33 | |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 34 | |
Clemenz Portmann | c47c802 | 2017-04-04 09:10:30 -0700 | [diff] [blame] | 35 | def gray_scale_img(img): |
| 36 | """Return gray scale version of image.""" |
| 37 | if len(img.shape) == 2: |
| 38 | img_gray = img.copy() |
| 39 | elif len(img.shape) == 3: |
| 40 | if img.shape[2] == 1: |
| 41 | img_gray = img[:, :, 0].copy() |
| 42 | else: |
| 43 | img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) |
| 44 | return img_gray |
| 45 | |
| 46 | |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 47 | class Chart(object): |
| 48 | """Definition for chart object. |
| 49 | |
| 50 | Defines PNG reference file, chart size and distance, and scaling range. |
| 51 | """ |
| 52 | |
| 53 | def __init__(self, chart_file, height, distance, scale_start, scale_stop, |
Clemenz Portmann | 9f852a0 | 2018-10-31 19:52:25 -0700 | [diff] [blame] | 54 | scale_step, camera_id=None): |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 55 | """Initial constructor for class. |
| 56 | |
| 57 | Args: |
| 58 | chart_file: str; absolute path to png file of chart |
| 59 | height: float; height in cm of displayed chart |
| 60 | distance: float; distance in cm from camera of displayed chart |
| 61 | scale_start: float; start value for scaling for chart search |
| 62 | scale_stop: float; stop value for scaling for chart search |
| 63 | scale_step: float; step value for scaling for chart search |
Clemenz Portmann | 9f852a0 | 2018-10-31 19:52:25 -0700 | [diff] [blame] | 64 | camera_id: int; camera used for extractor |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 65 | """ |
| 66 | self._file = chart_file |
| 67 | self._height = height |
| 68 | self._distance = distance |
| 69 | self._scale_start = scale_start |
| 70 | self._scale_stop = scale_stop |
| 71 | self._scale_step = scale_step |
Clemenz Portmann | c47c802 | 2017-04-04 09:10:30 -0700 | [diff] [blame] | 72 | self.xnorm, self.ynorm, self.wnorm, self.hnorm, self.scale = its.image.chart_located_per_argv() |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 73 | if not self.xnorm: |
Clemenz Portmann | 9f852a0 | 2018-10-31 19:52:25 -0700 | [diff] [blame] | 74 | with its.device.ItsSession(camera_id) as cam: |
Clemenz Portmann | 96bed40 | 2017-12-15 09:59:45 -0800 | [diff] [blame] | 75 | props = cam.get_camera_properties() |
| 76 | if its.caps.read_3a(props): |
| 77 | self.locate(cam, props) |
| 78 | else: |
| 79 | print 'Chart locator skipped.' |
| 80 | self._set_scale_factors_to_one() |
| 81 | |
| 82 | def _set_scale_factors_to_one(self): |
| 83 | """Set scale factors to 1.0 for skipped tests.""" |
| 84 | self.wnorm = 1.0 |
| 85 | self.hnorm = 1.0 |
| 86 | self.xnorm = 0.0 |
| 87 | self.ynorm = 0.0 |
| 88 | self.scale = 1.0 |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 89 | |
| 90 | def _calc_scale_factors(self, cam, props, fmt, s, e, fd): |
| 91 | """Take an image with s, e, & fd to find the chart location. |
| 92 | |
| 93 | Args: |
| 94 | cam: An open device session. |
| 95 | props: Properties of cam |
| 96 | fmt: Image format for the capture |
| 97 | s: Sensitivity for the AF request as defined in |
| 98 | android.sensor.sensitivity |
| 99 | e: Exposure time for the AF request as defined in |
| 100 | android.sensor.exposureTime |
| 101 | fd: float; autofocus lens position |
| 102 | Returns: |
| 103 | template: numpy array; chart template for locator |
| 104 | img_3a: numpy array; RGB image for chart location |
| 105 | scale_factor: float; scaling factor for chart search |
| 106 | """ |
| 107 | req = its.objects.manual_capture_request(s, e) |
| 108 | req['android.lens.focusDistance'] = fd |
| 109 | cap_chart = its.image.stationary_lens_cap(cam, req, fmt) |
| 110 | img_3a = its.image.convert_capture_to_rgb_image(cap_chart, props) |
Clemenz Portmann | c47c802 | 2017-04-04 09:10:30 -0700 | [diff] [blame] | 111 | img_3a = its.image.rotate_img_per_argv(img_3a) |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 112 | its.image.write_image(img_3a, 'af_scene.jpg') |
| 113 | template = cv2.imread(self._file, cv2.IMREAD_ANYDEPTH) |
| 114 | focal_l = cap_chart['metadata']['android.lens.focalLength'] |
| 115 | pixel_pitch = (props['android.sensor.info.physicalSize']['height'] / |
| 116 | img_3a.shape[0]) |
| 117 | print ' Chart distance: %.2fcm' % self._distance |
| 118 | print ' Chart height: %.2fcm' % self._height |
| 119 | print ' Focal length: %.2fmm' % focal_l |
| 120 | print ' Pixel pitch: %.2fum' % (pixel_pitch*1E3) |
| 121 | print ' Template height: %dpixels' % template.shape[0] |
| 122 | chart_pixel_h = self._height * focal_l / (self._distance * pixel_pitch) |
| 123 | scale_factor = template.shape[0] / chart_pixel_h |
| 124 | print 'Chart/image scale factor = %.2f' % scale_factor |
| 125 | return template, img_3a, scale_factor |
| 126 | |
Clemenz Portmann | 96bed40 | 2017-12-15 09:59:45 -0800 | [diff] [blame] | 127 | def locate(self, cam, props): |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 128 | """Find the chart in the image, and append location to chart object. |
| 129 | |
| 130 | The values appended are: |
| 131 | xnorm: float; [0, 1] left loc of chart in scene |
| 132 | ynorm: float; [0, 1] top loc of chart in scene |
| 133 | wnorm: float; [0, 1] width of chart in scene |
| 134 | hnorm: float; [0, 1] height of chart in scene |
Clemenz Portmann | c47c802 | 2017-04-04 09:10:30 -0700 | [diff] [blame] | 135 | scale: float; scale factor to extract chart |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 136 | |
| 137 | Args: |
| 138 | cam: An open device session |
Clemenz Portmann | 96bed40 | 2017-12-15 09:59:45 -0800 | [diff] [blame] | 139 | props: Camera properties |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 140 | """ |
Clemenz Portmann | 96bed40 | 2017-12-15 09:59:45 -0800 | [diff] [blame] | 141 | if its.caps.read_3a(props): |
| 142 | s, e, _, _, fd = cam.do_3a(get_results=True) |
| 143 | fmt = {'format': 'yuv', 'width': VGA_WIDTH, 'height': VGA_HEIGHT} |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 144 | chart, scene, s_factor = self._calc_scale_factors(cam, props, fmt, |
| 145 | s, e, fd) |
| 146 | else: |
Clemenz Portmann | 96bed40 | 2017-12-15 09:59:45 -0800 | [diff] [blame] | 147 | print 'Chart locator skipped.' |
| 148 | self._set_scale_factors_to_one() |
| 149 | return |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 150 | scale_start = self._scale_start * s_factor |
| 151 | scale_stop = self._scale_stop * s_factor |
| 152 | scale_step = self._scale_step * s_factor |
Clemenz Portmann | c47c802 | 2017-04-04 09:10:30 -0700 | [diff] [blame] | 153 | self.scale = s_factor |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 154 | max_match = [] |
| 155 | # check for normalized image |
| 156 | if numpy.amax(scene) <= 1.0: |
| 157 | scene = (scene * 255.0).astype(numpy.uint8) |
Clemenz Portmann | c47c802 | 2017-04-04 09:10:30 -0700 | [diff] [blame] | 158 | scene_gray = gray_scale_img(scene) |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 159 | print 'Finding chart in scene...' |
| 160 | for scale in numpy.arange(scale_start, scale_stop, scale_step): |
| 161 | scene_scaled = scale_img(scene_gray, scale) |
Clemenz Portmann | 8ecc005 | 2018-08-13 10:13:26 -0700 | [diff] [blame] | 162 | if (scene_scaled.shape[0] < chart.shape[0] or |
| 163 | scene_scaled.shape[1] < chart.shape[1]): |
| 164 | continue |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 165 | result = cv2.matchTemplate(scene_scaled, chart, cv2.TM_CCOEFF) |
| 166 | _, opt_val, _, top_left_scaled = cv2.minMaxLoc(result) |
| 167 | # print out scale and match |
| 168 | print ' scale factor: %.3f, opt val: %.f' % (scale, opt_val) |
| 169 | max_match.append((opt_val, top_left_scaled)) |
| 170 | |
| 171 | # determine if optimization results are valid |
| 172 | opt_values = [x[0] for x in max_match] |
| 173 | if 2.0*min(opt_values) > max(opt_values): |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 174 | estring = ('Warning: unable to find chart in scene!\n' |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 175 | 'Check camera distance and self-reported ' |
| 176 | 'pixel pitch, focal length and hyperfocal distance.') |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 177 | print estring |
Clemenz Portmann | 96bed40 | 2017-12-15 09:59:45 -0800 | [diff] [blame] | 178 | self._set_scale_factors_to_one() |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 179 | else: |
| 180 | if (max(opt_values) == opt_values[0] or |
| 181 | max(opt_values) == opt_values[len(opt_values)-1]): |
| 182 | estring = ('Warning: chart is at extreme range of locator ' |
| 183 | 'check.\n') |
| 184 | print estring |
| 185 | # find max and draw bbox |
| 186 | match_index = max_match.index(max(max_match, key=lambda x: x[0])) |
Clemenz Portmann | c47c802 | 2017-04-04 09:10:30 -0700 | [diff] [blame] | 187 | self.scale = scale_start + scale_step * match_index |
| 188 | print 'Optimum scale factor: %.3f' % self.scale |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 189 | top_left_scaled = max_match[match_index][1] |
| 190 | h, w = chart.shape |
| 191 | bottom_right_scaled = (top_left_scaled[0] + w, |
| 192 | top_left_scaled[1] + h) |
Clemenz Portmann | c47c802 | 2017-04-04 09:10:30 -0700 | [diff] [blame] | 193 | top_left = (int(top_left_scaled[0]/self.scale), |
| 194 | int(top_left_scaled[1]/self.scale)) |
| 195 | bottom_right = (int(bottom_right_scaled[0]/self.scale), |
| 196 | int(bottom_right_scaled[1]/self.scale)) |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 197 | self.wnorm = float((bottom_right[0]) - top_left[0]) / scene.shape[1] |
| 198 | self.hnorm = float((bottom_right[1]) - top_left[1]) / scene.shape[0] |
| 199 | self.xnorm = float(top_left[0]) / scene.shape[1] |
| 200 | self.ynorm = float(top_left[1]) / scene.shape[0] |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 201 | |
| 202 | |
Will Guedes | 0f11cfb | 2018-04-09 08:47:31 -0500 | [diff] [blame] | 203 | def get_angle(input_img): |
| 204 | """Computes anglular inclination of chessboard in input_img. |
| 205 | |
| 206 | Angle estimation algoritm description: |
| 207 | Input: 2D grayscale image of chessboard. |
| 208 | Output: Angle of rotation of chessboard perpendicular to |
| 209 | chessboard. Assumes chessboard and camera are parallel to |
| 210 | each other. |
| 211 | |
| 212 | 1) Use adaptive threshold to make image binary |
| 213 | 2) Find countours |
| 214 | 3) Filter out small contours |
| 215 | 4) Filter out all non-square contours |
| 216 | 5) Compute most common square shape. |
| 217 | The assumption here is that the most common square instances |
| 218 | are the chessboard squares. We've shown that with our current |
| 219 | tuning, we can robustly identify the squares on the sensor fusion |
| 220 | chessboard. |
| 221 | 6) Return median angle of most common square shape. |
| 222 | |
| 223 | USAGE NOTE: This function has been tuned to work for the chessboard used in |
| 224 | the sensor_fusion tests. See images in test_images/rotated_chessboard/ for |
| 225 | sample captures. If this function is used with other chessboards, it may not |
| 226 | work as expected. |
| 227 | |
| 228 | TODO: Make algorithm more robust so it works on any type of |
| 229 | chessboard. |
| 230 | |
| 231 | Args: |
| 232 | input_img (2D numpy.ndarray): Grayscale image stored as a 2D |
| 233 | numpy array. |
| 234 | |
| 235 | Returns: |
| 236 | Median angle of squares in degrees identified in the image. |
| 237 | """ |
| 238 | # Tuning parameters |
Clemenz Portmann | 51bdde5 | 2018-05-11 12:01:33 -0700 | [diff] [blame] | 239 | min_square_area = (float)(input_img.shape[1] * 0.05) |
Will Guedes | 0f11cfb | 2018-04-09 08:47:31 -0500 | [diff] [blame] | 240 | |
| 241 | # Creates copy of image to avoid modifying original. |
| 242 | img = numpy.array(input_img, copy=True) |
| 243 | |
| 244 | # Scale pixel values from 0-1 to 0-255 |
Clemenz Portmann | 51bdde5 | 2018-05-11 12:01:33 -0700 | [diff] [blame] | 245 | img *= 255 |
Will Guedes | 0f11cfb | 2018-04-09 08:47:31 -0500 | [diff] [blame] | 246 | img = img.astype(numpy.uint8) |
| 247 | |
| 248 | thresh = cv2.adaptiveThreshold( |
| 249 | img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 201, 2) |
| 250 | |
| 251 | # Find all contours |
Clemenz Portmann | 51bdde5 | 2018-05-11 12:01:33 -0700 | [diff] [blame] | 252 | contours = [] |
| 253 | cv2_version = cv2.__version__ |
| 254 | if cv2_version.startswith('2.4.'): |
| 255 | contours, _ = cv2.findContours( |
| 256 | thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) |
| 257 | elif cv2_version.startswith('3.2.'): |
| 258 | _, contours, _ = cv2.findContours( |
| 259 | thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) |
Will Guedes | 0f11cfb | 2018-04-09 08:47:31 -0500 | [diff] [blame] | 260 | |
| 261 | # Filter contours to squares only. |
| 262 | square_contours = [] |
| 263 | |
| 264 | for contour in contours: |
| 265 | rect = cv2.minAreaRect(contour) |
| 266 | _, (width, height), angle = rect |
| 267 | |
| 268 | # Skip non-squares (with 0.1 tolerance) |
| 269 | tolerance = 0.1 |
| 270 | if width < height * (1 - tolerance) or width > height * (1 + tolerance): |
| 271 | continue |
| 272 | |
| 273 | # Remove very small contours. |
| 274 | # These are usually just tiny dots due to noise. |
| 275 | area = cv2.contourArea(contour) |
| 276 | if area < min_square_area: |
| 277 | continue |
| 278 | |
Clemenz Portmann | 51bdde5 | 2018-05-11 12:01:33 -0700 | [diff] [blame] | 279 | if cv2_version.startswith('2.4.'): |
| 280 | box = numpy.int0(cv2.cv.BoxPoints(rect)) |
| 281 | elif cv2_version.startswith('3.2.'): |
| 282 | box = numpy.int0(cv2.boxPoints(rect)) |
Will Guedes | 0f11cfb | 2018-04-09 08:47:31 -0500 | [diff] [blame] | 283 | square_contours.append(contour) |
| 284 | |
| 285 | areas = [] |
| 286 | for contour in square_contours: |
| 287 | area = cv2.contourArea(contour) |
| 288 | areas.append(area) |
| 289 | |
| 290 | median_area = numpy.median(areas) |
| 291 | |
| 292 | filtered_squares = [] |
| 293 | filtered_angles = [] |
| 294 | for square in square_contours: |
| 295 | area = cv2.contourArea(square) |
| 296 | if area < median_area * 0.90 or area > median_area * 1.10: |
| 297 | continue |
| 298 | |
| 299 | filtered_squares.append(square) |
| 300 | _, (width, height), angle = cv2.minAreaRect(square) |
| 301 | filtered_angles.append(angle) |
| 302 | |
| 303 | if len(filtered_angles) < 10: |
| 304 | return None |
| 305 | |
| 306 | return numpy.median(filtered_angles) |
| 307 | |
| 308 | |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 309 | class __UnitTest(unittest.TestCase): |
| 310 | """Run a suite of unit tests on this module. |
| 311 | """ |
| 312 | |
| 313 | def test_compute_image_sharpness(self): |
| 314 | """Unit test for compute_img_sharpness. |
| 315 | |
| 316 | Test by using PNG of ISO12233 chart and blurring intentionally. |
| 317 | 'sharpness' should drop off by sqrt(2) for 2x blur of image. |
| 318 | |
| 319 | We do one level of blur as PNG image is not perfect. |
| 320 | """ |
| 321 | yuv_full_scale = 1023.0 |
| 322 | chart_file = os.path.join(os.environ['CAMERA_ITS_TOP'], 'pymodules', |
| 323 | 'its', 'test_images', 'ISO12233.png') |
| 324 | chart = cv2.imread(chart_file, cv2.IMREAD_ANYDEPTH) |
| 325 | white_level = numpy.amax(chart).astype(float) |
| 326 | sharpness = {} |
| 327 | for j in [2, 4, 8]: |
| 328 | blur = cv2.blur(chart, (j, j)) |
| 329 | blur = blur[:, :, numpy.newaxis] |
| 330 | sharpness[j] = (yuv_full_scale * |
Clemenz Portmann | 51d765f | 2017-07-14 14:56:45 -0700 | [diff] [blame] | 331 | its.image.compute_image_sharpness(blur / |
| 332 | white_level)) |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 333 | self.assertTrue(numpy.isclose(sharpness[2]/sharpness[4], |
| 334 | numpy.sqrt(2), atol=0.1)) |
| 335 | self.assertTrue(numpy.isclose(sharpness[4]/sharpness[8], |
| 336 | numpy.sqrt(2), atol=0.1)) |
| 337 | |
Will Guedes | 0f11cfb | 2018-04-09 08:47:31 -0500 | [diff] [blame] | 338 | def test_get_angle_identify_unrotated_chessboard_angle(self): |
| 339 | basedir = os.path.join( |
| 340 | os.path.dirname(__file__), 'test_images/rotated_chessboards/') |
| 341 | |
| 342 | normal_img_path = os.path.join(basedir, 'normal.jpg') |
| 343 | wide_img_path = os.path.join(basedir, 'wide.jpg') |
| 344 | |
| 345 | normal_img = cv2.cvtColor( |
| 346 | cv2.imread(normal_img_path), cv2.COLOR_BGR2GRAY) |
| 347 | wide_img = cv2.cvtColor( |
| 348 | cv2.imread(wide_img_path), cv2.COLOR_BGR2GRAY) |
| 349 | |
| 350 | assert get_angle(normal_img) == 0 |
| 351 | assert get_angle(wide_img) == 0 |
| 352 | |
| 353 | def test_get_angle_identify_rotated_chessboard_angle(self): |
| 354 | basedir = os.path.join( |
| 355 | os.path.dirname(__file__), 'test_images/rotated_chessboards/') |
| 356 | |
| 357 | # Array of the image files and angles containing rotated chessboards. |
| 358 | test_cases = [ |
| 359 | ('_15_ccw', 15), |
| 360 | ('_30_ccw', 30), |
| 361 | ('_45_ccw', 45), |
| 362 | ('_60_ccw', 60), |
| 363 | ('_75_ccw', 75), |
| 364 | ('_90_ccw', 90) |
| 365 | ] |
| 366 | |
| 367 | # For each rotated image pair (normal, wide). Check if angle is |
| 368 | # identified as expected. |
| 369 | for suffix, angle in test_cases: |
| 370 | # Define image paths |
| 371 | normal_img_path = os.path.join( |
| 372 | basedir, 'normal{}.jpg'.format(suffix)) |
| 373 | wide_img_path = os.path.join( |
| 374 | basedir, 'wide{}.jpg'.format(suffix)) |
| 375 | |
| 376 | # Load and color convert images |
| 377 | normal_img = cv2.cvtColor( |
| 378 | cv2.imread(normal_img_path), cv2.COLOR_BGR2GRAY) |
| 379 | wide_img = cv2.cvtColor( |
| 380 | cv2.imread(wide_img_path), cv2.COLOR_BGR2GRAY) |
| 381 | |
| 382 | # Assert angle is as expected up to 2.0 degrees of accuracy. |
| 383 | assert numpy.isclose( |
| 384 | abs(get_angle(normal_img)), angle, 2.0) |
| 385 | assert numpy.isclose( |
| 386 | abs(get_angle(wide_img)), angle, 2.0) |
| 387 | |
Yin-Chia Yeh | b2a3865 | 2016-10-14 16:41:06 -0700 | [diff] [blame] | 388 | |
| 389 | if __name__ == '__main__': |
| 390 | unittest.main() |