scikit-image
168 строк · 5.1 Кб
1import numpy as np
2from scipy import signal
3
4
5def approximate_polygon(coords, tolerance):
6"""Approximate a polygonal chain with the specified tolerance.
7
8It is based on the Douglas-Peucker algorithm.
9
10Note that the approximated polygon is always within the convex hull of the
11original polygon.
12
13Parameters
14----------
15coords : (K, 2) array
16Coordinate array.
17tolerance : float
18Maximum distance from original points of polygon to approximated
19polygonal chain. If tolerance is 0, the original coordinate array
20is returned.
21
22Returns
23-------
24coords : (L, 2) array
25Approximated polygonal chain where L <= K.
26
27References
28----------
29.. [1] https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
30"""
31if tolerance <= 0:
32return coords
33
34chain = np.zeros(coords.shape[0], 'bool')
35# pre-allocate distance array for all points
36dists = np.zeros(coords.shape[0])
37chain[0] = True
38chain[-1] = True
39pos_stack = [(0, chain.shape[0] - 1)]
40end_of_chain = False
41
42while not end_of_chain:
43start, end = pos_stack.pop()
44# determine properties of current line segment
45r0, c0 = coords[start, :]
46r1, c1 = coords[end, :]
47dr = r1 - r0
48dc = c1 - c0
49segment_angle = -np.arctan2(dr, dc)
50segment_dist = c0 * np.sin(segment_angle) + r0 * np.cos(segment_angle)
51
52# select points in-between line segment
53segment_coords = coords[start + 1 : end, :]
54segment_dists = dists[start + 1 : end]
55
56# check whether to take perpendicular or euclidean distance with
57# inner product of vectors
58
59# vectors from points -> start and end
60dr0 = segment_coords[:, 0] - r0
61dc0 = segment_coords[:, 1] - c0
62dr1 = segment_coords[:, 0] - r1
63dc1 = segment_coords[:, 1] - c1
64# vectors points -> start and end projected on start -> end vector
65projected_lengths0 = dr0 * dr + dc0 * dc
66projected_lengths1 = -dr1 * dr - dc1 * dc
67perp = np.logical_and(projected_lengths0 > 0, projected_lengths1 > 0)
68eucl = np.logical_not(perp)
69segment_dists[perp] = np.abs(
70segment_coords[perp, 0] * np.cos(segment_angle)
71+ segment_coords[perp, 1] * np.sin(segment_angle)
72- segment_dist
73)
74segment_dists[eucl] = np.minimum(
75# distance to start point
76np.sqrt(dc0[eucl] ** 2 + dr0[eucl] ** 2),
77# distance to end point
78np.sqrt(dc1[eucl] ** 2 + dr1[eucl] ** 2),
79)
80
81if np.any(segment_dists > tolerance):
82# select point with maximum distance to line
83new_end = start + np.argmax(segment_dists) + 1
84pos_stack.append((new_end, end))
85pos_stack.append((start, new_end))
86chain[new_end] = True
87
88if len(pos_stack) == 0:
89end_of_chain = True
90
91return coords[chain, :]
92
93
94# B-Spline subdivision
95_SUBDIVISION_MASKS = {
96# degree: (mask_even, mask_odd)
97# extracted from (degree + 2)th row of Pascal's triangle
981: ([1, 1], [1, 1]),
992: ([3, 1], [1, 3]),
1003: ([1, 6, 1], [0, 4, 4]),
1014: ([5, 10, 1], [1, 10, 5]),
1025: ([1, 15, 15, 1], [0, 6, 20, 6]),
1036: ([7, 35, 21, 1], [1, 21, 35, 7]),
1047: ([1, 28, 70, 28, 1], [0, 8, 56, 56, 8]),
105}
106
107
108def subdivide_polygon(coords, degree=2, preserve_ends=False):
109"""Subdivision of polygonal curves using B-Splines.
110
111Note that the resulting curve is always within the convex hull of the
112original polygon. Circular polygons stay closed after subdivision.
113
114Parameters
115----------
116coords : (K, 2) array
117Coordinate array.
118degree : {1, 2, 3, 4, 5, 6, 7}, optional
119Degree of B-Spline. Default is 2.
120preserve_ends : bool, optional
121Preserve first and last coordinate of non-circular polygon. Default is
122False.
123
124Returns
125-------
126coords : (L, 2) array
127Subdivided coordinate array.
128
129References
130----------
131.. [1] http://mrl.nyu.edu/publications/subdiv-course2000/coursenotes00.pdf
132"""
133if degree not in _SUBDIVISION_MASKS:
134raise ValueError("Invalid B-Spline degree. Only degree 1 - 7 is " "supported.")
135
136circular = np.all(coords[0, :] == coords[-1, :])
137
138method = 'valid'
139if circular:
140# remove last coordinate because of wrapping
141coords = coords[:-1, :]
142# circular convolution by wrapping boundaries
143method = 'same'
144
145mask_even, mask_odd = _SUBDIVISION_MASKS[degree]
146# divide by total weight
147mask_even = np.array(mask_even, float) / (2**degree)
148mask_odd = np.array(mask_odd, float) / (2**degree)
149
150even = signal.convolve2d(
151coords.T, np.atleast_2d(mask_even), mode=method, boundary='wrap'
152)
153odd = signal.convolve2d(
154coords.T, np.atleast_2d(mask_odd), mode=method, boundary='wrap'
155)
156
157out = np.zeros((even.shape[1] + odd.shape[1], 2))
158out[1::2] = even.T
159out[::2] = odd.T
160
161if circular:
162# close polygon
163out = np.vstack([out, out[0, :]])
164
165if preserve_ends and not circular:
166out = np.vstack([coords[0, :], out, coords[-1, :]])
167
168return out
169