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