diff --git a/Classify3DMASCDialog.ui b/Classify3DMASCDialog.ui
new file mode 100644
index 0000000..6a248af
--- /dev/null
+++ b/Classify3DMASCDialog.ui
@@ -0,0 +1,182 @@
+
+
+ Classify3DMASCDialog
+
+
+
+ 0
+ 0
+ 700
+ 250
+
+
+
+ 3DMASC Classify
+
+
+ -
+
+
-
+
+
+ Classifier file
+
+
+
+ -
+
+
+ true
+
+
+
+
+
+ -
+
+
+ Data
+
+
+
-
+
+
+ Cloud
+
+
+
+ -
+
+
+ Role
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ PC1
+
+
+
+ -
+
+
+ PC2
+
+
+
+ -
+
+
+ CTX
+
+
+
+
+
+
+ -
+
+
+ color:red;
+
+
+ Assign each role to the right cloud, and select the role of the cloud to be classified
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 23
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Classify3DMASCDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Classify3DMASCDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/q3DMASC.cpp b/q3DMASC.cpp
index f06f061..0a0a63a 100644
--- a/q3DMASC.cpp
+++ b/q3DMASC.cpp
@@ -21,6 +21,7 @@
#include "q3DMASCDisclaimerDialog.h"
#include "q3DMASCClassifier.h"
#include "q3DMASCTools.h"
+#include "qClassify3DMASCDialog.h"
//qCC_db
#include
@@ -45,7 +46,8 @@ 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(selectedEntities.size() == 1 && selectedEntities[0]->isA(CC_TYPES::POINT_CLOUD));
+ m_classifyAction->setEnabled(m_app->dbRootObject()->getChildrenNumber() != 0);
}
if (m_trainAction)
@@ -96,7 +98,84 @@ void q3DMASCPlugin::doClassifyAction()
return;
}
- //TODO
+ 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();
+ }
+
+ QSet cloudLabels;
+ if (!masc::Tools::LoadClassifierCloudLabels(inputFilename, cloudLabels))
+ {
+ 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() > 3)
+ {
+ m_app->dispToConsole("This classifier uses more than 3 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);
+ if (!classifDlg.exec())
+ {
+ //process cancelled by the user
+ return;
+ }
+
+ masc::Tools::NamedClouds clouds;
+ QString mainCloudLabel;
+ classifDlg.getClouds(clouds, mainCloudLabel);
+
+ masc::Feature::Set features;
+ masc::Classifier classifier;
+ if (!masc::Tools::LoadClassifier(inputFilename, clouds, features, classifier, m_app->getMainWindow()))
+ {
+ return;
+ }
+
+ //the 'main cloud' is the cloud that should be classified
+ masc::CorePoints corePoints;
+ 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
+ QString error;
+ if (!masc::Tools::PrepareFeatures(corePoints, features, error, &pDlg))
+ {
+ m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
+ return;
+ }
+ pDlg.close();
+ QCoreApplication::processEvents();
+ pDlg.setAutoClose(true); //restore the default behavior of the progress dialog
+
+ //apply classifier
+ {
+ QString errorMessage;
+ if (!classifier.classify(features, corePoints.cloud, errorMessage, m_app->getMainWindow()))
+ {
+ m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE);
+ return;
+ }
+ }
}
void q3DMASCPlugin::doTrainAction()
@@ -105,17 +184,6 @@ void q3DMASCPlugin::doTrainAction()
if (!ShowTrainDisclaimer(m_app))
return;
- //if (m_selectedEntities.size() != 2
- // || !m_selectedEntities[0]->isA(CC_TYPES::POINT_CLOUD)
- // || !m_selectedEntities[1]->isA(CC_TYPES::POINT_CLOUD))
- //{
- // m_app->dispToConsole("Select two point clouds!",ccMainAppInterface::ERR_CONSOLE_MESSAGE);
- // return;
- //}
- //
- //ccPointCloud* cloud1 = static_cast(m_selectedEntities[0]);
- //ccPointCloud* cloud2 = static_cast(m_selectedEntities[1]);
-
masc::TrainParameters params;
if (params.testDataRatio < 0 || params.testDataRatio > 0.99f)
{
@@ -123,31 +191,12 @@ void q3DMASCPlugin::doTrainAction()
return;
}
- masc::Feature::Set features;
-#if 0
- if (m_selectedEntities.empty() || !m_selectedEntities.front()->isA(CC_TYPES::POINT_CLOUD))
- {
- m_app->dispToConsole("Select one and only one point cloud!", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
- return;
- }
-
- ccPointCloud* cloud = static_cast(m_selectedEntities.front());
-
- //features
- {
- Feature::Shared featureZ(new PointFeature(PointFeature::Z, cloud));
- features.push_back(featureZ);
-
- Feature::Shared featureIntensity(new PointFeature(PointFeature::Intensity, cloud));
- features.push_back(featureIntensity);
- }
-#else
QString inputFilename;
{
QSettings settings;
settings.beginGroup("3DMASC");
QString inputPath = settings.value("FilePath", QCoreApplication::applicationDirPath()).toString();
- inputFilename = QFileDialog::getOpenFileName(m_app->getMainWindow(), "Load 3DMASC script file", inputPath, "*.txt");
+ inputFilename = QFileDialog::getOpenFileName(m_app->getMainWindow(), "Load 3DMASC training file", inputPath, "*.txt");
if (inputFilename.isNull())
{
//process cancelled by the user
@@ -159,7 +208,8 @@ void q3DMASCPlugin::doTrainAction()
std::vector loadedClouds;
masc::CorePoints corePoints;
- if (!masc::Tools::LoadFile(inputFilename, features, loadedClouds, corePoints))
+ masc::Feature::Set features;
+ if (!masc::Tools::LoadTrainingFile(inputFilename, features, loadedClouds, corePoints))
{
while (!loadedClouds.empty())
{
@@ -223,8 +273,6 @@ void q3DMASCPlugin::doTrainAction()
QCoreApplication::processEvents();
pDlg.setAutoClose(true); //restore the default behavior of the progress dialog
-#endif
-
//randomly select the training points
QScopedPointer trainSubset(new CCLib::ReferenceCloud(corePoints.cloud));
QScopedPointer testSubset(new CCLib::ReferenceCloud(corePoints.cloud));
@@ -234,18 +282,8 @@ void q3DMASCPlugin::doTrainAction()
return;
}
+ //train the classifier
masc::Classifier classifier;
- //QString outputFilename = QCoreApplication::applicationDirPath() + "/classifier.yaml";
- //if (QFile(outputFilename).exists())
- //{
- // if (!classifier.fromFile(outputFilename, m_app->getMainWindow()))
- // {
- // m_app->dispToConsole("Failed to load previous classifier file", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
- // return;
- // }
- // m_app->dispToConsole("Previous classifier loaded", ccMainAppInterface::WRN_CONSOLE_MESSAGE);
- //}
- //else
{
QString errorMessage;
if (!classifier.train(corePoints.cloud, params.rt, features, errorMessage, trainSubset.data(), m_app->getMainWindow()))
@@ -259,7 +297,7 @@ void q3DMASCPlugin::doTrainAction()
QSettings settings;
settings.beginGroup("3DMASC");
QString outputPath = settings.value("FilePath", QCoreApplication::applicationDirPath()).toString();
- outputFilename = QFileDialog::getSaveFileName(m_app->getMainWindow(), "Save 3DMASC classifier", outputPath, "*.yaml");
+ outputFilename = QFileDialog::getSaveFileName(m_app->getMainWindow(), "Save 3DMASC classifier", outputPath, "*.txt");
if (outputFilename.isNull())
{
//process cancelled by the user
@@ -270,8 +308,10 @@ void q3DMASCPlugin::doTrainAction()
}
//save the classifier
- classifier.toFile(outputFilename, m_app->getMainWindow());
- m_app->dispToConsole("Classifier succesfully created", ccMainAppInterface::WRN_CONSOLE_MESSAGE);
+ if (masc::Tools::SaveClassifier(outputFilename, features, classifier, m_app->getMainWindow()))
+ {
+ m_app->dispToConsole("Classifier succesfully saved to " + outputFilename, ccMainAppInterface::STD_CONSOLE_MESSAGE);
+ }
}
//test classifier
diff --git a/q3DMASCClassifier.cpp b/q3DMASCClassifier.cpp
index 25be940..d46cae2 100644
--- a/q3DMASCClassifier.cpp
+++ b/q3DMASCClassifier.cpp
@@ -22,6 +22,8 @@
//qCC_db
#include
+#include
+#include
#include
//qCC_io
@@ -94,6 +96,126 @@ static QSharedPointer GetSource(const Feature::Shared& f, c
return source;
}
+bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QString& errorMessage, QWidget* parentWidget/*=nullptr*/)
+{
+ if (!cloud)
+ {
+ assert(false);
+ errorMessage = QObject::tr("Invalid input");
+ return false;
+ }
+
+ if (!m_rtrees || !m_rtrees->isTrained())
+ {
+ errorMessage = QObject::tr("Classifier hasn't been trained yet");
+ return false;
+ }
+
+ if (features.empty())
+ {
+ errorMessage = QObject::tr("Training method called without any feature?!");
+ return false;
+ }
+
+ //look for the classification field
+ CCLib::ScalarField* classificationSF = nullptr;
+ int classifSFIdx = cloud->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_CLASSIFICATION]); //LAS_FIELD_NAMES[LAS_CLASSIFICATION] = "Classification"
+ if (!classifSFIdx)
+ {
+ //create it if necessary
+ ccScalarField* _classificationSF = new ccScalarField(LAS_FIELD_NAMES[LAS_CLASSIFICATION]);
+ if (!_classificationSF->resizeSafe(cloud->size()))
+ {
+ _classificationSF->release();
+ errorMessage = QObject::tr("Not enough memory");
+ return false;
+ }
+ classifSFIdx = cloud->addScalarField(_classificationSF);
+ classificationSF = _classificationSF;
+ cloud->setCurrentDisplayedScalarField(classifSFIdx);
+ }
+ else
+ {
+ classificationSF = cloud->getScalarField(classifSFIdx);
+ }
+ assert(classificationSF);
+ classificationSF->fill(0); //0 = no classification?
+
+ int sampleCount = static_cast(cloud->size());
+ int attributesPerSample = static_cast(features.size());
+
+ ccLog::Print(QObject::tr("[3DMASC] Classifying %1 points with %2 feature(s)").arg(sampleCount).arg(attributesPerSample));
+
+ //allocate the data matrix
+ cv::Mat test_data;
+ try
+ {
+ test_data.create(1, attributesPerSample, CV_32FC1);
+ }
+ catch (const cv::Exception& cvex)
+ {
+ errorMessage = cvex.msg.c_str();
+ return false;
+ }
+
+ //create the field wrappers
+ std::vector< QSharedPointer > wrappers;
+ {
+ wrappers.reserve(attributesPerSample);
+ for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
+ {
+ const Feature::Shared &f = features[fIndex];
+ if (!f)
+ {
+ assert(false);
+ return false;
+ }
+
+ QSharedPointer source = GetSource(f, cloud);
+ if (!source || !source->isValid())
+ {
+ assert(false);
+ errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(f->sourceName);
+ return false;
+ }
+
+ wrappers.push_back(source);
+ }
+ }
+
+ QScopedPointer pDlg;
+ if (parentWidget)
+ {
+ pDlg.reset(new ccProgressDialog(parentWidget));
+ pDlg->setLabelText(QString("Classify (%1 points)").arg(sampleCount));
+ pDlg->show();
+ QCoreApplication::processEvents();
+ }
+ CCLib::NormalizedProgress nProgress(pDlg.data(), cloud->size());
+
+ for (unsigned i = 0; i < cloud->size(); ++i)
+ {
+ for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
+ {
+ double value = wrappers[fIndex]->pointValue(i);
+ test_data.at(0, fIndex) = static_cast(value);
+ }
+
+ float predictedClass = m_rtrees->predict(test_data.row(0));
+ classificationSF->setValue(i, static_cast(predictedClass));
+
+ if (pDlg && !nProgress.oneStep())
+ {
+ //process cancelled by the user
+ classificationSF->computeMinAndMax();
+ return false;
+ }
+ }
+ classificationSF->computeMinAndMax();
+
+ return true;
+}
+
bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* testSubset, AccuracyMetrics& metrics, QString& errorMessage, QWidget* parentWidget/*=nullptr*/)
{
metrics.sampleCount = metrics.goodGuess = 0;
@@ -154,6 +276,16 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t
return false;
}
+ QScopedPointer pDlg;
+ if (parentWidget)
+ {
+ pDlg.reset(new ccProgressDialog(parentWidget));
+ pDlg->setLabelText(QString("Evaluating the classifier on %1 points").arg(testSampleCount));
+ pDlg->show();
+ QCoreApplication::processEvents();
+ }
+ CCLib::NormalizedProgress nProgress(pDlg.data(), testSampleCount);
+
//fill the data matrix
for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
{
@@ -180,7 +312,7 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t
}
}
- //estimate the efficiency of the classiier
+ //estimate the efficiency of the classifier
{
metrics.sampleCount = testSubset->size();
metrics.goodGuess = 0;
@@ -201,6 +333,12 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t
{
++metrics.goodGuess;
}
+
+ if (pDlg && !nProgress.oneStep())
+ {
+ //process cancelled by the user
+ return false;
+ }
}
metrics.ratio = static_cast(metrics.goodGuess) / metrics.sampleCount;
@@ -310,6 +448,7 @@ bool Classifier::train( const ccPointCloud* cloud,
pDlg->setRange(0, 0); //infinite loop
pDlg->setLabelText("Training classifier");
pDlg->show();
+ QCoreApplication::processEvents();
}
m_rtrees = cv::ml::RTrees::create();
diff --git a/q3DMASCClassifier.h b/q3DMASCClassifier.h
index 027c47b..1d5fa5a 100644
--- a/q3DMASCClassifier.h
+++ b/q3DMASCClassifier.h
@@ -56,6 +56,9 @@ namespace masc
//! Evaluates the classifier
bool evaluate(const Feature::Set& features, CCLib::ReferenceCloud* testSubset, AccuracyMetrics& metrics, QString& errorMessage, QWidget* parentWidget = nullptr);
+ //! Applies the classifier
+ bool classify(const Feature::Set& features, ccPointCloud* cloud, QString& errorMessage, QWidget* parentWidget = nullptr);
+
//! Returns whether the classifier is valid or not
bool isValid() const;
diff --git a/q3DMASCTools.cpp b/q3DMASCTools.cpp
index 3587565..fa315dc 100644
--- a/q3DMASCTools.cpp
+++ b/q3DMASCTools.cpp
@@ -41,8 +41,17 @@
using namespace masc;
-bool Tools::SaveFeatureDescriptors(QString filename, const Feature::Set& features)
+bool Tools::SaveClassifier(QString filename, const Feature::Set& features, const masc::Classifier& classifier, QWidget* parent/*=nullptr*/)
{
+ //first save the classifier data (same base filename but with the ymal extension)
+ QFileInfo fi(filename);
+ QString yamlFilename = fi.completeBaseName() + ".yaml";
+ if (!classifier.toFile(fi.absoluteFilePath() + "/" + yamlFilename, parent))
+ {
+ ccLog::Error("Failed to save the classifier data");
+ return false;
+ }
+
QFile file(filename);
if (!file.open(QFile::Text | QFile::WriteOnly))
{
@@ -52,16 +61,522 @@ bool Tools::SaveFeatureDescriptors(QString filename, const Feature::Set& feature
QTextStream stream(&file);
- //header
- stream << "#3DMASC classifier" << endl;
+ stream << "# 3DMASC classifier file" << endl;
+ stream << "classifier: " << yamlFilename << endl;
+
+ //look for all clouds (labels)
+ QSet cloudLabels;
+ for (Feature::Shared f : features)
+ {
+ if (f->cloud1)
+ cloudLabels.insert(f->cloud1Label);
+ if (f->cloud2)
+ cloudLabels.insert(f->cloud2Label);
+ }
+
+ stream << "# Clouds (roles)" << endl;
+ for (const QString& label : cloudLabels)
+ {
+ stream << "cloud: " << label << endl;
+ }
+
+ stream << "# Features" << endl;
+ for (Feature::Shared f : features)
+ {
+ stream << "feature: " << f->toString() << endl;
+ }
return true;
}
-bool Tools::LoadFile( QString filename,
- Feature::Set& rawFeatures,
- std::vector& loadedClouds,
- CorePoints& corePoints)
+bool Tools::LoadClassifierCloudLabels(QString filename, QSet& labels)
+{
+ QFile file(filename);
+ if (!file.open(QFile::Text | QFile::ReadOnly))
+ {
+ ccLog::Warning(QString("Can't open file '%1'").arg(filename));
+ return false;
+ }
+
+ QTextStream stream(&file);
+ for (int lineNumber = 0; ; ++lineNumber)
+ {
+ QString line = stream.readLine();
+ if (line.isNull())
+ {
+ //eof
+ break;
+ }
+ ++lineNumber;
+
+ if (line.startsWith("CLOUD:"))
+ {
+ QString command = line.mid(6).trimmed();
+ QStringList tokens = command.split('=');
+ if (tokens.size() == 0)
+ {
+ ccLog::Warning("Malformed file: expecting some tokens after 'cloud:' on line #" + QString::number(lineNumber));
+ return false;
+ }
+
+ QString label = tokens.front();
+ labels.insert(label);
+ }
+ }
+
+ return true;
+}
+
+static bool CreateFeaturesFromCommand(const QString& command, int lineNumber, const Tools::NamedClouds& clouds, std::vector& rawFeatures, std::vector& scales)
+{
+ QStringList tokens = command.split('_');
+ if (tokens.empty())
+ {
+ ccLog::Warning("Malformed file: expecting at least one token after 'feature:' on line #" + QString::number(lineNumber));
+ return false;
+ }
+
+ Feature::Shared feature;
+
+ //read the type
+ QString typeStr = tokens[0].trimmed().toUpper();
+ {
+ for (int iteration = 0; iteration < 1; ++iteration) //fake loop for easy break
+ {
+ PointFeature::PointFeatureType pointFeatureType = PointFeature::FromUpperString(typeStr);
+ if (pointFeatureType != PointFeature::Invalid)
+ {
+ //we have a point feature
+ PointFeature* pointFeature = new PointFeature(pointFeatureType);
+
+ //specific case: 'SF#'
+ if (pointFeatureType == PointFeature::SF)
+ {
+ QString sfIndexStr = typeStr.mid(2);
+ bool ok = true;
+ int sfIndex = sfIndexStr.toInt(&ok);
+ if (!ok)
+ {
+ ccLog::Warning(QString("Malformed file: expecting a valid integer value after 'SF' on line #%1").arg(lineNumber));
+ delete pointFeature;
+ return false;
+ }
+ pointFeature->sourceSFIndex = sfIndex;
+ }
+
+ feature.reset(pointFeature);
+ break;
+ }
+
+ NeighborhoodFeature::NeighborhoodFeatureType neighborhoodFeatureType = NeighborhoodFeature::FromUpperString(typeStr);
+ if (neighborhoodFeatureType != NeighborhoodFeature::Invalid)
+ {
+ //we have a neighborhood feature
+ feature = NeighborhoodFeature::Shared(new NeighborhoodFeature(neighborhoodFeatureType));
+ break;
+ }
+
+ ContextBasedFeature::ContextBasedFeatureType contextBasedFeatureType = ContextBasedFeature::FromUpperString(typeStr);
+ if (contextBasedFeatureType != ContextBasedFeature::Invalid)
+ {
+ //we have a context-based feature
+ feature = ContextBasedFeature::Shared(new ContextBasedFeature(contextBasedFeatureType));
+ break;
+ }
+
+ DualCloudFeature::DualCloudFeatureType dualCloudFeatureType = DualCloudFeature::FromUpperString(typeStr);
+ if (dualCloudFeatureType != DualCloudFeature::Invalid)
+ {
+ //we have a dual cloud feature
+ feature = DualCloudFeature::Shared(new DualCloudFeature(dualCloudFeatureType));
+ break;
+ }
+
+ if (!feature)
+ {
+ ccLog::Warning(QString("Malformed file: unrecognized token '%1' after 'feature:' on line #%2").arg(typeStr).arg(lineNumber));
+ return false;
+ }
+ }
+ }
+ assert(feature);
+
+ //read the scales
+ bool useAllScales = false;
+ {
+ QString scaleStr = tokens[1].toUpper();
+ if (!scaleStr.startsWith("SC"))
+ {
+ ccLog::Warning(QString("Malformed file: unrecognized token '%1' (expecting the scale descriptor 'SC...' on line #%2").arg(typeStr).arg(lineNumber));
+ return false;
+ }
+
+ if (scaleStr == "SC0")
+ {
+ //no scale
+ }
+ else if (scaleStr == "SCX")
+ {
+ //all scales
+ useAllScales = true;
+ }
+ else
+ {
+ //read the specific scale index
+ bool ok = true;
+ feature->scale = scaleStr.mid(2).toDouble(&ok);
+ if (!ok)
+ {
+ ccLog::Warning(QString("Malformed file: expecting a valid number after 'SC:' on line #%1").arg(lineNumber));
+ return false;
+ }
+ }
+ }
+
+ //process the next tokens (may not be ordered)
+ int cloudCount = 0;
+ bool statDefined = false;
+ bool mathDefined = false;
+ for (int i = 2; i < tokens.size(); ++i)
+ {
+ QString token = tokens[i].trimmed().toUpper();
+
+ //is the token a 'stat' one?
+ if (!statDefined)
+ {
+ if (token == "MEAN")
+ {
+ feature->stat = Feature::MEAN;
+ statDefined = true;
+ }
+ else if (token == "MODE")
+ {
+ feature->stat = Feature::MODE;
+ statDefined = true;
+ }
+ else if (token == "STD")
+ {
+ feature->stat = Feature::STD;
+ statDefined = true;
+ }
+ else if (token == "RANGE")
+ {
+ feature->stat = Feature::RANGE;
+ statDefined = true;
+ }
+ else if (token == "SKEW")
+ {
+ feature->stat = Feature::SKEW;
+ statDefined = true;
+ }
+
+ if (statDefined)
+ {
+ continue;
+ }
+ }
+
+ //is the token a cloud name?
+ if (cloudCount < 2)
+ {
+ bool cloudNameMatches = false;
+ for (QMap::const_iterator it = clouds.begin(); it != clouds.end(); ++it)
+ {
+ QString key = it.key().toUpper();
+ if (key == token)
+ {
+ if (cloudCount == 0)
+ {
+ feature->cloud1 = it.value();
+ feature->cloud1Label = key;
+ }
+ else if (cloudCount == 1)
+ {
+ feature->cloud2 = it.value();
+ feature->cloud2Label = key;
+ }
+ else
+ {
+ //we can't fall here
+ assert(false);
+ }
+ ++cloudCount;
+ cloudNameMatches = true;
+ break;
+ }
+ }
+
+ if (cloudNameMatches)
+ {
+ continue;
+ }
+ }
+
+ //is the token a 'math' one?
+ if (!mathDefined)
+ {
+ if (token == "MINUS")
+ {
+ feature->op = Feature::MINUS;
+ mathDefined = true;
+ }
+ else if (token == "PLUS")
+ {
+ feature->op = Feature::PLUS;
+ mathDefined = true;
+ }
+ else if (token == "DIVIDE")
+ {
+ feature->op = Feature::DIVIDE;
+ mathDefined = true;
+ }
+ else if (token == "MULTIPLY")
+ {
+ feature->op = Feature::MULTIPLY;
+ mathDefined = true;
+ }
+
+ if (mathDefined)
+ {
+ continue;
+ }
+ }
+
+ //is the token a 'context' descriptor?
+ if (feature->getType() == Feature::Type::ContextBasedFeature && token.startsWith("CTX"))
+ {
+ //read the context label
+ QString ctxLabelStr = token.mid(2);
+ bool ok = true;
+ int ctxLabel = ctxLabelStr.toInt(&ok);
+ if (!ok)
+ {
+ ccLog::Warning(QString("Malformed file: expecting a valid integer value after 'CTX' on line #%1").arg(lineNumber));
+ return false;
+ }
+ static_cast(feature.data())->ctxClassLabel = ctxLabel;
+ continue;
+ }
+
+ //if we are here, it means we couldn't find a correspondance for the current token
+ ccLog::Warning(QString("Malformed file: unrecognized or unexpected token '%1' on line #%2").arg(token).arg(lineNumber));
+ return false;
+ }
+
+ //now create the various versions of rules (if any)
+ if (useAllScales)
+ {
+ if (scales.empty())
+ {
+ ccLog::Warning("Malformed file: 'SCx' token used while no scale is defined" + QString(" (line %1)").arg(lineNumber));
+ return false;
+ }
+ feature->scale = scales.front();
+
+ //we will duplicate the original feature AFTER having checked its consistency!
+ }
+
+ //now check the consistency of the rule
+ QString errorMessage;
+ if (!feature->checkValidity(errorMessage))
+ {
+ ccLog::Warning("Malformed feature: " + errorMessage + QString(" (line %1)").arg(lineNumber));
+ return false;
+ }
+
+ //save it
+ rawFeatures.push_back(feature);
+
+ if (useAllScales)
+ {
+ for (size_t i = 1; i < scales.size(); ++i)
+ {
+ //copy the original rule
+ Feature::Shared newFeature = feature->clone();
+ newFeature->scale = scales.at(i);
+
+ //as we only change the scale value, all the duplicated features should be valid
+ assert(newFeature->checkValidity(errorMessage));
+
+ rawFeatures.push_back(newFeature);
+ }
+ }
+
+ return true;
+}
+
+static bool ReadScales(const QString& command, std::vector& scales, int lineNumber)
+{
+ assert(scales.empty());
+
+ QStringList tokens = command.split(';');
+ if (tokens.empty())
+ {
+ ccLog::Warning("Malformed file: expecting at least one token after 'scales:' on line #" + QString::number(lineNumber));
+ return false;
+ }
+
+ for (const QString& token : tokens)
+ {
+ if (token.contains(':'))
+ {
+ //it's probably a range
+ QStringList subTokens = token.trimmed().split(':');
+ if (subTokens.size() != 3)
+ {
+ ccLog::Warning(QString("Malformed file: expecting 3 tokens for a range of scales (%1)").arg(token));
+ return false;
+ }
+ bool ok[3] = { true, true, true };
+ double start = subTokens[0].trimmed().toDouble(ok);
+ double step = subTokens[1].toDouble(ok + 1);
+ double stop = subTokens[2].toDouble(ok + 2);
+ if (!ok[0] || !ok[1] || !ok[2])
+ {
+ ccLog::Warning(QString("Malformed file: invalid values in scales range (%1) on line #%2").arg(token).arg(lineNumber));
+ return false;
+ }
+ if (stop < start || step <= 1.0 - 6)
+ {
+ ccLog::Warning(QString("Malformed file: invalid range (%1) on line #%2").arg(token).arg(lineNumber));
+ return false;
+ }
+
+ for (double v = start; v <= stop + 1.0e-6; v += step)
+ {
+ scales.push_back(v);
+ }
+ }
+ else
+ {
+ bool ok = true;
+ double v = token.trimmed().toDouble(&ok);
+ if (!ok)
+ {
+ ccLog::Warning(QString("Malformed file: invalid scale value (%1) on line #%2").arg(token).arg(lineNumber));
+ return false;
+ }
+ scales.push_back(v);
+ }
+ }
+
+ scales.shrink_to_fit();
+ return true;
+}
+
+static bool ReadCorePoints(const QString& command, const Tools::NamedClouds& clouds, masc::CorePoints& corePoints, int lineNumber)
+{
+ QStringList tokens = command.split('_');
+ if (tokens.empty())
+ {
+ ccLog::Warning("Malformed file: expecting tokens after 'core_points:' on line #" + QString::number(lineNumber));
+ return false;
+ }
+ QString pcName = tokens[0].trimmed();
+ if (!clouds.contains(pcName))
+ {
+ ccLog::Warning(QString("Malformed file: unknown cloud '%1' on line #%2 (make sure it is declared before the core points)").arg(pcName).arg(lineNumber));
+ return false;
+ }
+ corePoints.origin = clouds[pcName];
+
+ //should we sub-sample the origin cloud?
+ if (tokens.size() > 1)
+ {
+ if (tokens[1].toUpper() == "SS")
+ {
+ if (tokens.size() < 3)
+ {
+ ccLog::Warning("Malformed file: missing token after 'SS' on line #" + QString::number(lineNumber));
+ return false;
+ }
+ QString options = tokens[2];
+ if (options.startsWith('R'))
+ {
+ corePoints.selectionMethod = CorePoints::RANDOM;
+ }
+ else if (options.startsWith('S'))
+ {
+ corePoints.selectionMethod = CorePoints::SPATIAL;
+ }
+ else
+ {
+ ccLog::Warning("Malformed file: unknown option after 'SS' on line #" + QString::number(lineNumber));
+ return false;
+ }
+
+ //read the subsampling parameter (ignore the first character)
+ bool ok = false;
+ corePoints.selectionParam = options.mid(1).toDouble(&ok);
+ if (!ok)
+ {
+ ccLog::Warning("Malformed file: expecting a number after 'SS_X' on line #" + QString::number(lineNumber));
+ return false;
+ }
+
+ } //end of subsampling options
+ }
+
+ return true;
+}
+
+static bool ReadCloud(const QString& command, Tools::NamedClouds& clouds, QDir& defaultDir, int lineNumber)
+{
+ QStringList tokens = command.split('=');
+ if (tokens.size() != 2)
+ {
+ ccLog::Warning("Malformed file: expecting 2 tokens after 'cloud:' on line #" + QString::number(lineNumber));
+ return false;
+ }
+
+ QString pcName = tokens[0].trimmed();
+ QString pcFilename = defaultDir.absoluteFilePath(tokens[1].trimmed());
+ //try to open the cloud
+ {
+ FileIOFilter::LoadParameters parameters;
+ parameters.alwaysDisplayLoadDialog = false;
+ CC_FILE_ERROR error = CC_FERR_NO_ERROR;
+ ccHObject* object = FileIOFilter::LoadFromFile(pcFilename, parameters, error);
+ if (error != CC_FERR_NO_ERROR || !object)
+ {
+ //error message already issued
+ if (object)
+ delete object;
+ return false;
+ }
+ ccHObject::Container cloudsInFile;
+ object->filterChildren(cloudsInFile, false, CC_TYPES::POINT_CLOUD, true);
+ if (cloudsInFile.empty())
+ {
+ ccLog::Warning("File doesn't contain a single cloud");
+ delete object;
+ return false;
+ }
+ else if (cloudsInFile.size() > 1)
+ {
+ ccLog::Warning("File contains more than one cloud, only the first one will be kept");
+ }
+ ccPointCloud* pc = static_cast(cloudsInFile.front());
+ for (size_t i = 1; i < cloudsInFile.size(); ++i)
+ {
+ delete cloudsInFile[i];
+ }
+ if (pc->getParent())
+ pc->getParent()->detachChild(pc);
+ pc->setName(pcName); //DGM: warning, may not be acceptable in the GUI version?
+ clouds.insert(pcName, pc);
+ }
+
+ return true;
+}
+
+static bool LoadFileCommon( const QString& filename,
+ Tools::NamedClouds& clouds,
+ bool cloudsAreProvided,
+ std::vector& rawFeatures,
+ masc::CorePoints* corePoints = nullptr,
+ masc::Classifier* classifier = nullptr,
+ QWidget* parent = nullptr)
{
QFileInfo fi(filename);
if (!fi.exists())
@@ -81,7 +596,6 @@ bool Tools::LoadFile( QString filename,
{
assert(rawFeatures.empty());
std::vector scales;
- QMap clouds;
QTextStream stream(&file);
for (int lineNumber = 0; ; ++lineNumber)
@@ -106,112 +620,57 @@ bool Tools::LoadFile( QString filename,
line = line.left(commentIndex);
QString upperLine = line.toUpper();
- if (upperLine.startsWith("CLOUD:")) //clouds
+ if (upperLine.startsWith("CLASSIFIER:")) //classifier
{
- QString command = line.mid(6);
- QStringList tokens = command.split('=');
- if (tokens.size() != 2)
+ if (!classifier)
{
- ccLog::Warning("Malformed file: expecting 2 tokens after 'cloud:' on line #" + QString::number(lineNumber));
+ //no need to load the classifier
+ continue;
+ }
+ if (classifier->isValid())
+ {
+ ccLog::Warning("Malformed file: can't declare the classifier file twice! (line #" + QString::number(lineNumber) + ")");
return false;
}
- QString pcName = tokens[0].trimmed();
- QString pcFilename = fi.absoluteDir().absoluteFilePath(tokens[1].trimmed());
- //try to open the cloud
+ QString yamlFilename = line.mid(11).trimmed();
+ if (!classifier->fromFile(fi.absolutePath() + "/" + yamlFilename, parent))
{
- FileIOFilter::LoadParameters parameters;
- parameters.alwaysDisplayLoadDialog = false;
- CC_FILE_ERROR error = CC_FERR_NO_ERROR;
- ccHObject* object = FileIOFilter::LoadFromFile(pcFilename, parameters, error);
- if (error != CC_FERR_NO_ERROR || !object)
- {
- //error message already issued
- if (object)
- delete object;
- return false;
- }
- ccHObject::Container cloudsInFile;
- object->filterChildren(cloudsInFile, false, CC_TYPES::POINT_CLOUD, true);
- if (cloudsInFile.empty())
- {
- ccLog::Warning("File doesn't contain a single cloud");
- delete object;
- return false;
- }
- else if (cloudsInFile.size() > 1)
- {
- ccLog::Warning("File contains more than one cloud, only the first one will be kept");
- }
- ccPointCloud* pc = static_cast(cloudsInFile.front());
- for (size_t i = 1; i < cloudsInFile.size(); ++i)
- {
- delete cloudsInFile[i];
- }
- if (pc->getParent())
- pc->getParent()->detachChild(pc);
- pc->setName(pcName); //DGM: warning, may not be acceptable in the GUI version?
- clouds.insert(pcName, pc);
- loadedClouds.push_back(pc);
+ ccLog::Warning("Failed to load the classifier file from " + yamlFilename);
+ return false;
+ }
+ ccLog::Print("[3DMASC] Classifier data loaded from " + yamlFilename);
+ }
+ else if (upperLine.startsWith("CLOUD:")) //clouds
+ {
+ if (cloudsAreProvided)
+ {
+ //no need to load the clouds in this case
+ continue;
+ }
+ QString command = line.mid(6);
+ if (!ReadCloud(command, clouds, fi.absoluteDir(), lineNumber))
+ {
+ return false;
}
}
else if (upperLine.startsWith("CORE_POINTS:")) //core points
{
- if (corePoints.origin)
+ if (!corePoints)
+ {
+ //no need to load the core points
+ continue;
+ }
+ if (corePoints->origin)
{
ccLog::Warning("Malformed file: can't declare core points twice! (line #" + QString::number(lineNumber) + ")");
return false;
}
QString command = line.mid(12);
- QStringList tokens = command.split('_');
- if (tokens.empty())
+
+ if (!ReadCorePoints(command, clouds, *corePoints, lineNumber))
{
- ccLog::Warning("Malformed file: expecting tokens after 'core_points:' on line #" + QString::number(lineNumber));
return false;
}
- QString pcName = tokens[0].trimmed();
- if (!clouds.contains(pcName))
- {
- ccLog::Warning(QString("Malformed file: unknown cloud '%1' on line #%2 (make sure it is declared before the core points)").arg(pcName).arg(lineNumber));
- return false;
- }
- corePoints.origin = clouds[pcName];
-
- //should we sub-sample the origin cloud?
- if (tokens.size() > 1)
- {
- if (tokens[1].toUpper() == "SS")
- {
- if (tokens.size() < 3)
- {
- ccLog::Warning("Malformed file: missing token after 'SS' on line #" + QString::number(lineNumber));
- return false;
- }
- QString options = tokens[2];
- if (options.startsWith('R'))
- {
- corePoints.selectionMethod = CorePoints::RANDOM;
- }
- else if (options.startsWith('S'))
- {
- corePoints.selectionMethod = CorePoints::SPATIAL;
- }
- else
- {
- ccLog::Warning("Malformed file: unknown option after 'SS' on line #" + QString::number(lineNumber));
- return false;
- }
-
- //read the subsampling parameter (ignore the first character)
- bool ok = false;
- corePoints.selectionParam = options.mid(1).toDouble(&ok);
- if (!ok)
- {
- ccLog::Warning("Malformed file: expecting a number after 'SS_X' on line #" + QString::number(lineNumber));
- return false;
- }
-
- } //end of subsampling options
- }
}
else if (upperLine.startsWith("SCALES:")) //scales
{
@@ -222,333 +681,20 @@ bool Tools::LoadFile( QString filename,
}
QString command = line.mid(7);
- QStringList tokens = command.split(';');
- if (tokens.empty())
+ if (!ReadScales(command, scales, lineNumber))
{
- ccLog::Warning("Malformed file: expecting at least one token after 'scales:' on line #" + QString::number(lineNumber));
return false;
}
-
- for (const QString& token : tokens)
- {
- if (token.contains(':'))
- {
- //it's probably a range
- QStringList subTokens = token.trimmed().split(':');
- if (subTokens.size() != 3)
- {
- ccLog::Warning(QString("Malformed file: expecting 3 tokens for a range of scales (%1)").arg(token));
- return false;
- }
- bool ok[3] = { true, true, true };
- double start = subTokens[0].trimmed().toDouble(ok);
- double step = subTokens[1].toDouble(ok + 1);
- double stop = subTokens[2].toDouble(ok + 2);
- if (!ok[0] || !ok[1] || !ok[2])
- {
- ccLog::Warning(QString("Malformed file: invalid values in scales range (%1) on line #%2").arg(token).arg(lineNumber));
- return false;
- }
- if (stop < start || step <= 1.0 - 6)
- {
- ccLog::Warning(QString("Malformed file: invalid range (%1) on line #%2").arg(token).arg(lineNumber));
- return false;
- }
-
- for (double v = start; v <= stop + 1.0e-6; v += step)
- {
- scales.push_back(v);
- }
- }
- else
- {
- bool ok = true;
- double v = token.trimmed().toDouble(&ok);
- if (!ok)
- {
- ccLog::Warning(QString("Malformed file: invalid scale value (%1) on line #%2").arg(token).arg(lineNumber));
- return false;
- }
- scales.push_back(v);
- }
- }
- scales.shrink_to_fit();
}
else if (upperLine.startsWith("FEATURE:")) //feature
{
QString command = line.mid(8);
- QStringList tokens = command.split('_');
- if (tokens.empty())
+
+ if (!CreateFeaturesFromCommand(command, lineNumber, clouds, rawFeatures, scales))
{
- ccLog::Warning("Malformed file: expecting at least one token after 'feature:' on line #" + QString::number(lineNumber));
+ //error message already issued
return false;
}
-
- Feature::Shared feature;
-
- //read the type
- QString typeStr = tokens[0].trimmed().toUpper();
- {
- for (int iteration = 0; iteration < 1; ++iteration) //fake loop for easy break
- {
- PointFeature::PointFeatureType pointFeatureType = PointFeature::FromUpperString(typeStr);
- if (pointFeatureType != PointFeature::Invalid)
- {
- //we have a point feature
- PointFeature* pointFeature = new PointFeature(pointFeatureType);
-
- //specific case: 'SF#'
- if (pointFeatureType == PointFeature::SF)
- {
- QString sfIndexStr = typeStr.mid(2);
- bool ok = true;
- int sfIndex = sfIndexStr.toInt(&ok);
- if (!ok)
- {
- ccLog::Warning(QString("Malformed file: expecting a valid integer value after 'SF' on line #%1").arg(lineNumber));
- delete pointFeature;
- return false;
- }
- pointFeature->sourceSFIndex = sfIndex;
- }
-
- feature.reset(pointFeature);
- break;
- }
-
- NeighborhoodFeature::NeighborhoodFeatureType neighborhoodFeatureType = NeighborhoodFeature::FromUpperString(typeStr);
- if (neighborhoodFeatureType != NeighborhoodFeature::Invalid)
- {
- //we have a neighborhood feature
- feature = NeighborhoodFeature::Shared(new NeighborhoodFeature(neighborhoodFeatureType));
- break;
- }
-
- ContextBasedFeature::ContextBasedFeatureType contextBasedFeatureType = ContextBasedFeature::FromUpperString(typeStr);
- if (contextBasedFeatureType != ContextBasedFeature::Invalid)
- {
- //we have a context-based feature
- feature = ContextBasedFeature::Shared(new ContextBasedFeature(contextBasedFeatureType));
- break;
- }
-
- DualCloudFeature::DualCloudFeatureType dualCloudFeatureType = DualCloudFeature::FromUpperString(typeStr);
- if (dualCloudFeatureType != DualCloudFeature::Invalid)
- {
- //we have a dual cloud feature
- feature = DualCloudFeature::Shared(new DualCloudFeature(dualCloudFeatureType));
- break;
- }
-
- if (!feature)
- {
- ccLog::Warning(QString("Malformed file: unrecognized token '%1' after 'feature:' on line #%2").arg(typeStr).arg(lineNumber));
- return false;
- }
- }
- }
- assert(feature);
-
- //read the scales
- bool useAllScales = false;
- {
- QString scaleStr = tokens[1].toUpper();
- if (!scaleStr.startsWith("SC"))
- {
- ccLog::Warning(QString("Malformed file: unrecognized token '%1' (expecting the scale descriptor 'SC...' on line #%2").arg(typeStr).arg(lineNumber));
- return false;
- }
-
- if (scaleStr == "SC0")
- {
- //no scale
- }
- else if (scaleStr == "SCX")
- {
- //all scales
- useAllScales = true;
- }
- else
- {
- //read the specific scale index
- bool ok = true;
- feature->scale = scaleStr.mid(2).toDouble(&ok);
- if (!ok)
- {
- ccLog::Warning(QString("Malformed file: expecting a valid number after 'SC:' on line #%1").arg(lineNumber));
- return false;
- }
- }
- }
-
- //process the next tokens (may not be ordered)
- int cloudCount = 0;
- bool statDefined = false;
- bool mathDefined = false;
- for (int i = 2; i < tokens.size(); ++i)
- {
- QString token = tokens[i].trimmed().toUpper();
-
- //is the token a 'stat' one?
- if (!statDefined)
- {
- if (token == "MEAN")
- {
- feature->stat = Feature::MEAN;
- statDefined = true;
- }
- else if (token == "MODE")
- {
- feature->stat = Feature::MODE;
- statDefined = true;
- }
- else if (token == "STD")
- {
- feature->stat = Feature::STD;
- statDefined = true;
- }
- else if (token == "RANGE")
- {
- feature->stat = Feature::RANGE;
- statDefined = true;
- }
- else if (token == "SKEW")
- {
- feature->stat = Feature::SKEW;
- statDefined = true;
- }
-
- if (statDefined)
- {
- continue;
- }
- }
-
- //is the token a cloud name?
- if (cloudCount < 2)
- {
- bool cloudNameMatches = false;
- for (QMap::const_iterator it = clouds.begin(); it != clouds.end(); ++it)
- {
- QString key = it.key().toUpper();
- if (key == token)
- {
- if (cloudCount == 0)
- {
- feature->cloud1 = it.value();
- feature->cloud1Label = key;
- }
- else if (cloudCount == 1)
- {
- feature->cloud2 = it.value();
- feature->cloud2Label = key;
- }
- else
- {
- //we can't fall here
- assert(false);
- }
- ++cloudCount;
- cloudNameMatches = true;
- break;
- }
- }
-
- if (cloudNameMatches)
- {
- continue;
- }
- }
-
- //is the token a 'math' one?
- if (!mathDefined)
- {
- if (token == "MINUS")
- {
- feature->op = Feature::MINUS;
- mathDefined = true;
- }
- else if (token == "PLUS")
- {
- feature->op = Feature::PLUS;
- mathDefined = true;
- }
- else if (token == "DIVIDE")
- {
- feature->op = Feature::DIVIDE;
- mathDefined = true;
- }
- else if (token == "MULTIPLY")
- {
- feature->op = Feature::MULTIPLY;
- mathDefined = true;
- }
-
- if (mathDefined)
- {
- continue;
- }
- }
-
- //is the token a 'context' descriptor?
- if (feature->getType() == Feature::Type::ContextBasedFeature && token.startsWith("CTX"))
- {
- //read the context label
- QString ctxLabelStr = token.mid(2);
- bool ok = true;
- int ctxLabel = ctxLabelStr.toInt(&ok);
- if (!ok)
- {
- ccLog::Warning(QString("Malformed file: expecting a valid integer value after 'CTX' on line #%1").arg(lineNumber));
- return false;
- }
- static_cast(feature.data())->ctxClassLabel = ctxLabel;
- continue;
- }
-
- //if we are here, it means we couldn't find a correspondance for the current token
- ccLog::Warning(QString("Malformed file: unrecognized or unexpected token '%1' on line #%2").arg(token).arg(lineNumber));
- return false;
- }
-
- //now create the various versions of rules (if any)
- if (useAllScales)
- {
- if (scales.empty())
- {
- ccLog::Warning("Malformed file: 'SCx' token used but not scales were defined" + QString(" (line %1)").arg(lineNumber));
- return false;
- }
- feature->scale = scales.front();
-
- //we will duplicate the original feature AFTER having checked its consistency!
- }
-
- //now check the consistency of the rule
- QString errorMessage;
- if (!feature->checkValidity(errorMessage))
- {
- ccLog::Warning("Malformed feature: " + errorMessage + QString(" (line %1)").arg(lineNumber));
- return false;
- }
-
- //save it
- rawFeatures.push_back(feature);
-
- if (useAllScales)
- {
- for (size_t i = 1; i < scales.size(); ++i)
- {
- //copy the original rule
- Feature::Shared newFeature = feature->clone();
- newFeature->scale = scales[i];
-
- //as we only change the scale value, all the duplicated features should be valid
- assert(newFeature->checkValidity(errorMessage));
-
- rawFeatures.push_back(newFeature);
- }
- }
}
else
{
@@ -568,6 +714,46 @@ bool Tools::LoadFile( QString filename,
return true;
}
+bool Tools::LoadClassifier(QString filename, const NamedClouds& clouds, Feature::Set& rawFeatures, masc::Classifier& classifier, QWidget* parent/*=nullptr*/)
+{
+ return LoadFileCommon(filename, const_cast(clouds), true, rawFeatures, nullptr, &classifier, parent);
+}
+
+bool Tools::LoadTrainingFile( QString filename,
+ Feature::Set& rawFeatures,
+ std::vector& loadedClouds,
+ CorePoints& corePoints)
+{
+ NamedClouds clouds;
+ if (LoadFileCommon(filename, clouds, false, rawFeatures, &corePoints, nullptr, nullptr))
+ {
+ try
+ {
+ loadedClouds.reserve(loadedClouds.size() + clouds.size());
+ }
+ catch (const std::bad_alloc&)
+ {
+ ccLog::Warning("Not enough memory");
+ //release some memory
+ for (NamedClouds::const_iterator it = clouds.begin(); it != clouds.end(); ++it)
+ {
+ delete it.value();
+ }
+ return false;
+ }
+
+ //otherwise transfer the clouds to the 'loadedClouds' vector
+ for (NamedClouds::const_iterator it = clouds.begin(); it != clouds.end(); ++it)
+ {
+ loadedClouds.push_back(it.value());
+ }
+ }
+ else
+ {
+ return false;
+ }
+}
+
CCLib::ScalarField* Tools::RetrieveSF(const ccPointCloud* cloud, const QString& sfName, bool caseSensitive/*=true*/)
{
if (!cloud)
diff --git a/q3DMASCTools.h b/q3DMASCTools.h
index 00a53f7..f1a3c2e 100644
--- a/q3DMASCTools.h
+++ b/q3DMASCTools.h
@@ -19,6 +19,7 @@
//Local
#include "FeaturesInterface.h"
+#include "q3DMASCClassifier.h"
//CCLib
#include
@@ -35,9 +36,15 @@ namespace masc
{
public:
- static bool LoadFile(QString filename, Feature::Set& rawFeatures, std::vector& loadedClouds, CorePoints& corePoints);
+ static bool LoadTrainingFile(QString filename, Feature::Set& rawFeatures, std::vector& loadedClouds, CorePoints& corePoints);
- static bool SaveFeatureDescriptors(QString filename, const Feature::Set& features);
+ static bool SaveClassifier(QString filename, const Feature::Set& features, const masc::Classifier& classifier, QWidget* parent = nullptr);
+
+ static bool LoadClassifierCloudLabels(QString filename, QSet& labels);
+
+ typedef QMap NamedClouds;
+
+ static bool LoadClassifier(QString filename, const NamedClouds& clouds, Feature::Set& rawFeatures, masc::Classifier& classifier, QWidget* parent = nullptr);
static bool PrepareFeatures(const CorePoints& corePoints, Feature::Set& features, QString& error, CCLib::GenericProgressCallback* progressCb = nullptr);
diff --git a/qClassify3DMASCDialog.cpp b/qClassify3DMASCDialog.cpp
new file mode 100644
index 0000000..fc65a8d
--- /dev/null
+++ b/qClassify3DMASCDialog.cpp
@@ -0,0 +1,190 @@
+//##########################################################################
+//# #
+//# CLOUDCOMPARE PLUGIN: qCANUPO #
+//# #
+//# 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: UNIVERSITE EUROPEENNE DE BRETAGNE #
+//# #
+//##########################################################################
+
+#include "qClassify3DMASCDialog.h"
+
+//qCC_plugins
+#include "../../ccMainAppInterface.h"
+
+//qCC_db
+#include
+
+//Qt
+#include
+#include
+#include
+//#include
+
+//system
+#include
+
+static ccPointCloud* GetCloudFromCombo(QComboBox* comboBox, ccHObject* dbRoot)
+{
+ assert(comboBox && dbRoot);
+ if (!comboBox || !dbRoot)
+ {
+ assert(false);
+ return 0;
+ }
+
+ //return the cloud currently selected in the combox box
+ int index = comboBox->currentIndex();
+ if (index < 0)
+ {
+ assert(false);
+ return 0;
+ }
+ unsigned uniqueID = comboBox->itemData(index).toUInt();
+ ccHObject* item = dbRoot->find(uniqueID);
+ if (!item || !item->isA(CC_TYPES::POINT_CLOUD))
+ {
+ assert(false);
+ return 0;
+ }
+ return static_cast(item);
+}
+
+Classify3DMASCDialog::Classify3DMASCDialog(ccMainAppInterface* app)
+ : QDialog(app ? app->getMainWindow() : 0)
+ , Ui::Classify3DMASCDialog()
+ , m_app(app)
+{
+ setupUi(this);
+
+ if (m_app)
+ {
+ //add list of clouds to the combo-boxes
+ ccHObject::Container clouds;
+ if (m_app->dbRootObject())
+ {
+ m_app->dbRootObject()->filterChildren(clouds, true, CC_TYPES::POINT_CLOUD);
+ }
+
+ unsigned cloudCount = 0;
+ for (size_t i = 0; i < clouds.size(); ++i)
+ {
+ if (clouds[i]->isA(CC_TYPES::POINT_CLOUD)) //as filterChildren only test 'isKindOf'
+ {
+ QString name = clouds[i]->getName() + QString(" [%1]").arg(clouds[i]->getUniqueID());
+ QVariant uniqueID(clouds[i]->getUniqueID());
+ cloud1ComboBox->addItem(name, uniqueID);
+ cloud2ComboBox->addItem(name, uniqueID);
+ cloud3ComboBox->addItem(name, uniqueID);
+ ++cloudCount;
+ }
+ }
+
+ //if 3 clouds are loaded, then there's chances that the first one is the global cloud!
+ cloud1ComboBox->setCurrentIndex(cloudCount > 0 ? (cloudCount > 2 ? 1 : 0) : -1);
+ cloud2ComboBox->setCurrentIndex(cloudCount > 1 ? (cloudCount > 2 ? 2 : 1) : -1);
+ cloud3ComboBox->setCurrentIndex(cloudCount > 2 ? 0 : -1);
+
+ if (cloudCount == 0 && app)
+ {
+ app->dispToConsole("You need at least 1 loaded cloud to classify it...", ccMainAppInterface::ERR_CONSOLE_MESSAGE);
+ }
+ }
+
+ cloud1RadioButton->setEnabled(false);
+ cloud2RadioButton->setEnabled(false);
+ cloud3RadioButton->setEnabled(false);
+ cloud1ComboBox->setEnabled(false);
+ cloud2ComboBox->setEnabled(false);
+ cloud3ComboBox->setEnabled(false);
+
+ onCloudChanged(0);
+}
+
+void Classify3DMASCDialog::setCloudRoles(const QSet& roles)
+{
+ int index = 0;
+ for (const QString& role : roles)
+ {
+ switch (index)
+ {
+ case 0:
+ cloud1RadioButton->setText(role);
+
+ cloud1RadioButton->setEnabled(true);
+ cloud1ComboBox->setEnabled(false);
+
+ cloud1RadioButton->setChecked(true);
+ break;
+ case 1:
+ cloud2RadioButton->setText(role);
+
+ cloud2RadioButton->setEnabled(true);
+ cloud2ComboBox->setEnabled(false);
+ break;
+ case 2:
+ cloud3RadioButton->setText(role);
+
+ cloud3RadioButton->setEnabled(true);
+ cloud3ComboBox->setEnabled(false);
+ break;
+ default:
+ //this dialog can't handle more than 3 roles!
+ break;
+ }
+ }
+}
+
+void Classify3DMASCDialog::getClouds(QMap& clouds, QString& mainCloud)
+{
+ if (!m_app)
+ {
+ assert(false);
+ return;
+ }
+
+ if (cloud1RadioButton->isEnabled())
+ {
+ clouds.insert(cloud1RadioButton->text(), GetCloudFromCombo(cloud1ComboBox, m_app->dbRootObject()));
+ if (cloud1RadioButton->isChecked())
+ {
+ mainCloud = cloud1RadioButton->text();
+ }
+ }
+ if (cloud2RadioButton->isEnabled())
+ {
+ clouds.insert(cloud2RadioButton->text(), GetCloudFromCombo(cloud2ComboBox, m_app->dbRootObject()));
+ if (cloud2RadioButton->isChecked())
+ {
+ mainCloud = cloud2RadioButton->text();
+ }
+ }
+ if (cloud3RadioButton->isEnabled())
+ {
+ clouds.insert(cloud3RadioButton->text(), GetCloudFromCombo(cloud3ComboBox, m_app->dbRootObject()));
+ if (cloud3RadioButton->isChecked())
+ {
+ mainCloud = cloud3RadioButton->text();
+ }
+ }
+}
+
+void Classify3DMASCDialog::onCloudChanged(int dummy)
+{
+ if (!cloud1RadioButton->isEnabled())
+ {
+ //this means that no role has been defined yet
+ buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+ return;
+ }
+
+ buttonBox->button(QDialogButtonBox::Ok)->setEnabled(cloud1ComboBox->currentIndex() >= 0);
+}
diff --git a/qClassify3DMASCDialog.h b/qClassify3DMASCDialog.h
new file mode 100644
index 0000000..1b48194
--- /dev/null
+++ b/qClassify3DMASCDialog.h
@@ -0,0 +1,53 @@
+#pragma once
+
+//##########################################################################
+//# #
+//# 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 #
+//# #
+//##########################################################################
+
+//Qt
+#include
+
+#include
+
+class ccMainAppInterface;
+class ccPointCloud;
+
+//! 3DMASC plugin 'classify' dialog
+class Classify3DMASCDialog : public QDialog, public Ui::Classify3DMASCDialog
+{
+ Q_OBJECT
+
+public:
+
+ //! Default constructor
+ Classify3DMASCDialog(ccMainAppInterface* app);
+
+ //! Sets the clouds roles
+ void setCloudRoles(const QSet& roles);
+
+ //! Get point clouds
+ void getClouds(QMap& clouds, QString& mainCloud);
+
+protected slots:
+
+ void onCloudChanged(int);
+
+protected:
+
+ //! Gives access to the application (data-base, UI, etc.)
+ ccMainAppInterface* m_app;
+
+};