//########################################################################## //# # //# 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" //qCC_db #include #include //qCC_io #include //Qt #include #include using namespace masc; Classifier::Classifier() { } bool Classifier::isValid() const { return (m_rtrees && m_rtrees->isTrained()); } bool Classifier::train(const TrainParameters& params, const Feature::Set& features, QString& errorMessage, QWidget* parentWidget/*=nullptr*/) { if (features.empty()) { errorMessage = QObject::tr("Training method called without any feature?!"); return false; } if (!features.front() || !features.front()->cloud) { errorMessage = QObject::tr("Invalid feature (no associated point cloud"); return false; } ccPointCloud* cloud = features.front()->cloud; //look for the classification field int classifSFIdx = cloud->getScalarFieldIndexByName(LAS_FIELD_NAMES[LAS_CLASSIFICATION]); //LAS_FIELD_NAMES[LAS_CLASSIFICATION] = "Classification" if (!classifSFIdx) { errorMessage = QObject::tr("Missing 'Classification' field on input cloud"); return false; } CCLib::ScalarField* classifSF = cloud->getScalarField(classifSFIdx); if (!classifSF || classifSF->size() < cloud->size()) { assert(false); errorMessage = QObject::tr("Invalid 'Classification' field on input cloud"); return false; } if (params.testDataRatio < 0 || params.testDataRatio > 0.99f) { errorMessage = QObject::tr("Invalid parameter (test data ratio)"); return false; } //std::vector features; //features.push_back(Feature::Shared(new PointFeature(cloud, PointFeature::Z, Feature::DimZ, "Z"))); //features.push_back(Feature::Shared(new PointFeature(cloud, PointFeature::Intensity, Feature::ScalarField, "Intensity"))); int totalSampleCount = static_cast(cloud->size()); int testSampleCount = static_cast(floor(totalSampleCount * params.testDataRatio)); int sampleCount = totalSampleCount - testSampleCount; int attributesPerSample = static_cast(features.size()); ccLog::Print(QString("[3DMASC] Training data: %1 samples with %2 feature(s) / %3 test samples").arg(sampleCount).arg(attributesPerSample).arg(testSampleCount)); //randomly choose the sample indexes std::vector isSample; if (testSampleCount > 0) { try { isSample.resize(totalSampleCount, true); } catch (const std::bad_alloc&) { errorMessage = QObject::tr("Not enough memory"); return false; } int randomCount = 0; int randIndex = 0; while (randomCount < testSampleCount) { randIndex = ((randIndex + std::rand()) % totalSampleCount); if (isSample[randIndex]) { isSample[randIndex] = false; ++randomCount; } } } //NUMBER_OF_TRAINING_SAMPLES = number of points //ATTRIBUTES_PER_SAMPLE = number of scalar fields cv::Mat training_data, train_labels; cv::Mat test_data, test_labels; try { training_data.create(sampleCount, attributesPerSample, CV_32FC1); train_labels.create(sampleCount, 1, CV_32FC1); test_data.create(testSampleCount, attributesPerSample, CV_32FC1); test_labels.create(testSampleCount, 1, CV_32FC1); } catch (const cv::Exception& cvex) { errorMessage = cvex.msg.c_str(); return false; } //fill the classification labels vector { unsigned sampleIndex = 0; unsigned testSampleIndex = 0; for (unsigned i = 0; i < cloud->size(); ++i) { ScalarType pointClass = classifSF->getValue(i); int iClass = static_cast(pointClass); if (iClass < 0 || iClass > 255) { errorMessage = QObject::tr("Classification values out of range (0-255)"); return false; } if (isSample[i]) { train_labels.at(sampleIndex++) = static_cast(iClass); } else { test_labels.at(testSampleIndex++) = static_cast(iClass); } } assert(sampleIndex + testSampleIndex == totalSampleCount); } //fill the training data matrix for (int fIndex = 0; fIndex < attributesPerSample; ++fIndex) { QScopedPointer source(nullptr); const Feature::Shared &f = features[fIndex]; switch (f->source) { case Feature::ScalarField: { int sfIdx = cloud->getScalarFieldIndexByName(qPrintable(f->sourceName)); if (sfIdx >= 0) { source.reset(new ScalarFieldWrapper(cloud->getScalarField(sfIdx))); } else { errorMessage = QObject::tr("Internal error: unknwon scalar field '%1'").arg(f->sourceName); return false; } } break; case Feature::DimX: source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimX)); break; case Feature::DimY: source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimY)); break; case Feature::DimZ: source.reset(new DimScalarFieldWrapper(cloud, DimScalarFieldWrapper::DimZ)); break; case Feature::Red: source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Red)); break; case Feature::Green: source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Green)); break; case Feature::Blue: source.reset(new ColorScalarFieldWrapper(cloud, ColorScalarFieldWrapper::Blue)); break; } if (!source || !source->isValid()) { assert(false); errorMessage = QObject::tr("Internal error: invalid source '%1'").arg(f->sourceName); return false; } unsigned sampleIndex = 0; unsigned testSampleIndex = 0; for (unsigned i = 0; i < cloud->size(); ++i) { double value = source->pointValue(i); if (isSample[i]) { assert(sampleIndex < sampleCount); training_data.at(sampleIndex++, fIndex) = static_cast(value); } else { assert(testSampleIndex< testSampleCount); test_data.at(testSampleIndex++, fIndex) = static_cast(value); } } assert(sampleIndex + testSampleIndex == totalSampleCount); } QProgressDialog pDlg(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.rt.maxDepth); m_rtrees->setMinSampleCount(params.rt.minSampleCount); m_rtrees->setCalculateVarImportance(params.rt.calcVarImportance); m_rtrees->setActiveVarCount(params.rt.activeVarCount); cv::TermCriteria terminationCriteria(cv::TermCriteria::MAX_ITER, params.rt.maxTreeCount, std::numeric_limits::epsilon()); m_rtrees->setTermCriteria(terminationCriteria); //rtrees->setRegressionAccuracy(0); //rtrees->setUseSurrogates(false); //rtrees->setMaxCategories(params.maxCategories); //not important? //rtrees->setPriors(cv::Mat()); try { m_rtrees->train(training_data, cv::ml::ROW_SAMPLE, train_labels); } 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; } pDlg.hide(); QCoreApplication::processEvents(); if (!m_rtrees->isTrained()) { errorMessage = QObject::tr("Training failed for an unknown reason..."); m_rtrees.release(); return false; } //estimate the efficiency of the classiier { int goodGuessCount = 0; for (int j = 0; j < testSampleCount; ++j) { if (m_rtrees->predict(test_data.row(j)) == test_labels.at(j)) { ++goodGuessCount; } } float acc = static_cast(goodGuessCount) / testSampleCount; ccLog::Print(QString("Correct = %1 / %2 --> Accuracy = %3").arg(goodGuessCount).arg(testSampleCount).arg(acc)); } //QString outputFilename = QCoreApplication::applicationDirPath() + "/classifier.yaml"; 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(); m_rtrees->save(filename.toStdString()); pDlg.close(); QCoreApplication::processEvents(); ccLog::Print("Classifier file saved to: " + filename); return true; } bool Classifier::fromFile(QString filename, QWidget* parentWidget/*=nullptr*/) { //load the classifier QProgressDialog pDlg(parentWidget); pDlg.setRange(0, 0); //infinite loop pDlg.setLabelText(QObject::tr("Loading classifier")); pDlg.show(); QCoreApplication::processEvents(); m_rtrees = cv::ml::RTrees::load(filename.toStdString()); pDlg.close(); QCoreApplication::processEvents(); if (!m_rtrees->isTrained()) { ccLog::Warning(QObject::tr("Loaded classifier doesn't seem to be trained")); } return true; }