22 #include "../../../core/IO/qPDALIO/include/LASFields.h"
28 #include <QCoreApplication>
29 #include <QMessageBox>
30 #include <QProgressDialog>
31 #include <QtConcurrent>
40 #if defined(CV_MAC_OS) || defined(CV_LINUX)
58 assert(!fs.
name.isEmpty());
65 QObject::tr(
"Internal error: unknown scalar field '%1'")
103 QString& errorMessage,
104 QWidget* parentWidget ,
109 errorMessage = QObject::tr(
"Invalid input");
114 errorMessage = QObject::tr(
"Invalid classifier");
118 if (featureSources.empty()) {
119 errorMessage = QObject::tr(
120 "Training method called without any feature (source)?!");
125 int cvConfidenceIdx =
127 if (cvConfidenceIdx >= 0)
129 cvConfidenceIdx = cloud->
addScalarField(
"Classification_confidence");
138 if (classificationSF)
142 "Classification SF found: copy it in Classification_backup, a "
143 "confusion matrix will be generated");
149 "Classification_backup");
150 classifSFBackup =
static_cast<ccScalarField*
>(classificationSF);
158 errorMessage = QObject::tr(
"Not enough memory");
162 classificationSF = _classificationSF;
164 assert(classificationSF);
165 classificationSF->
fill(0);
167 int sampleCount =
static_cast<int>(cloud->
size());
168 int attributesPerSample =
static_cast<int>(featureSources.size());
171 QObject::tr(
"[3DMASC] Classifying %1 points with %2 feature(s)")
173 .arg(attributesPerSample));
176 std::vector<IScalarFieldWrapper::Shared> wrappers;
178 wrappers.reserve(attributesPerSample);
179 for (
int fIndex = 0; fIndex < attributesPerSample; ++fIndex) {
183 if (!source || !source->isValid()) {
186 QObject::tr(
"Internal error: invalid source '%1'")
191 wrappers.push_back(source);
195 QScopedPointer<ecvProgressDialog> pDlg;
198 pDlg->setLabelText(QString(
"Classify (%1 points)").arg(sampleCount));
200 QCoreApplication::processEvents();
205 int numberOfTrees =
static_cast<int>(
m_rtrees->getRoots().size());
206 bool cancelled =
false;
210 #pragma omp parallel for num_threads(omp_get_max_threads() - 2)
213 for (
int i = 0; i < static_cast<int>(cloud->
size()); ++i) {
218 test_data.create(1, attributesPerSample, CV_32FC1);
219 }
catch (
const cv::Exception& cvex) {
220 errorMessage = cvex.msg.c_str();
226 for (
int fIndex = 0; fIndex < attributesPerSample; ++fIndex) {
227 double value = wrappers[fIndex]->pointValue(i);
228 test_data.at<
float>(0, fIndex) =
static_cast<float>(value);
231 float predictedClass =
232 m_rtrees->predict(test_data.row(0), cv::noArray(),
233 cv::ml::DTrees::PREDICT_MAX_VOTE);
234 classificationSF->
setValue(i,
static_cast<int>(predictedClass));
238 cv::ml::DTrees::PREDICT_MAX_VOTE);
240 for (
int col = 0; col <
result.cols;
242 if (predictedClass ==
result.at<
int>(0, col)) {
246 if (classIndex != -1) {
247 float nbVotes =
result.at<
int>(
251 static_cast<ScalarType
>(
281 QCoreApplication::processEvents();
284 if (classifSFBackup !=
nullptr) {
297 QString& errorMessage,
300 QString outputSFName ,
301 QWidget* parentWidget ,
306 errorMessage = QObject::tr(
"Invalid input cloud");
310 metrics.
ratio = 0.0f;
313 errorMessage = QObject::tr(
"Classifier hasn't been trained yet");
317 if (featureSources.empty()) {
318 errorMessage = QObject::tr(
319 "Training method called without any feature (source)?!");
323 errorMessage = QObject::tr(
324 "Invalid test subset (associated point cloud is different)");
330 if (!classifSF || classifSF->size() < testCloud->
size()) {
332 errorMessage = QObject::tr(
333 "Missing/invalid 'Classification' field on input cloud");
340 if (!outputSFName.isEmpty()) {
346 CVLog::Print(
"add " + outputSFName +
" to the TEST cloud");
355 "Classification_confidence");
356 if (cvConfidenceIdx >= 0)
359 CVLog::Print(
"add Classification_confidence to the TEST cloud");
366 unsigned testSampleCount =
367 (testSubset ? testSubset->
size() : testCloud->
size());
368 int attributesPerSample =
static_cast<int>(featureSources.size());
371 QObject::tr(
"[3DMASC] Testing data: %1 samples with %2 feature(s)")
372 .arg(testSampleCount)
373 .arg(attributesPerSample));
378 test_data.create(
static_cast<int>(testSampleCount), attributesPerSample,
380 }
catch (
const cv::Exception& cvex) {
381 errorMessage = cvex.msg.c_str();
385 QScopedPointer<ecvProgressDialog> pDlg;
388 pDlg->setLabelText(QString(
"Evaluating the classifier on %1 points")
389 .arg(testSampleCount));
391 QCoreApplication::processEvents();
396 for (
int fIndex = 0; fIndex < attributesPerSample; ++fIndex) {
399 if (!source || !source->isValid()) {
401 errorMessage = QObject::tr(
"Internal error: invalid source '%1'")
406 for (
unsigned i = 0; i < testSampleCount; ++i) {
407 unsigned pointIndex =
409 double value = source->pointValue(pointIndex);
410 test_data.at<
float>(i, fIndex) =
static_cast<float>(value);
414 int numberOfTrees =
static_cast<int>(
m_rtrees->getRoots().size());
417 std::vector<ScalarType> actualClass(testSampleCount);
418 std::vector<ScalarType> predictectedClass(testSampleCount);
423 for (
unsigned i = 0; i < testSampleCount; ++i) {
424 unsigned pointIndex =
426 ScalarType pointClass = classifSF->
getValue(pointIndex);
427 int iClass =
static_cast<int>(pointClass);
434 float fPredictedClass =
435 m_rtrees->predict(test_data.row(i), cv::noArray(),
436 cv::ml::DTrees::PREDICT_MAX_VOTE);
437 int iPredictedClass =
static_cast<int>(fPredictedClass);
438 actualClass.at(i) = iClass;
439 predictectedClass.at(i) = iPredictedClass;
440 if (iPredictedClass == iClass) {
445 static_cast<ScalarType
>(iPredictedClass));
446 if (cvConfidenceSF) {
450 cv::ml::DTrees::PREDICT_MAX_VOTE);
452 for (
int col = 0; col <
result.cols;
454 if (iPredictedClass ==
result.at<
int>(0, col)) {
458 if (classIndex != -1) {
459 float nbVotes =
result.at<
int>(
462 i,
static_cast<ScalarType
>(
488 confusionMatrix->show();
503 QCoreApplication::processEvents();
512 QString& errorMessage,
515 QWidget* parentWidget ) {
516 if (featureSources.empty()) {
517 errorMessage = QObject::tr(
518 "Training method called without any feature (source)?!");
522 errorMessage = QObject::tr(
"Invalid input cloud");
527 errorMessage = QObject::tr(
528 "Invalid train subset (associated point cloud is different)");
534 if (!classifSF || classifSF->size() < cloud->
size()) {
536 errorMessage = QObject::tr(
537 "Missing/invalid 'Classification' field on input cloud");
542 static_cast<int>(trainSubset ? trainSubset->
size() : cloud->
size());
543 int attributesPerSample =
static_cast<int>(featureSources.size());
547 QString(
"[3DMASC] Training data: %1 samples with %2 feature(s)")
549 .arg(attributesPerSample));
552 cv::Mat training_data, train_labels;
554 training_data.create(sampleCount, attributesPerSample, CV_32FC1);
555 train_labels.create(sampleCount, 1, CV_32FC1);
556 }
catch (
const cv::Exception& cvex) {
557 errorMessage = cvex.msg.c_str();
563 for (
int i = 0; i < sampleCount; ++i) {
565 (trainSubset ?
static_cast<int>(
568 ScalarType pointClass = classifSF->
getValue(pointIndex);
569 int iClass =
static_cast<int>(pointClass);
576 train_labels.at<
float>(i) =
static_cast<unsigned char>(iClass);
581 for (
int fIndex = 0; fIndex < attributesPerSample; ++fIndex) {
585 if (!source || !source->isValid()) {
587 errorMessage = QObject::tr(
"Internal error: invalid source '%1'")
592 for (
int i = 0; i < sampleCount; ++i) {
594 (trainSubset ?
static_cast<int>(
597 double value = source->pointValue(pointIndex);
598 training_data.at<
float>(i, fIndex) =
static_cast<float>(value);
602 QScopedPointer<QProgressDialog> pDlg;
604 pDlg.reset(
new QProgressDialog(parentWidget));
605 pDlg->setRange(0, 0);
606 pDlg->setLabelText(
"Training classifier");
608 QCoreApplication::processEvents();
611 m_rtrees = cv::ml::RTrees::create();
621 m_rtrees->setCalculateVarImportance(
true);
623 cv::TermCriteria terminationCriteria(
624 cv::TermCriteria::MAX_ITER,
params.maxTreeCount,
625 std::numeric_limits<double>::epsilon());
626 m_rtrees->setTermCriteria(terminationCriteria);
628 QFuture<bool> future = QtConcurrent::run([&]() {
631 cv::Mat sampleIndexes =
632 cv::Mat::zeros(1, training_data.rows, CV_8U);
637 cv::Mat varTypes(training_data.cols + 1, 1, CV_8U);
638 varTypes.setTo(cv::Scalar::all(cv::ml::VAR_ORDERED));
639 varTypes.at<
uchar>(training_data.cols) = cv::ml::VAR_CATEGORICAL;
641 cv::Ptr<cv::ml::TrainData> trainData = cv::ml::TrainData::create(
642 training_data, cv::ml::ROW_SAMPLE,
644 cv::noArray(), sampleIndexes,
645 cv::noArray(), varTypes);
647 bool success =
m_rtrees->train(trainData);
648 if (!success || !
m_rtrees->isClassifier()) {
649 errorMessage =
"Training failed";
652 }
catch (
const cv::Exception& cvex) {
654 errorMessage = cvex.msg.c_str();
656 }
catch (
const std::exception& stdex) {
657 errorMessage = stdex.what();
660 errorMessage = QObject::tr(
"Unknown error");
667 while (!future.isFinished()) {
668 #if defined(CV_WINDOWS)
674 if (pDlg->wasCanceled()) {
678 "The training is still in progress, not possible to "
685 pDlg->setValue(pDlg->value() + 1);
687 QCoreApplication::processEvents();
692 QCoreApplication::processEvents();
695 if (future.isCanceled() || !future.result() || !
m_rtrees->isTrained()) {
696 errorMessage = QObject::tr(
"Training failed for an unknown reason...");
705 QWidget* parentWidget )
const {
708 QObject::tr(
"Classifier hasn't been trained, can't save it"));
713 QProgressDialog pDlg(parentWidget);
715 pDlg.setLabelText(QObject::tr(
"Saving classifier"));
717 QCoreApplication::processEvents();
719 cv::String cvFilename =
filename.toStdString();
723 QCoreApplication::processEvents();
726 QString::fromStdString(cvFilename));
731 QWidget* parentWidget ) {
733 QScopedPointer<QProgressDialog> pDlg;
735 pDlg.reset(
new QProgressDialog(parentWidget));
736 pDlg->setRange(0, 0);
737 pDlg->setLabelText(QObject::tr(
"Loading classifier"));
739 QCoreApplication::processEvents();
744 }
catch (
const cv::Exception& cvex) {
752 QCoreApplication::processEvents();
756 CVLog::Error(QObject::tr(
"Loaded classifier is invalid"));
758 }
else if (!
m_rtrees->isTrained()) {
760 QObject::tr(
"Loaded classifier doesn't seem to be trained"));
constexpr ScalarType NAN_VALUE
NaN as a ScalarType value.
const char LAS_FIELD_NAMES[][28]
cmdLineReadable * params[]
virtual void release()
Decrease counter and deletes object when 0.
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
static bool Print(const char *format,...)
Prints out a formatted message in console.
static bool Error(const char *format,...)
Display an error dialog with formatted message.
QSharedPointer< IScalarFieldWrapper > Shared
3DMASC plugin 'train' dialog
void addConfusionMatrixAndSaveTraces(ConfusionMatrix *ptr)
virtual void setRedraw(bool state)
Sets entity redraw mode.
virtual void showSF(bool state)
Sets active scalarfield visibility.
void setRedrawFlagRecursive(bool redraw=false)
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
void setCurrentDisplayedScalarField(int index)
Sets the currently displayed scalar field.
int addScalarField(const char *uniqueName) override
Creates a new scalar field and registers it.
void deleteScalarField(int index) override
Deletes a specific scalar field.
A scalar field associated to display-related parameters.
void computeMinAndMax() override
Determines the min and max values.
bool oneStep()
Increments total progress value of a single unit.
int getScalarFieldIndexByName(const char *name) const
Returns the index of a scalar field represented by its name.
ScalarField * getScalarField(int index) const
Returns a pointer to a specific scalar field.
unsigned size() const override
A very simple point cloud (no point duplication)
virtual GenericIndexedCloudPersist * getAssociatedCloud()
Returns the associated (source) cloud.
unsigned size() const override
Returns the number of points.
virtual unsigned getPointGlobalIndex(unsigned localIndex) const
A simple scalar field (to be associated to a point cloud)
void fill(ScalarType fillValue=0)
Fills the array with a particular value.
virtual void computeMinAndMax()
Determines the min and max values.
ScalarType & getValue(std::size_t index)
void setValue(std::size_t index, ScalarType value)
const char * getName() const
Returns scalar field name.
void setName(const char *name)
Sets scalar field name.
bool resizeSafe(std::size_t count, bool initNewElements=false, ScalarType valueForNewElements=0)
Resizes memory (no exception thrown)
Main application interface (for plugins)
virtual void dispToConsole(QString message, ConsoleMessageLevel level=STD_CONSOLE_MESSAGE)=0
Graphical progress indicator (thread-safe)
bool toFile(QString filename, QWidget *parentWidget=nullptr) const
Saves the classifier to file.
bool isValid() const
Returns whether the classifier is valid or not.
bool evaluate(const Feature::Source::Set &featureSources, ccPointCloud *testCloud, AccuracyMetrics &metrics, QString &errorMessage, Train3DMASCDialog &train3DMASCDialog, cloudViewer::ReferenceCloud *testSubset=nullptr, QString outputSFName=QString(), QWidget *parentWidget=nullptr, ecvMainAppInterface *app=nullptr)
Evaluates the classifier.
bool classify(const Feature::Source::Set &featureSources, ccPointCloud *cloud, QString &errorMessage, QWidget *parentWidget=nullptr, ecvMainAppInterface *app=nullptr)
Applies the classifier.
bool train(const ccPointCloud *cloud, const RandomTreesParams ¶ms, const Feature::Source::Set &featureSources, QString &errorMessage, cloudViewer::ReferenceCloud *trainSubset=nullptr, ecvMainAppInterface *app=nullptr, QWidget *parentWidget=nullptr)
Train the classifier.
bool fromFile(QString filename, QWidget *parentWidget=nullptr)
Loads the classifier from file.
cv::Ptr< cv::ml::RTrees > m_rtrees
Random trees (OpenCV)
Classifier()
Default constructor.
void Sleep(int milliseconds)
static IScalarFieldWrapper::Shared GetSource(const Feature::Source &fs, const ccPointCloud *cloud)
Classifier accuracy metrics.
Sources of values for this feature.
std::vector< Source > Set