// 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 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 #include #include #include namespace gte { template 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> const& points, size_t numIterations, bool useEllipseForInitialGuess, Ellipse2& ellipse) { Vector2 C; Matrix2x2 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 box; GetContainer(static_cast(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 solver; std::array eval; std::array, 2> evec; solver(M(0, 0), M(0, 1), M(1, 1), +1, eval, evec); Real const one = static_cast(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> const& points, Matrix2x2 const& M, Vector2& C) { Real const zero = static_cast(0); Real const one = static_cast(1); Real const two = static_cast(2); Real const three = static_cast(3); Real const four = static_cast(4); Real const epsilon = static_cast(1e-06); std::vector> MDelta(points.size()); std::vector a(points.size()); Real invQuantity = one / static_cast(points.size()); Vector2 negDFDC = Vector2::Zero(); Real aMean = zero, aaMean = zero; for (size_t i = 0; i < points.size(); ++i) { Vector2 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 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 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 rmMap; RootsPolynomial::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> const& points, Vector2 const& C, Matrix2x2& M) { Real const zero = static_cast(0); Real const one = static_cast(1); Real const two = static_cast(2); Real const half = static_cast(0.5); Real const epsilon = static_cast(1e-06); std::vector> Delta(points.size()); std::vector a(points.size()); Real invQuantity = one / static_cast(points.size()); Matrix2x2 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 q; q[0] = aaMean; q[1] = two * abMean; q[2] = bbMean; // Compute the coefficients of q'(t). std::array 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) 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 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> const& points, Vector2 const& C, Matrix2x2 const& M) const { Real error = static_cast(0); for (auto const& P : points) { Vector2 Delta = P - C; Real a = Dot(Delta, M * Delta) - static_cast(1); error += a * a; } error /= static_cast(points.size()); return error; } }; }