From da6293e52224a15828c323754d509c59ea2159d5 Mon Sep 17 00:00:00 2001 From: Daniel Girardeau-Montaut Date: Tue, 26 Mar 2019 14:28:27 +0100 Subject: [PATCH] WIP --- ContextBasedFeature.cpp | 2 +- FeaturesInterface.cpp | 21 +++ FeaturesInterface.h | 27 +++- NeighborhoodFeature.cpp | 2 +- PointFeature.cpp | 48 ++++--- PointFeature.h | 25 ++-- q3DMASC.cpp | 275 ++++++++++++++++++++++++++------------ q3DMASCClassifier.cpp | 164 +++++++++++++---------- q3DMASCClassifier.h | 15 ++- q3DMASCCommands.h | 6 +- q3DMASCTools.cpp | 41 ++++-- q3DMASCTools.h | 6 +- qClassify3DMASCDialog.cpp | 11 +- qClassify3DMASCDialog.h | 2 +- 14 files changed, 420 insertions(+), 225 deletions(-) diff --git a/ContextBasedFeature.cpp b/ContextBasedFeature.cpp index ad5f498..f7cde81 100644 --- a/ContextBasedFeature.cpp +++ b/ContextBasedFeature.cpp @@ -69,7 +69,7 @@ bool ContextBasedFeature::prepare( const CorePoints& corePoints, error = QString("Failed to prepare scalar %1 @ scale %2").arg(resultSFName).arg(scale); return false; } - sourceName = sf->getName(); + source.name = sf->getName(); if (!scaled()) //with 'kNN' neighbors, we can compute the values right away { diff --git a/FeaturesInterface.cpp b/FeaturesInterface.cpp index f3489a1..73a1621 100644 --- a/FeaturesInterface.cpp +++ b/FeaturesInterface.cpp @@ -133,3 +133,24 @@ bool Feature::PerformMathOp(const IScalarFieldWrapper& sf1, const IScalarFieldWr return true; } + +bool Feature::ExtractSources(const Set& features, Source::Set& sources) +{ + sources.clear(); + try + { + sources.reserve(features.size()); + } + catch (const std::bad_alloc&) + { + ccLog::Warning("Not enough memory"); + return false; + } + + for (Feature::Shared f : features) + { + sources.push_back(f->source); + } + + return true; +} diff --git a/FeaturesInterface.h b/FeaturesInterface.h index a9abceb..4351cb9 100644 --- a/FeaturesInterface.h +++ b/FeaturesInterface.h @@ -118,20 +118,36 @@ namespace masc } //! Sources of values for this feature - enum Source + struct Source { - ScalarField, DimX, DimY, DimZ, Red, Green, Blue + using Set = std::vector; + + //! Sources types + enum Type + { + ScalarField, DimX, DimY, DimZ, Red, Green, Blue + }; + + Source(Type t = ScalarField, QString n = QString()) + : type(t) + , name(n) + {} + + Type type; + QString name; }; + //! Extracts the set of 'sources' from a set of features + static bool ExtractSources(const Set& features, Source::Set& sources); + public: //methods //! Default constructor - Feature(double p_scale = std::numeric_limits::quiet_NaN(), Source p_source = ScalarField, QString p_sourceName = QString()) + Feature(double p_scale = std::numeric_limits::quiet_NaN(), Source::Type p_source = Source::ScalarField, QString p_sourceName = QString()) : scale(p_scale) , cloud1(nullptr) , cloud2(nullptr) - , source(p_source) - , sourceName(p_sourceName) + , source(p_source, p_sourceName) , stat(NO_STAT) , op(NO_OPERATION) {} @@ -202,7 +218,6 @@ namespace masc QString cloud1Label, cloud2Label; Source source; //values source - QString sourceName; //feature source name (mandatory for scalar fields if the SF index is not set) Stat stat; //only considered if a scale is defined Operation op; //only considered if 2 clouds are defined diff --git a/NeighborhoodFeature.cpp b/NeighborhoodFeature.cpp index c3648b0..1f9c944 100644 --- a/NeighborhoodFeature.cpp +++ b/NeighborhoodFeature.cpp @@ -88,7 +88,7 @@ bool NeighborhoodFeature::prepare( const CorePoints& corePoints, error = QString("Failed to prepare scalar %1 @ scale %2").arg(resultSFName).arg(scale); return false; } - sourceName = sf1->getName(); + source.name = sf1->getName(); if (cloud2 && op != Feature::NO_OPERATION) { diff --git a/PointFeature.cpp b/PointFeature.cpp index d27b6cb..f4dfa5d 100644 --- a/PointFeature.cpp +++ b/PointFeature.cpp @@ -506,7 +506,7 @@ bool PointFeature::prepare( const CorePoints& corePoints, return false; } - sourceName = statSF1->getName(); + source.name = statSF1->getName(); } else //not scaled { @@ -536,7 +536,7 @@ bool PointFeature::prepare( const CorePoints& corePoints, if (field2 && op != Feature::NO_OPERATION) { QString resultSFName2 = cloud2Label + "." + field2->getName() + QString("_") + Feature::StatToString(stat) + "@" + QString::number(scale); - keepStatSF2 = (corePoints.cloud->getScalarFieldIndexByName(qPrintable(resultSFName2)) >= 0); //we remember that the scalar field was already existing! + //keepStatSF2 = (corePoints.cloud->getScalarFieldIndexByName(qPrintable(resultSFName2)) >= 0); //we remember that the scalar field was already existing! assert(!statSF2); statSF2 = PrepareSF(corePoints.cloud, qPrintable(resultSFName2), generatedScalarFields); @@ -610,7 +610,7 @@ bool PointFeature::prepare( const CorePoints& corePoints, corePoints.cloud->setCurrentDisplayedScalarField(newSFIdx); } - sourceName = resultSF->getName(); + source.name = resultSF->getName(); return true; } @@ -712,8 +712,8 @@ bool PointFeature::computeStat(const CCLib::DgmOctree::NeighboursSet& pointsInNe case Feature::MODE: { CCLib::WeibullDistribution w; - w.computeParameters(values); - outputValue = w.computeMode(); + if (w.computeParameters(values)) + outputValue = w.computeMode(); } break; @@ -741,8 +741,8 @@ bool PointFeature::computeStat(const CCLib::DgmOctree::NeighboursSet& pointsInNe case Feature::SKEW: { CCLib::WeibullDistribution w; - w.computeParameters(values); - outputValue = w.computeSkewness(); + if (w.computeParameters(values)) + outputValue = w.computeSkewness(); } break; @@ -802,25 +802,23 @@ bool PointFeature::finish(const CorePoints& corePoints, QString& error) success = false; } } + statSF2->computeMinAndMax(); - if (keepStatSF2) - { - statSF2->computeMinAndMax(); - } - else - { - int sfIndex2 = corePoints.cloud->getScalarFieldIndexByName(statSF2->getName()); - if (sfIndex2 >= 0) - { - corePoints.cloud->deleteScalarField(sfIndex2); - } - else - { - assert(false); - statSF2->release(); - } - statSF2 = nullptr; - } + //DGM: we don't delete it now! As it could be used by other features! + //if (!keepStatSF2) + //{ + // int sfIndex2 = corePoints.cloud->getScalarFieldIndexByName(statSF2->getName()); + // if (sfIndex2 >= 0) + // { + // corePoints.cloud->deleteScalarField(sfIndex2); + // } + // else + // { + // assert(false); + // statSF2->release(); + // } + // statSF2 = nullptr; + //} } return success; diff --git a/PointFeature.h b/PointFeature.h index 4c71151..e482781 100644 --- a/PointFeature.h +++ b/PointFeature.h @@ -148,38 +148,31 @@ namespace masc , field2(nullptr) , statSF1(nullptr) , statSF2(nullptr) - , keepStatSF2(false) + //, keepStatSF2(false) { //auomatically set the right source for specific features switch (type) { case X: - source = DimX; - sourceName = "X"; + source = { Source::DimX, "X" }; break; case Y: - source = DimY; - sourceName = "Y"; + source = { Source::DimY, "Y" }; break; case Z: - source = DimZ; - sourceName = "Z"; + source = { Source::DimZ, "Z" }; break; case R: - source = Red; - sourceName = "Red"; + source = { Source::Red, "Red" }; break; case G: - source = Green; - sourceName = "Green"; + source = { Source::Green, "Green" }; break; case B: - source = Blue; - sourceName = "Blue"; + source = { Source::Blue, "Blue" }; break; default: - source = ScalarField; - //sourceName --> TBD later + source = { Source::ScalarField, QString() }; //source name --> TBD later break; } } @@ -219,6 +212,6 @@ namespace masc //! For scaled features CCLib::ScalarField *statSF1, *statSF2; - bool keepStatSF2; + //bool keepStatSF2; }; } diff --git a/q3DMASC.cpp b/q3DMASC.cpp index e7d369d..97e8832 100644 --- a/q3DMASC.cpp +++ b/q3DMASC.cpp @@ -116,7 +116,7 @@ void q3DMASCPlugin::doClassifyAction() settings.endGroup(); } - QSet cloudLabels; + QList cloudLabels; QString corePointsLabel; bool filenamesSpecified = false; if (!masc::Tools::LoadClassifierCloudLabels(inputFilename, cloudLabels, corePointsLabel, filenamesSpecified)) @@ -159,11 +159,15 @@ void q3DMASCPlugin::doClassifyAction() { 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) - delete clouds["TEST"]; clouds.remove("TEST"); } @@ -190,7 +194,9 @@ void q3DMASCPlugin::doClassifyAction() //apply classifier { QString errorMessage; - if (!classifier.classify(features, corePoints.cloud, errorMessage, m_app->getMainWindow())) + 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.releaseAllSFs(); @@ -235,7 +241,7 @@ void q3DMASCPlugin::doTrainAction() } //load the cloud labels (PC1, PC2, CTX, etc.) - QSet cloudLabels; + QList cloudLabels; QString corePointsLabel; bool filenamesSpecified = false; if (!masc::Tools::LoadClassifierCloudLabels(inputFilename, cloudLabels, corePointsLabel, filenamesSpecified)) @@ -285,7 +291,7 @@ void q3DMASCPlugin::doTrainAction() static masc::TrainParameters s_params; masc::Feature::Set features; - if (!masc::Tools::LoadTrainingFile(inputFilename, features, loadedClouds, corePoints, s_params)) + if (!masc::Tools::LoadTrainingFile(inputFilename, features, loadedClouds, s_params, &corePoints)) { m_app->dispToConsole("Failed to load the training file", ccMainAppInterface::ERR_CONSOLE_MESSAGE); return; @@ -318,6 +324,34 @@ void q3DMASCPlugin::doTrainAction() m_app->dispToConsole(it.key() + " = " + it.value()->getName(), ccMainAppInterface::STD_CONSOLE_MESSAGE); } + //test role + ccPointCloud* testCloud = nullptr; + bool needTestSuite = false; + masc::Feature::Set featuresTest; + 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, 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.maxDepthSpinBox->setValue(s_params.rt.maxDepth); @@ -325,6 +359,7 @@ void q3DMASCPlugin::doTrainAction() trainDlg.activeVarCountSpinBox->setValue(s_params.rt.activeVarCount); trainDlg.minSampleCountSpinBox->setValue(s_params.rt.minSampleCount); trainDlg.testDataRatioSpinBox->setValue(static_cast(s_params.testDataRatio * 100)); + trainDlg.testDataRatioSpinBox->setEnabled(testCloud == nullptr); //display the loaded features and let the user select the ones to use trainDlg.setResultText("Select features and press 'Run'"); @@ -335,6 +370,17 @@ void q3DMASCPlugin::doTrainAction() originalFeatures.push_back(FeatureSelection(f)); trainDlg.addFeature(f->toString(), originalFeatures.back().importance, originalFeatures.back().selected); } + + std::vector originalFeaturesTest; + if (testCloud && needTestSuite) + { + originalFeaturesTest.reserve(featuresTest.size()); + for (const masc::Feature::Shared& f : featuresTest) + { + originalFeaturesTest.push_back(FeatureSelection(f)); + } + } + if (!trainDlg.exec()) { delete group; @@ -350,7 +396,7 @@ void q3DMASCPlugin::doTrainAction() delete group; return; } - + if (corePoints.cloud != corePoints.origin) { //auto-hide the other clouds @@ -388,22 +434,13 @@ void q3DMASCPlugin::doTrainAction() group = nullptr; } - //test role - ccPointCloud* testCloud = nullptr; - if (loadedClouds.contains("TEST")) - { - testCloud = loadedClouds["TEST"]; - loadedClouds.remove("TEST"); - } - - //train / test subsets - QScopedPointer trainSubset(new CCLib::ReferenceCloud(corePoints.cloud)); - QScopedPointer testSubset(new CCLib::ReferenceCloud(corePoints.cloud)); + QSharedPointer trainSubset, testSubset; float previousTestSubsetRatio = -1.0f; + SFCollector generatedScalarFields, generatedScalarFieldsTest; - SFCollector generatedScalarFields; - + //we will train + evaluate the classifier, then display the reuslts + //then let the user change parameters and (potentially) start again for (int iteration = 0; ; ++iteration) { //look for selected features @@ -425,87 +462,97 @@ void q3DMASCPlugin::doTrainAction() } } + masc::Classifier classifier; if (features.empty()) { m_app->dispToConsole("No feature selected!", ccMainAppInterface::ERR_CONSOLE_MESSAGE); - continue; - } - - //prepare the features - if (!toPrepare.empty()) - { - progressDlg.setAutoClose(false); //we don't want the progress dialog to 'pop' for each feature - QString error; - if (!masc::Tools::PrepareFeatures(corePoints, toPrepare, error, &progressDlg, &generatedScalarFields)) - { - m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE); - generatedScalarFields.releaseAllSFs(); - return; - } - progressDlg.setAutoClose(true); //restore the default behavior of the progress dialog - progressDlg.close(); - QCoreApplication::processEvents(); - m_app->redrawAll(); - - //flag the prepared features as 'prepared' ;) - for (FeatureSelection& fs : originalFeatures) - { - if (fs.selected && !fs.prepared) - fs.prepared = true; - } - } - - masc::Classifier classifier; - - //retrieve parameters - s_params.rt.maxDepth = trainDlg.maxDepthSpinBox->value(); - s_params.rt.maxTreeCount = trainDlg.maxTreeCountSpinBox->value(); - s_params.rt.activeVarCount = trainDlg.activeVarCountSpinBox->value(); - s_params.rt.minSampleCount = trainDlg.minSampleCountSpinBox->value(); - float testDataRatio = s_params.testDataRatio = trainDlg.testDataRatioSpinBox->value() / 100.0f; - QScopedPointer testSubset2; - if (testCloud) - { - m_app->dispToConsole("Test data cloud provided (ignoring test data ratio)", ccMainAppInterface::WRN_CONSOLE_MESSAGE); - testDataRatio = 0.0f; - testSubset2.reset(new CCLib::ReferenceCloud(testCloud)); - if (!testSubset2->reserve(testCloud->size())) - { - m_app->dispToConsole("Not enough memory to evaluate the classifier", ccMainAppInterface::ERR_CONSOLE_MESSAGE); - generatedScalarFields.releaseAllSFs(); - return; - } - testSubset2->addPointIndex(0, testCloud->size()); - } - - if (testDataRatio < 0.0f || testDataRatio > 0.99f) - { - assert(false); - m_app->dispToConsole("Invalid test data ratio", ccMainAppInterface::ERR_CONSOLE_MESSAGE); } else { - if (previousTestSubsetRatio != testDataRatio) + //prepare the features (should be done once) + if (!toPrepare.empty()) { - //randomly select the training points - testSubset->clear(); - trainSubset->clear(); - if (!masc::Tools::RandomSubset(corePoints.cloud, testDataRatio, testSubset.data(), trainSubset.data())) + progressDlg.setAutoClose(false); //we don't want the progress dialog to 'pop' for each feature + QString error; + if (!masc::Tools::PrepareFeatures(corePoints, toPrepare, error, &progressDlg, &generatedScalarFields)) { - m_app->dispToConsole("Not enough memory", ccMainAppInterface::ERR_CONSOLE_MESSAGE); + m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE); generatedScalarFields.releaseAllSFs(); + generatedScalarFieldsTest.releaseAllSFs(); return; } - previousTestSubsetRatio = testDataRatio; + progressDlg.setAutoClose(true); //restore the default behavior of the progress dialog + progressDlg.hide(); + 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 CCLib::ReferenceCloud(corePoints.cloud)); + trainSubset->clear(); + + if (!testSubset) + testSubset.reset(new CCLib::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.releaseAllSFs(); + generatedScalarFieldsTest.releaseAllSFs(); + 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, features, errorMessage, trainSubset.data(), m_app, m_app->getMainWindow())) + 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.releaseAllSFs(); + generatedScalarFieldsTest.releaseAllSFs(); return; } trainDlg.setFirstRunDone(); @@ -514,12 +561,72 @@ void q3DMASCPlugin::doTrainAction() //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(i); + + //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.setAutoClose(false); //we don't want the progress dialog to 'pop' for each feature + 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.releaseAllSFs(); + generatedScalarFieldsTest.releaseAllSFs(); + return; + } + progressDlg.setAutoClose(true); //restore the default behavior of the progress dialog + progressDlg.hide(); + 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(features, testSubset2 ? testSubset2.data() : testSubset.data(), metrics, errorMessage, m_app->getMainWindow())) + if (!classifier.evaluate( featureSources, + testCloud ? testCloud : corePoints.cloud, + metrics, + errorMessage, + testCloud ? nullptr : testSubset.data(), + "Classification_pred", + m_app->getMainWindow())) { m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE); generatedScalarFields.releaseAllSFs(); + generatedScalarFieldsTest.releaseAllSFs(); return; } @@ -551,6 +658,7 @@ void q3DMASCPlugin::doTrainAction() } } + //now wait for the user input while (true) { if (!trainDlg.exec()) @@ -559,6 +667,7 @@ void q3DMASCPlugin::doTrainAction() if (!s_keepAttributes) { generatedScalarFields.releaseAllSFs(); + generatedScalarFieldsTest.releaseAllSFs(); } return; } @@ -583,7 +692,7 @@ void q3DMASCPlugin::doTrainAction() } //save the classifier - if (masc::Tools::SaveClassifier(outputFilename, features, classifier, m_app->getMainWindow())) + 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(); diff --git a/q3DMASCClassifier.cpp b/q3DMASCClassifier.cpp index f1ff7d2..7f632f1 100644 --- a/q3DMASCClassifier.cpp +++ b/q3DMASCClassifier.cpp @@ -48,50 +48,45 @@ bool Classifier::isValid() const return (m_rtrees && m_rtrees->isTrained()); } -static IScalarFieldWrapper::Shared GetSource(const Feature::Shared& f, const ccPointCloud* cloud) +static IScalarFieldWrapper::Shared GetSource(const Feature::Source& fs, const ccPointCloud* cloud) { IScalarFieldWrapper::Shared source(nullptr); - if (!f) - { - assert(false); - ccLog::Warning(QObject::tr("Internal error: invalid feature (nullptr)")); - } - switch (f->source) + switch (fs.type) { - case Feature::ScalarField: + case Feature::Source::ScalarField: { - assert(!f->sourceName.isEmpty()); - int sfIdx = cloud->getScalarFieldIndexByName(qPrintable(f->sourceName)); + assert(fs.name.isEmpty()); + int sfIdx = cloud->getScalarFieldIndexByName(qPrintable(fs.name)); if (sfIdx >= 0) { source.reset(new ScalarFieldWrapper(cloud->getScalarField(sfIdx))); } else { - ccLog::Warning(QObject::tr("Internal error: unknwon scalar field '%1'").arg(f->sourceName)); + ccLog::Warning(QObject::tr("Internal error: unknwon scalar field '%1'").arg(fs.name)); return IScalarFieldWrapper::Shared(nullptr); } } break; - case Feature::DimX: + case Feature::Source::DimX: source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimX)); break; - case Feature::DimY: + case Feature::Source::DimY: source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimY)); break; - case Feature::DimZ: + case Feature::Source::DimZ: source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimZ)); break; - case Feature::Red: + case Feature::Source::Red: source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Red)); break; - case Feature::Green: + case Feature::Source::Green: source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Green)); break; - case Feature::Blue: + case Feature::Source::Blue: source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Blue)); break; } @@ -99,7 +94,11 @@ static IScalarFieldWrapper::Shared GetSource(const Feature::Shared& f, const ccP return source; } -bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QString& errorMessage, QWidget* parentWidget/*=nullptr*/) +bool Classifier::classify( const Feature::Source::Set& featureSources, + ccPointCloud* cloud, + QString& errorMessage, + QWidget* parentWidget/*=nullptr*/ + ) { if (!cloud) { @@ -114,9 +113,9 @@ bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QSt return false; } - if (features.empty()) + if (featureSources.empty()) { - errorMessage = QObject::tr("Training method called without any feature?!"); + errorMessage = QObject::tr("Training method called without any feature (source)?!"); return false; } @@ -138,7 +137,7 @@ bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QSt classificationSF->fill(0); //0 = no classification? int sampleCount = static_cast(cloud->size()); - int attributesPerSample = static_cast(features.size()); + int attributesPerSample = static_cast(featureSources.size()); ccLog::Print(QObject::tr("[3DMASC] Classifying %1 points with %2 feature(s)").arg(sampleCount).arg(attributesPerSample)); @@ -160,18 +159,13 @@ bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QSt wrappers.reserve(attributesPerSample); for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex) { - const Feature::Shared &f = features[fIndex]; - if (!f) - { - assert(false); - return false; - } + const Feature::Source& fs = featureSources[fIndex]; - IScalarFieldWrapper::Shared source = GetSource(f, cloud); + IScalarFieldWrapper::Shared source = GetSource(fs, cloud); if (!source || !source->isValid()) { assert(false); - errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(f->sourceName); + errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(fs.name); return false; } @@ -226,8 +220,21 @@ bool Classifier::classify(const Feature::Set& features, ccPointCloud* cloud, QSt return success; } -bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* testSubset, AccuracyMetrics& metrics, QString& errorMessage, QWidget* parentWidget/*=nullptr*/) +bool Classifier::evaluate(const Feature::Source::Set& featureSources, + ccPointCloud* testCloud, + AccuracyMetrics& metrics, + QString& errorMessage, + CCLib::ReferenceCloud* testSubset/*=nullptr=*/, + QString outputSFName/*=QString()*/, + QWidget* parentWidget/*=nullptr*/) { + if (!testCloud) + { + //invalid input + assert(false); + errorMessage = QObject::tr("Invalid input cloud"); + return false; + } metrics.sampleCount = metrics.goodGuess = 0; metrics.ratio = 0.0f; @@ -237,35 +244,52 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t return false; } - if (features.empty()) + if (featureSources.empty()) { - errorMessage = QObject::tr("Training method called without any feature?!"); + errorMessage = QObject::tr("Training method called without any feature (source)?!"); return false; } - if (!testSubset) + if (testSubset && testSubset->getAssociatedCloud() != testCloud) { - assert(false); - errorMessage = QObject::tr("No test subset provided"); - return false; - } - ccPointCloud* cloud = dynamic_cast(testSubset->getAssociatedCloud()); - if (!cloud) - { - errorMessage = QObject::tr("Invalid test subset (associated point cloud is not a ccPointCloud)"); + errorMessage = QObject::tr("Invalid test subset (associated point cloud is different)"); return false; } //look for the classification field - CCLib::ScalarField* classifSF = GetClassificationSF(cloud); - if (!classifSF || classifSF->size() < cloud->size()) + CCLib::ScalarField* classifSF = GetClassificationSF(testCloud); + if (!classifSF || classifSF->size() < testCloud->size()) { assert(false); - errorMessage = QObject::tr("Missing/Invalid 'Classification' field on input cloud"); + errorMessage = QObject::tr("Missing/invalid 'Classification' field on input cloud"); return false; } - int testSampleCount = static_cast(testSubset->size()); - int attributesPerSample = static_cast(features.size()); + CCLib::ScalarField* outputSF = nullptr; + if (!outputSFName.isEmpty()) + { + int outSFIndex = testCloud->getScalarFieldIndexByName(qPrintable(outputSFName)); + if (outSFIndex < 0) + { + ccScalarField* _outputSF = new ccScalarField(qPrintable(outputSFName)); + if (!_outputSF->resizeSafe(testCloud->size())) + { + errorMessage = QObject::tr("Not enough memory to create output scalar field"); + _outputSF->release(); + return false; + } + testCloud->addScalarField(_outputSF); + outputSF = _outputSF; + } + else + { + outputSF = testCloud->getScalarField(outSFIndex); + } + outputSF->fill(NAN_VALUE); + outputSF->computeMinAndMax(); + } + + unsigned testSampleCount = (testSubset ? testSubset->size() : testCloud->size()); + int attributesPerSample = static_cast(featureSources.size()); ccLog::Print(QObject::tr("[3DMASC] Testing data: %1 samples with %2 feature(s)").arg(testSampleCount).arg(attributesPerSample)); @@ -273,7 +297,7 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t cv::Mat test_data; try { - test_data.create(testSampleCount, attributesPerSample, CV_32FC1); + test_data.create(static_cast(testSampleCount), attributesPerSample, CV_32FC1); } catch (const cv::Exception& cvex) { @@ -294,24 +318,18 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t //fill the data matrix for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex) { - const Feature::Shared &f = features[fIndex]; - if (!f) - { - assert(false); - return false; - } - - IScalarFieldWrapper::Shared source = GetSource(f, cloud); + const Feature::Source& fs = featureSources[fIndex]; + IScalarFieldWrapper::Shared source = GetSource(fs, testCloud); if (!source || !source->isValid()) { assert(false); - errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(f->sourceName); + errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(fs.name); return false; } - for (unsigned i = 0; i < testSubset->size(); ++i) + for (unsigned i = 0; i < testSampleCount; ++i) { - unsigned pointIndex = testSubset->getPointGlobalIndex(i); + unsigned pointIndex = (testSubset ? testSubset->getPointGlobalIndex(i) : i); double value = source->pointValue(pointIndex); test_data.at(i, fIndex) = static_cast(value); } @@ -319,12 +337,12 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t //estimate the efficiency of the classifier { - metrics.sampleCount = testSubset->size(); + metrics.sampleCount = testSampleCount; metrics.goodGuess = 0; - for (unsigned i = 0; i < testSubset->size(); ++i) + for (unsigned i = 0; i < testSampleCount; ++i) { - unsigned pointIndex = testSubset->getPointGlobalIndex(i); + unsigned pointIndex = (testSubset ? testSubset->getPointGlobalIndex(i) : i); ScalarType pointClass = classifSF->getValue(pointIndex); int iClass = static_cast(pointClass); //if (iClass < 0 || iClass > 255) @@ -334,10 +352,15 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t //} float predictedClass = m_rtrees->predict(test_data.row(i)); - if (static_cast(predictedClass) == iClass) + int iPredictedClass = static_cast(predictedClass); + if (iPredictedClass == iClass) { ++metrics.goodGuess; } + if (outputSF) + { + outputSF->setValue(pointIndex, static_cast(iPredictedClass)); + } if (pDlg && !nProgress.oneStep()) { @@ -346,6 +369,9 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t } } + if (outputSF) + outputSF->computeMinAndMax(); + metrics.ratio = static_cast(metrics.goodGuess) / metrics.sampleCount; } @@ -354,15 +380,15 @@ bool Classifier::evaluate(const Feature::Set& features, CCLib::ReferenceCloud* t bool Classifier::train( const ccPointCloud* cloud, const RandomTreesParams& params, - const Feature::Set& features, + const Feature::Source::Set& featureSources, QString& errorMessage, CCLib::ReferenceCloud* trainSubset/*=nullptr*/, ccMainAppInterface* app/*=nullptr*/, QWidget* parentWidget/*=nullptr*/) { - if (features.empty()) + if (featureSources.empty()) { - errorMessage = QObject::tr("Training method called without any feature?!"); + errorMessage = QObject::tr("Training method called without any feature (source)?!"); return false; } if (!cloud) @@ -387,7 +413,7 @@ bool Classifier::train( const ccPointCloud* cloud, } int sampleCount = static_cast(trainSubset ? trainSubset->size() : cloud->size()); - int attributesPerSample = static_cast(features.size()); + int attributesPerSample = static_cast(featureSources.size()); if (app) { @@ -426,13 +452,13 @@ bool Classifier::train( const ccPointCloud* cloud, //fill the training data matrix for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex) { - const Feature::Shared &f = features[fIndex]; + const Feature::Source& fs = featureSources[fIndex]; - IScalarFieldWrapper::Shared source = GetSource(f, cloud); + IScalarFieldWrapper::Shared source = GetSource(fs, cloud); if (!source || !source->isValid()) { assert(false); - errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(f->sourceName); + errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(fs.name); return false; } diff --git a/q3DMASCClassifier.h b/q3DMASCClassifier.h index 35511da..8baaf79 100644 --- a/q3DMASCClassifier.h +++ b/q3DMASCClassifier.h @@ -46,7 +46,7 @@ namespace masc //! Train the classifier bool train( const ccPointCloud* cloud, const RandomTreesParams& params, - const Feature::Set& features, + const Feature::Source::Set& featureSources, QString& errorMessage, CCLib::ReferenceCloud* trainSubset = nullptr, ccMainAppInterface* app = nullptr, @@ -61,10 +61,19 @@ namespace masc }; //! Evaluates the classifier - bool evaluate(const Feature::Set& features, CCLib::ReferenceCloud* testSubset, AccuracyMetrics& metrics, QString& errorMessage, QWidget* parentWidget = nullptr); + bool evaluate( const Feature::Source::Set& featureSources, + ccPointCloud* testCloud, + AccuracyMetrics& metrics, + QString& errorMessage, + CCLib::ReferenceCloud* testSubset = nullptr, + QString outputSFName = QString(), + QWidget* parentWidget = nullptr); //! Applies the classifier - bool classify(const Feature::Set& features, ccPointCloud* cloud, QString& errorMessage, QWidget* parentWidget = nullptr); + bool classify( const Feature::Source::Set& featureSources, + ccPointCloud* cloud, + QString& errorMessage, + QWidget* parentWidget = nullptr); //! Returns whether the classifier is valid or not bool isValid() const; diff --git a/q3DMASCCommands.h b/q3DMASCCommands.h index 01b4374..ff21fc6 100644 --- a/q3DMASCCommands.h +++ b/q3DMASCCommands.h @@ -103,7 +103,7 @@ struct Command3DMASCClassif : public ccCommandLineInterface::Command } //try to load the clouds roles from the classifier file - QSet cloudLabels; + QList cloudLabels; QString corePointsLabel; bool filenamesSpecified = false; if (!masc::Tools::LoadClassifierCloudLabels(classifierFilename, cloudLabels, corePointsLabel, filenamesSpecified)) @@ -182,7 +182,9 @@ struct Command3DMASCClassif : public ccCommandLineInterface::Command //apply classifier { - if (!classifier.classify(features, corePoints.cloud, errorMessage, cmd.widgetParent())) + masc::Feature::Source::Set featureSources; + masc::Feature::ExtractSources(features, featureSources); + if (!classifier.classify(featureSources, corePoints.cloud, errorMessage, cmd.widgetParent())) { generatedScalarFields.releaseAllSFs(); return cmd.error(errorMessage); diff --git a/q3DMASCTools.cpp b/q3DMASCTools.cpp index 93f13ed..b30d613 100644 --- a/q3DMASCTools.cpp +++ b/q3DMASCTools.cpp @@ -41,7 +41,11 @@ using namespace masc; -bool Tools::SaveClassifier(QString filename, const Feature::Set& features, const masc::Classifier& classifier, QWidget* parent/*=nullptr*/) +bool Tools::SaveClassifier( QString filename, + const Feature::Set& features, + const QString corePointsRole, + const masc::Classifier& classifier, + QWidget* parent/*=nullptr*/) { //first save the classifier data (same base filename but with the ymal extension) QFileInfo fi(filename); @@ -66,13 +70,17 @@ bool Tools::SaveClassifier(QString filename, const Feature::Set& features, const stream << "classifier: " << yamlFilename << endl; //look for all clouds (labels) - QSet cloudLabels; + QList cloudLabels; for (Feature::Shared f : features) { - if (f->cloud1) - cloudLabels.insert(f->cloud1Label); - if (f->cloud2) - cloudLabels.insert(f->cloud2Label); + if (f->cloud1 && !cloudLabels.contains(f->cloud1Label)) + cloudLabels.push_back(f->cloud1Label); + if (f->cloud2 && !cloudLabels.contains(f->cloud2Label)) + cloudLabels.push_back(f->cloud2Label); + } + if (!corePointsRole.isEmpty() && !cloudLabels.contains(corePointsRole)) + { + cloudLabels.push_back(corePointsRole); } stream << "# Clouds (roles)" << endl; @@ -80,6 +88,12 @@ bool Tools::SaveClassifier(QString filename, const Feature::Set& features, const { stream << "cloud: " << label << endl; } + + if (!corePointsRole.isEmpty()) + { + stream << "# Core points (classified role)" << endl; + stream << "core_points: " << corePointsRole << endl; + } stream << "# Features" << endl; for (Feature::Shared f : features) @@ -90,7 +104,7 @@ bool Tools::SaveClassifier(QString filename, const Feature::Set& features, const return true; } -bool Tools::LoadClassifierCloudLabels(QString filename, QSet& labels, QString& corePointsLabel, bool& filenamesSpecified) +bool Tools::LoadClassifierCloudLabels(QString filename, QList& labels, QString& corePointsLabel, bool& filenamesSpecified) { //just in case corePointsLabel.clear(); @@ -127,7 +141,12 @@ bool Tools::LoadClassifierCloudLabels(QString filename, QSet& labels, Q } QString label = tokens.front(); - labels.insert(label); + if (labels.contains(label)) + { + ccLog::Warning(QString("Malformed file: role '%1:' is already defined/used on line #%2").arg(label).arg(lineNumber)); + return false; + } + labels.push_back(label); if (tokens.size() > 1) ++filenameCount; @@ -850,11 +869,11 @@ bool Tools::LoadClassifier(QString filename, const NamedClouds& clouds, Feature: bool Tools::LoadTrainingFile( QString filename, Feature::Set& rawFeatures, NamedClouds& loadedClouds, - CorePoints& corePoints, - TrainParameters& parameters) + TrainParameters& parameters, + CorePoints* corePoints/*=nullptr*/) { bool cloudsWereProvided = !loadedClouds.empty(); - if (LoadFileCommon(filename, loadedClouds, cloudsWereProvided, rawFeatures, &corePoints, nullptr, ¶meters, nullptr)) + if (LoadFileCommon(filename, loadedClouds, cloudsWereProvided, rawFeatures, corePoints, nullptr, ¶meters, nullptr)) { return true; } diff --git a/q3DMASCTools.h b/q3DMASCTools.h index d997c08..3209c81 100644 --- a/q3DMASCTools.h +++ b/q3DMASCTools.h @@ -38,11 +38,11 @@ namespace masc typedef QMap NamedClouds; - static bool LoadTrainingFile(QString filename, Feature::Set& rawFeatures, NamedClouds& loadedClouds, CorePoints& corePoints, TrainParameters& parameters); + static bool LoadTrainingFile(QString filename, Feature::Set& rawFeatures, NamedClouds& loadedClouds, TrainParameters& parameters, CorePoints* corePoints = nullptr); - static bool SaveClassifier(QString filename, const Feature::Set& features, const masc::Classifier& classifier, QWidget* parent = nullptr); + static bool SaveClassifier(QString filename, const Feature::Set& features, const QString corePointsRole, const masc::Classifier& classifier, QWidget* parent = nullptr); - static bool LoadClassifierCloudLabels(QString filename, QSet& labels, QString& corePointsLabel, bool& filenamesSpecified); + static bool LoadClassifierCloudLabels(QString filename, QList& 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 d922d7d..ac8e58a 100644 --- a/qClassify3DMASCDialog.cpp +++ b/qClassify3DMASCDialog.cpp @@ -90,9 +90,12 @@ Classify3DMASCDialog::Classify3DMASCDialog(ccMainAppInterface* app, bool trainMo } //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); + cloud1ComboBox->setCurrentIndex(/*cloudCount > 0 ? (cloudCount > 2 ? 1 : 0) : */-1); + connect(cloud1ComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onCloudChanged(int))); + cloud2ComboBox->setCurrentIndex(/*cloudCount > 1 ? (cloudCount > 2 ? 2 : 1) : */-1); + connect(cloud2ComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onCloudChanged(int))); + cloud3ComboBox->setCurrentIndex(/*cloudCount > 2 ? 0 : */-1); + connect(cloud3ComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onCloudChanged(int))); testCloudComboBox->setCurrentIndex(-1); if (cloudCount == 0 && app) @@ -110,7 +113,7 @@ Classify3DMASCDialog::Classify3DMASCDialog(ccMainAppInterface* app, bool trainMo onCloudChanged(0); } -void Classify3DMASCDialog::setCloudRoles(const QSet& roles, QString corePointsLabel) +void Classify3DMASCDialog::setCloudRoles(const QList& roles, QString corePointsLabel) { int index = 0; for (const QString& role : roles) diff --git a/qClassify3DMASCDialog.h b/qClassify3DMASCDialog.h index 2fd1b8b..a68d9fd 100644 --- a/qClassify3DMASCDialog.h +++ b/qClassify3DMASCDialog.h @@ -36,7 +36,7 @@ public: Classify3DMASCDialog(ccMainAppInterface* app, bool trainMode = false); //! Sets the clouds roles - void setCloudRoles(const QSet& roles, QString corePointsLabel); + void setCloudRoles(const QList& roles, QString corePointsLabel); //! Returns the selected point clouds void getClouds(QMap& clouds, QString& mainCloud) const;