mirror of
https://github.com/dgirardeau/q3DMASC.git
synced 2026-08-29 16:40:49 +08:00
752 lines
22 KiB
C++
752 lines
22 KiB
C++
//##########################################################################
|
|
//# #
|
|
//# CLOUDCOMPARE PLUGIN: q3DMASC #
|
|
//# #
|
|
//# This program is free software; you can redistribute it and/or modify #
|
|
//# it under the terms of the GNU General Public License as published by #
|
|
//# the Free Software Foundation; version 2 or later of the License. #
|
|
//# #
|
|
//# This program is distributed in the hope that it will be useful, #
|
|
//# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
|
//# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
|
//# GNU General Public License for more details. #
|
|
//# #
|
|
//# COPYRIGHT: Dimitri Lague / CNRS / UEB #
|
|
//# #
|
|
//##########################################################################
|
|
|
|
#include "q3DMASCClassifier.h"
|
|
|
|
//Local
|
|
#include "ScalarFieldWrappers.h"
|
|
#include "q3DMASCTools.h"
|
|
|
|
//qCC_db
|
|
#include <ccPointCloud.h>
|
|
#include <ccScalarField.h>
|
|
#include <ccProgressDialog.h>
|
|
#include <ccLog.h>
|
|
|
|
//qPDALIO
|
|
#include "../../../core/IO/qPDALIO/include/LASFields.h"
|
|
|
|
//qCC_plugins
|
|
#include <ccMainAppInterface.h>
|
|
|
|
//Qt
|
|
#include <QCoreApplication>
|
|
#include <QProgressDialog>
|
|
#include <QtConcurrent>
|
|
#include <QMessageBox>
|
|
|
|
#include "qTrain3DMASCDialog.h"
|
|
#include "confusionmatrix.h"
|
|
|
|
#if defined(_OPENMP)
|
|
#include <omp.h>
|
|
#endif
|
|
|
|
using namespace masc;
|
|
|
|
Classifier::Classifier()
|
|
{
|
|
}
|
|
|
|
bool Classifier::isValid() const
|
|
{
|
|
return (m_rtrees && m_rtrees->isClassifier() && m_rtrees->isTrained());
|
|
}
|
|
|
|
static IScalarFieldWrapper::Shared GetSource(const Feature::Source& fs, const ccPointCloud* cloud)
|
|
{
|
|
IScalarFieldWrapper::Shared source(nullptr);
|
|
|
|
switch (fs.type)
|
|
{
|
|
case Feature::Source::ScalarField:
|
|
{
|
|
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: unknown scalar field '%1'").arg(fs.name));
|
|
return IScalarFieldWrapper::Shared(nullptr);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Feature::Source::DimX:
|
|
source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimX));
|
|
break;
|
|
case Feature::Source::DimY:
|
|
source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimY));
|
|
break;
|
|
case Feature::Source::DimZ:
|
|
source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimZ));
|
|
break;
|
|
|
|
case Feature::Source::Red:
|
|
source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Red));
|
|
break;
|
|
case Feature::Source::Green:
|
|
source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Green));
|
|
break;
|
|
case Feature::Source::Blue:
|
|
source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Blue));
|
|
break;
|
|
}
|
|
|
|
return source;
|
|
}
|
|
|
|
bool Classifier::classify( const Feature::Source::Set& featureSources,
|
|
ccPointCloud* cloud,
|
|
QString& errorMessage,
|
|
QWidget* parentWidget/*=nullptr*/
|
|
)
|
|
{
|
|
if (!cloud)
|
|
{
|
|
assert(false);
|
|
errorMessage = QObject::tr("Invalid input");
|
|
return false;
|
|
}
|
|
|
|
if (!isValid())
|
|
{
|
|
errorMessage = QObject::tr("Invalid classifier");
|
|
return false;
|
|
}
|
|
|
|
if (featureSources.empty())
|
|
{
|
|
errorMessage = QObject::tr("Training method called without any feature (source)?!");
|
|
return false;
|
|
}
|
|
|
|
// add a ccConfidence value if needed
|
|
int cvConfidenceIdx = cloud->getScalarFieldIndexByName("Classification_confidence");
|
|
if (cvConfidenceIdx >= 0) // if the scalar field exists, delete it
|
|
cloud->deleteScalarField(cvConfidenceIdx);
|
|
cvConfidenceIdx = cloud->addScalarField("Classification_confidence");
|
|
ccScalarField* cvConfidenceSF = static_cast<ccScalarField*>(cloud->getScalarField(cvConfidenceIdx));
|
|
|
|
//look for the classification field
|
|
CCCoreLib::ScalarField* classificationSF = Tools::GetClassificationSF(cloud);
|
|
ccScalarField* classifSFBackup = nullptr;
|
|
|
|
if (classificationSF) //save classification field (if any) by renaming it "Classification_backup"
|
|
{
|
|
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);
|
|
|
|
classificationSF->setName("Classification_backup"); // rename the classification field
|
|
classifSFBackup = static_cast<ccScalarField*>(classificationSF);
|
|
}
|
|
|
|
//create the classification SF
|
|
ccScalarField* _classificationSF = new ccScalarField(LAS_FIELD_NAMES[LAS_CLASSIFICATION]);
|
|
if (!_classificationSF->resizeSafe(cloud->size()))
|
|
{
|
|
_classificationSF->release();
|
|
errorMessage = QObject::tr("Not enough memory");
|
|
return false;
|
|
}
|
|
cloud->addScalarField(_classificationSF);
|
|
classificationSF = _classificationSF;
|
|
|
|
assert(classificationSF);
|
|
classificationSF->fill(0); //0 = no classification?
|
|
|
|
int sampleCount = static_cast<int>(cloud->size());
|
|
int attributesPerSample = static_cast<int>(featureSources.size());
|
|
|
|
ccLog::Print(QObject::tr("[3DMASC] Classifying %1 points with %2 feature(s)").arg(sampleCount).arg(attributesPerSample));
|
|
|
|
//create the field wrappers
|
|
std::vector< IScalarFieldWrapper::Shared > wrappers;
|
|
{
|
|
wrappers.reserve(attributesPerSample);
|
|
for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
|
|
{
|
|
const Feature::Source& fs = featureSources[fIndex];
|
|
|
|
IScalarFieldWrapper::Shared source = GetSource(fs, cloud);
|
|
if (!source || !source->isValid())
|
|
{
|
|
assert(false);
|
|
errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(fs.name);
|
|
return false;
|
|
}
|
|
|
|
wrappers.push_back(source);
|
|
}
|
|
}
|
|
|
|
QScopedPointer<ccProgressDialog> pDlg;
|
|
if (parentWidget)
|
|
{
|
|
pDlg.reset(new ccProgressDialog(parentWidget));
|
|
pDlg->setLabelText(QString("Classify (%1 points)").arg(sampleCount));
|
|
pDlg->show();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
CCCoreLib::NormalizedProgress nProgress(pDlg.data(), cloud->size());
|
|
|
|
bool success = true;
|
|
int numberOfTrees = static_cast<int>(m_rtrees->getRoots().size());
|
|
#ifndef _DEBUG
|
|
#if defined(_OPENMP)
|
|
omp_set_num_threads(std::max(1, omp_get_max_threads() - 2));
|
|
#pragma omp parallel for
|
|
#endif
|
|
#endif
|
|
for (int i = 0; i < static_cast<int>(cloud->size()); ++i)
|
|
{
|
|
//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();
|
|
success = false;
|
|
break;
|
|
}
|
|
|
|
for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
|
|
{
|
|
double value = wrappers[fIndex]->pointValue(i);
|
|
test_data.at<float>(0, fIndex) = static_cast<float>(value);
|
|
}
|
|
|
|
float predictedClass = m_rtrees->predict(test_data.row(0), cv::noArray(), cv::ml::DTrees::PREDICT_MAX_VOTE);
|
|
classificationSF->setValue(i, static_cast<int>(predictedClass));
|
|
// compute the confidence
|
|
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++) // look for the index of the predicted class
|
|
if (predictedClass == result.at<int>(0, col))
|
|
{
|
|
classIndex = col;
|
|
break;
|
|
}
|
|
if (classIndex != -1)
|
|
{
|
|
float nbVotes = result.at<int>(1, classIndex); // get the number of votes
|
|
cvConfidenceSF->setValue(i, static_cast<ScalarType>(nbVotes / numberOfTrees)); // compute the confidence
|
|
}
|
|
else
|
|
cvConfidenceSF->setValue(i, CCCoreLib::NAN_VALUE);
|
|
|
|
if (pDlg && !nProgress.oneStep())
|
|
{
|
|
//process cancelled by the user
|
|
success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
classificationSF->computeMinAndMax();
|
|
cvConfidenceSF->computeMinAndMax();
|
|
|
|
//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();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
if (classifSFBackup != nullptr)
|
|
ConfusionMatrix *confusionMatrix = new ConfusionMatrix(*classifSFBackup, *classificationSF);
|
|
|
|
return success;
|
|
}
|
|
|
|
bool Classifier::evaluate(const Feature::Source::Set& featureSources,
|
|
ccPointCloud* testCloud,
|
|
AccuracyMetrics& metrics,
|
|
QString& errorMessage,
|
|
Train3DMASCDialog& train3DMASCDialog,
|
|
CCCoreLib::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;
|
|
|
|
if (!m_rtrees || !m_rtrees->isTrained())
|
|
{
|
|
errorMessage = QObject::tr("Classifier hasn't been trained yet");
|
|
return false;
|
|
}
|
|
|
|
if (featureSources.empty())
|
|
{
|
|
errorMessage = QObject::tr("Training method called without any feature (source)?!");
|
|
return false;
|
|
}
|
|
if (testSubset && testSubset->getAssociatedCloud() != testCloud)
|
|
{
|
|
errorMessage = QObject::tr("Invalid test subset (associated point cloud is different)");
|
|
return false;
|
|
}
|
|
|
|
//look for the classification field
|
|
CCCoreLib::ScalarField* classifSF = Tools::GetClassificationSF(testCloud);
|
|
if (!classifSF || classifSF->size() < testCloud->size())
|
|
{
|
|
assert(false);
|
|
errorMessage = QObject::tr("Missing/invalid 'Classification' field on input cloud");
|
|
return false;
|
|
}
|
|
|
|
ccScalarField* outSF = nullptr;
|
|
ccScalarField* cvConfidenceSF = nullptr;
|
|
|
|
if (!outputSFName.isEmpty())
|
|
{
|
|
int outIdx = testCloud->getScalarFieldIndexByName(qPrintable(outputSFName));
|
|
if (outIdx >= 0)
|
|
testCloud->deleteScalarField(outIdx);
|
|
else
|
|
ccLog::Print("add " + outputSFName + " to the TEST cloud");
|
|
outIdx = testCloud->addScalarField(qPrintable(outputSFName));
|
|
outSF = static_cast<ccScalarField*>(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::Print("add Classification_confidence to the TEST cloud");
|
|
cvConfidenceIdx = testCloud->addScalarField("Classification_confidence");
|
|
cvConfidenceSF = static_cast<ccScalarField*>(testCloud->getScalarField(cvConfidenceIdx));
|
|
}
|
|
|
|
unsigned testSampleCount = (testSubset ? testSubset->size() : testCloud->size());
|
|
int attributesPerSample = static_cast<int>(featureSources.size());
|
|
|
|
ccLog::Print(QObject::tr("[3DMASC] Testing data: %1 samples with %2 feature(s)").arg(testSampleCount).arg(attributesPerSample));
|
|
|
|
//allocate the data matrix
|
|
cv::Mat test_data;
|
|
try
|
|
{
|
|
test_data.create(static_cast<int>(testSampleCount), attributesPerSample, CV_32FC1);
|
|
}
|
|
catch (const cv::Exception& cvex)
|
|
{
|
|
errorMessage = cvex.msg.c_str();
|
|
return false;
|
|
}
|
|
|
|
QScopedPointer<ccProgressDialog> pDlg;
|
|
if (parentWidget)
|
|
{
|
|
pDlg.reset(new ccProgressDialog(parentWidget));
|
|
pDlg->setLabelText(QString("Evaluating the classifier on %1 points").arg(testSampleCount));
|
|
pDlg->show();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
CCCoreLib::NormalizedProgress nProgress(pDlg.data(), testSampleCount);
|
|
|
|
//fill the data matrix
|
|
for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
|
|
{
|
|
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(fs.name);
|
|
return false;
|
|
}
|
|
|
|
for (unsigned i = 0; i < testSampleCount; ++i)
|
|
{
|
|
unsigned pointIndex = (testSubset ? testSubset->getPointGlobalIndex(i) : i);
|
|
double value = source->pointValue(pointIndex);
|
|
test_data.at<float>(i, fIndex) = static_cast<float>(value);
|
|
}
|
|
}
|
|
|
|
int numberOfTrees = static_cast<int>(m_rtrees->getRoots().size());
|
|
|
|
//estimate the efficiency of the classifier
|
|
std::vector<ScalarType> actualClass(testSampleCount);
|
|
std::vector<ScalarType> predictectedClass(testSampleCount);
|
|
{
|
|
metrics.sampleCount = testSampleCount;
|
|
metrics.goodGuess = 0;
|
|
|
|
for (unsigned i = 0; i < testSampleCount; ++i)
|
|
{
|
|
unsigned pointIndex = (testSubset ? testSubset->getPointGlobalIndex(i) : i);
|
|
ScalarType pointClass = classifSF->getValue(pointIndex);
|
|
int iClass = static_cast<int>(pointClass);
|
|
//if (iClass < 0 || iClass > 255)
|
|
//{
|
|
// errorMessage = QObject::tr("Classification values out of range (0-255)");
|
|
// return false;
|
|
//}
|
|
|
|
float fPredictedClass = m_rtrees->predict(test_data.row(i), cv::noArray(), cv::ml::DTrees::PREDICT_MAX_VOTE);
|
|
int iPredictedClass = static_cast<int>(fPredictedClass);
|
|
actualClass.at(i) = iClass;
|
|
predictectedClass.at(i) = iPredictedClass;
|
|
if (iPredictedClass == iClass)
|
|
{
|
|
++metrics.goodGuess;
|
|
}
|
|
if (outSF)
|
|
{
|
|
outSF->setValue(pointIndex, static_cast<ScalarType>(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<int>(0, col))
|
|
{
|
|
classIndex = col;
|
|
break;
|
|
}
|
|
if (classIndex != -1)
|
|
{
|
|
float nbVotes = result.at<int>(1, classIndex); // get the number of votes
|
|
cvConfidenceSF->setValue(i, static_cast<ScalarType>(nbVotes / numberOfTrees)); // compute the confidence
|
|
}
|
|
else
|
|
cvConfidenceSF->setValue(i, CCCoreLib::NAN_VALUE);
|
|
}
|
|
}
|
|
|
|
if (pDlg && !nProgress.oneStep())
|
|
{
|
|
//process cancelled by the user
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (outSF)
|
|
outSF->computeMinAndMax();
|
|
if (cvConfidenceSF)
|
|
cvConfidenceSF->computeMinAndMax();
|
|
|
|
metrics.ratio = static_cast<float>(metrics.goodGuess) / metrics.sampleCount;
|
|
}
|
|
|
|
train3DMASCDialog.addConfusionMatrixAndSaveTraces(new ConfusionMatrix(actualClass, predictectedClass));
|
|
|
|
//show the Classification_prediction field by default
|
|
if (outSF)
|
|
{
|
|
int classifSFIdx = testCloud->getScalarFieldIndexByName(outSF->getName());
|
|
testCloud->setCurrentDisplayedScalarField(classifSFIdx);
|
|
testCloud->showSF(true);
|
|
}
|
|
|
|
if (parentWidget && testCloud->getDisplay())
|
|
{
|
|
testCloud->getDisplay()->redraw();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Classifier::train( const ccPointCloud* cloud,
|
|
const RandomTreesParams& params,
|
|
const Feature::Source::Set& featureSources,
|
|
QString& errorMessage,
|
|
CCCoreLib::ReferenceCloud* trainSubset/*=nullptr*/,
|
|
ccMainAppInterface* app/*=nullptr*/,
|
|
QWidget* parentWidget/*=nullptr*/)
|
|
{
|
|
if (featureSources.empty())
|
|
{
|
|
errorMessage = QObject::tr("Training method called without any feature (source)?!");
|
|
return false;
|
|
}
|
|
if (!cloud)
|
|
{
|
|
errorMessage = QObject::tr("Invalid input cloud");
|
|
return false;
|
|
}
|
|
|
|
if (trainSubset && trainSubset->getAssociatedCloud() != cloud)
|
|
{
|
|
errorMessage = QObject::tr("Invalid train subset (associated point cloud is different)");
|
|
return false;
|
|
}
|
|
|
|
//look for the classification field
|
|
CCCoreLib::ScalarField* classifSF = Tools::GetClassificationSF(cloud);
|
|
if (!classifSF || classifSF->size() < cloud->size())
|
|
{
|
|
assert(false);
|
|
errorMessage = QObject::tr("Missing/invalid 'Classification' field on input cloud");
|
|
return false;
|
|
}
|
|
|
|
int sampleCount = static_cast<int>(trainSubset ? trainSubset->size() : cloud->size());
|
|
int attributesPerSample = static_cast<int>(featureSources.size());
|
|
|
|
if (app)
|
|
{
|
|
app->dispToConsole(QString("[3DMASC] Training data: %1 samples with %2 feature(s)").arg(sampleCount).arg(attributesPerSample));
|
|
}
|
|
|
|
cv::Mat training_data, train_labels;
|
|
try
|
|
{
|
|
training_data.create(sampleCount, attributesPerSample, CV_32FC1);
|
|
train_labels.create(sampleCount, 1, CV_32FC1);
|
|
}
|
|
catch (const cv::Exception& cvex)
|
|
{
|
|
errorMessage = cvex.msg.c_str();
|
|
return false;
|
|
}
|
|
|
|
//fill the classification labels vector
|
|
{
|
|
for (int i = 0; i < sampleCount; ++i)
|
|
{
|
|
int pointIndex = (trainSubset ? static_cast<int>(trainSubset->getPointGlobalIndex(i)) : i);
|
|
ScalarType pointClass = classifSF->getValue(pointIndex);
|
|
int iClass = static_cast<int>(pointClass);
|
|
//if (iClass < 0 || iClass > 255)
|
|
//{
|
|
// errorMessage = QObject::tr("Classification values out of range (0-255)");
|
|
// return false;
|
|
//}
|
|
|
|
train_labels.at<float>(i) = static_cast<unsigned char>(iClass);
|
|
}
|
|
}
|
|
|
|
//fill the training data matrix
|
|
for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
|
|
{
|
|
const Feature::Source& fs = featureSources[fIndex];
|
|
|
|
IScalarFieldWrapper::Shared source = GetSource(fs, cloud);
|
|
if (!source || !source->isValid())
|
|
{
|
|
assert(false);
|
|
errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(fs.name);
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < sampleCount; ++i)
|
|
{
|
|
int pointIndex = (trainSubset ? static_cast<int>(trainSubset->getPointGlobalIndex(i)) : i);
|
|
double value = source->pointValue(pointIndex);
|
|
training_data.at<float>(i, fIndex) = static_cast<float>(value);
|
|
}
|
|
}
|
|
|
|
QScopedPointer<QProgressDialog> pDlg;
|
|
if (parentWidget)
|
|
{
|
|
pDlg.reset(new QProgressDialog(parentWidget));
|
|
pDlg->setRange(0, 0); //infinite loop
|
|
pDlg->setLabelText("Training classifier");
|
|
pDlg->show();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
m_rtrees = cv::ml::RTrees::create();
|
|
m_rtrees->setMaxDepth(params.maxDepth);
|
|
m_rtrees->setMinSampleCount(params.minSampleCount);
|
|
m_rtrees->setRegressionAccuracy(0);
|
|
// If true then surrogate splits will be built. These splits allow to work with missing data and compute variable importance correctly. Default value is false.
|
|
m_rtrees->setUseSurrogates(false);
|
|
m_rtrees->setPriors(cv::Mat());
|
|
//m_rtrees->setMaxCategories(params.maxCategories); //not important?
|
|
m_rtrees->setCalculateVarImportance(true);
|
|
m_rtrees->setActiveVarCount(params.activeVarCount);
|
|
cv::TermCriteria terminationCriteria(cv::TermCriteria::MAX_ITER, params.maxTreeCount, std::numeric_limits<double>::epsilon());
|
|
m_rtrees->setTermCriteria(terminationCriteria);
|
|
|
|
QFuture<bool> future = QtConcurrent::run([&]()
|
|
{
|
|
// Code in this block will run in another thread
|
|
try
|
|
{
|
|
cv::Mat sampleIndexes = cv::Mat::zeros(1, training_data.rows, CV_8U);
|
|
// cv::Mat trainSamples = sampleIndexes.colRange(0, sampleCount);
|
|
// trainSamples.setTo(cv::Scalar::all(1));
|
|
|
|
cv::Mat varTypes(training_data.cols + 1, 1, CV_8U);
|
|
varTypes.setTo(cv::Scalar::all(cv::ml::VAR_ORDERED));
|
|
varTypes.at<uchar>(training_data.cols) = cv::ml::VAR_CATEGORICAL;
|
|
|
|
cv::Ptr<cv::ml::TrainData> trainData = cv::ml::TrainData::create(training_data, cv::ml::ROW_SAMPLE, train_labels, /* samples layout responses */
|
|
cv::noArray(), sampleIndexes, /* varIdx sampleIdx */
|
|
cv::noArray(), varTypes); // sampleWeights varType
|
|
|
|
bool success = m_rtrees->train(trainData);
|
|
if (!success || !m_rtrees->isClassifier())
|
|
{
|
|
errorMessage = "Training failed";
|
|
return false;
|
|
}
|
|
}
|
|
catch (const cv::Exception& cvex)
|
|
{
|
|
m_rtrees.release();
|
|
errorMessage = cvex.msg.c_str();
|
|
return false;
|
|
}
|
|
catch (const std::exception& stdex)
|
|
{
|
|
errorMessage = stdex.what();
|
|
return false;
|
|
}
|
|
catch (...)
|
|
{
|
|
errorMessage = QObject::tr("Unknown error");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
while (!future.isFinished())
|
|
{
|
|
#if defined(CC_WINDOWS)
|
|
::Sleep(500);
|
|
#else
|
|
usleep(500 * 1000);
|
|
#endif
|
|
if (pDlg)
|
|
{
|
|
if (pDlg->wasCanceled())
|
|
{
|
|
// 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);
|
|
}
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
if (pDlg)
|
|
{
|
|
pDlg->close();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
if (future.isCanceled() || !future.result() || !m_rtrees->isTrained())
|
|
{
|
|
errorMessage = QObject::tr("Training failed for an unknown reason...");
|
|
m_rtrees.release();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Classifier::toFile(QString filename, QWidget* parentWidget/*=nullptr*/) const
|
|
{
|
|
if (!m_rtrees)
|
|
{
|
|
ccLog::Warning(QObject::tr("Classifier hasn't been trained, can't save it"));
|
|
return false;
|
|
}
|
|
|
|
//save the classifier
|
|
QProgressDialog pDlg(parentWidget);
|
|
pDlg.setRange(0, 0); //infinite loop
|
|
pDlg.setLabelText(QObject::tr("Saving classifier"));
|
|
pDlg.show();
|
|
QCoreApplication::processEvents();
|
|
|
|
cv::String cvFilename = filename.toStdString();
|
|
m_rtrees->save(cvFilename);
|
|
|
|
pDlg.close();
|
|
QCoreApplication::processEvents();
|
|
|
|
ccLog::Print("Classifier file saved to: " + QString::fromStdString(cvFilename));
|
|
return true;
|
|
}
|
|
|
|
bool Classifier::fromFile(QString filename, QWidget* parentWidget/*=nullptr*/)
|
|
{
|
|
//load the classifier
|
|
QScopedPointer<QProgressDialog> pDlg;
|
|
if (parentWidget)
|
|
{
|
|
pDlg.reset(new QProgressDialog(parentWidget));
|
|
pDlg->setRange(0, 0); //infinite loop
|
|
pDlg->setLabelText(QObject::tr("Loading classifier"));
|
|
pDlg->show();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
try
|
|
{
|
|
m_rtrees = cv::ml::RTrees::load(filename.toStdString());
|
|
}
|
|
catch (const cv::Exception& cvex)
|
|
{
|
|
ccLog::Warning(cvex.msg.c_str());
|
|
ccLog::Error("Failed to load file: " + filename);
|
|
return false;
|
|
}
|
|
|
|
if (pDlg)
|
|
{
|
|
pDlg->close();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
if (m_rtrees->empty() || !m_rtrees->isClassifier())
|
|
{
|
|
ccLog::Error(QObject::tr("Loaded classifier is invalid"));
|
|
return false;
|
|
}
|
|
else if (!m_rtrees->isTrained())
|
|
{
|
|
ccLog::Warning(QObject::tr("Loaded classifier doesn't seem to be trained"));
|
|
}
|
|
|
|
return true;
|
|
}
|