MeshLib
 
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 Eigen::Matrix<T, 3, 3> Q = hatA / trace;
280 Eigen::Vector<T, 6> pVec{ P( 0, 0 ), P( 0, 1 ), P( 0, 2 ), P( 1, 1 ), P( 1, 2 ), P( 2, 2 ) };
281 Eigen::Vector<T, 3> alpha = precomputedF1_ * pVec;
282 Eigen::Vector<T, 3> beta = Q * alpha;
283 T error = ( pVec.dot( precomputedF2_ * pVec ) - 4 * alpha.dot( beta ) + 4 * beta.dot( precomputedF0_ * beta ) ) / static_cast< T >( normalizedPoints_.size() );
284
285 // some times appears floating points calculation errors. Error is a non negative value by default, so, make it positive.
286 // 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
287 if ( error < 0 )
288 error = std::abs( error );
289
290 PC = beta;
291 rsqr = pVec.dot( precomputedMu_ ) + beta.dot( beta );
292
293 return error;
294 }
295
296 T fitCylindeHemisphereSingleThreaded( Eigen::Vector<T, 3>& PC, Eigen::Vector<T, 3>& W, T& resultedRootSquare ) const
297 {
298 T const theraStep = static_cast< T >( 2 * PI ) / thetaResolution_;
299 T const phiStep = static_cast< T >( PI2 ) / phiResolution_;
300
301 // problem in list. 16. => start from pole.
302 W = { 0, 0, 1 };
303 T minError = G( W, PC, resultedRootSquare );
304
305 for ( size_t j = 1; j <= phiResolution_; ++j )
306 {
307 T phi = phiStep * j; // [0 .. pi/2]
308 T cosPhi = std::cos( phi );
309 T sinPhi = std::sin( phi );
310 for ( size_t i = 0; i < thetaResolution_; ++i )
311 {
312 T theta = theraStep * i; // [0 .. 2*pi)
313 T cosTheta = std::cos( theta );
314 T sinTheta = std::sin( theta );
315 Eigen::Vector<T, 3> currW{ cosTheta * sinPhi, sinTheta * sinPhi, cosPhi };
316 Eigen::Vector<T, 3> currPC{};
317 T rsqr;
318 T error = G( currW, currPC, rsqr );
319 if ( error < minError )
320 {
321 minError = error;
322 resultedRootSquare = rsqr;
323 W = currW;
324 PC = currPC;
325 }
326 }
327 }
328
329 return minError;
330 }
331
332 class BestHemisphereStoredData
333 {
334 public:
335 T error = std::numeric_limits<T>::max();
336 T rootSquare = std::numeric_limits<T>::max();
337 Eigen::Vector<T, 3> W;
338 Eigen::Vector<T, 3> PC;
339 };
340
341 T fitCylindeHemisphereMultiThreaded( Eigen::Vector<T, 3>& PC, Eigen::Vector<T, 3>& W, T& resultedRootSquare ) const
342 {
343 T const theraStep = static_cast< T >( 2 * PI ) / thetaResolution_;
344 T const phiStep = static_cast< T >( PI2 ) / phiResolution_;
345
346 // problem in list. 16. => start from pole.
347 W = { 0, 0, 1 };
348 T minError = G( W, PC, resultedRootSquare );
349
350 std::vector<BestHemisphereStoredData> storedData;
351 storedData.resize( phiResolution_ + 1 ); // [0 .. pi/2] +1 for include upper bound
352
353 tbb::parallel_for( tbb::blocked_range<size_t>( size_t( 0 ), phiResolution_ + 1 ),
354 [&] ( const tbb::blocked_range<size_t>& range )
355 {
356 for ( size_t j = range.begin(); j < range.end(); ++j )
357 {
358
359 T phi = phiStep * j; // [0 .. pi/2]
360 T cosPhi = std::cos( phi );
361 T sinPhi = std::sin( phi );
362 for ( size_t i = 0; i < thetaResolution_; ++i )
363 {
364
365 T theta = theraStep * i; // [0 .. 2*pi)
366 T cosTheta = std::cos( theta );
367 T sinTheta = std::sin( theta );
368 Eigen::Vector<T, 3> currW{ cosTheta * sinPhi, sinTheta * sinPhi, cosPhi };
369 Eigen::Vector<T, 3> currPC{};
370 T rsqr;
371 T error = G( currW, currPC, rsqr );
372
373 if ( error < storedData[j].error )
374 {
375 storedData[j].error = error;
376 storedData[j].rootSquare = rsqr;
377 storedData[j].W = currW;
378 storedData[j].PC = currPC;
379 }
380 }
381 }
382 }
383 );
384
385 for ( size_t i = 0; i <= phiResolution_; ++i )
386 {
387 if ( storedData[i].error < minError )
388 {
389 minError = storedData[i].error;
390 resultedRootSquare = storedData[i].rootSquare;
391 W = storedData[i].W;
392 PC = storedData[i].PC;
393 }
394 }
395
396 return minError;
397 };
398
399 T SpecificAxisFit( Eigen::Vector<T, 3>& PC, Eigen::Vector<T, 3>& W, T& resultedRootSquare )
400 {
401 W = baseCylinderAxis_;
402 return G( W, PC, resultedRootSquare );
403 };
404};
405
406}
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: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