OpenVDB  12.1.1
PrincipalComponentAnalysisImpl.h
Go to the documentation of this file.
1 // Copyright Contributors to the OpenVDB Project
2 // SPDX-License-Identifier: Apache-2.0
3 
4 #ifndef OPENVDB_POINTS_POINT_PRINCIPAL_COMPONENT_ANALYSIS_IMPL_HAS_BEEN_INCLUDED
5 #define OPENVDB_POINTS_POINT_PRINCIPAL_COMPONENT_ANALYSIS_IMPL_HAS_BEEN_INCLUDED
6 
7 /// when enabled, prints timings for each substep of the PCA algorithm
8 #ifdef OPENVDB_PROFILE_PCA
10 #endif
11 
12 /// @brief Experimental option to skip storing the self weight for the
13 /// weighted PCA when set to 0
14 /// @todo Decide what's more correct and remove this. Self contributions aren't
15 /// guaranteed with maxSourcePointsPerVoxel/maxTargetPointsPerVoxel
16 #define OPENVDB_PCA_SELF_CONTRIBUTION 1
17 
18 namespace openvdb {
20 namespace OPENVDB_VERSION_NAME {
21 namespace points {
22 
23 namespace pca_internal {
24 
25 #ifdef OPENVDB_PROFILE_PCA
26 using TimerT = util::CpuTimer;
27 #else
28 struct NoTimer {
29 inline void start(const char*) {}
30 inline void stop() {}
31 };
32 using TimerT = NoTimer;
33 #endif
34 
35 struct PcaTimer final : public TimerT
36 {
37  PcaTimer(util::NullInterrupter* const interrupt)
38  : TimerT()
39  , mInterrupt(interrupt) {}
40  inline void start(const char* msg)
41  {
42  TimerT::start(msg);
43  if (mInterrupt) mInterrupt->start(msg);
44  }
45  inline void stop()
46  {
47  TimerT::stop();
48  if (mInterrupt) mInterrupt->end();
49  }
51 };
52 
53 using WeightSumT = double;
54 using WeightedPositionSumT = Vec3d;
55 using GroupIndexT = points::AttributeSet::Descriptor::GroupIndex;
56 
58 {
59  size_t mPosSumIndex;
62  size_t mPWsIndex;
64 };
65 
66 template <typename T, typename LeafNodeT>
67 inline T* initPcaArrayAttribute(LeafNodeT& leaf, const size_t idx, const bool fill = true)
68 {
69  auto& array = leaf.attributeArray(idx);
70  OPENVDB_ASSERT(array.valueType() == typeNameAsString<T>());
71  OPENVDB_ASSERT(array.codecType() == std::string(NullCodec::name()));
72  array.expand(fill); // @note does nothing if array is already not uniform
73  const char* data = array.constDataAsByteArray();
74  return reinterpret_cast<T*>(const_cast<char*>(data));
75 }
76 
77 /// @note The PCA transfer modules are broken into two separate steps, the
78 /// first which computes the weighted neighbourhoods of points and the second
79 /// which computes the covariance matrices. Both these steps perform an
80 /// identical loop over each points neighbourhood, with the second being
81 /// necessary to re-compute the exact positions and per position weights.
82 /// In theory, only a single loop is required; each point could instead
83 /// create and append a large list of the exact neighbourhood positions and
84 /// weights that impact it and use these to compute the covariance after the
85 /// first neighbourhood loop has completed (a true gather style approach).
86 ///
87 /// The double loop technique was chosen to better handle the computation
88 /// of anisotropy for _all_ points and _all_ neighbourhoods (i.e. no limiting
89 /// via the max point per voxel (MPPV) options). It makes the PCA method
90 /// extremely memory efficient for _all_ MPPV and radius values. However
91 /// it might be worth falling back to a gather stlye approach when the MPPV
92 /// and radius values are relatively small. We should investigate this in the
93 /// future.
94 template <typename PointDataTreeT>
96  : public VolumeTransfer<PointDataTreeT>
97  , public InterruptableTransfer
98 {
100  using LeafNodeType = typename PointDataTreeT::LeafNodeType;
101  // We know the codec is null as this works on the world space positions
102  // that are temporarily computed for this algorithm
104 
105  PcaTransfer(const AttrIndices& indices,
106  const PcaSettings& settings,
107  const Real vs,
109  util::NullInterrupter* interrupt)
110  : BaseT(manager.tree())
111  , InterruptableTransfer(interrupt)
112  , mIndices(indices)
113  , mSettings(settings)
114  , mDxInv(1.0/vs)
115  , mManager(manager)
116  , mTargetPosition()
117  , mSourcePosition() {
118  OPENVDB_ASSERT(std::isfinite(mSettings.searchRadius));
119  OPENVDB_ASSERT(std::isfinite(mDxInv));
120  }
121 
122  PcaTransfer(const PcaTransfer& other)
123  : BaseT(other)
124  , InterruptableTransfer(other)
125  , mIndices(other.mIndices)
126  , mSettings(other.mSettings)
127  , mDxInv(other.mDxInv)
128  , mManager(other.mManager)
129  , mTargetPosition()
130  , mSourcePosition() {}
131 
132  /*
133  // This is no longer used but kept for reference; each axis iterations of
134  // derived PCA methods uses this technique to skip voxels entirely if a
135  // point's position and search radius does not intersect it
136 
137  static bool VoxelIntersectsSphere(const Coord& ijk, const Vec3d& PosIS, const Real r2)
138  {
139  const Vec3d min = ijk.asVec3d() - 0.5;
140  const Vec3d max = ijk.asVec3d() + 0.5;
141  Real dmin = 0;
142  for (int i = 0; i < 3; ++i) {
143  if (PosIS[i] < min[i]) dmin += math::Pow2(PosIS[i] - min[i]);
144  else if (PosIS[i] > max[i]) dmin += math::Pow2(PosIS[i] - max[i]);
145  }
146  return dmin <= r2;
147  }
148  */
149 
150  float searchRadius() const { return mSettings.searchRadius; }
151  size_t neighbourThreshold() const { return mSettings.neighbourThreshold; }
152  size_t maxSourcePointsPerVoxel() const { return mSettings.maxSourcePointsPerVoxel; }
153  size_t maxTargetPointsPerVoxel() const { return mSettings.maxTargetPointsPerVoxel; }
154 
155  Vec3i range(const Coord&, size_t) const { return this->range(); }
156  Vec3i range() const { return Vec3i(math::Round(mSettings.searchRadius * mDxInv)); }
157 
158  inline LeafNodeType* initialize(const Coord& origin, const size_t idx, const CoordBBox& bounds)
159  {
160  BaseT::initialize(origin, idx, bounds);
161  auto& leaf = mManager.leaf(idx);
162  mTargetPosition = std::make_unique<PositionHandleT>(leaf.constAttributeArray(mIndices.mPWsIndex));
163  return &leaf;
164  }
165 
166  inline bool startPointLeaf(const typename PointDataTreeT::LeafNodeType& leaf)
167  {
168  mSourcePosition = std::make_unique<PositionHandleT>(leaf.constAttributeArray(mIndices.mPWsIndex));
169 #if OPENVDB_PCA_SELF_CONTRIBUTION == 0
170  mIsSameLeaf = this->mTargetPosition->array() == this->mSourcePosition->array();
171 #endif
172  return true;
173  }
174 
175  bool endPointLeaf(const typename PointDataTreeT::LeafNodeType&) { return true; }
176 
177 protected:
180  const Real mDxInv;
182  std::unique_ptr<PositionHandleT> mTargetPosition;
183  std::unique_ptr<PositionHandleT> mSourcePosition;
184 #if OPENVDB_PCA_SELF_CONTRIBUTION == 0
185  bool mIsSameLeaf {false};
186 #endif
187 };
188 
189 template <typename PointDataTreeT>
191  : public PcaTransfer<PointDataTreeT>
192 {
194 
195  static const Index DIM = PointDataTreeT::LeafNodeType::DIM;
196  static const Index LOG2DIM = PointDataTreeT::LeafNodeType::LOG2DIM;
197 
199  const PcaSettings& settings,
200  const Real vs,
202  util::NullInterrupter* interrupt)
203  : BaseT(indices, settings, vs, manager, interrupt)
204  , mWeights()
205  , mWeightedPositions()
206  , mCounts() {}
207 
209  : BaseT(other)
210  , mWeights()
211  , mWeightedPositions()
212  , mCounts() {}
213 
214  inline void initialize(const Coord& origin, const size_t idx, const CoordBBox& bounds)
215  {
216  auto& leaf = (*BaseT::initialize(origin, idx, bounds));
217  mWeights = initPcaArrayAttribute<WeightSumT>(leaf, this->mIndices.mWeightSumIndex);
218  mWeightedPositions = initPcaArrayAttribute<WeightedPositionSumT>(leaf, this->mIndices.mPosSumIndex);
219  // track neighbours
220  mCounts.assign(this->mTargetPosition->size(), 0);
221  }
222 
223  inline void rasterizePoints(const Coord&, const Index start, const Index end, const CoordBBox& bounds)
224  {
225  const Index step = std::max(Index(1), Index((end - start) / this->maxSourcePointsPerVoxel()));
226 
227  const auto* const data = this->template buffer<0>();
228  const auto& mask = *(this->template mask<0>());
229 
230  const float searchRadiusInv = 1.0f / this->searchRadius();
231  const Real searchRadius2 = math::Pow2(this->searchRadius());
232  const Real searchRadiusIS2 = math::Pow2(this->searchRadius() * this->mDxInv);
233 
234  OPENVDB_ASSERT(std::isfinite(searchRadiusInv));
235  OPENVDB_ASSERT(std::isfinite(searchRadius2));
236  OPENVDB_ASSERT(std::isfinite(searchRadiusIS2));
237 
238  const Coord& a(bounds.min());
239  const Coord& b(bounds.max());
240 
241  for (Index srcid = start; srcid < end; srcid += step) {
242  const Vec3d Psrc(this->mSourcePosition->get(srcid));
243  const Vec3d PsrcIS = Psrc * this->mDxInv;
244 
245  for (Coord c = a; c.x() <= b.x(); ++c.x()) {
246  const Real minx = c.x() - 0.5;
247  const Real maxx = c.x() + 0.5;
248  const Real dminx =
249  (PsrcIS[0] < minx ? math::Pow2(PsrcIS[0] - minx) :
250  (PsrcIS[0] > maxx ? math::Pow2(PsrcIS[0] - maxx) : 0));
251  if (dminx > searchRadiusIS2) continue; // next target voxel
252  const Index i = ((c.x() & (DIM-1u)) << 2*LOG2DIM); // unsigned bit shift mult
253  for (c.y() = a.y(); c.y() <= b.y(); ++c.y()) {
254  const Real miny = c.y() - 0.5;
255  const Real maxy = c.y() + 0.5;
256  const Real dminxy = dminx +
257  (PsrcIS[1] < miny ? math::Pow2(PsrcIS[1] - miny) :
258  (PsrcIS[1] > maxy ? math::Pow2(PsrcIS[1] - maxy) : 0));
259  if (dminxy > searchRadiusIS2) continue; // next target voxel
260  const Index ij = i + ((c.y() & (DIM-1u)) << LOG2DIM);
261  for (c.z() = a.z(); c.z() <= b.z(); ++c.z()) {
262  const Index offset = ij + /*k*/(c.z() & (DIM-1u));
263  if (!mask.isOn(offset)) continue; // next target voxel
264 
265  const Real minz = c.z() - 0.5;
266  const Real maxz = c.z() + 0.5;
267  const Real dminxyz = dminxy +
268  (PsrcIS[2] < minz ? math::Pow2(PsrcIS[2] - minz) :
269  (PsrcIS[2] > maxz ? math::Pow2(PsrcIS[2] - maxz) : 0));
270  // Does this point's radius overlap the voxel c
271  if (dminxyz > searchRadiusIS2) continue; // next target voxel
272 
273  // src point overlaps voxel c
274  const Index targetEnd = data[offset];
275  const Index targetStart = (offset == 0) ? 0 : Index(data[offset - 1]);
276  const Index targetStep =
277  std::max(Index(1), Index((targetEnd - targetStart) / this->maxTargetPointsPerVoxel()));
278 
279  /// @warning stepping in this way does not guarantee
280  /// we get a self contribution, could guarantee this
281  /// by enabling the OPENVDB_PCA_SELF_CONTRIBUTION == 0
282  /// check and adding it afterwards.
283  for (Index tgtid = targetStart; tgtid < targetEnd; tgtid += targetStep) {
284 #if OPENVDB_PCA_SELF_CONTRIBUTION == 0
285  if (OPENVDB_UNLIKELY(this->mIsSameLeaf && tgtid == srcid)) continue;
286 #endif
287  const Vec3d Ptgt(this->mTargetPosition->get(tgtid));
288  const Real d2 = (Psrc - Ptgt).lengthSqr();
289  if (d2 > searchRadius2) continue;
290 
291  // src point (srcid) reaches target point (tgtid)
292  const float weight = 1.0f - math::Pow3(float(math::Sqrt(d2)) * searchRadiusInv);
293  OPENVDB_ASSERT(weight >= 0.0f && weight <= 1.0f);
294 
295  mWeights[tgtid] += weight;
296  mWeightedPositions[tgtid] += Psrc * weight; // @note: world space position is weighted
297  ++mCounts[tgtid];
298  }
299  }
300  }
301  } // outer sdf voxel
302  } // point idx
303  }
304 
305  bool finalize(const Coord&, size_t idx)
306  {
307  // Add points to group with counts which are more than the neighbouring threshold
308  auto& leaf = this->mManager.leaf(idx);
309 
310  {
311  // @todo add API to get the array from the group handle. The handle
312  // calls loadData but not expand.
313  auto& array = leaf.attributeArray(this->mIndices.mEllipsesGroupIndex.first);
314  array.loadData(); // so we can call setUnsafe/getUnsafe
315  array.expand();
316  }
317 
318  points::GroupWriteHandle group(leaf.groupWriteHandle(this->mIndices.mEllipsesGroupIndex));
319 
320  const int32_t threshold = int32_t(this->neighbourThreshold());
321  for (Index i = 0; i < this->mTargetPosition->size(); ++i) {
322  // turn points OFF if they are ON and don't meet max neighbour requirements
323  if ((threshold == 0 || (mCounts[i] < threshold)) && group.getUnsafe(i)) {
324  group.setUnsafe(i, false);
325  }
326  if (mCounts[i] <= 0) continue;
327  // Normalize weights
328  OPENVDB_ASSERT(mWeights[i] >= 0.0f);
329  mWeights[i] = 1.0 / mWeights[i];
330  mWeightedPositions[i] *= mWeights[i];
331  }
332  return true;
333  }
334 
335 private:
336  WeightSumT* mWeights;
337  WeightedPositionSumT* mWeightedPositions;
338  std::vector<int32_t> mCounts;
339 };
340 
341 template <typename PointDataTreeT>
344 {
346 
347  static const Index DIM = PointDataTreeT::LeafNodeType::DIM;
348  static const Index LOG2DIM = PointDataTreeT::LeafNodeType::LOG2DIM;
349 
351  const PcaSettings& settings,
352  const Real vs,
354  util::NullInterrupter* interrupt)
355  : BaseT(indices, settings, vs, manager, interrupt)
356  , mInclusionGroupHandle()
357  , mWeights()
358  , mWeightedPositions()
359  , mCovMats() {}
360 
362  : BaseT(other)
363  , mInclusionGroupHandle()
364  , mWeights()
365  , mWeightedPositions()
366  , mCovMats() {}
367 
368  inline void initialize(const Coord& origin, const size_t idx, const CoordBBox& bounds)
369  {
370  auto& leaf = (*BaseT::initialize(origin, idx, bounds));
371  mInclusionGroupHandle = std::make_unique<points::GroupHandle>(leaf.groupHandle(this->mIndices.mEllipsesGroupIndex));
372  mWeights = initPcaArrayAttribute<WeightSumT>(leaf, this->mIndices.mWeightSumIndex);
373  mWeightedPositions = initPcaArrayAttribute<WeightedPositionSumT>(leaf, this->mIndices.mPosSumIndex);
374  mCovMats = initPcaArrayAttribute<math::Mat3s>(leaf, this->mIndices.mCovMatrixIndex);
375  }
376 
377  inline void rasterizePoints(const Coord&, const Index start, const Index end, const CoordBBox& bounds)
378  {
379  const Index step = std::max(Index(1), Index((end - start) / this->maxSourcePointsPerVoxel()));
380 
381  const auto* const data = this->template buffer<0>();
382  const auto& mask = *(this->template mask<0>());
383 
384  const float searchRadiusInv = 1.0f/this->searchRadius();
385  const Real searchRadius2 = math::Pow2(this->searchRadius());
386  const Real searchRadiusIS2 = math::Pow2(this->searchRadius() * this->mDxInv);
387 
388  OPENVDB_ASSERT(std::isfinite(searchRadiusInv));
389  OPENVDB_ASSERT(std::isfinite(searchRadius2));
390  OPENVDB_ASSERT(std::isfinite(searchRadiusIS2));
391 
392  const Coord& a(bounds.min());
393  const Coord& b(bounds.max());
394 
395  for (Index srcid = start; srcid < end; srcid += step) {
396  const Vec3d Psrc(this->mSourcePosition->get(srcid));
397  const Vec3d PsrcIS = Psrc * this->mDxInv;
398 
399  for (Coord c = a; c.x() <= b.x(); ++c.x()) {
400  const Real minx = c.x() - 0.5;
401  const Real maxx = c.x() + 0.5;
402  const Real dminx =
403  (PsrcIS[0] < minx ? math::Pow2(PsrcIS[0] - minx) :
404  (PsrcIS[0] > maxx ? math::Pow2(PsrcIS[0] - maxx) : 0));
405  if (dminx > searchRadiusIS2) continue;
406  const Index i = ((c.x() & (DIM-1u)) << 2*LOG2DIM); // unsigned bit shift mult
407  for (c.y() = a.y(); c.y() <= b.y(); ++c.y()) {
408  const Real miny = c.y() - 0.5;
409  const Real maxy = c.y() + 0.5;
410  const Real dminxy = dminx +
411  (PsrcIS[1] < miny ? math::Pow2(PsrcIS[1] - miny) :
412  (PsrcIS[1] > maxy ? math::Pow2(PsrcIS[1] - maxy) : 0));
413  if (dminxy > searchRadiusIS2) continue;
414  const Index ij = i + ((c.y() & (DIM-1u)) << LOG2DIM);
415  for (c.z() = a.z(); c.z() <= b.z(); ++c.z()) {
416  const Index offset = ij + /*k*/(c.z() & (DIM-1u));
417  if (!mask.isOn(offset)) continue;
418 
419  const Real minz = c.z() - 0.5;
420  const Real maxz = c.z() + 0.5;
421  const Real dminxyz = dminxy +
422  (PsrcIS[2] < minz ? math::Pow2(PsrcIS[2] - minz) :
423  (PsrcIS[2] > maxz ? math::Pow2(PsrcIS[2] - maxz) : 0));
424  // Does this point's radius overlap the voxel c
425  if (dminxyz > searchRadiusIS2) continue;
426 
427  const Index targetEnd = data[offset];
428  const Index targetStart = (offset == 0) ? 0 : Index(data[offset - 1]);
429  const Index targetStep =
430  std::max(Index(1), Index((targetEnd - targetStart) / this->maxTargetPointsPerVoxel()));
431 
432  for (Index tgtid = targetStart; tgtid < targetEnd; tgtid += targetStep) {
433  if (!mInclusionGroupHandle->get(tgtid)) continue;
434 #if OPENVDB_PCA_SELF_CONTRIBUTION == 0
435  if (OPENVDB_UNLIKELY(this->mIsSameLeaf && tgtid == srcid)) continue;
436 #endif
437  const Vec3d Ptgt(this->mTargetPosition->get(tgtid));
438  const Real d2 = (Psrc - Ptgt).lengthSqr();
439  if (d2 > searchRadius2) continue;
440 
441  // @note I've observed some performance degradation if
442  // we don't take copies of the buffers here (aliasing?)
443  const WeightSumT totalWeightInv = mWeights[tgtid];
444  const WeightedPositionSumT currWeightSum = mWeightedPositions[tgtid];
445 
446  // re-compute weight
447  // @note A gather style approach might be better,
448  // where each point appends weights/positions into
449  // a container. We lose some time having to re-
450  // iterate, but this is far more memory efficient.
451  const WeightSumT weight = 1.0f - math::Pow3(float(math::Sqrt(d2)) * searchRadiusInv);
452  const WeightedPositionSumT posMeanDiff = Psrc - currWeightSum;
453  // @note Could extract the mult by totalWeightInv
454  // and put it into a loop in finalize() - except
455  // it would be mat3*float rather than vec*float,
456  // which would probably better as maxppv increases
457  const WeightedPositionSumT x = (totalWeightInv * weight) * posMeanDiff;
458 
459  float* const m = mCovMats[tgtid].asPointer();
460  /// @note: equal to:
461  // mat.setCol(0, mat.col(0) + (x * posMeanDiff[0]));
462  // mat.setCol(1, mat.col(1) + (x * posMeanDiff[1]));
463  // mat.setCol(2, mat.col(2) + (x * posMeanDiff[2]));
464  // @todo formalize precision of these methods
465  m[0] += float(x[0] * posMeanDiff[0]);
466  m[1] += float(x[0] * posMeanDiff[1]);
467  m[2] += float(x[0] * posMeanDiff[2]);
468  //
469  m[3] += float(x[1] * posMeanDiff[0]);
470  m[4] += float(x[1] * posMeanDiff[1]);
471  m[5] += float(x[1] * posMeanDiff[2]);
472  //
473  m[6] += float(x[2] * posMeanDiff[0]);
474  m[7] += float(x[2] * posMeanDiff[1]);
475  m[8] += float(x[2] * posMeanDiff[2]);
476  } //point idx
477  }
478  }
479  } // outer sdf voxel
480  } // point idx
481  }
482 
483  bool finalize(const Coord&, size_t) { return true; }
484 
485 private:
486  points::GroupHandle::UniquePtr mInclusionGroupHandle;
487  const WeightSumT* mWeights;
488  const WeightedPositionSumT* mWeightedPositions;
489  math::Mat3s* mCovMats;
490  std::vector<int32_t> mCounts;
491 };
492 
493 /// @brief Sort a vector into descending order and output a vector of the resulting order
494 /// @param vector Vector to sort
495 template <typename Scalar>
496 inline Vec3i
498 {
499  Vec3i order(0,1,2);
500  if (vector[0] < vector[1]) {
501  std::swap(vector[0], vector[1]);
502  std::swap(order[0], order[1]);
503  }
504  if (vector[1] < vector[2]) {
505  std::swap(vector[1], vector[2]);
506  std::swap(order[1], order[2]);
507  }
508  if (vector[0] < vector[1]) {
509  std::swap(vector[0], vector[1]);
510  std::swap(order[0], order[1]);
511  }
512  return order;
513 }
514 
515 /// @brief Decomposes a symmetric matrix into its eigenvalues and a rotation matrix of eigenvectors.
516 /// Note that if mat is positive-definite, this will be equivalent to a singular value
517 /// decomposition where V = U.
518 /// @param mat Matrix to decompose
519 /// @param U rotation matrix. The order of its columns (which will be eigenvectors) will match
520 /// the eigenvalues in sigma
521 /// @param sigma vector of eigenvalues
522 template <typename Scalar>
523 inline bool
526  math::Vec3<Scalar>& sigma)
527 {
529  const bool diagonalized = math::diagonalizeSymmetricMatrix(mat, Q, sigma);
530 
531  if (!diagonalized) return false;
532 
533  // need to sort eigenvalues and eigenvectors
534  Vec3i order = descendingOrder(sigma);
535 
536  // we need to re-order the matrix ("Q") columns to match the new eigenvalue order
537  // to obtain the correct "U" matrix
538  U.setColumns(Q.col(order[0]), Q.col(order[1]), Q.col(order[2]));
539 
540  return true;
541 }
542 
543 } // namespace pca_internal
544 
545 
546 template <typename PointDataGridT>
547 inline void
548 pca(PointDataGridT& points,
549  const PcaSettings& settings,
550  const PcaAttributes& attrs)
551 {
553 
554  using namespace pca_internal;
555 
556  using PointDataTreeT = typename PointDataGridT::TreeType;
557  using LeafManagerT = tree::LeafManager<PointDataTreeT>;
558  using LeafNodeT = typename PointDataTreeT::LeafNodeType;
559 
560  auto& tree = points.tree();
561  const auto leaf = tree.cbeginLeaf();
562  if (!leaf) return;
563 
564  // Small lambda to init any one of the necessary ellipsoid attributes
565  // @note Various algorithms here assume that we are responsible for creating
566  // these attributes and so can optimize accordingly. If this changes we'll
567  // need to also change those optimizations (e.g. NullCodec, loadData, etc)
568  const auto initAttribute = [&](const std::string& name, const auto val)
569  {
570  using ValueT = std::decay_t<decltype(val)>;
571  if (leaf->hasAttribute(name)) {
572  OPENVDB_THROW(KeyError, "PCA attribute '" << name << "' already exists!");
573  }
574 
575  points::appendAttribute<ValueT>(tree, name, val);
576  return leaf->attributeSet().find(name);
577  };
578 
579  //
580 
581  const size_t pvsIdx = leaf->attributeSet().find("P");
582  const auto& xform = points.constTransform();
583  const double vs = xform.voxelSize()[0];
584  LeafManagerT manager(tree);
585 
586  // 1) Create persisting attributes
587  const size_t pwsIdx = initAttribute(attrs.positionWS, zeroVal<PcaAttributes::PosWsT>());
588  const size_t rotIdx = initAttribute(attrs.rotation, zeroVal<PcaAttributes::RotationT>());
589  const size_t strIdx = initAttribute(attrs.stretch, PcaAttributes::StretchT(settings.nonAnisotropicStretch));
590 
591  // 2) Create temporary attributes
592  const auto& descriptor = leaf->attributeSet().descriptor();
593  const std::vector<std::string> temps {
594  descriptor.uniqueName("_weightedpositionsums"),
595  descriptor.uniqueName("_inv_weightssum")
596  };
597 
598  const size_t posSumIndex = initAttribute(temps[0], zeroVal<WeightedPositionSumT>());
599  const size_t weightSumIndex = initAttribute(temps[1], zeroVal<WeightSumT>());
600 
601  // 3) Create ellipses group
602  if (!leaf->attributeSet().descriptor().hasGroup(attrs.ellipses)) {
603  points::appendGroup(tree, attrs.ellipses);
604  // Include everything by default to start with
605  points::setGroup(tree, attrs.ellipses, true);
606  }
607 
608  // Re-acquire the updated descriptor and get the group idx
609  const GroupIndexT ellipsesIdx =
610  leaf->attributeSet().descriptor().groupIndex(attrs.ellipses);
611 
612  PcaTimer timer(settings.interrupter);
613 
614  // 3) Store the world space position on the PDG to speed up subsequent
615  // calculations.
616  timer.start("Compute position world spaces");
617  manager.foreach([&](LeafNodeT& leafnode, size_t)
618  {
619  using PvsT = Vec3f;
620  using PwsT = PcaAttributes::PosWsT;
621 
622  points::AttributeHandle<PvsT> Pvs(leafnode.constAttributeArray(pvsIdx));
623  PwsT* Pws = initPcaArrayAttribute<PwsT>(leafnode, pwsIdx, /*fill=*/false);
624 
625  for (auto voxel = leafnode.cbeginValueOn(); voxel; ++voxel) {
626  const Coord voxelCoord = voxel.getCoord();
627  const Vec3d coordVec = voxelCoord.asVec3d();
628  for (auto iter = leafnode.beginIndexVoxel(voxelCoord); iter; ++iter) {
629  Pws[*iter] = xform.indexToWorld(Pvs.get(*iter) + coordVec);
630  }
631  }
632  });
633 
634  timer.stop();
635  if (util::wasInterrupted(settings.interrupter)) return;
636 
637  AttrIndices indices;
638  indices.mPosSumIndex = posSumIndex;
639  indices.mWeightSumIndex = weightSumIndex;
640  indices.mCovMatrixIndex = rotIdx;
641  indices.mPWsIndex = pwsIdx;
642  indices.mEllipsesGroupIndex = ellipsesIdx;
643 
644  // 4) Init temporary attributes and calculate:
645  // sum_j w_{i,j} * x_j / (sum_j w_j)
646  // And neighbour counts for each point.
647  // simultaneously calculates the sum of weighted vector positions (sum w_{i,j} * x_i)
648  // weighted against the inverse sum of weights (1.0 / sum w_{i,j}). Also counts number
649  // of neighours each point has and updates the ellipses group based on minimum
650  // neighbour threshold. Those points which are "included" but which lack sufficient
651  // neighbours will be marked as "not included".
652  timer.start("Compute position weights");
653  {
654  WeightPosSumsTransfer<PointDataTreeT>
655  transfer(indices, settings, float(vs), manager, settings.interrupter);
656  points::rasterize<PointDataGridT, decltype(transfer)> (points, transfer);
657  }
658 
659  timer.stop();
660  if (util::wasInterrupted(settings.interrupter)) return;
661 
662  // 5) Principal axes define the rotation matrix of the ellipsoid.
663  // Calculates covariance matrices given weighted sums of positions and
664  // sums of weights per-particle
665  timer.start("Compute covariance matrices");
666  {
667  CovarianceTransfer<PointDataTreeT>
668  transfer(indices, settings, float(vs), manager, settings.interrupter);
669  points::rasterize<PointDataGridT, decltype(transfer)>(points, transfer);
670  }
671 
672  timer.stop();
673  if (util::wasInterrupted(settings.interrupter)) return;
674 
675  // 6) radii stretches are given by the scaled singular values. Decompose
676  // the covariance matrix into its principal axes and their lengths
677  timer.start("Decompose covariance matrices");
678  manager.foreach([&](LeafNodeT& leafnode, size_t)
679  {
680  AttributeWriteHandle<Vec3f, NullCodec> stretchHandle(leafnode.attributeArray(strIdx));
681  AttributeWriteHandle<math::Mat3s, NullCodec> rotHandle(leafnode.attributeArray(rotIdx));
682  GroupHandle ellipsesGroupHandle(leafnode.groupHandle(ellipsesIdx));
683 
684  for (Index idx = 0; idx < stretchHandle.size(); ++idx) {
685  if (!ellipsesGroupHandle.get(idx)) {
686  rotHandle.set(idx, math::Mat3s::identity());
687  continue;
688  }
689 
690  // get singular values of the covariance matrix
691  math::Mat3s u;
692  Vec3s sigma;
693  decomposeSymmetricMatrix(rotHandle.get(idx), u, sigma);
694 
695  // fix sigma values, the principal lengths
696  auto maxs = sigma[0] * settings.allowedAnisotropyRatio;
697  sigma[1] = std::max(sigma[1], maxs);
698  sigma[2] = std::max(sigma[2], maxs);
699 
700  // should only happen if all neighbours are coincident
701  // @note The specific tolerance here relates to the normalization
702  // of the stetch values in step (7) e.g. s*(1.0/cbrt(s.product())).
703  // math::Tolerance<float>::value is 1e-7f, but we have a bit more
704  // flexibility here, we can deal with smaller values, common for
705  // the case where a point only has one neighbour
706  // @todo have to manually construct the tolerance because
707  // math::Tolerance<Vec3f> resolves to 0.0. fix this in the math lib
708  if (math::isApproxZero(sigma, Vec3f(1e-11f))) {
709  sigma = Vec3f::ones();
710  }
711 
712  stretchHandle.set(idx, sigma);
713  rotHandle.set(idx, u);
714  }
715  });
716  timer.stop();
717 
718  // 7) normalise the principal lengths such that the transformation they
719  // describe 'preserves volume' thus becoming the stretch of the ellipsoids
720 
721  // Calculates the average volume change that would occur if applying the
722  // transformations defined by the calculate principal axes and their
723  // lengths using the determinant of the stretch component of the
724  // transformation (given by the sum of the diagonal values) and uses this
725  // value to normalise these lengths such that they are relative to the
726  // identity. This ensures that the transformation preserves volume.
727  timer.start("Normalise the principal lengths");
728  manager.foreach([&](LeafNodeT& leafnode, size_t)
729  {
730  points::GroupFilter filter(ellipsesIdx);
731  filter.reset(leafnode);
732  AttributeWriteHandle<Vec3f, NullCodec> stretchHandle(leafnode.attributeArray(strIdx));
733 
734  for (Index i = 0; i < stretchHandle.size(); ++i)
735  {
736  if (!filter.valid(&i)) continue;
737  const Vec3f stretch = stretchHandle.get(i);
738  OPENVDB_ASSERT(stretch != Vec3f::zero());
739  const float stretchScale = 1.0f / std::cbrt(stretch.product());
740  stretchHandle.set(i, stretchScale * stretch);
741  }
742  });
743 
744  timer.stop();
745  if (util::wasInterrupted(settings.interrupter)) return;
746 
747  // 8) do laplacian position smoothing here as we have the weights already
748  /// calculated (calculates the smoothed kernel origins as described in
749  /// the paper). averagePositions value biases the smoothed positions
750  /// towards the weighted mean positions. 1.0 will use the weighted means
751  /// while 0.0 will use the original world-space positions
752  timer.start("Laplacian smooth positions");
753  if (settings.averagePositions > 0.0f)
754  {
755  manager.foreach([&](LeafNodeT& leafnode, size_t) {
756  AttributeWriteHandle<Vec3d, NullCodec> Pws(leafnode.attributeArray(pwsIdx));
757  AttributeHandle<WeightedPositionSumT, NullCodec> weightedPosSumHandle(leafnode.constAttributeArray(posSumIndex));
758 
759  for (Index i = 0; i < Pws.size(); ++i)
760  {
761  // @note Technically possible for the weights to be valid
762  // _and_ zero.
763  if (math::isApproxZero(weightedPosSumHandle.get(i),
764  Vec3d(math::Tolerance<double>::value()))) {
765  continue;
766  }
767  const Vec3d smoothedPosition = (1.0f - settings.averagePositions) *
768  Pws.get(i) + settings.averagePositions * weightedPosSumHandle.get(i);
769  Pws.set(i, smoothedPosition);
770  }
771  });
772  }
773 
774  timer.stop();
775 
776  // Remove temporary attributes
777  points::dropAttributes(tree, temps);
778 }
779 
780 }
781 }
782 }
783 
784 #endif // OPENVDB_POINTS_POINT_PRINCIPAL_COMPONENT_ANALYSIS_IMPL_HAS_BEEN_INCLUDED
Helper metafunction used to determine if the first template parameter is a specialization of the clas...
Definition: Types.h:263
const AttrIndices & mIndices
Definition: PrincipalComponentAnalysisImpl.h:178
Simple timer for basic profiling.
Definition: CpuTimer.h:66
util::NullInterrupter * interrupter
Definition: PrincipalComponentAnalysis.h:149
Definition: AttributeGroup.h:102
The VolumeTransfer module provides methods to automatically setup and access destination buffers for ...
Definition: PointTransfer.h:296
Definition: Exceptions.h:59
void start(const char *msg)
Definition: PrincipalComponentAnalysisImpl.h:40
void setColumns(const Vec3< T > &v1, const Vec3< T > &v2, const Vec3< T > &v3)
Set the columns of this matrix to the vectors v1, v2, v3.
Definition: Mat3.h:209
Definition: PrincipalComponentAnalysisImpl.h:57
#define OPENVDB_THROW(exception, message)
Definition: Exceptions.h:74
WeightPosSumsTransfer(const WeightPosSumsTransfer &other)
Definition: PrincipalComponentAnalysisImpl.h:208
void initialize(const Coord &origin, const size_t idx, const CoordBBox &bounds)
Definition: PrincipalComponentAnalysisImpl.h:214
OPENVDB_IMPORT void initialize()
Global registration of native Grid, Transform, Metadata and Point attribute types. Also initializes blosc (if enabled).
void start(const char *)
Definition: PrincipalComponentAnalysisImpl.h:29
float Sqrt(float x)
Return the square root of a floating-point value.
Definition: Math.h:761
const PcaSettings & mSettings
Definition: PrincipalComponentAnalysisImpl.h:179
bool startPointLeaf(const typename PointDataTreeT::LeafNodeType &leaf)
Definition: PrincipalComponentAnalysisImpl.h:166
const Real mDxInv
Definition: PrincipalComponentAnalysisImpl.h:180
std::unique_ptr< PositionHandleT > mTargetPosition
Definition: PrincipalComponentAnalysisImpl.h:182
static Vec3< T > zero()
Predefined constants, e.g. Vec3d v = Vec3d::xNegAxis();.
Definition: Vec3.h:467
Index32 Index
Definition: Types.h:54
void dropAttributes(PointDataTreeT &tree, const std::vector< size_t > &indices)
Drops attributes from the VDB tree.
Definition: PointAttributeImpl.h:238
bool isApproxZero(const Type &x)
Return true if x is equal to zero to within the default floating-point comparison tolerance...
Definition: Math.h:349
void rasterizePoints(const Coord &, const Index start, const Index end, const CoordBBox &bounds)
Definition: PrincipalComponentAnalysisImpl.h:377
Vec3i range(const Coord &, size_t) const
Definition: PrincipalComponentAnalysisImpl.h:155
const tree::LeafManager< PointDataTreeT > & mManager
Definition: PrincipalComponentAnalysisImpl.h:181
size_t mPWsIndex
Definition: PrincipalComponentAnalysisImpl.h:62
float averagePositions
Definition: PrincipalComponentAnalysis.h:147
void rasterizePoints(const Coord &, const Index start, const Index end, const CoordBBox &bounds)
Definition: PrincipalComponentAnalysisImpl.h:223
Index filtering on group membership.
Definition: AttributeGroup.h:135
Write-able version of AttributeHandle.
Definition: AttributeArray.h:831
GroupIndexT mEllipsesGroupIndex
Definition: PrincipalComponentAnalysisImpl.h:63
void appendGroup(PointDataTreeT &tree, const Name &group)
Appends a new empty group to the VDB tree.
Definition: PointGroupImpl.h:209
float nonAnisotropicStretch
Definition: PrincipalComponentAnalysis.h:99
Vec3< int32_t > Vec3i
Definition: Vec3.h:662
PcaTimer(util::NullInterrupter *const interrupt)
Definition: PrincipalComponentAnalysisImpl.h:37
double WeightSumT
Definition: PrincipalComponentAnalysisImpl.h:53
#define OPENVDB_UNLIKELY(x)
Definition: Platform.h:92
WeightPosSumsTransfer(const AttrIndices &indices, const PcaSettings &settings, const Real vs, tree::LeafManager< PointDataTreeT > &manager, util::NullInterrupter *interrupt)
Definition: PrincipalComponentAnalysisImpl.h:198
Base class for interrupters.
Definition: NullInterrupter.h:25
Definition: PrincipalComponentAnalysisImpl.h:28
float searchRadius() const
Definition: PrincipalComponentAnalysisImpl.h:150
Definition: AttributeArray.h:763
T & x()
Reference to the component, e.g. v.x() = 4.5f;.
Definition: Vec3.h:86
void pca(PointDataGridT &points, const PcaSettings &settings, const PcaAttributes &attrs)
Calculate ellipsoid transformations from the local point distributions as described in Yu and Turk&#39;s ...
Definition: PrincipalComponentAnalysisImpl.h:548
Various settings for the neighborhood analysis of point distributions.
Definition: PrincipalComponentAnalysis.h:66
std::unique_ptr< GroupHandle > UniquePtr
Definition: AttributeGroup.h:77
#define OPENVDB_ASSERT(X)
Definition: Assert.h:41
Vec3< T > col(int j) const
Get jth column, e.g. Vec3d v = m.col(0);.
Definition: Mat3.h:168
size_t maxTargetPointsPerVoxel() const
Definition: PrincipalComponentAnalysisImpl.h:153
typename PointDataTreeT::LeafNodeType LeafNodeType
Definition: PrincipalComponentAnalysisImpl.h:100
Definition: AttributeGroup.h:73
size_t mWeightSumIndex
Definition: PrincipalComponentAnalysisImpl.h:60
Vec3i range() const
Definition: PrincipalComponentAnalysisImpl.h:156
Type Pow2(Type x)
Return x2.
Definition: Math.h:548
Tolerance for floating-point comparison.
Definition: Math.h:148
CovarianceTransfer(const CovarianceTransfer &other)
Definition: PrincipalComponentAnalysisImpl.h:361
T * initPcaArrayAttribute(LeafNodeT &leaf, const size_t idx, const bool fill=true)
Definition: PrincipalComponentAnalysisImpl.h:67
Definition: Exceptions.h:13
float Round(float x)
Return x rounded to the nearest integer.
Definition: Math.h:819
points::AttributeSet::Descriptor::GroupIndex GroupIndexT
Definition: PrincipalComponentAnalysisImpl.h:55
size_t mPosSumIndex
Definition: PrincipalComponentAnalysisImpl.h:59
Definition: Mat.h:165
Definition: PrincipalComponentAnalysisImpl.h:342
Vec3i descendingOrder(math::Vec3< Scalar > &vector)
Sort a vector into descending order and output a vector of the resulting order.
Definition: PrincipalComponentAnalysisImpl.h:497
std::string ellipses
A point group to create that represents points which have valid ellipsoidal neighborhood. Points outside of this group will have their stretch and rotation attributes set to describe a canonical sphere. Note however that all points, regardless of this groups membership flag, will still contribute to their neighbours and may have their world space position deformed in relation to their neighboring points.
Definition: PrincipalComponentAnalysis.h:185
Vec3d WeightedPositionSumT
Definition: PrincipalComponentAnalysisImpl.h:54
util::NullInterrupter *const mInterrupt
Definition: PrincipalComponentAnalysisImpl.h:50
PcaTransfer(const AttrIndices &indices, const PcaSettings &settings, const Real vs, tree::LeafManager< PointDataTreeT > &manager, util::NullInterrupter *interrupt)
Definition: PrincipalComponentAnalysisImpl.h:105
bool decomposeSymmetricMatrix(const math::Mat3< Scalar > &mat, math::Mat3< Scalar > &U, math::Vec3< Scalar > &sigma)
Decomposes a symmetric matrix into its eigenvalues and a rotation matrix of eigenvectors. Note that if mat is positive-definite, this will be equivalent to a singular value decomposition where V = U.
Definition: PrincipalComponentAnalysisImpl.h:524
double Real
Definition: Types.h:60
bool wasInterrupted(T *i, int percent=-1)
Definition: NullInterrupter.h:49
void setGroup(PointDataTreeT &tree, const PointIndexTreeT &indexTree, const std::vector< short > &membership, const Name &group, const bool remove=false)
Sets group membership from a PointIndexTree-ordered vector.
Definition: PointGroupImpl.h:438
size_t maxSourcePointsPerVoxel() const
Definition: PrincipalComponentAnalysisImpl.h:152
void stop()
Definition: PrincipalComponentAnalysisImpl.h:45
Definition: PrincipalComponentAnalysisImpl.h:95
PcaTransfer(const PcaTransfer &other)
Definition: PrincipalComponentAnalysisImpl.h:122
InterruptableTransfer module, when derived from allows for schemes to callback into a interrupter...
Definition: PointTransfer.h:230
bool endPointLeaf(const typename PointDataTreeT::LeafNodeType &)
Definition: PrincipalComponentAnalysisImpl.h:175
The persistent attributes created by the PCA methods.
Definition: PrincipalComponentAnalysis.h:155
size_t mCovMatrixIndex
Definition: PrincipalComponentAnalysisImpl.h:61
void reset(const LeafT &leaf)
Definition: AttributeGroup.h:151
LeafNodeType * initialize(const Coord &origin, const size_t idx, const CoordBBox &bounds)
Definition: PrincipalComponentAnalysisImpl.h:158
std::string positionWS
Definition: PrincipalComponentAnalysis.h:176
std::string stretch
Definition: PrincipalComponentAnalysis.h:161
Type Pow3(Type x)
Return x3.
Definition: Math.h:552
bool finalize(const Coord &, size_t idx)
Definition: PrincipalComponentAnalysisImpl.h:305
std::unique_ptr< PositionHandleT > mSourcePosition
Definition: PrincipalComponentAnalysisImpl.h:183
const TreeType & tree() const
Return a const reference to tree associated with this manager.
Definition: LeafManager.h:303
bool diagonalizeSymmetricMatrix(const Mat3< T > &input, Mat3< T > &Q, Vec3< T > &D, unsigned int MAX_ITERATIONS=250)
Use Jacobi iterations to decompose a symmetric 3x3 matrix (diagonalize and compute eigenvectors) ...
Definition: Mat3.h:736
float allowedAnisotropyRatio
Definition: PrincipalComponentAnalysis.h:94
void stop()
Definition: PrincipalComponentAnalysisImpl.h:30
#define OPENVDB_VERSION_NAME
The version namespace name for this library version.
Definition: version.h.in:121
std::string rotation
Definition: PrincipalComponentAnalysis.h:167
Definition: PrincipalComponentAnalysisImpl.h:35
static Vec3< T > ones()
Definition: Vec3.h:468
size_t neighbourThreshold() const
Definition: PrincipalComponentAnalysisImpl.h:151
CovarianceTransfer(const AttrIndices &indices, const PcaSettings &settings, const Real vs, tree::LeafManager< PointDataTreeT > &manager, util::NullInterrupter *interrupt)
Definition: PrincipalComponentAnalysisImpl.h:350
Definition: PrincipalComponentAnalysisImpl.h:190
Vec3< float > Vec3s
Definition: Vec3.h:664
const std::enable_if<!VecTraits< T >::IsVec, T >::type & max(const T &a, const T &b)
Definition: Composite.h:110
bool valid(const IterT &iter) const
Definition: AttributeGroup.h:156
bool finalize(const Coord &, size_t)
Definition: PrincipalComponentAnalysisImpl.h:483
3x3 matrix class.
Definition: Mat3.h:28
T product() const
Return the product of all the vector components.
Definition: Vec3.h:357
void initialize(const Coord &origin, const size_t idx, const CoordBBox &bounds)
Definition: PrincipalComponentAnalysisImpl.h:368
#define OPENVDB_USE_VERSION_NAMESPACE
Definition: version.h.in:218