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