// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2005 Imetric 3D GmbH * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include // #define FC_USE_EIGEN #include #include #ifdef FC_USE_EIGEN # include #endif #include "Approximation.h" #include "CylinderFit.h" #include "Elements.h" #include "SphereFit.h" #include "Utilities.h" using namespace MeshCore; Approximation::Approximation() = default; Approximation::~Approximation() { Clear(); } void Approximation::GetMgcVectorArray(std::vector>& rcPts) const { std::list::const_iterator It; rcPts.reserve(_vPoints.size()); for (It = _vPoints.begin(); It != _vPoints.end(); ++It) { rcPts.push_back(Base::convertTo(*It)); } } void Approximation::AddPoint(const Base::Vector3f& point) { _vPoints.push_back(point); _bIsFitted = false; } void Approximation::AddPoints(const std::vector& points) { std::copy(points.begin(), points.end(), std::back_inserter(_vPoints)); _bIsFitted = false; } void Approximation::AddPoints(const std::set& points) { std::copy(points.begin(), points.end(), std::back_inserter(_vPoints)); _bIsFitted = false; } void Approximation::AddPoints(const std::list& points) { std::copy(points.begin(), points.end(), std::back_inserter(_vPoints)); _bIsFitted = false; } void Approximation::AddPoints(const MeshPointArray& points) { std::copy(points.begin(), points.end(), std::back_inserter(_vPoints)); _bIsFitted = false; } Base::Vector3f Approximation::GetGravity() const { Base::Vector3f clGravity; if (!_vPoints.empty()) { for (const auto& vPoint : _vPoints) { clGravity += vPoint; } clGravity *= 1.0F / float(_vPoints.size()); } return clGravity; } std::size_t Approximation::CountPoints() const { return _vPoints.size(); } void Approximation::Clear() { _vPoints.clear(); _bIsFitted = false; } float Approximation::GetLastResult() const { return _fLastResult; } bool Approximation::Done() const { return _bIsFitted; } // ------------------------------------------------------------------------------- PlaneFit::PlaneFit() : _vBase(0, 0, 0) , _vDirU(1, 0, 0) , _vDirV(0, 1, 0) , _vDirW(0, 0, 1) {} float PlaneFit::Fit() { _bIsFitted = true; if (CountPoints() < 3) { return std::numeric_limits::max(); } double sxx {0.0}; double sxy {0.0}; double sxz {0.0}; double syy {0.0}; double syz {0.0}; double szz {0.0}; double mx {0.0}; double my {0.0}; double mz {0.0}; for (const auto& vPoint : _vPoints) { sxx += double(vPoint.x * vPoint.x); sxy += double(vPoint.x * vPoint.y); sxz += double(vPoint.x * vPoint.z); syy += double(vPoint.y * vPoint.y); syz += double(vPoint.y * vPoint.z); szz += double(vPoint.z * vPoint.z); mx += double(vPoint.x); my += double(vPoint.y); mz += double(vPoint.z); } size_t nSize = _vPoints.size(); sxx = sxx - mx * mx / (double(nSize)); sxy = sxy - mx * my / (double(nSize)); sxz = sxz - mx * mz / (double(nSize)); syy = syy - my * my / (double(nSize)); syz = syz - my * mz / (double(nSize)); szz = szz - mz * mz / (double(nSize)); #if defined(FC_USE_EIGEN) Eigen::Matrix3d covMat = Eigen::Matrix3d::Zero(); covMat(0, 0) = sxx; covMat(1, 1) = syy; covMat(2, 2) = szz; covMat(0, 1) = sxy; covMat(1, 0) = sxy; covMat(0, 2) = sxz; covMat(2, 0) = sxz; covMat(1, 2) = syz; covMat(2, 1) = syz; Eigen::SelfAdjointEigenSolver eig(covMat); Eigen::Vector3d u = eig.eigenvectors().col(1); Eigen::Vector3d v = eig.eigenvectors().col(2); Eigen::Vector3d w = eig.eigenvectors().col(0); _vDirU.Set(u.x(), u.y(), u.z()); _vDirV.Set(v.x(), v.y(), v.z()); _vDirW.Set(w.x(), w.y(), w.z()); _vBase.Set(mx / (float)nSize, my / (float)nSize, mz / (float)nSize); float sigma = w.dot(covMat * w); #else // Covariance matrix Wm4::Matrix3 akMat(sxx, sxy, sxz, sxy, syy, syz, sxz, syz, szz); Wm4::Matrix3 rkRot, rkDiag; try { akMat.EigenDecomposition(rkRot, rkDiag); } catch (const std::exception&) { return std::numeric_limits::max(); } // We know the Eigenvalues are ordered // rkDiag(0,0) <= rkDiag(1,1) <= rkDiag(2,2) // // points describe a line or even are identical if (rkDiag(1, 1) <= 0) { return std::numeric_limits::max(); } Wm4::Vector3 U = rkRot.GetColumn(1); Wm4::Vector3 V = rkRot.GetColumn(2); Wm4::Vector3 W = rkRot.GetColumn(0); // It may happen that the result have nan values for (int i = 0; i < 3; i++) { if (boost::math::isnan(W[i])) { return std::numeric_limits::max(); } } // In some cases when the points exactly lie on a plane it can happen that // U or V have nan values but W is valid. // In this case create an orthonormal basis bool validUV = true; for (int i = 0; i < 3; i++) { if (boost::math::isnan(U[i]) || boost::math::isnan(V[i])) { validUV = false; break; } } if (!validUV) { Wm4::Vector3::GenerateOrthonormalBasis(U, V, W); } _vDirU.Set(float(U.X()), float(U.Y()), float(U.Z())); _vDirV.Set(float(V.X()), float(V.Y()), float(V.Z())); _vDirW.Set(float(W.X()), float(W.Y()), float(W.Z())); _vBase.Set(float(mx / nSize), float(my / nSize), float(mz / nSize)); float sigma = float(W.Dot(akMat * W)); #endif // In case sigma is nan if (boost::math::isnan(sigma)) { return std::numeric_limits::max(); } // This must be caused by some round-off errors. Theoretically it's impossible // that 'sigma' becomes negative because the covariance matrix is positive semi-definite. if (sigma < 0) { sigma = 0; } // make a right-handed system if ((_vDirU % _vDirV) * _vDirW < 0.0F) { Base::Vector3f tmp = _vDirU; _vDirU = _vDirV; _vDirV = tmp; } if (nSize > 3) { sigma = sqrt(sigma / (nSize - 3)); } else { sigma = 0; } _fLastResult = sigma; return _fLastResult; } Base::Vector3f PlaneFit::GetBase() const { if (_bIsFitted) { return _vBase; } return Base::Vector3f(); } Base::Vector3f PlaneFit::GetDirU() const { if (_bIsFitted) { return _vDirU; } return Base::Vector3f(); } Base::Vector3f PlaneFit::GetDirV() const { if (_bIsFitted) { return _vDirV; } return Base::Vector3f(); } Base::Vector3f PlaneFit::GetNormal() const { if (_bIsFitted) { return _vDirW; } return Base::Vector3f(); } float PlaneFit::GetDistanceToPlane(const Base::Vector3f& rcPoint) const { float fResult = std::numeric_limits::max(); if (_bIsFitted) { fResult = (rcPoint - _vBase) * _vDirW; } return fResult; } float PlaneFit::GetStdDeviation() const { // Mean: M=(1/N)*SUM Xi // Variance: VAR=(N/N-1)*[(1/N)*SUM(Xi^2)-M^2] // Standard deviation: SD=SQRT(VAR) // Standard error of the mean: SE=SD/SQRT(N) if (!_bIsFitted) { return std::numeric_limits::max(); } float fSumXi = 0.0F, fSumXi2 = 0.0F, fMean = 0.0F, fDist = 0.0F; float ulPtCt = float(CountPoints()); std::list::const_iterator cIt; for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { fDist = GetDistanceToPlane(*cIt); fSumXi += fDist; fSumXi2 += (fDist * fDist); } fMean = (1.0F / ulPtCt) * fSumXi; return sqrtf((ulPtCt / (ulPtCt - 1.0F)) * ((1.0F / ulPtCt) * fSumXi2 - fMean * fMean)); } float PlaneFit::GetSignedStdDeviation() const { // if the nearest point to the gravity is at the side // of normal direction the value will be // positive otherwise negative if (!_bIsFitted) { return std::numeric_limits::max(); } float fSumXi = 0.0F, fSumXi2 = 0.0F, fMean = 0.0F, fDist = 0.0F; float fMinDist = std::numeric_limits::max(); float fFactor = 0.0F; float ulPtCt = float(CountPoints()); Base::Vector3f clGravity, clPt; std::list::const_iterator cIt; for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { clGravity += *cIt; } clGravity *= (1.0F / ulPtCt); for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { if ((clGravity - *cIt).Length() < fMinDist) { fMinDist = (clGravity - *cIt).Length(); clPt = *cIt; } fDist = GetDistanceToPlane(*cIt); fSumXi += fDist; fSumXi2 += (fDist * fDist); } // which side if ((clPt - clGravity) * GetNormal() > 0) { fFactor = 1.0F; } else { fFactor = -1.0F; } fMean = 1.0F / ulPtCt * fSumXi; return fFactor * sqrtf((ulPtCt / (ulPtCt - 3.0F)) * ((1.0F / ulPtCt) * fSumXi2 - fMean * fMean)); } void PlaneFit::ProjectToPlane() { Base::Vector3f cGravity(GetGravity()); Base::Vector3f cNormal(GetNormal()); for (auto& cPnt : _vPoints) { float fD = (cPnt - cGravity) * cNormal; cPnt = cPnt - fD * cNormal; } } void PlaneFit::Dimension(float& length, float& width) const { const Base::Vector3f& bs = _vBase; const Base::Vector3f& ex = _vDirU; const Base::Vector3f& ey = _vDirV; Base::BoundBox3f bbox; std::list::const_iterator cIt; for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { Base::Vector3f pnt = *cIt; pnt.TransformToCoordinateSystem(bs, ex, ey); bbox.Add(pnt); } length = bbox.MaxX - bbox.MinX; width = bbox.MaxY - bbox.MinY; } std::vector PlaneFit::GetLocalPoints() const { std::vector localPoints; if (_bIsFitted && _fLastResult < std::numeric_limits::max()) { Base::Vector3d bs = Base::convertTo(this->_vBase); Base::Vector3d ex = Base::convertTo(this->_vDirU); Base::Vector3d ey = Base::convertTo(this->_vDirV); // Base::Vector3d ez = Base::convertTo(this->_vDirW); localPoints.insert(localPoints.begin(), _vPoints.begin(), _vPoints.end()); for (auto& localPoint : localPoints) { Base::Vector3d clPoint = Base::convertTo(localPoint); clPoint.TransformToCoordinateSystem(bs, ex, ey); localPoint.Set( static_cast(clPoint.x), static_cast(clPoint.y), static_cast(clPoint.z) ); } } return localPoints; } Base::BoundBox3f PlaneFit::GetBoundings() const { Base::BoundBox3f bbox; std::vector pts = GetLocalPoints(); for (const auto& it : pts) { bbox.Add(it); } return bbox; } // ------------------------------------------------------------------------------- bool QuadraticFit::GetCurvatureInfo( double x, double y, double z, double& rfCurv0, double& rfCurv1, Base::Vector3f& rkDir0, Base::Vector3f& rkDir1, double& dDistance ) { assert(_bIsFitted); bool bResult = false; if (_bIsFitted) { Wm4::Vector3 Dir0, Dir1; FunctionContainer clFuncCont(_fCoeff); bResult = clFuncCont.CurvatureInfo(x, y, z, rfCurv0, rfCurv1, Dir0, Dir1, dDistance); dDistance = double(clFuncCont.GetGradient(x, y, z).Length()); rkDir0 = Base::convertTo(Dir0); rkDir1 = Base::convertTo(Dir1); } return bResult; } bool QuadraticFit::GetCurvatureInfo(double x, double y, double z, double& rfCurv0, double& rfCurv1) { bool bResult = false; if (_bIsFitted) { FunctionContainer clFuncCont(_fCoeff); bResult = clFuncCont.CurvatureInfo(x, y, z, rfCurv0, rfCurv1); } return bResult; } const double& QuadraticFit::GetCoeffArray() const { return _fCoeff[0]; } double QuadraticFit::GetCoeff(std::size_t ulIndex) const { assert(ulIndex < 10); if (_bIsFitted) { return _fCoeff[ulIndex]; } return double(std::numeric_limits::max()); } float QuadraticFit::Fit() { float fResult = std::numeric_limits::max(); if (CountPoints() > 0) { std::vector> cPts; GetMgcVectorArray(cPts); fResult = (float)Wm4::QuadraticFit3(CountPoints(), cPts.data(), _fCoeff); _fLastResult = fResult; _bIsFitted = true; } return fResult; } void QuadraticFit::CalcEigenValues( double& dLambda1, double& dLambda2, double& dLambda3, Base::Vector3f& clEV1, Base::Vector3f& clEV2, Base::Vector3f& clEV3 ) const { assert(_bIsFitted); /* * F(x,y,z) = a11*x*x + a22*y*y + a33*z*z +2*a12*x*y + 2*a13*x*z + 2*a23*y*z + 2*a10*x + 2*a20*y * + 2*a30*z * a00 = 0 * * Form matrix: * * ( a11 a12 a13 ) * A = ( a21 a22 a23 ) where a[i,j] = a[j,i] * ( a31 a32 a33 ) * * Coefficients of the Quadrik-Fit based on the notation used here: * * 0 = C[0] + C[1]*X + C[2]*Y + C[3]*Z + C[4]*X^2 + C[5]*Y^2 * + C[6]*Z^2 + C[7]*X*Y + C[8]*X*Z + C[9]*Y*Z * * Square: a11 := c[4], a22 := c[5], a33 := c[6] * Mixed: a12 := c[7]/2, a13 := c[8]/2, a23 := c[9]/2 * Linear: a10 := c[1]/2, a20 := c[2]/2, a30 := c[3]/2 * Constant: a00 := c[0] * */ Wm4::Matrix3 akMat( _fCoeff[4], _fCoeff[7] / 2.0, _fCoeff[8] / 2.0, _fCoeff[7] / 2.0, _fCoeff[5], _fCoeff[9] / 2.0, _fCoeff[8] / 2.0, _fCoeff[9] / 2.0, _fCoeff[6] ); Wm4::Matrix3 rkRot, rkDiag; akMat.EigenDecomposition(rkRot, rkDiag); Wm4::Vector3 vEigenU = rkRot.GetColumn(0); Wm4::Vector3 vEigenV = rkRot.GetColumn(1); Wm4::Vector3 vEigenW = rkRot.GetColumn(2); clEV1 = Base::convertTo(vEigenU); clEV2 = Base::convertTo(vEigenV); clEV3 = Base::convertTo(vEigenW); dLambda1 = rkDiag[0][0]; dLambda2 = rkDiag[1][1]; dLambda3 = rkDiag[2][2]; } void QuadraticFit::CalcZValues(double x, double y, double& dZ1, double& dZ2) const { assert(_bIsFitted); double dDisk = _fCoeff[3] * _fCoeff[3] + 2 * _fCoeff[3] * _fCoeff[8] * x + 2 * _fCoeff[3] * _fCoeff[9] * y + _fCoeff[8] * _fCoeff[8] * x * x + 2 * _fCoeff[8] * x * _fCoeff[9] * y + _fCoeff[9] * _fCoeff[9] * y * y - 4 * _fCoeff[6] * _fCoeff[0] - 4 * _fCoeff[6] * _fCoeff[1] * x - 4 * _fCoeff[6] * _fCoeff[2] * y - 4 * _fCoeff[6] * _fCoeff[7] * x * y - 4 * _fCoeff[6] * _fCoeff[4] * x * x - 4 * _fCoeff[6] * _fCoeff[5] * y * y; if (fabs(_fCoeff[6]) < 0.000005) { dZ1 = double(std::numeric_limits::max()); dZ2 = double(std::numeric_limits::max()); return; } if (dDisk < 0.0) { dZ1 = double(std::numeric_limits::max()); dZ2 = double(std::numeric_limits::max()); return; } dDisk = sqrt(dDisk); dZ1 = 0.5 * ((-_fCoeff[3] - _fCoeff[8] * x - _fCoeff[9] * y + dDisk) / _fCoeff[6]); dZ2 = 0.5 * ((-_fCoeff[3] - _fCoeff[8] * x - _fCoeff[9] * y - dDisk) / _fCoeff[6]); } // ------------------------------------------------------------------------------- SurfaceFit::SurfaceFit() : _fCoeff {} {} float SurfaceFit::Fit() { float fResult = std::numeric_limits::max(); if (CountPoints() > 0) { fResult = float(PolynomFit()); _fLastResult = fResult; _bIsFitted = true; } return fResult; } bool SurfaceFit::GetCurvatureInfo( double x, double y, double z, double& rfCurv0, double& rfCurv1, Base::Vector3f& rkDir0, Base::Vector3f& rkDir1, double& dDistance ) { bool bResult = false; if (_bIsFitted) { Wm4::Vector3 Dir0, Dir1; FunctionContainer clFuncCont(_fCoeff); bResult = clFuncCont.CurvatureInfo(x, y, z, rfCurv0, rfCurv1, Dir0, Dir1, dDistance); dDistance = double(clFuncCont.GetGradient(x, y, z).Length()); rkDir0 = Base::convertTo(Dir0); rkDir1 = Base::convertTo(Dir1); } return bResult; } bool SurfaceFit::GetCurvatureInfo(double x, double y, double z, double& rfCurv0, double& rfCurv1) { assert(_bIsFitted); bool bResult = false; if (_bIsFitted) { FunctionContainer clFuncCont(_fCoeff); bResult = clFuncCont.CurvatureInfo(x, y, z, rfCurv0, rfCurv1); } return bResult; } double SurfaceFit::PolynomFit() { if (PlaneFit::Fit() >= std::numeric_limits::max()) { return double(std::numeric_limits::max()); } Base::Vector3d bs = Base::convertTo(this->_vBase); Base::Vector3d ex = Base::convertTo(this->_vDirU); Base::Vector3d ey = Base::convertTo(this->_vDirV); // Base::Vector3d ez = Base::convertTo(this->_vDirW); // A*x = b // See also www.cs.jhu.edu/~misha/Fall05/10.23.05.pdf // z = f(x,y) = a*x^2 + b*y^2 + c*x*y + d*x + e*y + f // z = P * Vi with Vi=(xi^2,yi^2,xiyi,xi,yi,1) and P=(a,b,c,d,e,f) // To get the best-fit values the sum needs to be minimized: // S = sum[(z-zi)^2} -> min with zi=z coordinates of the given points // <=> S = sum[z^2 - 2*z*zi + zi^2] -> min // <=> S(P) = sum[(P*Vi)^2 - 2*(P*Vi)*zi + zi^2] -> min // To get the optimum the gradient of the expression must be the null vector // Note: grad F(P) = (P*Vi)^2 = 2*(P*Vi)*Vi // grad G(P) = -2*(P*Vi)*zi = -2*Vi*zi // grad H(P) = zi^2 = 0 // => grad S(P) = sum[2*(P*Vi)*Vi - 2*Vi*zi] = 0 // <=> sum[(P*Vi)*Vi] = sum[Vi*zi] // <=> sum[(Vi*Vi^t)*P] = sum[Vi*zi] where (Vi*Vi^t) is a symmetric matrix // <=> sum[(Vi*Vi^t)]*P = sum[Vi*zi] Eigen::Matrix A = Eigen::Matrix::Zero(); Eigen::Matrix b = Eigen::Matrix::Zero(); Eigen::Matrix x = Eigen::Matrix::Zero(); std::vector transform; transform.reserve(_vPoints.size()); double dW2 = 0; for (const auto& it : _vPoints) { Base::Vector3d clPoint = Base::convertTo(it); clPoint.TransformToCoordinateSystem(bs, ex, ey); transform.push_back(clPoint); double dU = clPoint.x; double dV = clPoint.y; double dW = clPoint.z; double dU2 = dU * dU; double dV2 = dV * dV; double dUV = dU * dV; dW2 += dW * dW; A(0, 0) = A(0, 0) + dU2 * dU2; A(0, 1) = A(0, 1) + dU2 * dV2; A(0, 2) = A(0, 2) + dU2 * dUV; A(0, 3) = A(0, 3) + dU2 * dU; A(0, 4) = A(0, 4) + dU2 * dV; A(0, 5) = A(0, 5) + dU2; b(0) = b(0) + dU2 * dW; A(1, 1) = A(1, 1) + dV2 * dV2; A(1, 2) = A(1, 2) + dV2 * dUV; A(1, 3) = A(1, 3) + dV2 * dU; A(1, 4) = A(1, 4) + dV2 * dV; A(1, 5) = A(1, 5) + dV2; b(1) = b(1) + dV2 * dW; A(2, 2) = A(2, 2) + dUV * dUV; A(2, 3) = A(2, 3) + dUV * dU; A(2, 4) = A(2, 4) + dUV * dV; A(2, 5) = A(2, 5) + dUV; b(3) = b(3) + dUV * dW; A(3, 3) = A(3, 3) + dU * dU; A(3, 4) = A(3, 4) + dU * dV; A(3, 5) = A(3, 5) + dU; b(3) = b(3) + dU * dW; A(4, 4) = A(4, 4) + dV * dV; A(4, 5) = A(4, 5) + dV; b(5) = b(5) + dV * dW; A(5, 5) = A(5, 5) + 1; b(5) = b(5) + 1 * dW; } // Mat is symmetric // A(1, 0) = A(0, 1); A(2, 0) = A(0, 2); A(3, 0) = A(0, 3); A(4, 0) = A(0, 4); A(5, 0) = A(0, 5); A(2, 1) = A(1, 2); A(3, 1) = A(1, 3); A(4, 1) = A(1, 4); A(5, 1) = A(1, 5); A(3, 2) = A(2, 3); A(4, 2) = A(2, 4); A(5, 2) = A(2, 5); A(4, 3) = A(3, 4); A(5, 3) = A(3, 5); A(5, 4) = A(4, 5); Eigen::HouseholderQR> qr(A); x = qr.solve(b); // FunctionContainer gets an implicit function F(x,y,z) = 0 of this form // _fCoeff[0] + // _fCoeff[1]*x + _fCoeff[2]*y + _fCoeff[3]*z + // _fCoeff[4]*x^2 + _fCoeff[5]*y^2 + _fCoeff[6]*z^2 + // _fCoeff[7]*x*y + _fCoeff[8]*x*z + _fCoeff[9]*y*z // // The bivariate polynomial surface we get here is of the form // z = f(x,y) = a*x^2 + b*y^2 + c*x*y + d*x + e*y + f // Writing it as implicit surface F(x,y,z) = 0 gives this form // F(x,y,z) = f(x,y) - z = a*x^2 + b*y^2 + c*x*y + d*x + e*y - z + f // Thus: // _fCoeff[0] = f // _fCoeff[1] = d // _fCoeff[2] = e // _fCoeff[3] = -1 // _fCoeff[4] = a // _fCoeff[5] = b // _fCoeff[6] = 0 // _fCoeff[7] = c // _fCoeff[8] = 0 // _fCoeff[9] = 0 _fCoeff[0] = x(5); _fCoeff[1] = x(3); _fCoeff[2] = x(4); _fCoeff[3] = -1.0; _fCoeff[4] = x(0); _fCoeff[5] = x(1); _fCoeff[6] = 0.0; _fCoeff[7] = x(2); _fCoeff[8] = 0.0; _fCoeff[9] = 0.0; // Get S(P) = sum[(P*Vi)^2 - 2*(P*Vi)*zi + zi^2] double sigma = 0; FunctionContainer clFuncCont(_fCoeff); for (const auto& it : transform) { double u = it.x; double v = it.y; double z = clFuncCont.F(u, v, 0.0); sigma += z * z; } sigma += dW2 - 2 * x.dot(b); // This must be caused by some round-off errors. Theoretically it's impossible // that 'sigma' becomes negative. if (sigma < 0) { sigma = 0; } if (!_vPoints.empty()) { sigma = sqrt(sigma / _vPoints.size()); } _fLastResult = static_cast(sigma); return double(_fLastResult); } double SurfaceFit::Value(double x, double y) const { double z = 0.0; if (_bIsFitted) { FunctionContainer clFuncCont(_fCoeff); z = clFuncCont.F(x, y, 0.0); } return z; } void SurfaceFit::GetCoefficients(double& a, double& b, double& c, double& d, double& e, double& f) const { a = _fCoeff[4]; b = _fCoeff[5]; c = _fCoeff[7]; d = _fCoeff[1]; e = _fCoeff[2]; f = _fCoeff[0]; } void SurfaceFit::Transform(std::vector& pts) const { Base::Vector3d bs = Base::convertTo(this->_vBase); Base::Vector3d ex = Base::convertTo(this->_vDirU); Base::Vector3d ey = Base::convertTo(this->_vDirV); Base::Vector3d ez = Base::convertTo(this->_vDirW); Base::Matrix4D mat; mat[0][0] = ex.x; mat[0][1] = ey.x; mat[0][2] = ez.x; mat[0][3] = bs.x; mat[1][0] = ex.y; mat[1][1] = ey.y; mat[1][2] = ez.y; mat[1][3] = bs.y; mat[2][0] = ex.z; mat[2][1] = ey.z; mat[2][2] = ez.z; mat[2][3] = bs.z; std::transform(pts.begin(), pts.end(), pts.begin(), [&mat](const Base::Vector3f& pt) { Base::Vector3f v(pt); mat.multVec(v, v); return v; }); } void SurfaceFit::Transform(std::vector& pts) const { Base::Vector3d bs = Base::convertTo(this->_vBase); Base::Vector3d ex = Base::convertTo(this->_vDirU); Base::Vector3d ey = Base::convertTo(this->_vDirV); Base::Vector3d ez = Base::convertTo(this->_vDirW); Base::Matrix4D mat; mat[0][0] = ex.x; mat[0][1] = ey.x; mat[0][2] = ez.x; mat[0][3] = bs.x; mat[1][0] = ex.y; mat[1][1] = ey.y; mat[1][2] = ez.y; mat[1][3] = bs.y; mat[2][0] = ex.z; mat[2][1] = ey.z; mat[2][2] = ez.z; mat[2][3] = bs.z; std::transform(pts.begin(), pts.end(), pts.begin(), [&mat](const Base::Vector3d& pt) { Base::Vector3d v(pt); mat.multVec(v, v); return v; }); } /*! * \brief SurfaceFit::toBezier * This function computes the Bezier representation of the polynomial surface of the form * f(x,y) = a*x*x + b*y*y + c*x*y + d*y + e*f + f * by getting the 3x3 control points. */ std::vector SurfaceFit::toBezier(double umin, double umax, double vmin, double vmax) const { std::vector pts; pts.reserve(9); // the Bezier surface is defined by the 3x3 control points // P11 P21 P31 // P12 P22 P32 // P13 P23 P33 // // The surface goes through the points P11, P31, P31 and P33 // To get the four control points P21, P12, P32 and P23 we inverse // the de-Casteljau algorithm used for Bezier curves of degree 2 // as we already know the points for the parameters // (0, 0.5), (0.5, 0), (0.5, 1.0) and (1.0, 0.5) // To get the control point P22 we inverse the de-Casteljau algorithm // for the surface point on (0.5, 0.5) double umid = 0.5 * (umin + umax); double vmid = 0.5 * (vmin + vmax); // first row double z11 = Value(umin, vmin); double v21 = Value(umid, vmin); double z31 = Value(umax, vmin); double z21 = 2.0 * v21 - 0.5 * (z11 + z31); // third row double z13 = Value(umin, vmax); double v23 = Value(umid, vmax); double z33 = Value(umax, vmax); double z23 = 2.0 * v23 - 0.5 * (z13 + z33); // second row double v12 = Value(umin, vmid); double z12 = 2.0 * v12 - 0.5 * (z11 + z13); double v32 = Value(umax, vmid); double z32 = 2.0 * v32 - 0.5 * (z31 + z33); double v22 = Value(umid, vmid); double z22 = 4.0 * v22 - 0.25 * (z11 + z31 + z13 + z33 + 2.0 * (z12 + z21 + z32 + z23)); // first row pts.emplace_back(umin, vmin, z11); pts.emplace_back(umid, vmin, z21); pts.emplace_back(umax, vmin, z31); // second row pts.emplace_back(umin, vmid, z12); pts.emplace_back(umid, vmid, z22); pts.emplace_back(umax, vmid, z32); // third row pts.emplace_back(umin, vmax, z13); pts.emplace_back(umid, vmax, z23); pts.emplace_back(umax, vmax, z33); return pts; } // ------------------------------------------------------------------------------- namespace MeshCore { struct LMCylinderFunctor { Eigen::MatrixXd measuredValues; // Compute 'm' errors, one for each data point, for the given parameter values in 'x' int operator()(const Eigen::VectorXd& xvec, Eigen::VectorXd& fvec) const { // 'xvec' has dimensions n x 1 // It contains the current estimates for the parameters. // 'fvec' has dimensions m x 1 // It will contain the error for each data point. double aParam = xvec(0); // dir_x double bParam = xvec(1); // dir_y double cParam = xvec(2); // dir_z double dParam = xvec(3); // cnt_x double eParam = xvec(4); // cnt_y double fParam = xvec(5); // cnt_z double gParam = xvec(6); // radius // use distance functions (fvec(i)) for cylinders as defined in the paper: // Least-Squares Fitting Algorithms of the NIST Algorithm Testing System for (int i = 0; i < values(); i++) { double xValue = measuredValues(i, 0); double yValue = measuredValues(i, 1); double zValue = measuredValues(i, 2); double a = aParam / (sqrt(aParam * aParam + bParam * bParam + cParam * cParam)); double b = bParam / (sqrt(aParam * aParam + bParam * bParam + cParam * cParam)); double c = cParam / (sqrt(aParam * aParam + bParam * bParam + cParam * cParam)); double x = dParam; double y = eParam; double z = fParam; double u = c * (yValue - y) - b * (zValue - z); double v = a * (zValue - z) - c * (xValue - x); double w = b * (xValue - x) - a * (yValue - y); fvec(i) = sqrt(u * u + v * v + w * w) - gParam; } return 0; } // Compute the jacobian of the errors int df(const Eigen::VectorXd& x, Eigen::MatrixXd& fjac) const { // 'x' has dimensions n x 1 // It contains the current estimates for the parameters. // 'fjac' has dimensions m x n // It will contain the jacobian of the errors, calculated numerically in this case. const double epsilon = 1e-5; for (int i = 0; i < x.size(); i++) { Eigen::VectorXd xPlus(x); xPlus(i) += epsilon; Eigen::VectorXd xMinus(x); xMinus(i) -= epsilon; Eigen::VectorXd fvecPlus(values()); operator()(xPlus, fvecPlus); Eigen::VectorXd fvecMinus(values()); operator()(xMinus, fvecMinus); Eigen::VectorXd fvecDiff(values()); fvecDiff = (fvecPlus - fvecMinus) / (2.0F * epsilon); fjac.block(0, i, values(), 1) = fvecDiff; } return 0; } // Number of data points, i.e. values. int m; // Returns 'm', the number of values. int values() const { return m; } // The number of parameters, i.e. inputs. int n; // Returns 'n', the number of inputs. int inputs() const { return n; } }; } // namespace MeshCore CylinderFit::CylinderFit() : _vBase(0, 0, 0) , _vAxis(0, 0, 1) {} Base::Vector3f CylinderFit::GetInitialAxisFromNormals(const std::vector& n) const { #if 0 int nc = 0; double x = 0.0; double y = 0.0; double z = 0.0; for (int i = 0; i < (int)n.size()-1; ++i) { for (int j = i+1; j < (int)n.size(); ++j) { Base::Vector3f cross = n[i] % n[j]; if (cross.Sqr() > 1.0e-6) { cross.Normalize(); x += cross.x; y += cross.y; z += cross.z; ++nc; } } } if (nc > 0) { x /= (double)nc; y /= (double)nc; z /= (double)nc; Base::Vector3f axis(x,y,z); axis.Normalize(); return axis; } PlaneFit planeFit; planeFit.AddPoints(n); planeFit.Fit(); return planeFit.GetNormal(); #endif // Like a plane fit where the base is at (0,0,0) double sxx {0.0}; double sxy {0.0}; double sxz {0.0}; double syy {0.0}; double syz {0.0}; double szz {0.0}; for (auto it : n) { sxx += double(it.x * it.x); sxy += double(it.x * it.y); sxz += double(it.x * it.z); syy += double(it.y * it.y); syz += double(it.y * it.z); szz += double(it.z * it.z); } Eigen::Matrix3d covMat = Eigen::Matrix3d::Zero(); covMat(0, 0) = sxx; covMat(1, 1) = syy; covMat(2, 2) = szz; covMat(0, 1) = sxy; covMat(1, 0) = sxy; covMat(0, 2) = sxz; covMat(2, 0) = sxz; covMat(1, 2) = syz; covMat(2, 1) = syz; Eigen::SelfAdjointEigenSolver eig(covMat); Eigen::Vector3d w = eig.eigenvectors().col(0); Base::Vector3f normal; normal.Set(w.x(), w.y(), w.z()); return normal; } void CylinderFit::SetInitialValues(const Base::Vector3f& b, const Base::Vector3f& n) { _vBase = b; _vAxis = n; _initialGuess = true; } float CylinderFit::Fit() { if (CountPoints() < 7) { return std::numeric_limits::max(); } _bIsFitted = true; #if 1 // Do the cylinder fit MeshCoreFit::CylinderFit cylFit; cylFit.AddPoints(_vPoints); if (_initialGuess) { cylFit.SetApproximations( _fRadius, Base::Vector3d(_vBase.x, _vBase.y, _vBase.z), Base::Vector3d(_vAxis.x, _vAxis.y, _vAxis.z) ); } float result = cylFit.Fit(); if (result < std::numeric_limits::max()) { Base::Vector3d base = cylFit.GetBase(); Base::Vector3d dir = cylFit.GetAxis(); # if defined(FC_DEBUG) Base::Console().log( "MeshCoreFit::Cylinder Fit: Base: (%0.4f, %0.4f, %0.4f), Axis: (%0.6f, %0.6f, " "%0.6f), Radius: %0.4f, Std Dev: %0.4f, Iterations: %d\n", base.x, base.y, base.z, dir.x, dir.y, dir.z, cylFit.GetRadius(), cylFit.GetStdDeviation(), cylFit.GetNumIterations() ); # endif _vBase = Base::convertTo(base); _vAxis = Base::convertTo(dir); _fRadius = (float)cylFit.GetRadius(); _fLastResult = result; } #else int m = static_cast(_vPoints.size()); int n = 7; Eigen::MatrixXd measuredValues(m, 3); int index = 0; for (const auto& it : _vPoints) { measuredValues(index, 0) = it.x; measuredValues(index, 1) = it.y; measuredValues(index, 2) = it.z; index++; } Eigen::VectorXd x(n); x(0) = 1.0; // initial value for dir_x x(1) = 1.0; // initial value for dir_y x(2) = 1.0; // initial value for dir_z x(3) = 0.0; // initial value for cnt_x x(4) = 0.0; // initial value for cnt_y x(5) = 0.0; // initial value for cnt_z x(6) = 0.0; // initial value for radius // // Run the LM optimization // Create a LevenbergMarquardt object and pass it the functor. // LMCylinderFunctor functor; functor.measuredValues = measuredValues; functor.m = m; functor.n = n; Eigen::LevenbergMarquardt lm(functor); int status = lm.minimize(x); Base::Console() .log("Cylinder fit: %d, iterations: %d, gradient norm: %f\n", status, lm.iter, lm.gnorm); _vAxis.x = x(0); _vAxis.y = x(1); _vAxis.z = x(2); _vAxis.Normalize(); _vBase.x = x(3); _vBase.y = x(4); _vBase.z = x(5); _fRadius = x(6); _fLastResult = lm.gnorm; #endif return _fLastResult; } float CylinderFit::GetRadius() const { return _fRadius; } Base::Vector3f CylinderFit::GetBase() const { if (_bIsFitted) { return _vBase; } return Base::Vector3f(); } Base::Vector3f CylinderFit::GetAxis() const { if (_bIsFitted) { return _vAxis; } return Base::Vector3f(); } float CylinderFit::GetDistanceToCylinder(const Base::Vector3f& rcPoint) const { float fResult = std::numeric_limits::max(); if (_bIsFitted) { fResult = rcPoint.DistanceToLine(_vBase, _vAxis) - _fRadius; } return fResult; } float CylinderFit::GetStdDeviation() const { // Mean: M=(1/N)*SUM Xi // Variance: VAR=(N/N-1)*[(1/N)*SUM(Xi^2)-M^2] // Standard deviation: SD=SQRT(VAR) // Standard error of the mean: SE=SD/SQRT(N) if (!_bIsFitted) { return std::numeric_limits::max(); } float fSumXi = 0.0F, fSumXi2 = 0.0F, fMean = 0.0F, fDist = 0.0F; float ulPtCt = float(CountPoints()); std::list::const_iterator cIt; for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { fDist = GetDistanceToCylinder(*cIt); fSumXi += fDist; fSumXi2 += (fDist * fDist); } fMean = (1.0F / ulPtCt) * fSumXi; return sqrtf((ulPtCt / (ulPtCt - 1.0F)) * ((1.0F / ulPtCt) * fSumXi2 - fMean * fMean)); } void CylinderFit::GetBounding(Base::Vector3f& bottom, Base::Vector3f& top) const { float distMin = std::numeric_limits::max(); float distMax = std::numeric_limits::min(); std::list::const_iterator cIt; for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { float dist = cIt->DistanceToPlane(_vBase, _vAxis); if (dist < distMin) { distMin = dist; bottom = *cIt; } if (dist > distMax) { distMax = dist; top = *cIt; } } // Project the points onto the cylinder axis bottom = bottom.Perpendicular(_vBase, _vAxis); top = top.Perpendicular(_vBase, _vAxis); } void CylinderFit::ProjectToCylinder() { Base::Vector3f cBase(GetBase()); Base::Vector3f cAxis(GetAxis()); for (auto& cPnt : _vPoints) { if (cPnt.DistanceToLine(cBase, cAxis) > 0) { Base::Vector3f proj; cBase.ProjectToPlane(cPnt, cAxis, proj); Base::Vector3f diff = cPnt - proj; diff.Normalize(); cPnt = proj + diff * _fRadius; } else { // Point is on the cylinder axis, so it can be moved in // any direction perpendicular to the cylinder axis Base::Vector3f cMov(cPnt); do { float x = (float(rand()) / float(RAND_MAX)); float y = (float(rand()) / float(RAND_MAX)); float z = (float(rand()) / float(RAND_MAX)); cMov.Move(x, y, z); } while (cMov.DistanceToLine(cBase, cAxis) == 0); Base::Vector3f proj; cMov.ProjectToPlane(cPnt, cAxis, proj); Base::Vector3f diff = cPnt - proj; diff.Normalize(); cPnt = proj + diff * _fRadius; } } } // ----------------------------------------------------------------------------- SphereFit::SphereFit() : _vCenter(0, 0, 0) {} float SphereFit::GetRadius() const { if (_bIsFitted) { return _fRadius; } return std::numeric_limits::max(); } Base::Vector3f SphereFit::GetCenter() const { if (_bIsFitted) { return _vCenter; } return Base::Vector3f(); } float SphereFit::Fit() { _bIsFitted = true; if (CountPoints() < 4) { return std::numeric_limits::max(); } std::vector input; std::transform( _vPoints.begin(), _vPoints.end(), std::back_inserter(input), [](const Base::Vector3f& v) { return Wm4::Vector3d(v.x, v.y, v.z); } ); Wm4::Sphere3d sphere; Wm4::SphereFit3(input.size(), input.data(), 10, sphere, false); _vCenter = Base::convertTo(sphere.Center); _fRadius = float(sphere.Radius); // TODO _fLastResult = 0; #if defined(_DEBUG) Base::Console().message( " WildMagic Sphere Fit: Center: (%0.4f, %0.4f, %0.4f), Radius: " "%0.4f, Std Dev: %0.4f\n", _vCenter.x, _vCenter.y, _vCenter.z, _fRadius, GetStdDeviation() ); #endif MeshCoreFit::SphereFit sphereFit; sphereFit.AddPoints(_vPoints); sphereFit.ComputeApproximations(); float result = sphereFit.Fit(); if (result < std::numeric_limits::max()) { Base::Vector3d center = sphereFit.GetCenter(); #if defined(_DEBUG) Base::Console().message( "MeshCoreFit::Sphere Fit: Center: (%0.4f, %0.4f, %0.4f), Radius: " "%0.4f, Std Dev: %0.4f, Iterations: %d\n", center.x, center.y, center.z, sphereFit.GetRadius(), sphereFit.GetStdDeviation(), sphereFit.GetNumIterations() ); #endif _vCenter = Base::convertTo(center); _fRadius = (float)sphereFit.GetRadius(); _fLastResult = result; } return _fLastResult; } float SphereFit::GetDistanceToSphere(const Base::Vector3f& rcPoint) const { float fResult = std::numeric_limits::max(); if (_bIsFitted) { fResult = Base::Vector3f(rcPoint - _vCenter).Length() - _fRadius; } return fResult; } float SphereFit::GetStdDeviation() const { // Mean: M=(1/N)*SUM Xi // Variance: VAR=(N/N-1)*[(1/N)*SUM(Xi^2)-M^2] // Standard deviation: SD=SQRT(VAR) // Standard error of the mean: SE=SD/SQRT(N) if (!_bIsFitted) { return std::numeric_limits::max(); } float fSumXi = 0.0F, fSumXi2 = 0.0F, fMean = 0.0F, fDist = 0.0F; float ulPtCt = float(CountPoints()); std::list::const_iterator cIt; for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { fDist = GetDistanceToSphere(*cIt); fSumXi += fDist; fSumXi2 += (fDist * fDist); } fMean = (1.0F / ulPtCt) * fSumXi; return sqrtf((ulPtCt / (ulPtCt - 1.0F)) * ((1.0F / ulPtCt) * fSumXi2 - fMean * fMean)); } void SphereFit::ProjectToSphere() { for (auto& cPnt : _vPoints) { // Compute unit vector from sphere centre to point. // Because this vector is orthogonal to the sphere's surface at the // intersection point we can easily compute the projection point on the // closest surface point using the radius of the sphere Base::Vector3f diff = cPnt - _vCenter; double length = diff.Length(); if (length == 0.0) { // Point is exactly at the sphere center, so it can be projected in any direction onto // the sphere! So here just project in +Z direction cPnt.z += _fRadius; } else { diff /= length; // normalizing the vector cPnt = _vCenter + diff * _fRadius; } } } // ------------------------------------------------------------------------------- PolynomialFit::PolynomialFit() : _fCoeff {} {} float PolynomialFit::Fit() { std::vector x, y, z; x.reserve(_vPoints.size()); y.reserve(_vPoints.size()); z.reserve(_vPoints.size()); for (const auto& it : _vPoints) { x.push_back(it.x); y.push_back(it.y); z.push_back(it.z); } try { float* coeff = Wm4::PolyFit3(_vPoints.size(), x.data(), y.data(), z.data(), 2, 2); for (int i = 0; i < 9; i++) { _fCoeff[i] = coeff[i]; } } catch (const std::exception&) { return std::numeric_limits::max(); } return 0.0F; } float PolynomialFit::Value(float x, float y) const { float fValue = _fCoeff[0] + _fCoeff[1] * x + _fCoeff[2] * x * x + _fCoeff[3] * y + _fCoeff[4] * x * y + _fCoeff[5] * x * x * y + _fCoeff[6] * y * y + _fCoeff[7] * x * y * y + _fCoeff[8] * x * x * y * y; return fValue; }