From b5e5597e2d4d1ebe7e643365f0d05b10e092296c Mon Sep 17 00:00:00 2001 From: Daniel Girardeau-Montaut Date: Sun, 20 Jan 2019 22:12:20 +0100 Subject: [PATCH] New training dialog + selection of the most important features --- Train3DMASCDialog.ui | 132 ++++++++++++++------- q3DMASC.cpp | 252 ++++++++++++++++++++++++++++++----------- q3DMASCClassifier.cpp | 20 +++- q3DMASCClassifier.h | 11 +- q3DMASCTools.cpp | 3 +- qTrain3DMASCDialog.cpp | 87 ++++++++++++++ qTrain3DMASCDialog.h | 25 ++++ 7 files changed, 419 insertions(+), 111 deletions(-) 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; };