"""Polygonal chain simplification using NumPy.""" import numpy as np # approximate_polygon: scikit-image 0.25.2, copyright 2009-2022 the scikit-image team. # BSD-3-Clause; see ../../licenses/scikit-image.txt. 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 : (K, 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 : (L, 2) array Approximated polygonal chain where L <= K. References ---------- .. [1] https://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, :]