mirror of
https://github.com/dgirardeau/q3DMASC.git
synced 2026-08-29 16:40:49 +08:00
308 lines
9.6 KiB
C
308 lines
9.6 KiB
C
#pragma once
|
|
|
|
//##########################################################################
|
|
//# #
|
|
//# CLOUDCOMPARE PLUGIN: qCANUPO #
|
|
//# #
|
|
//# 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: UEB (UNIVERSITE EUROPEENNE DE BRETAGNE) / CNRS #
|
|
//# #
|
|
//##########################################################################
|
|
|
|
//CloudCompare
|
|
#include "../../ccCommandLineInterface.h"
|
|
|
|
//Local
|
|
#include "q3DMASCTools.h"
|
|
|
|
//qCC_db
|
|
#include <ccProgressDialog.h>
|
|
|
|
//Qt
|
|
#include <QDialog>
|
|
|
|
static const char COMMAND_3DMASC_CLASSIFY[] = "3DMASC_CLASSIFY";
|
|
static const char COMMAND_3DMASC_KEEP_ATTRIBS[] = "KEEP_ATTRIBUTES";
|
|
static const char COMMAND_3DMASC_ONLY_FEATURES[] = "ONLY_FEATURES";
|
|
static const char COMMAND_3DMASC_SKIP_FEATURES[] = "SKIP_FEATURES";
|
|
|
|
struct Command3DMASCClassif : public ccCommandLineInterface::Command
|
|
{
|
|
Command3DMASCClassif() : ccCommandLineInterface::Command("3DMASC Classify", COMMAND_3DMASC_CLASSIFY) {}
|
|
|
|
virtual bool process(ccCommandLineInterface& cmd) override
|
|
{
|
|
cmd.print("[3DMASC]");
|
|
|
|
if (cmd.clouds().empty())
|
|
{
|
|
return cmd.error("No cloud loaded");
|
|
}
|
|
|
|
int minArgumentCount = 2;
|
|
if (cmd.arguments().size() < minArgumentCount)
|
|
{
|
|
return cmd.error(QString("Missing parameter(s): options, classifier filename (.txt) and cloud roles after \"-%1\"").arg(COMMAND_3DMASC_CLASSIFY));
|
|
}
|
|
|
|
bool keepAttributes = false;
|
|
bool onlyFeatures = false;
|
|
bool skipFeatures = false;
|
|
QString featureSourceFilename;
|
|
while (true)
|
|
{
|
|
QString argument = cmd.arguments().front();
|
|
if (ccCommandLineInterface::IsCommand(argument, COMMAND_3DMASC_KEEP_ATTRIBS))
|
|
{
|
|
keepAttributes = true;
|
|
cmd.print("Will keep attributes");
|
|
//local option confirmed, we can move on
|
|
cmd.arguments().pop_front();
|
|
}
|
|
else if (ccCommandLineInterface::IsCommand(argument, COMMAND_3DMASC_ONLY_FEATURES))
|
|
{
|
|
onlyFeatures = true;
|
|
cmd.print("Will compute only the features");
|
|
//local option confirmed, we can move on
|
|
cmd.arguments().pop_front();
|
|
|
|
//we are bound to to keep the features!
|
|
keepAttributes = true;
|
|
}
|
|
else if (ccCommandLineInterface::IsCommand(argument, COMMAND_3DMASC_SKIP_FEATURES))
|
|
{
|
|
skipFeatures = true;
|
|
cmd.print("Will skip the computation of features");
|
|
//local option confirmed, we can move on
|
|
cmd.arguments().pop_front();
|
|
|
|
featureSourceFilename = cmd.arguments().front();
|
|
if (featureSourceFilename.isEmpty())
|
|
{
|
|
return cmd.error(QString("Missing parameter(s): feature sources filename after \"-%1\"").arg(COMMAND_3DMASC_SKIP_FEATURES));
|
|
}
|
|
cmd.arguments().pop_front();
|
|
|
|
//we only expect the classifier filename now
|
|
--minArgumentCount;
|
|
}
|
|
else
|
|
{
|
|
//urecognized option
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (onlyFeatures && skipFeatures)
|
|
{
|
|
return cmd.error("Can't compute only the features and skip them at the same time :p");
|
|
}
|
|
|
|
if (cmd.arguments().size() < minArgumentCount)
|
|
{
|
|
return cmd.error(QString("Missing parameter(s): classifier filename (.txt) and/or cloud roles after \"-%1\"").arg(COMMAND_3DMASC_CLASSIFY));
|
|
}
|
|
|
|
QString classifierFilename = cmd.arguments().front();
|
|
cmd.print("Classifier filename: " + classifierFilename);
|
|
QCoreApplication::processEvents();
|
|
cmd.arguments().pop_front();
|
|
|
|
ccPointCloud* classifiedCloud = nullptr;
|
|
SFCollector generatedScalarFields;
|
|
masc::Feature::Source::Set featureSources;
|
|
|
|
if (!skipFeatures)
|
|
{
|
|
//we need to load the cloud roles and match them with the already loaded clouds
|
|
QString cloudRolesStr = cmd.arguments().front();
|
|
cmd.print("Cloud roles: " + cloudRolesStr);
|
|
QCoreApplication::processEvents();
|
|
cmd.arguments().pop_front();
|
|
|
|
//process the cloud roles description
|
|
QStringList tokens = cloudRolesStr.split(QRegExp("\\s+"), QString::SkipEmptyParts);
|
|
|
|
masc::Tools::NamedClouds cloudPerRole;
|
|
QString mainCloudRole;
|
|
for (const QString& token : tokens)
|
|
{
|
|
QStringList subTokens = token.split("=");
|
|
int subTokenCount = subTokens.size();
|
|
if (subTokenCount != 2)
|
|
{
|
|
return cmd.error("Malformed cloud roles description (expecting: \"PC1=1 PC2=3 CTX=2\" for instance)");
|
|
}
|
|
QString role = subTokens[0].toUpper();
|
|
bool ok = false;
|
|
unsigned cloudIndex = subTokens[1].toUInt(&ok);
|
|
if (!ok || cloudIndex == 0)
|
|
{
|
|
return cmd.error("Malformed cloud roles description (expecting the cloud index corresponding to each role - starting from 1)");
|
|
}
|
|
if (cloudIndex > cmd.clouds().size())
|
|
{
|
|
return cmd.error(QString("Cloud index %1 exceeds the number of loaded clouds (=%2)").arg(cloudIndex).arg(cmd.clouds().size()));
|
|
}
|
|
cloudPerRole.insert(role, cmd.clouds()[cloudIndex - 1].pc);
|
|
|
|
if (mainCloudRole.isEmpty())
|
|
{
|
|
mainCloudRole = role;
|
|
}
|
|
}
|
|
|
|
//try to load the cloud roles from the classifier file
|
|
QList<QString> cloudLabels;
|
|
QString corePointsLabel;
|
|
bool filenamesSpecified = false;
|
|
if (!masc::Tools::LoadClassifierCloudLabels(classifierFilename, cloudLabels, corePointsLabel, filenamesSpecified))
|
|
{
|
|
return cmd.error("Failed to read classifier file");
|
|
}
|
|
|
|
if (!corePointsLabel.isEmpty())
|
|
{
|
|
//we use the core points source as 'main role' by default
|
|
mainCloudRole = corePointsLabel;
|
|
cmd.print("Core points source: " + corePointsLabel + "(will be used as the classified cloud)");
|
|
}
|
|
cmd.print("The classified cloud role will be " + mainCloudRole);
|
|
|
|
//if (!filenamesSpecified)
|
|
//{
|
|
// return cmd.error("Filenames were not specified for at least one role");
|
|
//}
|
|
|
|
for (QString label : cloudLabels)
|
|
{
|
|
if (!cloudPerRole.contains(label.toUpper()))
|
|
{
|
|
return cmd.error(QString("Role %1 has not been defined").arg(label));
|
|
}
|
|
}
|
|
|
|
//load features
|
|
masc::Feature::Set features;
|
|
if (!masc::Tools::LoadFile(classifierFilename, &cloudPerRole, true, &features, nullptr, nullptr, nullptr, cmd.widgetParent()))
|
|
{
|
|
return cmd.error("Failed to load the classifier");
|
|
}
|
|
|
|
//internal consistency check
|
|
if (!cloudPerRole.contains(mainCloudRole))
|
|
{
|
|
return cmd.error("Classified cloud not loaded/defined?!");
|
|
}
|
|
|
|
//remove the test cloud (if any)
|
|
if (cloudPerRole.contains("TEST"))
|
|
{
|
|
delete cloudPerRole["TEST"];
|
|
cloudPerRole.remove("TEST");
|
|
}
|
|
|
|
//the 'main cloud' is the cloud that should be classified
|
|
masc::CorePoints corePoints;
|
|
corePoints.origin = corePoints.cloud = classifiedCloud = cloudPerRole[mainCloudRole];
|
|
corePoints.role = mainCloudRole;
|
|
|
|
//prepare the main cloud
|
|
QScopedPointer<ccProgressDialog> pDlg;
|
|
if (!cmd.silentMode())
|
|
{
|
|
pDlg.reset(new ccProgressDialog(true, cmd.widgetParent()));
|
|
pDlg->setAutoClose(false); //we don't want the progress dialog to 'pop' for each feature
|
|
}
|
|
|
|
QString errorMessage;
|
|
if (!masc::Tools::PrepareFeatures(corePoints, features, errorMessage, pDlg.data(), &generatedScalarFields))
|
|
{
|
|
generatedScalarFields.releaseSFs(false);
|
|
return cmd.error(errorMessage);
|
|
}
|
|
|
|
if (pDlg)
|
|
{
|
|
pDlg->setAutoClose(true); //restore the default behavior of the progress dialog
|
|
pDlg->hide();
|
|
QCoreApplication::processEvents();
|
|
}
|
|
|
|
//don't forget to extract the sources before finishing this step
|
|
masc::Feature::ExtractSources(features, featureSources);
|
|
|
|
if (onlyFeatures)
|
|
{
|
|
QFileInfo fi(classifierFilename);
|
|
featureSourceFilename = fi.absolutePath() + "/" + fi.completeBaseName() + "_feature_sources.txt";
|
|
if (masc::Feature::SaveSources(featureSources, featureSourceFilename))
|
|
{
|
|
cmd.print("Feature sources file saved: " + featureSourceFilename);
|
|
//return true;
|
|
}
|
|
else
|
|
{
|
|
return cmd.error("Faild to write feature sources to file: " + featureSourceFilename);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//we use the first loaded cloud by default
|
|
classifiedCloud = cmd.clouds().front().pc;
|
|
|
|
//load the feature 'sources'
|
|
if (!masc::Feature::LoadSources(featureSources, featureSourceFilename))
|
|
{
|
|
return cmd.error("Failed to load feature sources from: " + featureSourceFilename);
|
|
}
|
|
}
|
|
|
|
//apply classifier
|
|
if (!onlyFeatures)
|
|
{
|
|
masc::Classifier classifier;
|
|
if (!masc::Tools::LoadFile(classifierFilename, nullptr, false, nullptr, nullptr, &classifier, nullptr, cmd.widgetParent()))
|
|
{
|
|
return cmd.error("Failed to load the classifier");
|
|
}
|
|
|
|
QString errorMessage;
|
|
if (!classifier.classify(featureSources, classifiedCloud, errorMessage, cmd.widgetParent()))
|
|
{
|
|
generatedScalarFields.releaseSFs(false);
|
|
return cmd.error(errorMessage);
|
|
}
|
|
|
|
generatedScalarFields.releaseSFs(keepAttributes);
|
|
}
|
|
|
|
if (cmd.autoSaveMode() || onlyFeatures)
|
|
{
|
|
for (CLCloudDesc& desc : cmd.clouds())
|
|
{
|
|
if (desc.pc == classifiedCloud)
|
|
{
|
|
QString errorStr = cmd.exportEntity(desc, onlyFeatures ? "WITH_FEATURES" : "CLASSIFIED");
|
|
if (!errorStr.isEmpty())
|
|
{
|
|
return cmd.error(errorStr);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|