""" Python implementation of Algorithm for Automatically Fitting Digitized Curves by Philip J. Schneider "Graphics Gems", Academic Press, 1990 """ from __future__ import print_function from numpy import * import bezier # Fit one (ore more) Bezier curves to a set of points def fitCurve(points, maxError): leftTangent = normalize(points[1] - points[0]) rightTangent = normalize(points[-2] - points[-1]) return fitCubic(points, leftTangent, rightTangent, maxError) def fitCubic(points, leftTangent, rightTangent, error): # Use heuristic if region only has two points in it if (len(points) == 2): dist = linalg.norm(points[0] - points[1]) / 3.0 bezCurve = [points[0], points[0] + leftTangent * dist, points[1] + rightTangent * dist, points[1]] return [bezCurve] # Parameterize points, and attempt to fit curve u = chordLengthParameterize(points) bezCurve = generateBezier(points, u, leftTangent, rightTangent) # Find max deviation of points to fitted curve maxError, splitPoint = computeMaxError(points, bezCurve, u) if maxError < error: return [bezCurve] # If error not too large, try some reparameterization and iteration if maxError < error**2: for i in range(20): uPrime = reparameterize(bezCurve, points, u) bezCurve = generateBezier(points, uPrime, leftTangent, rightTangent) maxError, splitPoint = computeMaxError(points, bezCurve, uPrime) if maxError < error: return [bezCurve] u = uPrime # Fitting failed -- split at max error point and fit recursively beziers = [] centerTangent = normalize(points[splitPoint-1] - points[splitPoint+1]) beziers += fitCubic(points[:splitPoint+1], leftTangent, centerTangent, error) beziers += fitCubic(points[splitPoint:], -centerTangent, rightTangent, error) return beziers def generateBezier(points, parameters, leftTangent, rightTangent): bezCurve = [points[0], None, None, points[-1]] # compute the A's A = zeros((len(parameters), 2, 2)) for i, u in enumerate(parameters): A[i][0] = leftTangent * 3*(1-u)**2 * u A[i][1] = rightTangent * 3*(1-u) * u**2 # Create the C and X matrices C = zeros((2, 2)) X = zeros(2) for i, (point, u) in enumerate(zip(points, parameters)): C[0][0] += dot(A[i][0], A[i][0]) C[0][1] += dot(A[i][0], A[i][1]) C[1][0] += dot(A[i][0], A[i][1]) C[1][1] += dot(A[i][1], A[i][1]) tmp = point - bezier.q([points[0], points[0], points[-1], points[-1]], u) X[0] += dot(A[i][0], tmp) X[1] += dot(A[i][1], tmp) # Compute the determinants of C and X det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1] det_C0_X = C[0][0] * X[1] - C[1][0] * X[0] det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1] # Finally, derive alpha values alpha_l = 0.0 if det_C0_C1 == 0 else det_X_C1 / det_C0_C1 alpha_r = 0.0 if det_C0_C1 == 0 else det_C0_X / det_C0_C1 # If alpha negative, use the Wu/Barsky heuristic (see text) */ # (if alpha is 0, you get coincident control points that lead to # divide by zero in any subsequent NewtonRaphsonRootFind() call. */ segLength = linalg.norm(points[0] - points[-1]) epsilon = 1.0e-6 * segLength if alpha_l < epsilon or alpha_r < epsilon: # fall back on standard (probably inaccurate) formula, and subdivide further if needed. bezCurve[1] = bezCurve[0] + leftTangent * (segLength / 3.0) bezCurve[2] = bezCurve[3] + rightTangent * (segLength / 3.0) else: # First and last control points of the Bezier curve are # positioned exactly at the first and last data points # Control points 1 and 2 are positioned an alpha distance out # on the tangent vectors, left and right, respectively bezCurve[1] = bezCurve[0] + leftTangent * alpha_l bezCurve[2] = bezCurve[3] + rightTangent * alpha_r return bezCurve def reparameterize(bezier, points, parameters): return [newtonRaphsonRootFind(bezier, point, u) for point, u in zip(points, parameters)] def newtonRaphsonRootFind(bez, point, u): """ Newton's root finding algorithm calculates f(x)=0 by reiterating x_n+1 = x_n - f(x_n)/f'(x_n) We are trying to find curve parameter u for some point p that minimizes the distance from that point to the curve. Distance point to curve is d=q(u)-p. At minimum distance the point is perpendicular to the curve. We are solving f = q(u)-p * q'(u) = 0 with f' = q'(u) * q'(u) + q(u)-p * q''(u) gives u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)| """ d = bezier.q(bez, u)-point numerator = (d * bezier.qprime(bez, u)).sum() denominator = (bezier.qprime(bez, u)**2 + d * bezier.qprimeprime(bez, u)).sum() if denominator == 0.0: return u else: return u - numerator/denominator def chordLengthParameterize(points): u = [0.0] for i in range(1, len(points)): u.append(u[i-1] + linalg.norm(points[i] - points[i-1])) for i, _ in enumerate(u): u[i] = u[i] / u[-1] return u def computeMaxError(points, bez, parameters): maxDist = 0.0 splitPoint = len(points)/2 for i, (point, u) in enumerate(zip(points, parameters)): dist = linalg.norm(bezier.q(bez, u)-point)**2 if dist > maxDist: maxDist = dist splitPoint = i return maxDist, splitPoint def normalize(v): return v / linalg.norm(v)