from shapely.geometry import LineString, MultiLineString
import numpy as np
import math
[docs]class CurveDetector(object):
"""
Class for detecting curves along polylines based on geometric features of
rays, arcs, and complex segments.
To-do
-----
- Deal with adjacent curves/reverse curves
- Minimum points/length for curve definition
- Add point/length buffer
- Full curve radius estimate
"""
def __init__(self, line):
# Log parameters
self.line = line
# Initialize fitting properties
self._segment_mask = None
@property
def line(self):
return self._line
@line.setter
def line(self, line):
if not isinstance(line, (LineString, MultiLineString)):
raise TypeError("Input line must be valid shapely linear geometry.")
else:
self._line = line
@property
def segment_mask(self):
"""
A boolean array mask indicating which 4-point segments have been
detected to be part of a fitted curve.
"""
return self._segment_mask
@property
def point_mask(self):
"""
A boolean array mask indicating which points have been detected to be
part of a fitted curve
"""
size = self.size
mask = np.zeros(size, dtype=bool)
for i in range(4):
mask[i:size+i-3] = mask[i:size+i-3] | self.segment_mask
return mask
@property
def point_map(self):
"""
Array of values indicating which points are associated with which
curves. Points with a value of 0 are not associated with any unique
fitted curve.
"""
mask = self.point_mask
res = (np.append([True], np.diff(mask * 1) > 0) * mask).cumsum() * mask
return res
@property
def curves(self):
"""
A list of shapely LineStrings for fitted curves.
"""
# Iterate through unique curve numbers
point_map = self.point_map
lines = []
for num in np.unique(point_map)[1:]:
# Get mask for curve number
mask = point_map == num
# Create curve linestring
lines.append(LineString(list(zip(self.xs[mask], self.ys[mask]))))
return lines
@property
def size(self):
return len(self.xs)
@property
def xs(self):
# Compute x values
xs = np.array(self.line.xy[0], dtype=float)
return xs
@property
def ys(self):
# Compute x values
ys = np.array(self.line.xy[1], dtype=float)
return ys
@property
def dx(self):
"""
X-dimension distance between adjacent points.
Size = n - 1
"""
dx = np.diff(self.xs)
return dx
@property
def dy(self):
"""
Y-dimension distance between adjacent points.
Size = n - 1
"""
dy = np.diff(self.ys)
return dy
@property
def bearing(self):
"""
Bearing of the ray defined by two adjacent points.
Size = n - 1
"""
bearing = np.arctan2(self.dy, self.dx)
return bearing
@property
def ray_length(self):
"""
Length of the ray defined by two adjacent points.
Size = n - 1
"""
ray_length = (self.dx ** 2 + self.dy ** 2) ** 0.5
return ray_length
@property
def relangle(self):
"""
Relative angle between two adjacent rays.
Size = n - 2
"""
relangle = np.diff(self.bearing)
return relangle
@property
def direction(self):
"""
The direction of the relative angle between two adjacent rays,
indicating a left-hand angle (1) and a right-hand angle (0).
Size = n - 2
"""
direction = (self.relangle > 0) * 1
return direction
@property
def span(self):
"""
Span length of each 3-point arc, measuring between the begin and end
points of each arc defined by two adjacent rays.
Size = n - 2
"""
dx, dy = self.dx, self.dy
span = ((dx[:-1]+dx[1:])**2+(dy[:-1]+dy[1:])**2)**0.5
return span
@property
def span_ratio(self):
"""
Ratio of the smaller ray to the larger ray of each arc defined by
two adjacent rays.
Size = n - 2
"""
ray_length = self.ray_length
span_ratio = ray_length[:-1]/ray_length[1:]
span_ratio = np.where(span_ratio>1, 1/span_ratio, span_ratio)
return span_ratio
[docs] def span_index(self, span_ratio_sensitivity=0.2):
"""
Abstract quantification of the influence of each arc's span ratio on
curve detection given an input sensitivity value between 0 and 1.
Size = n - 2
Parameters
----------
span_ratio_sensitivity : number [0, 1], default 0.35
A measure of the sensitivity of the detector to a given arc's span
ratio where 0 means no sensitivity and 1 means full sensitivity.
"""
span_index = self.span_ratio*span_ratio_sensitivity+(1-span_ratio_sensitivity)
return span_index
@property
def radius(self):
"""
The radius of each arc defined by two adjacent rays.
Size = n - 2
"""
# Compute radius based on arc span and relative angle
radius = self.span/(2*np.sin(math.pi-self.relangle))
# Address effective tangents
radius = np.where(self.relangle<math.pi, radius, np.inf)
return radius
@property
def central_angle(self):
"""
The central angle of each arc defined by two adjacent rays.
Size = n - 2
"""
central_angle = 2*np.arcsin(self.span/(2*self.radius))
return central_angle
@property
def arc_length(self):
"""
The actual outer arc length of each arc defined by two adjacent rays.
Size = n - 2
"""
arc_length = self.central_angle*self.radius
return arc_length
@property
def radius_max(self):
"""
The maximum radius between all pairs of adjacent arcs (i.e., 4-point
segments).
Size = n - 3
"""
radius_max = np.max([self.radius[:-1],self.radius[1:]], axis=0)
return radius_max
@property
def radius_dif(self):
"""
The mathematical difference in radius between all pairs of adjacent
arcs (i.e., 4-point segments).
Size = n - 3
"""
radius_dif = (self.radius[1:]-self.radius[:-1])
return radius_dif
@property
def radius_scale(self):
"""
The mathematical difference in radius between all pairs of adjacent
arcs (i.e., 4-point segments), normalized by the maximum radius between
each pair.
Size = n - 3
"""
radius_scale = self.radius_dif/self.radius_max
return radius_scale
[docs] def fit(self, max_radius=10000, max_radius_scale=0.65, span_ratio_sensitivity=0.35):
"""
A test of whether or not adjacent arcs have similar radii based on
input detection parameters for the maximum radius scale and the
maximum radius which can be detected within a curve.
Size = n - 3
Parameters
----------
max_radius : number, default 10000
The maximum radius to be considered within a detected curve.
max_radius_scale : float [0, 1], default 0.65
The maximum span ratio-adjusted radius scale value to be considered
within a detected curve. If span_ratio_sensitivity == 0, no
adjustements to the radius scale are made.
span_ratio_sensitivity : number [0, 1], default 0.35
A measure of the sensitivity of the detector to a given arc's span
ratio where 0 means no sensitivity and 1 means full sensitivity.
"""
# Compute span index
# - Span index is used to adjust sensitivity to differing radii which
# may result from inconsistently spaced line points (i.e., large
# span index values)
span_index = self.span_index(span_ratio_sensitivity)
# Compute radius match boolean mask
self._segment_mask = \
(np.abs(self.radius_scale*span_index[:-1]*span_index[1:]) < max_radius_scale) & \
(np.abs(self.radius_max) < max_radius)