From 9bbf288b8f57947b8cceca9fe6f11472877a3857 Mon Sep 17 00:00:00 2001 From: Paul Leroy Date: Wed, 31 May 2023 22:00:10 +0200 Subject: [PATCH] Prevent the user from cancelling the training. --- CMakeLists.txt | 2 +- FeaturesInterface.cpp | 4 +- ScalarFieldCollector.cpp | 16 +++--- confusionmatrix.cpp | 3 -- q3DMASC.cpp | 4 +- q3DMASCClassifier.cpp | 108 +++++++++++++++++++++++++-------------- q3DMASCTools.cpp | 44 +++++++++------- 7 files changed, 109 insertions(+), 72 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c5ee181..40d0b92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ if (INSTALL_Q3DMASC_PLUGIN) project( Q3DMASC_PLUGIN ) AddPlugin( NAME ${PROJECT_NAME} ) - set(Q3DMASC_PLUGIN_VERSION "0.7+") + set(Q3DMASC_PLUGIN_VERSION "0.8") include( CMakePolicies NO_POLICY_SCOPE ) diff --git a/FeaturesInterface.cpp b/FeaturesInterface.cpp index b71bf90..d68af77 100644 --- a/FeaturesInterface.cpp +++ b/FeaturesInterface.cpp @@ -51,12 +51,12 @@ CCCoreLib::ScalarField* Feature::PrepareSF(ccPointCloud* cloud, const char* resu int sfIdx = cloud->getScalarFieldIndexByName(resultSFName); if (sfIdx >= 0) { - ccLog::Warning("Existing SF: " + QString(resultSFName) + ", do not store in generatedScalarFields"); +// ccLog::Warning("Existing SF: " + QString(resultSFName) + ", do not store in generatedScalarFields"); resultSF = cloud->getScalarField(sfIdx); } else { - ccLog::Warning("SF does not exist, create it: " + QString(resultSFName) + ", SFCollector::Behavior " + QString::number(behavior)); +// ccLog::Warning("SF does not exist, create it: " + QString(resultSFName) + ", SFCollector::Behavior " + QString::number(behavior)); ccScalarField* newSF = new ccScalarField(resultSFName); if (!newSF->resizeSafe(cloud->size())) { diff --git a/ScalarFieldCollector.cpp b/ScalarFieldCollector.cpp index ad505bb..5b4b765 100644 --- a/ScalarFieldCollector.cpp +++ b/ScalarFieldCollector.cpp @@ -29,10 +29,10 @@ void SFCollector::push(ccPointCloud* cloud, CCCoreLib::ScalarField* sf, Behavior behavior) { assert(!scalarFields.contains(sf)); - if (scalarFields.contains(sf)) - ccLog::Warning(QString("[SFCollector] scalar field '%1' HAS ALREADY BEEN COLLECTED").arg(sf->getName()) + ", behaviour " + QString::number(behavior)); - else - ccLog::Warning(QString("[SFCollector] collect scalar field '%1'").arg(sf->getName()) + ", behaviour " + QString::number(behavior)); +// if (scalarFields.contains(sf)) +// ccLog::Warning(QString("[SFCollector] scalar field '%1' HAS ALREADY BEEN COLLECTED").arg(sf->getName()) + ", behaviour " + QString::number(behavior)); +// else +// ccLog::Warning(QString("[SFCollector] collect scalar field '%1'").arg(sf->getName()) + ", behaviour " + QString::number(behavior)); SFDesc desc; desc.behavior = behavior; desc.cloud = cloud; @@ -48,7 +48,7 @@ void SFCollector::releaseSFs(bool keepByDefault) if (desc.behavior == ALWAYS_KEEP || (keepByDefault && desc.behavior == CAN_REMOVE)) { - ccLog::Warning(QString("[SFCollector] Keep scalar field '%1'").arg(sf->getName())); +// ccLog::Warning(QString("[SFCollector] Keep scalar field '%1'").arg(sf->getName())); //keep this SF continue; } @@ -56,7 +56,7 @@ void SFCollector::releaseSFs(bool keepByDefault) int sfIdx = desc.cloud->getScalarFieldIndexByName(sf->getName()); if (sfIdx >= 0) { - ccLog::Warning(QString("[SFCollector] Remove scalar field '%1'").arg(sf->getName())); +// ccLog::Warning(QString("[SFCollector] Remove scalar field '%1'").arg(sf->getName())); desc.cloud->deleteScalarField(sfIdx); } else @@ -72,9 +72,9 @@ bool SFCollector::setBehavior(CCCoreLib::ScalarField *sf, Behavior behavior) { if (scalarFields.contains(sf)) { - Behavior previousBehavior = scalarFields[sf].behavior; +// Behavior previousBehavior = scalarFields[sf].behavior; scalarFields[sf].behavior = behavior; - ccLog::Warning("behavior of " + QString(sf->getName()) + " changed from " + QString::number(previousBehavior) + " to " + QString::number(behavior)); +// ccLog::Warning("behavior of " + QString(sf->getName()) + " changed from " + QString::number(previousBehavior) + " to " + QString::number(behavior)); } return true; diff --git a/confusionmatrix.cpp b/confusionmatrix.cpp index befaf73..8cebfff 100644 --- a/confusionmatrix.cpp +++ b/confusionmatrix.cpp @@ -229,8 +229,6 @@ void ConfusionMatrix::compute(const std::vector& actual, const std:: // FILL THE QTABLEWIDGET // add the confusion matrix values - QBrush greenBrush(QColorConstants::Svg::palegreen); - QFont f( "Tahoma", 10, QFont::Bold ); for (int row = 0; row < nbClasses; row++) for (int column = 0; column < nbClasses; column++) { @@ -239,7 +237,6 @@ void ConfusionMatrix::compute(const std::vector& actual, const std:: if (row == column) { newItem->setBackground(getColor(val / vec_TP_FN.at(row, 0), 0, 128, 255)); - newItem->setFont(f); } else newItem->setBackground(getColor(val / vec_TP_FN.at(row, 0), 200, 50, 50)); diff --git a/q3DMASC.cpp b/q3DMASC.cpp index d1a1da0..962dcfa 100644 --- a/q3DMASC.cpp +++ b/q3DMASC.cpp @@ -507,7 +507,7 @@ void q3DMASCPlugin::doTrainAction() { 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)) + if (!masc::Tools::PrepareFeatures(corePoints, toPrepare, error, &progressDlg, &generatedScalarFields)) { m_app->dispToConsole(error, ccMainAppInterface::ERR_CONSOLE_MESSAGE); generatedScalarFields.releaseSFs(false); @@ -655,7 +655,7 @@ void q3DMASCPlugin::doTrainAction() errorMessage, trainDlg, testCloud ? nullptr : testSubset.data(), - "Classification_pred", + testCloud ? "Classification_prediction" : "", // outputSFName, empty is the test cloud is not a separate cloud m_app->getMainWindow())) { m_app->dispToConsole(errorMessage, ccMainAppInterface::ERR_CONSOLE_MESSAGE); diff --git a/q3DMASCClassifier.cpp b/q3DMASCClassifier.cpp index b210192..9b9c4bb 100644 --- a/q3DMASCClassifier.cpp +++ b/q3DMASCClassifier.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include "qTrain3DMASCDialog.h" #include "confusionmatrix.h" @@ -123,28 +124,30 @@ bool Classifier::classify( const Feature::Source::Set& featureSources, return false; } - //look for the classification field - CCCoreLib::ScalarField* classificationSF = Tools::GetClassificationSF(cloud); // add a ccConfidence value if needed int cvConfidenceIdx = cloud->getScalarFieldIndexByName("Classification_confidence"); - if (cvConfidenceIdx > 0) // if the scalar field exists, delete it + if (cvConfidenceIdx >= 0) // if the scalar field exists, delete it cloud->deleteScalarField(cvConfidenceIdx); cvConfidenceIdx = cloud->addScalarField("Classification_confidence"); CCCoreLib::ScalarField* cvConfidenceSF = cloud->getScalarField(cvConfidenceIdx); + //look for the classification field + CCCoreLib::ScalarField* classificationSF = Tools::GetClassificationSF(cloud); ccScalarField* classifSFBackup = nullptr; - if (classificationSF) + if (classificationSF) //save classification field (if any) { - //save previous classification field (if any) - int sfIdx = cloud->getScalarFieldIndexByName("Classification_prev"); - if (sfIdx > 0) + ccLog::Warning("Classification SF found: copy it in Classification_backup, a confusion matrix will be generated"); + // delete Classification_backup field (if any) + int sfIdx = cloud->getScalarFieldIndexByName("Classification_backup"); + if (sfIdx >= 0) cloud->deleteScalarField(sfIdx); + // backup the classification field try { - classifSFBackup = new ccScalarField(*static_cast(classificationSF)); - classifSFBackup->setName("Classification_prev"); + classifSFBackup = new ccScalarField(*static_cast(classificationSF)); // copy constructor + classifSFBackup->setName("Classification_backup"); cloud->addScalarField(classifSFBackup); } catch (const std::bad_alloc) @@ -237,7 +240,7 @@ bool Classifier::classify( const Feature::Source::Set& featureSources, cv::Mat result; m_rtrees->getVotes(test_data, result, cv::ml::DTrees::PREDICT_MAX_VOTE); int classIndex = -1; - for (int col = 0; col < result.cols; col++) + for (int col = 0; col < result.cols; col++) // look for the index of the predicted class if (predictedClass == result.at(0, col)) { classIndex = col; @@ -245,8 +248,8 @@ bool Classifier::classify( const Feature::Source::Set& featureSources, } if (classIndex != -1) { - float nbVotes = result.at(1, classIndex); - cvConfidenceSF->setValue(i, static_cast(nbVotes / numberOfTrees)); + float nbVotes = result.at(1, classIndex); // get the number of votes + cvConfidenceSF->setValue(i, static_cast(nbVotes / numberOfTrees)); // compute the confidence } else cvConfidenceSF->setValue(i, CCCoreLib::NAN_VALUE); @@ -326,28 +329,31 @@ bool Classifier::evaluate(const Feature::Source::Set& featureSources, return false; } - CCCoreLib::ScalarField* outputSF = nullptr; + CCCoreLib::ScalarField* outSF = nullptr; + CCCoreLib::ScalarField* cvConfidenceSF = nullptr; + + ccLog::Warning("[evaluate] TEST cloud " + testCloud->getName()); + 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; - } + int outIdx = testCloud->getScalarFieldIndexByName(qPrintable(outputSFName)); + if (outIdx >= 0) + testCloud->deleteScalarField(outIdx); else - { - outputSF = testCloud->getScalarField(outSFIndex); - } - outputSF->fill(CCCoreLib::NAN_VALUE); - outputSF->computeMinAndMax(); + ccLog::Warning("add " + outputSFName + " to the TEST cloud"); + outIdx = testCloud->addScalarField(qPrintable(outputSFName)); + outSF = testCloud->getScalarField(outIdx); + } + + if (outSF) // add a Classification_confidence value to the test cloud if needed + { + int cvConfidenceIdx = testCloud->getScalarFieldIndexByName("Classification_confidence"); + if (cvConfidenceIdx >= 0) // if the scalar field exists, delete it + testCloud->deleteScalarField(cvConfidenceIdx); + else + ccLog::Warning("add Classification_confidence to the TEST cloud"); + cvConfidenceIdx = testCloud->addScalarField("Classification_confidence"); + cvConfidenceSF = testCloud->getScalarField(cvConfidenceIdx); } unsigned testSampleCount = (testSubset ? testSubset->size() : testCloud->size()); @@ -397,6 +403,7 @@ bool Classifier::evaluate(const Feature::Source::Set& featureSources, } } + int numberOfTrees = m_rtrees->getRoots().size(); //estimate the efficiency of the classifier std::vector actualClass(testSampleCount); @@ -424,9 +431,29 @@ bool Classifier::evaluate(const Feature::Source::Set& featureSources, { ++metrics.goodGuess; } - if (outputSF) + if (outSF) { - outputSF->setValue(pointIndex, static_cast(iPredictedClass)); + outSF->setValue(pointIndex, static_cast(iPredictedClass)); + if (cvConfidenceSF) + { + // compute the confidence + cv::Mat result; + m_rtrees->getVotes(test_data.row(i), result, cv::ml::DTrees::PREDICT_MAX_VOTE); + int classIndex = -1; + for (int col = 0; col < result.cols; col++) // look for the index of the predicted class + if (iPredictedClass == result.at(0, col)) + { + classIndex = col; + break; + } + if (classIndex != -1) + { + float nbVotes = result.at(1, classIndex); // get the number of votes + cvConfidenceSF->setValue(i, static_cast(nbVotes / numberOfTrees)); // compute the confidence + } + else + cvConfidenceSF->setValue(i, CCCoreLib::NAN_VALUE); + } } if (pDlg && !nProgress.oneStep()) @@ -436,8 +463,10 @@ bool Classifier::evaluate(const Feature::Source::Set& featureSources, } } - if (outputSF) - outputSF->computeMinAndMax(); + if (outSF) + outSF->computeMinAndMax(); + if (cvConfidenceSF) + cvConfidenceSF->computeMinAndMax(); metrics.ratio = static_cast(metrics.goodGuess) / metrics.sampleCount; } @@ -617,8 +646,13 @@ bool Classifier::train( const ccPointCloud* cloud, { if (pDlg->wasCanceled()) { - future.cancel(); - break; +// future.cancel(); + QMessageBox msgBox; + msgBox.setText("The training is still in progress, not possible to cancel."); + msgBox.exec(); +// break; + pDlg->reset(); + pDlg->show(); } pDlg->setValue(pDlg->value() + 1); } diff --git a/q3DMASCTools.cpp b/q3DMASCTools.cpp index 6cd94a6..db1049c 100644 --- a/q3DMASCTools.cpp +++ b/q3DMASCTools.cpp @@ -1060,9 +1060,8 @@ bool Tools::PrepareFeatures(const CorePoints& corePoints, Feature::Set& features return false; } - // if the feature already exists and if useExistingFeatures is checked, simply populate generatedScalarFields //prepare the feature - if (!feature->prepare(corePoints, errorStr, progressCb, generatedScalarFields)) + if (!feature->prepare(corePoints, errorStr, progressCb, generatedScalarFields)) { //something failed (error should be up to date) return false; @@ -1089,18 +1088,21 @@ bool Tools::PrepareFeatures(const CorePoints& corePoints, Feature::Set& features fas.scales.push_back(feature->scale); } } - //build the scaled feature list attached to the second cloud (if any) - if (feature->cloud2 && feature->cloud2 != feature->cloud1 && feature->op != Feature::NO_OPERATION - && !static_cast(feature.data())->statSF1WasAlreadyExisting - && !static_cast(feature.data())->statSF2WasAlreadyExisting) // nothing to compute if the scalar field was already there + if (feature->cloud2 + && feature->cloud2 != feature->cloud1 + && feature->op != Feature::NO_OPERATION + && !static_cast(feature.data())->statSF1WasAlreadyExisting) // nothing to compute if the scalar field was already there { - FeaturesAndScales& fas = cloudsWithScaledFeatures[feature->cloud2]; - ++fas.featureCount; - fas.pointFeaturesPerScale[feature->scale].push_back(qSharedPointerCast(feature)); - if (std::find(fas.scales.begin(), fas.scales.end(), feature->scale) == fas.scales.end()) + if (!static_cast(feature.data())->statSF2WasAlreadyExisting) { - fas.scales.push_back(feature->scale); + FeaturesAndScales& fas = cloudsWithScaledFeatures[feature->cloud2]; + ++fas.featureCount; + fas.pointFeaturesPerScale[feature->scale].push_back(qSharedPointerCast(feature)); + if (std::find(fas.scales.begin(), fas.scales.end(), feature->scale) == fas.scales.end()) + { + fas.scales.push_back(feature->scale); + } } } } @@ -1123,16 +1125,20 @@ bool Tools::PrepareFeatures(const CorePoints& corePoints, Feature::Set& features } //build the scaled feature list attached to the second cloud (if any) - if (feature->cloud2 && feature->cloud2 != feature->cloud1 && feature->op != Feature::NO_OPERATION - && !static_cast(feature.data())->sf1WasAlreadyExisting - && !static_cast(feature.data())->sf2WasAlreadyExisting) // nothing to compute if the scalar field was already there + if (feature->cloud2 + && feature->cloud2 != feature->cloud1 + && feature->op != Feature::NO_OPERATION + && !static_cast(feature.data())->sf1WasAlreadyExisting) // nothing to compute if the scalar field was already there { - FeaturesAndScales& fas = cloudsWithScaledFeatures[feature->cloud2]; - fas.neighborhoodFeaturesPerScale[feature->scale].push_back(qSharedPointerCast(feature)); - ++fas.featureCount; - if (std::find(fas.scales.begin(), fas.scales.end(), feature->scale) == fas.scales.end()) + if (!static_cast(feature.data())->sf2WasAlreadyExisting) { - fas.scales.push_back(feature->scale); + FeaturesAndScales& fas = cloudsWithScaledFeatures[feature->cloud2]; + fas.neighborhoodFeaturesPerScale[feature->scale].push_back(qSharedPointerCast(feature)); + ++fas.featureCount; + if (std::find(fas.scales.begin(), fas.scales.end(), feature->scale) == fas.scales.end()) + { + fas.scales.push_back(feature->scale); + } } } }