ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
q3DMASCCommands.h
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #pragma once
9 
10 // ##########################################################################
11 // # #
12 // # ACLOUDVIEWER PLUGIN: qCANUPO #
13 // # #
14 // # This program is free software; you can redistribute it and/or modify #
15 // # it under the terms of the GNU General Public License as published by #
16 // # the Free Software Foundation; version 2 or later of the License. #
17 // # #
18 // # This program is distributed in the hope that it will be useful, #
19 // # but WITHOUT ANY WARRANTY; without even the implied warranty of #
20 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
21 // # GNU General Public License for more details. #
22 // # #
23 // # COPYRIGHT: UEB (UNIVERSITE EUROPEENNE DE BRETAGNE) / CNRS #
24 // # #
25 // ##########################################################################
26 
27 // ACloudViewer
29 
30 // Local
31 #include "q3DMASCTools.h"
32 
33 // qCC_db
34 #include <ecvProgressDialog.h>
35 
36 // Qt
37 #include <QCoreApplication>
38 #include <QDialog>
39 #include <QFileInfo>
40 
41 // Qt5/Qt6 Compatibility
42 #include <QtCompat.h>
43 
44 static const char COMMAND_3DMASC_CLASSIFY[] = "3DMASC_CLASSIFY";
45 static const char COMMAND_3DMASC_KEEP_ATTRIBS[] = "KEEP_ATTRIBUTES";
46 static const char COMMAND_3DMASC_ONLY_FEATURES[] = "ONLY_FEATURES";
47 static const char COMMAND_3DMASC_SKIP_FEATURES[] = "SKIP_FEATURES";
48 
51  : ccCommandLineInterface::Command("3DMASC Classify",
53 
54  virtual bool process(ccCommandLineInterface& cmd) override {
55  cmd.print("[3DMASC]");
56 
57  if (cmd.clouds().empty()) {
58  return cmd.error("No cloud loaded");
59  }
60 
61  int minArgumentCount = 2;
62  if (cmd.arguments().size() < minArgumentCount) {
63  return cmd.error(
64  QString("Missing parameter(s): options, classifier "
65  "filename (.txt) and cloud roles after \"-%1\"")
67  }
68 
69  bool keepAttributes = false;
70  bool onlyFeatures = false;
71  bool skipFeatures = false;
72  QString featureSourceFilename;
73  while (true) {
74  QString argument = cmd.arguments().front();
76  argument, COMMAND_3DMASC_KEEP_ATTRIBS)) {
77  keepAttributes = true;
78  cmd.print("Will keep attributes");
79  // local option confirmed, we can move on
80  cmd.arguments().pop_front();
82  argument, COMMAND_3DMASC_ONLY_FEATURES)) {
83  onlyFeatures = true;
84  cmd.print("Will compute only the features");
85  // local option confirmed, we can move on
86  cmd.arguments().pop_front();
87 
88  // we are bound to to keep the features!
89  keepAttributes = true;
91  argument, COMMAND_3DMASC_SKIP_FEATURES)) {
92  skipFeatures = true;
93  cmd.print("Will skip the computation of features");
94  // local option confirmed, we can move on
95  cmd.arguments().pop_front();
96 
97  featureSourceFilename = cmd.arguments().front();
98  if (featureSourceFilename.isEmpty()) {
99  return cmd.error(
100  QString("Missing parameter(s): feature sources "
101  "filename after \"-%1\"")
103  }
104  cmd.arguments().pop_front();
105 
106  // we only expect the classifier filename now
107  --minArgumentCount;
108  } else {
109  // urecognized option
110  break;
111  }
112  }
113 
114  if (onlyFeatures && skipFeatures) {
115  return cmd.error(
116  "Can't compute only the features and skip them at the same "
117  "time :p");
118  }
119 
120  if (cmd.arguments().size() < minArgumentCount) {
121  return cmd.error(
122  QString("Missing parameter(s): classifier filename (.txt) "
123  "and/or cloud roles after \"-%1\"")
125  }
126 
127  QString classifierFilename = cmd.arguments().front();
128  cmd.print("Classifier filename: " + classifierFilename);
129  QCoreApplication::processEvents();
130  cmd.arguments().pop_front();
131 
132  ccPointCloud* classifiedCloud = nullptr;
133  SFCollector generatedScalarFields;
134  masc::Feature::Source::Set featureSources;
135 
136  if (!skipFeatures) {
137  // we need to load the cloud roles and match them with the already
138  // loaded clouds
139  QString cloudRolesStr = cmd.arguments().front();
140  cmd.print("Cloud roles: " + cloudRolesStr);
141  QCoreApplication::processEvents();
142  cmd.arguments().pop_front();
143 
144  // process the cloud roles description
145  QStringList tokens = cloudRolesStr.simplified().split(
146  QChar(' '), QtCompat::SkipEmptyParts);
147 
148  masc::Tools::NamedClouds cloudPerRole;
149  QString mainCloudRole;
150  for (const QString& token : tokens) {
151  QStringList subTokens = token.split("=");
152  int subTokenCount = subTokens.size();
153  if (subTokenCount != 2) {
154  return cmd.error(
155  "Malformed cloud roles description (expecting: "
156  "\"PC1=1 PC2=3 CTX=2\" for instance)");
157  }
158  QString role = subTokens[0].toUpper();
159  bool ok = false;
160  unsigned cloudIndex = subTokens[1].toUInt(&ok);
161  if (!ok || cloudIndex == 0) {
162  return cmd.error(
163  "Malformed cloud roles description (expecting the "
164  "cloud index corresponding to each role - starting "
165  "from 1)");
166  }
167  if (cloudIndex > cmd.clouds().size()) {
168  return cmd.error(QString("Cloud index %1 exceeds the "
169  "number of loaded clouds (=%2)")
170  .arg(cloudIndex)
171  .arg(cmd.clouds().size()));
172  }
173  cloudPerRole.insert(role, cmd.clouds()[cloudIndex - 1].pc);
174 
175  if (mainCloudRole.isEmpty()) {
176  mainCloudRole = role;
177  }
178  }
179 
180  // try to load the cloud roles from the classifier file
181  QList<QString> cloudLabels;
182  QString corePointsLabel;
183  bool filenamesSpecified = false;
184  QMap<QString, QString> labelsAndNames;
186  classifierFilename, cloudLabels, corePointsLabel,
187  filenamesSpecified, labelsAndNames)) {
188  return cmd.error("Failed to read classifier file");
189  }
190 
191  if (!corePointsLabel.isEmpty()) {
192  // we use the core points source as 'main role' by default
193  mainCloudRole = corePointsLabel;
194  cmd.print("Core points source: " + corePointsLabel +
195  " (will be used as the classified cloud)");
196  }
197  cmd.print("The classified cloud role will be " + mainCloudRole);
198 
199  // if (!filenamesSpecified)
200  //{
201  // return cmd.error("Filenames were not specified for at least one
202  // role");
203  // }
204 
205  for (QString label : cloudLabels) {
206  if (!cloudPerRole.contains(label.toUpper())) {
207  return cmd.error(
208  QString("Role %1 has not been defined").arg(label));
209  }
210  }
211 
212  // load features
213  masc::Feature::Set features;
214  std::vector<double> scales;
215  if (!masc::Tools::LoadFile(classifierFilename, &cloudPerRole, true,
216  &features, &scales, nullptr, nullptr,
217  nullptr, cmd.widgetParent())) {
218  return cmd.error("Failed to load the classifier");
219  }
220 
221  // internal consistency check
222  if (!cloudPerRole.contains(mainCloudRole)) {
223  return cmd.error("Classified cloud not loaded/defined?!");
224  }
225 
226  // remove the test cloud (if any)
227  if (cloudPerRole.contains("TEST")) {
228  delete cloudPerRole["TEST"];
229  cloudPerRole.remove("TEST");
230  }
231 
232  // the 'main cloud' is the cloud that should be classified
234  corePoints.origin = corePoints.cloud = classifiedCloud =
235  cloudPerRole[mainCloudRole];
236  corePoints.role = mainCloudRole;
237 
238  // prepare the main cloud
239  QScopedPointer<ecvProgressDialog> pDlg;
240  if (!cmd.silentMode()) {
241  pDlg.reset(new ecvProgressDialog(true, cmd.widgetParent()));
242  pDlg->setAutoClose(false); // we don't want the progress dialog
243  // to 'pop' for each feature
244  }
245 
246  QString errorMessage;
248  errorMessage, pDlg.data(),
249  &generatedScalarFields)) {
250  generatedScalarFields.releaseSFs(false);
251  return cmd.error(errorMessage);
252  }
253 
254  if (pDlg) {
255  pDlg->setAutoClose(true); // restore the default behavior of
256  // the progress dialog
257  pDlg->close();
258  QCoreApplication::processEvents();
259  }
260 
261  // don't forget to extract the sources before finishing this step
262  masc::Feature::ExtractSources(features, featureSources);
263 
264  if (onlyFeatures) {
265  QFileInfo fi(classifierFilename);
266  featureSourceFilename = fi.absolutePath() + "/" +
267  fi.completeBaseName() +
268  "_feature_sources.txt";
269  if (masc::Feature::SaveSources(featureSources,
270  featureSourceFilename)) {
271  cmd.print("Feature sources file saved: " +
272  featureSourceFilename);
273  // return true;
274  } else {
275  return cmd.error(
276  "Faild to write feature sources to file: " +
277  featureSourceFilename);
278  }
279  }
280  } else {
281  // we use the first loaded cloud by default
282  classifiedCloud = cmd.clouds().front().pc;
283 
284  // load the feature 'sources'
285  if (!masc::Feature::LoadSources(featureSources,
286  featureSourceFilename)) {
287  return cmd.error("Failed to load feature sources from: " +
288  featureSourceFilename);
289  }
290  }
291 
292  // apply classifier
293  if (!onlyFeatures) {
294  masc::Classifier classifier;
295  if (!masc::Tools::LoadFile(classifierFilename, nullptr, false,
296  nullptr, nullptr, nullptr, &classifier,
297  nullptr, cmd.widgetParent())) {
298  return cmd.error("Failed to load the classifier");
299  }
300 
301  QString errorMessage;
302  if (!classifier.classify(featureSources, classifiedCloud,
303  errorMessage, cmd.widgetParent())) {
304  generatedScalarFields.releaseSFs(false);
305  return cmd.error(errorMessage);
306  }
307 
308  generatedScalarFields.releaseSFs(keepAttributes);
309  }
310 
311  if (cmd.autoSaveMode() || onlyFeatures) {
312  for (CLCloudDesc& desc : cmd.clouds()) {
313  if (desc.pc == classifiedCloud) {
314  QString errorStr = cmd.exportEntity(
315  desc,
316  onlyFeatures ? "WITH_FEATURES" : "CLASSIFIED");
317  if (!errorStr.isEmpty()) {
318  return cmd.error(errorStr);
319  }
320  break;
321  }
322  }
323  }
324 
325  return true;
326  }
327 };
SF collector.
void releaseSFs(bool keepByDefault)
Command line interface.
virtual QStringList & arguments()=0
Returns the list of arguments.
virtual void print(const QString &message) const =0
virtual bool error(const QString &message) const =0
static bool IsCommand(const QString &token, const char *command)
Test whether a command line token is a valid command keyword or not.
bool silentMode() const
Returns the silent mode.
virtual QString exportEntity(CLEntityDesc &entityDesc, const QString &suffix=QString(), QString *outputFilename=nullptr, ccCommandLineInterface::ExportOptions options=ExportOption::NoOptions)=0
Exports a cloud or a mesh.
virtual QDialog * widgetParent()
Returns a (widget) parent (if any is available)
virtual std::vector< CLCloudDesc > & clouds()
Currently opened point clouds and their filename.
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
Graphical progress indicator (thread-safe)
bool classify(const Feature::Source::Set &featureSources, ccPointCloud *cloud, QString &errorMessage, QWidget *parentWidget=nullptr, ecvMainAppInterface *app=nullptr)
Applies the classifier.
static bool LoadFile(const QString &filename, Tools::NamedClouds *clouds, bool cloudsAreProvided, std::vector< Feature::Shared > *rawFeatures=nullptr, std::vector< double > *rawScales=nullptr, masc::CorePoints *corePoints=nullptr, masc::Classifier *classifier=nullptr, TrainParameters *parameters=nullptr, QWidget *parent=nullptr)
static bool PrepareFeatures(const CorePoints &corePoints, Feature::Set &features, QString &error, cloudViewer::GenericProgressCallback *progressCb=nullptr, SFCollector *generatedScalarFields=nullptr)
static bool LoadClassifierCloudLabels(QString filename, QList< QString > &labels, QString &corePointsLabel, bool &filenamesSpecified, QMap< QString, QString > &rolesAndNames)
QMap< QString, ccPointCloud * > NamedClouds
Definition: q3DMASCTools.h:43
constexpr Qt::SplitBehavior SkipEmptyParts
Definition: QtCompat.h:302
static const char COMMAND_3DMASC_KEEP_ATTRIBS[]
static const char COMMAND_3DMASC_ONLY_FEATURES[]
static const char COMMAND_3DMASC_SKIP_FEATURES[]
static const char COMMAND_3DMASC_CLASSIFY[]
cloudViewer::GenericIndexedCloud * corePoints
Loaded cloud description.
virtual bool process(ccCommandLineInterface &cmd) override
Main process.
Command(const QString &name, const QString &keyword)
Default constructor.
Core points descriptor.
Definition: CorePoints.h:39
std::vector< Source > Set
static bool ExtractSources(const Set &features, Source::Set &sources)
Extracts the set of 'sources' from a set of features.
static bool SaveSources(const Source::Set &sources, QString filename)
Saves a set of 'sources' to a file.
std::vector< Shared > Set
Set of features.
static bool LoadSources(Source::Set &sources, QString filename)
Loads a set of 'sources' from a file.