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