mirror of
https://github.com/dgirardeau/q3DMASC.git
synced 2026-08-29 08:34:48 +08:00
4fde7ecba9
disclaimer and icons updates
794 lines
26 KiB
C++
794 lines
26 KiB
C++
//##########################################################################
|
|
//# #
|
|
//# CLOUDCOMPARE PLUGIN: q3DMASC #
|
|
//# #
|
|
//# This program is free software; you can redistribute it and/or modify #
|
|
//# it under the terms of the GNU General Public License as published by #
|
|
//# the Free Software Foundation; version 2 or later of the License. #
|
|
//# #
|
|
//# This program is distributed in the hope that it will be useful, #
|
|
//# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
|
//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
|
//# GNU General Public License for more details. #
|
|
//# #
|
|
//# COPYRIGHT: Dimitri Lague / CNRS / UEB #
|
|
//# #
|
|
//##########################################################################
|
|
|
|
#include "q3DMASC.h"
|
|
|
|
//local
|
|
#include "q3DMASCDisclaimerDialog.h"
|
|
#include "q3DMASCClassifier.h"
|
|
#include "q3DMASCTools.h"
|
|
#include "qClassify3DMASCDialog.h"
|
|
#include "qTrain3DMASCDialog.h"
|
|
#include "q3DMASCCommands.h"
|
|
|
|
//qCC_db
|
|
#include <ccPointCloud.h>
|
|
#include <ccProgressDialog.h>
|
|
|
|
//Qt
|
|
#include <QtGui>
|
|
#include <QtCore>
|
|
#include <QApplication>
|
|
#include <QFileDialog>
|
|
#include <QMessageBox>
|
|
#include <QSettings>
|
|
|
|
q3DMASCPlugin::q3DMASCPlugin(QObject* parent/*=0*/)
|
|
: QObject(parent)
|
|
, ccStdPluginInterface( ":/CC/plugin/q3DMASCPlugin/info.json" )
|
|
, m_classifyAction(0)
|
|
, m_trainAction(0)
|
|
{
|
|
}
|
|
|
|
void q3DMASCPlugin::onNewSelection(const ccHObject::Container& selectedEntities)
|
|
{
|
|
if (m_classifyAction)
|
|
{
|
|
//classification: only one point cloud
|
|
//m_classifyAction->setEnabled(selectedEntities.size() == 1 && selectedEntities[0]->isA(CC_TYPES::POINT_CLOUD));
|
|
m_classifyAction->setEnabled(m_app->dbRootObject()->getChildrenNumber() != 0);
|
|
}
|
|
|
|
if (m_trainAction)
|
|
{
|
|
//m_trainAction->setEnabled(m_app && m_app->dbRootObject() && m_app->dbRootObject()->getChildrenNumber() != 0); //need some loaded entities to train the classifier!
|
|
m_trainAction->setEnabled(true);
|
|
}
|
|
|
|
m_selectedEntities = selectedEntities;
|
|
}
|
|
|
|
QList<QAction*> q3DMASCPlugin::getActions()
|
|
{
|
|
QList<QAction*> group;
|
|
|
|
if (!m_trainAction)
|
|
{
|
|
m_trainAction = new QAction("Train classifier", this);
|
|
m_trainAction->setToolTip("Train classifier");
|
|
m_trainAction->setIcon(QIcon(QString::fromUtf8(":/CC/plugin/q3DMASCPlugin/3DMASC TRAIN.png")));
|
|
connect(m_trainAction, SIGNAL(triggered()), this, SLOT(doTrainAction()));
|
|
}
|
|
group.push_back(m_trainAction);
|
|
|
|
if (!m_classifyAction)
|
|
{
|
|
m_classifyAction = new QAction("Classify", this);
|
|
m_classifyAction->setToolTip("Classify cloud");
|
|
m_classifyAction->setIcon(QIcon(QString::fromUtf8(":/CC/plugin/q3DMASCPlugin/3DMASC CLASSIFY.png")));
|
|
connect(m_classifyAction, SIGNAL(triggered()), this, SLOT(doClassifyAction()));
|
|
}
|
|
group.push_back(m_classifyAction);
|
|
|
|
return group;
|
|
}
|
|
|
|
void q3DMASCPlugin::doClassifyAction()
|
|
{
|
|
if (!m_app)
|
|
{
|
|
assert(false);
|
|
return;
|
|
}
|
|
|
|
//disclaimer accepted?
|
|
if (!ShowClassifyDisclaimer(m_app))
|
|
{
|
|
return;
|
|
}
|
|
|
|
QString inputFilename;
|
|
{
|
|
QSettings settings;
|
|
settings.beginGroup("3DMASC");
|
|
QString inputPath = settings.value("FilePath", QCoreApplication::applicationDirPath()).toString();
|
|
inputFilename = QFileDialog::getOpenFileName(m_app->getMainWindow(), "Load 3DMASC classifier file", inputPath, "*.txt");
|
|
if (inputFilename.isNull())
|
|
{
|
|
//process cancelled by the user
|
|
return;
|
|
}
|
|
settings.setValue("FilePath", QFileInfo(inputFilename).absolutePath());
|
|
settings.endGroup();
|
|
}
|
|
|
|
QList<QString> cloudLabels;
|
|
QString corePointsLabel;
|
|
bool filenamesSpecified = false;
|
|
if (!masc::Tools::LoadClassifierCloudLabels(inputFilename, cloudLabels, corePointsLabel, filenamesSpecified))
|
|
{
|
|
m_app->dispToConsole("Failed to read classifier file (see Console)", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
if (cloudLabels.empty())
|
|
{
|
|
m_app->dispToConsole("Invalid classifier file (no cloud label defined)", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
else if (cloudLabels.size() > 4 + (cloudLabels.contains("TEST") ? 1 : 0))
|
|
{
|
|
m_app->dispToConsole("This classifier uses more than 4 clouds (the GUI version cannot handle it)", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
|
|
//now show a dialog where the user will be able to set the cloud roles
|
|
Classify3DMASCDialog classifDlg(m_app);
|
|
classifDlg.setCloudRoles(cloudLabels, corePointsLabel);
|
|
classifDlg.label_trainOrClassify->setText(corePointsLabel + " will be classified");
|
|
classifDlg.classifierFileLineEdit->setText(inputFilename);
|
|
classifDlg.testCloudComboBox->hide();
|
|
classifDlg.testLabel->hide();
|
|
if (!classifDlg.exec())
|
|
{
|
|
//process cancelled by the user
|
|
return;
|
|
}
|
|
static bool s_keepAttributes = classifDlg.keepAttributesCheckBox->isChecked();
|
|
|
|
masc::Tools::NamedClouds clouds;
|
|
QString mainCloudLabel = corePointsLabel;
|
|
classifDlg.getClouds(clouds);
|
|
|
|
masc::Feature::Set features;
|
|
masc::Classifier classifier;
|
|
if (!masc::Tools::LoadClassifier(inputFilename, clouds, features, classifier, m_app->getMainWindow()))
|
|
{
|
|
return;
|
|
}
|
|
if (!classifier.isValid())
|
|
{
|
|
m_app->dispToConsole("No classifier or invalid classifier", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
|
|
if (clouds.contains("TEST"))
|
|
{
|
|
//remove the test cloud (if any)
|
|
clouds.remove("TEST");
|
|
}
|
|
|
|
//the 'main cloud' is the cloud that should be classified
|
|
masc::CorePoints corePoints;
|
|
corePoints.origin = corePoints.cloud = clouds[mainCloudLabel];
|
|
corePoints.role = mainCloudLabel;
|
|
|
|
//prepare the main cloud
|
|
ccProgressDialog progressDlg(true, m_app->getMainWindow());
|
|
progressDlg.show();
|
|
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, &progressDlg, &generatedScalarFields))
|
|
{
|
|
m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
generatedScalarFields.releaseSFs(false);
|
|
return;
|
|
}
|
|
progressDlg.close();
|
|
QCoreApplication::processEvents();
|
|
|
|
//apply classifier
|
|
{
|
|
QString errorMessage;
|
|
masc::Feature::Source::Set featureSources;
|
|
masc::Feature::ExtractSources(features, featureSources);
|
|
if (!classifier.classify(featureSources, corePoints.cloud, errorMessage, m_app->getMainWindow()))
|
|
{
|
|
m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
generatedScalarFields.releaseSFs(false);
|
|
return;
|
|
}
|
|
|
|
generatedScalarFields.releaseSFs(s_keepAttributes);
|
|
}
|
|
}
|
|
|
|
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<float>::quiet_NaN();
|
|
};
|
|
|
|
void q3DMASCPlugin::saveTrainParameters(const masc::TrainParameters& params)
|
|
{
|
|
QSettings settings("OSUR", "q3DMASC");
|
|
settings.setValue("TrainParameters/maxDepth", params.rt.maxDepth);
|
|
settings.setValue("TrainParameters/minSampleCount", params.rt.minSampleCount);
|
|
settings.setValue("TrainParameters/activeVarCount", params.rt.activeVarCount);
|
|
settings.setValue("TrainParameters/maxTreeCount", params.rt.maxTreeCount);
|
|
}
|
|
|
|
void q3DMASCPlugin::loadTrainParameters(masc::TrainParameters& params)
|
|
{
|
|
QSettings settings("OSUR", "q3DMASC");
|
|
params.rt.maxDepth = settings.value("TrainParameters/maxDepth", 25).toInt();
|
|
params.rt.minSampleCount = settings.value("TrainParameters/minSampleCount", 10).toInt();
|
|
params.rt.activeVarCount = settings.value("TrainParameters/activeVarCount", 0).toInt();
|
|
params.rt.maxTreeCount = settings.value("TrainParameters/maxTreeCount", 100).toInt();
|
|
}
|
|
|
|
void q3DMASCPlugin::doTrainAction()
|
|
{
|
|
//disclaimer accepted?
|
|
if (!ShowTrainDisclaimer(m_app))
|
|
return;
|
|
|
|
QString inputFilename;
|
|
{
|
|
QSettings settings;
|
|
settings.beginGroup("3DMASC");
|
|
QString inputPath = settings.value("FilePath", QCoreApplication::applicationDirPath()).toString();
|
|
inputFilename = QFileDialog::getOpenFileName(m_app->getMainWindow(), "Load 3DMASC training file", inputPath, "*.txt");
|
|
if (inputFilename.isNull())
|
|
{
|
|
//process cancelled by the user
|
|
return;
|
|
}
|
|
settings.setValue("FilePath", QFileInfo(inputFilename).absolutePath());
|
|
settings.endGroup();
|
|
}
|
|
|
|
//load the cloud labels (PC1, PC2, CTX, etc.)
|
|
QList<QString> cloudLabels;
|
|
QString corePointsLabel;
|
|
bool filenamesSpecified = false;
|
|
if (!masc::Tools::LoadClassifierCloudLabels(inputFilename, cloudLabels, corePointsLabel, filenamesSpecified))
|
|
{
|
|
m_app->dispToConsole("Failed to read classifier file (see Console)", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
if (cloudLabels.empty())
|
|
{
|
|
m_app->dispToConsole("Invalid classifier file (no cloud label defined)", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
|
|
masc::Tools::NamedClouds loadedClouds;
|
|
masc::CorePoints corePoints;
|
|
|
|
//if no filename is specified in the training file, we are bound to ask the user to specify them
|
|
bool useCloudsFromDB = (!filenamesSpecified || QMessageBox::question(m_app->getMainWindow(), "Use clouds in DB", "Use clouds in db (yes) or clouds specified in the file(no)?", QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes);
|
|
QString mainCloudLabel = corePointsLabel;
|
|
if (useCloudsFromDB)
|
|
{
|
|
if (cloudLabels.size() > 4 + (cloudLabels.contains("TEST") ? 1 : 0))
|
|
{
|
|
m_app->dispToConsole("This classifier uses more than 4 different clouds (the GUI version cannot handle it)", ccMainAppInterface::WRN_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
|
|
//now show a dialog where the user will be able to set the cloud roles
|
|
Classify3DMASCDialog classifDlg(m_app, true);
|
|
classifDlg.setWindowTitle("3DMASC Train");
|
|
classifDlg.setCloudRoles(cloudLabels, corePointsLabel);
|
|
classifDlg.label_trainOrClassify->setText("The classifier will be trained on " + corePointsLabel);
|
|
classifDlg.classifierFileLineEdit->setText(inputFilename);
|
|
classifDlg.keepAttributesCheckBox->hide(); // this parameter is set in the trainDlg dialog
|
|
if (!classifDlg.exec())
|
|
{
|
|
//process cancelled by the user
|
|
return;
|
|
}
|
|
|
|
classifDlg.getClouds(loadedClouds);
|
|
m_app->dispToConsole("Training cloud: " + mainCloudLabel, ccMainAppInterface::STD_CONSOLE_MESSAGE);
|
|
corePoints.origin = loadedClouds[mainCloudLabel];
|
|
corePoints.role = mainCloudLabel;
|
|
}
|
|
|
|
static masc::TrainParameters s_params;
|
|
loadTrainParameters(s_params); // load the saved parameters or the default values
|
|
masc::Feature::Set features;
|
|
std::vector<double> scales;
|
|
if (!masc::Tools::LoadTrainingFile(inputFilename, features, scales, loadedClouds, s_params, &corePoints, m_app->getMainWindow()))
|
|
{
|
|
m_app->dispToConsole("Failed to load the training file", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
|
|
if (!corePoints.origin)
|
|
{
|
|
m_app->dispToConsole("Core points not defined", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
|
|
if (mainCloudLabel.isEmpty())
|
|
{
|
|
mainCloudLabel = corePoints.role;
|
|
}
|
|
|
|
if (!masc::Tools::GetClassificationSF(corePoints.origin))
|
|
{
|
|
m_app->dispToConsole("Missing 'Classification' field on core points cloud", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
|
|
ccHObject* group = new ccHObject("3DMASC");
|
|
if (!useCloudsFromDB)
|
|
{
|
|
//add the loaded clouds to the main DB (so that we don't need to handle them anymore)
|
|
for (masc::Tools::NamedClouds::const_iterator it = loadedClouds.begin(); it != loadedClouds.end(); ++it)
|
|
{
|
|
group->addChild(it.value());
|
|
}
|
|
}
|
|
|
|
for (masc::Tools::NamedClouds::iterator it = loadedClouds.begin(); it != loadedClouds.end(); ++it)
|
|
{
|
|
if (it.value())
|
|
m_app->dispToConsole(it.key() + " = " + it.value()->getName(), ccMainAppInterface::STD_CONSOLE_MESSAGE);
|
|
else
|
|
ccLog::Warning(it.key() + " is not associated to a point cloud");
|
|
}
|
|
|
|
//test role
|
|
ccPointCloud* testCloud = nullptr;
|
|
bool needTestSuite = false;
|
|
masc::Feature::Set featuresTest;
|
|
std::vector<double> scalesTest;
|
|
if (loadedClouds.contains("TEST"))
|
|
{
|
|
testCloud = loadedClouds["TEST"];
|
|
loadedClouds.remove("TEST");
|
|
|
|
if (testCloud != corePoints.origin && testCloud != corePoints.cloud)
|
|
{
|
|
//we need a duplicated test suite!!!
|
|
needTestSuite = true;
|
|
//replace the main cloud by the test cloud
|
|
masc::Tools::NamedClouds loadedCloudsTest;
|
|
loadedCloudsTest = loadedClouds;
|
|
loadedCloudsTest[mainCloudLabel] = testCloud;
|
|
|
|
//simply reload the classification file to create duplicated features
|
|
masc::TrainParameters tempParams;
|
|
if (!masc::Tools::LoadTrainingFile(inputFilename, featuresTest, scalesTest, loadedCloudsTest, tempParams))
|
|
{
|
|
m_app->dispToConsole("Failed to load the training file (for test)", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//show the training dialog for the first time
|
|
Train3DMASCDialog trainDlg(m_app->getMainWindow());
|
|
trainDlg.setWindowModality(Qt::WindowModal); // to be able to move the confusion matrix window
|
|
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<int>(s_params.testDataRatio * 100));
|
|
trainDlg.testDataRatioSpinBox->setEnabled(testCloud == nullptr);
|
|
trainDlg.setInputFilePath(inputFilename);
|
|
|
|
//display the loaded features and let the user select the ones to use
|
|
trainDlg.setResultText("Select features and press 'Run'");
|
|
std::vector<FeatureSelection> 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);
|
|
}
|
|
for(double scale : scales)
|
|
trainDlg.addScale(scale, true);
|
|
trainDlg.connectScaleSelectionToFeatureSelection();
|
|
|
|
std::vector<FeatureSelection> originalFeaturesTest;
|
|
if (testCloud && needTestSuite)
|
|
{
|
|
originalFeaturesTest.reserve(featuresTest.size());
|
|
for (const masc::Feature::Shared& f : featuresTest)
|
|
{
|
|
originalFeaturesTest.push_back(FeatureSelection(f));
|
|
}
|
|
}
|
|
|
|
static bool s_keepAttributes = trainDlg.keepAttributesCheckBox->isChecked();
|
|
if (!trainDlg.exec())
|
|
{
|
|
delete group;
|
|
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());
|
|
progressDlg.setAutoClose(false);
|
|
if (!corePoints.prepare(&progressDlg))
|
|
{
|
|
m_app->dispToConsole("Failed to compute/prepare the core points!", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
delete group;
|
|
return;
|
|
}
|
|
|
|
if (corePoints.cloud != corePoints.origin)
|
|
{
|
|
//auto-hide the other clouds
|
|
for (ccPointCloud* pc : loadedClouds)
|
|
{
|
|
pc->setEnabled(false);
|
|
}
|
|
//set an explicit name for the core points
|
|
QString corePointsName = corePoints.origin->getName();
|
|
switch (corePoints.selectionMethod)
|
|
{
|
|
case masc::CorePoints::NONE:
|
|
break;
|
|
case masc::CorePoints::RANDOM:
|
|
corePointsName += "_SS_Random@" + QString::number(corePoints.selectionParam);
|
|
break;
|
|
case masc::CorePoints::SPATIAL:
|
|
corePointsName += "_SS_Spatial@" + QString::number(corePoints.selectionParam);
|
|
break;
|
|
default:
|
|
assert(false);
|
|
}
|
|
corePoints.cloud->setName(QString("Core points (%1)").arg(corePointsName));
|
|
group->addChild(corePoints.cloud);
|
|
}
|
|
|
|
if (group->getChildrenNumber() != 0)
|
|
{
|
|
m_app->addToDB(group);
|
|
QCoreApplication::processEvents();
|
|
}
|
|
else
|
|
{
|
|
delete group;
|
|
group = nullptr;
|
|
}
|
|
|
|
//train / test subsets
|
|
QSharedPointer<CCCoreLib::ReferenceCloud> trainSubset, testSubset;
|
|
float previousTestSubsetRatio = -1.0f;
|
|
SFCollector generatedScalarFields, generatedScalarFieldsTest;
|
|
|
|
//we will train + evaluate the classifier, then display the results
|
|
//then let the user change parameters and (potentially) start again
|
|
for (int iteration = 0; ; ++iteration)
|
|
{
|
|
//look for selected features
|
|
features.clear();
|
|
masc::Feature::Set toPrepare;
|
|
for (size_t i = 0; i < originalFeatures.size(); ++i)
|
|
{
|
|
originalFeatures[i].selected = trainDlg.isFeatureSelected(originalFeatures[i].feature->toString());
|
|
|
|
//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);
|
|
}
|
|
}
|
|
|
|
masc::Classifier classifier;
|
|
if (features.empty())
|
|
{
|
|
m_app->dispToConsole("No feature selected!", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
}
|
|
else
|
|
{
|
|
//prepare the features (should be done once)
|
|
if (!toPrepare.empty())
|
|
{
|
|
progressDlg.show();
|
|
QString error;
|
|
if (!masc::Tools::PrepareFeatures(corePoints, toPrepare, error, &progressDlg, &generatedScalarFields))
|
|
{
|
|
m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
generatedScalarFields.releaseSFs(false);
|
|
generatedScalarFieldsTest.releaseSFs(false);
|
|
return;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
//retrieve parameters
|
|
s_params.rt.maxDepth = trainDlg.maxDepthSpinBox->value();
|
|
s_params.rt.maxTreeCount = trainDlg.maxTreeCountSpinBox->value();
|
|
s_params.rt.activeVarCount = trainDlg.activeVarCountSpinBox->value();
|
|
s_params.rt.minSampleCount = trainDlg.minSampleCountSpinBox->value();
|
|
float testDataRatio = 0.0f;
|
|
|
|
if (!testCloud)
|
|
{
|
|
//we need to generate test subsets
|
|
testDataRatio = s_params.testDataRatio = trainDlg.testDataRatioSpinBox->value() / 100.0f;
|
|
if (testDataRatio < 0.0f || testDataRatio > 0.99f)
|
|
{
|
|
assert(false);
|
|
m_app->dispToConsole("Invalid test data ratio", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
trainSubset.clear();
|
|
testSubset.clear();
|
|
}
|
|
else if (previousTestSubsetRatio != testDataRatio)
|
|
{
|
|
if (!trainSubset)
|
|
trainSubset.reset(new CCCoreLib::ReferenceCloud(corePoints.cloud));
|
|
trainSubset->clear();
|
|
|
|
if (!testSubset)
|
|
testSubset.reset(new CCCoreLib::ReferenceCloud(corePoints.cloud));
|
|
testSubset->clear();
|
|
|
|
//randomly select the training points
|
|
if (!masc::Tools::RandomSubset(corePoints.cloud, testDataRatio, testSubset.data(), trainSubset.data()))
|
|
{
|
|
m_app->dispToConsole("Not enough memory to generate the test subsets", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
generatedScalarFields.releaseSFs(false);
|
|
generatedScalarFieldsTest.releaseSFs(false);
|
|
return;
|
|
}
|
|
previousTestSubsetRatio = testDataRatio;
|
|
}
|
|
}
|
|
|
|
//extract the sources (after having prepared the features!)
|
|
masc::Feature::Source::Set featureSources;
|
|
masc::Feature::ExtractSources(features, featureSources);
|
|
|
|
//train the classifier
|
|
{
|
|
QString errorMessage;
|
|
if (!classifier.train( corePoints.cloud,
|
|
s_params.rt,
|
|
featureSources,
|
|
errorMessage,
|
|
trainSubset.data(),
|
|
m_app,
|
|
m_app->getMainWindow()
|
|
))
|
|
{
|
|
m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
generatedScalarFields.releaseSFs(false);
|
|
generatedScalarFieldsTest.releaseSFs(false);
|
|
return;
|
|
}
|
|
trainDlg.setFirstRunDone();
|
|
// trainDlg.shouldSaveClassifier(); // useless?
|
|
}
|
|
|
|
//test the trained classifier
|
|
{
|
|
if (testCloud)
|
|
{
|
|
//look for selected features
|
|
if (needTestSuite)
|
|
{
|
|
featuresTest.clear();
|
|
masc::Feature::Set toPrepareTest;
|
|
for (size_t i = 0; i < originalFeaturesTest.size(); ++i)
|
|
{
|
|
originalFeaturesTest[i].selected = trainDlg.isFeatureSelected(originalFeatures[i].feature->toString());
|
|
//if the feature is selected
|
|
if (originalFeaturesTest[i].selected)
|
|
{
|
|
if (!originalFeaturesTest[i].prepared)
|
|
{
|
|
//we should prepare it first!
|
|
toPrepareTest.push_back(originalFeaturesTest[i].feature);
|
|
}
|
|
featuresTest.push_back(originalFeaturesTest[i].feature);
|
|
}
|
|
}
|
|
|
|
//prepare the features and the test cloud
|
|
if (!toPrepareTest.empty())
|
|
{
|
|
progressDlg.show();
|
|
QString error;
|
|
masc::CorePoints corePointsTest;
|
|
corePointsTest.cloud = corePointsTest.origin = testCloud;
|
|
corePointsTest.role = mainCloudLabel;
|
|
if (!masc::Tools::PrepareFeatures(corePointsTest, toPrepareTest, error, &progressDlg, &generatedScalarFieldsTest))
|
|
{
|
|
m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
generatedScalarFields.releaseSFs(false);
|
|
generatedScalarFieldsTest.releaseSFs(false);
|
|
return;
|
|
}
|
|
progressDlg.close();
|
|
QCoreApplication::processEvents();
|
|
m_app->redrawAll();
|
|
|
|
//flag the prepared features as 'prepared' ;)
|
|
for (FeatureSelection& fs : originalFeaturesTest)
|
|
{
|
|
if (fs.selected && !fs.prepared)
|
|
fs.prepared = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
masc::Classifier::AccuracyMetrics metrics;
|
|
QString errorMessage;
|
|
if (!classifier.evaluate( featureSources,
|
|
testCloud ? testCloud : corePoints.cloud,
|
|
metrics,
|
|
errorMessage,
|
|
trainDlg,
|
|
testCloud ? nullptr : testSubset.data(),
|
|
testCloud ? "Classification_prediction" : "", // outputSFName, empty is the test cloud is not a separate cloud
|
|
m_app->getMainWindow()))
|
|
{
|
|
m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
|
|
generatedScalarFields.releaseSFs(false);
|
|
generatedScalarFieldsTest.releaseSFs(false);
|
|
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<int>(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<float>(i, 0)));
|
|
assert(selectedFeatureIndex < importanceMat.rows);
|
|
originalFeatures[i].importance = importanceMat.at<float>(selectedFeatureIndex, 0);
|
|
++selectedFeatureIndex;
|
|
}
|
|
else
|
|
{
|
|
originalFeatures[i].importance = std::numeric_limits<float>::quiet_NaN();
|
|
}
|
|
trainDlg.setFeatureImportance(originalFeatures[i].feature->toString(), originalFeatures[i].importance);
|
|
}
|
|
|
|
trainDlg.sortByFeatureImportance();
|
|
|
|
// if the checkbox "Save traces" is checked
|
|
if (trainDlg.getSaveTrace())
|
|
{
|
|
//save the classifier in the trace directory with a generic name depending on the run
|
|
QString tracePath = trainDlg.getTracePath();
|
|
if (!tracePath.isEmpty())
|
|
{
|
|
QString outputFilePath = tracePath + "/run_" + QString::number(trainDlg.getRun()) + ".txt";
|
|
if (masc::Tools::SaveClassifier(outputFilePath, features, mainCloudLabel, classifier, m_app->getMainWindow()))
|
|
{
|
|
m_app->dispToConsole("Classifier succesfully saved to " + outputFilePath, ccMainAppInterface::STD_CONSOLE_MESSAGE);
|
|
trainDlg.setClassifierSaved();
|
|
}
|
|
else
|
|
{
|
|
m_app->dispToConsole("Failed to save classifier file");
|
|
}
|
|
|
|
// save feature importance
|
|
QString filename = tracePath + "/run_" + QString::number(trainDlg.getRun()) + "_feature_importance.txt";
|
|
QFile file(filename);
|
|
if (!file.open(QFile::Text | QFile::WriteOnly))
|
|
{
|
|
ccLog::Warning(QString("Can't open file '%1' for writing").arg(filename));
|
|
}
|
|
QTextStream stream(&file);
|
|
stream << "# feature importance" << endl;
|
|
for (size_t i = 0; i < originalFeatures.size(); ++i)
|
|
{
|
|
if (originalFeatures[i].selected)
|
|
{
|
|
stream << originalFeatures[i].feature->toString() << " " << originalFeatures[i].importance << endl;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//now wait for the user input
|
|
while (true) // ew!
|
|
{
|
|
if (!trainDlg.exec())
|
|
{
|
|
saveTrainParameters(s_params);
|
|
//the dialog can be closed
|
|
if (trainDlg.keepAttributesCheckBox->isChecked())
|
|
s_keepAttributes = true;
|
|
else
|
|
s_keepAttributes = false;
|
|
generatedScalarFields.releaseSFs(s_keepAttributes);
|
|
generatedScalarFieldsTest.releaseSFs(s_keepAttributes);
|
|
return;
|
|
}
|
|
|
|
//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, mainCloudLabel, 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void q3DMASCPlugin::registerCommands(ccCommandLineInterface* cmd)
|
|
{
|
|
if (!cmd)
|
|
{
|
|
assert(false);
|
|
return;
|
|
}
|
|
|
|
cmd->registerCommand(ccCommandLineInterface::Command::Shared(new Command3DMASCClassif));
|
|
}
|