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