375 lines
14 KiB
Python
375 lines
14 KiB
Python
"""This submodule contains tools that deal with generic, degree n, Bezier
|
|
curves.
|
|
Note: Bezier curves here are always represented by the tuple of their control
|
|
points given by their standard representation."""
|
|
|
|
# External dependencies:
|
|
from __future__ import division, absolute_import, print_function
|
|
from math import factorial as fac, ceil, log, sqrt
|
|
from numpy import poly1d
|
|
|
|
# Internal dependencies
|
|
from .polytools import real, imag, polyroots, polyroots01
|
|
|
|
|
|
# Evaluation ##################################################################
|
|
|
|
def n_choose_k(n, k):
|
|
return fac(n)//fac(k)//fac(n-k)
|
|
|
|
|
|
def bernstein(n, t):
|
|
"""returns a list of the Bernstein basis polynomials b_{i, n} evaluated at
|
|
t, for i =0...n"""
|
|
t1 = 1-t
|
|
return [n_choose_k(n, k) * t1**(n-k) * t**k for k in range(n+1)]
|
|
|
|
|
|
def bezier_point(p, t):
|
|
"""Evaluates the Bezier curve given by it's control points, p, at t.
|
|
Note: Uses Horner's rule for cubic and lower order Bezier curves.
|
|
Warning: Be concerned about numerical stability when using this function
|
|
with high order curves."""
|
|
|
|
# begin arc support block ########################
|
|
try:
|
|
p.large_arc
|
|
return p.point(t)
|
|
except:
|
|
pass
|
|
# end arc support block ##########################
|
|
|
|
deg = len(p) - 1
|
|
if deg == 3:
|
|
return p[0] + t*(
|
|
3*(p[1] - p[0]) + t*(
|
|
3*(p[0] + p[2]) - 6*p[1] + t*(
|
|
-p[0] + 3*(p[1] - p[2]) + p[3])))
|
|
elif deg == 2:
|
|
return p[0] + t*(
|
|
2*(p[1] - p[0]) + t*(
|
|
p[0] - 2*p[1] + p[2]))
|
|
elif deg == 1:
|
|
return p[0] + t*(p[1] - p[0])
|
|
elif deg == 0:
|
|
return p[0]
|
|
else:
|
|
bern = bernstein(deg, t)
|
|
return sum(bern[k]*p[k] for k in range(deg+1))
|
|
|
|
|
|
# Conversion ##################################################################
|
|
|
|
def bezier2polynomial(p, numpy_ordering=True, return_poly1d=False):
|
|
"""Converts a tuple of Bezier control points to a tuple of coefficients
|
|
of the expanded polynomial.
|
|
return_poly1d : returns a numpy.poly1d object. This makes computations
|
|
of derivatives/anti-derivatives and many other operations quite quick.
|
|
numpy_ordering : By default (to accommodate numpy) the coefficients will
|
|
be output in reverse standard order."""
|
|
if len(p) == 4:
|
|
coeffs = (-p[0] + 3*(p[1] - p[2]) + p[3],
|
|
3*(p[0] - 2*p[1] + p[2]),
|
|
3*(p[1]-p[0]),
|
|
p[0])
|
|
elif len(p) == 3:
|
|
coeffs = (p[0] - 2*p[1] + p[2],
|
|
2*(p[1] - p[0]),
|
|
p[0])
|
|
elif len(p) == 2:
|
|
coeffs = (p[1]-p[0],
|
|
p[0])
|
|
elif len(p) == 1:
|
|
coeffs = p
|
|
else:
|
|
# https://en.wikipedia.org/wiki/Bezier_curve#Polynomial_form
|
|
n = len(p) + 1
|
|
coeffs = [fac(n)//fac(n-j) * sum(
|
|
(-1)**(i+j) * p[i] / (fac(i) * fac(j-i)) for i in xrange(j+1))
|
|
for j in range(n+1)]
|
|
if not numpy_ordering:
|
|
coeffs.reverse()
|
|
if return_poly1d:
|
|
return poly1d(coeffs)
|
|
return coeffs
|
|
|
|
|
|
def polynomial2bezier(poly):
|
|
"""Converts a cubic or lower order Polynomial object (or a sequence of
|
|
coefficients) to a CubicBezier, QuadraticBezier, or Line object as
|
|
appropriate."""
|
|
if isinstance(poly, poly1d):
|
|
c = poly.coeffs
|
|
else:
|
|
c = poly
|
|
order = len(c)-1
|
|
if order == 3:
|
|
bpoints = (c[3], c[2]/3 + c[3], (c[1] + 2*c[2])/3 + c[3],
|
|
c[0] + c[1] + c[2] + c[3])
|
|
elif order == 2:
|
|
bpoints = (c[2], c[1]/2 + c[2], c[0] + c[1] + c[2])
|
|
elif order == 1:
|
|
bpoints = (c[1], c[0] + c[1])
|
|
else:
|
|
raise AssertionError("This function is only implemented for linear, "
|
|
"quadratic, and cubic polynomials.")
|
|
return bpoints
|
|
|
|
|
|
# Curve Splitting #############################################################
|
|
|
|
def split_bezier(bpoints, t):
|
|
"""Uses deCasteljau's recursion to split the Bezier curve at t into two
|
|
Bezier curves of the same order."""
|
|
def split_bezier_recursion(bpoints_left_, bpoints_right_, bpoints_, t_):
|
|
if len(bpoints_) == 1:
|
|
bpoints_left_.append(bpoints_[0])
|
|
bpoints_right_.append(bpoints_[0])
|
|
else:
|
|
new_points = [None]*(len(bpoints_) - 1)
|
|
bpoints_left_.append(bpoints_[0])
|
|
bpoints_right_.append(bpoints_[-1])
|
|
for i in range(len(bpoints_) - 1):
|
|
new_points[i] = (1 - t_)*bpoints_[i] + t_*bpoints_[i + 1]
|
|
bpoints_left_, bpoints_right_ = split_bezier_recursion(
|
|
bpoints_left_, bpoints_right_, new_points, t_)
|
|
return bpoints_left_, bpoints_right_
|
|
|
|
bpoints_left = []
|
|
bpoints_right = []
|
|
bpoints_left, bpoints_right = \
|
|
split_bezier_recursion(bpoints_left, bpoints_right, bpoints, t)
|
|
bpoints_right.reverse()
|
|
return bpoints_left, bpoints_right
|
|
|
|
|
|
def halve_bezier(p):
|
|
|
|
# begin arc support block ########################
|
|
try:
|
|
p.large_arc
|
|
return p.split(0.5)
|
|
except:
|
|
pass
|
|
# end arc support block ##########################
|
|
|
|
if len(p) == 4:
|
|
return ([p[0], (p[0] + p[1])/2, (p[0] + 2*p[1] + p[2])/4,
|
|
(p[0] + 3*p[1] + 3*p[2] + p[3])/8],
|
|
[(p[0] + 3*p[1] + 3*p[2] + p[3])/8,
|
|
(p[1] + 2*p[2] + p[3])/4, (p[2] + p[3])/2, p[3]])
|
|
else:
|
|
return split_bezier(p, 0.5)
|
|
|
|
|
|
# Bounding Boxes ##############################################################
|
|
|
|
def bezier_real_minmax(p):
|
|
"""returns the minimum and maximum for any real cubic bezier"""
|
|
local_extremizers = [0, 1]
|
|
if len(p) == 4: # cubic case
|
|
a = [p.real for p in p]
|
|
denom = a[0] - 3*a[1] + 3*a[2] - a[3]
|
|
if denom != 0:
|
|
delta = a[1]**2 - (a[0] + a[1])*a[2] + a[2]**2 + (a[0] - a[1])*a[3]
|
|
if delta >= 0: # otherwise no local extrema
|
|
sqdelta = sqrt(delta)
|
|
tau = a[0] - 2*a[1] + a[2]
|
|
r1 = (tau + sqdelta)/denom
|
|
r2 = (tau - sqdelta)/denom
|
|
if 0 < r1 < 1:
|
|
local_extremizers.append(r1)
|
|
if 0 < r2 < 1:
|
|
local_extremizers.append(r2)
|
|
local_extrema = [bezier_point(a, t) for t in local_extremizers]
|
|
return min(local_extrema), max(local_extrema)
|
|
|
|
# find reverse standard coefficients of the derivative
|
|
dcoeffs = bezier2polynomial(a, return_poly1d=True).deriv().coeffs
|
|
|
|
# find real roots, r, such that 0 <= r <= 1
|
|
local_extremizers += polyroots01(dcoeffs)
|
|
local_extrema = [bezier_point(a, t) for t in local_extremizers]
|
|
return min(local_extrema), max(local_extrema)
|
|
|
|
|
|
def bezier_bounding_box(bez):
|
|
"""returns the bounding box for the segment in the form
|
|
(xmin, xmax, ymin, ymax).
|
|
Warning: For the non-cubic case this is not particularly efficient."""
|
|
|
|
# begin arc support block ########################
|
|
try:
|
|
bla = bez.large_arc
|
|
return bez.bbox() # added to support Arc objects
|
|
except:
|
|
pass
|
|
# end arc support block ##########################
|
|
|
|
if len(bez) == 4:
|
|
xmin, xmax = bezier_real_minmax([p.real for p in bez])
|
|
ymin, ymax = bezier_real_minmax([p.imag for p in bez])
|
|
return xmin, xmax, ymin, ymax
|
|
poly = bezier2polynomial(bez, return_poly1d=True)
|
|
x = real(poly)
|
|
y = imag(poly)
|
|
dx = x.deriv()
|
|
dy = y.deriv()
|
|
x_extremizers = [0, 1] + polyroots(dx, realroots=True,
|
|
condition=lambda r: 0 < r < 1)
|
|
y_extremizers = [0, 1] + polyroots(dy, realroots=True,
|
|
condition=lambda r: 0 < r < 1)
|
|
x_extrema = [x(t) for t in x_extremizers]
|
|
y_extrema = [y(t) for t in y_extremizers]
|
|
return min(x_extrema), max(x_extrema), min(y_extrema), max(y_extrema)
|
|
|
|
|
|
def box_area(xmin, xmax, ymin, ymax):
|
|
"""
|
|
INPUT: 2-tuple of cubics (given by control points)
|
|
OUTPUT: boolean
|
|
"""
|
|
return (xmax - xmin)*(ymax - ymin)
|
|
|
|
|
|
def interval_intersection_width(a, b, c, d):
|
|
"""returns the width of the intersection of intervals [a,b] and [c,d]
|
|
(thinking of these as intervals on the real number line)"""
|
|
return max(0, min(b, d) - max(a, c))
|
|
|
|
|
|
def boxes_intersect(box1, box2):
|
|
"""Determines if two rectangles, each input as a tuple
|
|
(xmin, xmax, ymin, ymax), intersect."""
|
|
xmin1, xmax1, ymin1, ymax1 = box1
|
|
xmin2, xmax2, ymin2, ymax2 = box2
|
|
if interval_intersection_width(xmin1, xmax1, xmin2, xmax2) and \
|
|
interval_intersection_width(ymin1, ymax1, ymin2, ymax2):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
# Intersections ###############################################################
|
|
|
|
class ApproxSolutionSet(list):
|
|
"""A class that behaves like a set but treats two elements , x and y, as
|
|
equivalent if abs(x-y) < self.tol"""
|
|
def __init__(self, tol):
|
|
self.tol = tol
|
|
|
|
def __contains__(self, x):
|
|
for y in self:
|
|
if abs(x - y) < self.tol:
|
|
return True
|
|
return False
|
|
|
|
def appadd(self, pt):
|
|
if pt not in self:
|
|
self.append(pt)
|
|
|
|
|
|
class BPair(object):
|
|
def __init__(self, bez1, bez2, t1, t2):
|
|
self.bez1 = bez1
|
|
self.bez2 = bez2
|
|
self.t1 = t1 # t value to get the mid point of this curve from cub1
|
|
self.t2 = t2 # t value to get the mid point of this curve from cub2
|
|
|
|
|
|
def bezier_intersections(bez1, bez2, longer_length, tol=1e-8, tol_deC=1e-8):
|
|
"""INPUT:
|
|
bez1, bez2 = [P0,P1,P2,...PN], [Q0,Q1,Q2,...,PN] defining the two
|
|
Bezier curves to check for intersections between.
|
|
longer_length - the length (or an upper bound) on the longer of the two
|
|
Bezier curves. Determines the maximum iterations needed together with tol.
|
|
tol - is the smallest distance that two solutions can differ by and still
|
|
be considered distinct solutions.
|
|
OUTPUT: a list of tuples (t,s) in [0,1]x[0,1] such that
|
|
abs(bezier_point(bez1[0],t) - bezier_point(bez2[1],s)) < tol_deC
|
|
Note: This will return exactly one such tuple for each intersection
|
|
(assuming tol_deC is small enough)."""
|
|
maxits = int(ceil(1-log(tol_deC/longer_length)/log(2)))
|
|
pair_list = [BPair(bez1, bez2, 0.5, 0.5)]
|
|
intersection_list = []
|
|
k = 0
|
|
approx_point_set = ApproxSolutionSet(tol)
|
|
while pair_list and k < maxits:
|
|
new_pairs = []
|
|
delta = 0.5**(k + 2)
|
|
for pair in pair_list:
|
|
bbox1 = bezier_bounding_box(pair.bez1)
|
|
bbox2 = bezier_bounding_box(pair.bez2)
|
|
if boxes_intersect(bbox1, bbox2):
|
|
if box_area(*bbox1) < tol_deC and box_area(*bbox2) < tol_deC:
|
|
point = bezier_point(bez1, pair.t1)
|
|
if point not in approx_point_set:
|
|
approx_point_set.append(point)
|
|
# this is the point in the middle of the pair
|
|
intersection_list.append((pair.t1, pair.t2))
|
|
|
|
# this prevents the output of redundant intersection points
|
|
for otherPair in pair_list:
|
|
if pair.bez1 == otherPair.bez1 or \
|
|
pair.bez2 == otherPair.bez2 or \
|
|
pair.bez1 == otherPair.bez2 or \
|
|
pair.bez2 == otherPair.bez1:
|
|
pair_list.remove(otherPair)
|
|
else:
|
|
(c11, c12) = halve_bezier(pair.bez1)
|
|
(t11, t12) = (pair.t1 - delta, pair.t1 + delta)
|
|
(c21, c22) = halve_bezier(pair.bez2)
|
|
(t21, t22) = (pair.t2 - delta, pair.t2 + delta)
|
|
new_pairs += [BPair(c11, c21, t11, t21),
|
|
BPair(c11, c22, t11, t22),
|
|
BPair(c12, c21, t12, t21),
|
|
BPair(c12, c22, t12, t22)]
|
|
pair_list = new_pairs
|
|
k += 1
|
|
if k >= maxits:
|
|
raise Exception("bezier_intersections has reached maximum "
|
|
"iterations without terminating... "
|
|
"either there's a problem/bug or you can fix by "
|
|
"raising the max iterations or lowering tol_deC")
|
|
return intersection_list
|
|
|
|
|
|
def bezier_by_line_intersections(bezier, line):
|
|
"""Returns tuples (t1,t2) such that bezier.point(t1) ~= line.point(t2)."""
|
|
# The method here is to translate (shift) then rotate the complex plane so
|
|
# that line starts at the origin and proceeds along the positive real axis.
|
|
# After this transformation, the intersection points are the real roots of
|
|
# the imaginary component of the bezier for which the real component is
|
|
# between 0 and abs(line[1]-line[0])].
|
|
assert len(line[:]) == 2
|
|
assert line[0] != line[1]
|
|
if not any(p != bezier[0] for p in bezier):
|
|
raise ValueError("bezier is nodal, use "
|
|
"bezier_by_line_intersection(bezier[0], line) "
|
|
"instead for a bool to be returned.")
|
|
|
|
# First let's shift the complex plane so that line starts at the origin
|
|
shifted_bezier = [z - line[0] for z in bezier]
|
|
shifted_line_end = line[1] - line[0]
|
|
line_length = abs(shifted_line_end)
|
|
|
|
# Now let's rotate the complex plane so that line falls on the x-axis
|
|
rotation_matrix = line_length/shifted_line_end
|
|
transformed_bezier = [rotation_matrix*z for z in shifted_bezier]
|
|
|
|
# Now all intersections should be roots of the imaginary component of
|
|
# the transformed bezier
|
|
transformed_bezier_imag = [p.imag for p in transformed_bezier]
|
|
coeffs_y = bezier2polynomial(transformed_bezier_imag)
|
|
roots_y = list(polyroots01(coeffs_y)) # returns real roots 0 <= r <= 1
|
|
|
|
transformed_bezier_real = [p.real for p in transformed_bezier]
|
|
intersection_list = []
|
|
for bez_t in set(roots_y):
|
|
xval = bezier_point(transformed_bezier_real, bez_t)
|
|
if 0 <= xval <= line_length:
|
|
line_t = xval/line_length
|
|
intersection_list.append((bez_t, line_t))
|
|
return intersection_list
|
|
|