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.
313 lines
12 KiB
313 lines
12 KiB
// David Eberly, Geometric Tools, Redmond WA 98052
|
|
// Copyright (c) 1998-2021
|
|
// Distributed under the Boost Software License, Version 1.0.
|
|
// https://www.boost.org/LICENSE_1_0.txt
|
|
// https://www.geometrictools.com/License/Boost/LICENSE_1_0.txt
|
|
// Version: 4.0.2020.10.10
|
|
|
|
#pragma once
|
|
|
|
// An ellipse is defined implicitly by (X-C)^T * M * (X-C) = 1, where C is
|
|
// the center, M is a positive definite matrix and X is any point on the
|
|
// ellipsoid. The code implements a nonlinear least-squares fitting algorithm
|
|
// for the error function
|
|
// F(C,M) = sum_{i=0}^{n-1} ((X[i] - C)^T * M * (X[i] - C) - 1)^2
|
|
// for n data points X[0] through X[n-1]. An Ellipse2<Real> object has
|
|
// member 'center' that corresponds to C. It also has axes with unit-length
|
|
// directions 'axis[]' and corresponding axis half-lengths 'extent[]'. The
|
|
// matrix is M = sum_{i=0}^1 axis[i] * axis[i]^T / extent[i]^2, where axis[i]
|
|
// is a 2x1 vector and axis[i]^T is a 1x2 vector.
|
|
//
|
|
// The minimizer uses a 2-step gradient descent algorithm.
|
|
//
|
|
// Given the current (C,M), locate a minimum of
|
|
// G(t) = F(C - t * dF(C,M)/dC, M)
|
|
// for t > 0. The function G(t) >= 0 is a polynomial of degree 4 with
|
|
// derivative G'(t) that is a polynomial of degree 3. G'(t) must have a
|
|
// positive root because G(0) > 0 and G'(0) < 0 and the G-coefficient of t^4
|
|
// is positive. The positive root that T produces the smallest G-value is used
|
|
// to update the center C' = C - T * dF/dC(C,M).
|
|
//
|
|
// Given the current (C,M), locate a minimum of
|
|
// H(t) = F(C, M - t * dF(C,M)/dM)
|
|
// for t > 0. The function H(t) >= 0 is a polynomial of degree 2 with
|
|
// derivative H'(t) that is a polynomial of degree 1. H'(t) must have a
|
|
// positive root because H(0) > 0 and H'(0) < 0 and the H-coefficient of t^2
|
|
// is positive. The positive root T that produces the smallest G-value is used
|
|
// to update the matrix M' = M - T * dF/dC(C,M) as long as M' is positive
|
|
// definite. If M' is not positive definite, the root is halved for a finite
|
|
// number of steps until M' is positive definite.
|
|
|
|
#include <Mathematics/ContOrientedBox2.h>
|
|
#include <Mathematics/Hyperellipsoid.h>
|
|
#include <Mathematics/Matrix2x2.h>
|
|
#include <Mathematics/RootsPolynomial.h>
|
|
|
|
namespace gte
|
|
{
|
|
template <typename Real>
|
|
class ApprEllipse2
|
|
{
|
|
public:
|
|
// If you want this function to compute the initial guess for the
|
|
// ellipsoid, set 'useEllipseForInitialGuess' to true. An oriented
|
|
// bounding box containing the points is used to start the minimizer.
|
|
// Set 'useEllipseForInitialGuess' to true if you want the initial
|
|
// guess to be the input ellipse. This is useful if you want to
|
|
// repeat the query. The returned 'Real' value is the error function
|
|
// value for the output 'ellipse'.
|
|
Real operator()(std::vector<Vector2<Real>> const& points,
|
|
size_t numIterations, bool useEllipseForInitialGuess,
|
|
Ellipse2<Real>& ellipse)
|
|
{
|
|
Vector2<Real> C;
|
|
Matrix2x2<Real> M; // the zero matrix
|
|
if (useEllipseForInitialGuess)
|
|
{
|
|
C = ellipse.center;
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
auto product = OuterProduct(ellipse.axis[i], ellipse.axis[i]);
|
|
M += product / (ellipse.extent[i] * ellipse.extent[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OrientedBox2<Real> box;
|
|
GetContainer(static_cast<int>(points.size()), points.data(), box);
|
|
C = box.center;
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
auto product = OuterProduct(box.axis[i], box.axis[i]);
|
|
M += product / (box.extent[i] * box.extent[i]);
|
|
}
|
|
}
|
|
|
|
Real error = ErrorFunction(points, C, M);
|
|
for (size_t i = 0; i < numIterations; ++i)
|
|
{
|
|
error = UpdateMatrix(points, C, M);
|
|
error = UpdateCenter(points, M, C);
|
|
}
|
|
|
|
// Extract the ellipse axes and extents.
|
|
SymmetricEigensolver2x2<Real> solver;
|
|
std::array<Real, 2> eval;
|
|
std::array<std::array<Real, 2>, 2> evec;
|
|
solver(M(0, 0), M(0, 1), M(1, 1), +1, eval, evec);
|
|
|
|
Real const one = static_cast<Real>(1);
|
|
ellipse.center = C;
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
ellipse.axis[i] = { evec[i][0], evec[i][1] };
|
|
ellipse.extent[i] = one / std::sqrt(eval[i]);
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
private:
|
|
Real UpdateCenter(std::vector<Vector2<Real>> const& points,
|
|
Matrix2x2<Real> const& M, Vector2<Real>& C)
|
|
{
|
|
Real const zero = static_cast<Real>(0);
|
|
Real const one = static_cast<Real>(1);
|
|
Real const two = static_cast<Real>(2);
|
|
Real const three = static_cast<Real>(3);
|
|
Real const four = static_cast<Real>(4);
|
|
Real const epsilon = static_cast<Real>(1e-06);
|
|
|
|
std::vector<Vector2<Real>> MDelta(points.size());
|
|
std::vector<Real> a(points.size());
|
|
Real invQuantity = one / static_cast<Real>(points.size());
|
|
Vector2<Real> negDFDC = Vector2<Real>::Zero();
|
|
Real aMean = zero, aaMean = zero;
|
|
for (size_t i = 0; i < points.size(); ++i)
|
|
{
|
|
Vector2<Real> Delta = points[i] - C;
|
|
MDelta[i] = M * Delta;
|
|
a[i] = Dot(Delta, MDelta[i]) - one;
|
|
aMean += a[i];
|
|
aaMean += a[i] * a[i];
|
|
negDFDC += a[i] * MDelta[i];
|
|
}
|
|
aMean *= invQuantity;
|
|
aaMean *= invQuantity;
|
|
if (Normalize(negDFDC) < epsilon)
|
|
{
|
|
return aaMean;
|
|
}
|
|
|
|
Real bMean = zero, abMean = zero, bbMean = zero;
|
|
Real c = Dot(negDFDC, M * negDFDC);
|
|
for (size_t i = 0; i < points.size(); ++i)
|
|
{
|
|
Real b = Dot(negDFDC, MDelta[i]);
|
|
bMean += b;
|
|
abMean += a[i] * b;
|
|
bbMean += b * b;
|
|
}
|
|
bMean *= invQuantity;
|
|
abMean *= invQuantity;
|
|
bbMean *= invQuantity;
|
|
|
|
// Compute the coefficients of the quartic polynomial q(t) that
|
|
// represents the error function on the given line in the gradient
|
|
// descent minimization.
|
|
std::array<Real, 5> q;
|
|
q[0] = aaMean;
|
|
q[1] = -four * abMean;
|
|
q[2] = four * bbMean + two * c * aMean;
|
|
q[3] = -four * c * bMean;
|
|
q[4] = c * c;
|
|
|
|
// Compute the coefficients of q'(t).
|
|
std::array<Real, 4> dq;
|
|
dq[0] = q[1];
|
|
dq[1] = two * q[2];
|
|
dq[2] = three * q[3];
|
|
dq[3] = four * q[4];
|
|
|
|
// Compute the roots of q'(t).
|
|
std::map<Real, int> rmMap;
|
|
RootsPolynomial<Real>::SolveCubic(dq[0], dq[1], dq[2], dq[3], rmMap);
|
|
|
|
// Choose the root that leads to the minimum along the gradient descent
|
|
// line and update the center to that point.
|
|
Real minError = aaMean;
|
|
Real minRoot = zero;
|
|
for (auto const& rm : rmMap)
|
|
{
|
|
Real root = rm.first;
|
|
if (root > zero)
|
|
{
|
|
Real error = q[0] + root * (q[1] + root * (q[2] + root * (q[3] + root * q[4])));
|
|
if (error < minError)
|
|
{
|
|
minError = error;
|
|
minRoot = root;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (minRoot > zero)
|
|
{
|
|
C += minRoot * negDFDC;
|
|
return minError;
|
|
}
|
|
return aaMean;
|
|
}
|
|
|
|
Real UpdateMatrix(std::vector<Vector2<Real>> const& points,
|
|
Vector2<Real> const& C, Matrix2x2<Real>& M)
|
|
{
|
|
Real const zero = static_cast<Real>(0);
|
|
Real const one = static_cast<Real>(1);
|
|
Real const two = static_cast<Real>(2);
|
|
Real const half = static_cast<Real>(0.5);
|
|
Real const epsilon = static_cast<Real>(1e-06);
|
|
|
|
std::vector<Vector2<Real>> Delta(points.size());
|
|
std::vector<Real> a(points.size());
|
|
Real invQuantity = one / static_cast<Real>(points.size());
|
|
Matrix2x2<Real> negDFDM; // zero matrix, symmetric
|
|
|
|
Real aaMean = zero;
|
|
for (size_t i = 0; i < points.size(); ++i)
|
|
{
|
|
Delta[i] = points[i] - C;
|
|
a[i] = Dot(Delta[i], M * Delta[i]) - one;
|
|
Real twoA = two * a[i];
|
|
negDFDM(0, 0) -= a[i] * Delta[i][0] * Delta[i][0];
|
|
negDFDM(0, 1) -= twoA * Delta[i][0] * Delta[i][1];
|
|
negDFDM(1, 1) -= a[i] * Delta[i][1] * Delta[i][1];
|
|
aaMean += a[i] * a[i];
|
|
}
|
|
aaMean *= invQuantity;
|
|
|
|
// Normalize the matrix as if it were a vector of numbers.
|
|
Real length = std::sqrt(
|
|
negDFDM(0, 0) * negDFDM(0, 0) + negDFDM(0, 1) * negDFDM(0, 1) +
|
|
negDFDM(1, 1) * negDFDM(1, 1));
|
|
if (length < epsilon)
|
|
{
|
|
return aaMean;
|
|
}
|
|
Real invLength = one / length;
|
|
negDFDM(0, 0) *= invLength;
|
|
negDFDM(0, 1) *= invLength;
|
|
negDFDM(1, 1) *= invLength;
|
|
|
|
// Fill in the lower triangular portion because negGradM is a
|
|
// symmetric matrix.
|
|
negDFDM(1, 0) = negDFDM(0, 1);
|
|
|
|
Real abMean = zero, bbMean = zero;
|
|
for (size_t i = 0; i < points.size(); ++i)
|
|
{
|
|
Real b = Dot(Delta[i], negDFDM * Delta[i]);
|
|
abMean += a[i] * b;
|
|
bbMean += b * b;
|
|
}
|
|
abMean *= invQuantity;
|
|
bbMean *= invQuantity;
|
|
|
|
// Compute the coefficients of the quadratic polynomial q(t) that
|
|
// represents the error function on the given line in the gradient
|
|
// descent minimization.
|
|
std::array<Real, 3> q;
|
|
q[0] = aaMean;
|
|
q[1] = two * abMean;
|
|
q[2] = bbMean;
|
|
|
|
// Compute the coefficients of q'(t).
|
|
std::array<Real, 2> dq;
|
|
dq[0] = q[1];
|
|
dq[1] = two * q[2];
|
|
|
|
// Compute the root as long as it is positive and
|
|
// M + root * negGradM is a positive definite matrix.
|
|
Real root = -dq[0] / dq[1];
|
|
if (root > zero)
|
|
{
|
|
// Use Sylvester's criterion for testing positive definitess.
|
|
// A for(;;) loop terminates for floating-point arithmetic but
|
|
// not for rational (BSRational<UInteger>) arithmetic. Limit
|
|
// the number of iterations so that the loop terminates for
|
|
// rational arithmetic but 'return' occurs for floating-point
|
|
// arithmetic.
|
|
for (size_t k = 0; k < 2048; ++k)
|
|
{
|
|
Matrix2x2<Real> nextM = M + root * negDFDM;
|
|
if (nextM(0, 0) > zero)
|
|
{
|
|
Real det = Determinant(nextM);
|
|
if (det > zero)
|
|
{
|
|
M = nextM;
|
|
Real minError = q[0] + root * (q[1] + root * q[2]);
|
|
return minError;
|
|
}
|
|
}
|
|
root *= half;
|
|
}
|
|
}
|
|
return aaMean;
|
|
}
|
|
|
|
Real ErrorFunction(std::vector<Vector2<Real>> const& points,
|
|
Vector2<Real> const& C, Matrix2x2<Real> const& M) const
|
|
{
|
|
Real error = static_cast<Real>(0);
|
|
for (auto const& P : points)
|
|
{
|
|
Vector2<Real> Delta = P - C;
|
|
Real a = Dot(Delta, M * Delta) - static_cast<Real>(1);
|
|
error += a * a;
|
|
}
|
|
error /= static_cast<Real>(points.size());
|
|
return error;
|
|
}
|
|
};
|
|
}
|
|
|