Merge pull request #250 from ahojnnes/approx-polygon

ENH: Add polygon approximation and subdivision.
This commit is contained in:
Stefan van der Walt
2012-08-24 02:53:17 -07:00
6 changed files with 309 additions and 9 deletions
+2 -5
View File
@@ -105,11 +105,8 @@
Shape views: ``util.shape.view_as_windows`` and ``util.shape.view_as_blocks``
- Johannes Schönberger
Polygon, circle and ellipse drawing functions
Adaptive thresholding
Implementation of Matlab's `regionprops`
Estimation of geometric transformation parameters
Local binary pattern texture classification
Drawing functions, adaptive thresholding, regionprops, geometric
transformations, LBPs, polygon approximations, and more.
- Pavel Campr
Fixes and tests for Histograms of Oriented Gradients.
+77
View File
@@ -0,0 +1,77 @@
"""
====================
Approximate Polygons
====================
This example shows how to approximate polygonal chains with the Douglas-Peucker
algorithm.
"""
import numpy as np
import matplotlib.pyplot as plt
from skimage.draw import ellipse
from skimage.measure import find_contours, approximate_polygon, \
subdivide_polygon
hand = np.array([[1.64516129, 1.16145833],
[1.64516129, 1.59375 ],
[1.35080645, 1.921875 ],
[1.375 , 2.18229167],
[1.68548387, 1.9375 ],
[1.60887097, 2.55208333],
[1.68548387, 2.69791667],
[1.76209677, 2.56770833],
[1.83064516, 1.97395833],
[1.89516129, 2.75 ],
[1.9516129 , 2.84895833],
[2.01209677, 2.76041667],
[1.99193548, 1.99479167],
[2.11290323, 2.63020833],
[2.2016129 , 2.734375 ],
[2.25403226, 2.60416667],
[2.14919355, 1.953125 ],
[2.30645161, 2.36979167],
[2.39112903, 2.36979167],
[2.41532258, 2.1875 ],
[2.1733871 , 1.703125 ],
[2.07782258, 1.16666667]])
# subdivide polygon using 2nd degree B-Splines
new_hand = hand.copy()
for _ in range(5):
new_hand = subdivide_polygon(new_hand, degree=2, preserve_ends=True)
# approximate subdivided polygon with Douglas-Peucker algorithm
appr_hand = approximate_polygon(new_hand, tolerance=0.02)
print "Number of coordinates:", len(hand), len(new_hand), len(appr_hand)
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(9, 4))
ax1.plot(hand[:, 0], hand[:, 1])
ax1.plot(new_hand[:, 0], new_hand[:, 1])
ax1.plot(appr_hand[:, 0], appr_hand[:, 1])
# create two ellipses in image
img = np.zeros((800, 800), 'int32')
rr, cc = ellipse(250, 250, 180, 230, img.shape)
img[rr, cc] = 1
rr, cc = ellipse(600, 600, 150, 90, img.shape)
img[rr, cc] = 1
plt.gray()
ax2.imshow(img)
# approximate / simplify coordinates of the two ellipses
for contour in find_contours(img, 0):
coords = approximate_polygon(contour, tolerance=2.5)
ax2.plot(coords[:, 1], coords[:, 0], '-r', linewidth=2)
coords2 = approximate_polygon(contour, tolerance=39.5)
ax2.plot(coords2[:, 1], coords2[:, 0], '-g', linewidth=2)
print "Number of coordinates:", len(contour), len(coords), len(coords2)
ax2.axis((0, 800, 0, 800))
plt.show()
+4 -4
View File
@@ -19,11 +19,11 @@ import numpy as np
img = np.zeros((500, 500, 3), 'uint8')
#: draw line
# draw line
rr, cc = line(120, 123, 20, 400)
img[rr,cc,0] = 255
#: fill polygon
# fill polygon
poly = np.array((
(300, 300),
(480, 320),
@@ -34,11 +34,11 @@ poly = np.array((
rr, cc = polygon(poly[:,0], poly[:,1], img.shape)
img[rr,cc,1] = 255
#: fill circle
# fill circle
rr, cc = circle(200, 200, 100, img.shape)
img[rr,cc,:] = (255, 255, 0)
#: fill ellipse
# fill ellipse
rr, cc = ellipse(300, 300, 100, 200, img.shape)
img[rr,cc,2] = 255
+1
View File
@@ -1,3 +1,4 @@
from .find_contours import find_contours
from ._regionprops import regionprops, perimeter
from ._structural_similarity import structural_similarity
from ._polygon import approximate_polygon, subdivide_polygon
+169
View File
@@ -0,0 +1,169 @@
import numpy as np
from scipy import signal
def approximate_polygon(coords, tolerance):
"""Approximate a polygonal chain with the specified tolerance.
It is based on the Douglas-Peucker algorithm.
Note that the approximated polygon is always within the convex hull of the
original polygon.
Parameters
----------
coords : (N, 2) array
Coordinate array.
tolerance : float
Maximum distance from original points of polygon to approximated
polygonal chain. If tolerance is 0, the original coordinate array
is returned.
Returns
-------
coords : (M, 2) array
Approximated polygonal chain where M <= N.
References
----------
.. [1] http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
"""
if tolerance == 0:
return coords
chain = np.zeros(coords.shape[0], 'bool')
# pre-allocate distance array for all points
dists = np.zeros(coords.shape[0])
chain[0] = True
chain[-1] = True
pos_stack = [(0, chain.shape[0] - 1)]
end_of_chain = False
while not end_of_chain:
start, end = pos_stack.pop()
# determine properties of current line segment
r0, c0 = coords[start, :]
r1, c1 = coords[end, :]
dr = r1 - r0
dc = c1 - c0
segment_angle = - np.arctan2(dr, dc)
segment_dist = c0 * np.sin(segment_angle) + r0 * np.cos(segment_angle)
# select points in-between line segment
segment_coords = coords[start + 1:end, :]
segment_dists = dists[start + 1:end]
# check whether to take perpendicular or euclidean distance with
# inner product of vectors
# vectors from points -> start and end
dr0 = segment_coords[:, 0] - r0
dc0 = segment_coords[:, 1] - c0
dr1 = segment_coords[:, 0] - r1
dc1 = segment_coords[:, 1] - c1
# vectors points -> start and end projected on start -> end vector
projected_lengths0 = dr0 * dr + dc0 * dc
projected_lengths1 = - dr1 * dr - dc1 * dc
perp = np.logical_and(projected_lengths0 > 0,
projected_lengths1 > 0)
eucl = np.logical_not(perp)
segment_dists[perp] = np.abs(
segment_coords[perp, 0] * np.cos(segment_angle)
+ segment_coords[perp, 1] * np.sin(segment_angle)
- segment_dist
)
segment_dists[eucl] = np.minimum(
# distance to start point
np.sqrt(dc0[eucl] ** 2 + dr0[eucl] ** 2),
# distance to end point
np.sqrt(dc1[eucl] ** 2 + dr1[eucl] ** 2)
)
if np.any(segment_dists > tolerance):
# select point with maximum distance to line
new_end = start + np.argmax(segment_dists) + 1
pos_stack.append((new_end, end))
pos_stack.append((start, new_end))
chain[new_end] = True
if len(pos_stack) == 0:
end_of_chain = True
return coords[chain, :]
# B-Spline subdivision
_SUBDIVISION_MASKS = {
# degree: (mask_even, mask_odd)
# extracted from (degree + 2)th row of Pascal's triangle
1: ([1, 1], [1, 1]),
2: ([3, 1], [1, 3]),
3: ([1, 6, 1], [0, 4, 4]),
4: ([5, 10, 1], [1, 10, 5]),
5: ([1, 15, 15, 1], [0, 6, 20, 6]),
6: ([7, 35, 21, 1], [1, 21, 35, 7]),
7: ([1, 28, 70, 28, 1], [0, 8, 56, 56, 8]),
}
def subdivide_polygon(coords, degree=2, preserve_ends=False):
"""Subdivision of polygonal curves using B-Splines.
Note that the resulting curve is always within the convex hull of the
original polygon. Circular polygons stay closed after subdivision.
Parameters
----------
coords : (N, 2) array
Coordinate array.
degree : {1, 2, 3, 4, 5, 6, 7}, optional
Degree of B-Spline. Default is 2.
preserve_ends : bool, optional
Preserve first and last coordinate of non-circular polygon. Default is
False.
Returns
-------
coords : (M, 2) array
Subdivided coordinate array.
References
----------
.. [1] http://mrl.nyu.edu/publications/subdiv-course2000/coursenotes00.pdf
"""
if degree not in _SUBDIVISION_MASKS:
raise ValueError("Invalid B-Spline degree. Only degree 1 - 7 is "
"supported.")
circular = np.all(coords[0, :] == coords[-1, :])
method = 'valid'
if circular:
# remove last coordinate because of wrapping
coords = coords[:-1, :]
# circular convolution by wrapping boundaries
method = 'same'
mask_even, mask_odd = _SUBDIVISION_MASKS[degree]
# divide by total weight
mask_even = np.array(mask_even, np.float) / (2 ** degree)
mask_odd = np.array(mask_odd, np.float) / (2 ** degree)
even = signal.convolve2d(coords.T, np.atleast_2d(mask_even), mode=method,
boundary='wrap')
odd = signal.convolve2d(coords.T, np.atleast_2d(mask_odd), mode=method,
boundary='wrap')
out = np.zeros((even.shape[1] + odd.shape[1], 2))
out[1::2] = even.T
out[::2] = odd.T
if circular:
# close polygon
out = np.vstack([out, out[0, :]])
if preserve_ends and not circular:
out = np.vstack([coords[0, :], out, coords[-1, :]])
return out
+56
View File
@@ -0,0 +1,56 @@
import numpy as np
from skimage.measure import approximate_polygon, subdivide_polygon
from skimage.measure._polygon import _SUBDIVISION_MASKS
square = np.array([
[0, 0], [0, 1], [0, 2], [0, 3],
[1, 3], [2, 3], [3, 3],
[3, 2], [3, 1], [3, 0],
[2, 0], [1, 0], [0, 0]
])
def test_approximate_polygon():
out = approximate_polygon(square, 0.1)
np.testing.assert_array_equal(out, square[(0, 3, 6, 9, 12), :])
out = approximate_polygon(square, 2.2)
np.testing.assert_array_equal(out, square[(0, 6, 12), :])
out = approximate_polygon(square[(0, 1, 3, 4, 5, 6, 7, 9, 11, 12), :], 0.1)
np.testing.assert_array_equal(out, square[(0, 3, 6, 9, 12), :])
out = approximate_polygon(square, -1)
np.testing.assert_array_equal(out, square)
def test_subdivide_polygon():
new_square1 = square
new_square2 = square[:-1]
new_square3 = square[:-1]
# test iterative subdvision
for _ in range(10):
square1, square2, square3 = new_square1, new_square2, new_square3
# test different B-Spline degrees
for degree in range(1, 7):
mask_len = len(_SUBDIVISION_MASKS[degree][0])
# test circular
new_square1 = subdivide_polygon(square1, degree)
np.testing.assert_array_equal(new_square1[-1], new_square1[0])
np.testing.assert_equal(new_square1.shape[0],
2 * square1.shape[0] - 1)
# test non-circular
new_square2 = subdivide_polygon(square2, degree)
np.testing.assert_equal(new_square2.shape[0],
2 * (square2.shape[0] - mask_len + 1))
# test non-circular, preserve_ends
new_square3 = subdivide_polygon(square3, degree, True)
np.testing.assert_equal(new_square3[0], square3[0])
np.testing.assert_equal(new_square3[-1], square3[-1])
print mask_len
np.testing.assert_equal(new_square3.shape[0],
2 * (square3.shape[0] - mask_len + 2))
if __name__ == "__main__":
np.testing.run_module_suite()