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