Files
q3DMASC/q3DMASC.cpp
T
2023-12-22 16:35:41 +01:00

779 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");
}
QString exportFilePath = tracePath + "/run_" + QString::number(trainDlg.getRun()) + ".csv";
}
}
}
}
//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));
}