From ba1b4ac58c78b9906e0bde19c9303a0e774b44d4 Mon Sep 17 00:00:00 2001 From: Daniel Girardeau-Montaut Date: Mon, 25 Mar 2019 18:45:28 +0100 Subject: [PATCH] Various updates/fixes --- Classify3DMASCDialog.ui | 61 +++++++++++++++++++++++++-------------- PointFeature.cpp | 2 +- q3DMASC.cpp | 46 ++++++++++++++++++++++------- q3DMASCClassifier.cpp | 57 +++++++++++++++++++----------------- q3DMASCClassifier.h | 3 ++ q3DMASCCommands.h | 20 +++++++++++-- q3DMASCTools.cpp | 54 +++++++++++++++++++++++++++------- q3DMASCTools.h | 2 +- qClassify3DMASCDialog.cpp | 17 +++++++++-- qClassify3DMASCDialog.h | 6 ++-- trainDisclaimerDlg.ui | 2 +- 11 files changed, 188 insertions(+), 82 deletions(-) diff --git a/Classify3DMASCDialog.ui b/Classify3DMASCDialog.ui index ebe8242..554b0c7 100644 --- a/Classify3DMASCDialog.ui +++ b/Classify3DMASCDialog.ui @@ -7,7 +7,7 @@ 0 0 700 - 250 + 300 @@ -52,10 +52,16 @@ Data - - + + + + color:red; + - Role + Assign each role to the right cloud, and select the role of the cloud to be classified + + + Qt::AlignCenter @@ -69,13 +75,17 @@ - - - - - 0 - 0 - + + + + Cloud + + + + + + + Role @@ -86,10 +96,13 @@ - - - - Cloud + + + + + 0 + 0 + @@ -118,15 +131,19 @@ - - - color:red; + + + + 0 + 0 + + + + + - Assign each role to the right cloud, and select the role of the cloud to be classified - - - Qt::AlignCenter + TEST (optional) diff --git a/PointFeature.cpp b/PointFeature.cpp index 2f1f492..8601b8c 100644 --- a/PointFeature.cpp +++ b/PointFeature.cpp @@ -371,7 +371,7 @@ static bool ComputeMathOpWithNearestNeighbor( const CorePoints& corePoints, ScalarType s = NAN_VALUE; int neighborhoodSize = 0; - if (octree->findPointNeighbourhood(P, &Yk, 1, octreeLevel, maxSquareDist) >= 1) + if (octree->findPointNeighbourhood(P, &Yk, 1, octreeLevel, maxSquareDist, 0.0, &neighborhoodSize) >= 1) { double s1 = field1.pointValue(corePoints.originIndex(i)); double s2 = field2.pointValue(Yk.getPointGlobalIndex(0)); diff --git a/q3DMASC.cpp b/q3DMASC.cpp index fd81d7b..b772542 100644 --- a/q3DMASC.cpp +++ b/q3DMASC.cpp @@ -117,7 +117,9 @@ void q3DMASCPlugin::doClassifyAction() } QSet cloudLabels; - if (!masc::Tools::LoadClassifierCloudLabels(inputFilename, 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; @@ -135,7 +137,7 @@ void q3DMASCPlugin::doClassifyAction() //now show a dialog where the user will be able to set the cloud roles Classify3DMASCDialog classifDlg(m_app); - classifDlg.setCloudRoles(cloudLabels); + classifDlg.setCloudRoles(cloudLabels, corePointsLabel); classifDlg.classifierFileLineEdit->setText(inputFilename); static bool s_keepAttributes = false; classifDlg.keepAttributesCheckBox->setChecked(s_keepAttributes); @@ -233,7 +235,9 @@ void q3DMASCPlugin::doTrainAction() //load the cloud labels (PC1, PC2, CTX, etc.) QSet cloudLabels; - if (!masc::Tools::LoadClassifierCloudLabels(inputFilename, 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; @@ -246,8 +250,11 @@ void q3DMASCPlugin::doTrainAction() static bool s_keepAttributes = false; masc::Tools::NamedClouds loadedClouds; + masc::CorePoints corePoints; - bool useCloudsFromDB = (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); + //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; if (useCloudsFromDB) { if (cloudLabels.size() > 3) @@ -259,7 +266,7 @@ void q3DMASCPlugin::doTrainAction() //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); + classifDlg.setCloudRoles(cloudLabels, corePointsLabel); classifDlg.classifierFileLineEdit->setText(inputFilename); classifDlg.keepAttributesCheckBox->setChecked(s_keepAttributes); if (!classifDlg.exec()) @@ -269,12 +276,12 @@ void q3DMASCPlugin::doTrainAction() } s_keepAttributes = classifDlg.keepAttributesCheckBox->isChecked(); - QString mainCloudLabel; classifDlg.getClouds(loadedClouds, mainCloudLabel); + m_app->dispToConsole("Training cloud: " + mainCloudLabel, ccMainAppInterface::STD_CONSOLE_MESSAGE); + corePoints.origin = loadedClouds[mainCloudLabel]; } static masc::TrainParameters s_params; - masc::CorePoints corePoints; masc::Feature::Set features; if (!masc::Tools::LoadTrainingFile(inputFilename, features, loadedClouds, corePoints, s_params)) { @@ -282,6 +289,18 @@ void q3DMASCPlugin::doTrainAction() return; } + if (!corePoints.origin) + { + m_app->dispToConsole("Core points not defined", ccMainAppInterface::ERR_CONSOLE_MESSAGE); + return; + } + + if (!masc::Classifier::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) { @@ -292,6 +311,11 @@ void q3DMASCPlugin::doTrainAction() } } + for (masc::Tools::NamedClouds::const_iterator it = loadedClouds.begin(); it != loadedClouds.end(); ++it) + { + m_app->dispToConsole(it.key() + " = " + it.value()->getName(), ccMainAppInterface::STD_CONSOLE_MESSAGE); + } + //show the training dialog for the first time Train3DMASCDialog trainDlg(m_app->getMainWindow()); trainDlg.maxDepthSpinBox->setValue(s_params.rt.maxDepth); @@ -452,25 +476,25 @@ void q3DMASCPlugin::doTrainAction() testSubset2->addPointIndex(0, testCloud->size()); } - if (s_params.testDataRatio < 0.0f || s_params.testDataRatio > 0.99f) + if (testDataRatio < 0.0f || testDataRatio > 0.99f) { assert(false); m_app->dispToConsole("Invalid test data ratio", ccMainAppInterface::ERR_CONSOLE_MESSAGE); } else { - if (previousTestSubsetRatio != s_params.testDataRatio) + if (previousTestSubsetRatio != testDataRatio) { //randomly select the training points testSubset->clear(); trainSubset->clear(); - if (!masc::Tools::RandomSubset(corePoints.cloud, s_params.testDataRatio, testSubset.data(), trainSubset.data())) + if (!masc::Tools::RandomSubset(corePoints.cloud, testDataRatio, testSubset.data(), trainSubset.data())) { m_app->dispToConsole("Not enough memory", ccMainAppInterface::ERR_CONSOLE_MESSAGE); generatedScalarFields.releaseAllSFs(); return; } - previousTestSubsetRatio = s_params.testDataRatio; + previousTestSubsetRatio = testDataRatio; } //train the classifier diff --git a/q3DMASCClassifier.cpp b/q3DMASCClassifier.cpp index f4fe8ab..f1ff7d2 100644 --- a/q3DMASCClassifier.cpp +++ b/q3DMASCClassifier.cpp @@ -121,9 +121,8 @@ bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QSt } //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 < 0) + CCLib::ScalarField* classificationSF = GetClassificationSF(cloud); + if (!classificationSF) { //create it if necessary ccScalarField* _classificationSF = new ccScalarField(LAS_FIELD_NAMES[LAS_CLASSIFICATION]); @@ -133,13 +132,8 @@ bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QSt errorMessage = QObject::tr("Not enough memory"); return false; } - classifSFIdx = cloud->addScalarField(_classificationSF); classificationSF = _classificationSF; } - else - { - classificationSF = cloud->getScalarField(classifSFIdx); - } assert(classificationSF); classificationSF->fill(0); //0 = no classification? @@ -216,8 +210,13 @@ bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QSt } classificationSF->computeMinAndMax(); - cloud->setCurrentDisplayedScalarField(classifSFIdx); - cloud->showSF(true); + //show the classification field by default + { + int classifSFIdx = cloud->getScalarFieldIndexByName(classificationSF->getName()); + cloud->setCurrentDisplayedScalarField(classifSFIdx); + cloud->showSF(true); + } + if (parentWidget && cloud->getDisplay()) { cloud->getDisplay()->redraw(); @@ -257,17 +256,11 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t } //look for the classification field - int classifSFIdx = cloud->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_CLASSIFICATION]); //LAS_FIELD_NAMES[LAS_CLASSIFICATION] = "Classification" - if (!classifSFIdx) - { - errorMessage = QObject::tr("Missing 'Classification' field on input cloud"); - return false; - } - CCLib::ScalarField* classifSF = cloud->getScalarField(classifSFIdx); + CCLib::ScalarField* classifSF = GetClassificationSF(cloud); if (!classifSF || classifSF->size() < cloud->size()) { assert(false); - errorMessage = QObject::tr("Invalid 'Classification' field on input cloud"); + errorMessage = QObject::tr("Missing/Invalid 'Classification' field on input cloud"); return false; } @@ -385,17 +378,11 @@ bool Classifier::train( const ccPointCloud* cloud, } //look for the classification field - int classifSFIdx = cloud->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_CLASSIFICATION]); //LAS_FIELD_NAMES[LAS_CLASSIFICATION] = "Classification" - if (!classifSFIdx) - { - errorMessage = QObject::tr("Missing 'Classification' field on input cloud"); - return false; - } - CCLib::ScalarField* classifSF = cloud->getScalarField(classifSFIdx); + CCLib::ScalarField* classifSF = GetClassificationSF(cloud); if (!classifSF || classifSF->size() < cloud->size()) { assert(false); - errorMessage = QObject::tr("Invalid 'Classification' field on input cloud"); + errorMessage = QObject::tr("Missing/invalid 'Classification' field on input cloud"); return false; } @@ -603,3 +590,21 @@ bool Classifier::fromFile(QString filename, QWidget* parentWidget/*=nullptr*/) return true; } + +CCLib::ScalarField* Classifier::GetClassificationSF(const ccPointCloud* cloud) +{ + if (!cloud) + { + //invalid input cloud + assert(false); + return nullptr; + } + //look for the classification field + int classifSFIdx = cloud->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_CLASSIFICATION]); //LAS_FIELD_NAMES[LAS_CLASSIFICATION] = "Classification" + if (classifSFIdx < 0) + { + return nullptr; + } + return cloud->getScalarField(classifSFIdx); +} + diff --git a/q3DMASCClassifier.h b/q3DMASCClassifier.h index fff22a5..35511da 100644 --- a/q3DMASCClassifier.h +++ b/q3DMASCClassifier.h @@ -76,6 +76,9 @@ namespace masc inline cv::Mat getVarImportance() const { return m_rtrees->getVarImportance(); } + //! Helper: returns the classification SF associated to a cloud (if any) + static CCLib::ScalarField* GetClassificationSF(const ccPointCloud* cloud); + protected: //! Random trees (OpenCV) diff --git a/q3DMASCCommands.h b/q3DMASCCommands.h index c627cdf..9d9d93d 100644 --- a/q3DMASCCommands.h +++ b/q3DMASCCommands.h @@ -94,22 +94,36 @@ struct Command3DMASCClassif : public ccCommandLineInterface::Command { return cmd.error(QString("Cloud index %1 exceeds the number of loaded clouds (=%2)").arg(cloudIndex).arg(cmd.clouds().size())); } - cloudPerRole.insert(role, cmd.clouds()[cloudIndex-1].pc); + cloudPerRole.insert(role, cmd.clouds()[cloudIndex - 1].pc); if (mainCloudRole.isEmpty()) { mainCloudRole = role; - cmd.print("The classified cloud role will be " + role); } } //try to load the clouds roles from the classifier file QSet cloudLabels; - if (!masc::Tools::LoadClassifierCloudLabels(classifierFilename, cloudLabels)) + QString corePointsLabel; + bool filenamesSpecified = false; + if (!masc::Tools::LoadClassifierCloudLabels(classifierFilename, cloudLabels, corePointsLabel, filenamesSpecified)) { return cmd.error("Failed to read classifier file"); } + if (!corePointsLabel.isEmpty()) + { + //we use the core points source as 'main role' by default + mainCloudRole = corePointsLabel; + cmd.print("Core points source: " + corePointsLabel + "(will be used as the classified cloud)"); + } + cmd.print("The classified cloud role will be " + mainCloudRole); + + if (!filenamesSpecified) + { + return cmd.error("Filenames were not specified for at least one role"); + } + for (QString label : cloudLabels) { if (!cloudPerRole.contains(label.toUpper())) diff --git a/q3DMASCTools.cpp b/q3DMASCTools.cpp index 4e1afff..218628b 100644 --- a/q3DMASCTools.cpp +++ b/q3DMASCTools.cpp @@ -29,9 +29,6 @@ #include #include -//qPDALIO -#include "../../core/IO/qPDALIO/src/LASFields.h" - //Qt #include #include @@ -93,8 +90,12 @@ bool Tools::SaveClassifier(QString filename, const Feature::Set& features, const return true; } -bool Tools::LoadClassifierCloudLabels(QString filename, QSet& labels) +bool Tools::LoadClassifierCloudLabels(QString filename, QSet& labels, QString& corePointsLabel, bool& filenamesSpecified) { + //just in case + corePointsLabel.clear(); + labels.clear(); + QFile file(filename); if (!file.open(QFile::Text | QFile::ReadOnly)) { @@ -103,6 +104,7 @@ bool Tools::LoadClassifierCloudLabels(QString filename, QSet& labels) } QTextStream stream(&file); + int filenameCount = 0; for (int lineNumber = 0; ; ++lineNumber) { QString line = stream.readLine(); @@ -126,9 +128,30 @@ bool Tools::LoadClassifierCloudLabels(QString filename, QSet& labels) QString label = tokens.front(); labels.insert(label); + + if (tokens.size() > 1) + ++filenameCount; + } + else if (line.startsWith("CORE_POINTS:")) + { + if (!corePointsLabel.isEmpty()) + { + //core points defined multiple times?! + continue; + } + QString command = line.mid(12); + QStringList tokens = command.split('_'); + if (tokens.empty()) + { + ccLog::Warning("Malformed file: expecting tokens after 'core_points:' on line #" + QString::number(lineNumber)); + return false; + } + corePointsLabel = tokens[0].trimmed(); } } + filenamesSpecified = (filenameCount > 0 && filenameCount == labels.size()); + return true; } @@ -639,6 +662,7 @@ static bool LoadFileCommon( const QString& filename, std::vector scales; QTextStream stream(&file); + bool badFeatures = false; for (int lineNumber = 1; ; ++lineNumber) { QString line = stream.readLine(); @@ -716,14 +740,16 @@ static bool LoadFileCommon( const QString& filename, } if (corePoints->origin) { - ccLog::Warning("Malformed file: can't declare core points twice! (line #" + QString::number(lineNumber) + ")"); - return false; + ccLog::Warning("Core points already defined (those declared on line #" + QString::number(lineNumber) + " will be ignored)"); } - QString command = line.mid(12); - - if (!ReadCorePoints(command, clouds, *corePoints, lineNumber)) + else { - return false; + QString command = line.mid(12); + + if (!ReadCorePoints(command, clouds, *corePoints, lineNumber)) + { + return false; + } } } else if (upperLine.startsWith("SCALES:")) //scales @@ -747,7 +773,8 @@ static bool LoadFileCommon( const QString& filename, if (!CreateFeaturesFromCommand(command, lineNumber, clouds, rawFeatures, scales)) { //error message already issued - return false; + //return false; + badFeatures = true; //we continue as we want to get ALL the errors } } else if (upperLine.startsWith("PARAM_")) //parameter @@ -797,6 +824,11 @@ static bool LoadFileCommon( const QString& filename, return false; } } + + if (badFeatures) + { + return false; + } } catch (const std::bad_alloc&) { diff --git a/q3DMASCTools.h b/q3DMASCTools.h index 0dd7791..d997c08 100644 --- a/q3DMASCTools.h +++ b/q3DMASCTools.h @@ -42,7 +42,7 @@ namespace masc static bool SaveClassifier(QString filename, const Feature::Set& features, const masc::Classifier& classifier, QWidget* parent = nullptr); - static bool LoadClassifierCloudLabels(QString filename, QSet& labels); + static bool LoadClassifierCloudLabels(QString filename, QSet& labels, QString& corePointsLabel, bool& filenamesSpecified); static bool LoadClassifier(QString filename, const NamedClouds& clouds, Feature::Set& rawFeatures, masc::Classifier& classifier, QWidget* parent = nullptr); diff --git a/qClassify3DMASCDialog.cpp b/qClassify3DMASCDialog.cpp index bcf3e12..d922d7d 100644 --- a/qClassify3DMASCDialog.cpp +++ b/qClassify3DMASCDialog.cpp @@ -84,6 +84,7 @@ Classify3DMASCDialog::Classify3DMASCDialog(ccMainAppInterface* app, bool trainMo cloud1ComboBox->addItem(name, uniqueID); cloud2ComboBox->addItem(name, uniqueID); cloud3ComboBox->addItem(name, uniqueID); + testCloudComboBox->addItem(name, uniqueID); ++cloudCount; } } @@ -92,6 +93,7 @@ Classify3DMASCDialog::Classify3DMASCDialog(ccMainAppInterface* app, bool trainMo cloud1ComboBox->setCurrentIndex(cloudCount > 0 ? (cloudCount > 2 ? 1 : 0) : -1); cloud2ComboBox->setCurrentIndex(cloudCount > 1 ? (cloudCount > 2 ? 2 : 1) : -1); cloud3ComboBox->setCurrentIndex(cloudCount > 2 ? 0 : -1); + testCloudComboBox->setCurrentIndex(-1); if (cloudCount == 0 && app) { @@ -108,7 +110,7 @@ Classify3DMASCDialog::Classify3DMASCDialog(ccMainAppInterface* app, bool trainMo onCloudChanged(0); } -void Classify3DMASCDialog::setCloudRoles(const QSet& roles) +void Classify3DMASCDialog::setCloudRoles(const QSet& roles, QString corePointsLabel) { int index = 0; for (const QString& role : roles) @@ -117,13 +119,18 @@ void Classify3DMASCDialog::setCloudRoles(const QSet& roles) { case 0: cloud1RadioButton->setText(role); - cloud1RadioButton->setChecked(true); + if (corePointsLabel.isEmpty() || corePointsLabel == role) + cloud1RadioButton->setChecked(true); break; case 1: cloud2RadioButton->setText(role); + if (corePointsLabel == role) + cloud2RadioButton->setChecked(true); break; case 2: cloud3RadioButton->setText(role); + if (corePointsLabel == role) + cloud3RadioButton->setChecked(true); break; default: //this dialog can't handle more than 3 roles! @@ -151,7 +158,7 @@ void Classify3DMASCDialog::setCloudRoles(const QSet& roles) } } -void Classify3DMASCDialog::getClouds(QMap& clouds, QString& mainCloud) +void Classify3DMASCDialog::getClouds(QMap& clouds, QString& mainCloud) const { if (!m_app) { @@ -183,6 +190,10 @@ void Classify3DMASCDialog::getClouds(QMap& clouds, QStri mainCloud = cloud3RadioButton->text(); } } + if (testCloudComboBox->currentIndex() >= 0) + { + clouds.insert("TEST", GetCloudFromCombo(testCloudComboBox, m_app->dbRootObject())); + } } void Classify3DMASCDialog::onCloudChanged(int dummy) diff --git a/qClassify3DMASCDialog.h b/qClassify3DMASCDialog.h index ee3c542..2fd1b8b 100644 --- a/qClassify3DMASCDialog.h +++ b/qClassify3DMASCDialog.h @@ -36,10 +36,10 @@ public: Classify3DMASCDialog(ccMainAppInterface* app, bool trainMode = false); //! Sets the clouds roles - void setCloudRoles(const QSet& roles); + void setCloudRoles(const QSet& roles, QString corePointsLabel); - //! Get point clouds - void getClouds(QMap& clouds, QString& mainCloud); + //! Returns the selected point clouds + void getClouds(QMap& clouds, QString& mainCloud) const; protected slots: diff --git a/trainDisclaimerDlg.ui b/trainDisclaimerDlg.ui index e5a80a3..39a2e5b 100644 --- a/trainDisclaimerDlg.ui +++ b/trainDisclaimerDlg.ui @@ -74,7 +74,7 @@ p, li { white-space: pre-wrap; } - +