MeshLib
 
Loading...
Searching...
No Matches
MRCylinderApproximator.h
Go to the documentation of this file.
1#include "MRMeshFwd.h"
2#include "MRCylinder3.h"
3#include "MRVector.h"
4#include "MRMatrix.h"
5#include "MRPch/MRTBB.h"
6#include "MRToFromEigen.h"
7#include "MRConstants.h"
8#include "MRPch/MRSpdlog.h"
9
10
11#ifdef _MSC_VER
12#pragma warning(push)
13#pragma warning(disable:5054) //operator '&': deprecated between enumerations of different types
14#pragma warning(disable:4127) //C4127. "Consider using 'if constexpr' statement instead"
15#elif defined(__clang__)
16#elif defined(__GNUC__)
17#pragma GCC diagnostic push
18#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
19#endif
20
21#include <Eigen/Eigenvalues>
22
23#ifdef _MSC_VER
24#pragma warning(pop)
25#elif defined(__clang__)
26#elif defined(__GNUC__)
27#pragma GCC diagnostic pop
28#endif
29
30
31// https://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf
32
33namespace MR
34{
35template <typename T>
37{
38
39private:
40
41 enum class CylinderFitterType
42 {
43 // The algorithm implimentation needs an initial approximation to refine the cylinder axis.
44 // In this option, we sort through several possible options distributed over the hemisphere.
46
47 // In this case, we assume that there is an external estimate for the cylinder axis.
48 // Therefore, we specify only the position that is given from the outside
49 SpecificAxisFit
50
51 // TODO for Meshes try to impliment specific algorithm from https://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf
52 // TODO Also, an estimate of the cylinder axis can be obtained by the gravel component method or the like. But this requires additional. experiments.
53 // TODO for future try eigen vector covariance https://www.geometrictools.com/Documentation/RobustEigenSymmetric3x3.pdf
54 };
55
56 CylinderFitterType fitter_ = CylinderFitterType::HemisphereSearchFit;
57
58 // CylinderFitterType::SpecificAxisFit params
59 Eigen::Vector<T, 3> baseCylinderAxis_;
60
61 // CylinderFitterType::HemisphereSearchFit params
62 size_t thetaResolution_ = 0;
63 size_t phiResolution_ = 0;
64 bool isMultithread_ = true;
65
66 //Input data converted to Eigen format and normalized to the avgPoint position of all points in the cloud.
67 std::vector<Eigen::Vector<T, 3>> normalizedPoints_ = {};
68
69 // Precalculated values for speed up.
70 // In https://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf page 35-36:
71 // Text below is a direct copy from pdf file:
72 // The sample application that used equation (94) directly was really slow.
73 // On an Intel CoreTM i7-6700 CPU at 3.40 GHz, the single - threaded version for 10765 points required 129 seconds
74 // and the multithreaded version using 8 hyperthreads required 22 seconds.The evaluation of G using the precomputed summations is much faster.
75 // The single - threaded version required 85 milliseconds and the multithreaded version using 8 hyperthreads required 22 milliseconds.
76
77 Eigen::Vector <T, 6> precomputedMu_ = {};
78 Eigen::Matrix <T, 3, 3> precomputedF0_ = {};
79 Eigen::Matrix <T, 3, 6> precomputedF1_ = {};
80 Eigen::Matrix <T, 6, 6> precomputedF2_ = {};
81
82public:
84 {
85 reset();
86 };
87
88 void reset()
89 {
90 thetaResolution_ = 0;
91 phiResolution_ = 0;
92 precomputedMu_.setZero();
93 precomputedF0_.setZero();
94 precomputedF1_.setZero();
95 precomputedF2_.setZero();
96 normalizedPoints_.clear();
97 };
98 // Solver for CylinderFitterType::HemisphereSearchFit type
99 // thetaResolution_, phiResolution_ must be positive and as large as it posible. Price is CPU time. (~ 100 gives good results).
100 T solveGeneral( const std::vector<MR::Vector3<T>>& points, Cylinder3<T>& cylinder, size_t theta = 180, size_t phi = 90, bool isMultithread = true )
101 {
102 thetaResolution_ = theta;
103 phiResolution_ = phi;
104 isMultithread_ = isMultithread;
105 fitter_ = CylinderFitterType::HemisphereSearchFit;
106 assert( thetaResolution_ > 0 );
107 assert( phiResolution_ > 0 );
108 auto result = solve( points, cylinder );
109 reset();
110 return result;
111 };
112
113 // Solver for CylinderFitterType::SpecificAxisFit type
114 // Simplet way in case of we already know clinder axis
115 T solveSpecificAxis( const std::vector<MR::Vector3<T>>& points, Cylinder3<T>& cylinder, MR::Vector3<T> const& cylinderAxis )
116 {
117
118 baseCylinderAxis_ = MR::toEigen( cylinderAxis.normalized() );
119 fitter_ = CylinderFitterType::SpecificAxisFit;
120 assert( baseCylinderAxis_.isZero() == false ); // "The cylinder axis must be nonzero."
121 auto result = solve( points, cylinder );
122 reset();
123 return result;
124 };
125private:
126 // main solver.
127 T solve( const std::vector<MR::Vector3<T>>& points, Cylinder3<T>& cylinder )
128 {
129 if ( points.size() < 6 )
130 {
131 spdlog::warn( "Cylinder3Approximation :: Too low point for cylinder approximation count={}" , points.size() );
132 return -1;
133 }
134 assert( points.size() >= 6 ); // "At least 6 points needs for correct fitting requires ."
135
136 normalizedPoints_.clear();
137 cylinder = Cylinder3<T>();
138 Vector3<T> avgPoint;
139 Eigen::Vector<T, 3> bestPC;
140 Eigen::Vector<T, 3> bestW; // cylinder main axis
141 T rootSquare = 0;
142 T error = 0;
143
144 //For significant increase speed, we make a preliminary calculation based on the initial data.
145 updatePrecomputeParams( points, avgPoint );
146
147 // Listing 16. page 38.
148 if ( fitter_ == CylinderFitterType::HemisphereSearchFit )
149 {
150 if ( isMultithread_ )
151 error = fitCylindeHemisphereMultiThreaded( bestPC, bestW, rootSquare );
152
153 else
154 error = fitCylindeHemisphereSingleThreaded( bestPC, bestW, rootSquare );
155 }
156 else if ( fitter_ == CylinderFitterType::SpecificAxisFit )
157 {
158 error = SpecificAxisFit( bestPC, bestW, rootSquare );
159 }
160 else
161 {
162 spdlog::warn( "Cylinder3Approximation :: unsupported fitter" );
163 assert( false ); // unsupported fitter
164 return -1;
165 }
166
167 assert( rootSquare >= 0 );
168
169 cylinder.center() = fromEigen( bestPC ) + avgPoint;
170 cylinder.direction() = ( fromEigen( bestW ) ).normalized();
171 cylinder.radius = std::sqrt( rootSquare );
172
173 // Calculate a max. possible length of a cylinder covered by dataset.
174 // Project point on a main cylinder axis
175 T hmin = std::numeric_limits<T>::max();
176 T hmax = -std::numeric_limits<T>::max();
177
178 for ( size_t i = 0; i < points.size(); ++i )
179 {
180 T h = MR::dot( cylinder.direction(), points[i] - cylinder.center() );
181 hmin = std::min( h, hmin );
182 hmax = std::max( h, hmax );
183 }
184 T hmid = ( hmin + hmax ) / 2;
185
186 // Very tiny correct a cylinder center.
187 cylinder.center() = cylinder.center() + hmid * cylinder.direction();
188 cylinder.length = hmax - hmin;
189
190 assert( cylinder.length >= 0 );
191
192 return error;
193 }
194
195 void updatePrecomputeParams( const std::vector <MR::Vector3<T>>& points, Vector3<T>& average )
196 {
197 // Listing 15. page 37.
198 normalizedPoints_.resize( points.size() );
199
200 // calculate avg point of dataset
201 average = Vector3<T>{};
202 for ( size_t i = 0; i < points.size(); ++i )
203 average += points[i];
204 average = average / static_cast< T > ( points.size() );
205
206 // normalize points and store it.
207 for ( size_t i = 0; i < points.size(); ++i )
208 normalizedPoints_[i] = toEigen( points[i] - average );
209
210 const Eigen::Vector<T, 6> zeroEigenVector6{ 0, 0, 0, 0, 0, 0 };
211 std::vector<Eigen::Vector<T, 6>> products( normalizedPoints_.size(), zeroEigenVector6 );
212 precomputedMu_ = zeroEigenVector6;
213
214 for ( size_t i = 0; i < normalizedPoints_.size(); ++i )
215 {
216 products[i][0] = normalizedPoints_[i][0] * normalizedPoints_[i][0];
217 products[i][1] = normalizedPoints_[i][0] * normalizedPoints_[i][1];
218 products[i][2] = normalizedPoints_[i][0] * normalizedPoints_[i][2];
219 products[i][3] = normalizedPoints_[i][1] * normalizedPoints_[i][1];
220 products[i][4] = normalizedPoints_[i][1] * normalizedPoints_[i][2];
221 products[i][5] = normalizedPoints_[i][2] * normalizedPoints_[i][2];
222 precomputedMu_[0] += products[i][0];
223 precomputedMu_[1] += 2 * products[i][1];
224 precomputedMu_[2] += 2 * products[i][2];
225 precomputedMu_[3] += products[i][3];
226 precomputedMu_[4] += 2 * products[i][4];
227 precomputedMu_[5] += products[i][5];
228 }
229 precomputedMu_ = precomputedMu_ / points.size();
230
231 precomputedF0_.setZero();
232 precomputedF1_.setZero();
233 precomputedF2_.setZero();
234 for ( size_t i = 0; i < normalizedPoints_.size(); ++i )
235 {
236 Eigen::Vector<T, 6> delta{};
237 delta[0] = products[i][0] - precomputedMu_[0];
238 delta[1] = 2 * products[i][1] - precomputedMu_[1];
239 delta[2] = 2 * products[i][2] - precomputedMu_[2];
240 delta[3] = products[i][3] - precomputedMu_[3];
241 delta[4] = 2 * products[i][4] - precomputedMu_[4];
242 delta[5] = products[i][5] - precomputedMu_[5];
243 precomputedF0_( 0, 0 ) += products[i][0];
244 precomputedF0_( 0, 1 ) += products[i][1];
245 precomputedF0_( 0, 2 ) += products[i][2];
246 precomputedF0_( 1, 1 ) += products[i][3];
247 precomputedF0_( 1, 2 ) += products[i][4];
248 precomputedF0_( 2, 2 ) += products[i][5];
249 precomputedF1_ = precomputedF1_ + normalizedPoints_[i] * delta.transpose();
250 precomputedF2_ += delta * delta.transpose();
251 }
252 precomputedF0_ = precomputedF0_ / static_cast < T > ( points.size() );
253 precomputedF0_( 1, 0 ) = precomputedF0_( 0, 1 );
254 precomputedF0_( 2, 0 ) = precomputedF0_( 0, 2 );
255 precomputedF0_( 2, 1 ) = precomputedF0_( 1, 2 );
256 precomputedF1_ = precomputedF1_ / static_cast < T > ( points.size() );
257 precomputedF2_ = precomputedF2_ / static_cast < T > ( points.size() );
258 }
259
260 // Core minimization function.
261 // Functional that needs to be minimized to obtain the optimal value of W (i.e. the cylinder axis)
262 // General definition is formula 94, but for speed up we use formula 99.
263 // https://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf
264 T G( const Eigen::Vector<T, 3>& W, Eigen::Vector<T, 3>& PC, T& rsqr ) const
265 {
266 Eigen::Matrix<T, 3, 3> P = Eigen::Matrix<T, 3, 3>::Identity() - ( W * W.transpose() ); // P = I - W * W^T
267
268 Eigen::Matrix<T, 3, 3> S;
269 S << 0, -W[2], W[1],
270 W[2], 0, -W[0],
271 -W[1], W[0], 0;
272
273 Eigen::Matrix<T, 3, 3> A = P * precomputedF0_ * P;
274 Eigen::Matrix<T, 3, 3> hatA = -( S * A * S );
275 Eigen::Matrix<T, 3, 3> hatAA = hatA * A;
276 T trace = hatAA.trace();
277 Eigen::Matrix<T, 3, 3> Q = hatA / trace;
278 Eigen::Vector<T, 6> pVec{ P( 0, 0 ), P( 0, 1 ), P( 0, 2 ), P( 1, 1 ), P( 1, 2 ), P( 2, 2 ) };
279 Eigen::Vector<T, 3> alpha = precomputedF1_ * pVec;
280 Eigen::Vector<T, 3> beta = Q * alpha;
281 T error = ( pVec.dot( precomputedF2_ * pVec ) - 4 * alpha.dot( beta ) + 4 * beta.dot( precomputedF0_ * beta ) ) / static_cast< T >( normalizedPoints_.size() );
282
283 // some times appears floating points calculation errors. Error is a non negative value by default, so, make it positive.
284 // absolute value (instead of error=0) is used to avoid collisions for near-null values and subsequent ambiguous work, since this code can be used in parallel algorithms
285 if ( error < 0 )
286 error = std::abs( error );
287
288 PC = beta;
289 rsqr = pVec.dot( precomputedMu_ ) + beta.dot( beta );
290
291 return error;
292 }
293
294 T fitCylindeHemisphereSingleThreaded( Eigen::Vector<T, 3>& PC, Eigen::Vector<T, 3>& W, T& resultedRootSquare ) const
295 {
296 T const theraStep = static_cast< T >( 2 * PI ) / thetaResolution_;
297 T const phiStep = static_cast< T >( PI2 ) / phiResolution_;
298
299 // problem in list. 16. => start from pole.
300 W = { 0, 0, 1 };
301 T minError = G( W, PC, resultedRootSquare );
302
303 for ( size_t j = 1; j <= phiResolution_; ++j )
304 {
305 T phi = phiStep * j; // [0 .. pi/2]
306 T cosPhi = std::cos( phi );
307 T sinPhi = std::sin( phi );
308 for ( size_t i = 0; i < thetaResolution_; ++i )
309 {
310 T theta = theraStep * i; // [0 .. 2*pi)
311 T cosTheta = std::cos( theta );
312 T sinTheta = std::sin( theta );
313 Eigen::Vector<T, 3> currW{ cosTheta * sinPhi, sinTheta * sinPhi, cosPhi };
314 Eigen::Vector<T, 3> currPC{};
315 T rsqr;
316 T error = G( currW, currPC, rsqr );
317 if ( error < minError )
318 {
319 minError = error;
320 resultedRootSquare = rsqr;
321 W = currW;
322 PC = currPC;
323 }
324 }
325 }
326
327 return minError;
328 }
329
330 class BestHemisphereStoredData
331 {
332 public:
333 T error = std::numeric_limits<T>::max();
334 T rootSquare = std::numeric_limits<T>::max();
335 Eigen::Vector<T, 3> W;
336 Eigen::Vector<T, 3> PC;
337 };
338
339 T fitCylindeHemisphereMultiThreaded( Eigen::Vector<T, 3>& PC, Eigen::Vector<T, 3>& W, T& resultedRootSquare ) const
340 {
341 T const theraStep = static_cast< T >( 2 * PI ) / thetaResolution_;
342 T const phiStep = static_cast< T >( PI2 ) / phiResolution_;
343
344 // problem in list. 16. => start from pole.
345 W = { 0, 0, 1 };
346 T minError = G( W, PC, resultedRootSquare );
347
348 std::vector<BestHemisphereStoredData> storedData;
349 storedData.resize( phiResolution_ + 1 ); // [0 .. pi/2] +1 for include upper bound
350
351 tbb::parallel_for( tbb::blocked_range<size_t>( size_t( 0 ), phiResolution_ + 1 ),
352 [&] ( const tbb::blocked_range<size_t>& range )
353 {
354 for ( size_t j = range.begin(); j < range.end(); ++j )
355 {
356
357 T phi = phiStep * j; // [0 .. pi/2]
358 T cosPhi = std::cos( phi );
359 T sinPhi = std::sin( phi );
360 for ( size_t i = 0; i < thetaResolution_; ++i )
361 {
362
363 T theta = theraStep * i; // [0 .. 2*pi)
364 T cosTheta = std::cos( theta );
365 T sinTheta = std::sin( theta );
366 Eigen::Vector<T, 3> currW{ cosTheta * sinPhi, sinTheta * sinPhi, cosPhi };
367 Eigen::Vector<T, 3> currPC{};
368 T rsqr;
369 T error = G( currW, currPC, rsqr );
370
371 if ( error < storedData[j].error )
372 {
373 storedData[j].error = error;
374 storedData[j].rootSquare = rsqr;
375 storedData[j].W = currW;
376 storedData[j].PC = currPC;
377 }
378 }
379 }
380 }
381 );
382
383 for ( size_t i = 0; i <= phiResolution_; ++i )
384 {
385 if ( storedData[i].error < minError )
386 {
387 minError = storedData[i].error;
388 resultedRootSquare = storedData[i].rootSquare;
389 W = storedData[i].W;
390 PC = storedData[i].PC;
391 }
392 }
393
394 return minError;
395 };
396
397 T SpecificAxisFit( Eigen::Vector<T, 3>& PC, Eigen::Vector<T, 3>& W, T& resultedRootSquare )
398 {
399 W = baseCylinderAxis_;
400 return G( W, PC, resultedRootSquare );
401 };
402};
403
404}
405
406
407
408
Definition MRCylinderApproximator.h:37
T solveSpecificAxis(const std::vector< MR::Vector3< T > > &points, Cylinder3< T > &cylinder, MR::Vector3< T > const &cylinderAxis)
Definition MRCylinderApproximator.h:115
Cylinder3Approximation()
Definition MRCylinderApproximator.h:83
void reset()
Definition MRCylinderApproximator.h:88
T solveGeneral(const std::vector< MR::Vector3< T > > &points, Cylinder3< T > &cylinder, size_t theta=180, size_t phi=90, bool isMultithread=true)
Definition MRCylinderApproximator.h:100
Definition MRCylinder3.h:12
T radius
Definition MRCylinder3.h:51
T length
Definition MRCylinder3.h:52
Vector3< T > & direction(void)
Definition MRCylinder3.h:40
Vector3< T > & center(void)
Definition MRCylinder3.h:30
Vector2< T > fromEigen(const Eigen::Matrix< T, 2, 1 > &ev)
Definition MRToFromEigen.h:22
Eigen::Matrix< T, 2, 1 > toEigen(const Vector2< T > &v)
Definition MRToFromEigen.h:28
Definition MRCameraOrientationPlugin.h:7
Cylinder3
Definition MRMesh/MRMeshFwd.h:259
MRMESH_CLASS Vector3
Definition MRMesh/MRMeshFwd.h:136
Definition MRMesh/MRVector3.h:19
Vector3 normalized() const MR_REQUIRES_IF_SUPPORTED(std
Definition MRMesh/MRVector3.h:55