diff --git a/Train3DMASCDialog.ui b/Train3DMASCDialog.ui
index 71a935c..1283fe4 100644
--- a/Train3DMASCDialog.ui
+++ b/Train3DMASCDialog.ui
@@ -6,8 +6,8 @@
0
0
- 397
- 476
+ 400
+ 500
@@ -103,7 +103,7 @@
-
- Other
+ Other parameters
-
@@ -136,26 +136,96 @@
-
-
-
- Qt::Vertical
+
+
+ QFrame::StyledPanel
-
-
- 20
- 40
-
+
+ QFrame::Raised
-
+
+
-
+
+
+ Run
+
+
+
+ -
+
+
+ false
+
+
+ Save
+
+
+
+ -
+
+
+ Close
+
+
+
+
+ runPushButton
+ closePushButton
+ savePushButton
+
-
-
-
- Qt::Horizontal
+
+
+
+ 0
+ 0
+
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+ Results
+
+
-
+
+
+ correct = 0/0 - accuracy = 0.0
+
+
+
+ -
+
+
+ Qt::ScrollBarAlwaysOn
+
+
+ false
+
+
+ false
+
+
+ QAbstractItemView::NoSelection
+
+
+ 170
+
+
+ false
+
+
+
+ Feature
+
+
+
+
+ Importance
+
+
+
+
+
@@ -163,34 +233,18 @@
- buttonBox
- accepted()
+ runPushButton
+ clicked()
Train3DMASCDialog
accept()
- 248
- 254
+ 76
+ 296
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- Train3DMASCDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
+ 198
+ 237
diff --git a/q3DMASC.cpp b/q3DMASC.cpp
index c7bd74f..2dd306b 100644
--- a/q3DMASC.cpp
+++ b/q3DMASC.cpp
@@ -162,19 +162,19 @@ void q3DMASCPlugin::doClassifyAction()
corePoints.origin = corePoints.cloud = clouds[mainCloudLabel];
//prepare the main cloud
- ccProgressDialog pDlg(true, m_app->getMainWindow());
- pDlg.setAutoClose(false); //we don't want the progress dialog to 'pop' for each feature
+ ccProgressDialog progressDlg(true, m_app->getMainWindow());
+ progressDlg.setAutoClose(false); //we don't want the progress dialog to 'pop' for each feature
QString error;
SFCollector generatedScalarFields;
- if (!masc::Tools::PrepareFeatures(corePoints, features, error, &pDlg, &generatedScalarFields))
+ if (!masc::Tools::PrepareFeatures(corePoints, features, error, &progressDlg, &generatedScalarFields))
{
m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
generatedScalarFields.clear();
return;
}
- pDlg.close();
+ progressDlg.close();
QCoreApplication::processEvents();
- pDlg.setAutoClose(true); //restore the default behavior of the progress dialog
+ progressDlg.setAutoClose(true); //restore the default behavior of the progress dialog
//apply classifier
{
@@ -193,6 +193,15 @@ void q3DMASCPlugin::doClassifyAction()
}
}
+struct FeatureSelection
+{
+ FeatureSelection(masc::Feature::Shared f = masc::Feature::Shared(nullptr)) : feature(f) {}
+ masc::Feature::Shared feature;
+ bool selected = true;
+ bool prepared = false;
+ float importance = std::numeric_limits::quiet_NaN();
+};
+
void q3DMASCPlugin::doTrainAction()
{
//disclaimer accepted?
@@ -236,8 +245,32 @@ void q3DMASCPlugin::doTrainAction()
group->addChild(pc);
}
- ccProgressDialog pDlg(true, m_app->getMainWindow());
- if (!corePoints.prepare(&pDlg))
+ //show the training dialog for the first time
+ Train3DMASCDialog trainDlg(m_app->getMainWindow());
+ trainDlg.maxDepthSpinBox->setValue(s_params.rt.maxDepth);
+ trainDlg.maxTreeCountSpinBox->setValue(s_params.rt.maxTreeCount);
+ trainDlg.activeVarCountSpinBox->setValue(s_params.rt.activeVarCount);
+ trainDlg.minSampleCountSpinBox->setValue(s_params.rt.minSampleCount);
+ trainDlg.testDataRatioSpinBox->setValue(static_cast(s_params.testDataRatio * 100));
+
+ //display the loaded features and let the user select the ones to use
+ trainDlg.setResultText("Select features and press 'Run'");
+ std::vector originalFeatures;
+ originalFeatures.reserve(features.size());
+ for (const masc::Feature::Shared& f : features)
+ {
+ originalFeatures.push_back(FeatureSelection(f));
+ trainDlg.addFeature(f->toString(), originalFeatures.back().importance, originalFeatures.back().selected);
+ }
+ if (!trainDlg.exec())
+ {
+ return;
+ }
+ assert(!trainDlg.shouldSaveClassifier()); //the save button should be disabled at this point
+
+ //compute the core points (if necessary)
+ ccProgressDialog progressDlg(true, m_app->getMainWindow());
+ if (!corePoints.prepare(&progressDlg))
{
m_app->dispToConsole("Failed to compute/prepare the core points!", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
delete group;
@@ -271,33 +304,64 @@ void q3DMASCPlugin::doTrainAction()
m_app->addToDB(group);
QCoreApplication::processEvents();
- pDlg.setAutoClose(false); //we don't want the progress dialog to 'pop' for each feature
- QString error;
- if (!masc::Tools::PrepareFeatures(corePoints, features, error, &pDlg))
- {
- m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
- delete group;
- return;
- }
- pDlg.setAutoClose(true); //restore the default behavior of the progress dialog
- pDlg.close();
- QCoreApplication::processEvents();
+ //train / test subsets
+ QScopedPointer trainSubset(new CCLib::ReferenceCloud(corePoints.cloud));
+ QScopedPointer testSubset(new CCLib::ReferenceCloud(corePoints.cloud));
+ float previousTrainSubsetRatio = -1.0f;
- m_app->redrawAll();
-
- while (true)
+ for (int iteration = 0; ; ++iteration)
{
- Train3DMASCDialog trainDlg(m_app->getMainWindow());
- trainDlg.maxDepthSpinBox->setValue(s_params.rt.maxDepth);
- trainDlg.maxTreeCountSpinBox->setValue(s_params.rt.maxTreeCount);
- trainDlg.activeVarCountSpinBox->setValue(s_params.rt.activeVarCount);
- trainDlg.minSampleCountSpinBox->setValue(s_params.rt.minSampleCount);
- trainDlg.testDataRatioSpinBox->setValue(static_cast(s_params.testDataRatio * 100));
- if (!trainDlg.exec())
+ //look for selected features
+ features.clear();
+ masc::Feature::Set toPrepare;
+ for (size_t i = 0; i < originalFeatures.size(); ++i)
{
- return;
+ originalFeatures[i].selected = trainDlg.isFeatureSelected(i);
+
+ //if the feature is selected
+ if (originalFeatures[i].selected)
+ {
+ if (!originalFeatures[i].prepared)
+ {
+ //we should prepare it first!
+ toPrepare.push_back(originalFeatures[i].feature);
+ }
+ features.push_back(originalFeatures[i].feature);
+ }
}
+ if (features.empty())
+ {
+ m_app->dispToConsole("No feature selected!", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
+ continue;
+ }
+
+ //prepare the features
+ if (!toPrepare.empty())
+ {
+ progressDlg.setAutoClose(false); //we don't want the progress dialog to 'pop' for each feature
+ QString error;
+ if (!masc::Tools::PrepareFeatures(corePoints, toPrepare, error, &progressDlg))
+ {
+ m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
+ return;
+ }
+ progressDlg.setAutoClose(true); //restore the default behavior of the progress dialog
+ progressDlg.close();
+ QCoreApplication::processEvents();
+ m_app->redrawAll();
+
+ //flag the prepared features as 'prepared' ;)
+ for (FeatureSelection& fs : originalFeatures)
+ {
+ if (fs.selected && !fs.prepared)
+ fs.prepared = true;
+ }
+ }
+
+ masc::Classifier classifier;
+
+ //retrieve parameters
s_params.rt.maxDepth = trainDlg.maxDepthSpinBox->value();
s_params.rt.maxTreeCount = trainDlg.maxTreeCountSpinBox->value();
s_params.rt.activeVarCount = trainDlg.activeVarCountSpinBox->value();
@@ -307,64 +371,116 @@ void q3DMASCPlugin::doTrainAction()
{
assert(false);
m_app->dispToConsole("Invalid test data ratio", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
- return;
}
-
- //randomly select the training points
- QScopedPointer trainSubset(new CCLib::ReferenceCloud(corePoints.cloud));
- QScopedPointer testSubset(new CCLib::ReferenceCloud(corePoints.cloud));
- if (!masc::Tools::RandomSubset(corePoints.cloud, s_params.testDataRatio, testSubset.data(), trainSubset.data()))
+ else
{
- m_app->dispToConsole("Not enough memory", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
- return;
- }
-
- //train the classifier
- masc::Classifier classifier;
- {
- QString errorMessage;
- if (!classifier.train(corePoints.cloud, s_params.rt, features, errorMessage, trainSubset.data(), m_app->getMainWindow()))
+ if (previousTrainSubsetRatio != s_params.testDataRatio)
{
- m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
- return;
- }
-
- QString outputFilename;
- {
- QSettings settings;
- settings.beginGroup("3DMASC");
- QString outputPath = settings.value("FilePath", QCoreApplication::applicationDirPath()).toString();
- outputFilename = QFileDialog::getSaveFileName(m_app->getMainWindow(), "Save 3DMASC classifier", outputPath, "*.txt");
- if (outputFilename.isNull())
+ //randomly select the training points
+ testSubset->clear();
+ trainSubset->clear();
+ if (!masc::Tools::RandomSubset(corePoints.cloud, s_params.testDataRatio, testSubset.data(), trainSubset.data()))
{
- //process cancelled by the user
+ m_app->dispToConsole("Not enough memory", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
return;
}
- settings.setValue("FilePath", QFileInfo(outputFilename).absolutePath());
- settings.endGroup();
+ previousTrainSubsetRatio = s_params.testDataRatio;
}
- //save the classifier
- if (masc::Tools::SaveClassifier(outputFilename, features, classifier, m_app->getMainWindow()))
+ //train the classifier
{
- m_app->dispToConsole("Classifier succesfully saved to " + outputFilename, ccMainAppInterface::STD_CONSOLE_MESSAGE);
+ QString errorMessage;
+ if (!classifier.train(corePoints.cloud, s_params.rt, features, errorMessage, trainSubset.data(), m_app, m_app->getMainWindow()))
+ {
+ m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
+ return;
+ }
+ trainDlg.setFirstRunDone();
+ trainDlg.shouldSaveClassifier();
+ }
+
+ //test the trained classifier
+ {
+ masc::Classifier::AccuracyMetrics metrics;
+ QString errorMessage;
+ if (!classifier.evaluate(features, testSubset.data(), metrics, errorMessage, m_app->getMainWindow()))
+ {
+ m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
+ return;
+ }
+
+ QString resultText = QString("Correct guess = %1 / %2 --> accuracy = %3").arg(metrics.goodGuess).arg(metrics.sampleCount).arg(metrics.ratio);
+ m_app->dispToConsole(resultText, ccMainAppInterface::STD_CONSOLE_MESSAGE);
+ trainDlg.setResultText(resultText);
+
+ cv::Mat importanceMat = classifier.getVarImportance();
+ //m_app->dispToConsole(QString("Var importance size = %1 x %2").arg(importanceMat.rows).arg(importanceMat.cols));
+ assert(static_cast(features.size()) == importanceMat.rows);
+ int selectedFeatureIndex = 0;
+ for (size_t i = 0; i < originalFeatures.size(); ++i)
+ {
+ if (originalFeatures[i].selected)
+ {
+ //m_app->dispToConsole(QString("Feature #%1 importance = %2").arg(i + 1).arg(importanceMat.at(i, 0)));
+ assert(selectedFeatureIndex < importanceMat.rows);
+ originalFeatures[i].importance = importanceMat.at(selectedFeatureIndex, 0);
+ ++selectedFeatureIndex;
+ }
+ else
+ {
+ originalFeatures[i].importance = std::numeric_limits::quiet_NaN();
+ }
+ trainDlg.setFeatureImportance(i, originalFeatures[i].importance);
+ }
}
}
- //test classifier
+ while (true)
{
- masc::Classifier::AccuracyMetrics metrics;
- QString errorMessage;
- if (!classifier.evaluate(features, testSubset.data(), metrics, errorMessage, m_app->getMainWindow()))
+ if (!trainDlg.exec())
{
- m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
+ //the dialog can be closed
return;
}
- m_app->dispToConsole(QString("Correct = %1 / %2 --> accuracy = %3").arg(metrics.goodGuess).arg(metrics.sampleCount).arg(metrics.ratio), ccMainAppInterface::STD_CONSOLE_MESSAGE);
+ //if the save button has been clicked
+ if (trainDlg.shouldSaveClassifier())
+ {
+ //ask for the output filename
+ QString outputFilename;
+ {
+ QSettings settings;
+ settings.beginGroup("3DMASC");
+ QString outputPath = settings.value("FilePath", QCoreApplication::applicationDirPath()).toString();
+ outputFilename = QFileDialog::getSaveFileName(m_app->getMainWindow(), "Save 3DMASC classifier", outputPath, "*.txt");
+ if (outputFilename.isNull())
+ {
+ //process cancelled by the user
+ continue;
+ }
+ settings.setValue("FilePath", QFileInfo(outputFilename).absolutePath());
+ settings.endGroup();
+ }
+
+ //save the classifier
+ if (masc::Tools::SaveClassifier(outputFilename, features, classifier, m_app->getMainWindow()))
+ {
+ m_app->dispToConsole("Classifier succesfully saved to " + outputFilename, ccMainAppInterface::STD_CONSOLE_MESSAGE);
+ trainDlg.setClassifierSaved();
+ }
+ else
+ {
+ m_app->dispToConsole("Failed to save classifier file");
+ }
+ }
+ else //we will run the classifier another time
+ {
+ //stop the local loop
+ break;
+ }
}
- break;
+ //we are going to restart the classification process
}
}
diff --git a/q3DMASCClassifier.cpp b/q3DMASCClassifier.cpp
index 15dd8ef..098ea03 100644
--- a/q3DMASCClassifier.cpp
+++ b/q3DMASCClassifier.cpp
@@ -29,6 +29,9 @@
//qCC_io
#include
+//qCC_plugins
+#include
+
//Qt
#include
#include
@@ -360,6 +363,7 @@ bool Classifier::train( const ccPointCloud* cloud,
const Feature::Set& features,
QString& errorMessage,
CCLib::ReferenceCloud* trainSubset/*=nullptr*/,
+ ccMainAppInterface* app/*=nullptr*/,
QWidget* parentWidget/*=nullptr*/)
{
if (features.empty())
@@ -397,7 +401,10 @@ bool Classifier::train( const ccPointCloud* cloud,
int sampleCount = static_cast(trainSubset ? trainSubset->size() : cloud->size());
int attributesPerSample = static_cast(features.size());
- ccLog::Print(QString("[3DMASC] Training data: %1 samples with %2 feature(s)").arg(sampleCount).arg(attributesPerSample));
+ if (app)
+ {
+ app->dispToConsole(QString("[3DMASC] Training data: %1 samples with %2 feature(s)").arg(sampleCount).arg(attributesPerSample));
+ }
cv::Mat training_data, train_labels;
try
@@ -530,6 +537,17 @@ bool Classifier::train( const ccPointCloud* cloud,
return false;
}
+ if (app)
+ {
+ cv::Mat mat = m_rtrees->getVarImportance();
+ app->dispToConsole(QString("Var importance size = %1 x %2").arg(mat.rows).arg(mat.cols));
+ assert(attributesPerSample == mat.rows);
+ for (int i = 0; i < mat.rows; ++i)
+ {
+ app->dispToConsole(QString("Feature #%1 importance = %2").arg(i + 1).arg(mat.at(i, 0)));
+ }
+ }
+
return true;
}
diff --git a/q3DMASCClassifier.h b/q3DMASCClassifier.h
index 1d5fa5a..fff22a5 100644
--- a/q3DMASCClassifier.h
+++ b/q3DMASCClassifier.h
@@ -31,6 +31,7 @@
#include
class QWidget;
+class ccMainAppInterface;
//! 3DMASC classifier
namespace masc
@@ -43,7 +44,13 @@ namespace masc
Classifier();
//! Train the classifier
- bool train(const ccPointCloud* cloud, const RandomTreesParams& params, const Feature::Set& features, QString& errorMessage, CCLib::ReferenceCloud* trainSubset = nullptr, QWidget* parentWidget = nullptr);
+ bool train( const ccPointCloud* cloud,
+ const RandomTreesParams& params,
+ const Feature::Set& features,
+ QString& errorMessage,
+ CCLib::ReferenceCloud* trainSubset = nullptr,
+ ccMainAppInterface* app = nullptr,
+ QWidget* parentWidget = nullptr);
//! Classifier accuracy metrics
struct AccuracyMetrics
@@ -67,6 +74,8 @@ namespace masc
//! Loads the classifier from file
bool fromFile(QString filename, QWidget* parentWidget = nullptr);
+ inline cv::Mat getVarImportance() const { return m_rtrees->getVarImportance(); }
+
protected:
//! Random trees (OpenCV)
diff --git a/q3DMASCTools.cpp b/q3DMASCTools.cpp
index 678c4ee..0382c6e 100644
--- a/q3DMASCTools.cpp
+++ b/q3DMASCTools.cpp
@@ -632,7 +632,7 @@ static bool LoadFileCommon( const QString& filename,
std::vector scales;
QTextStream stream(&file);
- for (int lineNumber = 0; ; ++lineNumber)
+ for (int lineNumber = 1; ; ++lineNumber)
{
QString line = stream.readLine();
if (line.isNull())
@@ -640,7 +640,6 @@ static bool LoadFileCommon( const QString& filename,
//eof
break;
}
- ++lineNumber;
if (line.startsWith("#"))
{
diff --git a/qTrain3DMASCDialog.cpp b/qTrain3DMASCDialog.cpp
index 4854296..e88da40 100644
--- a/qTrain3DMASCDialog.cpp
+++ b/qTrain3DMASCDialog.cpp
@@ -17,9 +17,96 @@
#include "qTrain3DMASCDialog.h"
+//Qt
+#include
+#include
+
+//System
+#include
+
Train3DMASCDialog::Train3DMASCDialog(QWidget* parent/*=nullptr*/)
: QDialog(parent)
, Ui::Train3DMASCDialog()
+ , classifierSaved(false)
+ , saveRequested(false)
{
setupUi(this);
+
+ connect(closePushButton, SIGNAL(clicked()), this, SLOT(onClose()));
+ connect(savePushButton, SIGNAL(clicked()), this, SLOT(onSave()));
+}
+
+void Train3DMASCDialog::clearResults()
+{
+ resultLabel->clear();
+ tableWidget->clear();
+}
+
+int Train3DMASCDialog::addFeature(QString name, float importance, bool isChecked/*=true*/)
+{
+ int index = tableWidget->rowCount();
+ tableWidget->setRowCount(index + 1);
+
+ QTableWidgetItem* nameItem = new QTableWidgetItem(name);
+ nameItem->setCheckState(isChecked ? Qt::Checked : Qt::Unchecked);
+ tableWidget->setItem(index, 0, nameItem);
+
+ QTableWidgetItem* importanceItem = new QTableWidgetItem(std::isnan(importance) ? QString() : QString::number(importance));
+ tableWidget->setItem(index, 1, importanceItem);
+
+ return index;
+}
+
+void Train3DMASCDialog::setResultText(QString text)
+{
+ resultLabel->setText(text);
+}
+
+void Train3DMASCDialog::setFirstRunDone()
+{
+ runPushButton->setText(tr("Retry"));
+ savePushButton->setEnabled(true);
+}
+
+bool Train3DMASCDialog::isFeatureSelected(size_t index) const
+{
+ if (static_cast(index) >= tableWidget->rowCount())
+ {
+ assert(false);
+ return false;
+ }
+
+ return (tableWidget->item(static_cast(index), 0)->checkState() == Qt::Checked);
+}
+
+void Train3DMASCDialog::setFeatureImportance(size_t index, float importance)
+{
+ if (static_cast(index) >= tableWidget->rowCount())
+ {
+ assert(false);
+ return;
+ }
+
+ if (!std::isnan(importance))
+ {
+ tableWidget->item(static_cast(index), 1)->setText(QString::number(importance));
+ }
+ else
+ {
+ tableWidget->item(static_cast(index), 1)->setText(QString());
+ }
+}
+
+void Train3DMASCDialog::onClose()
+{
+ if (!classifierSaved && QMessageBox::question(this, "Classifier not saved", "Classifier not saved. Do you confirm you want to close the tool?", QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
+ return;
+
+ reject();
+}
+
+void Train3DMASCDialog::onSave()
+{
+ saveRequested = true;
+ accept();
}
diff --git a/qTrain3DMASCDialog.h b/qTrain3DMASCDialog.h
index 31ea61e..ca7e105 100644
--- a/qTrain3DMASCDialog.h
+++ b/qTrain3DMASCDialog.h
@@ -32,4 +32,29 @@ public:
//! Default constructor
Train3DMASCDialog(QWidget* parent = nullptr);
+ void clearResults();
+
+ //! Adds a feature (entry) to the results table
+ /** \return the row index
+ **/
+ int addFeature(QString name, float importance, bool isChecked = true);
+
+ void setResultText(QString text);
+ void setFirstRunDone();
+ inline void setClassifierSaved() { classifierSaved = true; saveRequested = false; }
+
+ bool isFeatureSelected(size_t index) const;
+ void setFeatureImportance(size_t index, float importance);
+
+ inline bool shouldSaveClassifier() const { return saveRequested; }
+
+protected slots:
+
+ void onClose();
+ void onSave();
+
+protected: //members
+
+ bool classifierSaved;
+ bool saveRequested;
};