ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
qM3C2Process.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "qM3C2Process.h"
9 
10 // local
11 #include "qM3C2Dialog.h"
12 #include "qM3C2DisclaimerDialog.h"
13 #include "qM3C2Tools.h"
14 
15 // cloudViewer
16 #include <CloudSamplingTools.h>
17 
18 // CV_DB_LIB
19 #include <ecvGenericPointCloud.h>
20 #include <ecvHObjectCaster.h>
21 #include <ecvNormalVectors.h>
22 #include <ecvOctree.h>
23 #include <ecvOctreeProxy.h>
24 #include <ecvPointCloud.h>
25 #include <ecvProgressDialog.h>
26 #include <ecvScalarField.h>
27 
28 // ECV_PLUGINS
29 #include <ecvMainAppInterface.h>
30 
31 // Qt
32 #include <QApplication>
33 #include <QElapsedTimer>
34 #include <QMessageBox>
35 #include <QtConcurrentMap>
36 #include <QtCore>
37 #include <QtGui>
38 
40 static const char M3C2_DIST_SF_NAME[] = "M3C2 distance";
41 static const char DIST_UNCERTAINTY_SF_NAME[] = "distance uncertainty";
42 static const char SIG_CHANGE_SF_NAME[] = "significant change";
43 static const char STD_DEV_CLOUD1_SF_NAME[] = "%1_cloud1";
44 static const char STD_DEV_CLOUD2_SF_NAME[] = "%1_cloud2";
45 static const char DENSITY_CLOUD1_SF_NAME[] = "Npoints_cloud1";
46 static const char DENSITY_CLOUD2_SF_NAME[] = "Npoints_cloud2";
47 static const char NORMAL_SCALE_SF_NAME[] = "normal scale";
48 
49 static void RemoveScalarField(ccPointCloud* cloud, const char sfName[]) {
50  int sfIdx = cloud ? cloud->getScalarFieldIndexByName(sfName) : -1;
51  if (sfIdx >= 0) {
52  cloud->deleteScalarField(sfIdx);
53  }
54 }
55 
56 static ScalarType SCALAR_ZERO = 0;
57 static ScalarType SCALAR_ONE = 1;
58 
59 // Precision maps (See "3D uncertainty-based topographic change detection with
60 // SfM photogrammetry: precision maps for ground control and directly
61 // georeferenced surveys" by James et al.)
62 struct PrecisionMaps {
63  PrecisionMaps() : sX(nullptr), sY(nullptr), sZ(nullptr), scale(1.0) {}
64  bool valid() const {
65  return (sX != nullptr && sY != nullptr && sZ != nullptr);
66  }
68  double scale;
69 };
70 
71 // Computes the uncertainty based on 'precision maps' (as scattered scalar
72 // fields)
74  const CCVector3& N,
75  const PrecisionMaps& PM) {
76  size_t count = set.size();
77  if (count == 0) {
78  assert(false);
79  return 0;
80  }
81 
82  int minIndex = -1;
83  if (count == 1) {
84  minIndex = 0;
85  } else {
86  // compute gravity center
87  CCVector3d G(0, 0, 0);
88  for (size_t i = 0; i < count; ++i) {
89  G.x += set[i].point->x;
90  G.y += set[i].point->y;
91  G.z += set[i].point->z;
92  }
93 
94  G.x /= count;
95  G.y /= count;
96  G.z /= count;
97 
98  // now look for the point that is the closest to the gravity center
99  double minSquareDist = -1.0;
100  minIndex = -1;
101  for (size_t i = 0; i < count; ++i) {
102  CCVector3d dG(G.x - set[i].point->x, G.y - set[i].point->y,
103  G.z - set[i].point->z);
104  double squareDist = dG.norm2();
105  if (minIndex < 0 || squareDist < minSquareDist) {
106  minSquareDist = squareDist;
107  minIndex = static_cast<int>(i);
108  }
109  }
110  }
111 
112  assert(minIndex >= 0);
113  unsigned pointIndex = set[minIndex].pointIndex;
114  CCVector3d sigma(PM.sX->getValue(pointIndex) * PM.scale,
115  PM.sY->getValue(pointIndex) * PM.scale,
116  PM.sZ->getValue(pointIndex) * PM.scale);
117 
118  CCVector3d NS(N.x * sigma.x, N.y * sigma.y, N.z * sigma.z);
119 
120  return NS.norm();
121 }
122 
123 // Structure for parallel call to ComputeM3C2DistForPoint
124 struct M3C2Params {
125  // input data
129 
130  // main options
133  bool updateNormal = false;
134  bool exportNormal = false;
135  bool useMedian = false;
136  bool computeConfidence = false;
137  bool progressiveSearch = false;
138  bool onlyPositiveSearch = false;
139  unsigned minPoints4Stats = 3;
140  double registrationRms = 0;
141 
142  // export
144  bool keepOriginalCloud = false;
145 
146  // octrees
148  unsigned char level1 = 0;
150  unsigned char level2 = 0;
151 
152  // scalar fields
153  ccScalarField* m3c2DistSF = nullptr; // M3C2 distance
154  ccScalarField* distUncertaintySF = nullptr; // distance uncertainty
155  ccScalarField* sigChangeSF = nullptr; // significant change
157  nullptr; // standard deviation information for cloud #1
159  nullptr; // standard deviation information for cloud #2
161  nullptr; // export point density at projection scale for cloud #1
163  nullptr; // export point density at projection scale for cloud #2
164 
165  // precision maps
167  bool usePrecisionMaps = false;
168 
169  // progress notification
171  bool processCanceled = false;
172 };
174 
175 void ComputeM3C2DistForPoint(unsigned index) {
176  if (s_M3C2Params.processCanceled) return;
177 
178  ScalarType dist = NAN_VALUE;
179 
180  // get core point #i
181  CCVector3 P;
182  s_M3C2Params.corePoints->getPoint(index, P);
183 
184  // get core point's normal #i
185  CCVector3 N(0, 0, 1);
186  if (s_M3C2Params.updateNormal) // i.e. all cases but the VERTICAL mode
187  {
190  }
191 
192  // output point
193  CCVector3 outputP = P;
194 
195  // compute M3C2 distance
196  {
197  double mean1 = 0, stdDev1 = 0;
198  bool validStats1 = false;
199 
200  // extract cloud #1's neighbourhood
202  cn1.center = P;
203  cn1.dir = N;
204  cn1.level = s_M3C2Params.level1;
208 
210  // progressive search
211  size_t previousNeighbourCount = 0;
212  while (cn1.currentHalfLength < cn1.maxHalfLength) {
213  size_t neighbourCount =
215  ->getPointsInCylindricalNeighbourhoodProgressive(
216  cn1);
217  if (neighbourCount != previousNeighbourCount) {
218  // do we have enough points for computing stats?
219  if (neighbourCount >= s_M3C2Params.minPoints4Stats) {
222  mean1, stdDev1);
223  validStats1 = true;
224  // do we have a sharp enough 'mean' to stop?
225  if (fabs(mean1) + 2 * stdDev1 <
226  static_cast<double>(cn1.currentHalfLength))
227  break;
228  }
229  previousNeighbourCount = neighbourCount;
230  }
231  }
232  } else {
233  s_M3C2Params.cloud1Octree->getPointsInCylindricalNeighbourhood(cn1);
234  }
235 
236  size_t n1 = cn1.neighbours.size();
237  if (n1 != 0) {
238  // compute stat. dispersion on cloud #1 neighbours (if necessary)
239  if (!validStats1) {
241  cn1.neighbours, s_M3C2Params.useMedian, mean1, stdDev1);
242  }
243 
247  // compute the Precision Maps derived sigma
248  stdDev1 = ComputePMUncertainty(cn1.neighbours, N,
250  }
251 
253  // shift output point on the 1st cloud
254  outputP += static_cast<PointCoordinateType>(mean1) * N;
255  }
256 
257  // save cloud #1's std. dev.
259  ScalarType val = static_cast<ScalarType>(stdDev1);
260  s_M3C2Params.stdDevCloud1SF->setValue(index, val);
261  }
262  }
263 
264  // save cloud #1's density
266  ScalarType val = static_cast<ScalarType>(n1);
268  }
269 
270  // now we can process cloud #2
271  if (n1 != 0 ||
274  double mean2 = 0, stdDev2 = 0;
275  bool validStats2 = false;
276 
277  // extract cloud #2's neighbourhood
279  cn2.center = P;
280  cn2.dir = N;
281  cn2.level = s_M3C2Params.level2;
285 
287  // progressive search
288  size_t previousNeighbourCount = 0;
289  while (cn2.currentHalfLength < cn2.maxHalfLength) {
290  size_t neighbourCount =
292  ->getPointsInCylindricalNeighbourhoodProgressive(
293  cn2);
294  if (neighbourCount != previousNeighbourCount) {
295  // do we have enough points for computing stats?
296  if (neighbourCount >= s_M3C2Params.minPoints4Stats) {
299  mean2, stdDev2);
300  validStats2 = true;
301  // do we have a sharp enough 'mean' to stop?
302  if (fabs(mean2) + 2 * stdDev2 <
303  static_cast<double>(cn2.currentHalfLength))
304  break;
305  }
306  previousNeighbourCount = neighbourCount;
307  }
308  }
309  } else {
310  s_M3C2Params.cloud2Octree->getPointsInCylindricalNeighbourhood(
311  cn2);
312  }
313 
314  size_t n2 = cn2.neighbours.size();
315  if (n2 != 0) {
316  // compute stat. dispersion on cloud #2 neighbours (if
317  // necessary)
318  if (!validStats2) {
320  s_M3C2Params.useMedian, mean2,
321  stdDev2);
322  }
323  assert(stdDev2 != stdDev2 ||
324  stdDev2 >= 0); // first inequality fails if stdDev2 is
325  // NaN ;)
326 
329  // shift output point on the 2nd cloud
330  outputP += static_cast<PointCoordinateType>(mean2) * N;
331  }
332 
336  // compute the Precision Maps derived sigma
337  stdDev2 = ComputePMUncertainty(cn2.neighbours, N,
339  }
340 
341  if (n1 != 0) {
342  // m3c2 dist = distance between i1 and i2 (i.e. either the
343  // mean or the median of both neighborhoods)
344  dist = static_cast<ScalarType>(mean2 - mean1);
346 
347  // confidence interval
349  ScalarType LODStdDev = NAN_VALUE;
351  LODStdDev = stdDev1 * stdDev1 +
352  stdDev2 * stdDev2; // equation (2) in
353  // M3C2-PM article
354  }
355  // standard M3C2 algortihm: have we enough points for
356  // computing the confidence interval?
357  else if (n1 >= s_M3C2Params.minPoints4Stats &&
359  LODStdDev = (stdDev1 * stdDev1) / n1 +
360  (stdDev2 * stdDev2) / n2;
361  }
362 
363  if (!std::isnan(LODStdDev)) {
364  // distance uncertainty (see eq. (1) in M3C2
365  // article)
366  ScalarType LOD = static_cast<ScalarType>(
367  1.96 * (sqrt(LODStdDev) +
369 
372  LOD);
373  }
374 
376  bool significant = (dist < -LOD || dist > LOD);
377  if (significant) {
379  index,
380  SCALAR_ONE); // already equal to
381  // SCALAR_ZERO
382  // otherwise
383  }
384  }
385  }
386  // else //DGM: scalar fields have already been
387  // initialized with the right 'default' values
388  //{
389  // if (distUncertaintySF)
390  // distUncertaintySF->setValue(index,
391  // NAN_VALUE); if (sigChangeSF)
392  // sigChangeSF->setValue(index,
393  // SCALAR_ZERO);
394  // }
395  }
396  }
397 
398  // save cloud #2's std. dev.
400  ScalarType val = static_cast<ScalarType>(stdDev2);
401  s_M3C2Params.stdDevCloud2SF->setValue(index, val);
402  }
403  }
404 
405  // save cloud #2's density
407  ScalarType val = static_cast<ScalarType>(n2);
409  }
410  }
411  }
412 
413  // output point
415  *const_cast<CCVector3*>(s_M3C2Params.outputCloud->getPoint(index)) =
416  outputP;
417  }
420  }
421 
422  // progress notification
425  }
426 }
427 
429  QString& errorMessage,
430  ccPointCloud*& outputCloud,
431  bool allowDialogs,
432  QWidget* parentWidget /*=nullptr*/,
433  ecvMainAppInterface* app /*=nullptr*/) {
434  errorMessage.clear();
435  outputCloud = nullptr;
436 
437  // get the clouds in the right order
438  ccPointCloud* cloud1 = dlg.getCloud1();
439  ccPointCloud* cloud2 = dlg.getCloud2();
440 
441  if (!cloud1 || !cloud2) {
442  assert(false);
443  return false;
444  }
445 
446  // normals computation parameters
447  double normalScale = dlg.normalScaleDoubleSpinBox->value();
448  double projectionScale = dlg.cylDiameterDoubleSpinBox->value();
450  double samplingDist = dlg.cpSubsamplingDoubleSpinBox->value();
451  ccScalarField* normalScaleSF = 0; // normal scale (multi-scale mode only)
452 
453  // other parameters are stored in 's_M3C2Params' for parallel call
456  projectionScale / 2); // we want the radius in fact ;)
458  dlg.cylHalfHeightDoubleSpinBox->value());
461  dlg.rmsCheckBox->isChecked() ? dlg.rmsDoubleSpinBox->value() : 0.0;
464  s_M3C2Params.useMedian = dlg.useMedianCheckBox->isChecked();
467  !dlg.useSinglePass4DepthCheckBox->isChecked();
469  dlg.positiveSearchOnlyCheckBox->isChecked();
470 
471  // precision maps
472  {
474  dlg.precisionMapsGroupBox->isEnabled() &&
475  dlg.precisionMapsGroupBox->isChecked();
477  if (allowDialogs &&
478  QMessageBox::question(parentWidget, "Precision Maps",
479  "Are you sure you want to compute the "
480  "M3C2 distances with precision maps?",
481  QMessageBox::Yes,
482  QMessageBox::No) == QMessageBox::No) {
484  dlg.precisionMapsGroupBox->setChecked(false);
485  }
486  }
489  cloud1->getScalarField(dlg.c1SxComboBox->currentIndex());
491  cloud1->getScalarField(dlg.c1SyComboBox->currentIndex());
493  cloud1->getScalarField(dlg.c1SzComboBox->currentIndex());
494  s_M3C2Params.cloud1PM.scale = dlg.pm1ScaleDoubleSpinBox->value();
495 
497  cloud2->getScalarField(dlg.c2SxComboBox->currentIndex());
499  cloud2->getScalarField(dlg.c2SyComboBox->currentIndex());
501  cloud2->getScalarField(dlg.c2SzComboBox->currentIndex());
502  s_M3C2Params.cloud2PM.scale = dlg.pm2ScaleDoubleSpinBox->value();
503 
504  if (!s_M3C2Params.cloud1PM.valid() ||
506  errorMessage = "Invalid 'Precision maps' settings!";
507  return false;
508  }
509  }
510  }
511 
512  // max thread count
513  int maxThreadCount = dlg.getMaxThreadCount();
514 
515  // progress dialog
516  ecvProgressDialog pDlg(parentWidget);
517 
518  // Duration: initialization & normals computation
519  QElapsedTimer initTimer;
520  initTimer.start();
521 
522  // compute octree(s) if necessary
523  s_M3C2Params.cloud1Octree = cloud1->getOctree();
524  if (!s_M3C2Params.cloud1Octree) {
525  s_M3C2Params.cloud1Octree = cloud1->computeOctree(&pDlg);
526  if (s_M3C2Params.cloud1Octree && cloud1->getParent() && app) {
527  app->addToDB(cloud1->getOctreeProxy());
528  }
529  }
530  if (!s_M3C2Params.cloud1Octree) {
531  errorMessage = "Failed to compute cloud #1's octree!";
532  return false;
533  }
534 
535  s_M3C2Params.cloud2Octree = cloud2->getOctree();
536  if (!s_M3C2Params.cloud2Octree) {
537  s_M3C2Params.cloud2Octree = cloud2->computeOctree(&pDlg);
538  if (s_M3C2Params.cloud2Octree && cloud2->getParent() && app) {
539  app->addToDB(cloud2->getOctreeProxy());
540  }
541  }
542  if (!s_M3C2Params.cloud2Octree) {
543  errorMessage = "Failed to compute cloud #2's octree!";
544  return false;
545  }
546 
547  // start the job
548  bool error = false;
549 
550  // should we generate the core points?
551  bool corePointsHaveBeenSubsampled = false;
552  if (!s_M3C2Params.corePoints && samplingDist > 0) {
554  cloudViewer::ReferenceCloud* subsampled =
556  cloud1, static_cast<PointCoordinateType>(samplingDist),
557  modParams, s_M3C2Params.cloud1Octree.data(), &pDlg);
558 
559  if (subsampled) {
561  static_cast<ccPointCloud*>(cloud1)->partialClone(
562  subsampled);
563 
564  // don't need those references anymore
565  delete subsampled;
566  subsampled = 0;
567  }
568 
569  if (s_M3C2Params.corePoints) {
571  QString("%1.subsampled [min dist. = %2]")
572  .arg(cloud1->getName())
573  .arg(samplingDist));
575  // s_M3C2Params.corePoints->setDisplay(cloud1->getDisplay());
576  if (app) {
577  app->dispToConsole(
578  QString("[M3C2] Sub-sampled cloud has been saved "
579  "('%1')")
583  }
584  corePointsHaveBeenSubsampled = true;
585  } else {
586  errorMessage = "Failed to compute sub-sampled core points!";
587  error = true;
588  }
589  }
590 
591  // output
592  QString outputName(s_M3C2Params.usePrecisionMaps ? "M3C2-PM output"
593  : "M3C2 output");
594 
595  if (!error) {
596  // whatever the case, at this point we should have core points
597  assert(s_M3C2Params.corePoints);
598  if (app)
599  app->dispToConsole(QString("[M3C2] Core points: %1")
600  .arg(s_M3C2Params.corePoints->size()),
602 
605  } else {
607  /*outputName*/); // setName will be called at the end
610  ->size())) // resize as we will 'set' the new
611  // points positions in
612  // 'ComputeM3C2DistForPoint'
613  {
614  errorMessage = "Not enough memory!";
615  error = true;
616  }
618  false); // we can hide the core points
619  }
620  }
621 
622  // compute normals
623  if (!error) {
624  bool normalsAreOk = false;
625  bool useCorePointsOnly = dlg.normUseCorePointsCheckBox->isChecked();
626 
627  switch (normMode) {
629  outputName += QString(" [HORIZONTAL]");
633  s_M3C2Params.coreNormals->link(); // will be released anyway at
634  // the end of the process
635 
636  std::vector<PointCoordinateType> radii;
637  if (normMode == qM3C2Normals::MULTI_SCALE_MODE) {
638  // get multi-scale parameters
639  double startScale = dlg.minScaleDoubleSpinBox->value();
640  double step = dlg.stepScaleDoubleSpinBox->value();
641  double stopScale = dlg.maxScaleDoubleSpinBox->value();
642  stopScale =
643  std::max(startScale, stopScale); // just to be sure
644  // generate all corresponding 'scales'
645  for (double scale = startScale; scale <= stopScale;
646  scale += step) {
647  radii.push_back(
648  static_cast<PointCoordinateType>(scale / 2));
649  }
650 
651  outputName += QString(" scale=[%1:%2:%3]")
652  .arg(startScale)
653  .arg(step)
654  .arg(stopScale);
655 
656  normalScaleSF = new ccScalarField(NORMAL_SCALE_SF_NAME);
657  normalScaleSF->link(); // will be released anyway at the
658  // end of the process
659  } else {
660  outputName += QString(" scale=%1").arg(normalScale);
661  // otherwise, we use a unique scale by default
662  radii.push_back(static_cast<PointCoordinateType>(
663  normalScale / 2)); // we want the radius in fact ;)
664  }
665 
666  bool invalidNormals = false;
667  ccPointCloud* baseCloud =
668  (useCorePointsOnly ? s_M3C2Params.corePoints : cloud1);
669  ccOctree* baseOctree =
670  (baseCloud == cloud1 ? s_M3C2Params.cloud1Octree.data()
671  : 0);
672 
673  // dedicated core points method
676  baseCloud, radii, invalidNormals, maxThreadCount,
677  normalScaleSF, &pDlg, baseOctree);
678 
679  // now fix the orientation
680  if (normalsAreOk) {
681  // some invalid normals?
682  if (invalidNormals && app) {
683  app->dispToConsole(
684  "[M3C2] Some normals are invalid! You may have "
685  "to increase the scale.",
687  }
688 
689  // make normals horizontal if necessary
690  if (normMode == qM3C2Normals::HORIZ_MODE) {
693  }
694 
695  // then either use a simple heuristic
696  bool usePreferredOrientation =
697  dlg.normOriPreferredRadioButton->isChecked();
698  if (usePreferredOrientation) {
699  int preferredOrientation =
700  dlg.normOriPreferredComboBox->currentIndex();
701  assert(preferredOrientation >=
703  preferredOrientation <=
708  static_cast<ccNormalVectors::Orientation>(
709  preferredOrientation))) {
710  errorMessage =
711  "[M3C2] Failed to re-orient the normals "
712  "(invalid parameter?)";
713  error = true;
714  }
715  } else // or use external points
716  {
719  assert(orientationCloud);
720 
724  maxThreadCount, &pDlg)) {
725  errorMessage =
726  "[M3C2] Failed to re-orient the normals "
727  "with input point cloud!";
728  error = true;
729  }
730  }
731 
732  if (!error && s_M3C2Params.coreNormals) {
736  }
737  }
738  } break;
739 
741  outputName += QString(" scale=%1").arg(normalScale);
743  (corePointsHaveBeenSubsampled ? s_M3C2Params.corePoints
744  : cloud1);
745  s_M3C2Params.coreNormals = sourceCloud->normals();
746  normalsAreOk = (s_M3C2Params.coreNormals &&
748  sourceCloud->size());
749  s_M3C2Params.coreNormals->link(); // will be released anyway at
750  // the end of the process
751 
752  // DGM TODO: should we export the normals to the output cloud?
753  } break;
754 
756  normalsAreOk = s_M3C2Params.corePoints &&
758  if (normalsAreOk) {
762  ->link(); // will be released anyway at the end of
763  // the process
764  }
765  } break;
766 
768  outputName += QString(" scale=%1").arg(normalScale);
769  outputName += QString(" [VERTICAL]");
770 
771  // nothing to do
772  normalsAreOk = true;
773  } break;
774  }
775 
776  if (!normalsAreOk) {
777  errorMessage = "Failed to compute normals!";
778  error = true;
779  }
780  }
781 
782  if (!error && s_M3C2Params.coreNormals && corePointsHaveBeenSubsampled) {
785  for (unsigned i = 0; i < s_M3C2Params.coreNormals->currentSize();
786  ++i)
790  } else if (app) {
791  app->dispToConsole(
792  "Failed to allocate memory for core points normals!",
794  }
795  }
796 
797  qint64 initTime_ms = initTimer.elapsed();
798 
799  while (!error) // fake loop for easy break
800  {
801  // we display init. timing only if no error occurred!
802  if (app)
803  app->dispToConsole(
804  QString("[M3C2] Initialization & normal computation: %1 s.")
805  .arg(initTime_ms / 1000.0, 0, 'f', 3),
807 
808  QElapsedTimer distCompTimer;
809  distCompTimer.start();
810 
811  // we are either in vertical mode or we have as many normals as core
812  // points
813  unsigned corePointCount = s_M3C2Params.corePoints->size();
814  assert(normMode == qM3C2Normals::VERT_MODE ||
816  corePointCount == s_M3C2Params.coreNormals->currentSize()));
817 
818  pDlg.reset();
819  cloudViewer::NormalizedProgress nProgress(&pDlg, corePointCount);
820  pDlg.setMethodTitle(QObject::tr("M3C2 Distances Computation"));
821  pDlg.setInfo(QObject::tr("Core points: %1").arg(corePointCount));
822  pDlg.start();
824 
825  // allocate distances SF
828  if (!s_M3C2Params.m3c2DistSF->resizeSafe(corePointCount, true,
829  NAN_VALUE)) {
830  errorMessage = "Failed to allocate memory for distance values!";
831  error = true;
832  break;
833  }
834  // allocate dist. uncertainty SF
838  if (!s_M3C2Params.distUncertaintySF->resizeSafe(corePointCount, true,
839  NAN_VALUE)) {
840  errorMessage =
841  "Failed to allocate memory for dist. uncertainty values!";
842  error = true;
843  break;
844  }
845  // allocate change significance SF
848  if (!s_M3C2Params.sigChangeSF->resizeSafe(corePointCount, true,
849  SCALAR_ZERO)) {
850  if (app)
851  app->dispToConsole(
852  "Failed to allocate memory for change significance "
853  "values!",
857  // no need to stop just for this SF!
858  // error = true;
859  // break;
860  }
861 
862  if (dlg.exportStdDevInfoCheckBox->isChecked()) {
863  QString prefix("STD");
865  prefix = "SigmaN";
866  } else if (s_M3C2Params.useMedian) {
867  prefix = "IQR";
868  }
869  // allocate cloud #1 std. dev. SF
870  QString stdDevSFName1 = QString(STD_DEV_CLOUD1_SF_NAME).arg(prefix);
872  new ccScalarField(qPrintable(stdDevSFName1));
874  if (!s_M3C2Params.stdDevCloud1SF->resizeSafe(corePointCount, true,
875  NAN_VALUE)) {
876  if (app)
877  app->dispToConsole(
878  "Failed to allocate memory for cloud #1 std. dev. "
879  "values!",
883  }
884  // allocate cloud #2 std. dev. SF
885  QString stdDevSFName2 = QString(STD_DEV_CLOUD2_SF_NAME).arg(prefix);
887  new ccScalarField(qPrintable(stdDevSFName2));
889  if (!s_M3C2Params.stdDevCloud2SF->resizeSafe(corePointCount, true,
890  NAN_VALUE)) {
891  if (app)
892  app->dispToConsole(
893  "Failed to allocate memory for cloud #2 std. dev. "
894  "values!",
898  }
899  }
900  if (dlg.exportDensityAtProjScaleCheckBox->isChecked()) {
901  // allocate cloud #1 density SF
905  if (!s_M3C2Params.densityCloud1SF->resizeSafe(corePointCount, true,
906  NAN_VALUE)) {
907  if (app)
908  app->dispToConsole(
909  "Failed to allocate memory for cloud #1 density "
910  "values!",
914  }
915  // allocate cloud #2 density SF
919  if (!s_M3C2Params.densityCloud2SF->resizeSafe(corePointCount, true,
920  NAN_VALUE)) {
921  if (app)
922  app->dispToConsole(
923  "Failed to allocate memory for cloud #2 density "
924  "values!",
928  }
929  }
930 
931  // get best levels for neighbourhood extraction on both octrees
933 
936  ->findBestLevelForAGivenNeighbourhoodSizeExtraction(
937  static_cast<PointCoordinateType>(
938  2.5 *
940  .projectionRadius)); // 2.5 =
941  // empirical!
942  if (app)
943  app->dispToConsole(
944  QString("[M3C2] Working subdivision level (cloud #1): %1")
945  .arg(s_M3C2Params.level1),
947 
950  ->findBestLevelForAGivenNeighbourhoodSizeExtraction(
951  static_cast<PointCoordinateType>(
952  2.5 *
954  .projectionRadius)); // 2.5 =
955  // empirical!
956  if (app)
957  app->dispToConsole(
958  QString("[M3C2] Working subdivision level (cloud #2): %1")
959  .arg(s_M3C2Params.level2),
961 
962  // other options
968  ->resizeTheNormsTable()) // resize because we will 'set'
969  // the normal in
970  // ComputeM3C2DistForPoint
971  {
972  if (app)
973  app->dispToConsole(
974  "Failed to allocate memory for exporting normals!",
976  s_M3C2Params.exportNormal = false;
977  }
980 
981  // compute distances
982  {
983  std::vector<unsigned> pointIndexes;
984  bool useParallelStrategy = true;
985 #ifdef _DEBUG
986  useParallelStrategy = false;
987 #endif
988  if (useParallelStrategy) {
989  try {
990  pointIndexes.resize(corePointCount);
991  } catch (const std::bad_alloc&) {
992  // not enough memory
993  useParallelStrategy = false;
994  }
995  }
996 
997  if (useParallelStrategy) {
998  for (unsigned i = 0; i < corePointCount; ++i) {
999  pointIndexes[i] = i;
1000  }
1001 
1002  if (maxThreadCount == 0) {
1003  maxThreadCount = QThread::idealThreadCount();
1004  }
1005  assert(maxThreadCount > 0 &&
1006  maxThreadCount <= QThread::idealThreadCount());
1007  QThreadPool::globalInstance()->setMaxThreadCount(
1008  maxThreadCount);
1009  QtConcurrent::blockingMap(pointIndexes,
1011  } else {
1012  // manually call the static per-point method!
1013  for (unsigned i = 0; i < corePointCount; ++i) {
1015  }
1016  }
1017  }
1018 
1020  errorMessage = "Process canceled by user!";
1021  error = true;
1022  } else {
1023  qint64 distTime_ms = distCompTimer.elapsed();
1024  // we display init. timing only if no error occurred!
1025  if (app)
1026  app->dispToConsole(
1027  QString("[M3C2] Distances computation: %1 s.")
1028  .arg(static_cast<double>(distTime_ms) / 1000.0,
1029  0, 'f', 3),
1031  }
1032 
1033  s_M3C2Params.nProgress = 0;
1034 
1035  break; // to break from fake loop
1036  }
1037 
1038  // associate scalar fields to the output cloud
1039  //(use reverse order so as to get the index of
1040  // the most important one at the end)
1041  if (!error) {
1043  int sfIdx = -1;
1044 
1045  // normal scales
1046  if (normalScaleSF) {
1047  normalScaleSF->computeMinAndMax();
1048  // in case the output cloud is the original cloud, we must remove
1049  // the former SF
1051  normalScaleSF->getName());
1052  sfIdx = s_M3C2Params.outputCloud->addScalarField(normalScaleSF);
1053  }
1054 
1055  // add clouds' density SFs to output cloud
1058  // in case the output cloud is the original cloud, we must remove
1059  // the former SF
1064  }
1067  // in case the output cloud is the original cloud, we must remove
1068  // the former SF
1073  }
1074 
1075  // add clouds' std. dev. SFs to output cloud
1078  // in case the output cloud is the original cloud, we must remove
1079  // the former SF
1084  }
1086  // add cloud #2 std. dev. SF to output cloud
1088  // in case the output cloud is the original cloud, we must remove
1089  // the former SF
1094  }
1095 
1096  if (s_M3C2Params.sigChangeSF) {
1097  // add significance SF to output cloud
1100  // in case the output cloud is the original cloud, we must remove
1101  // the former SF
1106  }
1107 
1109  // add dist. uncertainty SF to output cloud
1111  // in case the output cloud is the original cloud, we must remove
1112  // the former SF
1117  }
1118 
1119  if (s_M3C2Params.m3c2DistSF) {
1120  // add M3C2 distances SF to output cloud
1123  // in case the output cloud is the original cloud, we must remove
1124  // the former SF
1129  }
1130 
1132  ->invalidateBoundingBox(); // see 'const_cast<...>' in
1133  // ComputeM3C2DistForPoint ;)
1138 
1139  if (s_M3C2Params.outputCloud != cloud1 &&
1140  s_M3C2Params.outputCloud != cloud2) {
1141  s_M3C2Params.outputCloud->setName(outputName);
1142  // s_M3C2Params.outputCloud->setDisplay(s_M3C2Params.corePoints->getDisplay());
1145  if (app) {
1147  } else {
1148  // command line mode
1149  outputCloud = s_M3C2Params.outputCloud;
1150  }
1151  }
1152  } else if (s_M3C2Params.outputCloud) {
1154  delete s_M3C2Params.outputCloud;
1155  }
1157  }
1158 
1159  if (app) app->refreshAll();
1160 
1161  // release structures
1162  if (normalScaleSF) normalScaleSF->release();
1172 
1173  return !error;
1174 }
constexpr ScalarType NAN_VALUE
NaN as a ScalarType value.
Definition: CVConst.h:76
float PointCoordinateType
Type of the coordinates of a (N-D) point.
Definition: CVTypes.h:16
int count
virtual void link()
Increase counter.
Definition: CVShareable.cpp:33
virtual void release()
Decrease counter and deletes object when 0.
Definition: CVShareable.cpp:35
Array of compressed 3D normals (single index)
Type y
Definition: CVGeom.h:137
Type x
Definition: CVGeom.h:137
Type z
Definition: CVGeom.h:137
Type norm2() const
Returns vector square norm.
Definition: CVGeom.h:417
Type norm() const
Returns vector norm.
Definition: CVGeom.h:424
Type & getValue(size_t index)
Definition: ecvArray.h:100
unsigned currentSize() const
Definition: ecvArray.h:112
virtual void setVisible(bool state)
Sets entity visibility.
virtual void showNormals(bool state)
Sets normals visibility.
virtual void showSF(bool state)
Sets active scalarfield visibility.
virtual ccOctree::Shared computeOctree(cloudViewer::GenericProgressCallback *progressCb=nullptr, bool autoAddChild=true)
Computes the cloud octree.
virtual ccOctreeProxy * getOctreeProxy() const
Returns the associated octree proxy (if any)
virtual ccOctree::Shared getOctree() const
Returns the associated octree (if any)
void importParametersFrom(const ccGenericPointCloud *cloud)
Imports the parameters from another cloud.
ccHObject * getParent() const
Returns parent object.
Definition: ecvHObject.h:245
static bool UpdateNormalOrientations(ccGenericPointCloud *theCloud, NormsIndexesTableType &theNormsCodes, Orientation preferredOrientation)
Updates normals orientation based on a preferred orientation.
static const CCVector3 & GetNormal(unsigned normIndex)
Static access to ccNormalVectors::getNormal.
Orientation
'Default' orientations
@ PLUS_X
N.x always positive.
virtual QString getName() const
Returns object name.
Definition: ecvObject.h:72
virtual void setName(const QString &name)
Sets object name.
Definition: ecvObject.h:75
virtual void setEnabled(bool state)
Sets the "enabled" property.
Definition: ecvObject.h:102
Octree structure.
Definition: ecvOctree.h:27
QSharedPointer< ccOctree > Shared
Shared pointer.
Definition: ecvOctree.h:32
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
void setCurrentDisplayedScalarField(int index)
Sets the currently displayed scalar field.
bool resizeTheNormsTable()
Resizes the compressed normals array.
void setPointNormalIndex(size_t pointIndex, CompressedNormType norm)
Sets a particular point compressed normal.
void setNormsTable(NormsIndexesTableType *norms)
Sets the (compressed) normals table.
NormsIndexesTableType * normals() const
Returns pointer on compressed normals indexes table.
int addScalarField(const char *uniqueName) override
Creates a new scalar field and registers it.
bool hasNormals() const override
Returns whether normals are enabled or not.
void invalidateBoundingBox() override
Invalidates bounding box.
bool resize(unsigned numberOfPoints) override
Resizes all the active features arrays.
void deleteScalarField(int index) override
Deletes a specific scalar field.
void setPointNormal(size_t pointIndex, const CCVector3 &N)
Sets a particular point normal (shortcut)
A scalar field associated to display-related parameters.
void setMinDisplayed(ScalarType val)
Sets the minimum displayed value.
void setSymmetricalScale(bool state)
Sets whether the color scale should be symmetrical or not.
void computeMinAndMax() override
Determines the min and max values.
static ReferenceCloud * resampleCloudSpatially(GenericIndexedCloudPersist *cloud, PointCoordinateType minDistance, const SFModulationParams &modParams, DgmOctree *octree=nullptr, GenericProgressCallback *progressCb=nullptr)
Resamples a point cloud (process based on inter point distance)
std::vector< PointDescriptor > NeighboursSet
A set of neighbours.
Definition: DgmOctree.h:133
virtual unsigned size() const =0
Returns the number of points.
bool oneStep()
Increments total progress value of a single unit.
int getScalarFieldIndexByName(const char *name) const
Returns the index of a scalar field represented by its name.
ScalarField * getScalarField(int index) const
Returns a pointer to a specific scalar field.
unsigned size() const override
Definition: PointCloudTpl.h:38
const CCVector3 * getPoint(unsigned index) const override
A very simple point cloud (no point duplication)
A simple scalar field (to be associated to a point cloud)
Definition: ScalarField.h:25
ScalarType & getValue(std::size_t index)
Definition: ScalarField.h:92
void setValue(std::size_t index, ScalarType value)
Definition: ScalarField.h:96
const char * getName() const
Returns scalar field name.
Definition: ScalarField.h:43
bool resizeSafe(std::size_t count, bool initNewElements=false, ScalarType valueForNewElements=0)
Resizes memory (no exception thrown)
Definition: ScalarField.cpp:81
Main application interface (for plugins)
virtual void refreshAll(bool only2D=false, bool forceRedraw=true)=0
Redraws all GL windows that have the 'refresh' flag on.
virtual void addToDB(ccHObject *obj, bool updateZoom=false, bool autoExpandDBTree=true, bool checkDimensions=false, bool autoRedraw=true)=0
virtual void dispToConsole(QString message, ConsoleMessageLevel level=STD_CONSOLE_MESSAGE)=0
Graphical progress indicator (thread-safe)
virtual void start() override
virtual void setInfo(const char *infoStr) override
Notifies some information about the ongoing process.
virtual void setMethodTitle(const char *methodTitle) override
Notifies the algorithm title.
M3C2 plugin's main dialog.
Definition: qM3C2Dialog.h:22
qM3C2Normals::ComputationMode getNormalsComputationMode() const
Returns selected normals computation mode.
unsigned getMinPointsForStats(unsigned defaultValue=5) const
ExportOptions
Exportation options.
Definition: qM3C2Dialog.h:57
@ PROJECT_ON_CLOUD1
Definition: qM3C2Dialog.h:58
@ PROJECT_ON_CLOUD2
Definition: qM3C2Dialog.h:59
ccPointCloud * getNormalsOrientationCloud() const
Returns the cloud to be used for normals orientation (if any)
ExportOptions getExportOption() const
Returns selected export option.
ccPointCloud * getCloud2() const
Returns cloud #2.
Definition: qM3C2Dialog.h:34
ccPointCloud * getCloud1() const
Returns cloud #1.
Definition: qM3C2Dialog.h:32
ccPointCloud * getCorePointsCloud() const
Get core points cloud (if any)
int getMaxThreadCount() const
Returns the max number of threads to use.
bool keepOriginalCloud() const
ComputationMode
Normals computation mode.
Definition: qM3C2Tools.h:27
@ USE_CLOUD1_NORMALS
Definition: qM3C2Tools.h:29
@ USE_CORE_POINTS_NORMALS
Definition: qM3C2Tools.h:33
@ MULTI_SCALE_MODE
Definition: qM3C2Tools.h:30
static void MakeNormalsHorizontal(NormsIndexesTableType &normsCodes)
Makes all normals horizontal.
Definition: qM3C2Tools.cpp:429
static bool ComputeCorePointsNormals(cloudViewer::GenericIndexedCloud *corePoints, NormsIndexesTableType *corePointsNormals, ccGenericPointCloud *sourceCloud, const std::vector< PointCoordinateType > &sortedRadii, bool &invalidNormals, int maxThreadCount=0, ccScalarField *normalScale=nullptr, cloudViewer::GenericProgressCallback *progressCb=nullptr, cloudViewer::DgmOctree *inputOctree=nullptr)
Computes normals on core points only.
Definition: qM3C2Tools.cpp:179
static bool UpdateNormalOrientationsWithCloud(cloudViewer::GenericIndexedCloud *normCloud, NormsIndexesTableType &normsCodes, cloudViewer::GenericIndexedCloud *orientationCloud, int maxThreadCount=0, cloudViewer::GenericProgressCallback *progressCb=nullptr)
Definition: qM3C2Tools.cpp:346
static bool Compute(const qM3C2Dialog &dlg, QString &errorMessage, ccPointCloud *&outputCloud, bool allowDialogs, QWidget *parentWidget=nullptr, ecvMainAppInterface *app=nullptr)
static void ComputeStatistics(cloudViewer::DgmOctree::NeighboursSet &set, bool useMedian, double &meanOrMedian, double &stdDevOrIQR)
Computes statistics on a neighbors set.
Definition: qM3C2Tools.cpp:487
__host__ __device__ float2 fabs(float2 v)
Definition: cutil_math.h:1254
int max(int a, int b)
Definition: cutil_math.h:48
static double dist(double x1, double y1, double x2, double y2)
Definition: lsd.c:207
static void error(char *msg)
Definition: lsd.c:159
cloudViewer::NormalizedProgress * nProgress
ccGenericPointCloud * sourceCloud
static const char DENSITY_CLOUD2_SF_NAME[]
static ScalarType SCALAR_ONE
void ComputeM3C2DistForPoint(unsigned index)
static const char SIG_CHANGE_SF_NAME[]
static const char STD_DEV_CLOUD2_SF_NAME[]
static double ComputePMUncertainty(cloudViewer::DgmOctree::NeighboursSet &set, const CCVector3 &N, const PrecisionMaps &PM)
static const char STD_DEV_CLOUD1_SF_NAME[]
static const char DENSITY_CLOUD1_SF_NAME[]
static const char NORMAL_SCALE_SF_NAME[]
static const char M3C2_DIST_SF_NAME[]
Default name for M3C2 scalar fields.
static ScalarType SCALAR_ZERO
static void RemoveScalarField(ccPointCloud *cloud, const char sfName[])
static M3C2Params s_M3C2Params
static const char DIST_UNCERTAINTY_SF_NAME[]
ccScalarField * normalScale
Definition: qM3C2Tools.cpp:44
std::vector< PointCoordinateType > radii
Definition: qM3C2Tools.cpp:42
cloudViewer::GenericIndexedCloud * orientationCloud
Definition: qM3C2Tools.cpp:301
bool invalidNormals
Definition: qM3C2Tools.cpp:45
ccPointCloud * outputCloud
bool onlyPositiveSearch
NormsIndexesTableType * coreNormals
ccOctree::Shared cloud2Octree
bool keepOriginalCloud
ccScalarField * distUncertaintySF
double registrationRms
bool computeConfidence
ccScalarField * densityCloud1SF
bool usePrecisionMaps
PrecisionMaps cloud1PM
bool progressiveSearch
unsigned minPoints4Stats
qM3C2Dialog::ExportOptions exportOption
bool processCanceled
PointCoordinateType projectionDepth
ccScalarField * m3c2DistSF
unsigned char level2
cloudViewer::NormalizedProgress * nProgress
PointCoordinateType projectionRadius
PrecisionMaps cloud2PM
ccScalarField * densityCloud2SF
ccScalarField * stdDevCloud1SF
ccScalarField * sigChangeSF
ccOctree::Shared cloud1Octree
ccScalarField * stdDevCloud2SF
unsigned char level1
ccPointCloud * corePoints
cloudViewer::ScalarField * sY
bool valid() const
cloudViewer::ScalarField * sX
cloudViewer::ScalarField * sZ
Parameters for the scalar-field based modulation of a parameter.
unsigned char level
subdivision level at which to apply the extraction process
Definition: DgmOctree.h:670
PointCoordinateType maxHalfLength
Cylinder (half) length.
Definition: DgmOctree.h:666
NeighboursSet neighbours
Neighbour points falling inside the cylinder.
Definition: DgmOctree.h:668
PointCoordinateType radius
Cylinder radius.
Definition: DgmOctree.h:664
CCVector3 dir
Cylinder axis (direction)
Definition: DgmOctree.h:662
PointCoordinateType currentHalfLength
Current search depth.
Definition: DgmOctree.h:701