MeshLib
 
Loading...
Searching...
No Matches
MRConeApproximator.h
Go to the documentation of this file.
1#pragma once
2
3#include "MRMeshFwd.h"
4#include "MRCone3.h"
5#include "MRToFromEigen.h"
6#include "MRConstants.h"
7#include "MRPch/MRTBB.h"
8#include <algorithm>
9
10#ifdef _MSC_VER
11#pragma warning(push)
12#pragma warning(disable: 4068) // unknown pragmas
13#pragma warning(disable: 4127) // conditional expression is constant
14#pragma warning(disable: 4464) // relative include path contains '..'
15#pragma warning(disable: 4643) // Forward declaring 'tuple' in namespace std is not permitted by the C++ Standard.
16#pragma warning(disable: 5054) // operator '|': deprecated between enumerations of different types
17#pragma warning(disable: 4244) // casting float to double
18#elif defined(__clang__)
19#elif defined(__GNUC__)
20#pragma GCC diagnostic push
21#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
22#endif
23
24#include <unsupported/Eigen/NonLinearOptimization>
25#include <unsupported/Eigen/NumericalDiff>
26#include <Eigen/Dense>
27
28#ifdef _MSC_VER
29#pragma warning(pop)
30#elif defined(__clang__)
31#elif defined(__GNUC__)
32#pragma GCC diagnostic pop
33#endif
34
35
36// Main idea is here: https://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf pages 45-51
37// Below we will write out the function and Jacobian for minimization by the Levenberg-Marquard method
38// and use it to clarify the apex of the cone and the direction of its main axis.
39
40namespace MR
41{
42
43// to use Levenberg-Marquardt minimization we need a special type of functor
44// to look1: https://github.com/cryos/eigen/blob/master/unsupported/test/NonLinearOptimization.cpp : lmder_functor
45// to look2: https://eigen.tuxfamily.org/dox-devel/unsupported/group__NonLinearOptimization__Module.html
46template <typename T>
48{
49 using Scalar = T;
50 using InputType = Eigen::Matrix<T, Eigen::Dynamic, 1>;
51 using ValueType = Eigen::Matrix<T, Eigen::Dynamic, 1>;
52 using JacobianType = Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic>;
53
54 std::vector <Eigen::Vector3<T>> points;
55
56 void setPoints( const std::vector<MR::Vector3<T>>& pointsMR )
57 {
58 points.reserve( pointsMR.size() );
59 for ( auto i = 0; i < pointsMR.size(); ++i )
60 {
61 points.push_back( toEigen( pointsMR[i] ) );
62 }
63 }
64
65 int inputs() const
66 {
67 return 6;
68 }
69 int values() const
70 {
71 return static_cast< int > ( points.size() );
72 }
73
74 // https://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf formula 103
75 // F[i](V,W) = D^T * (I - W * W^T) * D
76 // where: D = V - X[i] and P = (V,W)
77 int operator()( const InputType& x, ValueType& F ) const
78 {
79 Eigen::Vector3<T> V;
80 V( 0 ) = x( 0 );
81 V( 1 ) = x( 1 );
82 V( 2 ) = x( 2 );
83
84 Eigen::Vector3<T> W;
85 W( 0 ) = x( 3 );
86 W( 1 ) = x( 4 );
87 W( 2 ) = x( 5 );
88
89 for ( int i = 0; i < points.size(); ++i )
90 {
91 Eigen::Vector3<T> delta = V - points[i];
92 T deltaDotW = delta.dot( W );
93 F[i] = delta.squaredNorm() - deltaDotW * deltaDotW;
94 }
95 return 0;
96 }
97
98 // Here we calculate a Jacobian.
99 // function name requested by Eigen lib.
100 int df( const InputType& x, JacobianType& J ) const
101 {
102
103 Eigen::Vector3<T> V;
104 V( 0 ) = x( 0 );
105 V( 1 ) = x( 1 );
106 V( 2 ) = x( 2 );
107
108 Eigen::Vector3<T> W;
109 W( 0 ) = x( 3 );
110 W( 1 ) = x( 4 );
111 W( 2 ) = x( 5 );
112
113 for ( int i = 0; i < points.size(); ++i )
114 {
115 const Eigen::Vector3<T>& P = points[i];
116 Eigen::Vector3<T> D = ( V - P );
117 T PW = D.dot( W );
118 Eigen::Vector3<T> PVW = V - P - PW * W;
119 Eigen::Vector3<T> PWD = PW * D;
120
121 // Derivative of f with respect to the components of vertex V
122 J( i, 0 ) = 2 * PVW.x();
123 J( i, 1 ) = 2 * PVW.y();
124 J( i, 2 ) = 2 * PVW.z();
125
126 // Derivative of f with respect to the components of the vector W
127 J( i, 3 ) = -2 * PWD.x();
128 J( i, 4 ) = -2 * PWD.y();
129 J( i, 5 ) = -2 * PWD.z();
130 }
131 return 0;
132 }
133
134};
135
136
138{
139 ApproximationPCM, // approximation of cone axis by principal component method
142};
143
150
151// Class for approximation cloud point by cone.
152// We will calculate the initial approximation of the cone and then use a minimizer to refine the parameters.
153// minimizer is LevenbergMarquardt now.
154// TODO: Possible we could add GaussNewton in future.
155template <typename T>
157{
158public:
159
161
162 // returns RMS for original points
163 T solve( const std::vector<MR::Vector3<T>>& points,
164 Cone3<T>& cone, const Cone3ApproximationParams& params = {} )
165
166 {
167 params_ = params;
168
169 switch ( params_.coneFitterType )
170 {
172 return solveSpecificAxisFit_( points, cone );
173 break;
175 return solveHemisphereSearchFit_( points, cone );
176 break;
178 return solveApproximationPCM_( points, cone );
179 break;
180 default:
181 return std::numeric_limits<T>::max();
182 break;
183 };
184 }
185
186
187private:
188
189 // cone fitter main params
190 Cone3ApproximationParams params_;
191
192 // solver for single axis case.
193 T solveFixedAxis_( const std::vector<MR::Vector3<T>>& points,
194 Cone3<T>& cone, bool useConeInputAsInitialGuess = false )
195
196 {
197 ConeFittingFunctor<T> coneFittingFunctor;
198 coneFittingFunctor.setPoints( points );
199 Eigen::LevenbergMarquardt<ConeFittingFunctor<T>, T> lm( coneFittingFunctor );
200 lm.parameters.maxfev = params_.levenbergMarquardtMaxIteration;
201
202 MR::Vector3<T> center, U;
203 computeCenterAndNormal_( points, center, U );
204
205 MR::Vector3<T>& coneAxis = cone.direction();
206 if ( useConeInputAsInitialGuess )
207 {
208 coneAxis = coneAxis.normalized();
209 }
210 else
211 {
212 cone = computeInitialCone_( points, center, U );
213 }
214
215 Eigen::VectorX<T> fittedParams( 6 );
216 coneToFitParams_( cone, fittedParams );
217 [[maybe_unused]] Eigen::LevenbergMarquardtSpace::Status result = lm.minimize( fittedParams );
218
219 // Looks like a bug in Eigen. Eigen::LevenbergMarquardtSpace::Status have error codes only. Not return value for Success minimization.
220 // So just log status
221
222 fitParamsToCone_( fittedParams, cone );
223
224 T const one_v = static_cast< T >( 1 );
225 auto cosAngle = std::clamp( one_v / coneAxis.length(), static_cast< T >( 0 ), one_v );
226 cone.angle = std::acos( cosAngle );
227 cone.direction() = cone.direction().normalized();
228 cone.height = calculateConeHeight_( points, cone );
229
230 return getApproximationRMS_( points, cone );
231 }
232
233 T solveApproximationPCM_( const std::vector<MR::Vector3<T>>& points, Cone3<T>& cone )
234 {
235 return solveFixedAxis_( points, cone, false );
236 }
237
238 T solveSpecificAxisFit_( const std::vector<MR::Vector3<T>>& points, Cone3<T>& cone )
239 {
240 return solveFixedAxis_( points, cone, true );
241 }
242
243 // brute force solver across hole hemisphere for cone axis original extimation.
244 T solveHemisphereSearchFit_( const std::vector<MR::Vector3<T>>& points, Cone3<T>& cone )
245 {
246 Vector3<T> center = computeCenter_( points );
247 ConeFittingFunctor<T> coneFittingFunctor;
248 coneFittingFunctor.setPoints( points );
249
250 constexpr T pi2 = static_cast< T >( PI2 );
251 T const theraStep = static_cast< T >( 2 * PI ) / params_.hemisphereSearchPhiResolution;
252 T const phiStep = pi2 / params_.hemisphereSearchPhiResolution;
253
254 struct BestCone {
255 Cone3<T> bestCone;
256 T minError = std::numeric_limits<T> ::max();
257 };
258 std::vector<BestCone> bestCones;
259 bestCones.resize( params_.hemisphereSearchPhiResolution + 1 );
260
261 tbb::parallel_for( tbb::blocked_range<size_t>( size_t( 0 ), params_.hemisphereSearchPhiResolution + 1 ),
262 [&] ( const tbb::blocked_range<size_t>& range )
263 {
264 for ( size_t j = range.begin(); j < range.end(); ++j )
265 {
266 T phi = phiStep * j; // [0 .. pi/2]
267 T cosPhi = std::cos( phi );
268 T sinPhi = std::sin( phi );
269 for ( size_t i = 0; i < params_.hemisphereSearchThetaResolution; ++i )
270 {
271 T theta = theraStep * i; // [0 .. 2*pi)
272 T cosTheta = std::cos( theta );
273 T sinTheta = std::sin( theta );
274
275 // cone main axis original extimation
276 Vector3<T> U( cosTheta * sinPhi, sinTheta * sinPhi, cosPhi );
277
278 auto tmpCone = computeInitialCone_( points, center, U );
279
280 Eigen::VectorX<T> fittedParams( 6 );
281 coneToFitParams_( tmpCone, fittedParams );
282
283 // create approximator and minimize functor
284 Eigen::LevenbergMarquardt<ConeFittingFunctor<T>, T> lm( coneFittingFunctor );
285 lm.parameters.maxfev = params_.levenbergMarquardtMaxIteration;
286 [[maybe_unused]] Eigen::LevenbergMarquardtSpace::Status result = lm.minimize( fittedParams );
287
288 // Looks like a bug in Eigen. Eigen::LevenbergMarquardtSpace::Status have error codes only.
289 // Not return value for Success minimization.
290
291 fitParamsToCone_( fittedParams, tmpCone );
292
293 T const one_v = static_cast< T >( 1 );
294 auto cosAngle = std::clamp( one_v / tmpCone.direction().length(), static_cast< T >( 0 ), one_v );
295 tmpCone.angle = std::acos( cosAngle );
296 tmpCone.direction() = tmpCone.direction().normalized();
297
298 // calculate approximation error and store best result.
299 T error = getApproximationRMS_( points, tmpCone );
300 if ( error < bestCones[j].minError )
301 {
302 bestCones[j].minError = error;
303 bestCones[j].bestCone = tmpCone;
304 }
305 }
306 }
307 } );
308
309 // find best result
310 auto bestAppox = std::min_element( bestCones.begin(), bestCones.end(), [] ( const BestCone& a, const BestCone& b )
311 {
312 return a.minError < b.minError;
313 } );
314
315 cone = bestAppox->bestCone;
316
317 // calculate cone height
318 cone.height = calculateConeHeight_( points, cone );
319
320 return bestAppox->minError;
321 }
322
323 // Calculate and return a length of cone based on set of initil points and inifinite cone surface given by cone param.
324 T calculateConeHeight_( const std::vector<MR::Vector3<T>>& points, Cone3<T>& cone )
325 {
326 T length = static_cast< T > ( 0 );
327 for ( auto i = 0; i < points.size(); ++i )
328 {
329 length = std::max( length, std::abs( MR::dot( points[i] - cone.apex(), cone.direction() ) ) );
330 }
331 return length;
332 }
333
334 T getApproximationRMS_( const std::vector<MR::Vector3<T>>& points, const Cone3<T>& cone )
335 {
336 if ( points.size() == 0 )
337 return std::numeric_limits<T>::max();
338
339 T error = 0;
340 for ( auto p : points )
341 error = error + ( cone.projectPoint( p ) - p ).lengthSq();
342
343 return error / points.size();
344 }
345
346 MR::Vector3<T> computeCenter_( const std::vector<MR::Vector3<T>>& points )
347 {
348 // Compute the average of the sample points.
349 MR::Vector3<T> center; // C in pdf
350 for ( auto i = 0; i < points.size(); ++i )
351 {
352 center += points[i];
353 }
354 center = center / static_cast< T >( points.size() );
355 return center;
356 }
357
358
359 void computeCenterAndNormal_( const std::vector<MR::Vector3<T>>& points, MR::Vector3<T>& center, MR::Vector3<T>& U )
360 {
361 center = computeCenter_( points );
362
363 // The cone axis is estimated from ZZTZ (see the https://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf, formula 120).
364 U = Vector3f(); // U in pdf
365 for ( auto i = 0; i < points.size(); ++i )
366 {
367 Vector3<T> Z = points[i] - center;
368 U += Z * MR::dot( Z, Z );
369 }
370 U = U.normalized();
371 }
372
373
374 // Calculates the initial parameters of the cone, which will later be used for minimization.
375 Cone3<T> computeInitialCone_( const std::vector<MR::Vector3<T>>& points, const MR::Vector3<T>& center, const MR::Vector3<T>& axis )
376 {
377 Cone3<T> result;
378 MR::Vector3<T>& coneApex = result.apex();
379 result.direction() = axis;
380 MR::Vector3<T>& U = result.direction(); // coneAxis
381 T& coneAngle = result.angle;
382
383 // C is center, U is coneAxis, X is points
384 // Compute the signed heights of the points along the cone axis relative to C.
385 // These are the projections of the points onto the line C+t*U. Also compute
386 // the radial distances of the points from the line C+t*U
387
388 std::vector<Vector2<T>> hrPairs( points.size() );
389 T hMin = std::numeric_limits<T>::max(), hMax = -hMin;
390 for ( auto i = 0; i < points.size(); ++i )
391 {
392 MR::Vector3<T> delta = points[i] - center;
393 T h = MR::dot( U, delta );
394 hMin = std::min( hMin, h );
395 hMax = std::max( hMax, h );
396 Vector3<T> projection = delta - MR::dot( U, delta ) * U;
397 T r = projection.length();
398 hrPairs[i] = { h, r };
399 }
400
401 // The radial distance is considered to be a function of height. Fit the
402 // (h,r) pairs with a line: r = rAverage = hrSlope * (h = hAverage);
403
404 MR::Vector2<T> avgPoint;
405 T a, b; // line y=a*x+b
406 findBestFitLine_( hrPairs, a, b, &avgPoint );
407 T hAverage = avgPoint.x;
408 T rAverage = avgPoint.y;
409 T hrSlope = a;
410
411 // If U is directed so that r increases as h increases, U is the correct
412 // cone axis estimate. However, if r decreases as h increases, -U is the
413 // correct cone axis estimate.
414 if ( hrSlope < 0 )
415 {
416 U = -U;
417 hrSlope = -hrSlope;
418 std::swap( hMin, hMax );
419 hMin = -hMin;
420 hMax = -hMax;
421 }
422
423 // Compute the extreme radial distance values for the points.
424 T rMin = rAverage + hrSlope * ( hMin - hAverage );
425 T rMax = rAverage + hrSlope * ( hMax - hAverage );
426 T hRange = hMax - hMin;
427 T rRange = rMax - rMin;
428
429 // Using trigonometry and right triangles, compute the tangent function of the cone angle.
430 T tanAngle = rRange / hRange;
431 coneAngle = std::atan2( rRange, hRange );
432
433 // Compute the cone vertex.
434 T offset = rMax / tanAngle - hMax;
435 coneApex = center - offset * U;
436 return result;
437 }
438
439 // Function for finding the best approximation of a straight line in general form y = a*x + b
440 void findBestFitLine_( const std::vector<MR::Vector2<T>>& xyPairs, T& lineA, T& lineB, MR::Vector2<T>* avg = nullptr )
441 {
442 auto numPoints = xyPairs.size();
443 Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic> A( numPoints, 2 );
444 Eigen::Vector<T, Eigen::Dynamic> b( numPoints );
445
446 for ( auto i = 0; i < numPoints; ++i )
447 {
448 A( i, 0 ) = xyPairs[i].x; // x-coordinate of the i-th point
449 A( i, 1 ) = 1.0; // constant 1.0 for dummy term b
450 b( i ) = xyPairs[i].y; // y-coordinate of the i-th point
451 if ( avg )
452 *avg = *avg + xyPairs[i];
453 }
454 if ( avg )
455 {
456 *avg = *avg / static_cast < T > ( xyPairs.size() );
457 }
458 // Solve the system of equations Ax = b using the least squares method
459 Eigen::Matrix<T, Eigen::Dynamic, 1> x = A.bdcSvd( Eigen::ComputeThinU | Eigen::ComputeThinV ).solve( b );
460
461 lineA = x( 0 );
462 lineB = x( 1 );
463
464 if ( avg )
465 {
466 *avg = *avg / static_cast < T > ( xyPairs.size() );
467 avg->y = lineA * avg->x + lineB;
468 }
469 }
470
471 // Convert data from Eigen minimizator representation to cone params.
472 void fitParamsToCone_( Eigen::Vector<T, Eigen::Dynamic>& fittedParams, Cone3<T>& cone )
473 {
474 cone.apex().x = fittedParams[0];
475 cone.apex().y = fittedParams[1];
476 cone.apex().z = fittedParams[2];
477
478 cone.direction().x = fittedParams[3];
479 cone.direction().y = fittedParams[4];
480 cone.direction().z = fittedParams[5];
481 }
482
483 // Convert data from cone params to Eigen minimizator representation.
484 void coneToFitParams_( Cone3<T>& cone, Eigen::Vector<T, Eigen::Dynamic>& fittedParams )
485 {
486 // The fittedParams guess for the cone vertex.
487 fittedParams[0] = cone.apex().x;
488 fittedParams[1] = cone.apex().y;
489 fittedParams[2] = cone.apex().z;
490
491 // The initial guess for the weighted cone axis.
492 T coneCosAngle = std::cos( cone.angle );
493 fittedParams[3] = cone.direction().x / coneCosAngle;
494 fittedParams[4] = cone.direction().y / coneCosAngle;
495 fittedParams[5] = cone.direction().z / coneCosAngle;
496 }
497
498
499};
500
501}
length
Definition MRObjectDimensionsEnum.h:14
Definition MRConeApproximator.h:157
T solve(const std::vector< MR::Vector3< T > > &points, Cone3< T > &cone, const Cone3ApproximationParams &params={})
Definition MRConeApproximator.h:163
Cone3Approximation()=default
Definition MRCone3.h:11
Eigen::Matrix< T, 2, 1 > toEigen(const Vector2< T > &v)
Definition MRToFromEigen.h:28
constexpr auto lengthSq(A a)
Definition MRImGuiVectorOperators.h:131
Definition MRCameraOrientationPlugin.h:8
ConeFitterType
Definition MRConeApproximator.h:138
Vector3< float > Vector3f
Definition MRDotNet/MRMeshFwd.h:8
Cone3
Definition MRMesh/MRMeshFwd.h:264
MRMESH_CLASS Vector3
Definition MRMesh/MRMeshFwd.h:136
Definition MRConeApproximator.h:144
int hemisphereSearchThetaResolution
Definition MRConeApproximator.h:148
int hemisphereSearchPhiResolution
Definition MRConeApproximator.h:147
int levenbergMarquardtMaxIteration
Definition MRConeApproximator.h:145
ConeFitterType coneFitterType
Definition MRConeApproximator.h:146
Definition MRConeApproximator.h:48
int values() const
Definition MRConeApproximator.h:69
Eigen::Matrix< T, Eigen::Dynamic, 1 > InputType
Definition MRConeApproximator.h:50
int inputs() const
Definition MRConeApproximator.h:65
int operator()(const InputType &x, ValueType &F) const
Definition MRConeApproximator.h:77
void setPoints(const std::vector< MR::Vector3< T > > &pointsMR)
Definition MRConeApproximator.h:56
std::vector< Eigen::Vector3< T > > points
Definition MRConeApproximator.h:54
int df(const InputType &x, JacobianType &J) const
Definition MRConeApproximator.h:100
Eigen::Matrix< T, Eigen::Dynamic, Eigen::Dynamic > JacobianType
Definition MRConeApproximator.h:52
Eigen::Matrix< T, Eigen::Dynamic, 1 > ValueType
Definition MRConeApproximator.h:51
T Scalar
Definition MRConeApproximator.h:49
Definition MRVector2.h:18
T x
Definition MRVector2.h:24
T y
Definition MRVector2.h:24
Definition MRMesh/MRVector3.h:19
auto length() const
Definition MRMesh/MRVector3.h:47
Vector3 normalized() const MR_REQUIRES_IF_SUPPORTED(std
Definition MRMesh/MRVector3.h:55
T angle(const Vector3< T > &a, const Vector3< T > &b)
Definition MRMesh/MRVector3.h:167