ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
q3DMASCTools.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 "q3DMASCTools.h"
9 
10 // Local
11 #include "ContextBasedFeature.h"
12 #include "DualCloudFeature.h"
13 #include "NeighborhoodFeature.h"
14 #include "PointFeature.h"
15 #include "ecvMainAppInterface.h"
16 
17 // qCC_io
18 #include <FileIOFilter.h>
19 // qCC_db
20 #include <ecvPointCloud.h>
21 #include <ecvScalarField.h>
22 
23 // qPDALIO
24 #include "../../../core/IO/qPDALIO/include/LASFields.h"
25 
26 // Qt
27 #include <QCoreApplication>
28 #include <QDir>
29 #include <QFile>
30 #include <QFileInfo>
31 #include <QMutex>
32 
33 // Qt5/Qt6 Compatibility
34 #include <QtCompat.h>
35 // system
36 #include <assert.h>
37 
38 #if defined(_OPENMP)
39 #include <omp.h>
40 #endif
41 using namespace masc;
42 
44  const Feature::Set& features,
45  const QString corePointsRole,
46  const masc::Classifier& classifier,
47  QWidget* parent /*=nullptr*/) {
48  // first save the classifier data (same base filename but with the yaml
49  // extension)
50  QFileInfo fi(filename);
51  QString yamlFilename = fi.baseName() + ".yaml";
52  QString yamlAbsoluteFilename =
53  fi.absoluteDir().absoluteFilePath(yamlFilename);
54  if (!classifier.toFile(yamlAbsoluteFilename, parent)) {
55  CVLog::Error("Failed to save the classifier data");
56  return false;
57  }
58 
59  QFile file(filename);
60  if (!file.open(QFile::Text | QFile::WriteOnly)) {
62  QString("Can't open file '%1' for writing").arg(filename));
63  return false;
64  }
65 
66  QTextStream stream(&file);
67 
68  stream << "# 3DMASC classifier file" << QtCompat::endl;
69  stream << "classifier: " << yamlFilename << QtCompat::endl;
70 
71  // look for all clouds (labels)
72  QList<QString> cloudLabels;
73  for (Feature::Shared f : features) {
74  if (f->cloud1 && !cloudLabels.contains(f->cloud1Label))
75  cloudLabels.push_back(f->cloud1Label);
76  if (f->cloud2 && !cloudLabels.contains(f->cloud2Label))
77  cloudLabels.push_back(f->cloud2Label);
78  }
79  if (!corePointsRole.isEmpty() && !cloudLabels.contains(corePointsRole)) {
80  cloudLabels.push_back(corePointsRole);
81  }
82 
83  stream << "# Clouds (roles)" << QtCompat::endl;
84  for (const QString& label : cloudLabels) {
85  stream << "cloud: " << label << QtCompat::endl;
86  }
87 
88  if (!corePointsRole.isEmpty()) {
89  stream << "# Core points (classified role)" << QtCompat::endl;
90  stream << "core_points: " << corePointsRole << QtCompat::endl;
91  }
92 
93  stream << "# Features" << QtCompat::endl;
94  for (Feature::Shared f : features) {
95  stream << "feature: " << f->toString() << QtCompat::endl;
96  }
97 
98  return true;
99 }
100 
102  QList<QString>& labels,
103  QString& corePointsLabel,
104  bool& filenamesSpecified,
105  QMap<QString, QString>& rolesAndNames) {
106  // just in case
107  corePointsLabel.clear();
108  labels.clear();
109  rolesAndNames.clear();
110 
111  QFile file(filename);
112  if (!file.open(QFile::Text | QFile::ReadOnly)) {
113  CVLog::Warning(QString("Can't open file '%1'").arg(filename));
114  return false;
115  }
116 
117  QTextStream stream(&file);
118  int filenameCount = 0;
119  for (int lineNumber = 0;; ++lineNumber) {
120  QString line = stream.readLine();
121  if (line.isNull()) {
122  // eof
123  break;
124  }
125  ++lineNumber;
126 
127  line = line.toUpper();
128  if (line.startsWith("CLOUD:")) {
129  QString command = line.mid(6).trimmed();
130  QStringList tokens = command.split('=');
131  if (tokens.size() == 0) {
133  "Malformed file: expecting some tokens after 'cloud:' "
134  "on line #" +
135  QString::number(lineNumber));
136  return false;
137  }
138 
139  QString label = tokens.front();
140  if (labels.contains(label)) {
141  CVLog::Warning(QString("Malformed file: role '%1:' is already "
142  "defined/used on line #%2")
143  .arg(label)
144  .arg(lineNumber));
145  return false;
146  }
147  labels.push_back(label);
148  rolesAndNames[label] = tokens.back();
149 
150  if (tokens.size() > 1) ++filenameCount;
151  } else if (line.startsWith("CORE_POINTS:")) {
152  if (!corePointsLabel.isEmpty()) {
153  // core points defined multiple times?!
154  continue;
155  }
156  QString command = line.mid(12);
157  QStringList tokens = command.split('_');
158  if (tokens.empty()) {
160  "Malformed file: expecting tokens after 'core_points:' "
161  "on line #" +
162  QString::number(lineNumber));
163  return false;
164  }
165  corePointsLabel = tokens[0].trimmed();
166  }
167  }
168 
169  filenamesSpecified = (filenameCount > 0 && filenameCount == labels.size());
170 
171  return true;
172 }
173 
174 bool CheckFeatureUnicity(std::vector<Feature::Shared>& rawFeatures,
175  Feature::Shared feature) {
176  if (!feature) return false;
177 
178  // check that the feature does not exists already!
179  for (const auto& feat : rawFeatures) {
180  if (feat->toString() == feature->toString()) {
181  return false;
182  }
183  }
184  return true;
185 }
186 
187 static bool CreateFeaturesFromCommand(const QString& command,
188  QString corePointsRole,
189  int lineNumber,
190  const Tools::NamedClouds& clouds,
191  std::vector<Feature::Shared>& rawFeatures,
192  std::vector<double>& scales) {
193  QStringList tokens = command.split('_');
194  if (tokens.empty()) {
196  "Malformed file: expecting at least one token after 'feature:' "
197  "on line #" +
198  QString::number(lineNumber));
199  return false;
200  }
201 
202  Feature::Shared feature;
203 
204  // read the type
205  QString typeStr = tokens[0].trimmed().toUpper();
206  {
207  for (int iteration = 0; iteration < 1;
208  ++iteration) // fake loop for easy break
209  {
210  PointFeature::PointFeatureType pointFeatureType =
212  if (pointFeatureType != PointFeature::Invalid) {
213  // we have a point feature
214  PointFeature* pointFeature = new PointFeature(pointFeatureType);
215 
216  // specific case: 'SF'
217  if (pointFeatureType == PointFeature::SF) {
218  QString sfIndexStr = typeStr.mid(2);
219  bool ok = true;
220  int sfIndex = sfIndexStr.toInt(&ok);
221  if (!ok) {
223  QString("Malformed file: expecting a valid "
224  "integer value after 'SF' on line #%1")
225  .arg(lineNumber));
226  delete pointFeature;
227  return false;
228  }
229  pointFeature->sourceSFIndex = sfIndex;
230  }
231 
232  feature.reset(pointFeature);
233  break;
234  }
235 
237  neighborhoodFeatureType =
239  if (neighborhoodFeatureType != NeighborhoodFeature::Invalid) {
240  // we have a neighborhood feature
241  feature = NeighborhoodFeature::Shared(
242  new NeighborhoodFeature(neighborhoodFeatureType));
243  break;
244  }
245 
247  contextBasedFeatureType =
249  if (contextBasedFeatureType != ContextBasedFeature::Invalid) {
250  // we have a context-based feature
251  QString featureStr =
252  ContextBasedFeature::ToString(contextBasedFeatureType);
253  int kNN = 1;
254  if (featureStr.length() < typeStr.length()) {
255  bool ok = false;
256  kNN = typeStr.mid(featureStr.length()).toInt(&ok);
257  if (!ok || kNN <= 0) {
258  CVLog::Warning(QString("Malformed file: expecting a "
259  "valid and positive number "
260  "after '%1' on line #%2")
261  .arg(featureStr)
262  .arg(lineNumber));
263  return false;
264  }
265  }
266  feature = ContextBasedFeature::Shared(
267  new ContextBasedFeature(contextBasedFeatureType, kNN));
268  break;
269  }
270 
271  DualCloudFeature::DualCloudFeatureType dualCloudFeatureType =
273  if (dualCloudFeatureType != DualCloudFeature::Invalid) {
274  // we have a dual cloud feature
275  feature = DualCloudFeature::Shared(
276  new DualCloudFeature(dualCloudFeatureType));
277  break;
278  }
279 
280  if (!feature) {
281  CVLog::Warning(QString("Malformed file: unrecognized token "
282  "'%1' after 'feature:' on line #%2")
283  .arg(typeStr)
284  .arg(lineNumber));
285  return false;
286  }
287  }
288  }
289  assert(feature);
290 
291  // read the scales
292  bool useAllScales = false;
293  {
294  QString scaleStr = tokens[1].toUpper();
295  if (!scaleStr.startsWith("SC")) {
296  CVLog::Warning(QString("Malformed file: unrecognized token '%1' "
297  "(expecting the scale descriptor 'SC...' on "
298  "line #%2")
299  .arg(typeStr)
300  .arg(lineNumber));
301  return false;
302  }
303 
304  if (scaleStr == "SC0") {
305  // no scale
306  } else if (scaleStr == "SCX") {
307  // all scales
308  useAllScales = true;
309  } else {
310  // read the specific scale value
311  bool ok = true;
312  feature->scale = scaleStr.mid(2).toDouble(&ok);
313  if (!ok) {
314  CVLog::Warning(QString("Malformed file: expecting a valid "
315  "number after 'SC:' on line #%1")
316  .arg(lineNumber));
317  return false;
318  }
319  }
320  }
321 
322  // process the next tokens (may not be ordered)
323  int cloudCount = 0;
324  bool statDefined = false;
325  bool mathDefined = false;
326  bool contextBasedFeatureDeprecatedSyntax = false;
327  for (int i = 2; i < tokens.size(); ++i) {
328  QString token = tokens[i].trimmed().toUpper();
329 
330  // is the token a 'stat' one?
331  if (!statDefined) {
332  if (token == "MEAN") {
333  feature->stat = Feature::MEAN;
334  statDefined = true;
335  } else if (token == "MODE") {
336  feature->stat = Feature::MODE;
337  statDefined = true;
338  } else if (token == "MEDIAN") {
339  feature->stat = Feature::MEDIAN;
340  statDefined = true;
341  } else if (token == "STD") {
342  feature->stat = Feature::STD;
343  statDefined = true;
344  } else if (token == "RANGE") {
345  feature->stat = Feature::RANGE;
346  statDefined = true;
347  } else if (token == "SKEW") {
348  feature->stat = Feature::SKEW;
349  statDefined = true;
350  }
351 
352  if (statDefined) {
353  continue;
354  }
355  }
356 
357  // is the token a cloud name?
358  if (cloudCount < 2) {
359  bool cloudNameMatches = false;
360  for (QMap<QString, ccPointCloud*>::const_iterator it =
361  clouds.begin();
362  it != clouds.end(); ++it) {
363  QString key = it.key().toUpper();
364  if (key == token) {
365  if (cloudCount == 0) {
366  feature->cloud1 = it.value();
367  feature->cloud1Label = key;
368 
369  if (feature &&
370  feature->getType() ==
372  // only one cloud is necessary for context based
373  // features the class should be just after the cloud
374  // name in the regular syntax
375  if (i + 1 < tokens.size()) {
376  bool ok = false;
377  int classLabel = tokens[i + 1].toInt(&ok);
378  if (!ok) {
379  contextBasedFeatureDeprecatedSyntax =
380  true; // let's try the deprecated
381  // syntax
382  } else {
383  qSharedPointerCast<ContextBasedFeature>(
384  feature)
385  ->ctxClassLabel = classLabel;
386  ++i;
387  }
388  } else {
390  QString("Malformed context based "
391  "features at line %1")
392  .arg(lineNumber));
393  return false;
394  }
395  }
396 
397  } else if (cloudCount == 1) {
398  if (contextBasedFeatureDeprecatedSyntax) {
399  feature->cloud1 = it.value();
400  feature->cloud1Label = key;
401  } else {
402  feature->cloud2 = it.value();
403  feature->cloud2Label = key;
404  }
405 
406  if (feature &&
407  feature->getType() ==
409  // this is the DEPRECATED syntax for the context
410  // based feature the class is just after the cloud
411  // name
412  if (i + 1 < tokens.size()) {
413  bool ok = false;
414  int classLabel = tokens[i + 1].toInt(&ok);
415  if (!ok) {
417  QString("ContextBasedFeature: "
418  "expecting a class number "
419  "after the context cloud "
420  "'%1' on line #%2")
421  .arg(token)
422  .arg(lineNumber));
423  return false;
424  } else {
426  "ContextBasedFeature: you are "
427  "using the DEPRECATED syntax, the "
428  "feature should contain only one "
429  "cloud, as in DZ1_SC0_CTX_10)");
430  qSharedPointerCast<ContextBasedFeature>(
431  feature)
432  ->ctxClassLabel = classLabel;
433  ++i;
434  }
435  } else {
437  QString("Malformed context based "
438  "features at line %1")
439  .arg(lineNumber));
440  return false;
441  }
442  }
443  } else {
444  // we can't fall here
445  assert(false);
446  }
447  ++cloudCount;
448  cloudNameMatches = true;
449  break;
450  }
451  }
452 
453  if (cloudNameMatches) {
454  continue;
455  }
456  }
457 
458  // is the token a 'math' one?
459  if (!mathDefined) {
460  if (token == "MINUS") {
461  feature->op = Feature::MINUS;
462  mathDefined = true;
463  } else if (token == "PLUS") {
464  feature->op = Feature::PLUS;
465  mathDefined = true;
466  } else if (token == "DIVIDE") {
467  feature->op = Feature::DIVIDE;
468  mathDefined = true;
469  } else if (token == "MULTIPLY") {
470  feature->op = Feature::MULTIPLY;
471  mathDefined = true;
472  }
473 
474  if (mathDefined) {
475  continue;
476  }
477  }
478 
479  // is the token a 'context' descriptor?
480  // if (feature->getType() == Feature::Type::ContextBasedFeature &&
481  // token.startsWith("CTX"))
482  //{
483  // //read the context label
484  // QString ctxLabelStr = token.mid(3);
485  // bool ok = true;
486  // int ctxLabel = ctxLabelStr.toInt(&ok);
487  // if (!ok)
488  // {
489  // CVLog::Warning(QString("Malformed file: expecting a
490  // valid integer value after 'CTX' on line #%1").arg(lineNumber));
491  // return false;
492  // }
493  // static_cast<ContextBasedFeature*>(feature.data())->ctxClassLabel
494  //= ctxLabel; continue;
495  // }
496 
497  // if we are here, it means we couldn't find a correspondance for the
498  // current token
499  CVLog::Warning(QString("Malformed file: unrecognized or unexpected "
500  "token '%1' on line #%2")
501  .arg(token)
502  .arg(lineNumber));
503  return false;
504  }
505 
506  // now create the various versions of rules (if any)
507  if (useAllScales) {
508  if (scales.empty()) {
510  "Malformed file: 'SCx' token used while no scale is "
511  "defined" +
512  QString(" (line %1)").arg(lineNumber));
513  return false;
514  }
515  feature->scale = scales.front();
516 
517  // we will duplicate the original feature AFTER having checked its
518  // consistency!
519  }
520 
521  // now check the consistency of the rule
522  QString errorMessage;
523  if (!feature->checkValidity(corePointsRole, errorMessage)) {
524  CVLog::Warning("Malformed feature: " + errorMessage +
525  QString(" (line %1)").arg(lineNumber));
526  return false;
527  }
528 
529  if (!CheckFeatureUnicity(
530  rawFeatures,
531  feature)) // check that the feature does not exists already!
532  {
533  CVLog::Warning("[3DMASC] duplicated feature " + feature->toString() +
534  ", check your parameter file");
535  return false;
536  } else {
537  // save the feature
538  rawFeatures.push_back(feature);
539  }
540 
541  if (useAllScales) {
542  for (size_t i = 1; i < scales.size(); ++i) {
543  // copy the original rule
544  Feature::Shared newFeature = feature->clone();
545  newFeature->scale = scales.at(i);
546 
547  // as we only change the scale value, all the duplicated features
548  // should be valid
549  assert(newFeature->checkValidity(corePointsRole, errorMessage));
550 
551  if (!CheckFeatureUnicity(rawFeatures,
552  newFeature)) // check that the feature
553  // does not exists already!
554  {
555  CVLog::Warning("[3DMASC] duplicated feature " +
556  newFeature->toString() +
557  ", check your parameter file");
558  return false;
559  } else {
560  // save the feature
561  rawFeatures.push_back(newFeature);
562  }
563  }
564  }
565 
566  return true;
567 }
568 
569 static bool ReadScales(const QString& command,
570  std::vector<double>& scales,
571  int lineNumber) {
572  assert(scales.empty());
573 
574  QStringList tokens = command.split(';');
575  if (tokens.empty()) {
577  "Malformed file: expecting at least one token after 'scales:' "
578  "on line #" +
579  QString::number(lineNumber));
580  return false;
581  }
582 
583  for (const QString& token : tokens) {
584  if (token.contains(':')) {
585  // it's probably a range
586  QStringList subTokens = token.trimmed().split(':');
587  if (subTokens.size() != 3) {
588  CVLog::Warning(QString("Malformed file: expecting 3 tokens for "
589  "a range of scales (%1)")
590  .arg(token));
591  return false;
592  }
593  bool ok[3] = {true, true, true};
594  double start = subTokens[0].trimmed().toDouble(ok);
595  double step = subTokens[1].toDouble(ok + 1);
596  double stop = subTokens[2].toDouble(ok + 2);
597  if (!ok[0] || !ok[1] || !ok[2]) {
598  CVLog::Warning(QString("Malformed file: invalid values in "
599  "scales range (%1) on line #%2")
600  .arg(token)
601  .arg(lineNumber));
602  return false;
603  }
604  if (stop < start || step <= 1.0 - 6) {
605  CVLog::Warning(QString("Malformed file: invalid range (%1) on "
606  "line #%2")
607  .arg(token)
608  .arg(lineNumber));
609  return false;
610  }
611 
612  for (double v = start; v <= stop + 1.0e-6; v += step) {
613  scales.push_back(v);
614  }
615  } else {
616  bool ok = true;
617  double v = token.trimmed().toDouble(&ok);
618  if (!ok) {
619  CVLog::Warning(QString("Malformed file: invalid scale value "
620  "(%1) on line #%2")
621  .arg(token)
622  .arg(lineNumber));
623  return false;
624  }
625  scales.push_back(v);
626  }
627  }
628 
629  scales.shrink_to_fit();
630  return true;
631 }
632 
633 static bool ReadCorePoints(const QString& command,
634  const Tools::NamedClouds& clouds,
636  int lineNumber) {
637  QStringList tokens = command.split('_');
638  if (tokens.empty()) {
640  "Malformed file: expecting tokens after 'core_points:' on line "
641  "#" +
642  QString::number(lineNumber));
643  return false;
644  }
645  QString pcName = tokens[0].trimmed();
646  if (!clouds.contains(pcName)) {
648  QString("Malformed file: unknown cloud '%1' on line #%2 (make "
649  "sure it is declared before the core points)")
650  .arg(pcName)
651  .arg(lineNumber));
652  return false;
653  }
654  corePoints.origin = clouds[pcName];
655  corePoints.role = pcName;
656 
657  // should we sub-sample the origin cloud?
658  if (tokens.size() > 1) {
659  if (tokens[1].toUpper() == "SS") {
660  if (tokens.size() < 3) {
662  "Malformed file: missing token after 'SS' on line #" +
663  QString::number(lineNumber));
664  return false;
665  }
666  QString options = tokens[2];
667  if (options.startsWith('R')) {
668  corePoints.selectionMethod = CorePoints::RANDOM;
669  } else if (options.startsWith('S')) {
670  corePoints.selectionMethod = CorePoints::SPATIAL;
671  } else {
673  "Malformed file: unknown option after 'SS' on line #" +
674  QString::number(lineNumber));
675  return false;
676  }
677 
678  // read the subsampling parameter (ignore the first character)
679  bool ok = false;
680  corePoints.selectionParam = options.mid(1).toDouble(&ok);
681  if (!ok) {
683  "Malformed file: expecting a number after 'SS_X' on "
684  "line #" +
685  QString::number(lineNumber));
686  return false;
687  }
688 
689  } // end of subsampling options
690  }
691 
692  return true;
693 }
694 
695 static bool ReadCloud(const QString& command,
696  Tools::NamedClouds& clouds,
697  const QDir& defaultDir,
698  int lineNumber,
699  FileIOFilter::LoadParameters& loadParameters) {
700  QStringList tokens = command.split('=');
701  if (tokens.size() != 2) {
703  "Malformed file: expecting 2 tokens after 'cloud:' on line #" +
704  QString::number(lineNumber));
705  return false;
706  }
707 
708  QString pcName = tokens[0].trimmed();
709  QString pcFilename = defaultDir.absoluteFilePath(tokens[1].trimmed());
710  // try to open the cloud
711  {
713  ccHObject* object =
714  FileIOFilter::LoadFromFile(pcFilename, loadParameters, error);
715  if (error != CC_FERR_NO_ERROR || !object) {
716  // error message already issued
717  if (object) delete object;
718  return false;
719  }
720  ccHObject::Container cloudsInFile;
721  object->filterChildren(cloudsInFile, false, CV_TYPES::POINT_CLOUD,
722  true);
723  if (cloudsInFile.empty()) {
724  CVLog::Warning("File doesn't contain a single cloud");
725  delete object;
726  return false;
727  } else if (cloudsInFile.size() > 1) {
729  "File contains more than one cloud, only the first one "
730  "will be kept");
731  }
732  ccPointCloud* pc = static_cast<ccPointCloud*>(cloudsInFile.front());
733  for (size_t i = 1; i < cloudsInFile.size(); ++i) {
734  delete cloudsInFile[i];
735  }
736  if (pc->getParent()) pc->getParent()->detachChild(pc);
737  pc->setName(pcName); // DGM: warning, may not be acceptable in the GUI
738  // version?
739  clouds.insert(pcName, pc);
740  }
741 
742  return true;
743 }
744 
746  const QString& filename,
747  Tools::NamedClouds* clouds,
748  bool cloudsAreProvided,
749  std::vector<Feature::Shared>*
750  rawFeatures /*=nullptr*/, // requires 'clouds'
751  std::vector<double>* rawScales /*=nullptr*/,
752  masc::CorePoints* corePoints /*=nullptr*/, // requires 'clouds'
753  masc::Classifier* classifier /*=nullptr*/,
754  TrainParameters* parameters /*=nullptr*/,
755  QWidget* parent /*=nullptr*/) {
756  QFileInfo fi(filename);
757  if (!fi.exists()) {
758  CVLog::Warning(QString("Can't find file '%1'").arg(filename));
759  return false;
760  }
761 
762  QFile file(filename);
763  if (!file.open(QFile::Text | QFile::ReadOnly)) {
764  CVLog::Warning(QString("Can't open file '%1'").arg(filename));
765  return false;
766  }
767 
768  // to use the same 'global shift' for multiple files
769  CCVector3d loadCoordinatesShift(0, 0, 0);
770  bool loadCoordinatesTransEnabled = false;
771  FileIOFilter::LoadParameters loadParameters;
772  if (!cloudsAreProvided) {
773  loadParameters.alwaysDisplayLoadDialog = true;
774  loadParameters.shiftHandlingMode =
776  loadParameters.coordinatesShift = &loadCoordinatesShift;
777  loadParameters.coordinatesShiftEnabled = &loadCoordinatesTransEnabled;
778  loadParameters.parentWidget = parent;
780  }
781 
782  try {
783  assert(!rawFeatures || rawFeatures->empty());
784  std::vector<double> scales;
785 
786  QTextStream stream(&file);
787  bool badFeatures = false;
788  for (int lineNumber = 1;; ++lineNumber) {
789  if (stream.atEnd()) break;
790  QString line = stream.readLine();
791  if (line.isEmpty()) {
792  continue;
793  }
794 
795  if (line.startsWith("#")) {
796  // comment
797  continue;
798  }
799 
800  // strip out the potential comment at the end of the line as well
801  int commentIndex = line.indexOf('#');
802  if (commentIndex >= 0) line = line.left(commentIndex);
803 
804  QString upperLine = line.toUpper();
805  if (upperLine.startsWith("CLASSIFIER:")) // classifier
806  {
807  if (!classifier) {
808  // no need to load the classifier
809  continue;
810  }
811  if (classifier->isValid()) {
813  "Malformed file: can't declare the classifier file "
814  "twice! (line #" +
815  QString::number(lineNumber) + ")");
816  return false;
817  }
818  QString yamlFilename = line.mid(11).trimmed();
819  QString yamlAbsoluteFilename =
820  fi.absoluteDir().absoluteFilePath(yamlFilename);
821  if (!classifier->fromFile(yamlAbsoluteFilename, parent)) {
822  CVLog::Warning("Failed to load the classifier file from " +
823  yamlAbsoluteFilename);
824  return false;
825  }
826  CVLog::Print("[3DMASC] Classifier data loaded from " +
827  yamlAbsoluteFilename);
828  } else if (upperLine.startsWith("CLOUD:")) // clouds
829  {
830  if (!clouds || cloudsAreProvided) {
831  // no need to load the clouds in this case
832  continue;
833  }
834  QString command = line.mid(6);
835  if (!ReadCloud(command, *clouds, fi.absoluteDir(), lineNumber,
836  loadParameters)) {
837  return false;
838  }
839  } else if (upperLine.startsWith("TEST:")) // test cloud
840  {
841  if (!clouds || cloudsAreProvided) {
842  // no need to load the clouds in this case
843  continue;
844  }
845  QString command = line.mid(5);
846  if (!ReadCloud("TEST=" + command, *clouds, fi.absoluteDir(),
847  lineNumber,
848  loadParameters)) // add the TEST keyword so that
849  // the cloud will be loaded as
850  // the TEST cloud
851  {
852  return false;
853  }
854  } else if (upperLine.startsWith("CORE_POINTS:")) // core points
855  {
856  if (!corePoints) {
857  // no need to load the core points
858  continue;
859  }
860  if (corePoints->origin) {
862  "Core points already defined (those declared on "
863  "line #" +
864  QString::number(lineNumber) + " will be ignored)");
865  } else {
866  QString command = line.mid(12);
867  if (clouds && !ReadCorePoints(command, *clouds, *corePoints,
868  lineNumber)) {
869  return false;
870  }
871  }
872  } else if (upperLine.startsWith("SCALES:")) // scales
873  {
874  if (!scales.empty()) {
876  "Malformed file: scales defined twice (line #" +
877  QString::number(lineNumber) + ")");
878  return false;
879  }
880 
881  QString command = line.mid(7);
882  if (!ReadScales(command, scales, lineNumber)) {
883  return false;
884  } else {
885  if (rawScales)
886  for (auto scale : scales) rawScales->push_back(scale);
887  }
888  } else if (upperLine.startsWith("FEATURE:")) // feature
889  {
890  QString command = line.mid(8);
891 
892  if (rawFeatures && clouds) {
894  command,
895  corePoints ? corePoints->role : QString(),
896  lineNumber, *clouds, *rawFeatures, scales)) {
897  // error message already issued
898  // return false;
899  badFeatures = true; // we continue as we want to get
900  // ALL the errors
901  }
902  }
903  } else if (upperLine.startsWith("PARAM_")) // parameter
904  {
905  if (parameters) // no need to actually read the parameters if
906  // the caller didn't requested them
907  {
908  QStringList tokens = upperLine.split("=");
909  if (tokens.size() != 2) {
911  QString("Line #%1: malformed parameter command "
912  "(expecting param_XXX=Y)")
913  .arg(lineNumber));
914  return false;
915  }
916  bool ok = false;
917  if (tokens[0] == "PARAM_MAX_DEPTH") {
918  parameters->rt.maxDepth = tokens[1].toInt(&ok);
919  } else if (tokens[0] == "PARAM_MAX_TREE_COUNT") {
920  parameters->rt.maxTreeCount = tokens[1].toInt(&ok);
921  } else if (tokens[0] == "PARAM_ACTIVE_VAR_COUNT") {
922  parameters->rt.activeVarCount = tokens[1].toInt(&ok);
923  } else if (tokens[0] == "PARAM_MIN_SAMPLE_COUNT") {
924  parameters->rt.minSampleCount = tokens[1].toInt(&ok);
925  } else if (tokens[0] == "PARAM_TEST_DATA_RATIO") {
926  parameters->testDataRatio = tokens[1].toFloat(&ok);
927  } else {
929  QString("Line #%1: unrecognized parameter: ")
930  .arg(lineNumber) +
931  tokens[0]);
932  }
933  if (!ok) {
934  CVLog::Warning(QString("Line #%1: invalid value for "
935  "parameter ")
936  .arg(lineNumber) +
937  tokens[0]);
938  }
939  }
940  } else {
942  QString("Line #%1: unrecognized token/command: ")
943  .arg(lineNumber) +
944  (line.length() < 10 ? line : line.left(10) + "..."));
945  return false;
946  }
947  }
948 
949  if (badFeatures) {
950  return false;
951  }
952  } catch (const std::bad_alloc&) {
953  CVLog::Warning("Not enough memory");
954  return false;
955  }
956 
957  if (rawFeatures) rawFeatures->shrink_to_fit();
958 
959  return true;
960 }
961 
963  NamedClouds& clouds,
964  Feature::Set& rawFeatures,
965  masc::Classifier& classifier,
966  QWidget* parent /*=nullptr*/) {
967  return LoadFile(filename, &clouds, true, &rawFeatures, nullptr, nullptr,
968  &classifier, nullptr, parent);
969 }
970 
972  Feature::Set& rawFeatures,
973  std::vector<double>& rawScales,
974  NamedClouds& loadedClouds,
975  TrainParameters& parameters,
976  CorePoints* corePoints /*=nullptr*/,
977  QWidget* parentWidget /*=nullptr*/) {
978  bool cloudsWereProvided = !loadedClouds.empty();
979  if (LoadFile(filename, &loadedClouds, cloudsWereProvided, &rawFeatures,
980  &rawScales, corePoints, nullptr, &parameters, parentWidget)) {
981  return true;
982  } else {
983  if (!cloudsWereProvided) {
984  // delete the already loaded clouds (if any)
985  for (NamedClouds::iterator it = loadedClouds.begin();
986  it != loadedClouds.end(); ++it)
987  delete it.value();
988  }
989  return false;
990  }
991 }
992 
994  const QString& sfName,
995  bool caseSensitive /*=true*/) {
996  if (!cloud) {
997  assert(false);
998  return nullptr;
999  }
1000  int sfIdx = -1;
1001  if (caseSensitive) {
1002  sfIdx = cloud->getScalarFieldIndexByName(qPrintable(sfName));
1003  } else {
1004  QString sfNameUpper = sfName.toUpper();
1005  for (unsigned i = 0; i < cloud->getNumberOfScalarFields(); ++i) {
1006  if (QString(cloud->getScalarField(i)->getName()).toUpper() ==
1007  sfNameUpper) {
1008  sfIdx = static_cast<int>(i);
1009  break;
1010  }
1011  }
1012  }
1013 
1014  if (sfIdx >= 0) {
1015  return cloud->getScalarField(sfIdx);
1016  } else {
1017  return nullptr;
1018  }
1019 }
1020 
1022  std::vector<double> scales;
1023  size_t featureCount = 0;
1024  QMap<double, std::vector<PointFeature::Shared>> pointFeaturesPerScale;
1025  QMap<double, std::vector<NeighborhoodFeature::Shared>>
1027  QMap<double, std::vector<ContextBasedFeature::Shared>>
1029 };
1030 
1032  const CorePoints& corePoints,
1033  Feature::Set& features,
1034  QString& errorStr,
1035  cloudViewer::GenericProgressCallback* progressCb /*=nullptr*/,
1036  SFCollector* generatedScalarFields /*=nullptr*/) {
1037  if (features.empty() || !corePoints.origin) {
1038  // invalid input parameters
1039  assert(false);
1040  return false;
1041  }
1042 
1043  // gather all the scales that need to be extracted
1044  QMap<ccPointCloud*, FeaturesAndScales> cloudsWithScaledFeatures;
1045  // and prepare the features (scalar fields, etc.) at the same time
1046  for (const Feature::Shared& feature : features) {
1047  QString errorMessage("invalid pointer");
1048  assert(!corePoints.role.isEmpty());
1049  if (!feature ||
1050  !feature->checkValidity(corePoints.role, errorMessage)) {
1051  errorStr = "Invalid rule/feature: " + errorMessage;
1052  return false;
1053  }
1054 
1055  // prepare the feature
1056  if (!feature->prepare(corePoints, errorStr, progressCb,
1057  generatedScalarFields)) {
1058  // something failed (error should be up to date)
1059  return false;
1060  }
1061 
1062  if (feature->scaled()) {
1063  try {
1064  switch (feature->getType()) {
1065  // Point features
1067  // build the scaled feature list attached to the first
1068  // cloud
1069  if (feature->cloud1 &&
1070  !feature->sf1WasAlreadyExisting) // nothing to
1071  // compute if the
1072  // scalar field
1073  // was already
1074  // there
1075  {
1076  FeaturesAndScales& fas =
1077  cloudsWithScaledFeatures[feature->cloud1];
1078  fas.pointFeaturesPerScale[feature->scale].push_back(
1079  qSharedPointerCast<PointFeature>(feature));
1080  ++fas.featureCount;
1081  if (std::find(fas.scales.begin(), fas.scales.end(),
1082  feature->scale) == fas.scales.end()) {
1083  fas.scales.push_back(feature->scale);
1084  }
1085  }
1086  // build the scaled feature list attached to the second
1087  // cloud (if any)
1088  if (feature->cloud2 &&
1089  feature->cloud2 != feature->cloud1 &&
1090  feature->op != Feature::NO_OPERATION) {
1091  if (!feature->sf1WasAlreadyExisting) // nothing to
1092  // compute if
1093  // the scalar
1094  // field was
1095  // already
1096  // there
1097  {
1098  if (!feature->sf2WasAlreadyExisting) {
1099  FeaturesAndScales& fas =
1100  cloudsWithScaledFeatures
1101  [feature->cloud2];
1102  ++fas.featureCount;
1103  fas.pointFeaturesPerScale[feature->scale]
1104  .push_back(qSharedPointerCast<
1105  PointFeature>(feature));
1106  if (std::find(fas.scales.begin(),
1107  fas.scales.end(),
1108  feature->scale) ==
1109  fas.scales.end()) {
1110  fas.scales.push_back(feature->scale);
1111  }
1112  }
1113  }
1114  }
1115  } break;
1116 
1117  // Neighborhood features
1119  // build the scaled feature list attached to the first
1120  // cloud
1121  if (feature->cloud1 &&
1122  !feature->sf1WasAlreadyExisting) // nothing to
1123  // compute if the
1124  // scalar field
1125  // was already
1126  // there
1127  {
1128  FeaturesAndScales& fas =
1129  cloudsWithScaledFeatures[feature->cloud1];
1130  fas.neighborhoodFeaturesPerScale[feature->scale]
1131  .push_back(qSharedPointerCast<
1132  NeighborhoodFeature>(feature));
1133  ++fas.featureCount;
1134  if (std::find(fas.scales.begin(), fas.scales.end(),
1135  feature->scale) == fas.scales.end()) {
1136  fas.scales.push_back(feature->scale);
1137  }
1138  }
1139 
1140  // build the scaled feature list attached to the second
1141  // cloud (if any)
1142  if (feature->cloud2 &&
1143  feature->cloud2 != feature->cloud1 &&
1144  feature->op != Feature::NO_OPERATION) {
1145  if (!feature->sf1WasAlreadyExisting) // nothing to
1146  // compute if
1147  // the scalar
1148  // field was
1149  // already
1150  // there
1151  {
1152  if (!feature->sf2WasAlreadyExisting) {
1153  FeaturesAndScales& fas =
1154  cloudsWithScaledFeatures
1155  [feature->cloud2];
1156  fas
1158  [feature->scale]
1159  .push_back(qSharedPointerCast<
1161  feature));
1162  ++fas.featureCount;
1163  if (std::find(fas.scales.begin(),
1164  fas.scales.end(),
1165  feature->scale) ==
1166  fas.scales.end()) {
1167  fas.scales.push_back(feature->scale);
1168  }
1169  }
1170  }
1171  }
1172  } break;
1173 
1174  // Context-based features
1176  // build the scaled feature list attached to the context
1177  // cloud
1178  if (feature->cloud1 &&
1179  !feature->sf1WasAlreadyExisting) // nothing to
1180  // compute if the
1181  // scalar field
1182  // was already
1183  // there
1184  {
1185  FeaturesAndScales& fas =
1186  cloudsWithScaledFeatures[feature->cloud1];
1187  fas.contextBasedFeaturesPerScale[feature->scale]
1188  .push_back(qSharedPointerCast<
1189  ContextBasedFeature>(feature));
1190  ++fas.featureCount;
1191  if (std::find(fas.scales.begin(), fas.scales.end(),
1192  feature->scale) == fas.scales.end()) {
1193  fas.scales.push_back(feature->scale);
1194  }
1195  }
1196  } break;
1197 
1198  default:
1199  assert(false);
1200  break;
1201  }
1202  } catch (const std::bad_alloc&) {
1203  errorStr = "Not enough memory";
1204  return false;
1205  }
1206  }
1207  }
1208 
1209  bool success = true;
1210 
1211  // if we have scaled features
1212  if (!cloudsWithScaledFeatures.empty()) {
1213  // for each cloud
1214  for (QMap<ccPointCloud*, FeaturesAndScales>::iterator it =
1215  cloudsWithScaledFeatures.begin();
1216  success && it != cloudsWithScaledFeatures.end(); ++it) {
1217  FeaturesAndScales& fas = it.value();
1218  ccPointCloud* sourceCloud = it.key();
1219 
1220  // sort the scales
1221  std::sort(fas.scales.begin(), fas.scales.end());
1222 
1223  // get the octree
1225  if (!octree) {
1226  CVLog::Print(QString("Computing octree of cloud %1 (%2 points)")
1227  .arg(sourceCloud->getName())
1228  .arg(sourceCloud->size()));
1229  if (progressCb) progressCb->start();
1230  QCoreApplication::processEvents();
1231  octree = sourceCloud->computeOctree(progressCb);
1232  if (!octree) {
1233  errorStr =
1234  "[Tools::PrepareFeatures] Failed to compute octree "
1235  "(not enough memory?)";
1236  return false;
1237  }
1238  }
1239 
1240  // now extract the neighborhoods from the biggest to the smallest
1241  // scale
1242  double largetScale = fas.scales.back();
1243  PointCoordinateType largestRadius =
1244  static_cast<PointCoordinateType>(
1245  largetScale / 2); // scale is the diameter!
1246  unsigned char octreeLevel =
1248  largestRadius);
1249 
1250  unsigned pointCount = corePoints.size();
1251  QString logMessage = QString("Computing %1 features on cloud %2 at "
1252  "%3 core points")
1253  .arg(fas.featureCount)
1254  .arg(sourceCloud->getName())
1255  .arg(pointCount);
1256  if (progressCb) {
1257  progressCb->setMethodTitle("Compute features");
1258  progressCb->setInfo(qPrintable(logMessage));
1259  }
1260  CVLog::Print(logMessage);
1261  cloudViewer::NormalizedProgress nProgress(progressCb, pointCount);
1262 
1263  bool cancelled = false;
1264 
1265 #ifndef _DEBUG
1266 #if defined(_OPENMP)
1267 #pragma omp parallel for num_threads(std::max(1, omp_get_max_threads() - 2))
1268 #endif
1269 #endif
1270  for (int i = 0; i < static_cast<int>(pointCount); ++i) {
1271  if (!cancelled) {
1272  QString localErrorStr;
1273  bool localSuccess = true;
1274 
1275  // spherical neighborhood extraction structure
1276  cloudViewer::DgmOctree::
1277  NearestNeighboursSphericalSearchStruct nNSS;
1278  {
1279  nNSS.level = octreeLevel;
1280  nNSS.queryPoint = *corePoints.cloud->getPoint(i);
1282  &nNSS.queryPoint, nNSS.cellPos, nNSS.level);
1283  octree->computeCellCenter(nNSS.cellPos, nNSS.level,
1284  nNSS.cellCenter);
1285  }
1286 
1287  // we extract the point's neighbors
1288  unsigned kNN =
1290  nNSS, largestRadius, true);
1291  if (kNN != 0) {
1292  nNSS.pointsInNeighbourhood.resize(kNN);
1293 
1294  // for each scale (from the largest to the smallest)
1295  for (size_t scaleIndex = 0;
1296  scaleIndex < fas.scales.size(); ++scaleIndex) {
1297  double currentScale =
1298  fas.scales[fas.scales.size() - 1 -
1299  scaleIndex]; // from the biggest
1300  // to the smallest!
1301 
1302  if (scaleIndex != 0) {
1303  double radius = currentScale /
1304  2; // scale is the diameter!
1305  double sqRadius = radius * radius;
1306  // remove the farthest points
1307  for (; kNN > 0; --kNN) {
1308  if (nNSS.pointsInNeighbourhood[kNN - 1]
1309  .squareDistd <= sqRadius) {
1310  break;
1311  }
1312  }
1313 
1314  if (kNN == 0) {
1315  // no need to go further
1316  break;
1317  }
1318  nNSS.pointsInNeighbourhood.resize(kNN);
1319  }
1320 
1321  // Point features
1322  for (PointFeature::Shared& feature :
1323  fas.pointFeaturesPerScale[currentScale]) {
1324  if (feature->cloud1 == sourceCloud &&
1325  feature->statSF1 && feature->field1 &&
1326  localSuccess) {
1327  double outputValue = 0;
1328  if (!feature->computeStat(
1329  nNSS.pointsInNeighbourhood,
1330  feature->field1, outputValue)) {
1331  // an error occurred
1332  localErrorStr =
1333  "An error occurred during the "
1334  "computation of feature " +
1335  feature->toString() +
1336  " on cloud " +
1337  feature->cloud1->getName();
1338  localSuccess = false;
1339  break;
1340  }
1341 
1342  ScalarType v1 = static_cast<ScalarType>(
1343  outputValue);
1344  feature->statSF1->setValue(i, v1);
1345  }
1346 
1347  if (feature->cloud2 == sourceCloud &&
1348  feature->statSF2 && feature->field2 &&
1349  localSuccess) {
1350  assert(feature->op !=
1352  double outputValue = 0;
1353  if (!feature->computeStat(
1354  nNSS.pointsInNeighbourhood,
1355  feature->field2, outputValue)) {
1356  // an error occurred
1357  localErrorStr =
1358  "An error occurred during the "
1359  "computation of feature " +
1360  feature->toString() +
1361  " on cloud " +
1362  feature->cloud2->getName();
1363  localSuccess = false;
1364  break;
1365  }
1366 
1367  ScalarType v2 = static_cast<ScalarType>(
1368  outputValue);
1369  feature->statSF2->setValue(i, v2);
1370  }
1371  }
1372 
1373  // Neighborhood features
1374  for (NeighborhoodFeature::Shared& feature :
1376  [currentScale]) {
1377  if (feature->cloud1 == sourceCloud &&
1378  feature->sf1 && localSuccess) {
1379  double outputValue = 0;
1380  if (!feature->computeValue(
1381  nNSS.pointsInNeighbourhood,
1382  nNSS.queryPoint, outputValue)) {
1383  // an error occurred
1384  localErrorStr =
1385  "An error occurred during the "
1386  "computation of feature " +
1387  feature->toString() +
1388  " on cloud " +
1389  feature->cloud1->getName();
1390  localSuccess = false;
1391  break;
1392  }
1393 
1394  ScalarType v1 = static_cast<ScalarType>(
1395  outputValue);
1396  feature->sf1->setValue(i, v1);
1397  }
1398 
1399  if (feature->cloud2 == sourceCloud &&
1400  feature->sf2 && localSuccess) {
1401  assert(feature->op !=
1403  double outputValue = 0;
1404  if (!feature->computeValue(
1405  nNSS.pointsInNeighbourhood,
1406  nNSS.queryPoint, outputValue)) {
1407  // an error occurred
1408  localErrorStr =
1409  "An error occurred during the "
1410  "computation of feature " +
1411  feature->toString() +
1412  " on cloud " +
1413  feature->cloud2->getName();
1414  localSuccess = false;
1415  break;
1416  }
1417 
1418  ScalarType v2 = static_cast<ScalarType>(
1419  outputValue);
1420  feature->sf2->setValue(i, v2);
1421  }
1422  }
1423 
1424  // Context-based features
1425  for (ContextBasedFeature::Shared& feature :
1427  [currentScale]) {
1428  if (feature->cloud1 == sourceCloud &&
1429  feature->sf && localSuccess) {
1430  ScalarType outputValue = 0;
1431  if (!feature->computeValue(
1432  nNSS.pointsInNeighbourhood,
1433  nNSS.queryPoint, outputValue)) {
1434  // an error occurred
1435  localErrorStr =
1436  "An error occurred during the "
1437  "computation of feature " +
1438  feature->toString() +
1439  " on cloud " +
1440  feature->cloud1->getName();
1441  localSuccess = false;
1442  break;
1443  }
1444 
1445  feature->sf->setValue(i, outputValue);
1446  }
1447  }
1448 
1449  if (!localSuccess) {
1450  localErrorStr = localErrorStr + " at scale " +
1451  QString::number(currentScale) +
1452  " on point " +
1453  QString::number(i);
1454  break;
1455  }
1456 
1457  } // for each scale
1458  }
1459 
1460  if (!localSuccess) {
1461  cancelled = true;
1462  success = false;
1463 #if defined(_OPENMP)
1464  errorStr = "Feature computation failed for point " +
1465  QString::number(i) + " (using OpenMP with " +
1466  QString::number(omp_get_num_threads()) +
1467  " threads)";
1468 #else
1469  errorStr = "Feature computation failed for point " +
1470  QString::number(i);
1471 #endif
1472  CVLog::Error(localErrorStr);
1473  }
1474 
1475  if (progressCb) {
1476  if (!cancelled) {
1477  cancelled = !nProgress.oneStep();
1478  if (cancelled) {
1479  // process cancelled by the user
1480 #if defined(_OPENMP)
1481  errorStr =
1482  "Process cancelled at point " +
1483  QString::number(i) +
1484  " (using OpenMP with " +
1485  QString::number(omp_get_num_threads()) +
1486  " threads)";
1487 #else
1488  errorStr = "Process cancelled at point " +
1489  QString::number(i);
1490 #endif
1491  CVLog::Warning(errorStr);
1492  success = false;
1493  }
1494  }
1495  }
1496  }
1497  } // for each point
1498 
1499  } // for each cloud
1500  }
1501 
1502  for (const Feature::Shared& feature : features) {
1503  // we have to 'finish' the process for scaled features
1504  if (feature->scaled() && !feature->finish(corePoints, errorStr)) {
1505  return false;
1506  }
1507  }
1508 
1509  return success;
1510 }
1511 
1513  float ratio,
1514  cloudViewer::ReferenceCloud* inRatioSubset,
1515  cloudViewer::ReferenceCloud* outRatioSubset) {
1516  if (!cloud) {
1517  CVLog::Warning("Invalid input cloud");
1518  return false;
1519  }
1520  if (!inRatioSubset || !outRatioSubset) {
1521  CVLog::Warning("Invalid input refence clouds");
1522  return false;
1523  }
1524  if (inRatioSubset->getAssociatedCloud() != cloud ||
1525  outRatioSubset->getAssociatedCloud() != cloud) {
1527  "Invalid input reference clouds (associated cloud is wrong)");
1528  return false;
1529  }
1530  if (ratio < 0.0f || ratio > 1.0f) {
1531  CVLog::Warning(QString("Invalid parameter (ratio: %1)").arg(ratio));
1532  return false;
1533  }
1534 
1535  unsigned inSampleCount =
1536  static_cast<unsigned>(floor(cloud->size() * ratio));
1537  assert(inSampleCount <= cloud->size());
1538  unsigned outSampleCount = cloud->size() - inSampleCount;
1539 
1540  // we draw the smallest population (faster)
1541  unsigned targetCount = inSampleCount;
1542  bool defaultState = false;
1543  if (outSampleCount < inSampleCount) {
1544  targetCount = outSampleCount;
1545  defaultState = true;
1546  }
1547 
1548  // reserve memory
1549  std::vector<bool> pointInsideRatio;
1550  try {
1551  pointInsideRatio.resize(cloud->size(), defaultState);
1552  } catch (const std::bad_alloc&) {
1553  CVLog::Warning("Not enough memory");
1554  return false;
1555  }
1556 
1557  if (!inRatioSubset->reserve(inSampleCount) ||
1558  !outRatioSubset->reserve(outSampleCount)) {
1559  CVLog::Warning("Not enough memory");
1560  inRatioSubset->clear();
1561  outRatioSubset->clear();
1562  return false;
1563  }
1564 
1565  // randomly choose the 'in' or 'out' indexes
1566  int randIndex = 0;
1567  unsigned randomCount = 0;
1568  while (randomCount < targetCount) {
1569  randIndex = ((randIndex + std::rand()) % cloud->size());
1570  if (pointInsideRatio[randIndex] == defaultState) {
1571  pointInsideRatio[randIndex] = !defaultState;
1572  ++randomCount;
1573  }
1574  }
1575 
1576  // now dispatch the points
1577  {
1578  for (unsigned i = 0; i < cloud->size(); ++i) {
1579  if (pointInsideRatio[i])
1580  inRatioSubset->addPointIndex(i);
1581  else
1582  outRatioSubset->addPointIndex(i);
1583  }
1584  assert(inRatioSubset->size() == inSampleCount);
1585  assert(outRatioSubset->size() == outSampleCount);
1586  }
1587 
1588  return true;
1589 }
1590 
1592  const ccPointCloud* cloud) {
1593  if (!cloud) {
1594  // invalid input cloud
1595  assert(false);
1596  return nullptr;
1597  }
1598  // look for the classification field
1599  int classifSFIdx = cloud->getScalarFieldIndexByName(
1601  [LAS_CLASSIFICATION]); // LAS_FIELD_NAMES[LAS_CLASSIFICATION]
1602  // = "Classification"
1603  if (classifSFIdx < 0) {
1604  return nullptr;
1605  }
1606  return cloud->getScalarField(classifSFIdx);
1607 }
float PointCoordinateType
Type of the coordinates of a (N-D) point.
Definition: CVTypes.h:16
std::string filename
int size
CC_FILE_ERROR
Typical I/O filter errors.
Definition: FileIOFilter.h:20
@ CC_FERR_NO_ERROR
Definition: FileIOFilter.h:21
const char LAS_FIELD_NAMES[][28]
Definition: LASFields.h:63
@ LAS_CLASSIFICATION
Definition: LASFields.h:44
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
static bool Print(const char *format,...)
Prints out a formatted message in console.
Definition: CVLog.cpp:113
static bool Error(const char *format,...)
Display an error dialog with formatted message.
Definition: CVLog.cpp:143
static void ResetSesionCounter()
static ccHObject * LoadFromFile(const QString &filename, LoadParameters &parameters, Shared filter, CC_FILE_ERROR &result)
Loads one or more entities from a file with a known filter.
SF collector.
virtual ccOctree::Shared computeOctree(cloudViewer::GenericProgressCallback *progressCb=nullptr, bool autoAddChild=true)
Computes the cloud octree.
virtual ccOctree::Shared getOctree() const
Returns the associated octree (if any)
Hierarchical CLOUDVIEWER Object.
Definition: ecvHObject.h:25
void detachChild(ccHObject *child)
Detaches a specific child.
ccHObject * getParent() const
Returns parent object.
Definition: ecvHObject.h:245
unsigned filterChildren(Container &filteredChildren, bool recursive=false, CV_CLASS_ENUM filter=CV_TYPES::OBJECT, bool strict=false) const
Collects the children corresponding to a certain pattern.
std::vector< ccHObject * > Container
Standard instances container (for children, etc.)
Definition: ecvHObject.h:337
virtual QString getName() const
Returns object name.
Definition: ecvObject.h:72
virtual void setName(const QString &name)
Sets object name.
Definition: ecvObject.h:75
QSharedPointer< ccOctree > Shared
Shared pointer.
Definition: ecvOctree.h:32
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
unsigned char findBestLevelForAGivenNeighbourhoodSizeExtraction(PointCoordinateType radius) const
Definition: DgmOctree.cpp:2664
int findNeighborsInASphereStartingFromCell(NearestNeighboursSearchStruct &nNSS, double radius, bool sortValues=true) const
Advanced form of the nearest neighbours search algorithm (in a sphere)
Definition: DgmOctree.cpp:2479
void getTheCellPosWhichIncludesThePoint(const CCVector3 *thePoint, Tuple3i &cellPos) const
Definition: DgmOctree.h:779
void computeCellCenter(CellCode code, unsigned char level, CCVector3 &center, bool isCodeTruncated=false) const
Definition: DgmOctree.h:862
virtual unsigned size() const =0
Returns the number of points.
virtual const CCVector3 * getPoint(unsigned index) const =0
Returns the ith point.
virtual void setInfo(const char *infoStr)=0
Notifies some information about the ongoing process.
virtual void setMethodTitle(const char *methodTitle)=0
Notifies the algorithm title.
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 getNumberOfScalarFields() const
Returns the number of associated (and active) scalar fields.
unsigned size() const override
Definition: PointCloudTpl.h:38
A very simple point cloud (no point duplication)
virtual bool addPointIndex(unsigned globalIndex)
Point global index insertion mechanism.
virtual GenericIndexedCloudPersist * getAssociatedCloud()
Returns the associated (source) cloud.
unsigned size() const override
Returns the number of points.
virtual void clear(bool releaseMemory=false)
Clears the cloud.
virtual bool reserve(unsigned n)
Reserves some memory for hosting the point references.
A simple scalar field (to be associated to a point cloud)
Definition: ScalarField.h:25
const char * getName() const
Returns scalar field name.
Definition: ScalarField.h:43
bool toFile(QString filename, QWidget *parentWidget=nullptr) const
Saves the classifier to file.
bool isValid() const
Returns whether the classifier is valid or not.
bool fromFile(QString filename, QWidget *parentWidget=nullptr)
Loads the classifier from file.
static bool RandomSubset(ccPointCloud *cloud, float ratio, cloudViewer::ReferenceCloud *inRatioSubset, cloudViewer::ReferenceCloud *outRatioSubset)
static bool LoadFile(const QString &filename, Tools::NamedClouds *clouds, bool cloudsAreProvided, std::vector< Feature::Shared > *rawFeatures=nullptr, std::vector< double > *rawScales=nullptr, masc::CorePoints *corePoints=nullptr, masc::Classifier *classifier=nullptr, TrainParameters *parameters=nullptr, QWidget *parent=nullptr)
static bool LoadClassifier(QString filename, NamedClouds &clouds, Feature::Set &rawFeatures, masc::Classifier &classifier, QWidget *parent=nullptr)
static cloudViewer::ScalarField * RetrieveSF(const ccPointCloud *cloud, const QString &sfName, bool caseSensitive=true)
static bool PrepareFeatures(const CorePoints &corePoints, Feature::Set &features, QString &error, cloudViewer::GenericProgressCallback *progressCb=nullptr, SFCollector *generatedScalarFields=nullptr)
static bool LoadClassifierCloudLabels(QString filename, QList< QString > &labels, QString &corePointsLabel, bool &filenamesSpecified, QMap< QString, QString > &rolesAndNames)
static bool LoadTrainingFile(QString filename, Feature::Set &rawFeatures, std::vector< double > &scales, NamedClouds &loadedClouds, TrainParameters &parameters, CorePoints *corePoints=nullptr, QWidget *parent=nullptr)
static cloudViewer::ScalarField * GetClassificationSF(const ccPointCloud *cloud)
Helper: returns the classification SF associated to a cloud (if any)
static bool SaveClassifier(QString filename, const Feature::Set &features, const QString corePointsRole, const masc::Classifier &classifier, QWidget *parent=nullptr)
QMap< QString, ccPointCloud * > NamedClouds
Definition: q3DMASCTools.h:43
static void error(char *msg)
Definition: lsd.c:159
@ POINT_CLOUD
Definition: CVTypes.h:104
QTextStream & endl(QTextStream &stream)
Definition: QtCompat.h:718
MiniVec< float, N > floor(const MiniVec< float, N > &a)
Definition: MiniVec.h:75
3DMASC classifier
bool CheckFeatureUnicity(std::vector< Feature::Shared > &rawFeatures, Feature::Shared feature)
static bool CreateFeaturesFromCommand(const QString &command, QString corePointsRole, int lineNumber, const Tools::NamedClouds &clouds, std::vector< Feature::Shared > &rawFeatures, std::vector< double > &scales)
static bool ReadCloud(const QString &command, Tools::NamedClouds &clouds, const QDir &defaultDir, int lineNumber, FileIOFilter::LoadParameters &loadParameters)
static bool ReadCorePoints(const QString &command, const Tools::NamedClouds &clouds, masc::CorePoints &corePoints, int lineNumber)
static bool ReadScales(const QString &command, std::vector< double > &scales, int lineNumber)
cloudViewer::NormalizedProgress * nProgress
cloudViewer::DgmOctree * octree
cloudViewer::GenericIndexedCloud * corePoints
unsigned char octreeLevel
ccGenericPointCloud * sourceCloud
QMap< double, std::vector< NeighborhoodFeature::Shared > > neighborhoodFeaturesPerScale
std::vector< double > scales
QMap< double, std::vector< ContextBasedFeature::Shared > > contextBasedFeaturesPerScale
QMap< double, std::vector< PointFeature::Shared > > pointFeaturesPerScale
Generic loading parameters.
Definition: FileIOFilter.h:51
CCVector3d * coordinatesShift
If applicable, applied shift on load (optional)
Definition: FileIOFilter.h:71
ecvGlobalShiftManager::Mode shiftHandlingMode
How to handle big coordinates.
Definition: FileIOFilter.h:64
QWidget * parentWidget
Parent widget (if any)
Definition: FileIOFilter.h:78
bool * coordinatesShiftEnabled
Whether shift on load has been applied after loading (optional)
Definition: FileIOFilter.h:69
unsigned char level
Level of subdivision of the octree at which to start the search.
Definition: DgmOctree.h:171
Tuple3i cellPos
Position in the octree of the cell including the query point.
Definition: DgmOctree.h:184
CCVector3 cellCenter
Coordinates of the center of the cell including the query point.
Definition: DgmOctree.h:189
Context-based feature.
static QString ToString(ContextBasedFeatureType type)
static ContextBasedFeatureType FromUpperString(const QString &token)
QSharedPointer< ContextBasedFeature > Shared
Core points descriptor.
Definition: CorePoints.h:39
Dual-cloud feature.
static DualCloudFeatureType FromUpperString(const QString &token)
std::vector< Shared > Set
Set of features.
QSharedPointer< Feature > Shared
Shared type.
Neighborhood-based feature.
QSharedPointer< NeighborhoodFeature > Shared
static NeighborhoodFeatureType FromUpperString(const QString &token)
Point feature.
Definition: PointFeature.h:36
static PointFeatureType FromUpperString(const QString &token)
Definition: PointFeature.h:107
QSharedPointer< PointFeature > Shared
Definition: PointFeature.h:38
int sourceSFIndex
Source scalar field index (if the feature source is 'ScalarField')
Definition: PointFeature.h:217
RandomTreesParams rt
Definition: Parameters.h:41