You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
9.5 KiB

// This file is part of libigl, a simple c++ geometry processing library.
//
// Copyright (C) 2020 Alec Jacobson <alecjacobson@gmail.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License
// v. 2.0. If a copy of the MPL was not distributed with this file, You can
// obtain one at http://mozilla.org/MPL/2.0/.
#include "fit_cubic_bezier.h"
#include "bezier.h"
#include "EPS.h"
// Adapted from main.c accompanying
// An Algorithm for Automatically Fitting Digitized Curves
// by Philip J. Schneider
// from "Graphics Gems", Academic Press, 1990
IGL_INLINE void igl::fit_cubic_bezier(
const Eigen::MatrixXd & d,
const double error,
std::vector<Eigen::MatrixXd> & cubics)
{
const int nPts = d.rows();
// Don't attempt to fit curve to single point
if(nPts==1) { return; }
// Avoid using zero tangent
const static auto tangent = [](
const Eigen::MatrixXd & d,
const int i,const int dir)->Eigen::RowVectorXd
{
int j = i;
const int nPts = d.rows();
Eigen::RowVectorXd t;
while(true)
{
// look at next point
j += dir;
if(j < 0 || j>=nPts)
{
// All points are coincident?
// give up and use zero tangent...
return Eigen::RowVectorXd::Zero(1,d.cols());
}
t = d.row(j)-d.row(i);
if(t.squaredNorm() > igl::DOUBLE_EPS)
{
break;
}
}
return t.normalized();
};
Eigen::RowVectorXd tHat1 = tangent(d,0,+1);
Eigen::RowVectorXd tHat2 = tangent(d,nPts-1,-1);
// If first and last points are identically equal, then consider closed
const bool closed = (d.row(0) - d.row(d.rows()-1)).squaredNorm() == 0;
// If closed loop make tangents match
if(closed)
{
tHat1 = (tHat1 - tHat2).eval().normalized();
tHat2 = -tHat1;
}
cubics.clear();
fit_cubic_bezier_substring(d,0,nPts-1,tHat1,tHat2,error,closed,cubics);
};
IGL_INLINE void igl::fit_cubic_bezier_substring(
const Eigen::MatrixXd & d,
const int first,
const int last,
const Eigen::RowVectorXd & tHat1,
const Eigen::RowVectorXd & tHat2,
const double error,
const bool force_split,
std::vector<Eigen::MatrixXd> & cubics)
{
// Helper functions
// Evaluate a Bezier curve at a particular parameter value
const static auto bezier_eval = [](const Eigen::MatrixXd & V, const double t)
{ Eigen::RowVectorXd P; bezier(V,t,P); return P; };
//
// Use Newton-Raphson iteration to find better root.
const static auto NewtonRaphsonRootFind = [](
const Eigen::MatrixXd & Q,
const Eigen::RowVectorXd & P,
const double u)->double
{
/* Compute Q(u) */
Eigen::RowVectorXd Q_u = bezier_eval(Q, u);
Eigen::MatrixXd Q1(3,Q.cols());
Eigen::MatrixXd Q2(2,Q.cols());
/* Generate control vertices for Q' */
for (int i = 0; i <= 2; i++)
{
Q1.row(i) = (Q.row(i+1) - Q.row(i)) * 3.0;
}
/* Generate control vertices for Q'' */
for (int i = 0; i <= 1; i++)
{
Q2.row(i) = (Q1.row(i+1) - Q1.row(i)) * 2.0;
}
/* Compute Q'(u) and Q''(u) */
const Eigen::RowVectorXd Q1_u = bezier_eval(Q1, u);
const Eigen::RowVectorXd Q2_u = bezier_eval(Q2, u);
/* Compute f(u)/f'(u) */
const double numerator = ((Q_u-P).array() * Q1_u.array()).array().sum();
const double denominator =
Q1_u.squaredNorm() + ((Q_u-P).array() * Q2_u.array()).array().sum();
/* u = u - f(u)/f'(u) */
return u - (numerator/denominator);
};
const static auto ComputeMaxError = [](
const Eigen::MatrixXd & d,
const int first,
const int last,
const Eigen::MatrixXd & bezCurve,
const Eigen::VectorXd & u,
int & splitPoint)->double
{
Eigen::VectorXd E(last - (first+1));
splitPoint = (last-first + 1)/2;
double maxDist = 0.0;
for (int i = first + 1; i < last; i++)
{
Eigen::RowVectorXd P = bezier_eval(bezCurve, u(i-first));
const double dist = (P-d.row(i)).squaredNorm();
E(i-(first+1)) = dist;
if (dist >= maxDist)
{
maxDist = dist;
// Worst offender
splitPoint = i;
}
}
//const double half_total = E.array().sum()/2;
//double run = 0;
//for (int i = first + 1; i < last; i++)
//{
// run += E(i-(first+1));
// if(run>half_total)
// {
// // When accumulated ½ the error --> more symmetric, but requires more
// // curves
// splitPoint = i;
// break;
// }
//}
return maxDist;
};
const static auto Straight = [](
const Eigen::MatrixXd & d,
const int first,
const int last,
const Eigen::RowVectorXd & tHat1,
const Eigen::RowVectorXd & tHat2,
Eigen::MatrixXd & bezCurve)
{
bezCurve.resize(4,d.cols());
const double dist = (d.row(last)-d.row(first)).norm()/3.0;
bezCurve.row(0) = d.row(first);
bezCurve.row(1) = d.row(first) + tHat1*dist;
bezCurve.row(2) = d.row(last) + tHat2*dist;
bezCurve.row(3) = d.row(last);
};
const static auto GenerateBezier = [](
const Eigen::MatrixXd & d,
const int first,
const int last,
const Eigen::VectorXd & uPrime,
const Eigen::RowVectorXd & tHat1,
const Eigen::RowVectorXd & tHat2,
Eigen::MatrixXd & bezCurve)
{
bezCurve.resize(4,d.cols());
const int nPts = last - first + 1;
const static auto B0 = [](const double u)->double
{ double tmp = 1.0 - u; return (tmp * tmp * tmp);};
const static auto B1 = [](const double u)->double
{ double tmp = 1.0 - u; return (3 * u * (tmp * tmp));};
const static auto B2 = [](const double u)->double
{ double tmp = 1.0 - u; return (3 * u * u * tmp); };
const static auto B3 = [](const double u)->double
{ return (u * u * u); };
/* Compute the A's */
std::vector<std::vector<Eigen::RowVectorXd> > A(nPts);
for (int i = 0; i < nPts; i++)
{
Eigen::RowVectorXd v1 = tHat1*B1(uPrime(i));
Eigen::RowVectorXd v2 = tHat2*B2(uPrime(i));
A[i] = {v1,v2};
}
/* Create the C and X matrices */
Eigen::MatrixXd C(2,2);
Eigen::VectorXd X(2);
C(0,0) = 0.0;
C(0,1) = 0.0;
C(1,0) = 0.0;
C(1,1) = 0.0;
X(0) = 0.0;
X(1) = 0.0;
for( int i = 0; i < nPts; i++)
{
C(0,0) += A[i][0].dot(A[i][0]);
C(0,1) += A[i][0].dot(A[i][1]);
C(1,0) = C(0,1);
C(1,1) += A[i][1].dot(A[i][1]);
const Eigen::RowVectorXd tmp =
d.row(first+i)-(
d.row(first)*B0(uPrime(i))+
d.row(first)*B1(uPrime(i))+
d.row(last)*B2(uPrime(i))+
d.row(last)*B3(uPrime(i)));
X(0) += A[i][0].dot(tmp);
X(1) += A[i][1].dot(tmp);
}
/* Compute the determinants of C and X */
double det_C0_C1 = C(0,0) * C(1,1) - C(1,0) * C(0,1);
const double det_C0_X = C(0,0) * X(1) - C(0,1) * X(0);
const double det_X_C1 = X(0) * C(1,1) - X(1) * C(0,1);
/* Finally, derive alpha values */
if (det_C0_C1 == 0.0)
{
det_C0_C1 = (C(0,0) * C(1,1)) * 10e-12;
}
const double alpha_l = det_X_C1 / det_C0_C1;
const double alpha_r = 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. */
if (alpha_l < 1.0e-6 || alpha_r < 1.0e-6)
{
return Straight(d,first,last,tHat1,tHat2,bezCurve);
}
bezCurve.row(0) = d.row(first);
bezCurve.row(1) = d.row(first) + tHat1*alpha_l;
bezCurve.row(2) = d.row(last) + tHat2*alpha_r;
bezCurve.row(3) = d.row(last);
};
const int maxIterations = 4;
// This is a bad idea if error<1 ...
//const double iterationError = error * error;
const double iterationError = 100 * error;
const int nPts = last - first + 1;
/* Use heuristic if region only has two points in it */
if(nPts == 2)
{
Eigen::MatrixXd bezCurve;
Straight(d,first,last,tHat1,tHat2,bezCurve);
cubics.push_back(bezCurve);
return;
}
// ChordLengthParameterize
Eigen::VectorXd u(last-first+1);
u(0) = 0;
for (int i = first+1; i <= last; i++)
{
u(i-first) = u(i-first-1) + (d.row(i)-d.row(i-1)).norm();
}
for (int i = first + 1; i <= last; i++)
{
u(i-first) = u(i-first) / u(last-first);
}
Eigen::MatrixXd bezCurve;
GenerateBezier(d, first, last, u, tHat1, tHat2, bezCurve);
int splitPoint;
double maxError = ComputeMaxError(d, first, last, bezCurve, u, splitPoint);
if (!force_split && maxError < error)
{
cubics.push_back(bezCurve);
return;
}
/* If error not too large, try some reparameterization */
/* and iteration */
if (maxError < iterationError)
{
for (int i = 0; i < maxIterations; i++)
{
Eigen::VectorXd uPrime;
// Reparameterize
uPrime.resize(last-first+1);
for (int i = first; i <= last; i++)
{
uPrime(i-first) = NewtonRaphsonRootFind(bezCurve, d.row(i), u(i- first));
}
GenerateBezier(d, first, last, uPrime, tHat1, tHat2, bezCurve);
maxError = ComputeMaxError(d, first, last, bezCurve, uPrime, splitPoint);
if (!force_split && maxError < error) {
cubics.push_back(bezCurve);
return;
}
u = uPrime;
}
}
/* Fitting failed -- split at max error point and fit recursively */
const Eigen::RowVectorXd tHatCenter =
(d.row(splitPoint-1)-d.row(splitPoint+1)).normalized();
//foobar
fit_cubic_bezier_substring(
d,first,splitPoint,tHat1,tHatCenter,error,false,cubics);
fit_cubic_bezier_substring(
d,splitPoint,last,(-tHatCenter).eval(),tHat2,error,false,cubics);
}
#ifdef IGL_STATIC_LIBRARY
// Explicit template instantiation
#endif