Files
q3DMASC/q3DMASCClassifier.cpp
T

712 lines
20 KiB
C++
Raw Normal View History

2018-10-26 10:41:22 +02:00
//##########################################################################
//# #
//# 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"
2019-04-05 11:15:57 +02:00
#include "q3DMASCTools.h"
2018-10-26 10:41:22 +02:00
//qCC_db
#include <ccPointCloud.h>
2018-11-04 23:31:48 +01:00
#include <ccScalarField.h>
#include <ccProgressDialog.h>
2018-10-26 10:41:22 +02:00
#include <ccLog.h>
//qPDALIO
2020-06-04 00:32:27 +02:00
#include "../../core/IO/qPDALIO/include/LASFields.h"
2018-10-26 10:41:22 +02:00
//qCC_plugins
#include <ccMainAppInterface.h>
2018-10-26 10:41:22 +02:00
//Qt
#include <QCoreApplication>
#include <QProgressDialog>
2018-11-04 11:56:13 +01:00
#include <QtConcurrent>
2018-10-26 10:41:22 +02:00
2023-01-18 23:57:59 +01:00
#include "qTrain3DMASCDialog.h"
2022-12-14 11:37:29 +01:00
#include "confusionmatrix.h"
2018-10-26 10:41:22 +02:00
using namespace masc;
Classifier::Classifier()
{
}
bool Classifier::isValid() const
{
return (m_rtrees && m_rtrees->isClassifier() && m_rtrees->isTrained());
2018-10-26 10:41:22 +02:00
}
2019-03-26 14:28:27 +01:00
static IScalarFieldWrapper::Shared GetSource(const Feature::Source& fs, const ccPointCloud* cloud)
2018-10-26 12:02:23 +02:00
{
IScalarFieldWrapper::Shared source(nullptr);
2018-10-26 10:41:22 +02:00
2019-03-26 14:28:27 +01:00
switch (fs.type)
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
case Feature::Source::ScalarField:
2018-10-26 12:02:23 +02:00
{
2019-03-27 15:03:58 +01:00
assert(!fs.name.isEmpty());
2019-03-26 14:28:27 +01:00
int sfIdx = cloud->getScalarFieldIndexByName(qPrintable(fs.name));
2018-10-26 12:02:23 +02:00
if (sfIdx >= 0)
{
source.reset(new ScalarFieldWrapper(cloud->getScalarField(sfIdx)));
}
else
{
2019-03-26 14:28:27 +01:00
ccLog::Warning(QObject::tr("Internal error: unknwon scalar field '%1'").arg(fs.name));
return IScalarFieldWrapper::Shared(nullptr);
2018-10-26 12:02:23 +02:00
}
}
break;
2019-03-26 14:28:27 +01:00
case Feature::Source::DimX:
2018-10-26 12:02:23 +02:00
source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimX));
break;
2019-03-26 14:28:27 +01:00
case Feature::Source::DimY:
2018-10-26 12:02:23 +02:00
source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimY));
break;
2019-03-26 14:28:27 +01:00
case Feature::Source::DimZ:
2018-10-26 12:02:23 +02:00
source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimZ));
break;
2019-03-26 14:28:27 +01:00
case Feature::Source::Red:
2018-10-26 12:02:23 +02:00
source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Red));
break;
2019-03-26 14:28:27 +01:00
case Feature::Source::Green:
2018-10-26 12:02:23 +02:00
source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Green));
break;
2019-03-26 14:28:27 +01:00
case Feature::Source::Blue:
2018-10-26 12:02:23 +02:00
source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Blue));
break;
}
return source;
}
2019-03-26 14:28:27 +01:00
bool Classifier::classify( const Feature::Source::Set& featureSources,
ccPointCloud* cloud,
QString& errorMessage,
QWidget* parentWidget/*=nullptr*/
)
2018-11-04 23:31:48 +01:00
{
if (!cloud)
{
assert(false);
errorMessage = QObject::tr("Invalid input");
return false;
}
if (!isValid())
2018-11-04 23:31:48 +01:00
{
errorMessage = QObject::tr("Invalid classifier");
2018-11-04 23:31:48 +01:00
return false;
}
2019-03-26 14:28:27 +01:00
if (featureSources.empty())
2018-11-04 23:31:48 +01:00
{
2019-03-26 14:28:27 +01:00
errorMessage = QObject::tr("Training method called without any feature (source)?!");
2018-11-04 23:31:48 +01:00
return false;
}
//look for the classification field
2020-06-04 00:32:27 +02:00
CCCoreLib::ScalarField* classificationSF = Tools::GetClassificationSF(cloud);
2022-12-08 09:19:52 +01:00
// add a ccConfidence value if needed
2022-12-14 11:37:29 +01:00
int cvConfidenceIdx = cloud->getScalarFieldIndexByName("Classification_confidence");
2022-12-09 17:45:08 +01:00
if (cvConfidenceIdx > 0) // if the scalar field exists, delete it
cloud->deleteScalarField(cvConfidenceIdx);
else
2022-12-14 11:37:29 +01:00
cvConfidenceIdx = cloud->addScalarField("Classification_confidence");
2022-12-08 09:19:52 +01:00
CCCoreLib::ScalarField* cvConfidenceSF = cloud->getScalarField(cvConfidenceIdx);
2022-12-14 11:37:29 +01:00
ccScalarField* classifSFBackup = nullptr;
if (classificationSF)
{
//save previous classification field (if any)
int sfIdx = cloud->getScalarFieldIndexByName("Classification_prev");
2022-12-09 17:45:08 +01:00
if (sfIdx > 0)
cloud->deleteScalarField(sfIdx);
try
{
2022-12-14 11:37:29 +01:00
classifSFBackup = new ccScalarField(*static_cast<ccScalarField*>(classificationSF));
2019-03-27 22:12:51 +01:00
classifSFBackup->setName("Classification_prev");
cloud->addScalarField(classifSFBackup);
}
catch (const std::bad_alloc)
{
ccLog::Warning("Not enough memory to backup the previous classification SF!");
}
}
else
2018-11-04 23:31:48 +01:00
{
//create the classification SF
2018-11-04 23:31:48 +01:00
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);
2018-11-04 23:31:48 +01:00
classificationSF = _classificationSF;
}
assert(classificationSF);
classificationSF->fill(0); //0 = no classification?
int sampleCount = static_cast<int>(cloud->size());
2019-03-26 14:28:27 +01:00
int attributesPerSample = static_cast<int>(featureSources.size());
2018-11-04 23:31:48 +01:00
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;
2018-11-04 23:31:48 +01:00
{
wrappers.reserve(attributesPerSample);
for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
{
2019-03-26 14:28:27 +01:00
const Feature::Source& fs = featureSources[fIndex];
2018-11-04 23:31:48 +01:00
2019-03-26 14:28:27 +01:00
IScalarFieldWrapper::Shared source = GetSource(fs, cloud);
2018-11-04 23:31:48 +01:00
if (!source || !source->isValid())
{
assert(false);
2019-03-26 14:28:27 +01:00
errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(fs.name);
2018-11-04 23:31:48 +01:00
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();
}
2020-06-04 00:32:27 +02:00
CCCoreLib::NormalizedProgress nProgress(pDlg.data(), cloud->size());
2018-11-04 23:31:48 +01:00
bool success = true;
2022-12-14 11:37:29 +01:00
int numberOfTrees = m_rtrees->getRoots().size();
2019-03-29 10:10:42 +01:00
#ifndef _DEBUG
#if defined(_OPENMP)
#pragma omp parallel for
#endif
#endif
for (int i = 0; i < static_cast<int>(cloud->size()); ++i)
2018-11-04 23:31:48 +01:00
{
2019-03-29 10:10:42 +01:00
//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;
2019-03-29 10:10:42 +01:00
}
2018-11-04 23:31:48 +01:00
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));
2022-12-08 09:19:52 +01:00
// 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++)
2022-12-14 11:37:29 +01:00
if (predictedClass == result.at<int>(0, col))
2022-12-08 09:19:52 +01:00
{
classIndex = col;
break;
}
if (classIndex != -1)
2022-12-14 11:37:29 +01:00
{
float nbVotes = result.at<int>(1, classIndex);
cvConfidenceSF->setValue(i, static_cast<ScalarType>(nbVotes / numberOfTrees));
}
2022-12-08 09:19:52 +01:00
else
cvConfidenceSF->setValue(i, CCCoreLib::NAN_VALUE);
2018-11-04 23:31:48 +01:00
if (pDlg && !nProgress.oneStep())
{
//process cancelled by the user
success = false;
break;
2018-11-04 23:31:48 +01:00
}
}
classificationSF->computeMinAndMax();
2022-12-14 11:37:29 +01:00
cvConfidenceSF->computeMinAndMax();
2018-11-04 23:31:48 +01:00
2019-03-25 18:45:28 +01:00
//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);
2022-12-14 11:37:29 +01:00
return success;
2018-11-04 23:31:48 +01:00
}
2019-03-26 14:28:27 +01:00
bool Classifier::evaluate(const Feature::Source::Set& featureSources,
ccPointCloud* testCloud,
AccuracyMetrics& metrics,
QString& errorMessage,
2023-01-18 23:57:59 +01:00
Train3DMASCDialog& train3DMASCDialog,
2020-06-04 00:32:27 +02:00
CCCoreLib::ReferenceCloud* testSubset/*=nullptr=*/,
2019-03-26 14:28:27 +01:00
QString outputSFName/*=QString()*/,
QWidget* parentWidget/*=nullptr*/)
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
if (!testCloud)
{
//invalid input
assert(false);
errorMessage = QObject::tr("Invalid input cloud");
return false;
}
2018-10-26 12:02:23 +02:00
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;
}
2019-03-26 14:28:27 +01:00
if (featureSources.empty())
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
errorMessage = QObject::tr("Training method called without any feature (source)?!");
2018-10-26 12:02:23 +02:00
return false;
}
2019-03-26 14:28:27 +01:00
if (testSubset && testSubset->getAssociatedCloud() != testCloud)
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
errorMessage = QObject::tr("Invalid test subset (associated point cloud is different)");
2018-10-26 10:41:22 +02:00
return false;
}
//look for the classification field
2020-06-04 00:32:27 +02:00
CCCoreLib::ScalarField* classifSF = Tools::GetClassificationSF(testCloud);
2019-03-26 14:28:27 +01:00
if (!classifSF || classifSF->size() < testCloud->size())
2018-10-26 10:41:22 +02:00
{
assert(false);
2019-03-26 14:28:27 +01:00
errorMessage = QObject::tr("Missing/invalid 'Classification' field on input cloud");
2018-10-26 10:41:22 +02:00
return false;
}
2020-06-04 00:32:27 +02:00
CCCoreLib::ScalarField* outputSF = nullptr;
2019-03-26 14:28:27 +01:00
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);
}
2020-06-04 00:32:27 +02:00
outputSF->fill(CCCoreLib::NAN_VALUE);
2019-03-26 14:28:27 +01:00
outputSF->computeMinAndMax();
}
unsigned testSampleCount = (testSubset ? testSubset->size() : testCloud->size());
int attributesPerSample = static_cast<int>(featureSources.size());
2018-10-26 12:02:23 +02:00
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
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
test_data.create(static_cast<int>(testSampleCount), attributesPerSample, CV_32FC1);
2018-10-26 12:02:23 +02:00
}
catch (const cv::Exception& cvex)
{
errorMessage = cvex.msg.c_str();
2018-10-26 10:41:22 +02:00
return false;
}
2018-11-04 23:31:48 +01:00
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();
}
2020-06-04 00:32:27 +02:00
CCCoreLib::NormalizedProgress nProgress(pDlg.data(), testSampleCount);
2018-11-04 23:31:48 +01:00
2018-10-26 12:02:23 +02:00
//fill the data matrix
for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
const Feature::Source& fs = featureSources[fIndex];
IScalarFieldWrapper::Shared source = GetSource(fs, testCloud);
2018-10-26 12:02:23 +02:00
if (!source || !source->isValid())
2018-10-26 10:41:22 +02:00
{
2018-10-26 12:02:23 +02:00
assert(false);
2019-03-26 14:28:27 +01:00
errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(fs.name);
2018-10-26 10:41:22 +02:00
return false;
}
2019-03-26 14:28:27 +01:00
for (unsigned i = 0; i < testSampleCount; ++i)
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
unsigned pointIndex = (testSubset ? testSubset->getPointGlobalIndex(i) : i);
2018-10-26 12:02:23 +02:00
double value = source->pointValue(pointIndex);
test_data.at<float>(i, fIndex) = static_cast<float>(value);
}
}
2023-01-18 23:57:59 +01:00
2018-11-04 23:31:48 +01:00
//estimate the efficiency of the classifier
2023-01-18 23:57:59 +01:00
std::vector<ScalarType> actualClass(testSampleCount);
std::vector<ScalarType> predictectedClass(testSampleCount);
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
metrics.sampleCount = testSampleCount;
2018-10-26 12:02:23 +02:00
metrics.goodGuess = 0;
2019-03-26 14:28:27 +01:00
for (unsigned i = 0; i < testSampleCount; ++i)
2018-10-26 10:41:22 +02:00
{
2019-03-26 14:28:27 +01:00
unsigned pointIndex = (testSubset ? testSubset->getPointGlobalIndex(i) : i);
2018-10-26 12:02:23 +02:00
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);
2023-01-18 23:57:59 +01:00
actualClass.at(i) = iClass;
predictectedClass.at(i) = iPredictedClass;
2019-03-26 14:28:27 +01:00
if (iPredictedClass == iClass)
2018-10-26 10:41:22 +02:00
{
2018-10-26 12:02:23 +02:00
++metrics.goodGuess;
2018-10-26 10:41:22 +02:00
}
2019-03-26 14:28:27 +01:00
if (outputSF)
{
outputSF->setValue(pointIndex, static_cast<ScalarType>(iPredictedClass));
}
2018-11-04 23:31:48 +01:00
if (pDlg && !nProgress.oneStep())
{
//process cancelled by the user
return false;
}
2018-10-26 10:41:22 +02:00
}
2018-10-26 12:02:23 +02:00
2019-03-26 14:28:27 +01:00
if (outputSF)
outputSF->computeMinAndMax();
2018-10-26 12:02:23 +02:00
metrics.ratio = static_cast<float>(metrics.goodGuess) / metrics.sampleCount;
}
2023-01-18 23:57:59 +01:00
std::unique_ptr<ConfusionMatrix> confusionMatrix(new ConfusionMatrix(actualClass, predictectedClass));
2023-01-20 17:49:29 +01:00
train3DMASCDialog.addConfusionMatrix(confusionMatrix);
2023-01-17 16:43:57 +01:00
2018-10-26 12:02:23 +02:00
return true;
}
2018-11-04 11:56:13 +01:00
bool Classifier::train( const ccPointCloud* cloud,
const RandomTreesParams& params,
2019-03-26 14:28:27 +01:00
const Feature::Source::Set& featureSources,
2018-11-04 11:56:13 +01:00
QString& errorMessage,
2020-06-04 00:32:27 +02:00
CCCoreLib::ReferenceCloud* trainSubset/*=nullptr*/,
ccMainAppInterface* app/*=nullptr*/,
2018-11-04 11:56:13 +01:00
QWidget* parentWidget/*=nullptr*/)
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
if (featureSources.empty())
2018-10-26 12:02:23 +02:00
{
2019-03-26 14:28:27 +01:00
errorMessage = QObject::tr("Training method called without any feature (source)?!");
2018-10-26 12:02:23 +02:00
return false;
2018-10-26 10:41:22 +02:00
}
2018-11-04 11:21:50 +01:00
if (!cloud)
2018-10-26 12:02:23 +02:00
{
2018-11-04 11:21:50 +01:00
errorMessage = QObject::tr("Invalid input cloud");
2018-10-26 12:02:23 +02:00
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
2020-06-04 00:32:27 +02:00
CCCoreLib::ScalarField* classifSF = Tools::GetClassificationSF(cloud);
2018-10-26 12:02:23 +02:00
if (!classifSF || classifSF->size() < cloud->size())
{
assert(false);
2019-03-25 18:45:28 +01:00
errorMessage = QObject::tr("Missing/invalid 'Classification' field on input cloud");
2018-10-26 12:02:23 +02:00
return false;
}
int sampleCount = static_cast<int>(trainSubset ? trainSubset->size() : cloud->size());
2019-03-26 14:28:27 +01:00
int attributesPerSample = static_cast<int>(featureSources.size());
2018-10-26 12:02:23 +02:00
if (app)
{
app->dispToConsole(QString("[3DMASC] Training data: %1 samples with %2 feature(s)").arg(sampleCount).arg(attributesPerSample));
}
2018-10-26 10:41:22 +02:00
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
{
2018-10-26 12:02:23 +02:00
for (int i = 0; i < sampleCount; ++i)
2018-10-26 10:41:22 +02:00
{
2018-10-26 12:02:23 +02:00
int pointIndex = (trainSubset ? static_cast<int>(trainSubset->getPointGlobalIndex(i)) : i);
ScalarType pointClass = classifSF->getValue(pointIndex);
2018-10-26 10:41:22 +02:00
int iClass = static_cast<int>(pointClass);
2018-10-26 12:02:23 +02:00
//if (iClass < 0 || iClass > 255)
//{
// errorMessage = QObject::tr("Classification values out of range (0-255)");
// return false;
//}
2018-10-26 10:41:22 +02:00
2018-10-26 12:02:23 +02:00
train_labels.at<float>(i) = static_cast<unsigned char>(iClass);
2018-10-26 10:41:22 +02:00
}
}
//fill the training data matrix
for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex)
{
2019-03-26 14:28:27 +01:00
const Feature::Source& fs = featureSources[fIndex];
2018-10-26 10:41:22 +02:00
2019-03-26 14:28:27 +01:00
IScalarFieldWrapper::Shared source = GetSource(fs, cloud);
2018-10-26 10:41:22 +02:00
if (!source || !source->isValid())
{
assert(false);
2019-03-26 14:28:27 +01:00
errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(fs.name);
2018-10-26 10:41:22 +02:00
return false;
}
2018-10-26 12:02:23 +02:00
for (int i = 0; i < sampleCount; ++i)
2018-10-26 10:41:22 +02:00
{
2018-10-26 12:02:23 +02:00
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);
2018-10-26 10:41:22 +02:00
}
}
2018-11-04 11:56:13 +01:00
QScopedPointer<QProgressDialog> pDlg;
if (parentWidget)
{
pDlg.reset(new QProgressDialog(parentWidget));
pDlg->setRange(0, 0); //infinite loop
pDlg->setLabelText("Training classifier");
pDlg->show();
2018-11-04 23:31:48 +01:00
QCoreApplication::processEvents();
2018-11-04 11:56:13 +01:00
}
2018-10-26 10:41:22 +02:00
m_rtrees = cv::ml::RTrees::create();
2018-10-26 12:02:23 +02:00
m_rtrees->setMaxDepth(params.maxDepth);
m_rtrees->setMinSampleCount(params.minSampleCount);
m_rtrees->setRegressionAccuracy(0);
2022-02-24 16:44:59 +01:00
// 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?
2019-01-20 13:57:27 +01:00
m_rtrees->setCalculateVarImportance(true);
2018-10-26 12:02:23 +02:00
m_rtrees->setActiveVarCount(params.activeVarCount);
2022-10-20 08:52:55 +02:00
cv::TermCriteria terminationCriteria(cv::TermCriteria::MAX_ITER, params.maxTreeCount, std::numeric_limits<double>::epsilon());
2018-10-26 10:41:22 +02:00
m_rtrees->setTermCriteria(terminationCriteria);
2018-11-04 11:56:13 +01:00
QFuture<bool> future = QtConcurrent::run([&]()
2018-10-26 10:41:22 +02:00
{
2018-11-04 11:56:13 +01:00
// Code in this block will run in another thread
try
{
cv::Mat sampleIndexes = cv::Mat::zeros(1, training_data.rows, CV_8U);
2022-12-08 09:19:52 +01:00
// 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;
2022-12-08 09:19:52 +01:00
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;
}
2018-11-04 11:56:13 +01:00
}
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;
}
2022-12-08 09:19:52 +01:00
2018-11-04 11:56:13 +01:00
return true;
});
while (!future.isFinished())
2018-10-26 10:41:22 +02:00
{
2018-11-04 11:56:13 +01:00
#if defined(CC_WINDOWS)
::Sleep(500);
#else
usleep(500 * 1000);
#endif
if (pDlg)
{
if (pDlg->wasCanceled())
{
future.cancel();
break;
}
pDlg->setValue(pDlg->value() + 1);
}
QCoreApplication::processEvents();
2018-10-26 10:41:22 +02:00
}
2018-11-04 11:56:13 +01:00
if (pDlg)
2018-10-26 10:41:22 +02:00
{
2018-11-04 11:56:13 +01:00
pDlg->close();
QCoreApplication::processEvents();
2018-10-26 10:41:22 +02:00
}
2018-11-04 11:56:13 +01:00
if (future.isCanceled() || !future.result() || !m_rtrees->isTrained())
2018-10-26 10:41:22 +02:00
{
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();
2018-12-01 14:39:11 +01:00
cv::String cvFilename = filename.toStdString();
m_rtrees->save(cvFilename);
2018-10-26 10:41:22 +02:00
pDlg.close();
QCoreApplication::processEvents();
2018-12-01 14:39:11 +01:00
ccLog::Print("Classifier file saved to: " + QString::fromStdString(cvFilename));
2018-10-26 10:41:22 +02:00
return true;
}
bool Classifier::fromFile(QString filename, QWidget* parentWidget/*=nullptr*/)
{
//load the classifier
2018-11-05 00:11:27 +01:00
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;
}
2018-10-26 10:41:22 +02:00
2018-11-05 00:11:27 +01:00
if (pDlg)
{
pDlg->close();
QCoreApplication::processEvents();
}
2018-10-26 10:41:22 +02:00
if (m_rtrees->empty() || !m_rtrees->isClassifier())
{
ccLog::Error(QObject::tr("Loaded classifier is invalid"));
return false;
}
else if (!m_rtrees->isTrained())
2018-10-26 10:41:22 +02:00
{
ccLog::Warning(QObject::tr("Loaded classifier doesn't seem to be trained"));
}
return true;
}