mirror of
https://github.com/wassname/scikit-image.git
synced 2026-07-02 06:48:05 +08:00
Merge pull request #250 from ahojnnes/approx-polygon
ENH: Add polygon approximation and subdivision.
This commit is contained in:
+2
-5
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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,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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user