From f6b3bb98891a6714b0a023a989e89e9aa58a42c3 Mon Sep 17 00:00:00 2001 From: Daniel Girardeau-Montaut Date: Wed, 27 Mar 2019 22:12:51 +0100 Subject: [PATCH] Command line enhanced --- DualCloudFeature.cpp | 6 + DualCloudFeature.h | 6 +- FeaturesInterface.cpp | 67 +++++++++ FeaturesInterface.h | 14 +- q3DMASCClassifier.cpp | 1 + q3DMASCCommands.h | 341 +++++++++++++++++++++++++++--------------- q3DMASCTools.cpp | 48 +++--- q3DMASCTools.h | 15 +- 8 files changed, 343 insertions(+), 155 deletions(-) diff --git a/DualCloudFeature.cpp b/DualCloudFeature.cpp index b0ffc8f..74ab863 100644 --- a/DualCloudFeature.cpp +++ b/DualCloudFeature.cpp @@ -28,6 +28,12 @@ bool DualCloudFeature::prepare( const CorePoints& corePoints, return false; } +QString DualCloudFeature::toString() const +{ + //use the default keyword + "_SC" + the scale + return ToString(type) + "_SC" + QString::number(scale); +} + bool DualCloudFeature::checkValidity(QString corePointRole, QString &error) const { if (!Feature::checkValidity(corePointRole, error)) diff --git a/DualCloudFeature.h b/DualCloudFeature.h index d5fb5a4..ac6af48 100644 --- a/DualCloudFeature.h +++ b/DualCloudFeature.h @@ -69,11 +69,7 @@ namespace masc virtual Feature::Shared clone() const override { return Feature::Shared(new DualCloudFeature(*this)); } virtual bool prepare(const CorePoints& corePoints, QString& error, CCLib::GenericProgressCallback* progressCb = nullptr, SFCollector* generatedScalarFields = nullptr) override; virtual bool checkValidity(QString corePointRole, QString &error) const override; - virtual QString toString() const override - { - //use the default keyword + "_SC" + the scale - return ToString(type) + "_SC" + QString::number(scale); - } + virtual QString toString() const override; //! Dual-cloud feature type /** \warning different from the feature type diff --git a/FeaturesInterface.cpp b/FeaturesInterface.cpp index 73a1621..8f4cfc7 100644 --- a/FeaturesInterface.cpp +++ b/FeaturesInterface.cpp @@ -134,6 +134,73 @@ bool Feature::PerformMathOp(const IScalarFieldWrapper& sf1, const IScalarFieldWr return true; } +bool Feature::SaveSources(const Source::Set& sources, QString filename) +{ + QFile file(filename); + if (!file.open(QFile::WriteOnly | QFile::Text)) + { + ccLog::Warning("Failed to open file for writing: " + filename); + return false; + } + + QTextStream stream(&file); + stream << "#Features_SF" << endl; + for (const Source& s : sources) + { + stream << s.type << ":" << s.name << endl; + } + + return true; +} + +bool Feature::LoadSources(Source::Set& sources, QString filename) +{ + QFile file(filename); + if (!file.open(QFile::ReadOnly | QFile::Text)) + { + ccLog::Warning("Failed to open file for reading: " + filename); + return false; + } + + QTextStream stream(&file); + QString header = stream.readLine(); + if (!header.startsWith("#Features_SF")) + { + ccLog::Warning("Unexpected header"); + return false; + } + + while (true) + { + QString line = stream.readLine(); + if (line.isNull()) + break; + if (line.isEmpty()) + continue; //unexpected but we can survive + QStringList tokens = line.split(':'); + if (tokens.size() != 2) + { + ccLog::Warning("Malformed file"); + return false; + } + + Source src; + bool ok = false; + int sourceType = tokens[0].toInt(&ok); + if (!ok || sourceType < Feature::Source::ScalarField || sourceType > Feature::Source::Blue) + { + ccLog::Warning("Unhandled source type"); + return false; + } + src.type = static_cast(sourceType); + src.name = tokens[1]; + + sources.push_back(src); + } + + return true; +} + bool Feature::ExtractSources(const Set& features, Source::Set& sources) { sources.clear(); diff --git a/FeaturesInterface.h b/FeaturesInterface.h index 4351cb9..e652055 100644 --- a/FeaturesInterface.h +++ b/FeaturesInterface.h @@ -125,7 +125,13 @@ namespace masc //! Sources types enum Type { - ScalarField, DimX, DimY, DimZ, Red, Green, Blue + ScalarField = 0, + DimX, + DimY, + DimZ, + Red, + Green, + Blue }; Source(Type t = ScalarField, QString n = QString()) @@ -140,6 +146,12 @@ namespace masc //! Extracts the set of 'sources' from a set of features static bool ExtractSources(const Set& features, Source::Set& sources); + //! Saves a set of 'sources' to a file + static bool SaveSources(const Source::Set& sources, QString filename); + + //! Loads a set of 'sources' from a file + static bool LoadSources(Source::Set& sources, QString filename); + public: //methods //! Default constructor diff --git a/q3DMASCClassifier.cpp b/q3DMASCClassifier.cpp index 35cdfa7..29b1979 100644 --- a/q3DMASCClassifier.cpp +++ b/q3DMASCClassifier.cpp @@ -132,6 +132,7 @@ bool Classifier::classify( const Feature::Source::Set& featureSources, try { ccScalarField* classifSFBackup = new ccScalarField(*static_cast(classificationSF)); + classifSFBackup->setName("Classification_prev"); cloud->addScalarField(classifSFBackup); } catch (const std::bad_alloc) diff --git a/q3DMASCCommands.h b/q3DMASCCommands.h index ff21fc6..5103a75 100644 --- a/q3DMASCCommands.h +++ b/q3DMASCCommands.h @@ -31,6 +31,8 @@ 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 { @@ -39,25 +41,74 @@ struct Command3DMASCClassif : public ccCommandLineInterface::Command virtual bool process(ccCommandLineInterface& cmd) override { cmd.print("[3DMASC]"); - if (cmd.arguments().size() < 2) + + if (cmd.clouds().empty()) { - return cmd.error(QString("Missing parameter(s): classifier filename (.txt) and cloud roles after \"-%1\"").arg(COMMAND_3DMASC_CLASSIFY)); + return cmd.error("No cloud loaded"); } - QString argument = cmd.arguments().front(); + 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; - if (ccCommandLineInterface::IsCommand(argument, COMMAND_3DMASC_KEEP_ATTRIBS)) + bool onlyFeatures = false; + bool skipFeatures = false; + QString featureSourceFilename; + while (true) { - keepAttributes = 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(); - //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 (cmd.arguments().size() < 2) + if (onlyFeatures && skipFeatures) { - return cmd.error(QString("Missing parameter(s): classifier filename (.txt) and cloud roles after \"-%1\"").arg(COMMAND_3DMASC_CLASSIFY)); + 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(); @@ -65,126 +116,168 @@ struct Command3DMASCClassif : public ccCommandLineInterface::Command QCoreApplication::processEvents(); cmd.arguments().pop_front(); - 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 clouds roles from the classifier file - QList 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)); - } - } - - masc::Feature::Set features; - masc::Classifier classifier; - if (!masc::Tools::LoadClassifier(classifierFilename, cloudPerRole, features, classifier, 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 = cloudPerRole[mainCloudRole]; - corePoints.role = mainCloudRole; - - //prepare the main cloud - QScopedPointer 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; + ccPointCloud* classifiedCloud = nullptr; SFCollector generatedScalarFields; - if (!masc::Tools::PrepareFeatures(corePoints, features, errorMessage, pDlg.data(), &generatedScalarFields)) - { - generatedScalarFields.releaseAllSFs(); - return cmd.error(errorMessage); - } + masc::Feature::Source::Set featureSources; - if (pDlg) + if (!skipFeatures) { - pDlg->setAutoClose(true); //restore the default behavior of the progress dialog - pDlg->close(); + //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 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 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.releaseAllSFs(); + 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::Feature::Source::Set featureSources; - masc::Feature::ExtractSources(features, featureSources); - if (!classifier.classify(featureSources, corePoints.cloud, errorMessage, cmd.widgetParent())) + 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.releaseAllSFs(); return cmd.error(errorMessage); @@ -196,13 +289,13 @@ struct Command3DMASCClassif : public ccCommandLineInterface::Command } } - if (cmd.autoSaveMode()) + if (cmd.autoSaveMode() || onlyFeatures) { for (CLCloudDesc& desc : cmd.clouds()) { - if (desc.pc == corePoints.origin) + if (desc.pc == classifiedCloud) { - QString errorStr = cmd.exportEntity(desc, "CLASSIFIED"); + QString errorStr = cmd.exportEntity(desc, onlyFeatures ? "WITH_FEATURES" : "CLASSIFIED"); if (!errorStr.isEmpty()) { return cmd.error(errorStr); diff --git a/q3DMASCTools.cpp b/q3DMASCTools.cpp index b30d613..fc6019c 100644 --- a/q3DMASCTools.cpp +++ b/q3DMASCTools.cpp @@ -653,14 +653,14 @@ static bool ReadCloud(const QString& command, Tools::NamedClouds& clouds, QDir& return true; } -static bool LoadFileCommon( const QString& filename, - Tools::NamedClouds& clouds, - bool cloudsAreProvided, - std::vector& rawFeatures, - masc::CorePoints* corePoints = nullptr, - masc::Classifier* classifier = nullptr, - TrainParameters* parameters = nullptr, - QWidget* parent = nullptr) +bool Tools::LoadFile( const QString& filename, + Tools::NamedClouds* clouds, + bool cloudsAreProvided, + std::vector* rawFeatures/*=nullptr*/, //requires 'clouds' + masc::CorePoints* corePoints/*=nullptr*/, //requires 'clouds' + masc::Classifier* classifier/*=nullptr*/, + TrainParameters* parameters/*=nullptr*/, + QWidget* parent/*=nullptr*/) { QFileInfo fi(filename); if (!fi.exists()) @@ -678,7 +678,7 @@ static bool LoadFileCommon( const QString& filename, try { - assert(rawFeatures.empty()); + assert(!rawFeatures || rawFeatures->empty()); std::vector scales; QTextStream stream(&file); @@ -727,26 +727,26 @@ static bool LoadFileCommon( const QString& filename, } else if (upperLine.startsWith("CLOUD:")) //clouds { - if (cloudsAreProvided) + if (!clouds || cloudsAreProvided) { //no need to load the clouds in this case continue; } QString command = line.mid(6); - if (!ReadCloud(command, clouds, fi.absoluteDir(), lineNumber)) + if (!ReadCloud(command, *clouds, fi.absoluteDir(), lineNumber)) { return false; } } else if (upperLine.startsWith("TEST:")) //test cloud { - if (cloudsAreProvided) + if (!clouds || cloudsAreProvided) { //no need to load the clouds in this case continue; } QString command = line.mid(5); - if (!ReadCloud("TEST=" + command, clouds, fi.absoluteDir(), lineNumber)) //add the TEST keyword so that the cloud will be loaded as the TEST cloud + if (!ReadCloud("TEST=" + command, *clouds, fi.absoluteDir(), lineNumber)) //add the TEST keyword so that the cloud will be loaded as the TEST cloud { return false; } @@ -766,7 +766,7 @@ static bool LoadFileCommon( const QString& filename, { QString command = line.mid(12); - if (!ReadCorePoints(command, clouds, *corePoints, lineNumber)) + if (clouds && !ReadCorePoints(command, *clouds, *corePoints, lineNumber)) { return false; } @@ -790,11 +790,14 @@ static bool LoadFileCommon( const QString& filename, { QString command = line.mid(8); - if (!CreateFeaturesFromCommand(command, corePoints ? corePoints->role : QString(), lineNumber, clouds, rawFeatures, scales)) + if (rawFeatures && clouds) { - //error message already issued - //return false; - badFeatures = true; //we continue as we want to get ALL the errors + if (!CreateFeaturesFromCommand(command, corePoints ? corePoints->role : QString(), lineNumber, *clouds, *rawFeatures, scales)) + { + //error message already issued + //return false; + badFeatures = true; //we continue as we want to get ALL the errors + } } } else if (upperLine.startsWith("PARAM_")) //parameter @@ -856,14 +859,15 @@ static bool LoadFileCommon( const QString& filename, return false; } - rawFeatures.shrink_to_fit(); + if (rawFeatures) + rawFeatures->shrink_to_fit(); return true; } -bool Tools::LoadClassifier(QString filename, const NamedClouds& clouds, Feature::Set& rawFeatures, masc::Classifier& classifier, QWidget* parent/*=nullptr*/) +bool Tools::LoadClassifier(QString filename, NamedClouds& clouds, Feature::Set& rawFeatures, masc::Classifier& classifier, QWidget* parent/*=nullptr*/) { - return LoadFileCommon(filename, const_cast(clouds), true, rawFeatures, nullptr, &classifier, nullptr, parent); + return LoadFile(filename, &clouds, true, &rawFeatures, nullptr, &classifier, nullptr, parent); } bool Tools::LoadTrainingFile( QString filename, @@ -873,7 +877,7 @@ bool Tools::LoadTrainingFile( QString filename, CorePoints* corePoints/*=nullptr*/) { bool cloudsWereProvided = !loadedClouds.empty(); - if (LoadFileCommon(filename, loadedClouds, cloudsWereProvided, rawFeatures, corePoints, nullptr, ¶meters, nullptr)) + if (LoadFile(filename, &loadedClouds, cloudsWereProvided, &rawFeatures, corePoints, nullptr, ¶meters, nullptr)) { return true; } diff --git a/q3DMASCTools.h b/q3DMASCTools.h index 3209c81..7657998 100644 --- a/q3DMASCTools.h +++ b/q3DMASCTools.h @@ -40,11 +40,20 @@ namespace masc static bool LoadTrainingFile(QString filename, Feature::Set& rawFeatures, NamedClouds& loadedClouds, TrainParameters& parameters, CorePoints* corePoints = nullptr); - static bool SaveClassifier(QString filename, const Feature::Set& features, const QString corePointsRole, const masc::Classifier& classifier, QWidget* parent = nullptr); - static bool LoadClassifierCloudLabels(QString filename, QList& labels, QString& corePointsLabel, bool& filenamesSpecified); - static bool LoadClassifier(QString filename, const NamedClouds& clouds, Feature::Set& rawFeatures, masc::Classifier& classifier, QWidget* parent = nullptr); + static bool LoadClassifier(QString filename, NamedClouds& clouds, Feature::Set& rawFeatures, masc::Classifier& classifier, QWidget* parent = nullptr); + + static bool LoadFile( const QString& filename, + Tools::NamedClouds* clouds, + bool cloudsAreProvided, + std::vector* rawFeatures = nullptr, //requires 'clouds' + masc::CorePoints* corePoints = nullptr, //requires 'clouds' + masc::Classifier* classifier = nullptr, + TrainParameters* parameters = nullptr, + QWidget* parent = nullptr); + + static bool SaveClassifier(QString filename, const Feature::Set& features, const QString corePointsRole, const masc::Classifier& classifier, QWidget* parent = nullptr); static bool PrepareFeatures(const CorePoints& corePoints, Feature::Set& features, QString& error, CCLib::GenericProgressCallback* progressCb = nullptr, SFCollector* generatedScalarFields = nullptr);