ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
q3DMASC.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 "q3DMASC.h"
9 
10 // local
11 #include "q3DMASCClassifier.h"
12 #include "q3DMASCCommands.h"
14 #include "q3DMASCTools.h"
15 #include "qClassify3DMASCDialog.h"
16 #include "qTrain3DMASCDialog.h"
17 
18 // qCC_db
19 #include <QtCompat.h>
20 #include <ecvPointCloud.h>
21 #include <ecvProgressDialog.h>
22 
23 // Qt
24 #include <QApplication>
25 #include <QFileDialog>
26 #include <QMessageBox>
27 #include <QSettings>
28 #include <QtCore>
29 #include <QtGui>
30 
31 q3DMASCPlugin::q3DMASCPlugin(QObject* parent /*=0*/)
32  : QObject(parent),
33  ccStdPluginInterface(":/CC/plugin/q3DMASCPlugin/info.json"),
34  m_classifyAction(0),
35  m_trainAction(0) {}
36 
38  const ccHObject::Container& selectedEntities) {
39  if (m_classifyAction) {
40  // classification: only one point cloud
41  // m_classifyAction->setEnabled(selectedEntities.size() == 1 &&
42  // selectedEntities[0]->isA(CV_TYPES::POINT_CLOUD));
43  m_classifyAction->setEnabled(
45  }
46 
47  if (m_trainAction) {
48  // m_trainAction->setEnabled(m_app && m_app->dbRootObject() &&
49  // m_app->dbRootObject()->getChildrenNumber() != 0); //need some loaded
50  // entities to train the classifier!
51  m_trainAction->setEnabled(true);
52  }
53 
54  m_selectedEntities = selectedEntities;
55 }
56 
57 QList<QAction*> q3DMASCPlugin::getActions() {
58  QList<QAction*> group;
59 
60  if (!m_trainAction) {
61  m_trainAction = new QAction("Train classifier", this);
62  m_trainAction->setToolTip("Train classifier");
63  m_trainAction->setIcon(QIcon(QString::fromUtf8(
64  ":/CC/plugin/q3DMASCPlugin/3DMASC TRAIN.png")));
65  connect(m_trainAction, SIGNAL(triggered()), this,
66  SLOT(doTrainAction()));
67  }
68  group.push_back(m_trainAction);
69 
70  if (!m_classifyAction) {
71  m_classifyAction = new QAction("Classify", this);
72  m_classifyAction->setToolTip("Classify cloud");
73  m_classifyAction->setIcon(QIcon(QString::fromUtf8(
74  ":/CC/plugin/q3DMASCPlugin/3DMASC CLASSIFY.png")));
75  connect(m_classifyAction, SIGNAL(triggered()), this,
76  SLOT(doClassifyAction()));
77  }
78  group.push_back(m_classifyAction);
79 
80  return group;
81 }
82 
84  if (!m_app) {
85  assert(false);
86  return;
87  }
88 
89  // disclaimer accepted?
91  return;
92  }
93 
94  QString inputFilename;
95  {
96  QSettings settings;
97  settings.beginGroup("3DMASC");
98  QString inputPath =
99  settings.value("FilePath",
100  QCoreApplication::applicationDirPath())
101  .toString();
102  inputFilename = QFileDialog::getOpenFileName(
103  m_app->getMainWindow(), "Load 3DMASC classifier file",
104  inputPath, "*.txt");
105  if (inputFilename.isNull()) {
106  // process cancelled by the user
107  return;
108  }
109  settings.setValue("FilePath", QFileInfo(inputFilename).absolutePath());
110  settings.endGroup();
111  }
112 
113  QList<QString> cloudLabels;
114  QString corePointsLabel;
115  bool filenamesSpecified = false;
116  QMap<QString, QString> rolesAndNames;
118  inputFilename, cloudLabels, corePointsLabel, filenamesSpecified,
119  rolesAndNames)) {
120  m_app->dispToConsole("Failed to read classifier file (see Console)",
122  return;
123  }
124  if (cloudLabels.empty()) {
125  m_app->dispToConsole("Invalid classifier file (no cloud label defined)",
127  return;
128  } else if (cloudLabels.size() >
129  4 + (cloudLabels.contains("TEST") ? 1 : 0)) {
131  "This classifier uses more than 4 clouds (the GUI version "
132  "cannot handle it)",
134  return;
135  }
136 
137  // now show a dialog where the user will be able to set the cloud roles
138  Classify3DMASCDialog classifDlg(m_app);
139  classifDlg.setCloudRoles(cloudLabels, corePointsLabel, rolesAndNames);
140  classifDlg.label_trainOrClassify->setText(corePointsLabel +
141  " will be classified");
142  classifDlg.classifierFileLineEdit->setText(inputFilename);
143  classifDlg.testCloudComboBox->hide();
144  classifDlg.testLabel->hide();
145  if (!classifDlg.exec()) {
146  // process cancelled by the user
147  return;
148  }
149  static bool s_keepAttributes =
150  classifDlg.keepAttributesCheckBox->isChecked();
151 
153  QString mainCloudLabel = corePointsLabel;
154  classifDlg.getClouds(clouds);
155 
156  masc::Feature::Set features;
157  masc::Classifier classifier;
158  if (!masc::Tools::LoadClassifier(inputFilename, clouds, features,
159  classifier, m_app->getMainWindow())) {
160  return;
161  }
162  if (!classifier.isValid()) {
163  m_app->dispToConsole("No classifier or invalid classifier",
165  return;
166  }
167 
168  if (clouds.contains("TEST")) {
169  // remove the test cloud (if any)
170  clouds.remove("TEST");
171  }
172 
173  // the 'main cloud' is the cloud that should be classified
175  corePoints.origin = corePoints.cloud = clouds[mainCloudLabel];
176  corePoints.role = mainCloudLabel;
177 
178  // prepare the main cloud
179  ecvProgressDialog progressDlg(true, m_app->getMainWindow());
180  progressDlg.show();
181  progressDlg.setAutoClose(false); // we don't want the progress dialog to
182  // 'pop' for each feature
183  QString error;
184  SFCollector generatedScalarFields;
185  if (!masc::Tools::PrepareFeatures(corePoints, features, error, &progressDlg,
186  &generatedScalarFields)) {
188  generatedScalarFields.releaseSFs(false);
189  return;
190  }
191  progressDlg.hide();
192  QCoreApplication::processEvents();
193 
194  // apply classifier
195  {
196  QString errorMessage;
197  masc::Feature::Source::Set featureSources;
198  masc::Feature::ExtractSources(features, featureSources);
199  if (!classifier.classify(featureSources, corePoints.cloud, errorMessage,
200  m_app->getMainWindow())) {
201  m_app->dispToConsole(errorMessage,
203  generatedScalarFields.releaseSFs(false);
204  return;
205  }
206 
207  generatedScalarFields.releaseSFs(s_keepAttributes);
208  }
209 }
210 
213  : feature(f) {}
215  bool selected = true;
216  bool prepared = false;
217  float importance = std::numeric_limits<float>::quiet_NaN();
218 };
219 
221  QSettings settings("OSUR", "q3DMASC");
222  settings.setValue("TrainParameters/maxDepth", params.rt.maxDepth);
223  settings.setValue("TrainParameters/minSampleCount",
224  params.rt.minSampleCount);
225  settings.setValue("TrainParameters/activeVarCount",
226  params.rt.activeVarCount);
227  settings.setValue("TrainParameters/maxTreeCount", params.rt.maxTreeCount);
228 }
229 
231  QSettings settings("OSUR", "q3DMASC");
232  params.rt.maxDepth = settings.value("TrainParameters/maxDepth", 25).toInt();
233  params.rt.minSampleCount =
234  settings.value("TrainParameters/minSampleCount", 10).toInt();
235  params.rt.activeVarCount =
236  settings.value("TrainParameters/activeVarCount", 0).toInt();
237  params.rt.maxTreeCount =
238  settings.value("TrainParameters/maxTreeCount", 100).toInt();
239 }
240 
242  // disclaimer accepted?
243  if (!ShowTrainDisclaimer(m_app)) return;
244 
245  QString inputFilename;
246  {
247  QSettings settings;
248  settings.beginGroup("3DMASC");
249  QString inputPath =
250  settings.value("FilePath",
251  QCoreApplication::applicationDirPath())
252  .toString();
253  inputFilename = QFileDialog::getOpenFileName(
254  m_app->getMainWindow(), "Load 3DMASC training file", inputPath,
255  "*.txt");
256  if (inputFilename.isNull()) {
257  // process cancelled by the user
258  return;
259  }
260  settings.setValue("FilePath", QFileInfo(inputFilename).absolutePath());
261  settings.endGroup();
262  }
263 
264  // load the cloud labels (PC1, PC2, CTX, etc.)
265  QList<QString> cloudLabels;
266  QString corePointsLabel;
267  bool filenamesSpecified = false;
268  QMap<QString, QString> rolesAndNames;
270  inputFilename, cloudLabels, corePointsLabel, filenamesSpecified,
271  rolesAndNames)) {
272  m_app->dispToConsole("Failed to read classifier file (see Console)",
274  return;
275  }
276  if (cloudLabels.empty()) {
277  m_app->dispToConsole("Invalid classifier file (no cloud label defined)",
279  return;
280  }
281 
282  masc::Tools::NamedClouds loadedClouds;
284 
285  // if no filename is specified in the training file, we are bound to ask the
286  // user to specify them
287  bool useCloudsFromDB =
288  (!filenamesSpecified ||
289  QMessageBox::question(m_app->getMainWindow(), "Use clouds in DB",
290  "Use clouds in db (yes) or clouds specified "
291  "in the file(no)?",
292  QMessageBox::Yes,
293  QMessageBox::No) == QMessageBox::Yes);
294  QString mainCloudLabel = corePointsLabel;
295  if (useCloudsFromDB) {
296  if (cloudLabels.size() > 4 + (cloudLabels.contains("TEST") ? 1 : 0)) {
298  "This classifier uses more than 4 different clouds (the "
299  "GUI version cannot handle it)",
301  return;
302  }
303 
304  // now show a dialog where the user will be able to set the cloud roles
305  Classify3DMASCDialog classifDlg(m_app, true);
306  classifDlg.setWindowTitle("3DMASC Train");
307  classifDlg.setCloudRoles(cloudLabels, corePointsLabel, rolesAndNames);
308  classifDlg.label_trainOrClassify->setText(
309  "The classifier will be trained on " + corePointsLabel);
310  classifDlg.classifierFileLineEdit->setText(inputFilename);
311  classifDlg.keepAttributesCheckBox
312  ->hide(); // this parameter is set in the trainDlg dialog
313  if (!classifDlg.exec()) {
314  // process cancelled by the user
315  return;
316  }
317 
318  classifDlg.getClouds(loadedClouds);
319  m_app->dispToConsole("Training cloud: " + mainCloudLabel,
321  corePoints.origin = loadedClouds[mainCloudLabel];
322  corePoints.role = mainCloudLabel;
323  }
324 
327  s_params); // load the saved parameters or the default values
328  masc::Feature::Set features;
329  std::vector<double> scales;
330  if (!masc::Tools::LoadTrainingFile(inputFilename, features, scales,
331  loadedClouds, s_params, &corePoints,
332  m_app->getMainWindow())) {
333  m_app->dispToConsole("Failed to load the training file",
335  return;
336  }
337 
338  if (!corePoints.origin) {
339  m_app->dispToConsole("Core points not defined",
341  return;
342  }
343 
344  if (mainCloudLabel.isEmpty()) {
345  mainCloudLabel = corePoints.role;
346  }
347 
350  "Missing 'Classification' field on core points cloud",
352  return;
353  }
354 
355  ccHObject* group = new ccHObject("3DMASC");
356  if (!useCloudsFromDB) {
357  // add the loaded clouds to the main DB (so that we don't need to handle
358  // them anymore)
359  for (masc::Tools::NamedClouds::const_iterator it = loadedClouds.begin();
360  it != loadedClouds.end(); ++it) {
361  group->addChild(it.value());
362  }
363  }
364 
365  for (masc::Tools::NamedClouds::iterator it = loadedClouds.begin();
366  it != loadedClouds.end(); ++it) {
367  if (it.value())
368  m_app->dispToConsole(it.key() + " = " + it.value()->getName(),
370  else
371  CVLog::Warning(it.key() + " is not associated to a point cloud");
372  }
373 
374  // test role
375  ccPointCloud* testCloud = nullptr;
376  bool needTestSuite = false;
377  masc::Feature::Set featuresTest;
378  std::vector<double> scalesTest;
379  if (loadedClouds.contains("TEST")) {
380  testCloud = loadedClouds["TEST"];
381  loadedClouds.remove("TEST");
382 
383  if (testCloud != corePoints.origin && testCloud != corePoints.cloud) {
384  // we need a duplicated test suite!!!
385  needTestSuite = true;
386  // replace the main cloud by the test cloud
387  masc::Tools::NamedClouds loadedCloudsTest;
388  loadedCloudsTest = loadedClouds;
389  loadedCloudsTest[mainCloudLabel] = testCloud;
390 
391  // simply reload the classification file to create duplicated
392  // features
393  masc::TrainParameters tempParams;
394  if (!masc::Tools::LoadTrainingFile(inputFilename, featuresTest,
395  scalesTest, loadedCloudsTest,
396  tempParams)) {
398  "Failed to load the training file (for TEST)",
400  return;
401  }
402  }
403  }
404 
405  // show the training dialog for the first time
407  trainDlg.setWindowModality(
408  Qt::WindowModal); // to be able to move the confusion matrix window
409  trainDlg.maxDepthSpinBox->setValue(s_params.rt.maxDepth);
410  trainDlg.maxTreeCountSpinBox->setValue(s_params.rt.maxTreeCount);
411  trainDlg.activeVarCountSpinBox->setValue(s_params.rt.activeVarCount);
412  trainDlg.minSampleCountSpinBox->setValue(s_params.rt.minSampleCount);
413  trainDlg.testDataRatioSpinBox->setValue(
414  static_cast<int>(s_params.testDataRatio * 100));
415  trainDlg.testDataRatioSpinBox->setEnabled(testCloud == nullptr);
416  trainDlg.setInputFilePath(inputFilename);
417 
418  // display the loaded features and let the user select the ones to use
419  trainDlg.setResultText("Select features and press 'Run'");
420 
421  std::vector<FeatureSelection> originalFeatures;
422  originalFeatures.reserve(features.size());
423  for (const masc::Feature::Shared& f : features) {
424  originalFeatures.push_back(FeatureSelection(f));
425  trainDlg.addFeature(f->toString(), originalFeatures.back().importance,
426  originalFeatures.back().selected);
427  }
428 
429  for (double scale : scales) trainDlg.addScale(scale, true);
431 
432  std::vector<FeatureSelection> originalFeaturesTest;
433  if (testCloud && needTestSuite) {
434  originalFeaturesTest.reserve(featuresTest.size());
435  for (const masc::Feature::Shared& f : featuresTest) {
436  originalFeaturesTest.push_back(FeatureSelection(f));
437  }
438  }
439 
440  static bool s_keepAttributes = trainDlg.keepAttributesCheckBox->isChecked();
441  if (!trainDlg.exec()) {
442  delete group;
443  return;
444  }
445  assert(!trainDlg.shouldSaveClassifier()); // the save button should be
446  // disabled at this point
447 
448  // compute the core points (if necessary)
449  ecvProgressDialog progressDlg(true, m_app->getMainWindow());
450  progressDlg.setAutoClose(false);
451  if (!corePoints.prepare(&progressDlg)) {
452  m_app->dispToConsole("Failed to compute/prepare the core points!",
454  delete group;
455  return;
456  }
457 
458  if (corePoints.cloud != corePoints.origin) {
459  // auto-hide the other clouds
460  for (ccPointCloud* pc : loadedClouds) {
461  pc->setEnabled(false);
462  }
463  // set an explicit name for the core points
464  QString corePointsName = corePoints.origin->getName();
465  switch (corePoints.selectionMethod) {
467  break;
469  corePointsName += "_SS_Random@" +
470  QString::number(corePoints.selectionParam);
471  break;
473  corePointsName += "_SS_Spatial@" +
474  QString::number(corePoints.selectionParam);
475  break;
476  default:
477  assert(false);
478  }
479  corePoints.cloud->setName(
480  QString("Core points (%1)").arg(corePointsName));
481  group->addChild(corePoints.cloud);
482  }
483 
484  if (group->getChildrenNumber() != 0) {
485  m_app->addToDB(group);
486  QCoreApplication::processEvents();
487  } else {
488  delete group;
489  group = nullptr;
490  }
491 
492  // train / test subsets
493  QSharedPointer<cloudViewer::ReferenceCloud> trainSubset, testSubset;
494  float previousTestSubsetRatio = -1.0f;
495  SFCollector generatedScalarFields;
496  SFCollector generatedScalarFieldsTest;
497 
498  // we will train + evaluate the classifier, then display the results
499  // then let the user change parameters and (potentially) start again
500  for (int iteration = 0;; ++iteration) {
501  // look for selected features
502  features.clear();
503  masc::Feature::Set toPrepare;
504  for (size_t i = 0; i < originalFeatures.size(); ++i) {
505  originalFeatures[i].selected = trainDlg.isFeatureSelected(
506  originalFeatures[i].feature->toString());
507 
508  // if the feature is selected
509  if (originalFeatures[i].selected) {
510  if (!originalFeatures[i].prepared) {
511  // we should prepare it first!
512  toPrepare.push_back(originalFeatures[i].feature);
513  }
514  features.push_back(originalFeatures[i].feature);
515  }
516  }
517 
518  masc::Classifier classifier;
519  if (features.empty()) {
520  m_app->dispToConsole("No feature selected!",
522  } else {
523  // prepare the features (should be done once)
524  if (!toPrepare.empty()) {
525  progressDlg.show();
526  QString error;
528  &progressDlg,
529  &generatedScalarFields)) {
532  generatedScalarFields.releaseSFs(false);
533  generatedScalarFieldsTest.releaseSFs(false);
534  return;
535  }
536  progressDlg.hide();
537  QCoreApplication::processEvents();
538  m_app->redrawAll();
539 
540  // flag the prepared features as 'prepared' ;)
541  for (FeatureSelection& fs : originalFeatures) {
542  if (fs.selected && !fs.prepared) fs.prepared = true;
543  }
544  }
545 
546  // retrieve parameters
547  s_params.rt.maxDepth = trainDlg.maxDepthSpinBox->value();
548  s_params.rt.maxTreeCount = trainDlg.maxTreeCountSpinBox->value();
549  s_params.rt.activeVarCount =
550  trainDlg.activeVarCountSpinBox->value();
551  s_params.rt.minSampleCount =
552  trainDlg.minSampleCountSpinBox->value();
553  float testDataRatio = 0.0f;
554 
555  if (!testCloud) {
556  // we need to generate test subsets
557  testDataRatio = s_params.testDataRatio =
558  trainDlg.testDataRatioSpinBox->value() / 100.0f;
559  if (testDataRatio < 0.0f || testDataRatio > 0.99f) {
560  assert(false);
562  "Invalid test data ratio",
564  trainSubset.clear();
565  testSubset.clear();
566  } else if (previousTestSubsetRatio != testDataRatio) {
567  if (!trainSubset)
568  trainSubset.reset(new cloudViewer::ReferenceCloud(
569  corePoints.cloud));
570  trainSubset->clear();
571 
572  if (!testSubset)
573  testSubset.reset(new cloudViewer::ReferenceCloud(
574  corePoints.cloud));
575  testSubset->clear();
576 
577  // randomly select the training points
579  corePoints.cloud, testDataRatio,
580  testSubset.data(), trainSubset.data())) {
582  "Not enough memory to generate the test "
583  "subsets",
585  generatedScalarFields.releaseSFs(false);
586  generatedScalarFieldsTest.releaseSFs(false);
587  return;
588  }
589  previousTestSubsetRatio = testDataRatio;
590  }
591  }
592 
593  // extract the sources (after having prepared the features!)
594  masc::Feature::Source::Set featureSources;
595  masc::Feature::ExtractSources(features, featureSources);
596 
597  // train the classifier
598  {
599  QString errorMessage;
600  if (!classifier.train(corePoints.cloud, s_params.rt,
601  featureSources, errorMessage,
602  trainSubset.data(), m_app,
603  m_app->getMainWindow())) {
605  errorMessage,
607  generatedScalarFields.releaseSFs(false);
608  generatedScalarFieldsTest.releaseSFs(false);
609  return;
610  }
611  trainDlg.setFirstRunDone();
612  // trainDlg.shouldSaveClassifier();
614  }
615 
616  // test the trained classifier
617  {
618  if (testCloud) {
619  // look for selected features
620  if (needTestSuite) {
621  featuresTest.clear();
622  masc::Feature::Set toPrepareTest;
623  for (size_t i = 0; i < originalFeaturesTest.size();
624  ++i) {
625  originalFeaturesTest[i]
626  .selected = trainDlg.isFeatureSelected(
627  originalFeatures[i].feature->toString());
628  // if the feature is selected
629  if (originalFeaturesTest[i].selected) {
630  if (!originalFeaturesTest[i].prepared) {
631  // we should prepare it first!
632  toPrepareTest.push_back(
633  originalFeaturesTest[i].feature);
634  }
635  featuresTest.push_back(
636  originalFeaturesTest[i].feature);
637  }
638  }
639 
640  // prepare the features and the test cloud
641  if (!toPrepareTest.empty()) {
642  masc::CorePoints corePointsTest;
643  corePointsTest.cloud = testCloud;
644  corePointsTest.origin = testCloud;
645  corePointsTest.role = mainCloudLabel;
646  progressDlg.show();
647  QCoreApplication::processEvents();
648  QString error;
650  corePointsTest, toPrepareTest, error,
651  &progressDlg,
652  &generatedScalarFieldsTest)) {
655  ERR_CONSOLE_MESSAGE);
656  generatedScalarFields.releaseSFs(false);
657  generatedScalarFieldsTest.releaseSFs(false);
658  return;
659  }
660  progressDlg.hide();
661  QCoreApplication::processEvents();
662  m_app->redrawAll();
663 
664  // flag the prepared features as 'prepared' ;)
665  for (FeatureSelection& fs : originalFeaturesTest) {
666  if (fs.selected && !fs.prepared)
667  fs.prepared = true;
668  }
669  }
670  }
671  }
672 
674  QString errorMessage;
675  if (!classifier.evaluate(
676  featureSources,
677  testCloud ? testCloud : corePoints.cloud, metrics,
678  errorMessage, trainDlg,
679  testCloud ? nullptr : testSubset.data(),
680  testCloud ? "Classification_prediction"
681  : "", // outputSFName, empty is the test
682  // cloud is not a separate cloud
683  m_app->getMainWindow(), m_app)) {
685  errorMessage,
687  generatedScalarFields.releaseSFs(false);
688  generatedScalarFieldsTest.releaseSFs(false);
689  return;
690  }
691 
692  QString resultText =
693  QString("Correct guess = %1 / %2 --> accuracy = %3")
694  .arg(metrics.goodGuess)
695  .arg(metrics.sampleCount)
696  .arg(metrics.ratio);
697  m_app->dispToConsole(resultText,
699  trainDlg.setResultText(resultText);
700 
701  cv::Mat importanceMat = classifier.getVarImportance();
702  // m_app->dispToConsole(QString("Var importance size = %1 x
703  // %2").arg(importanceMat.rows).arg(importanceMat.cols));
704  assert(static_cast<int>(features.size()) == importanceMat.rows);
705  int selectedFeatureIndex = 0;
706  for (size_t i = 0; i < originalFeatures.size(); ++i) {
707  if (originalFeatures[i].selected) {
708  // m_app->dispToConsole(QString("Feature #%1 importance
709  // = %2").arg(i + 1).arg(importanceMat.at<float>(i,
710  // 0)));
711  assert(selectedFeatureIndex < importanceMat.rows);
712  originalFeatures[i].importance =
713  importanceMat.at<float>(selectedFeatureIndex,
714  0);
715  ++selectedFeatureIndex;
716  } else {
717  originalFeatures[i].importance =
718  std::numeric_limits<float>::quiet_NaN();
719  }
720  trainDlg.setFeatureImportance(
721  originalFeatures[i].feature->toString(),
722  originalFeatures[i].importance);
723  }
724 
725  trainDlg.sortByFeatureImportance();
726 
727  // if the checkbox "Save traces" is checked
728  if (trainDlg.getSaveTrace()) {
729  // save the classifier in the trace directory with a generic
730  // name depending on the run
731  QString tracePath = trainDlg.getTracePath();
732  if (!tracePath.isEmpty()) {
733  QString outputFilePath =
734  tracePath + "/run_" +
735  QString::number(trainDlg.getRun()) + ".txt";
737  outputFilePath, features, mainCloudLabel,
738  classifier, m_app->getMainWindow())) {
740  "Classifier succesfully saved to " +
741  outputFilePath,
743  trainDlg.setClassifierSaved();
744  } else {
746  "Failed to save classifier file");
747  }
748 
749  // save feature importance
750  QString filename = tracePath + "/run_" +
751  QString::number(trainDlg.getRun()) +
752  "_feature_importance.txt";
753  QFile file(filename);
754  if (!file.open(QFile::Text | QFile::WriteOnly)) {
756  QString("Can't open file '%1' for writing")
757  .arg(filename));
758  }
759  QTextStream stream(&file);
760  stream << "# feature importance" << QtCompat::endl;
761  for (size_t i = 0; i < originalFeatures.size(); ++i) {
762  if (originalFeatures[i].selected) {
763  stream << originalFeatures[i]
764  .feature->toString()
765  << " " << originalFeatures[i].importance
766  << QtCompat::endl;
767  }
768  }
769  }
770  }
771  }
772  }
773 
774  // now wait for the user input
775  while (true) // ew!
776  {
777  if (!trainDlg.exec()) {
779  // the dialog can be closed
780  if (trainDlg.keepAttributesCheckBox->isChecked())
781  s_keepAttributes = true;
782  else
783  s_keepAttributes = false;
784  generatedScalarFields.releaseSFs(s_keepAttributes);
785  generatedScalarFieldsTest.releaseSFs(s_keepAttributes);
786  return;
787  }
788 
789  // if the save button has been clicked
790  if (trainDlg.shouldSaveClassifier()) {
791  // ask for the output filename
792  QString outputFilename;
793  {
794  QSettings settings;
795  settings.beginGroup("3DMASC");
796  QString outputPath =
797  settings.value("FilePath",
798  QCoreApplication::
799  applicationDirPath())
800  .toString();
801  outputFilename = QFileDialog::getSaveFileName(
802  m_app->getMainWindow(), "Save 3DMASC classifier",
803  outputPath, "*.txt");
804  if (outputFilename.isNull()) {
805  // process cancelled by the user
806  continue;
807  }
808  settings.setValue("FilePath",
809  QFileInfo(outputFilename).absolutePath());
810  settings.endGroup();
811  }
812 
813  // save the classifier
814  if (masc::Tools::SaveClassifier(outputFilename, features,
815  mainCloudLabel, classifier,
816  m_app->getMainWindow())) {
818  "Classifier succesfully saved to " + outputFilename,
820  trainDlg.setClassifierSaved();
821  } else {
822  m_app->dispToConsole("Failed to save classifier file");
823  }
824  } else // we will run the classifier another time
825  {
826  // stop the local loop
827  break;
828  }
829  }
830  }
831 }
832 
834  if (!cmd) {
835  assert(false);
836  return;
837  }
838 
839  cmd->registerCommand(
841 }
std::string filename
cmdLineReadable * params[]
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
3DMASC plugin 'classify' dialog
void setCloudRoles(const QList< QString > &roles, QString &corePointsLabel, const QMap< QString, QString > &rolesAndNames)
Sets the clouds roles.
void getClouds(QMap< QString, ccPointCloud * > &clouds) const
Returns the selected point clouds.
SF collector.
void releaseSFs(bool keepByDefault)
3DMASC plugin 'train' dialog
void connectScaleSelectionToFeatureSelection()
void setInputFilePath(QString filename)
bool isFeatureSelected(QString featureName) const
int addFeature(QString name, float importance, bool isChecked=true)
Adds a feature (entry) to the results table.
int addScale(double scale, bool isChecked=true)
bool shouldSaveClassifier() const
void setFeatureImportance(QString featureName, float importance)
void setResultText(QString text)
Command line interface.
virtual bool registerCommand(Command::Shared command)=0
Registers a new command.
Hierarchical CLOUDVIEWER Object.
Definition: ecvHObject.h:25
unsigned getChildrenNumber() const
Returns the number of children.
Definition: ecvHObject.h:312
virtual bool addChild(ccHObject *child, int dependencyFlags=DP_PARENT_OF_OTHER, int insertIndex=-1)
Adds a child.
std::vector< ccHObject * > Container
Standard instances container (for children, etc.)
Definition: ecvHObject.h:337
virtual void setEnabled(bool state)
Sets the "enabled" property.
Definition: ecvObject.h:102
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
Standard ECV plugin interface.
ecvMainAppInterface * m_app
Main application interface.
A very simple point cloud (no point duplication)
Main application interface (for plugins)
virtual ccHObject * dbRootObject()=0
Returns DB root (as a ccHObject)
virtual QMainWindow * getMainWindow()=0
Returns main window.
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
virtual void redrawAll(bool only2D=false, bool forceRedraw=true)
Graphical progress indicator (thread-safe)
bool isValid() const
Returns whether the classifier is valid or not.
bool evaluate(const Feature::Source::Set &featureSources, ccPointCloud *testCloud, AccuracyMetrics &metrics, QString &errorMessage, Train3DMASCDialog &train3DMASCDialog, cloudViewer::ReferenceCloud *testSubset=nullptr, QString outputSFName=QString(), QWidget *parentWidget=nullptr, ecvMainAppInterface *app=nullptr)
Evaluates the classifier.
bool classify(const Feature::Source::Set &featureSources, ccPointCloud *cloud, QString &errorMessage, QWidget *parentWidget=nullptr, ecvMainAppInterface *app=nullptr)
Applies the classifier.
cv::Mat getVarImportance() const
bool train(const ccPointCloud *cloud, const RandomTreesParams &params, const Feature::Source::Set &featureSources, QString &errorMessage, cloudViewer::ReferenceCloud *trainSubset=nullptr, ecvMainAppInterface *app=nullptr, QWidget *parentWidget=nullptr)
Train the classifier.
static bool RandomSubset(ccPointCloud *cloud, float ratio, cloudViewer::ReferenceCloud *inRatioSubset, cloudViewer::ReferenceCloud *outRatioSubset)
static bool LoadClassifier(QString filename, NamedClouds &clouds, Feature::Set &rawFeatures, masc::Classifier &classifier, QWidget *parent=nullptr)
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
void loadTrainParameters(masc::TrainParameters &params)
Definition: q3DMASC.cpp:230
QAction * m_classifyAction
Calssify action.
Definition: q3DMASC.h:42
q3DMASCPlugin(QObject *parent=nullptr)
Default constructor.
Definition: q3DMASC.cpp:31
QAction * m_trainAction
Train action.
Definition: q3DMASC.h:44
void doClassifyAction()
Definition: q3DMASC.cpp:83
void onNewSelection(const ccHObject::Container &selectedEntities) override
Definition: q3DMASC.cpp:37
ccHObject::Container m_selectedEntities
Currently selected entities.
Definition: q3DMASC.h:47
void doTrainAction()
Definition: q3DMASC.cpp:241
void saveTrainParameters(const masc::TrainParameters &params)
Definition: q3DMASC.cpp:220
void registerCommands(ccCommandLineInterface *cmd) override
Optional: registers commands (for the command line mode)
Definition: q3DMASC.cpp:833
QList< QAction * > getActions() override
Get a list of actions for this plugin.
Definition: q3DMASC.cpp:57
static void error(char *msg)
Definition: lsd.c:159
QTextStream & endl(QTextStream &stream)
Definition: QtCompat.h:718
static bool ShowClassifyDisclaimer(ecvMainAppInterface *app)
static bool ShowTrainDisclaimer(ecvMainAppInterface *app)
cloudViewer::GenericIndexedCloud * corePoints
qHoughNormalsDialog::Parameters s_params
FeatureSelection(masc::Feature::Shared f=masc::Feature::Shared(nullptr))
Definition: q3DMASC.cpp:212
masc::Feature::Shared feature
Definition: q3DMASC.cpp:214
QSharedPointer< Command > Shared
Shared type.
Classifier accuracy metrics.
Core points descriptor.
Definition: CorePoints.h:39
ccPointCloud * cloud
Core points cloud.
Definition: CorePoints.h:44
ccPointCloud * origin
Origin cloud.
Definition: CorePoints.h:41
QString role
Core points 'role'.
Definition: CorePoints.h:47
std::vector< Source > Set
static bool ExtractSources(const Set &features, Source::Set &sources)
Extracts the set of 'sources' from a set of features.
std::vector< Shared > Set
Set of features.
QSharedPointer< Feature > Shared
Shared type.