ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
ecvCommandLineCommands.cpp
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 // LOCAL
10 
11 // CV_CORE_LIB
12 #include <AutoSegmentationTools.h>
13 #include <CVConst.h>
14 #include <CloudSamplingTools.h>
16 #include <MeshSamplingTools.h>
17 #include <NormalDistribution.h>
19 #include <WeibullDistribution.h>
20 
21 // CV_DB_LIB
22 #include <ecvHObjectCaster.h>
23 #include <ecvNormalVectors.h>
24 #include <ecvPlane.h>
25 #include <ecvPolyline.h>
26 #include <ecvProgressDialog.h>
27 #include <ecvScalarField.h>
28 #include <ecvVolumeCalcTool.h>
29 
30 // qCC_io
31 #include <AsciiFilter.h>
32 #include <PlyFilter.h>
33 
34 // qCC
35 #include <ui_commandLineDlg.h>
36 
37 #include "ecvCommon.h"
38 #include "ecvComparisonDlg.h"
39 #include "ecvConsole.h"
40 #include "ecvCropTool.h"
41 #include "ecvLibAlgorithms.h"
42 #include "ecvRegistrationTools.h"
44 
45 // Local
46 #include "ecvEntityAction.h"
47 
48 // Qt
49 #include <QDateTime>
50 #include <QFileInfo>
51 
52 // commands
53 constexpr char COMMAND_CLOUD_EXPORT_FORMAT[] = "C_EXPORT_FMT";
54 constexpr char COMMAND_EXPORT_EXTENSION[] = "EXT";
55 constexpr char COMMAND_ASCII_EXPORT_PRECISION[] = "PREC";
56 constexpr char COMMAND_ASCII_EXPORT_SEPARATOR[] = "SEP";
57 constexpr char COMMAND_ASCII_EXPORT_ADD_COL_HEADER[] = "ADD_HEADER";
58 constexpr char COMMAND_ASCII_EXPORT_ADD_PTS_COUNT[] = "ADD_PTS_COUNT";
59 constexpr char COMMAND_MESH_EXPORT_FORMAT[] = "M_EXPORT_FMT";
60 constexpr char COMMAND_HIERARCHY_EXPORT_FORMAT[] = "H_EXPORT_FMT";
61 constexpr char COMMAND_OPEN[] = "O"; //+file name
62 constexpr char COMMAND_OPEN_SKIP_LINES[] = "SKIP"; //+number of lines to skip
63 constexpr char COMMAND_SUBSAMPLE[] =
64  "SS"; //+ method (RANDOM/SPATIAL/OCTREE) + parameter (resp. point count
66 constexpr char COMMAND_EXTRACT_CC[] = "EXTRACT_CC";
67 constexpr char COMMAND_CURVATURE[] = "CURV"; //+ curvature type (MEAN/GAUSS)
68 constexpr char COMMAND_DENSITY[] = "DENSITY"; //+ sphere radius
69 constexpr char COMMAND_DENSITY_TYPE[] = "TYPE"; //+ density type
70 constexpr char COMMAND_APPROX_DENSITY[] = "APPROX_DENSITY";
71 constexpr char COMMAND_SF_GRADIENT[] = "SF_GRAD";
72 constexpr char COMMAND_ROUGHNESS[] = "ROUGH";
73 constexpr char COMMAND_ROUGHNESS_UP_DIR[] = "UP_DIR";
74 constexpr char COMMAND_APPLY_TRANSFORMATION[] = "APPLY_TRANS";
75 constexpr char COMMAND_DROP_GLOBAL_SHIFT[] = "DROP_GLOBAL_SHIFT";
76 constexpr char COMMAND_SF_COLOR_SCALE[] = "SF_COLOR_SCALE";
77 constexpr char COMMAND_SF_CONVERT_TO_RGB[] = "SF_CONVERT_TO_RGB";
78 constexpr char COMMAND_FILTER_SF_BY_VALUE[] = "FILTER_SF";
79 constexpr char COMMAND_MERGE_CLOUDS[] = "MERGE_CLOUDS";
80 constexpr char COMMAND_MERGE_MESHES[] = "MERGE_MESHES";
81 constexpr char COMMAND_SET_ACTIVE_SF[] = "SET_ACTIVE_SF";
82 constexpr char COMMAND_REMOVE_ALL_SFS[] = "REMOVE_ALL_SFS";
83 constexpr char COMMAND_REMOVE_SF[] = "REMOVE_SF";
84 constexpr char COMMAND_REMOVE_SCAN_GRIDS[] = "REMOVE_SCAN_GRIDS";
85 constexpr char COMMAND_REMOVE_RGB[] = "REMOVE_RGB";
86 constexpr char COMMAND_REMOVE_NORMALS[] = "REMOVE_NORMALS";
87 constexpr char COMMAND_MATCH_BB_CENTERS[] = "MATCH_CENTERS";
88 constexpr char COMMAND_BEST_FIT_PLANE[] = "BEST_FIT_PLANE";
89 constexpr char COMMAND_BEST_FIT_PLANE_MAKE_HORIZ[] = "MAKE_HORIZ";
90 constexpr char COMMAND_BEST_FIT_PLANE_KEEP_LOADED[] = "KEEP_LOADED";
91 constexpr char COMMAND_ORIENT_NORMALS[] = "ORIENT_NORMS_MST";
92 constexpr char COMMAND_SOR_FILTER[] = "SOR";
93 constexpr char COMMAND_SAMPLE_MESH[] = "SAMPLE_MESH";
94 constexpr char COMMAND_CROP[] = "CROP";
95 constexpr char COMMAND_CROP_OUTSIDE[] = "OUTSIDE";
96 constexpr char COMMAND_CROP_2D[] = "CROP2D";
97 constexpr char COMMAND_COLOR_BANDING[] = "CBANDING";
98 constexpr char COMMAND_C2M_DIST[] = "C2M_DIST";
99 constexpr char COMMAND_C2M_DIST_FLIP_NORMALS[] = "FLIP_NORMS";
100 constexpr char COMMAND_C2C_DIST[] = "C2C_DIST";
101 constexpr char COMMAND_CLOSEST_POINT_SET[] = "CLOSEST_POINT_SET";
102 constexpr char COMMAND_C2C_SPLIT_XYZ[] = "SPLIT_XYZ";
103 constexpr char COMMAND_C2C_LOCAL_MODEL[] = "MODEL";
104 constexpr char COMMAND_C2X_MAX_DISTANCE[] = "MAX_DIST";
105 constexpr char COMMAND_C2X_OCTREE_LEVEL[] = "OCTREE_LEVEL";
106 constexpr char COMMAND_STAT_TEST[] = "STAT_TEST";
107 constexpr char COMMAND_DELAUNAY[] = "DELAUNAY";
108 constexpr char COMMAND_DELAUNAY_AA[] = "AA";
109 constexpr char COMMAND_DELAUNAY_BF[] = "BEST_FIT";
110 constexpr char COMMAND_DELAUNAY_MAX_EDGE_LENGTH[] = "MAX_EDGE_LENGTH";
111 constexpr char COMMAND_SF_ARITHMETIC[] = "SF_ARITHMETIC";
112 constexpr char COMMAND_SF_OP[] = "SF_OP";
113 constexpr char COMMAND_RENAME_SF[] = "RENAME_SF";
114 constexpr char COMMAND_COORD_TO_SF[] = "COORD_TO_SF";
115 constexpr char COMMAND_EXTRACT_VERTICES[] = "EXTRACT_VERTICES";
116 constexpr char COMMAND_ICP[] = "ICP";
117 constexpr char COMMAND_ICP_REFERENCE_IS_FIRST[] = "REFERENCE_IS_FIRST";
118 constexpr char COMMAND_ICP_MIN_ERROR_DIIF[] = "MIN_ERROR_DIFF";
119 constexpr char COMMAND_ICP_ITERATION_COUNT[] = "ITER";
120 constexpr char COMMAND_ICP_OVERLAP[] = "OVERLAP";
121 constexpr char COMMAND_ICP_ADJUST_SCALE[] = "ADJUST_SCALE";
122 constexpr char COMMAND_ICP_RANDOM_SAMPLING_LIMIT[] = "RANDOM_SAMPLING_LIMIT";
123 constexpr char COMMAND_ICP_ENABLE_FARTHEST_REMOVAL[] = "FARTHEST_REMOVAL";
124 constexpr char COMMAND_ICP_USE_MODEL_SF_AS_WEIGHT[] = "MODEL_SF_AS_WEIGHTS";
125 constexpr char COMMAND_ICP_USE_DATA_SF_AS_WEIGHT[] = "DATA_SF_AS_WEIGHTS";
126 constexpr char COMMAND_ICP_ROT[] = "ROT";
127 constexpr char COMMAND_PLY_EXPORT_FORMAT[] = "PLY_EXPORT_FMT";
128 constexpr char COMMAND_COMPUTE_GRIDDED_NORMALS[] = "COMPUTE_NORMALS";
129 constexpr char COMMAND_INVERT_NORMALS[] = "INVERT_NORMALS";
130 constexpr char COMMAND_COMPUTE_OCTREE_NORMALS[] = "OCTREE_NORMALS";
131 constexpr char COMMAND_CONVERT_NORMALS_TO_DIP[] = "NORMALS_TO_DIP";
132 constexpr char COMMAND_CONVERT_NORMALS_TO_SFS[] = "NORMALS_TO_SFS";
133 constexpr char COMMAND_CLEAR_NORMALS[] = "CLEAR_NORMALS";
134 constexpr char COMMAND_MESH_VOLUME[] = "MESH_VOLUME";
135 constexpr char COMMAND_VOLUME_TO_FILE[] = "TO_FILE";
136 constexpr char COMMAND_SAVE_CLOUDS[] = "SAVE_CLOUDS";
137 constexpr char COMMAND_SAVE_MESHES[] = "SAVE_MESHES";
138 constexpr char COMMAND_AUTO_SAVE[] = "AUTO_SAVE";
139 constexpr char COMMAND_LOG_FILE[] = "LOG_FILE";
140 constexpr char COMMAND_CLEAR[] = "CLEAR";
141 constexpr char COMMAND_CLEAR_CLOUDS[] = "CLEAR_CLOUDS";
142 constexpr char COMMAND_POP_CLOUDS[] = "POP_CLOUDS";
143 constexpr char COMMAND_CLEAR_MESHES[] = "CLEAR_MESHES";
144 constexpr char COMMAND_POP_MESHES[] = "POP_MESHES";
145 constexpr char COMMAND_NO_TIMESTAMP[] = "NO_TIMESTAMP";
146 constexpr char COMMAND_MOMENT[] = "MOMENT";
147 constexpr char COMMAND_FEATURE[] = "FEATURE";
148 constexpr char COMMAND_RGB_CONVERT_TO_SF[] = "RGB_CONVERT_TO_SF";
149 constexpr char COMMAND_FLIP_TRIANGLES[] = "FLIP_TRI";
150 constexpr char COMMAND_DEBUG[] = "DEBUG";
151 constexpr char COMMAND_VERBOSITY[] = "VERBOSITY";
152 constexpr char COMMAND_FILTER[] = "FILTER";
153 
154 // options / modifiers
155 constexpr char COMMAND_MAX_THREAD_COUNT[] = "MAX_TCOUNT";
156 constexpr char OPTION_ALL_AT_ONCE[] = "ALL_AT_ONCE";
157 constexpr char OPTION_ON[] = "ON";
158 constexpr char OPTION_OFF[] = "OFF";
159 constexpr char OPTION_FILE_NAMES[] = "FILE";
160 constexpr char OPTION_ORIENT[] = "ORIENT";
161 constexpr char OPTION_MODEL[] = "MODEL";
162 constexpr char OPTION_FIRST[] = "FIRST";
163 constexpr char OPTION_LAST[] = "LAST";
164 constexpr char OPTION_ALL[] = "ALL";
165 constexpr char OPTION_REGEX[] = "REGEX";
166 constexpr char OPTION_NOT[] = "NOT";
167 constexpr char OPTION_CLOUD[] = "CLOUD";
168 constexpr char OPTION_MESH[] = "MESH";
169 constexpr char OPTION_PERCENT[] = "PERCENT";
170 constexpr char OPTION_NUMBER_OF_POINTS[] = "NUMBER_OF_POINTS";
171 constexpr char OPTION_FORCE[] = "FORCE";
172 constexpr char OPTION_USE_ACTIVE_SF[] = "USE_ACTIVE_SF";
173 constexpr char OPTION_SF[] = "SF";
174 constexpr char OPTION_RGB[] = "RGB";
175 constexpr char OPTION_GAUSSIAN[] = "GAUSSIAN";
176 constexpr char OPTION_BILATERAL[] = "BILATERAL";
177 constexpr char OPTION_MEAN[] = "MEAN";
178 constexpr char OPTION_MEDIAN[] = "MEDIAN";
179 constexpr char OPTION_SIGMA[] = "SIGMA";
180 constexpr char OPTION_SIGMA_SF[] = "SIGMA_SF";
181 constexpr char OPTION_BURNT_COLOR_THRESHOLD[] = "BURNT_COLOR_THRESHOLD";
182 constexpr char OPTION_BLEND_GRAYSCALE[] = "BLEND_GRAYSCALE";
183 
185  int& sfIndex,
186  QString& sfName,
187  bool allowMinusOne = false) {
188  sfName = cmd.arguments().takeFirst();
189  if (sfName.toUpper() == OPTION_LAST) {
190  sfIndex = -2;
191  cmd.print(QObject::tr("SF index: LAST"));
192  } else {
193  bool validInt = false;
194  sfIndex = sfName.toInt(&validInt);
195  if (validInt) {
196  sfName.clear(); // user has provided an index, not a name
197 
198  if (sfIndex < 0) {
199  if (allowMinusOne && sfIndex == -1) {
200  // -1 means 'no active SF'
201  cmd.print(QObject::tr("SF index: none"));
202  } else {
203  // invalid index
204  cmd.warning(
205  QObject::tr("Invalid SF index: %1").arg(sfIndex));
206  return false;
207  }
208  } else {
209  cmd.print(QObject::tr("SF index: %1").arg(sfIndex));
210  }
211  } else {
212  cmd.print(QObject::tr("SF name: '%1'").arg(sfName));
213  sfIndex = -1;
214  }
215  }
216 
217  return true;
218 }
219 
221  int sfIndex,
222  const QString& sfName,
223  bool minusOneMeansCurrent = false) {
224  if (!cloud) {
225  assert(false);
226  return -1;
227  } else if (!cloud->hasScalarFields()) {
228  return -1;
229  } else if (sfIndex == -2) {
230  return static_cast<int>(cloud->getNumberOfScalarFields()) - 1;
231  } else if (sfIndex == -1) {
232  if (!sfName.isEmpty()) // the user has provided a SF name instead of an
233  // index
234  {
235  // check if this cloud has a scalar field with the input name
236  sfIndex = cloud->getScalarFieldIndexByName(
237  sfName.toStdString().c_str());
238  if (sfIndex < 0) {
239  CVLog::Warning(QObject::tr("Cloud %1 has no SF named '%2'")
240  .arg(cloud->getName())
241  .arg(sfName));
242  return -1;
243  }
244  } else if (minusOneMeansCurrent) {
245  return cloud->getCurrentInScalarFieldIndex();
246  } else {
248  QObject::tr("Input scalar field index is invalid: %1")
249  .arg(sfIndex));
250  return -1;
251  }
252  } else if (sfIndex >= static_cast<int>(cloud->getNumberOfScalarFields())) {
253  CVLog::Warning(QObject::tr("Cloud %1 has less scalar fields than the "
254  "SF index (%2/%3)")
255  .arg(cloud->getName())
256  .arg(sfIndex)
257  .arg(cloud->getNumberOfScalarFields()));
258  return -1;
259  }
260 
261  return sfIndex;
262 }
263 
265  int sfIndex,
266  const QString& sfName,
267  bool minusOneMeansCurrent = false) {
268  sfIndex = GetScalarFieldIndex(cloud, sfIndex, sfName, minusOneMeansCurrent);
269  if (sfIndex < 0) {
270  return nullptr;
271  } else {
272  return cloud->getScalarField(sfIndex);
273  }
274 }
275 
277  const QString& keyword)
278  : ccCommandLineInterface::Command(name, keyword) {}
279 
281  ccCommandLineInterface& cmd, QString& defaultExt) {
282  QString fileFilter;
283  defaultExt.clear();
284 
285  if (!cmd.arguments().isEmpty()) {
286  // test if the specified format corresponds to a known file type
287  QString argument = cmd.arguments().front().toUpper();
288  cmd.arguments().pop_front();
289 
290  const FileIOFilter::FilterContainer& filters =
292 
293  for (const auto& filter : filters) {
294  if (argument == filter->getDefaultExtension().toUpper()) {
295  // found
296  fileFilter = filter->getFileFilters(false)
297  .first(); // Take the first 'output' file
298  // filter by default (could we
299  // be smarter?)
300  defaultExt = filter->getDefaultExtension();
301  break;
302  }
303  }
304 
305  // haven't found anything?
306  if (fileFilter.isEmpty()) {
307  cmd.error(QObject::tr("Unhandled format specifier (%1)")
308  .arg(argument));
309  }
310  } else {
311  cmd.error(QObject::tr("Missing file format specifier!"));
312  }
313 
314  return fileFilter;
315 }
316 
318  : CommandChangeOutputFormat(QObject::tr("Change cloud output format"),
320 
322  QString defaultExt;
323  QString fileFilter = getFileFormatFilter(cmd, defaultExt);
324  if (fileFilter.isEmpty()) {
325  return false;
326  }
327 
328  cmd.setCloudExportFormat(fileFilter, defaultExt);
329  cmd.print(QObject::tr("Output export format (clouds) set to: %1")
330  .arg(defaultExt.toUpper()));
331 
332  // default options for ASCII output
333  if (fileFilter == AsciiFilter::GetFileFilter()) {
338  false); // default order: point, color, SF, normal
341  }
342 
343  // look for additional parameters
344  while (!cmd.arguments().empty()) {
345  QString argument = cmd.arguments().front();
348  // local option confirmed, we can move on
349  cmd.arguments().pop_front();
350 
351  if (cmd.arguments().empty()) {
352  return cmd.error(
353  QObject::tr("Missing parameter: extension after '%1'")
355  }
356 
358  cmd.arguments().takeFirst());
359  cmd.print(QObject::tr("New output extension for clouds: %1")
360  .arg(cmd.cloudExportExt()));
362  argument, COMMAND_ASCII_EXPORT_PRECISION)) {
363  // local option confirmed, we can move on
364  cmd.arguments().pop_front();
365 
366  if (cmd.arguments().empty()) {
367  return cmd.error(
368  QObject::tr(
369  "Missing parameter: precision value after '%1'")
371  }
372  bool ok;
373  int precision = cmd.arguments().takeFirst().toInt(&ok);
374  if (!ok || precision < 0) {
375  return cmd.error(
376  QObject::tr("Invalid value for precision! (%1)")
378  }
379 
380  if (fileFilter != AsciiFilter::GetFileFilter()) {
381  cmd.warning(QObject::tr("Argument '%1' is only applicable to "
382  "ASCII format!")
383  .arg(argument));
384  }
385 
389  argument, COMMAND_ASCII_EXPORT_SEPARATOR)) {
390  // local option confirmed, we can move on
391  cmd.arguments().pop_front();
392 
393  if (cmd.arguments().empty()) {
394  return cmd.error(QObject::tr("Missing parameter: separator "
395  "character after '%1'")
397  }
398 
399  if (fileFilter != AsciiFilter::GetFileFilter()) {
400  cmd.warning(QObject::tr("Argument '%1' is only applicable to "
401  "ASCII format!")
402  .arg(argument));
403  }
404 
405  QString separatorStr = cmd.arguments().takeFirst().toUpper();
406  // printf("%s\n",qPrintable(separatorStr));
407  int index = -1;
408  if (separatorStr == "SPACE") {
409  index = 0;
410  } else if (separatorStr == "SEMICOLON") {
411  index = 1;
412  } else if (separatorStr == "COMMA") {
413  index = 2;
414  } else if (separatorStr == "TAB") {
415  index = 3;
416  } else {
417  return cmd.error(QObject::tr("Invalid separator! ('%1')")
418  .arg(separatorStr));
419  }
420 
424  // local option confirmed, we can move on
425  cmd.arguments().pop_front();
426 
427  if (fileFilter != AsciiFilter::GetFileFilter()) {
428  cmd.warning(QObject::tr("Argument '%1' is only applicable to "
429  "ASCII format!")
430  .arg(argument));
431  }
432 
436  // local option confirmed, we can move on
437  cmd.arguments().pop_front();
438 
439  if (fileFilter != AsciiFilter::GetFileFilter()) {
440  cmd.warning(QObject::tr("Argument '%1' is only applicable to "
441  "ASCII format!")
442  .arg(argument));
443  }
444 
446  } else {
447  break; // as soon as we encounter an unrecognized argument, we
448  // break the local loop to go back to the main one!
449  }
450  }
451 
452  return true;
453 }
454 
456  : CommandChangeOutputFormat(QObject::tr("Change mesh output format"),
458 
460  QString defaultExt;
461  QString fileFilter = getFileFormatFilter(cmd, defaultExt);
462  if (fileFilter.isEmpty()) {
463  return false;
464  }
465 
466  cmd.setMeshExportFormat(fileFilter, defaultExt);
467  cmd.print(QObject::tr("Output export format (meshes) set to: %1")
468  .arg(defaultExt.toUpper()));
469 
470  // look for additional parameters
471  while (!cmd.arguments().empty()) {
472  QString argument = cmd.arguments().front();
473 
476  // local option confirmed, we can move on
477  cmd.arguments().pop_front();
478 
479  if (cmd.arguments().empty()) {
480  return cmd.error(
481  QObject::tr("Missing parameter: extension after '%1'")
483  }
484 
486  cmd.arguments().takeFirst());
487  cmd.print(QObject::tr("New output extension for meshes: %1")
488  .arg(cmd.meshExportExt()));
489  } else {
490  break; // as soon as we encounter an unrecognized argument, we
491  // break the local loop to go back to the main one!
492  }
493  }
494 
495  return true;
496 }
497 
499  : CommandChangeOutputFormat(QObject::tr("Change hierarchy output format"),
501 
503  QString defaultExt;
504  QString fileFilter = getFileFormatFilter(cmd, defaultExt);
505  if (fileFilter.isEmpty()) {
506  return false;
507  }
508 
509  cmd.setHierarchyExportFormat(fileFilter, defaultExt);
510  cmd.print(QObject::tr("Output export format (hierarchy) set to: %1")
511  .arg(defaultExt.toUpper()));
512 
513  // look for additional parameters
514  while (!cmd.arguments().empty()) {
515  QString argument = cmd.arguments().front();
516 
519  // local option confirmed, we can move on
520  cmd.arguments().pop_front();
521 
522  if (cmd.arguments().empty()) {
523  return cmd.error(
524  QObject::tr("Missing parameter: extension after '%1'")
526  }
527 
529  cmd.arguments().takeFirst());
530  cmd.print(QObject::tr("New output extension for hierarchies: %1")
531  .arg(cmd.hierarchyExportExt()));
532  } else {
533  break; // as soon as we encounter an unrecognized argument, we
534  // break the local loop to go back to the main one!
535  }
536  }
537 
538  return true;
539 }
540 
542  : ccCommandLineInterface::Command(QObject::tr("Load"), COMMAND_OPEN) {}
543 
545  cmd.print(QObject::tr("[LOADING]"));
546  if (cmd.arguments().empty()) {
547  return cmd.error(
548  QObject::tr("Missing parameter: filename after \"-%1\"")
549  .arg(COMMAND_OPEN));
550  }
551 
552  // optional parameters
553  int skipLines = 0;
555 
556  while (!cmd.arguments().empty()) {
557  QString argument = cmd.arguments().front();
560  // local option confirmed, we can move on
561  cmd.arguments().pop_front();
562 
563  if (cmd.arguments().empty()) {
564  return cmd.error(
565  QObject::tr(
566  "Missing parameter: number of lines after '%1'")
568  }
569 
570  bool ok;
571  skipLines = cmd.arguments().takeFirst().toInt(&ok);
572  if (!ok) {
573  return cmd.error(
574  QObject::tr(
575  "Invalid parameter: number of lines after '%1'")
577  }
578 
579  cmd.print(QObject::tr("Will skip %1 lines").arg(skipLines));
580  } else if (cmd.nextCommandIsGlobalShift()) {
581  // local option confirmed, we can move on
582  cmd.arguments().pop_front();
583 
584  if (!cmd.processGlobalShiftCommand(globalShiftOptions)) {
585  // error message already issued
586  return false;
587  }
588  } else {
589  break;
590  }
591  }
592 
593  if (skipLines >= 0) {
595  }
596 
597  // open specified file
598  QString filename(cmd.arguments().takeFirst());
599  if (!cmd.importFile(filename, globalShiftOptions)) {
600  return false;
601  }
602 
603  return true;
604 }
605 
607  : ccCommandLineInterface::Command(QObject::tr("Clears normals"),
609 
611  cmd.print(QObject::tr("[CLEAR NORMALS]"));
612  if (cmd.clouds().empty() && cmd.meshes().empty()) {
613  return cmd.error(
614  QObject::tr(
615  "No entity loaded (be sure to open at least one file "
616  "with \"-%1 [cloud filename]\" before \"-%2\")")
618  }
619 
620  for (const CLCloudDesc& thisCloudDesc : cmd.clouds()) {
621  ccPointCloud* cloud = thisCloudDesc.pc;
622  if (cloud) {
623  cloud->unallocateNorms();
624  }
625  }
626 
627  for (const CLMeshDesc& thisMeshDesc : cmd.meshes()) {
628  ccMesh* mesh = ccHObjectCaster::ToMesh(thisMeshDesc.mesh);
629  if (!mesh) {
630  assert(false);
631  continue;
632  }
633 
634  mesh->clearTriNormals();
635 
636  if (
637  mesh->getParent() && (mesh->getParent()->isA(CV_TYPES::MESH) /*|| mesh->getParent()->isKindOf(CV_TYPES::PRIMITIVE)*/) // TODO
639  ->getAssociatedCloud() ==
640  mesh->getAssociatedCloud()) {
641  // Can't remove per-vertex normals on a sub mesh!
642  } else {
643  // mesh is alone, we can freely remove normals
644  if (mesh->getAssociatedCloud() &&
646  static_cast<ccPointCloud*>(mesh->getAssociatedCloud())
647  ->unallocateNorms();
648  }
649  }
650  }
651 
652  return true;
653 }
654 
656  : ccCommandLineInterface::Command(QObject::tr("Invert normals"),
658 
660  cmd.print(QObject::tr("[INVERT NORMALS]"));
661 
662  if (cmd.clouds().empty() && cmd.meshes().empty()) {
663  return cmd.error(
664  QObject::tr("No input point cloud or mesh (be sure to open one "
665  "with \"-%1 [cloud filename]\" before \"-%2\")")
667  }
668 
669  for (CLCloudDesc& thisCloudDesc : cmd.clouds()) {
670  ccPointCloud* cloud = thisCloudDesc.pc;
671 
672  if (!cloud->hasNormals()) {
673  cmd.warning(QObject::tr("Cloud %1 has no normals")
674  .arg(cloud->getName()));
675  continue;
676  }
677 
678  cloud->invertNormals();
679 
680  if (cmd.autoSaveMode()) {
681  QString errorStr =
682  cmd.exportEntity(thisCloudDesc, "_INVERTED_NORMALS");
683  if (!errorStr.isEmpty()) {
684  return cmd.error(errorStr);
685  }
686  }
687  }
688 
689  for (CLMeshDesc& thisMeshDesc : cmd.meshes()) {
690  ccMesh* mesh = ccHObjectCaster::ToMesh(thisMeshDesc.mesh);
691  if (!mesh) {
692  assert(false);
693  continue;
694  }
695 
696  if (!mesh->hasNormals()) {
697  cmd.warning(
698  QObject::tr("Mesh %1 has no normals").arg(mesh->getName()));
699  continue;
700  }
701 
702  mesh->invertNormals();
703 
704  if (cmd.autoSaveMode()) {
705  QString errorStr =
706  cmd.exportEntity(thisMeshDesc, "_INVERTED_NORMALS");
707  if (!errorStr.isEmpty()) {
708  return cmd.error(errorStr);
709  }
710  }
711  }
712 
713  return true;
714 }
715 
717  : ccCommandLineInterface::Command(
718  QObject::tr("Compute normals with octree"),
720 
722  cmd.print(QObject::tr("[OCTREE NORMALS CALCULATION]"));
723  if (cmd.clouds().empty()) {
724  return cmd.error(
725  QObject::tr(
726  "No point cloud to compute normals (be sure to open "
727  "one with \"-%1 [cloud filename]\" before \"-%2\")")
729  }
730 
731  if (cmd.arguments().empty()) {
732  return cmd.error(QObject::tr("Missing parameter: radius after \"-%1\"")
734  }
735 
736  float radius = std::numeric_limits<float>::quiet_NaN(); // if this stays
737  QString radiusArg = cmd.arguments().takeFirst();
738  if (radiusArg.toUpper() != "AUTO") {
739  bool ok = false;
740  radius = radiusArg.toFloat(&ok);
741  if (!ok) {
742  return cmd.error(QObject::tr("Invalid radius"));
743  }
744  }
745  cmd.print(QObject::tr("\tRadius: %1").arg(radiusArg));
746 
748  ccNormalVectors::Orientation orientation =
749  ccNormalVectors::Orientation::UNDEFINED;
750 
751  while (!cmd.arguments().isEmpty()) {
752  QString argument = cmd.arguments().front().toUpper();
754  cmd.arguments().takeFirst();
755  if (!cmd.arguments().isEmpty()) {
756  QString orient_argument = cmd.arguments().takeFirst().toUpper();
757  if (orient_argument == "PLUS_ZERO" ||
758  orient_argument == "PLUS_ORIGIN") {
759  orientation = ccNormalVectors::Orientation::PLUS_ORIGIN;
760  } else if (orient_argument == "MINUS_ZERO" ||
761  orient_argument == "MINUS_ORIGIN") {
762  orientation = ccNormalVectors::Orientation::MINUS_ORIGIN;
763  } else if (orient_argument == "PLUS_BARYCENTER") {
764  orientation = ccNormalVectors::Orientation::PLUS_BARYCENTER;
765  } else if (orient_argument == "MINUS_BARYCENTER") {
766  orientation =
767  ccNormalVectors::Orientation::MINUS_BARYCENTER;
768  } else if (orient_argument == "PLUS_X") {
769  orientation = ccNormalVectors::Orientation::PLUS_X;
770  } else if (orient_argument == "MINUS_X") {
771  orientation = ccNormalVectors::Orientation::MINUS_X;
772  } else if (orient_argument == "PLUS_Y") {
773  orientation = ccNormalVectors::Orientation::PLUS_Y;
774  } else if (orient_argument == "MINUS_Y") {
775  orientation = ccNormalVectors::Orientation::MINUS_Y;
776  } else if (orient_argument == "PLUS_Z") {
777  orientation = ccNormalVectors::Orientation::PLUS_Z;
778  } else if (orient_argument == "MINUS_Z") {
779  orientation = ccNormalVectors::Orientation::MINUS_Z;
780  } else if (orient_argument == "PREVIOUS") {
781  orientation = ccNormalVectors::Orientation::PREVIOUS;
782  } else if (orient_argument == "PLUS_SENSOR_ORIGIN") {
783  orientation =
784  ccNormalVectors::Orientation::PLUS_SENSOR_ORIGIN;
785  } else if (orient_argument == "MINUS_SENSOR_ORIGIN") {
786  orientation =
787  ccNormalVectors::Orientation::MINUS_SENSOR_ORIGIN;
788  } else {
789  return cmd.error(QObject::tr("Invalid parameter: unknown "
790  "orientation '%1'")
791  .arg(orient_argument));
792  }
793  } else {
794  return cmd.error(QObject::tr("Missing orientation"));
795  }
796  } else if (ccCommandLineInterface::IsCommand(argument, OPTION_MODEL)) {
797  cmd.arguments().takeFirst();
798  if (!cmd.arguments().isEmpty()) {
799  QString model_arg = cmd.arguments().takeFirst().toUpper();
800  if (model_arg == "LS") {
801  model = CV_LOCAL_MODEL_TYPES::LS;
802  } else if (model_arg == "TRI") {
804  } else if (model_arg == "QUADRIC") {
806  } else {
807  return cmd.error(
808  QObject::tr("Invalid parameter: unknown model '%1'")
809  .arg(model_arg));
810  }
811  } else {
812  return cmd.error(QObject::tr("Missing model"));
813  }
814  } else {
815  break;
816  }
817  }
818 
819  for (const CLCloudDesc& thisCloudDesc : cmd.clouds()) {
820  ccPointCloud* cloud = thisCloudDesc.pc;
821 
822  QScopedPointer<ecvProgressDialog> progressDialog(nullptr);
823  if (!cmd.silentMode()) {
824  progressDialog.reset(
825  new ecvProgressDialog(true, cmd.widgetParent()));
826  progressDialog->setAutoClose(false);
827  }
828 
829  if (!cloud->getOctree()) {
830  if (!cloud->computeOctree(progressDialog.data())) {
831  return cmd.error(
832  QObject::tr("Failed to compute octree for cloud '%1'")
833  .arg(cloud->getName()));
834  }
835  }
836 
837  float thisCloudRadius = radius;
838  if (std::isnan(thisCloudRadius)) {
839  thisCloudRadius = ccNormalVectors::GuessBestRadius(
840  cloud, cloud->getOctree().data());
841  if (thisCloudRadius == 0) {
842  return cmd.error(QObject::tr("Failed to determine best normal "
843  "radius for cloud '%1'")
844  .arg(cloud->getName()));
845  }
846  cmd.print(QObject::tr("\tCloud %1 radius = %2")
847  .arg(cloud->getName())
848  .arg(thisCloudRadius));
849  }
850 
851  cmd.print(QObject::tr("computeNormalsWithOctree started..."));
852  bool success = cloud->computeNormalsWithOctree(
853  model, orientation, thisCloudRadius, progressDialog.data());
854  if (success) {
855  cmd.print(QObject::tr("computeNormalsWithOctree success"));
856  cmd.print(QObject::tr("cloud->hasNormals: %1")
857  .arg(cloud->hasNormals()));
858  } else {
859  return cmd.error(QObject::tr("computeNormalsWithOctree failed"));
860  }
861 
862  cloud->setName(cloud->getName() + QObject::tr(".OctreeNormal"));
863  if (cmd.autoSaveMode()) {
864  CLCloudDesc cloudDesc(cloud, thisCloudDesc.basename,
865  thisCloudDesc.path,
866  thisCloudDesc.indexInFile);
867  QString errorStr = cmd.exportEntity(cloudDesc, "OCTREE_NORMALS");
868  if (!errorStr.isEmpty()) {
869  return cmd.error(errorStr);
870  }
871  }
872  }
873 
874  return true;
875 }
876 
878  : ccCommandLineInterface::Command(
879  QObject::tr("Convert normals to dip and dip. dir."),
881 
883  cmd.print(QObject::tr("[CONVERT NORMALS TO DIP/DIP DIR]"));
884  if (cmd.clouds().empty()) {
885  return cmd.error(
886  QObject::tr("No input point cloud (be sure to open one with "
887  "\"-%1 [cloud filename]\" before \"-%2\")")
889  }
890 
891  for (CLCloudDesc& thisCloudDesc : cmd.clouds()) {
892  ccPointCloud* cloud = thisCloudDesc.pc;
893 
894  if (!cloud->hasNormals()) {
895  cmd.warning(QObject::tr("Cloud %1 has no normals")
896  .arg(cloud->getName()));
897  continue;
898  }
899 
900  ccHObject::Container container;
901  container.push_back(cloud);
903  container,
905  return cmd.error(QObject::tr(
906  "Failed to convert normals to dip and dip direction"));
907  }
908 
909  if (cmd.autoSaveMode()) {
910  QString errorStr =
911  cmd.exportEntity(thisCloudDesc, "_DIP_AND_DIP_DIR");
912  if (!errorStr.isEmpty()) {
913  return cmd.error(errorStr);
914  }
915  }
916  }
917 
918  return true;
919 }
920 
922  : ccCommandLineInterface::Command(
923  QObject::tr("Convert normals to scalar fields"),
925 
927  cmd.print(QObject::tr("[CONVERT NORMALS TO SCALAR FIELD(S)]"));
928  if (cmd.clouds().empty()) {
929  return cmd.error(
930  QObject::tr("No input point cloud (be sure to open one with "
931  "\"-%1 [cloud filename]\" before \"-%2\")")
933  }
934 
935  for (CLCloudDesc& thisCloudDesc : cmd.clouds()) {
936  ccPointCloud* cloud = thisCloudDesc.pc;
937 
938  if (!cloud->hasNormals()) {
939  cmd.warning(QObject::tr("Cloud %1 has no normals")
940  .arg(cloud->getName()));
941  continue;
942  }
943 
944  bool exportDims[3] = {true, true, true};
945 
946  ccHObject::Container container;
947  container.push_back(cloud);
948  if (!ccEntityAction::exportNormalToSF(container, cmd.widgetParent(),
949  exportDims)) {
950  return cmd.error(
951  QObject::tr("Failed to convert normals to scalar fields"));
952  }
953 
954  if (cmd.autoSaveMode()) {
955  QString errorStr = cmd.exportEntity(thisCloudDesc, "_NORM_TO_SF");
956  if (!errorStr.isEmpty()) {
957  return cmd.error(errorStr);
958  }
959  }
960  }
961 
962  return true;
963 }
964 
966  : ccCommandLineInterface::Command(QObject::tr("Subsample"),
968 
970  cmd.print(QObject::tr("[SUBSAMPLING]"));
971  if (cmd.clouds().empty()) {
972  return cmd.error(
973  QObject::tr("No point cloud to resample (be sure to open one "
974  "with \"-%1 [cloud filename]\" before \"-%2\")")
976  }
977 
978  if (cmd.arguments().empty()) {
979  return cmd.error(
980  QObject::tr(
981  "Missing parameter: resampling method after \"-%1\"")
982  .arg(COMMAND_SUBSAMPLE));
983  }
984 
985  QString method = cmd.arguments().takeFirst().toUpper();
986  cmd.print(QObject::tr("\tMethod: ") + method);
987  if (method == "RANDOM") {
988  if (cmd.arguments().empty()) {
989  return cmd.error(QObject::tr("Missing parameter: number of points "
990  "after \"-%1 RANDOM\"")
991  .arg(COMMAND_SUBSAMPLE));
992  }
993 
994  bool ok;
995  unsigned count = cmd.arguments().takeFirst().toUInt(&ok);
996  if (!ok) {
997  return cmd.error(QObject::tr(
998  "Invalid number of points for random resampling!"));
999  }
1000  cmd.print(QObject::tr("\tOutput points: %1").arg(count));
1001 
1002  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
1003  ccPointCloud* cloud = cmd.clouds()[i].pc;
1004  cmd.print(QObject::tr("\tProcessing cloud #%1 (%2)")
1005  .arg(i + 1)
1006  .arg(!cloud->getName().isEmpty()
1007  ? cloud->getName()
1008  : "no name"));
1009 
1010  cloudViewer::ReferenceCloud* refCloud =
1012  cloud, count, cmd.progressDialog());
1013  if (!refCloud) {
1014  return cmd.error(QObject::tr("Subsampling process failed!"));
1015  }
1016  cmd.print(QObject::tr("\tResult: %1 points").arg(refCloud->size()));
1017 
1018  // save output
1019  ccPointCloud* result = cloud->partialClone(refCloud);
1020  delete refCloud;
1021  refCloud = nullptr;
1022 
1023  if (result) {
1024  result->setName(cmd.clouds()[i].pc->getName() +
1025  QObject::tr(".subsampled"));
1026  if (cmd.autoSaveMode()) {
1027  CLCloudDesc cloudDesc(result, cmd.clouds()[i].basename,
1028  cmd.clouds()[i].path,
1029  cmd.clouds()[i].indexInFile);
1030  QString errorStr =
1031  cmd.exportEntity(cloudDesc, "RANDOM_SUBSAMPLED");
1032  if (!errorStr.isEmpty()) {
1033  delete result;
1034  return cmd.error(errorStr);
1035  }
1036  }
1037  // replace current cloud by this one
1038  delete cmd.clouds()[i].pc;
1039  cmd.clouds()[i].pc = result;
1040  cmd.clouds()[i].basename += QObject::tr("_SUBSAMPLED");
1041  // delete result;
1042  // result = 0;
1043  } else {
1044  return cmd.error(QObject::tr("Not enough memory!"));
1045  }
1046  }
1047  } else if (method == "SPATIAL") {
1048  if (cmd.arguments().empty()) {
1049  return cmd.error(QObject::tr("Missing parameter: spatial step "
1050  "after \"-%1 SPATIAL\"")
1051  .arg(COMMAND_SUBSAMPLE));
1052  }
1053  bool ok;
1054  double step = cmd.arguments().takeFirst().toDouble(&ok);
1055  if (!ok || step <= 0) {
1056  return cmd.error(
1057  QObject::tr("Invalid step value for spatial resampling!"));
1058  }
1059  cmd.print(QObject::tr("\tSpatial step: %1").arg(step));
1060 
1061  double sfMinSpacing = 0;
1062  double sfMaxSpacing = 0;
1063  bool useActiveSF = false;
1064  if (!cmd.arguments().empty()) {
1065  if (cmd.arguments().front().toUpper() == OPTION_USE_ACTIVE_SF) {
1066  // enable USE_ACTIVE_SF
1067  useActiveSF = true;
1068  cmd.arguments().pop_front();
1069  if (cmd.arguments().size() >= 2) {
1070  bool validMin = false;
1071  sfMinSpacing =
1072  cmd.arguments().takeFirst().toDouble(&validMin);
1073  bool validMax = false;
1074  sfMaxSpacing =
1075  cmd.arguments().takeFirst().toDouble(&validMax);
1076  if (!validMin || !validMax || sfMinSpacing < 0 ||
1077  sfMaxSpacing < 0) {
1078  return cmd.error(
1079  QObject::tr(
1080  "Invalid parameters: Two positive "
1081  "decimal number required after '%1'")
1082  .arg(OPTION_USE_ACTIVE_SF));
1083  }
1084  } else {
1085  return cmd.error(
1086  QObject::tr("Missing parameters: Two positive "
1087  "decimal number required after '%1'")
1088  .arg(OPTION_USE_ACTIVE_SF));
1089  }
1090  }
1091  }
1092 
1093  for (CLCloudDesc& desc : cmd.clouds()) {
1094  cmd.print(QObject::tr("\tProcessing cloud %1")
1095  .arg(!desc.pc->getName().isEmpty()
1096  ? desc.pc->getName()
1097  : "no name"));
1098 
1100  false);
1101 
1102  // handle Use Active SF on each cloud
1103  if (useActiveSF) {
1104  // look for the min and max sf values
1105  ccScalarField* sf = desc.pc->getCurrentDisplayedScalarField();
1106  if (!sf) {
1107  // warn the user, not use active SF and keep going
1108  cmd.warning(
1109  QObject::tr(
1110  "\tCan't use 'Use active SF': no active "
1111  "scalar field. Set one with '-%1'")
1112  .arg(COMMAND_SET_ACTIVE_SF));
1113  } else {
1114  // found active scalar field
1115  ScalarType sfMin = NAN_VALUE;
1116  ScalarType sfMax = NAN_VALUE;
1117  if (sf->countValidValues() > 0) {
1118  if (!ccScalarField::ValidValue(sfMin) ||
1119  sfMin > sf->getMin())
1120  sfMin = sf->getMin();
1121  if (!ccScalarField::ValidValue(sfMax) ||
1122  sfMax < sf->getMax())
1123  sfMax = sf->getMax();
1124  if (!ccScalarField::ValidValue(sfMin) ||
1125  !ccScalarField::ValidValue(sfMax)) {
1126  // warn the user, don't use 'Use Active SF' and keep
1127  // going
1128  cmd.warning(
1129  QObject::tr("\tCan't use 'Use active SF': "
1130  "scalar field '%1' has invalid "
1131  "min/max values.")
1132  .arg(QString::fromStdString(
1133  sf->getName())));
1134  } else {
1135  // everything validated use acitve SF for modulation
1136  // implementation of modParams.a/b values come from
1137  // MainWindow::doActionSubsample()
1138  modParams.enabled = true;
1139 
1140  double deltaSF = static_cast<double>(sfMax) -
1141  static_cast<double>(sfMin);
1142 
1143  if (cloudViewer::GreaterThanEpsilon(deltaSF)) {
1144  modParams.a =
1145  (sfMaxSpacing - sfMinSpacing) / deltaSF;
1146  modParams.b =
1147  sfMinSpacing - modParams.a * sfMin;
1148  } else {
1149  modParams.a = 0.0;
1150  modParams.b = sfMin;
1151  }
1152  cmd.print(QObject::tr("\tUse active SF: "
1153  "enabled\n\t\tSpacing at SF "
1154  "min (%1): %2\n\t\tSpacing "
1155  "at SF max (%3): %4")
1156  .arg(sfMin)
1157  .arg(sfMinSpacing)
1158  .arg(sfMax)
1159  .arg(sfMaxSpacing));
1160  }
1161  } else {
1162  // warn the user, not use Use Active SF and keep going
1163  cmd.warning(QObject::tr("\tCan't use 'Use active SF': "
1164  "scalar field '%2' does not "
1165  "have any valid value.")
1166  .arg(COMMAND_SET_ACTIVE_SF)
1167  .arg(QString::fromStdString(
1168  sf->getName())));
1169  }
1170  }
1171  }
1172 
1173  if (useActiveSF && !modParams.enabled) {
1174  cmd.print(
1175  QObject::tr("\t'Use active SF' disabled. Falling back "
1176  "to constant spacing."));
1177  }
1178 
1179  cloudViewer::ReferenceCloud* refCloud =
1181  desc.pc, static_cast<PointCoordinateType>(step),
1182  modParams, nullptr, cmd.progressDialog());
1183  if (!refCloud) {
1184  return cmd.error("Subsampling process failed!");
1185  }
1186  cmd.print(QObject::tr("\tResult: %1 points").arg(refCloud->size()));
1187 
1188  // save output
1189  ccPointCloud* result = desc.pc->partialClone(refCloud);
1190  delete refCloud;
1191  refCloud = nullptr;
1192 
1193  if (result) {
1194  result->setName(desc.pc->getName() +
1195  QObject::tr(".subsampled"));
1196  if (cmd.autoSaveMode()) {
1197  CLCloudDesc newDesc(result, desc.basename, desc.path,
1198  desc.indexInFile);
1199  QString errorStr =
1200  cmd.exportEntity(newDesc, "SPATIAL_SUBSAMPLED");
1201  if (!errorStr.isEmpty()) {
1202  delete result;
1203  return cmd.error(errorStr);
1204  }
1205  }
1206  // replace current cloud by this one
1207  delete desc.pc;
1208  desc.pc = result;
1209  desc.basename += "_SPATIAL_SUBSAMPLED";
1210  } else {
1211  return cmd.error(QObject::tr("Not enough memory!"));
1212  }
1213  }
1214  } else if (method == "OCTREE") {
1215  if (cmd.arguments().empty()) {
1216  return cmd.error(QObject::tr("Missing parameter: octree level "
1217  "after \"-%1 OCTREE\"")
1218  .arg(COMMAND_SUBSAMPLE));
1219  }
1220 
1221  bool ok = false;
1222  int octreeLevel = cmd.arguments().takeFirst().toInt(&ok);
1223  if (!ok || octreeLevel < 1 ||
1225  return cmd.error(QObject::tr("Invalid octree level!"));
1226  }
1227  cmd.print(QObject::tr("\tOctree level: %1").arg(octreeLevel));
1228 
1229  QScopedPointer<ecvProgressDialog> progressDialog(nullptr);
1230  if (!cmd.silentMode()) {
1231  progressDialog.reset(
1232  new ecvProgressDialog(false, cmd.widgetParent()));
1233  progressDialog->setAutoClose(false);
1234  }
1235 
1236  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
1237  ccPointCloud* cloud = cmd.clouds()[i].pc;
1238  cmd.print(QObject::tr("\tProcessing cloud #%1 (%2)")
1239  .arg(i + 1)
1240  .arg(!cloud->getName().isEmpty()
1241  ? cloud->getName()
1242  : "no name"));
1243 
1246  cloud, static_cast<unsigned char>(octreeLevel),
1248  NEAREST_POINT_TO_CELL_CENTER,
1249  progressDialog.data());
1250  if (!refCloud) {
1251  return cmd.error(QObject::tr("Subsampling process failed!"));
1252  }
1253  cmd.print(QObject::tr("\tResult: %1 points").arg(refCloud->size()));
1254 
1255  // save output
1256  ccPointCloud* result = cloud->partialClone(refCloud);
1257  delete refCloud;
1258  refCloud = nullptr;
1259 
1260  if (result) {
1261  result->setName(cmd.clouds()[i].pc->getName() +
1262  QObject::tr(".subsampled"));
1263  if (cmd.autoSaveMode()) {
1264  CLCloudDesc cloudDesc(result, cmd.clouds()[i].basename,
1265  cmd.clouds()[i].path,
1266  cmd.clouds()[i].indexInFile);
1267  QString errorStr = cmd.exportEntity(
1268  cloudDesc, QObject::tr("OCTREE_LEVEL_%1_SUBSAMPLED")
1269  .arg(octreeLevel));
1270  if (!errorStr.isEmpty()) {
1271  delete result;
1272  return cmd.error(errorStr);
1273  }
1274  }
1275  // replace current cloud by this one
1276  delete cmd.clouds()[i].pc;
1277  cmd.clouds()[i].pc = result;
1278  cmd.clouds()[i].basename += QObject::tr("_SUBSAMPLED");
1279  // delete result;
1280  // result = 0;
1281  } else {
1282  return cmd.error(QObject::tr("Not enough memory!"));
1283  }
1284  }
1285 
1286  if (progressDialog) {
1287  progressDialog->close();
1288  QCoreApplication::processEvents();
1289  }
1290  } else {
1291  return cmd.error(QObject::tr("Unknown method!"));
1292  }
1293 
1294  return true;
1295 }
1296 
1298  : ccCommandLineInterface::Command(QObject::tr("ExtractCCs"),
1299  COMMAND_EXTRACT_CC) {}
1300 
1302  cmd.print(QObject::tr("[CONNECTED COMPONENTS EXTRACTION]"));
1303  if (cmd.clouds().empty()) {
1304  return cmd.error(
1305  QObject::tr("No point cloud loaded (be sure to open one with "
1306  "\"-%1 [cloud filename]\" before \"-%2\")")
1308  }
1309 
1310  // octree level
1311  if (cmd.arguments().empty()) {
1312  return cmd.error(
1313  QObject::tr("Missing parameter: octree level after \"-%1\"")
1314  .arg(COMMAND_EXTRACT_CC));
1315  }
1316  bool ok;
1317  unsigned char octreeLevel =
1318  std::min<unsigned char>(cmd.arguments().takeFirst().toUShort(&ok),
1320  if (!ok) {
1321  return cmd.error(QObject::tr("Invalid octree level!"));
1322  }
1323  cmd.print(QObject::tr("\tOctree level: %1").arg(octreeLevel));
1324 
1325  // min number of points
1326  if (cmd.arguments().empty()) {
1327  return cmd.error(
1328  QObject::tr("Missing parameter: minimum number of points per "
1329  "component after \"-%1 [octree level]\"")
1330  .arg(COMMAND_EXTRACT_CC));
1331  }
1332  unsigned minPointCount = cmd.arguments().takeFirst().toUInt(&ok);
1333  if (!ok) {
1334  return cmd.error(QObject::tr("Invalid min. number of points!"));
1335  }
1336  cmd.print(QObject::tr("\tMin number of points per component: %1")
1337  .arg(minPointCount));
1338 
1339  try {
1340  QScopedPointer<ecvProgressDialog> progressDialog(nullptr);
1341  if (!cmd.silentMode()) {
1342  progressDialog.reset(
1343  new ecvProgressDialog(false, cmd.widgetParent()));
1344  progressDialog->setAutoClose(false);
1345  }
1346 
1347  std::vector<CLCloudDesc> inputClouds = cmd.clouds();
1348  cmd.clouds().resize(0);
1349  for (size_t i = 0; i < inputClouds.size(); ++i) {
1350  ccPointCloud* cloud = inputClouds[i].pc;
1351  cmd.print(QObject::tr("\tProcessing cloud #%1 (%2)")
1352  .arg(i + 1)
1353  .arg(!cloud->getName().isEmpty()
1354  ? cloud->getName()
1355  : "no name"));
1356 
1357  // we create/activate CCs label scalar field
1358  int sfIdx = cloud->getScalarFieldIndexByName(
1360  if (sfIdx < 0) {
1361  sfIdx = cloud->addScalarField(
1363  }
1364  if (sfIdx < 0) {
1365  cmd.error(QObject::tr(
1366  "Couldn't allocate a new scalar field for computing CC "
1367  "labels! Try to free some memory ..."));
1368  continue;
1369  }
1370  cloud->setCurrentScalarField(sfIdx);
1371 
1372  // try to label all CCs
1373  int componentCount = cloudViewer::AutoSegmentationTools::
1375  cloud, static_cast<unsigned char>(octreeLevel),
1376  false, progressDialog.data());
1377 
1378  if (componentCount == 0) {
1379  cmd.error(QObject::tr("No component found!"));
1380  continue;
1381  }
1382 
1386  extractConnectedComponents(cloud, components);
1387  cloud->deleteScalarField(sfIdx);
1388  sfIdx = -1;
1389 
1390  if (!success) {
1391  cmd.warning(QObject::tr(
1392  "An error occurred (failed to finish the extraction)"));
1393  continue;
1394  }
1395 
1396  // we create "real" point clouds for all input components
1397  int realIndex = 0;
1398  for (size_t j = 0; j < components.size(); ++j) {
1399  cloudViewer::ReferenceCloud* compIndexes = components[j];
1400 
1401  // if it has enough points
1402  if (compIndexes->size() >= minPointCount) {
1403  // we create a new entity
1404  ccPointCloud* compCloud = cloud->partialClone(compIndexes);
1405  if (compCloud) {
1406  //'shift on load' information
1407  compCloud->setGlobalShift(cloud->getGlobalShift());
1408  compCloud->setGlobalScale(cloud->getGlobalScale());
1409  compCloud->setName(QString(cloud->getName() + "_CC#%1")
1410  .arg(j + 1));
1411 
1412  CLCloudDesc cloudDesc(
1413  compCloud,
1414  inputClouds[i].basename +
1415  QObject::tr("_COMPONENT_%1")
1416  .arg(++realIndex),
1417  inputClouds[i].path);
1418  if (cmd.autoSaveMode()) {
1419  QString errorStr = cmd.exportEntity(
1420  cloudDesc, QString(), nullptr,
1422  ForceNoTimestamp);
1423  if (!errorStr.isEmpty()) {
1424  cmd.error(errorStr);
1425  }
1426  }
1427  // add cloud to the current pool
1428  cmd.clouds().push_back(cloudDesc);
1429  } else {
1430  cmd.warning(QObject::tr("Failed to create component "
1431  "#%1! (not enough memory)")
1432  .arg(j + 1));
1433  }
1434  }
1435 
1436  delete compIndexes;
1437  compIndexes = nullptr;
1438  }
1439 
1440  components.clear();
1441 
1442  if (cmd.clouds().empty()) {
1443  cmd.error(QObject::tr(
1444  "No component was created! Check the minimum size..."));
1445  } else {
1446  cmd.print(QObject::tr("%1 component(s) were created")
1447  .arg(cmd.clouds().size()));
1448  }
1449  }
1450 
1451  if (progressDialog) {
1452  progressDialog->close();
1453  QCoreApplication::processEvents();
1454  }
1455  } catch (const std::bad_alloc&) {
1456  cmd.error(QObject::tr("Not enough memory"));
1457  return false;
1458  }
1459 
1460  return true;
1461 }
1462 
1464  : ccCommandLineInterface::Command(QObject::tr("Curvature"),
1465  COMMAND_CURVATURE) {}
1466 
1468  cmd.print(QObject::tr("[CURVATURE]"));
1469 
1470  if (cmd.arguments().empty()) {
1471  return cmd.error(
1472  QObject::tr("Missing parameter: curvature type after \"-%1\"")
1473  .arg(COMMAND_CURVATURE));
1474  }
1475 
1476  QString curvTypeStr = cmd.arguments().takeFirst().toUpper();
1479  if (curvTypeStr == "MEAN") {
1480  // curvType = cloudViewer::Neighbourhood::MEAN_CURV;
1481  } else if (curvTypeStr == "GAUSS") {
1483  } else if (curvTypeStr == "NORMAL_CHANGE") {
1485  } else {
1486  return cmd.error(QObject::tr("Invalid curvature type after \"-%1\". "
1487  "Got '%2' instead of MEAN or GAUSS.")
1488  .arg(COMMAND_CURVATURE, curvTypeStr));
1489  }
1490 
1491  if (cmd.arguments().empty()) {
1492  return cmd.error(QObject::tr(
1493  "Missing parameter: kernel size after curvature type"));
1494  }
1495 
1496  bool paramOk = false;
1497  QString kernelStr = cmd.arguments().takeFirst();
1498  PointCoordinateType kernelSize =
1499  static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
1500  if (!paramOk) {
1501  return cmd.error(
1502  QObject::tr("Failed to read a numerical parameter: kernel size "
1503  "(after curvature type). Got '%1' instead.")
1504  .arg(kernelStr));
1505  }
1506  cmd.print(QObject::tr("\tKernel size: %1").arg(kernelSize));
1507 
1508  if (cmd.clouds().empty()) {
1509  return cmd.error(QObject::tr("No point cloud on which to compute "
1510  "curvature! (be sure to open one with "
1511  "\"-%1 [cloud filename]\" before \"-%2\")")
1513  }
1514 
1515  // Call MainWindow generic method
1516  ccHObject::Container entities;
1517  entities.resize(cmd.clouds().size());
1518  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
1519  entities[i] = cmd.clouds()[i].pc;
1520  }
1521 
1524  kernelSize, entities, nullptr, cmd.widgetParent())) {
1525  // save output
1526  if (cmd.autoSaveMode() &&
1527  !cmd.saveClouds(QObject::tr("%1_CURVATURE_KERNEL_%2")
1528  .arg(curvTypeStr)
1529  .arg(kernelSize))) {
1530  return false;
1531  }
1532  }
1533  return true;
1534 }
1535 
1536 static bool ReadDensityType(
1539  if (cmd.arguments().empty()) {
1540  return cmd.error(QObject::tr("Missing parameter: density type after "
1541  "\"-%1\" (KNN/SURFACE/VOLUME)")
1542  .arg(COMMAND_DENSITY_TYPE));
1543  }
1544 
1545  // read option confirmed, we can move on
1546  QString typeArg = cmd.arguments().takeFirst().toUpper();
1547  if (typeArg == "KNN") {
1549  } else if (typeArg == "SURFACE") {
1551  } else if (typeArg == "VOLUME") {
1553  } else {
1554  return cmd.error(
1555  QObject::tr("Invalid parameter: density type is expected after "
1556  "\"-%1\" (KNN/SURFACE/VOLUME)")
1557  .arg(COMMAND_DENSITY_TYPE));
1558  }
1559 
1560  return true;
1561 }
1562 
1564  : ccCommandLineInterface::Command(QObject::tr("ApproxDensity"),
1566 
1568  cmd.print(QObject::tr("[APPROX DENSITY]"));
1569  if (cmd.clouds().empty()) {
1570  return cmd.error(
1571  QObject::tr("No point cloud on which to compute approx. "
1572  "density! (be sure to open one with \"-%1 [cloud "
1573  "filename]\" before \"-%2\")")
1575  }
1576 
1577  // Call MainWindow generic method
1578  ccHObject::Container entities;
1579  entities.resize(cmd.clouds().size());
1580  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
1581  entities[i] = cmd.clouds()[i].pc;
1582  }
1583 
1584  // optional parameter: density type
1587  if (!cmd.arguments().empty()) {
1588  QString argument = cmd.arguments().front();
1590  // local option confirmed, we can move on
1591  cmd.arguments().pop_front();
1592  if (cmd.arguments().empty()) {
1593  return cmd.error(
1594  QObject::tr("Missing parameter: density type after "
1595  "\"-%1\" (KNN/SURFACE/VOLUME)")
1596  .arg(COMMAND_DENSITY_TYPE));
1597  }
1598  // read option confirmed, we can move on
1599  if (!ReadDensityType(cmd, densityType)) {
1600  return false;
1601  }
1602  }
1603  }
1604 
1607  densityType, 0, entities, nullptr, cmd.widgetParent())) {
1608  // save output
1609  if (cmd.autoSaveMode() && !cmd.saveClouds("APPROX_DENSITY")) {
1610  return false;
1611  }
1612  }
1613 
1614  return true;
1615 }
1616 
1618  : ccCommandLineInterface::Command(QObject::tr("Density"), COMMAND_DENSITY) {
1619 }
1620 
1622  cmd.print(QObject::tr("[DENSITY]"));
1623 
1624  if (cmd.arguments().empty()) {
1625  return cmd.error(
1626  QObject::tr("Missing parameter: sphere radius after \"-%1\"")
1627  .arg(COMMAND_DENSITY));
1628  }
1629 
1630  bool paramOk = false;
1631  QString kernelStr = cmd.arguments().takeFirst();
1632  PointCoordinateType kernelSize =
1633  static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
1634  if (!paramOk) {
1635  return cmd.error(
1636  QObject::tr("Failed to read a numerical parameter: sphere "
1637  "radius (after \"-%1\"). Got '%2' instead.")
1638  .arg(COMMAND_DENSITY, kernelStr));
1639  }
1640  cmd.print(QObject::tr("\tSphere radius: %1").arg(kernelSize));
1641 
1642  // optional parameter: density type
1645  if (!cmd.arguments().empty()) {
1646  QString argument = cmd.arguments().front();
1648  // local option confirmed, we can move on
1649  cmd.arguments().pop_front();
1650  if (cmd.arguments().empty()) {
1651  return cmd.error(
1652  QObject::tr("Missing parameter: density type after "
1653  "\"-%1\" (KNN/SURFACE/VOLUME)")
1654  .arg(COMMAND_DENSITY_TYPE));
1655  }
1656  // read option confirmed, we can move on
1657  if (!ReadDensityType(cmd, densityType)) {
1658  return false;
1659  }
1660  }
1661  }
1662 
1663  if (cmd.clouds().empty()) {
1664  return cmd.error(QObject::tr("No point cloud on which to compute "
1665  "density! (be sure to open one with \"-%1 "
1666  "[cloud filename]\" before \"-%2\")")
1667  .arg(COMMAND_OPEN, COMMAND_DENSITY));
1668  }
1669 
1670  // Call MainWindow generic method
1671  ccHObject::Container entities;
1672  entities.resize(cmd.clouds().size());
1673  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
1674  entities[i] = cmd.clouds()[i].pc;
1675  }
1676 
1679  densityType, kernelSize, entities, nullptr,
1680  cmd.widgetParent())) {
1681  // save output
1682  if (cmd.autoSaveMode() && !cmd.saveClouds("DENSITY")) {
1683  return false;
1684  }
1685  }
1686 
1687  return true;
1688 }
1689 
1691  : ccCommandLineInterface::Command(QObject::tr("SF gradient"),
1693 
1695  cmd.print(QObject::tr("[SF GRADIENT]"));
1696 
1697  if (cmd.arguments().empty()) {
1698  return cmd.error(QObject::tr("Missing parameter: boolean (whether SF "
1699  "is euclidean or not) after \"-%1\"")
1700  .arg(COMMAND_SF_GRADIENT));
1701  }
1702 
1703  QString euclideanStr = cmd.arguments().takeFirst().toUpper();
1704  bool euclidean = false;
1705  if (euclideanStr == "TRUE") {
1706  euclidean = true;
1707  } else if (euclideanStr != "FALSE") {
1708  return cmd.error(QObject::tr("Invalid boolean value after \"-%1\". Got "
1709  "'%2' instead of TRUE or FALSE.")
1710  .arg(COMMAND_SF_GRADIENT, euclideanStr));
1711  }
1712 
1713  if (cmd.clouds().empty()) {
1714  return cmd.error(QObject::tr("No point cloud on which to compute SF "
1715  "gradient! (be sure to open one with "
1716  "\"-%1 [cloud filename]\" before \"-%2\")")
1718  }
1719 
1720  // Call MainWindow generic method
1721  void* additionalParameters[1] = {&euclidean};
1722  ccHObject::Container entities;
1723  entities.reserve(cmd.clouds().size());
1724  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
1725  unsigned sfCount = cmd.clouds()[i].pc->getNumberOfScalarFields();
1726  if (sfCount == 0) {
1727  cmd.warning(QObject::tr("cmd.warning: cloud '%1' has no scalar "
1728  "field (it will be ignored)")
1729  .arg(cmd.clouds()[i].pc->getName()));
1730  } else {
1731  if (sfCount > 1) {
1732  cmd.warning(
1733  QObject::tr(
1734  "cmd.warning: cloud '%1' has several scalar "
1735  "fields (the active one will be used by "
1736  "default, or the first one if none is active)")
1737  .arg(cmd.clouds()[i].pc->getName()));
1738  }
1739 
1740  int activeSFIndex =
1741  cmd.clouds()[i].pc->getCurrentOutScalarFieldIndex();
1742  if (activeSFIndex < 0) {
1743  activeSFIndex = 0;
1744  }
1745 
1746  cmd.clouds()[i].pc->setCurrentDisplayedScalarField(activeSFIndex);
1747 
1748  entities.push_back(cmd.clouds()[i].pc);
1749  }
1750  }
1751 
1754  cmd.widgetParent(), additionalParameters)) {
1755  // save output
1756  if (cmd.autoSaveMode() &&
1757  !cmd.saveClouds(euclidean ? "EUCLIDEAN_SF_GRAD" : "SF_GRAD")) {
1758  return false;
1759  }
1760  }
1761 
1762  return true;
1763 }
1764 
1766  : ccCommandLineInterface::Command(QObject::tr("Roughness"),
1767  COMMAND_ROUGHNESS) {}
1768 
1770  if (cmd.arguments().empty()) {
1771  return cmd.error(
1772  QObject::tr("Missing parameter: kernel size after \"-%1\"")
1773  .arg(COMMAND_ROUGHNESS));
1774  }
1775 
1776  bool paramOk = false;
1777  QString kernelStr = cmd.arguments().takeFirst();
1778  PointCoordinateType kernelSize =
1779  static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
1780  if (!paramOk) {
1781  return cmd.error(
1782  QObject::tr("Failed to read a numerical parameter: kernel size "
1783  "(after \"-%1\"). Got '%2' instead.")
1784  .arg(COMMAND_ROUGHNESS, kernelStr));
1785  }
1786  cmd.print(QObject::tr("\tKernel size: %1").arg(kernelSize));
1787 
1788  // optional argument
1789  CCVector3 roughnessUpDir;
1790  CCVector3* _roughnessUpDir = nullptr;
1791  if (cmd.arguments().size() >= 4) {
1792  QString nextArg = cmd.arguments().first();
1793  if (nextArg.startsWith('-') &&
1794  nextArg.mid(1).toUpper() == COMMAND_ROUGHNESS_UP_DIR) {
1795  // option confirmed
1796  cmd.arguments().takeFirst();
1797  QString xStr = cmd.arguments().takeFirst();
1798  QString yStr = cmd.arguments().takeFirst();
1799  QString zStr = cmd.arguments().takeFirst();
1800  bool okX = false, okY = false, okZ = false;
1801  roughnessUpDir.x =
1802  static_cast<PointCoordinateType>(xStr.toDouble(&okX));
1803  roughnessUpDir.y =
1804  static_cast<PointCoordinateType>(yStr.toDouble(&okY));
1805  roughnessUpDir.z =
1806  static_cast<PointCoordinateType>(zStr.toDouble(&okZ));
1807  if (!okX || !okY || !okZ) {
1808  return cmd.error(
1809  QObject::tr("Invalid 'up direction' vector after "
1810  "option -%1 (3 coordinates expected)")
1811  .arg(COMMAND_ROUGHNESS_UP_DIR));
1812  }
1813  _roughnessUpDir = &roughnessUpDir;
1814  }
1815  }
1816 
1817  if (cmd.clouds().empty()) {
1818  return cmd.error(QObject::tr("No point cloud on which to compute "
1819  "roughness! (be sure to open one with "
1820  "\"-%1 [cloud filename]\" before \"-%2\")")
1822  }
1823 
1824  // Call MainWindow generic method
1825  ccHObject::Container entities;
1826  entities.resize(cmd.clouds().size());
1827  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
1828  entities[i] = cmd.clouds()[i].pc;
1829  }
1830 
1833  entities, _roughnessUpDir, cmd.widgetParent())) {
1834  // save output
1835  if (cmd.autoSaveMode() &&
1836  !cmd.saveClouds(
1837  QObject::tr("ROUGHNESS_KERNEL_%2").arg(kernelSize))) {
1838  return false;
1839  }
1840  }
1841 
1842  return true;
1843 }
1844 
1846  : ccCommandLineInterface::Command(QObject::tr("Apply Transformation"),
1848 
1850  cmd.print(QObject::tr("[APPLY TRANSFORMATION]"));
1851 
1852  if (cmd.arguments().empty()) {
1853  return cmd.error(
1854  QObject::tr(
1855  "Missing parameter: transformation file after \"-%1\"")
1857  }
1858 
1859  QString filename = cmd.arguments().takeFirst();
1860  ccGLMatrix mat;
1861  if (!mat.fromAsciiFile(filename)) {
1862  return cmd.error(
1863  QObject::tr("Failed to read transformation matrix file '%1'!")
1864  .arg(filename));
1865  }
1866 
1867  cmd.print(QObject::tr("Transformation:\n") + mat.toString(6));
1868 
1869  if (cmd.clouds().empty() && cmd.meshes().empty()) {
1870  return cmd.error(
1871  QObject::tr("No entity on which to apply the transformation! "
1872  "(be sure to open one with \"-%1 [filename]\" "
1873  "before \"-%2\")")
1875  }
1876 
1877  // apply transformation
1878  if (!cmd.clouds().empty()) {
1879  for (const CLCloudDesc& desc : cmd.clouds()) {
1880  desc.pc->applyGLTransformation_recursive(&mat);
1881  desc.pc->setName(desc.pc->getName() + ".transformed");
1882  }
1883  // save output
1884  if (cmd.autoSaveMode() && !cmd.saveClouds("TRANSFORMED")) {
1885  return false;
1886  }
1887  }
1888  if (!cmd.meshes().empty()) {
1889  for (const CLMeshDesc& desc : cmd.meshes()) {
1890  desc.mesh->applyGLTransformation_recursive(&mat);
1891  desc.mesh->setName(desc.mesh->getName() + ".transformed");
1892  }
1893  // save output
1894  if (cmd.autoSaveMode() && !cmd.saveMeshes("TRANSFORMED")) {
1895  return false;
1896  }
1897  }
1898 
1899  return true;
1900 }
1901 
1903  : ccCommandLineInterface::Command(QObject::tr("Drop global shift"),
1905 
1907  cmd.print(QObject::tr("[DROP GLOBAL SHIFT]"));
1908 
1909  if (cmd.clouds().empty() && cmd.meshes().empty()) {
1910  return cmd.error(QObject::tr("No loaded entity! (be sure to open one "
1911  "with \"-%1 [filename]\" before \"-%2\")")
1913  }
1914 
1915  // process clouds
1916  for (const CLCloudDesc& desc : cmd.clouds()) {
1917  desc.pc->setGlobalShift(0, 0, 0);
1918  }
1919 
1920  for (const CLMeshDesc& desc : cmd.meshes()) {
1921  bool isLocked = false;
1922  ccShiftedObject* shifted =
1923  ccHObjectCaster::ToShifted(desc.mesh, &isLocked);
1924  if (shifted && !isLocked) {
1925  shifted->setGlobalShift(0, 0, 0);
1926  }
1927  }
1928 
1929  return true;
1930 }
1931 
1933  : ccCommandLineInterface::Command(QObject::tr("SF color scale"),
1935 
1937  cmd.print(QObject::tr("[SF COLOR SCALE]"));
1938 
1939  if (cmd.arguments().empty()) {
1940  return cmd.error(
1941  QObject::tr("Missing parameter: color scale file after \"-%1\"")
1942  .arg(COMMAND_SF_COLOR_SCALE));
1943  }
1944 
1945  QString filename = cmd.arguments().takeFirst();
1946 
1948 
1949  if (!scale) {
1950  return cmd.error(QObject::tr("Failed to read color scale file '%1'!")
1951  .arg(filename));
1952  }
1953 
1954  if (cmd.clouds().empty()) {
1955  return cmd.error(QObject::tr("No point cloud on which to change the SF "
1956  "color scale! (be sure to open one with "
1957  "\"-%1 [cloud filename]\" before \"-%2\")")
1959  }
1960 
1961  for (auto& cloud : cmd.clouds()) {
1962  ccScalarField* sf = static_cast<ccScalarField*>(
1963  cloud.pc->getCurrentOutScalarField());
1964  if (sf) {
1965  sf->setColorScale(scale);
1966  }
1967  }
1968 
1969  if (cmd.autoSaveMode() && !cmd.saveClouds("COLOR_SCALE")) {
1970  return false;
1971  }
1972 
1973  return true;
1974 }
1975 
1977  : ccCommandLineInterface::Command(QObject::tr("SF convert to RGB"),
1979 
1981  cmd.print(QObject::tr("[SF CONVERT TO RGB]"));
1982 
1983  if (cmd.arguments().empty()) {
1984  return cmd.error(
1985  QObject::tr("Missing parameter: boolean (whether to mix with "
1986  "existing colors or not) after \"-%1\"")
1988  }
1989 
1990  QString mixWithExistingColorsStr = cmd.arguments().takeFirst().toUpper();
1991  bool mixWithExistingColors = false;
1992  if (mixWithExistingColorsStr == "TRUE") {
1993  mixWithExistingColors = true;
1994  } else if (mixWithExistingColorsStr != "FALSE") {
1995  return cmd.error(QObject::tr("Invalid boolean value after \"-%1\". Got "
1996  "'%2' instead of TRUE or FALSE.")
1998  mixWithExistingColorsStr));
1999  }
2000 
2001  if (cmd.clouds().empty()) {
2002  return cmd.error(QObject::tr("No point cloud on which to convert SF to "
2003  "RGB! (be sure to open one with \"-%1 "
2004  "[cloud filename]\" before \"-%2\")")
2006  }
2007 
2008  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
2009  ccPointCloud* pc = cmd.clouds()[i].pc;
2010  unsigned sfCount = pc->getNumberOfScalarFields();
2011  int activeSFIndex = pc->getCurrentOutScalarFieldIndex();
2012 
2013  if (sfCount == 0) {
2014  cmd.warning(QObject::tr("cmd.warning: cloud '%1' has no scalar "
2015  "field (it will be ignored)")
2016  .arg(pc->getName()));
2017  } else if (activeSFIndex < 0) {
2018  cmd.warning(QObject::tr("cmd.warning: cloud '%1' has no active "
2019  "scalar field (it will be ignored)")
2020  .arg(pc->getName()));
2021  } else {
2022  int displaySFIndex = pc->getCurrentDisplayedScalarFieldIndex();
2023  pc->setCurrentDisplayedScalarField(activeSFIndex);
2024 
2025  if (pc->convertCurrentScalarFieldToColors(mixWithExistingColors)) {
2026  pc->showColors(true);
2027  pc->showSF(false);
2028  } else {
2029  cmd.warning(QObject::tr("cmd.warning: cloud '%1' failed to "
2030  "convert SF to RGB")
2031  .arg(pc->getName()));
2032  }
2033 
2034  pc->setCurrentDisplayedScalarField(displaySFIndex);
2035  }
2036  }
2037 
2038  if (cmd.autoSaveMode() && !cmd.saveClouds("SF_CONVERT_TO_RGB")) {
2039  return false;
2040  }
2041 
2042  return true;
2043 }
2044 
2046  : ccCommandLineInterface::Command(QObject::tr("Filter by SF value"),
2048 
2049 // special SF values that can be used instead of explicit ones
2060 };
2061 
2063  cmd.print(QObject::tr("[FILTER BY VALUE]"));
2064 
2065  USE_SPECIAL_SF_VALUE useValForMin = USE_NONE;
2066  ScalarType minVal = 0;
2067  QString minValStr;
2068  {
2069  if (cmd.arguments().empty()) {
2070  return cmd.error(
2071  QObject::tr("Missing parameter: min value after \"-%1\"")
2073  }
2074 
2075  bool paramOk = false;
2076  minValStr = cmd.arguments().takeFirst();
2077  if (minValStr.toUpper() == "MIN") {
2078  useValForMin = USE_MIN;
2079  } else if (minValStr.toUpper() == "DISP_MIN") {
2080  useValForMin = USE_DISP_MIN;
2081  } else if (minValStr.toUpper() == "SAT_MIN") {
2082  useValForMin = USE_SAT_MIN;
2083  } else if (minValStr.toUpper() == "N_SIGMA_MIN") {
2084  useValForMin = USE_N_SIGMA_MIN;
2085  if (cmd.arguments().empty()) {
2086  return cmd.error(QObject::tr("Missing parameter: N value "
2087  "(after \"-%1 N_SIGMA_MIN\").")
2089  }
2090  minValStr = cmd.arguments().takeFirst();
2091  minVal = static_cast<ScalarType>(minValStr.toDouble(&paramOk));
2092  if (!paramOk) {
2093  return cmd.error(
2094  QObject::tr(
2095  "Failed to read a numerical parameter: N value "
2096  "(after \"N_SIGMA_MIN\"). Got '%2' instead.")
2097  .arg(minValStr));
2098  }
2099  } else {
2100  minVal = static_cast<ScalarType>(minValStr.toDouble(&paramOk));
2101  if (!paramOk) {
2102  return cmd.error(
2103  QObject::tr("Failed to read a numerical parameter: min "
2104  "value (after \"-%1\"). Got '%2' instead.")
2105  .arg(COMMAND_FILTER_SF_BY_VALUE, minValStr));
2106  }
2107  }
2108  }
2109 
2110  USE_SPECIAL_SF_VALUE useValForMax = USE_NONE;
2111  ScalarType maxVal = 0;
2112  QString maxValStr;
2113  {
2114  if (cmd.arguments().empty()) {
2115  return cmd.error(
2116  QObject::tr(
2117  "Missing parameter: max value after \"-%1\" {min}")
2119  }
2120 
2121  bool paramOk = false;
2122  maxValStr = cmd.arguments().takeFirst();
2123  if (maxValStr.toUpper() == "MAX") {
2124  useValForMax = USE_MAX;
2125  } else if (maxValStr.toUpper() == "DISP_MAX") {
2126  useValForMax = USE_DISP_MAX;
2127  } else if (maxValStr.toUpper() == "SAT_MAX") {
2128  useValForMax = USE_SAT_MAX;
2129  } else if (maxValStr.toUpper() == "N_SIGMA_MAX") {
2130  useValForMax = USE_N_SIGMA_MAX;
2131  if (cmd.arguments().empty()) {
2132  return cmd.error(QObject::tr("Missing parameter: N value "
2133  "(after \"-%1 N_SIGMA_MAX\").")
2135  }
2136  maxValStr = cmd.arguments().takeFirst();
2137  maxVal = static_cast<ScalarType>(maxValStr.toDouble(&paramOk));
2138  if (!paramOk) {
2139  return cmd.error(
2140  QObject::tr(
2141  "Failed to read a numerical parameter: N value "
2142  "(after \"N_SIGMA_MAX\"). Got '%2' instead.")
2143  .arg(maxValStr));
2144  }
2145  } else {
2146  maxVal = static_cast<ScalarType>(maxValStr.toDouble(&paramOk));
2147  if (!paramOk) {
2148  return cmd.error(
2149  QObject::tr(
2150  "Failed to read a numerical parameter: max "
2151  "value (after min value). Got '%1' instead.")
2152  .arg(COMMAND_FILTER_SF_BY_VALUE, maxValStr));
2153  }
2154  }
2155  }
2156 
2157  cmd.print(QObject::tr("\tInterval: [%1 - %2]").arg(minValStr, maxValStr));
2158 
2159  if (cmd.clouds().empty()) {
2160  return cmd.error(
2161  QObject::tr("No point cloud on which to filter SF! (be sure to "
2162  "open one or generate one with \"-%1 [cloud "
2163  "filename]\" before \"-%2\")")
2165  }
2166 
2167  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
2169  cmd.clouds()[i].pc->getCurrentOutScalarField();
2170  if (sf) {
2171  ScalarType thisMinVal = minVal;
2172  {
2173  switch (useValForMin) {
2174  case USE_MIN:
2175  thisMinVal = sf->getMin();
2176  break;
2177  case USE_DISP_MIN:
2178  thisMinVal = static_cast<ccScalarField*>(sf)
2179  ->displayRange()
2180  .start();
2181  break;
2182  case USE_SAT_MIN:
2183  thisMinVal = static_cast<ccScalarField*>(sf)
2184  ->saturationRange()
2185  .start();
2186  break;
2187  case USE_N_SIGMA_MIN:
2188  ScalarType mean;
2189  ScalarType variance;
2190  sf->computeMeanAndVariance(mean, &variance);
2191  thisMinVal = mean - (sqrt(variance) * minVal);
2192  break;
2193  default:
2194  // nothing to do
2195  break;
2196  }
2197  }
2198 
2199  ScalarType thisMaxVal = maxVal;
2200  {
2201  switch (useValForMax) {
2202  case USE_MAX:
2203  thisMaxVal = sf->getMax();
2204  break;
2205  case USE_DISP_MAX:
2206  thisMaxVal = static_cast<ccScalarField*>(sf)
2207  ->displayRange()
2208  .stop();
2209  break;
2210  case USE_SAT_MAX:
2211  thisMaxVal = static_cast<ccScalarField*>(sf)
2212  ->saturationRange()
2213  .stop();
2214  break;
2215  case USE_N_SIGMA_MAX:
2216  ScalarType mean;
2217  ScalarType variance;
2218  sf->computeMeanAndVariance(mean, &variance);
2219  thisMaxVal = mean + (sqrt(variance) * maxVal);
2220  break;
2221  default:
2222  // nothing to do
2223  break;
2224  }
2225  }
2226 
2227  ccPointCloud* fitleredCloud =
2228  cmd.clouds()[i].pc->filterPointsByScalarValue(thisMinVal,
2229  thisMaxVal);
2230  if (fitleredCloud) {
2231  cmd.print(
2232  QObject::tr("\t\tCloud '%1' --> %2/%3 points remaining")
2233  .arg(cmd.clouds()[i].pc->getName())
2234  .arg(fitleredCloud->size())
2235  .arg(cmd.clouds()[i].pc->size()));
2236 
2237  CLCloudDesc resultDesc(fitleredCloud, cmd.clouds()[i].basename,
2238  cmd.clouds()[i].path,
2239  cmd.clouds()[i].indexInFile);
2240  // replace current cloud by this one
2241  delete cmd.clouds()[i].pc;
2242  cmd.clouds()[i].pc = fitleredCloud;
2243  cmd.clouds()[i].basename += QObject::tr("_FILTERED_[%1_%2]")
2244  .arg(thisMinVal)
2245  .arg(thisMaxVal);
2246  if (cmd.autoSaveMode()) {
2247  QString errorStr = cmd.exportEntity(resultDesc);
2248  if (!errorStr.isEmpty()) {
2249  delete fitleredCloud;
2250  return cmd.error(errorStr);
2251  }
2252  }
2253  }
2254  }
2255  }
2256 
2257  return true;
2258 }
2259 
2261  : ccCommandLineInterface::Command(QObject::tr("Compute mesh volume"),
2263 
2265  cmd.print(QObject::tr("[COMPUTE MESH VOLUME]"));
2266 
2267  if (cmd.meshes().empty()) {
2268  cmd.warning(QObject::tr("No mesh loaded! Nothing to do..."));
2269  return true;
2270  }
2271 
2272  // optional parameters
2273  QString outputFilename;
2274  if (!cmd.arguments().empty()) {
2275  QString argument = cmd.arguments().front();
2276  if (ccCommandLineInterface::IsCommand(argument,
2278  // local option confirmed, we can move on
2279  cmd.arguments().pop_front();
2280 
2281  if (!cmd.arguments().empty()) {
2282  outputFilename = cmd.arguments().front();
2283  cmd.arguments().pop_front();
2284  cmd.print(QObject::tr("Volume report file: %1")
2285  .arg(outputFilename));
2286  } else {
2287  return cmd.error(
2288  QObject::tr("Missing argument: filename after '%1'")
2289  .arg(COMMAND_VOLUME_TO_FILE));
2290  }
2291  }
2292  }
2293 
2294  QFile outFile;
2295  QTextStream outStream(&outFile);
2296  if (!outputFilename.isEmpty()) {
2297  outFile.setFileName(outputFilename);
2298  if (!outFile.open(QFile::WriteOnly | QFile::Text)) {
2299  return cmd.error(
2300  QObject::tr("Failed to create/open volume report file"));
2301  }
2302  }
2303 
2304  // for each meshe
2305  for (CLMeshDesc& meshDesc : cmd.meshes()) {
2306  // we compute the mesh volume
2308  meshDesc.mesh);
2309 
2310  QString titleStr = QObject::tr("Mesh '%1'").arg(meshDesc.basename);
2311  if (meshDesc.indexInFile >= 0) {
2312  titleStr += QObject::tr(" (#%2)").arg(meshDesc.indexInFile);
2313  }
2314  cmd.print(titleStr);
2315  QString volumeStr = QObject::tr("V = %2").arg(V, 0, 'f', 8);
2316  cmd.print(volumeStr);
2317 
2318  if (outFile.isOpen()) {
2319  outStream << titleStr << QtCompat::endl;
2320  outStream << volumeStr << QtCompat::endl;
2321  }
2322  }
2323 
2324  return true;
2325 }
2326 
2328  : ccCommandLineInterface::Command(QObject::tr("Merge meshes"),
2330 
2332  cmd.print(QObject::tr("[MERGE MESHES]"));
2333 
2334  if (cmd.meshes().size() < 2) {
2335  cmd.warning(
2336  QObject::tr("Less than 2 meshes are loaded! Nothing to do..."));
2337  return true;
2338  }
2339 
2340  CLMeshDesc mergedMeshDesc;
2341  bool firstValidMesh = true;
2342 
2343  // create the destination mesh
2344  ccPointCloud* vertices = new ccPointCloud("vertices");
2345  QScopedPointer<ccMesh> mergedMesh(new ccMesh(vertices));
2346  mergedMesh->setName("Merged mesh");
2347  mergedMesh->addChild(vertices);
2348  vertices->setEnabled(false);
2349 
2350  // merge meshes
2351  for (CLMeshDesc& meshDesc : cmd.meshes()) {
2352  // get the mesh
2353  ccMesh* mesh = dynamic_cast<ccMesh*>(meshDesc.mesh);
2354  if (!mesh) {
2355  CVLog::Error(QObject::tr("Can't merge mesh '%1' (unhandled type)")
2356  .arg(meshDesc.basename));
2357  }
2358 
2359  if (mergedMesh->merge(mesh, true)) // merge it
2360  {
2361  if (firstValidMesh) {
2362  // copy the first valid mesh description
2363  mergedMeshDesc = meshDesc;
2364  mergedMeshDesc.mesh = nullptr;
2365  firstValidMesh = false;
2366  }
2367  } else {
2368  return cmd.error(QObject::tr("Merge operation failed"));
2369  }
2370 
2371  delete meshDesc.mesh;
2372  meshDesc.mesh = nullptr;
2373  }
2374 
2375  if (mergedMesh->size() == 0) {
2376  return cmd.error(QObject::tr("Result is empty"));
2377  }
2378 
2379  // clean the 'cmd.meshes()' vector
2380  cmd.removeMeshes();
2381  // add the new mesh
2382  mergedMeshDesc.basename += QObject::tr("_MERGED");
2383  mergedMeshDesc.mesh = mergedMesh.take();
2384  cmd.meshes().push_back(mergedMeshDesc);
2385 
2386  if (cmd.autoSaveMode()) {
2387  QString errorStr = cmd.exportEntity(mergedMeshDesc);
2388  if (!errorStr.isEmpty()) {
2389  return cmd.error(errorStr);
2390  }
2391  }
2392 
2393  return true;
2394 }
2395 
2397  : ccCommandLineInterface::Command(QObject::tr("Merge clouds"),
2399 
2401  cmd.print(QObject::tr("[MERGE CLOUDS]"));
2402 
2403  if (cmd.clouds().size() < 2) {
2404  cmd.warning(
2405  QObject::tr("Less than 2 clouds are loaded! Nothing to do..."));
2406  return true;
2407  }
2408 
2409  // merge clouds
2410  {
2411  for (size_t i = 1; i < cmd.clouds().size(); ++i) {
2412  unsigned beforePts = cmd.clouds().front().pc->size();
2413  unsigned newPts = cmd.clouds()[i].pc->size();
2414  *cmd.clouds().front().pc += cmd.clouds()[i].pc;
2415 
2416  // success?
2417  if (cmd.clouds().front().pc->size() == beforePts + newPts) {
2418  delete cmd.clouds()[i].pc;
2419  cmd.clouds()[i].pc = nullptr;
2420  } else {
2421  return cmd.error(
2422  QObject::tr("Fusion failed! (not enough memory?)"));
2423  }
2424  }
2425  }
2426 
2427  // clean the 'cmd.clouds()' vector
2428  cmd.clouds().resize(1);
2429  // update the first one
2430  cmd.clouds().front().basename += QObject::tr("_MERGED");
2431  if (cmd.autoSaveMode()) {
2432  QString errorStr = cmd.exportEntity(cmd.clouds().front());
2433  if (!errorStr.isEmpty()) {
2434  return cmd.error(errorStr);
2435  }
2436  }
2437  return true;
2438 }
2439 
2441  : ccCommandLineInterface::Command(QObject::tr("Set active SF"),
2443 
2445  if (cmd.arguments().empty()) {
2446  return cmd.error(
2447  QObject::tr(
2448  "Missing parameter: scalar field index after \"-%1\"")
2449  .arg(COMMAND_SET_ACTIVE_SF));
2450  }
2451 
2452  bool paramOk = false;
2453  QString sfIndexStr = cmd.arguments().takeFirst();
2454  int sfIndex = sfIndexStr.toInt(&paramOk);
2455  if (!paramOk) {
2456  return cmd.error(
2457  QObject::tr("Failed to read a numerical parameter: S.F. index "
2458  "(after \"-%1\"). Got '%2' instead.")
2459  .arg(COMMAND_SET_ACTIVE_SF, sfIndexStr));
2460  }
2461  cmd.print(QObject::tr("Set active S.F. index: %1").arg(sfIndex));
2462 
2463  if (cmd.clouds().empty()) {
2464  return cmd.error(
2465  QObject::tr("No point cloud loaded! (be sure to open one with "
2466  "\"-%1 [cloud filename]\" before \"-%2\")")
2468  }
2469 
2470  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
2471  if (cmd.clouds()[i].pc && cmd.clouds()[i].pc->hasScalarFields()) {
2472  if (static_cast<int>(
2473  cmd.clouds()[i].pc->getNumberOfScalarFields()) >
2474  sfIndex) {
2475  cmd.clouds()[i].pc->setCurrentScalarField(sfIndex);
2476  } else {
2477  cmd.warning(QObject::tr("Cloud '%1' has less scalar fields "
2478  "than the index to select!")
2479  .arg(cmd.clouds()[i].pc->getName()));
2480  }
2481  }
2482  }
2483 
2484  return true;
2485 }
2486 
2488  : ccCommandLineInterface::Command(QObject::tr("Remove all SF"),
2490 
2492  // no argument required
2493  for (auto& cloudDesc : cmd.clouds()) {
2494  if (cloudDesc.pc /* && cmd.clouds()[i].pc->hasScalarFields()*/) {
2495  cloudDesc.pc->deleteAllScalarFields();
2496  cloudDesc.pc->showSF(false);
2497  }
2498  }
2499 
2500  for (auto& meshDesc : cmd.meshes()) {
2501  if (meshDesc.mesh) {
2502  ccGenericPointCloud* cloud = meshDesc.mesh->getAssociatedCloud();
2503  if (cloud->isA(CV_TYPES::POINT_CLOUD)) {
2504  static_cast<ccPointCloud*>(cloud)->deleteAllScalarFields();
2505  cloud->showSF(false);
2506  }
2507  }
2508  }
2509 
2510  return true;
2511 }
2512 
2514  : ccCommandLineInterface::Command(QObject::tr("Remove a specific SF"),
2515  COMMAND_REMOVE_SF) {}
2516 
2517 bool CommandRemoveSF::removeSF(int sfIndex, ccPointCloud& pc) {
2518  if (pc.getNumberOfScalarFields() > static_cast<unsigned>(sfIndex)) {
2519  pc.deleteScalarField(sfIndex);
2520  if (pc.getNumberOfScalarFields() == 0) {
2521  pc.showSF(false);
2522  } else if (pc.getCurrentDisplayedScalarFieldIndex() < 0) {
2524  static_cast<int>(pc.getNumberOfScalarFields()) - 1);
2525  }
2526 
2527  return true;
2528  } else {
2529  return false;
2530  }
2531 }
2532 
2534  if (cmd.arguments().empty()) {
2535  return cmd.error(QObject::tr("Missing parameter: SF index after %1")
2536  .arg(COMMAND_REMOVE_SF));
2537  }
2538 
2539  bool paramOk = false;
2540  QString sfIndexStr = cmd.arguments().takeFirst();
2541  int sfIndex = sfIndexStr.toInt(&paramOk);
2542  if (!paramOk) {
2543  return cmd.error(QObject::tr("Failed to read a numerical parameter: SF "
2544  "index. Got '%1' instead.")
2545  .arg(sfIndexStr));
2546  }
2547  cmd.print(QObject::tr("\tSF index: %1").arg(sfIndex));
2548 
2549  if (sfIndex < 0) {
2550  return cmd.error(
2551  QObject::tr("Invalid SF index (positive value expected)"));
2552  }
2553 
2554  for (auto& cloudDesc : cmd.clouds()) {
2555  if (cloudDesc.pc) {
2556  if (!removeSF(sfIndex, *cloudDesc.pc)) {
2557  cmd.warning(QObject::tr("Cloud '%1' has not enough SFs")
2558  .arg(cloudDesc.pc->getName()));
2559  }
2560  }
2561  }
2562 
2563  for (auto& meshDesc : cmd.meshes()) {
2564  if (meshDesc.mesh) {
2565  ccGenericPointCloud* cloud = meshDesc.mesh->getAssociatedCloud();
2566  if (cloud->isA(CV_TYPES::POINT_CLOUD)) {
2567  ccPointCloud* pc = static_cast<ccPointCloud*>(cloud);
2568  if (!removeSF(sfIndex, *pc)) {
2569  cmd.warning(
2570  QObject::tr(
2571  "Mesh '%1' vertices have not enough SFs")
2572  .arg(meshDesc.mesh->getName()));
2573  }
2574  }
2575  }
2576  }
2577 
2578  return true;
2579 }
2580 
2582  : ccCommandLineInterface::Command(QObject::tr("Remove RGB"),
2583  COMMAND_REMOVE_RGB) {}
2584 
2586  // no argument required
2587  for (auto& cloudDesc : cmd.clouds()) {
2588  if (cloudDesc.pc) {
2589  cloudDesc.pc->unallocateColors();
2590  cloudDesc.pc->showColors(false);
2591  }
2592  }
2593 
2594  for (auto& meshDesc : cmd.meshes()) {
2595  if (meshDesc.mesh) {
2596  ccGenericPointCloud* cloud = meshDesc.mesh->getAssociatedCloud();
2597  if (cloud->isA(CV_TYPES::POINT_CLOUD)) {
2598  static_cast<ccPointCloud*>(cloud)->unallocateColors();
2599  cloud->showColors(false);
2600  }
2601  meshDesc.mesh->showColors(false);
2602  }
2603  }
2604 
2605  return true;
2606 }
2607 
2609  : ccCommandLineInterface::Command(QObject::tr("Remove normals"),
2611 
2613  // no argument required
2614  for (auto& cloudDesc : cmd.clouds()) {
2615  if (cloudDesc.pc) {
2616  cloudDesc.pc->unallocateNorms();
2617  cloudDesc.pc->showNormals(false);
2618  }
2619  }
2620 
2621  for (auto& meshDesc : cmd.meshes()) {
2622  if (meshDesc.mesh) {
2623  ccGenericPointCloud* cloud = meshDesc.mesh->getAssociatedCloud();
2624  if (cloud->isA(CV_TYPES::POINT_CLOUD)) {
2625  static_cast<ccPointCloud*>(cloud)->unallocateNorms();
2626  cloud->showNormals(false);
2627  }
2628  if (meshDesc.mesh->isA(CV_TYPES::MESH)) {
2629  static_cast<ccMesh*>(meshDesc.mesh)->clearTriNormals();
2630  meshDesc.mesh->showNormals(false);
2631  }
2632  }
2633  }
2634 
2635  return true;
2636 }
2637 
2639  : ccCommandLineInterface::Command(QObject::tr("Remove scan grids"),
2641 
2643  // no argument required
2644  for (CLCloudDesc& cloudDesc : cmd.clouds()) {
2645  if (cloudDesc.pc) {
2646  cloudDesc.pc->removeGrids();
2647  }
2648  }
2649 
2650  for (CLMeshDesc& meshDesc : cmd.meshes()) {
2651  if (meshDesc.mesh) {
2652  ccGenericPointCloud* cloud = meshDesc.mesh->getAssociatedCloud();
2653  if (cloud->isA(CV_TYPES::POINT_CLOUD)) {
2654  static_cast<ccPointCloud*>(cloud)->removeGrids();
2655  }
2656  }
2657  }
2658 
2659  return true;
2660 }
2661 
2663  : ccCommandLineInterface::Command(QObject::tr("Match B.B. centers"),
2665 
2667  cmd.print(QObject::tr("[MATCH B.B. CENTERS]"));
2668 
2669  std::vector<CLEntityDesc*> entities;
2670  for (auto& cloud : cmd.clouds()) {
2671  entities.push_back(&cloud);
2672  }
2673  for (auto& mesh : cmd.meshes()) {
2674  entities.push_back(&mesh);
2675  }
2676 
2677  if (entities.empty()) {
2678  return cmd.error("No entity loaded!");
2679  } else if (entities.size() == 1) {
2680  cmd.warning("Nothing to do: only one entity currently loaded!");
2681  return true;
2682  }
2683 
2684  CCVector3 firstCenter =
2685  entities.front()->getEntity()->getOwnBB().getCenter();
2686  for (size_t i = 1; i < entities.size(); ++i) {
2687  ccHObject* ent = entities[i]->getEntity();
2688  CCVector3 center = ent->getOwnBB().getCenter();
2689  CCVector3 T = firstCenter - center;
2690 
2691  // transformation (used only for translation)
2692  ccGLMatrix glTrans;
2693  glTrans += T;
2694 
2695  // apply translation matrix
2696  ent->applyGLTransformation_recursive(&glTrans);
2697  cmd.print(QObject::tr("Entity '%1' has been translated: (%2,%3,%4)")
2698  .arg(ent->getName())
2699  .arg(T.x)
2700  .arg(T.y)
2701  .arg(T.z));
2702  if (cmd.autoSaveMode()) {
2703  QString errorStr = cmd.exportEntity(*entities[i]);
2704  if (!errorStr.isEmpty()) {
2705  return cmd.error(errorStr);
2706  }
2707  }
2708  }
2709 
2710  return true;
2711 }
2712 
2714  : ccCommandLineInterface::Command(QObject::tr("Match best fit plane"),
2716 
2718  cmd.print(QObject::tr("[COMPUTE BEST FIT PLANE]"));
2719 
2720  // look for local options
2721  bool makeCloudsHoriz = false;
2722  bool keepLoaded = false;
2723 
2724  while (!cmd.arguments().empty()) {
2725  QString argument = cmd.arguments().front();
2727  argument, COMMAND_BEST_FIT_PLANE_MAKE_HORIZ)) {
2728  // local option confirmed, we can move on
2729  cmd.arguments().pop_front();
2730 
2731  makeCloudsHoriz = true;
2734  // local option confirmed, we can move on
2735  cmd.arguments().pop_front();
2736 
2737  keepLoaded = true;
2738  } else {
2739  break; // as soon as we encounter an unrecognized argument, we
2740  // break the local loop to go back to the main one!
2741  }
2742  }
2743 
2744  if (cmd.clouds().empty()) {
2745  return cmd.error(
2746  QObject::tr("No cloud available. Be sure to open one first!"));
2747  }
2748 
2749  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
2750  ccPointCloud* pc = cmd.clouds()[i].pc;
2751 
2752  // try to fit plane
2753  double rms = 0.0;
2754  ccPlane* pPlane = ccPlane::Fit(pc, &rms);
2755  if (pPlane) {
2756  cmd.print(QObject::tr("Plane successfully fitted: rms = %1")
2757  .arg(rms));
2758 
2759  CCVector3 N = pPlane->getNormal();
2761 
2762  CLMeshDesc planeDesc;
2763  planeDesc.mesh = pPlane;
2764  planeDesc.basename = cmd.clouds()[i].basename;
2765  planeDesc.path = cmd.clouds()[i].path;
2766 
2767  // save plane as a BIN file
2768  QString outputFilename;
2769  QString errorStr = cmd.exportEntity(planeDesc, "BEST_FIT_PLANE",
2770  &outputFilename);
2771  if (!errorStr.isEmpty()) {
2772  cmd.warning(errorStr);
2773  }
2774 
2775  // open text file to save plane related information
2776  QString txtFilename = QObject::tr("%1/%2_BEST_FIT_PLANE_INFO")
2777  .arg(cmd.clouds()[i].path,
2778  cmd.clouds()[i].basename);
2779  if (cmd.addTimestamp()) {
2780  txtFilename += QObject::tr("_%1").arg(
2781  QDateTime::currentDateTime().toString(
2782  "yyyy-MM-dd_hh'h'mm"));
2783  }
2784  txtFilename += QObject::tr(".txt");
2785  QFile txtFile(txtFilename);
2786  txtFile.open(QIODevice::WriteOnly | QIODevice::Text);
2787  QTextStream txtStream(&txtFile);
2788 
2789  txtStream << QObject::tr("Filename: %1").arg(outputFilename)
2790  << QtCompat::endl;
2791  txtStream << QObject::tr("Fitting RMS: %1").arg(rms)
2792  << QtCompat::endl;
2793 
2794  // We always consider the normal with a positive 'Z' by default!
2795  if (N.z < 0.0) {
2796  N *= -1.0;
2797  }
2798 
2799  int precision = cmd.numericalPrecision();
2800  txtStream << QObject::tr("Normal: (%1,%2,%3)")
2801  .arg(N.x, 0, 'f', precision)
2802  .arg(N.y, 0, 'f', precision)
2803  .arg(N.z, 0, 'f', precision)
2804  << QtCompat::endl;
2805 
2806  // we compute strike & dip by the way
2807  {
2808  PointCoordinateType dip = 0;
2809  PointCoordinateType dipDir = 0;
2812  dip, dipDir)
2813  << QtCompat::endl;
2814  }
2815 
2816  // compute the transformation matrix that would make this normal
2817  // points towards +Z
2818  ccGLMatrix makeZPosMatrix =
2820  CCVector3 Gt = C;
2821  makeZPosMatrix.applyRotation(Gt);
2822  makeZPosMatrix.setTranslation(C - Gt);
2823 
2824  txtStream << "Orientation matrix:" << QtCompat::endl;
2825  txtStream << makeZPosMatrix.toString(precision, ' ')
2826  << QtCompat::endl;
2827 
2828  // close the text file
2829  txtFile.close();
2830 
2831  if (keepLoaded) {
2832  // add the resulting plane (mesh) to the main set
2833  cmd.meshes().push_back(planeDesc);
2834  }
2835 
2836  if (makeCloudsHoriz) {
2837  // apply 'horizontal' matrix
2838  pc->applyGLTransformation_recursive(&makeZPosMatrix);
2839  cmd.print(QObject::tr("Cloud '%1' has been transformed with "
2840  "the above matrix")
2841  .arg(pc->getName()));
2842  cmd.clouds()[i].basename += QObject::tr("_HORIZ");
2843  if (cmd.autoSaveMode()) {
2844  QString errorStr = cmd.exportEntity(cmd.clouds()[i]);
2845  if (!errorStr.isEmpty()) {
2846  cmd.warning(errorStr);
2847  }
2848  }
2849  }
2850  } else {
2851  cmd.warning(
2852  QObject::tr(
2853  "Failed to compute best fit plane for cloud '%1'")
2854  .arg(cmd.clouds()[i].pc->getName()));
2855  }
2856  }
2857 
2858  return true;
2859 }
2860 
2862  : ccCommandLineInterface::Command(QObject::tr("Orient normals"),
2864 
2866  cmd.print(QObject::tr("[ORIENT NORMALS (MST)]"));
2867 
2868  if (cmd.arguments().empty()) {
2869  return cmd.error(
2870  QObject::tr(
2871  "Missing parameter: number of neighbors after \"-%1\"")
2872  .arg(COMMAND_ORIENT_NORMALS));
2873  }
2874 
2875  QString knnStr = cmd.arguments().takeFirst();
2876  bool ok;
2877  int knn = knnStr.toInt(&ok);
2878  if (!ok || knn <= 0) {
2879  return cmd.error(
2880  QObject::tr("Invalid parameter: number of neighbors (%1)")
2881  .arg(knnStr));
2882  }
2883 
2884  if (cmd.clouds().empty()) {
2885  return cmd.error(
2886  QObject::tr("No cloud available. Be sure to open one first!"));
2887  }
2888 
2889  QScopedPointer<ecvProgressDialog> progressDialog(nullptr);
2890  if (!cmd.silentMode()) {
2891  progressDialog.reset(new ecvProgressDialog(false, cmd.widgetParent()));
2892  progressDialog->setAutoClose(false);
2893  }
2894 
2895  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
2896  ccPointCloud* cloud = cmd.clouds()[i].pc;
2897  assert(cloud);
2898 
2899  if (!cloud->hasNormals()) {
2900  continue;
2901  }
2902 
2903  // computation
2904  if (cloud->orientNormalsWithMST(knn, progressDialog.data())) {
2905  cmd.clouds()[i].basename += QObject::tr("_NORMS_REORIENTED");
2906  if (cmd.autoSaveMode()) {
2907  QString errorStr = cmd.exportEntity(cmd.clouds()[i]);
2908  if (!errorStr.isEmpty()) {
2909  cmd.warning(errorStr);
2910  }
2911  }
2912  } else {
2913  return cmd.error(
2914  QObject::tr("Failed to orient the normals of cloud '%1'!")
2915  .arg(cloud->getName()));
2916  }
2917  }
2918 
2919  if (progressDialog) {
2920  progressDialog->close();
2921  QCoreApplication::processEvents();
2922  }
2923 
2924  return true;
2925 }
2926 
2928  : ccCommandLineInterface::Command(QObject::tr("S.O.R. filter"),
2929  COMMAND_SOR_FILTER) {}
2930 
2932  cmd.print(QObject::tr("[SOR FILTER]"));
2933 
2934  if (cmd.arguments().empty()) {
2935  return cmd.error(QObject::tr("Missing parameter: number of neighbors "
2936  "mode after \"-%1\"")
2937  .arg(COMMAND_SOR_FILTER));
2938  }
2939 
2940  QString knnStr = cmd.arguments().takeFirst();
2941  bool ok;
2942  int knn = knnStr.toInt(&ok);
2943  if (!ok || knn <= 0) {
2944  return cmd.error(
2945  QObject::tr("Invalid parameter: number of neighbors (%1)")
2946  .arg(knnStr));
2947  }
2948 
2949  if (cmd.arguments().empty()) {
2950  return cmd.error(
2951  QObject::tr("Missing parameter: sigma multiplier after number "
2952  "of neighbors (SOR)"));
2953  }
2954  QString sigmaStr = cmd.arguments().takeFirst();
2955  double nSigma = sigmaStr.toDouble(&ok);
2956  if (!ok || nSigma < 0) {
2957  return cmd.error(QObject::tr("Invalid parameter: sigma multiplier (%1)")
2958  .arg(nSigma));
2959  }
2960 
2961  if (cmd.clouds().empty()) {
2962  return cmd.error(
2963  QObject::tr("No cloud available. Be sure to open one first!"));
2964  }
2965 
2966  QScopedPointer<ecvProgressDialog> progressDialog(nullptr);
2967  if (!cmd.silentMode()) {
2968  progressDialog.reset(new ecvProgressDialog(false, cmd.widgetParent()));
2969  progressDialog->setAutoClose(false);
2970  }
2971 
2972  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
2973  ccPointCloud* cloud = cmd.clouds()[i].pc;
2974  assert(cloud);
2975 
2976  // computation
2977  cloudViewer::ReferenceCloud* selection =
2979  cloud, knn, nSigma, nullptr, progressDialog.data());
2980 
2981  if (selection) {
2982  ccPointCloud* cleanCloud = cloud->partialClone(selection);
2983  if (cleanCloud) {
2984  cleanCloud->setName(cloud->getName() + QObject::tr(".clean"));
2985  if (cmd.autoSaveMode()) {
2986  CLCloudDesc cloudDesc(cleanCloud, cmd.clouds()[i].basename,
2987  cmd.clouds()[i].path,
2988  cmd.clouds()[i].indexInFile);
2989  QString errorStr = cmd.exportEntity(cloudDesc, "SOR");
2990  if (!errorStr.isEmpty()) {
2991  delete cleanCloud;
2992  return cmd.error(errorStr);
2993  }
2994  }
2995  // replace current cloud by this one
2996  delete cmd.clouds()[i].pc;
2997  cmd.clouds()[i].pc = cleanCloud;
2998  cmd.clouds()[i].basename += QObject::tr("_SOR");
2999  // delete cleanCloud;
3000  // cleanCloud = 0;
3001  } else {
3002  return cmd.error(QObject::tr("Not enough memory to create a "
3003  "clean version of cloud '%1'!")
3004  .arg(cloud->getName()));
3005  }
3006 
3007  delete selection;
3008  selection = nullptr;
3009  } else {
3010  // no points fall inside selection!
3011  return cmd.error(QObject::tr("Failed to apply SOR filter on cloud "
3012  "'%1'! (not enough memory?)")
3013  .arg(cloud->getName()));
3014  }
3015  }
3016 
3017  if (progressDialog) {
3018  progressDialog->close();
3019  QCoreApplication::processEvents();
3020  }
3021 
3022  return true;
3023 }
3024 
3026  : ccCommandLineInterface::Command(
3027  QObject::tr("Extract vertices (as a standalone 'cloud')"),
3029 
3031  cmd.print(QObject::tr("[EXTRACT VERTICES]"));
3032 
3033  if (cmd.meshes().empty()) {
3034  cmd.warning(
3035  QObject::tr("No mesh available. Be sure to open one first!"));
3036  return false;
3037  }
3038 
3039  for (size_t i = 0; i < cmd.meshes().size(); ++i) {
3040  ccGenericMesh* mesh = cmd.meshes()[i].mesh;
3041  ccGenericPointCloud* cloud = mesh->getAssociatedCloud();
3043  if (!pc) {
3044  assert(false);
3045  continue;
3046  }
3047 
3048  // add the resulting cloud to the main set
3049  cmd.clouds().emplace_back(
3050  pc, cmd.meshes()[i].basename + QObject::tr(".vertices"),
3051  cmd.meshes()[i].path);
3052 
3053  // don't forget to detach the cloud before we delete the meshes!
3054  assert(pc->getParent() == mesh);
3055  if (pc->getParent()) {
3056  pc->getParent()->detachChild(pc);
3057  }
3058 
3059  // save it as well
3060  if (cmd.autoSaveMode()) {
3061  QString errorStr = cmd.exportEntity(cmd.clouds().back());
3062  if (!errorStr.isEmpty()) {
3063  return cmd.error(errorStr);
3064  }
3065  }
3066  }
3067 
3068  cmd.removeMeshes(false);
3069 
3070  return true;
3071 }
3072 
3074  : ccCommandLineInterface::Command(QObject::tr("Sample mesh"),
3076 
3078  cmd.print(QObject::tr("[SAMPLE POINTS ON MESH]"));
3079 
3080  if (cmd.arguments().empty()) {
3081  return cmd.error(QObject::tr("Missing parameter: sampling mode after "
3082  "\"-%1\" (POINTS/DENSITY)")
3083  .arg(COMMAND_SAMPLE_MESH));
3084  }
3085 
3086  bool useDensity = false;
3087  double parameter = 0;
3088 
3089  QString sampleMode = cmd.arguments().takeFirst().toUpper();
3090  if (sampleMode == "POINTS") {
3091  useDensity = false;
3092  } else if (sampleMode == "DENSITY") {
3093  useDensity = true;
3094  } else {
3095  return cmd.error(
3096  QObject::tr("Invalid parameter: unknown sampling mode \"%1\"")
3097  .arg(sampleMode));
3098  }
3099 
3100  if (cmd.arguments().empty()) {
3101  return cmd.error(
3102  QObject::tr("Missing parameter: value after sampling mode"));
3103  }
3104  bool conversionOk = false;
3105  parameter = cmd.arguments().takeFirst().toDouble(&conversionOk);
3106  if (!conversionOk) {
3107  return cmd.error(
3108  QObject::tr("Invalid parameter: value after sampling mode"));
3109  }
3110 
3111  if (cmd.meshes().empty()) {
3112  return cmd.error(
3113  QObject::tr("No mesh available. Be sure to open one first!"));
3114  }
3115 
3116  QScopedPointer<ecvProgressDialog> progressDialog(nullptr);
3117  if (!cmd.silentMode()) {
3118  progressDialog.reset(new ecvProgressDialog(false, cmd.widgetParent()));
3119  progressDialog->setAutoClose(false);
3120  }
3121 
3122  for (size_t i = 0; i < cmd.meshes().size(); ++i) {
3123  ccPointCloud* cloud = cmd.meshes()[i].mesh->samplePoints(
3124  useDensity, parameter, true, true, true, progressDialog.data());
3125 
3126  if (!cloud) {
3127  return cmd.error(QObject::tr("Cloud sampling failed!"));
3128  }
3129 
3130  // add the resulting cloud to the main set
3131  cmd.print(QObject::tr("Sampled cloud created: %1 points")
3132  .arg(cloud->size()));
3133  cmd.clouds().emplace_back(
3134  cloud,
3135  cmd.meshes()[i].basename + QObject::tr("_SAMPLED_POINTS"),
3136  cmd.meshes()[i].path);
3137 
3138  // save it as well
3139  if (cmd.autoSaveMode()) {
3140  QString errorStr = cmd.exportEntity(cmd.clouds().back());
3141  if (!errorStr.isEmpty()) {
3142  return cmd.error(errorStr);
3143  }
3144  }
3145  }
3146 
3147  if (progressDialog) {
3148  progressDialog->close();
3149  QCoreApplication::processEvents();
3150  }
3151 
3152  return true;
3153 }
3154 
3156  : ccCommandLineInterface::Command(QObject::tr("Crop"), COMMAND_CROP) {}
3157 
3159  cmd.print(QObject::tr("[CROP]"));
3160 
3161  if (cmd.arguments().empty()) {
3162  return cmd.error(QObject::tr("Missing parameter: box extents after "
3163  "\"-%1\" (Xmin:Ymin:Zmin:Xmax:Ymax:Zmax)")
3164  .arg(COMMAND_CROP));
3165  }
3166  if (cmd.clouds().empty() && cmd.meshes().empty()) {
3167  return cmd.error(
3168  QObject::tr("No point cloud or mesh available. Be sure to open "
3169  "or generate one first!"));
3170  }
3171 
3172  // decode box extents
3173  CCVector3 boxMin;
3174  CCVector3 boxMax;
3175  {
3176  QString boxBlock = cmd.arguments().takeFirst();
3177  QStringList tokens = boxBlock.split(':');
3178  if (tokens.size() != 6) {
3179  return cmd.error(
3180  QObject::tr("Invalid parameter: box extents (expected "
3181  "format is 'Xmin:Ymin:Zmin:Xmax:Ymax:Zmax')")
3182  .arg(COMMAND_CROP));
3183  }
3184 
3185  for (int i = 0; i < 6; ++i) {
3186  CCVector3* vec = (i < 3 ? &boxMin : &boxMax);
3187  bool ok = true;
3188  vec->u[i % 3] =
3189  static_cast<PointCoordinateType>(tokens[i].toDouble(&ok));
3190  if (!ok) {
3191  return cmd.error(
3192  QObject::tr("Invalid parameter: box extents (component "
3193  "#%1 is not a valid number)")
3194  .arg(i + 1));
3195  }
3196  }
3197  }
3198 
3199  // optional parameters
3200  bool inside = true;
3201  while (!cmd.arguments().empty()) {
3202  QString argument = cmd.arguments().front();
3204  // local option confirmed, we can move on
3205  cmd.arguments().pop_front();
3206  inside = false;
3207  } else {
3208  break;
3209  }
3210  }
3211 
3212  ccBBox cropBox(boxMin, boxMax);
3213  // crop clouds
3214  {
3215  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
3216  ccHObject* croppedCloud =
3217  ccCropTool::Crop(cmd.clouds()[i].pc, cropBox, inside);
3218  if (croppedCloud) {
3219  delete cmd.clouds()[i].pc;
3220  assert(croppedCloud->isA(CV_TYPES::POINT_CLOUD));
3221  cmd.clouds()[i].pc = static_cast<ccPointCloud*>(croppedCloud);
3222  cmd.clouds()[i].basename += "_CROPPED";
3223  if (cmd.autoSaveMode()) {
3224  QString errorStr = cmd.exportEntity(cmd.clouds()[i]);
3225  if (!errorStr.isEmpty()) {
3226  return cmd.error(errorStr);
3227  }
3228  }
3229  } else {
3230  // otherwise an error message has already been issued
3231  delete cmd.clouds()[i].pc;
3232  cmd.clouds()[i].pc = nullptr; // will be removed after this
3233  // loop
3234  // cmd.clouds()[i].basename += "_FULLY_CROPPED";
3235  }
3236  }
3237 
3238  // now clean the set of clouds in case some have been 'cropped out'
3239  for (auto it = cmd.clouds().begin(); it != cmd.clouds().end();) {
3240  if (it->pc == nullptr) {
3241  it = cmd.clouds().erase(it);
3242  } else {
3243  ++it;
3244  }
3245  }
3246  }
3247 
3248  // crop meshes
3249  {
3250  for (size_t i = 0; i < cmd.meshes().size(); ++i) {
3251  ccHObject* croppedMesh =
3252  ccCropTool::Crop(cmd.meshes()[i].mesh, cropBox, inside);
3253  if (croppedMesh) {
3254  delete cmd.meshes()[i].mesh;
3255  assert(croppedMesh->isA(CV_TYPES::MESH));
3256  cmd.meshes()[i].mesh = static_cast<ccMesh*>(croppedMesh);
3257  cmd.meshes()[i].basename += "_CROPPED";
3258  if (cmd.autoSaveMode()) {
3259  QString errorStr = cmd.exportEntity(cmd.meshes()[i]);
3260  if (!errorStr.isEmpty()) {
3261  return cmd.error(errorStr);
3262  }
3263  }
3264  } else {
3265  // otherwise an error message has already been issued
3266  delete cmd.meshes()[i].mesh;
3267  cmd.meshes()[i].mesh =
3268  nullptr; // will be removed after this loop
3269  // cmd.meshes()[i].basename += "_FULLY_CROPPED";
3270  }
3271  }
3272 
3273  // now clean the set of meshes in case some have been 'cropped out'
3274  for (auto it = cmd.meshes().begin(); it != cmd.meshes().end();) {
3275  if (it->mesh == nullptr) {
3276  it = cmd.meshes().erase(it);
3277  } else {
3278  ++it;
3279  }
3280  }
3281  }
3282 
3283  return true;
3284 }
3285 
3287  : ccCommandLineInterface::Command(QObject::tr("Crop"),
3289 
3291  cmd.print(QObject::tr("[COORD TO SF]"));
3292 
3293  if (cmd.arguments().empty()) {
3294  return cmd.error(
3295  QObject::tr("Missing parameter after \"-%1\" (DIMENSION)")
3296  .arg(COMMAND_COORD_TO_SF));
3297  }
3298  if (cmd.clouds().empty()) {
3299  return cmd.error(
3300  QObject::tr("No point cloud available. Be sure to open or "
3301  "generate one first!"));
3302  }
3303 
3304  // dimension
3305  bool exportDims[3] = {false, false, false};
3306  QString dimStr = cmd.arguments().takeFirst().toUpper();
3307  {
3308  if (dimStr == "X") {
3309  exportDims[0] = true;
3310  } else if (dimStr == "Y") {
3311  exportDims[1] = true;
3312  } else if (dimStr == "Z") {
3313  exportDims[2] = true;
3314  } else {
3315  return cmd.error(QObject::tr("Invalid parameter: dimension after "
3316  "\"-%1\" (expected: X, Y or Z)")
3317  .arg(COMMAND_COORD_TO_SF));
3318  }
3319  }
3320 
3321  // now we can export the corresponding coordinate
3322  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
3323  ccPointCloud* pc = cmd.clouds()[i].pc;
3324  if (pc->exportCoordToSF(exportDims)) {
3325  cmd.clouds()[i].basename += QObject::tr("_%1_TO_SF").arg(dimStr);
3326  if (cmd.autoSaveMode()) {
3327  QString errorStr = cmd.exportEntity(cmd.clouds()[i]);
3328  if (!errorStr.isEmpty()) {
3329  return cmd.error(errorStr);
3330  }
3331  }
3332  } else {
3333  return cmd.error(
3334  QObject::tr(
3335  "Failed to export coord. %1 to SF on cloud '%2'!")
3336  .arg(dimStr, cmd.clouds()[i].pc->getName()));
3337  }
3338  }
3339 
3340  return true;
3341 }
3342 
3344  : ccCommandLineInterface::Command(QObject::tr("Crop"), COMMAND_CROP_2D) {}
3345 
3347  cmd.print(QObject::tr("[CROP 2D]"));
3348 
3349  if (cmd.arguments().size() < 6) {
3350  return cmd.error(QObject::tr("Missing parameter(s) after \"-%1\" "
3351  "(ORTHO_DIM N X1 Y1 X2 Y2 ... XN YN)")
3352  .arg(COMMAND_CROP_2D));
3353  }
3354  if (cmd.clouds().empty()) {
3355  return cmd.error(
3356  QObject::tr("No point cloud available. Be sure to open or "
3357  "generate one first!"));
3358  }
3359 
3360  // decode poyline extents
3361  ccPointCloud vertices("polyline.vertices");
3362  ccPolyline poly(&vertices);
3363 
3364  // orthogonal dimension
3365  unsigned char orthoDim = 2;
3366  {
3367  QString orthoDimStr = cmd.arguments().takeFirst().toUpper();
3368  if (orthoDimStr == "X") {
3369  orthoDim = 0;
3370  } else if (orthoDimStr == "Y") {
3371  orthoDim = 1;
3372  } else if (orthoDimStr == "Z") {
3373  orthoDim = 2;
3374  } else {
3375  return cmd.error(
3376  QObject::tr("Invalid parameter: orthogonal dimension after "
3377  "\"-%1\" (expected: X, Y or Z)")
3378  .arg(COMMAND_CROP_2D));
3379  }
3380  }
3381 
3382  // number of vertices
3383  bool ok = true;
3384  unsigned N = 0;
3385  {
3386  QString countStr = cmd.arguments().takeFirst();
3387  N = countStr.toUInt(&ok);
3388  if (!ok) {
3389  return cmd.error(
3390  QObject::tr("Invalid parameter: number of vertices for the "
3391  "2D polyline after \"-%1\"")
3392  .arg(COMMAND_CROP_2D));
3393  }
3394  }
3395 
3396  // now read the vertices
3397  {
3398  if (!vertices.reserve(N) || !poly.addPointIndex(0, N)) {
3399  return cmd.error(QObject::tr("Not enough memory!"));
3400  }
3401 
3402  for (unsigned i = 0; i < N; ++i) {
3403  if (cmd.arguments().size() < 2) {
3404  return cmd.error(QObject::tr("Missing parameter(s): vertex #%1 "
3405  "data and following")
3406  .arg(i + 1));
3407  }
3408 
3409  CCVector3 P(0, 0, 0);
3410 
3411  QString coordStr = cmd.arguments().takeFirst();
3412  P.x = static_cast<PointCoordinateType>(coordStr.toDouble(&ok));
3413  if (!ok) {
3414  return cmd.error(
3415  QObject::tr(
3416  "Invalid parameter: X-coordinate of vertex #%1")
3417  .arg(i + 1));
3418  }
3419  /*QString */ coordStr = cmd.arguments().takeFirst();
3420  P.y = static_cast<PointCoordinateType>(coordStr.toDouble(&ok));
3421  if (!ok) {
3422  return cmd.error(
3423  QObject::tr(
3424  "Invalid parameter: Y-coordinate of vertex #%1")
3425  .arg(i + 1));
3426  }
3427 
3428  vertices.addPoint(
3429  P); // the polyline must be defined in the XY plane!
3430  }
3431 
3432  poly.setClosed(true);
3433  }
3434 
3435  // optional parameters
3436  bool inside = true;
3437  while (!cmd.arguments().empty()) {
3438  QString argument = cmd.arguments().front();
3440  // local option confirmed, we can move on
3441  cmd.arguments().pop_front();
3442  inside = false;
3443  } else {
3444  break;
3445  }
3446  }
3447 
3448  // now we can crop the loaded cloud(s)
3449  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
3451  cmd.clouds()[i].pc->crop2D(&poly, orthoDim, inside);
3452  if (ref) {
3453  if (ref->size() != 0) {
3454  ccPointCloud* croppedCloud =
3455  cmd.clouds()[i].pc->partialClone(ref);
3456  delete ref;
3457  ref = nullptr;
3458 
3459  if (croppedCloud) {
3460  delete cmd.clouds()[i].pc;
3461  cmd.clouds()[i].pc = croppedCloud;
3462  croppedCloud->setName(cmd.clouds()[i].pc->getName() +
3463  QObject::tr(".cropped"));
3464  cmd.clouds()[i].basename += "_CROPPED";
3465  if (cmd.autoSaveMode()) {
3466  QString errorStr = cmd.exportEntity(cmd.clouds()[i]);
3467  if (!errorStr.isEmpty()) {
3468  return cmd.error(errorStr);
3469  }
3470  }
3471  } else {
3472  return cmd.error(
3473  QObject::tr("Not enough memory to crop cloud '%1'!")
3474  .arg(cmd.clouds()[i].pc->getName()));
3475  }
3476  } else {
3477  delete ref;
3478  ref = nullptr;
3479  cmd.warning(QObject::tr("No point of cloud '%1' falls inside "
3480  "the input box!")
3481  .arg(cmd.clouds()[i].pc->getName()));
3482  }
3483  } else {
3484  return cmd.error(
3485  QObject::tr("Crop process failed! (not enough memory)"));
3486  }
3487  }
3488 
3489  return true;
3490 }
3491 
3493  : ccCommandLineInterface::Command(QObject::tr("Color banding"),
3495 
3497  cmd.print(QObject::tr("[COLOR BANDING]"));
3498 
3499  if (cmd.arguments().size() < 2) {
3500  return cmd.error(
3501  QObject::tr(
3502  "Missing parameter(s) after \"-%1\" (DIM FREQUENCY)")
3503  .arg(COMMAND_COLOR_BANDING));
3504  }
3505  if (cmd.clouds().empty() && cmd.meshes().empty()) {
3506  return cmd.error(QObject::tr(
3507  "No entity available. Be sure to open or generate one first!"));
3508  }
3509 
3510  // dimension
3511  unsigned char dim = 2;
3512  QString dimStr = "Z";
3513  {
3514  dimStr = cmd.arguments().takeFirst().toUpper();
3515  if (dimStr == "X") {
3516  dim = 0;
3517  } else if (dimStr == "Y") {
3518  dim = 1;
3519  } else if (dimStr == "Z") {
3520  dim = 2;
3521  } else {
3522  return cmd.error(QObject::tr("Invalid parameter: dimension after "
3523  "\"-%1\" (expected: X, Y or Z)")
3524  .arg(COMMAND_COLOR_BANDING));
3525  }
3526  }
3527 
3528  // frequency
3529  bool ok = true;
3530  double freq = 0;
3531  {
3532  QString countStr = cmd.arguments().takeFirst();
3533  freq = countStr.toDouble(&ok);
3534  if (!ok) {
3535  return cmd.error(QObject::tr("Invalid parameter: frequency after "
3536  "\"-%1 DIM\" (in Hz, integer value)")
3537  .arg(COMMAND_COLOR_BANDING));
3538  }
3539  }
3540 
3541  // process clouds
3542  if (!cmd.clouds().empty()) {
3543  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
3544  if (cmd.clouds()[i].pc) {
3545  if (!cmd.clouds()[i].pc->setRGBColorByBanding(dim, freq)) {
3546  return cmd.error(QObject::tr("Not enough memory"));
3547  }
3548  }
3549  }
3550 
3551  // save output
3552  if (cmd.autoSaveMode() &&
3553  !cmd.saveClouds(
3554  QObject::tr("COLOR_BANDING_%1_%2").arg(dimStr).arg(freq))) {
3555  return false;
3556  }
3557  }
3558 
3559  if (!cmd.meshes().empty()) {
3560  bool hasMeshes = false;
3561  for (size_t i = 0; i < cmd.meshes().size(); ++i) {
3562  ccPointCloud* cloud =
3563  ccHObjectCaster::ToPointCloud(cmd.meshes()[i].mesh);
3564  if (cloud) {
3565  if (!cloud->setRGBColorByBanding(dim, freq)) {
3566  return cmd.error(QObject::tr("Not enough memory"));
3567  }
3568  cmd.meshes()[i].mesh->showColors(true);
3569  hasMeshes = true;
3570  } else {
3571  cmd.warning(
3572  QObject::tr(
3573  "Vertices of mesh '%1' are locked (they may be "
3574  "shared by multiple entities for instance). "
3575  "Can't apply the current command on them.")
3576  .arg(cmd.meshes()[i].mesh->getName()));
3577  }
3578  }
3579 
3580  // save output
3581  if (hasMeshes && cmd.autoSaveMode() &&
3582  !cmd.saveMeshes(
3583  QObject::tr("COLOR_BANDING_%1_%2").arg(dimStr).arg(freq))) {
3584  return false;
3585  }
3586  }
3587 
3588  return true;
3589 }
3590 
3591 CommandDist::CommandDist(bool cloud2meshDist,
3592  const QString& name,
3593  const QString& keyword)
3594  : ccCommandLineInterface::Command(name, keyword),
3595  m_cloud2meshDist(cloud2meshDist) {}
3596 
3598  cmd.print(QObject::tr("[DISTANCE COMPUTATION]"));
3599 
3600  // compared cloud
3601  CLEntityDesc* compEntity = nullptr;
3602  ccHObject* compCloud = nullptr;
3603  size_t nextMeshIndex = 0;
3604  if (cmd.clouds().empty()) {
3605  // no cloud loaded
3606  if (!m_cloud2meshDist || cmd.meshes().size() < 2) {
3607  // we would need at least two meshes
3608  return cmd.error(
3609  QObject::tr("No point cloud available. Be sure to open or "
3610  "generate one first!"));
3611  } else {
3612  cmd.warning(
3613  QObject::tr("No point cloud available. Will use the first "
3614  "mesh vertices as compared cloud."));
3615  compEntity = &(cmd.meshes().front());
3616  compCloud = dynamic_cast<ccPointCloud*>(
3617  cmd.meshes()[nextMeshIndex++].mesh->getAssociatedCloud());
3618  if (!compCloud) {
3619  return cmd.error(QObject::tr("Unhandled mesh vertices type"));
3620  }
3621  }
3622  } else // at least two clouds
3623  {
3624  if (m_cloud2meshDist && cmd.clouds().size() != 1) {
3625  cmd.warning(
3626  QObject::tr("[C2M] Multiple point clouds loaded! Will take "
3627  "the first one by default."));
3628  }
3629  compEntity = &(cmd.clouds().front());
3630  compCloud = cmd.clouds().front().pc;
3631  }
3632  assert(compEntity && compCloud);
3633 
3634  // reference entity
3635  ccHObject* refEntity = nullptr;
3636  if (m_cloud2meshDist) {
3637  if (cmd.meshes().size() <= nextMeshIndex) {
3638  return cmd.error(QObject::tr(
3639  "No mesh available. Be sure to open one first!"));
3640  } else if (cmd.meshes().size() != nextMeshIndex + 1) {
3641  cmd.warning(QString("Multiple meshes loaded! We take the %1 one by "
3642  "default")
3643  .arg(nextMeshIndex == 0 ? "first" : "second"));
3644  }
3645  refEntity = cmd.meshes()[nextMeshIndex].mesh;
3646  } else {
3647  if (cmd.clouds().size() < 2) {
3648  return cmd.error(QObject::tr(
3649  "Only one point cloud available. Be sure to open or "
3650  "generate a second one before performing C2C distance!"));
3651  } else if (cmd.clouds().size() > 2) {
3652  cmd.warning(
3653  QObject::tr("More than 3 point clouds loaded! We take the "
3654  "second one as reference by default"));
3655  }
3656  refEntity = cmd.clouds()[1].pc;
3657  }
3658 
3659  // inner loop for Distance computation options
3660  bool flipNormals = false;
3661  double maxDist = 0.0;
3662  unsigned octreeLevel = 0;
3663  int maxThreadCount = 0;
3664 
3665  bool splitXYZ = false;
3666  int modelIndex = 0;
3667  bool useKNN = true;
3668  double nSize = 0;
3669 
3670  while (!cmd.arguments().empty()) {
3671  QString argument = cmd.arguments().front();
3672  if (ccCommandLineInterface::IsCommand(argument,
3674  // local option confirmed, we can move on
3675  cmd.arguments().pop_front();
3676 
3677  flipNormals = true;
3678 
3679  if (!m_cloud2meshDist) {
3680  cmd.warning(QObject::tr(
3681  "Parameter \"-%1\" ignored: only for C2M distance!"));
3682  }
3684  argument, COMMAND_C2X_MAX_DISTANCE)) {
3685  // local option confirmed, we can move on
3686  cmd.arguments().pop_front();
3687 
3688  if (cmd.arguments().empty()) {
3689  return cmd.error(
3690  QObject::tr("Missing parameter: value after \"-%1\"")
3691  .arg(COMMAND_C2X_MAX_DISTANCE));
3692  }
3693  bool conversionOk = false;
3694  maxDist = cmd.arguments().takeFirst().toDouble(&conversionOk);
3695  if (!conversionOk) {
3696  return cmd.error(
3697  QObject::tr("Invalid parameter: value after \"-%1\"")
3698  .arg(COMMAND_C2X_MAX_DISTANCE));
3699  }
3701  argument, COMMAND_C2X_OCTREE_LEVEL)) {
3702  // local option confirmed, we can move on
3703  cmd.arguments().pop_front();
3704 
3705  if (cmd.arguments().empty()) {
3706  return cmd.error(
3707  QObject::tr("Missing parameter: value after \"-%1\"")
3708  .arg(COMMAND_C2X_OCTREE_LEVEL));
3709  }
3710  bool conversionOk = false;
3711  octreeLevel = cmd.arguments().takeFirst().toUInt(&conversionOk);
3712  if (!conversionOk) {
3713  return cmd.error(
3714  QObject::tr("Invalid parameter: value after \"-%1\"")
3715  .arg(COMMAND_C2X_OCTREE_LEVEL));
3716  }
3717  } else if (ccCommandLineInterface::IsCommand(argument,
3719  // local option confirmed, we can move on
3720  cmd.arguments().pop_front();
3721 
3722  splitXYZ = true;
3723 
3724  if (m_cloud2meshDist) {
3725  cmd.warning(QObject::tr(
3726  "Parameter \"-%1\" ignored: only for C2C distance!"));
3727  }
3728  } else if (ccCommandLineInterface::IsCommand(argument,
3730  // local option confirmed, we can move on
3731  cmd.arguments().pop_front();
3732 
3733  if (!cmd.arguments().empty()) {
3734  QString modelType = cmd.arguments().takeFirst().toUpper();
3735  if (modelType == "LS") {
3736  modelIndex = 1;
3737  } else if (modelType == "TRI") {
3738  modelIndex = 2;
3739  } else if (modelType == "HF") {
3740  modelIndex = 3;
3741  } else {
3742  return cmd.error(QObject::tr("Invalid parameter: unknown "
3743  "model type \"%1\"")
3744  .arg(modelType));
3745  }
3746  } else {
3747  return cmd.error(QObject::tr("Missing parameter: model type "
3748  "after \"-%1\" (LS/TRI/HF)")
3749  .arg(COMMAND_C2C_LOCAL_MODEL));
3750  }
3751 
3752  if (!cmd.arguments().empty()) {
3753  QString nType = cmd.arguments().takeFirst().toUpper();
3754  if (nType == "KNN") {
3755  useKNN = true;
3756  } else if (nType == "SPHERE") {
3757  useKNN = false;
3758  } else {
3759  return cmd.error(QObject::tr("Invalid parameter: unknown "
3760  "neighborhood type \"%1\"")
3761  .arg(nType));
3762  }
3763  } else {
3764  return cmd.error(
3765  QObject::tr("Missing parameter: expected neighborhood "
3766  "type after model type (KNN/SPHERE)"));
3767  }
3768 
3769  // neighborhood size
3770  if (!cmd.arguments().empty()) {
3771  bool conversionOk = false;
3772  nSize = cmd.arguments().takeFirst().toDouble(&conversionOk);
3773  if (!conversionOk) {
3774  return cmd.error(QObject::tr(
3775  "Invalid parameter: neighborhood size"));
3776  }
3777  } else {
3778  return cmd.error(QObject::tr(
3779  "Missing parameter: expected neighborhood size after "
3780  "neighborhood type (neighbor count/sphere radius)"));
3781  }
3783  argument, COMMAND_MAX_THREAD_COUNT)) {
3784  // local option confirmed, we can move on
3785  cmd.arguments().pop_front();
3786 
3787  if (cmd.arguments().empty()) {
3788  return cmd.error(QObject::tr("Missing parameter: max thread "
3789  "count after '%1'")
3790  .arg(COMMAND_MAX_THREAD_COUNT));
3791  }
3792 
3793  bool ok;
3794  maxThreadCount = cmd.arguments().takeFirst().toInt(&ok);
3795  if (!ok || maxThreadCount < 0) {
3796  return cmd.error(QObject::tr("Invalid thread count! (after %1)")
3797  .arg(COMMAND_MAX_THREAD_COUNT));
3798  }
3799  } else {
3800  break; // as soon as we encounter an unrecognized argument, we
3801  // break the local loop to go back to the main one!
3802  }
3803  }
3804 
3805  // spawn dialog (virtually) so as to prepare the comparison process
3806  ccComparisonDlg compDlg(compCloud, refEntity,
3809  cmd.widgetParent(), true);
3810 
3811  if (!compDlg.initDialog()) {
3812  return cmd.error(QObject::tr("Failed to initialize comparison dialog"));
3813  }
3814 
3815  // update parameters
3816  if (maxDist > 0) {
3817  compDlg.maxDistCheckBox->setChecked(true);
3818  compDlg.maxSearchDistSpinBox->setValue(maxDist);
3819  }
3820  if (octreeLevel > 0) {
3821  compDlg.octreeLevelComboBox->setCurrentIndex(octreeLevel);
3822  }
3823  if (maxThreadCount != 0) {
3824  compDlg.maxThreadCountSpinBox->setValue(maxThreadCount);
3825  }
3826 
3827  // C2M-only parameters
3828  if (m_cloud2meshDist) {
3829  if (flipNormals) {
3830  compDlg.flipNormalsCheckBox->setChecked(true);
3831  }
3832  }
3833  // C2C-only parameters
3834  else {
3835  if (splitXYZ) {
3836  // DGM: not true anymore
3837  // if (maxDist > 0)
3838  // cmd.warning("'Split XYZ' option is ignored if max distance is
3839  // defined!");
3840  compDlg.split3DCheckBox->setChecked(true);
3841  }
3842  if (modelIndex != 0) {
3843  compDlg.localModelComboBox->setCurrentIndex(modelIndex);
3844  if (useKNN) {
3845  compDlg.lmKNNRadioButton->setChecked(true);
3846  compDlg.lmKNNSpinBox->setValue(static_cast<int>(nSize));
3847  } else {
3848  compDlg.lmRadiusRadioButton->setChecked(true);
3849  compDlg.lmRadiusDoubleSpinBox->setValue(nSize);
3850  }
3851  }
3852  }
3853 
3854  if (!compDlg.computeDistances()) {
3855  compDlg.cancelAndExit();
3856  return cmd.error(
3857  QObject::tr("An error occurred during distances computation!"));
3858  }
3859 
3860  compDlg.applyAndExit();
3861 
3862  QString suffix(m_cloud2meshDist ? "_C2M_DIST" : "_C2C_DIST");
3863  if (maxDist > 0) {
3864  suffix += QObject::tr("_MAX_DIST_%1").arg(maxDist);
3865  }
3866 
3867  compEntity->basename += suffix;
3868 
3869  if (cmd.autoSaveMode()) {
3870  QString errorStr = cmd.exportEntity(*compEntity);
3871  if (!errorStr.isEmpty()) {
3872  return cmd.error(errorStr);
3873  }
3874  }
3875 
3876  return true;
3877 }
3878 
3880  : CommandDist(true, QObject::tr("C2M distance"), COMMAND_C2M_DIST) {}
3881 
3883  : CommandDist(false, QObject::tr("C2C distance"), COMMAND_C2C_DIST) {}
3884 
3886  : ccCommandLineInterface::Command(QObject::tr("Closest Point Set"),
3888 
3890  cmd.print(QObject::tr("[CLOSEST POINT SET]"));
3891 
3892  if (cmd.clouds().size() < 2) {
3893  return cmd.error(
3894  QObject::tr("At least two point clouds are needed to compute "
3895  "the closest point set!"));
3896  } else if (cmd.clouds().size() > 2) {
3897  cmd.warning(
3898  QObject::tr("More than 3 point clouds loaded! We take the "
3899  "second one as reference by default"));
3900  }
3901 
3902  // COMPARED CLOUD / REFERENCE CLOUD
3903  CLCloudDesc compDesc = cmd.clouds().front();
3904  CLCloudDesc refDesc = cmd.clouds()[1];
3905  ccPointCloud* compPointCloud = compDesc.pc;
3906  ccPointCloud* refPointCloud = refDesc.pc;
3907 
3908  assert(compPointCloud && refPointCloud);
3909 
3910  ecvProgressDialog pDlg(true, nullptr);
3912  params;
3913  cloudViewer::ReferenceCloud closestPointSet(refPointCloud);
3914  params.CPSet = &closestPointSet;
3915 
3916  // COMPUTE CLOUD 2 CLOUD DISTANCE, THIS INCLUDES THE CLOSEST POINT SET
3917  // GENERATION
3918  int result =
3920  compPointCloud, refPointCloud, params, &pDlg);
3921 
3922  if (result >= 0) {
3923  // the extracted CPS will get the attributes of the reference cloud
3924  ccPointCloud* newCloud = refPointCloud->partialClone(&closestPointSet);
3925  // give to newCloud a name similar to the one generated by the GUI
3926  // Closest Point Set tool
3927  CLCloudDesc cloudDesc(
3928  newCloud,
3929  "[" + refDesc.basename + "]_CPSet(" + compDesc.basename + ")",
3930  cmd.clouds()[0].path);
3931  QString errorStr = cmd.exportEntity(
3932  cloudDesc, QString(), nullptr,
3934  if (!errorStr.isEmpty()) {
3935  cmd.error(errorStr);
3936  }
3937  // add cloud to the current pool
3938  cmd.clouds().push_back(cloudDesc);
3939  }
3940 
3941  return true;
3942 }
3943 
3945  : ccCommandLineInterface::Command(QObject::tr("Statistical test"),
3946  COMMAND_STAT_TEST) {}
3947 
3949  cmd.print(QObject::tr("[STATISTICAL TEST]"));
3950 
3951  // distribution
3952  cloudViewer::GenericDistribution* distrib = nullptr;
3953  {
3954  if (cmd.arguments().empty()) {
3955  return cmd.error(QObject::tr("Missing parameter: distribution type "
3956  "after \"-%1\" (GAUSS/WEIBULL)")
3957  .arg(COMMAND_STAT_TEST));
3958  }
3959 
3960  QString distribStr = cmd.arguments().takeFirst().toUpper();
3961  if (distribStr == "GAUSS") {
3962  // mu
3963  if (cmd.arguments().empty()) {
3964  return cmd.error(QObject::tr(
3965  "Missing parameter: mean value after \"GAUSS\""));
3966  }
3967  bool conversionOk = false;
3968  double mu = cmd.arguments().takeFirst().toDouble(&conversionOk);
3969  if (!conversionOk) {
3970  return cmd.error(QObject::tr(
3971  "Invalid parameter: mean value after \"GAUSS\""));
3972  }
3973  // sigma
3974  if (cmd.arguments().empty()) {
3975  return cmd.error(QObject::tr(
3976  "Missing parameter: sigma value after \"GAUSS\" {mu}"));
3977  }
3978  conversionOk = false;
3979  double sigma = cmd.arguments().takeFirst().toDouble(&conversionOk);
3980  if (!conversionOk) {
3981  return cmd.error(QObject::tr(
3982  "Invalid parameter: sigma value after \"GAUSS\" {mu}"));
3983  }
3984 
3987  N->setParameters(
3988  static_cast<ScalarType>(mu),
3989  static_cast<ScalarType>(sigma *
3990  sigma)); // warning: we input
3991  // sigma2 here (not sigma)
3992  distrib = static_cast<cloudViewer::GenericDistribution*>(N);
3993  } else if (distribStr == "WEIBULL") {
3994  // a
3995  if (cmd.arguments().empty()) {
3996  return cmd.error(QObject::tr(
3997  "Missing parameter: a value after \"WEIBULL\""));
3998  }
3999  bool conversionOk = false;
4000  double a = cmd.arguments().takeFirst().toDouble(&conversionOk);
4001  if (!conversionOk) {
4002  return cmd.error(QObject::tr(
4003  "Invalid parameter: a value after \"WEIBULL\""));
4004  }
4005  // b
4006  if (cmd.arguments().empty()) {
4007  return cmd.error(QObject::tr(
4008  "Missing parameter: b value after \"WEIBULL\" {a}"));
4009  }
4010  conversionOk = false;
4011  double b = cmd.arguments().takeFirst().toDouble(&conversionOk);
4012  if (!conversionOk) {
4013  return cmd.error(QObject::tr(
4014  "Invalid parameter: b value after \"WEIBULL\" {a}"));
4015  }
4016  // c
4017  if (cmd.arguments().empty()) {
4018  return cmd.error(
4019  QObject::tr("Missing parameter: shift value after "
4020  "\"WEIBULL\" {a} {b}"));
4021  }
4022  conversionOk = false;
4023  double shift = cmd.arguments().takeFirst().toDouble(&conversionOk);
4024  if (!conversionOk) {
4025  return cmd.error(
4026  QObject::tr("Invalid parameter: shift value after "
4027  "\"WEIBULL\" {a} {b}"));
4028  }
4029 
4032  N->setParameters(static_cast<ScalarType>(a),
4033  static_cast<ScalarType>(b),
4034  static_cast<ScalarType>(shift));
4035  distrib = static_cast<cloudViewer::GenericDistribution*>(N);
4036  } else {
4037  return cmd.error(
4038  QObject::tr(
4039  "Invalid parameter: unknown distribution \"%1\"")
4040  .arg(distribStr));
4041  }
4042  }
4043 
4044  // pValue
4045  double pValue = 0.0005;
4046  {
4047  if (cmd.arguments().empty()) {
4048  return cmd.error(QObject::tr(
4049  "Missing parameter: p-value after distribution"));
4050  }
4051  bool conversionOk = false;
4052  pValue = cmd.arguments().takeFirst().toDouble(&conversionOk);
4053  if (!conversionOk) {
4054  return cmd.error(QObject::tr(
4055  "Invalid parameter: p-value after distribution"));
4056  }
4057  }
4058 
4059  // kNN
4060  unsigned kNN = 16;
4061  {
4062  if (cmd.arguments().empty()) {
4063  return cmd.error(
4064  QObject::tr("Missing parameter: neighbors after p-value"));
4065  }
4066  bool conversionOk = false;
4067  kNN = cmd.arguments().takeFirst().toUInt(&conversionOk);
4068  if (!conversionOk) {
4069  return cmd.error(
4070  QObject::tr("Invalid parameter: neighbors after p-value"));
4071  }
4072  }
4073 
4074  if (cmd.clouds().empty()) {
4075  return cmd.error(
4076  QObject::tr("No cloud available. Be sure to open one first!"));
4077  }
4078 
4079  QScopedPointer<ecvProgressDialog> progressDialog(nullptr);
4080  if (!cmd.silentMode()) {
4081  progressDialog.reset(new ecvProgressDialog(false, cmd.widgetParent()));
4082  progressDialog->setAutoClose(false);
4083  }
4084 
4085  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
4086  ccPointCloud* pc = cmd.clouds()[i].pc;
4087 
4088  // we apply method on currently 'output' SF
4090  if (outSF) {
4091  assert(outSF->capacity() != 0);
4092 
4093  // force Chi2 Distances field as 'IN' field (create it by the way if
4094  // necessary)
4095  int chi2SfIdx = pc->getScalarFieldIndexByName(
4097  if (chi2SfIdx < 0) {
4098  chi2SfIdx =
4100  }
4101  if (chi2SfIdx < 0) {
4102  delete distrib;
4103  return cmd.error(QObject::tr(
4104  "Couldn't allocate a new scalar field for computing "
4105  "chi2 distances! Try to free some memory ..."));
4106  }
4107  pc->setCurrentInScalarField(chi2SfIdx);
4108 
4109  // compute octree if necessary
4110  ccOctree::Shared theOctree = pc->getOctree();
4111  if (!theOctree) {
4112  theOctree = pc->computeOctree(progressDialog.data());
4113  if (!theOctree) {
4114  delete distrib;
4115  cmd.error(QObject::tr(
4116  "Couldn't compute octree for cloud '%1'!")
4117  .arg(pc->getName()));
4118  break;
4119  }
4120  }
4121 
4122  double chi2dist = cloudViewer::StatisticalTestingTools::
4123  testCloudWithStatisticalModel(distrib, pc, kNN, pValue,
4124  progressDialog.data(),
4125  theOctree.data());
4126 
4127  cmd.print(QObject::tr("[Chi2 Test] %1 test result = %2")
4128  .arg(distrib->getName())
4129  .arg(chi2dist));
4130 
4131  // we set the theoretical Chi2 distance limit as the minimum
4132  // displayed SF value so that all points below are grayed
4133  {
4134  ccScalarField* chi2SF = static_cast<ccScalarField*>(
4135  pc->getCurrentInScalarField());
4136  assert(chi2SF);
4137  chi2SF->computeMinAndMax();
4138  chi2dist *= chi2dist;
4139  chi2SF->setMinDisplayed(static_cast<ScalarType>(chi2dist));
4140  chi2SF->setSymmetricalScale(false);
4141  chi2SF->setSaturationStart(static_cast<ScalarType>(chi2dist));
4142  // chi2SF->setSaturationStop(chi2dist);
4143  pc->setCurrentDisplayedScalarField(chi2SfIdx);
4144  pc->showSF(true);
4145  }
4146 
4147  cmd.clouds()[i].basename +=
4148  QObject::tr("_STAT_TEST_%1").arg(distrib->getName());
4149  if (cmd.autoSaveMode()) {
4150  QString errorStr = cmd.exportEntity(cmd.clouds()[i]);
4151  if (!errorStr.isEmpty()) {
4152  return cmd.error(errorStr);
4153  }
4154  }
4155  }
4156  }
4157 
4158  if (progressDialog) {
4159  progressDialog->close();
4160  QCoreApplication::processEvents();
4161  }
4162 
4163  return true;
4164 }
4165 
4167  : ccCommandLineInterface::Command(QObject::tr("Delaunay triangulation"),
4168  COMMAND_DELAUNAY) {}
4169 
4171  cmd.print(QObject::tr("[DELAUNAY TRIANGULATION]"));
4172 
4173  bool axisAligned = true;
4174  double maxEdgeLength = 0;
4175 
4176  while (!cmd.arguments().empty()) {
4177  QString argument = cmd.arguments().front();
4179  // local option confirmed, we can move on
4180  cmd.arguments().pop_front();
4181  axisAligned = true;
4182  } else if (ccCommandLineInterface::IsCommand(argument,
4184  // local option confirmed, we can move on
4185  cmd.arguments().pop_front();
4186  axisAligned = false;
4188  argument, COMMAND_DELAUNAY_MAX_EDGE_LENGTH)) {
4189  // local option confirmed, we can move on
4190  cmd.arguments().pop_front();
4191 
4192  if (cmd.arguments().empty()) {
4193  return cmd.error(
4194  QObject::tr("Missing parameter: max edge length value "
4195  "after '%1'")
4197  }
4198  bool ok;
4199  maxEdgeLength = cmd.arguments().takeFirst().toDouble(&ok);
4200  if (!ok) {
4201  return cmd.error(
4202  QObject::tr(
4203  "Invalid value for max edge length! (after %1)")
4205  cmd.print(
4206  QObject::tr("Max edge length: %1").arg(maxEdgeLength));
4207  }
4208  } else {
4209  break;
4210  }
4211  }
4212 
4213  cmd.print(QObject::tr("Axis aligned: %1").arg(axisAligned ? "yes" : "no"));
4214 
4215  // try to triangulate each cloud
4216  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
4217  ccPointCloud* cloud = cmd.clouds()[i].pc;
4218  cmd.print(QObject::tr("\tProcessing cloud #%1 (%2)")
4219  .arg(i + 1)
4220  .arg(!cloud->getName().isEmpty() ? cloud->getName()
4221  : "no name"));
4222 
4223  ccMesh* mesh = ccMesh::Triangulate(
4224  cloud,
4227  false, static_cast<PointCoordinateType>(maxEdgeLength),
4228  2 // XY plane by default
4229  );
4230 
4231  if (mesh) {
4232  cmd.print(QObject::tr("\tResulting mesh: #%1 faces, %2 vertices")
4233  .arg(mesh->size())
4234  .arg(mesh->getAssociatedCloud()->size()));
4235 
4236  CLMeshDesc meshDesc;
4237  meshDesc.mesh = mesh;
4238  meshDesc.basename = cmd.clouds()[i].basename;
4239  meshDesc.path = cmd.clouds()[i].path;
4240  meshDesc.indexInFile = cmd.clouds()[i].indexInFile;
4241 
4242  // save mesh
4243  if (cmd.autoSaveMode()) {
4244  QString outputFilename;
4245  QString errorStr =
4246  cmd.exportEntity(meshDesc, "DELAUNAY", &outputFilename);
4247  if (!errorStr.isEmpty()) {
4248  cmd.warning(errorStr);
4249  }
4250  }
4251 
4252  // add the resulting mesh to the main set
4253  cmd.meshes().push_back(meshDesc);
4254 
4255  // the mesh takes ownership of the cloud.
4256  // Therefore we have to remove all clouds from the 'cloud set'! (see
4257  // below) (otherwise bad things will happen when we'll clear it
4258  // later ;)
4259  cloud->setEnabled(false);
4260  mesh->addChild(cloud);
4261  }
4262  }
4263  // mehses have taken ownership of the clouds!
4264  cmd.clouds().resize(0);
4265 
4266  return true;
4267 }
4268 
4270  : ccCommandLineInterface::Command(QObject::tr("SF arithmetic"),
4272 
4274  cmd.print(QObject::tr("[SF ARITHMETIC]"));
4275 
4276  if (cmd.arguments().size() < 2) {
4277  return cmd.error(QObject::tr("Missing parameter(s): SF index and/or "
4278  "operation after '%1' (2 values expected)")
4279  .arg(COMMAND_SF_ARITHMETIC));
4280  }
4281 
4282  // read sf index
4283  int sfIndex = -1;
4284  bool ok = true;
4285  QString sfIndexStr = cmd.arguments().takeFirst();
4286  if (sfIndexStr.toUpper() == OPTION_LAST) {
4287  sfIndex = -2;
4288  } else {
4289  sfIndex = sfIndexStr.toInt(&ok);
4290  }
4291  if (!ok || sfIndex < 0) {
4292  return cmd.error(QObject::tr("Invalid SF index! (after %1)")
4293  .arg(COMMAND_SF_ARITHMETIC));
4294  }
4295 
4296  // read operation type
4299  {
4300  QString opName = cmd.arguments().takeFirst();
4302  if (operation == ccScalarFieldArithmeticsDlg::INVALID) {
4303  return cmd.error(
4304  QObject::tr("Unknown operation! (%1)").arg(opName));
4305  } else if (operation <= ccScalarFieldArithmeticsDlg::DIVIDE) {
4306  return cmd.error(
4307  QObject::tr("Operation %1 can't be applied with %2")
4308  .arg(opName, COMMAND_SF_ARITHMETIC));
4309  }
4310  }
4311 
4312  // apply operation on clouds
4313  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
4314  ccPointCloud* cloud = cmd.clouds()[i].pc;
4315  if (cloud && cloud->getNumberOfScalarFields() != 0 &&
4316  sfIndex < static_cast<int>(cloud->getNumberOfScalarFields())) {
4318  cloud, operation,
4319  sfIndex < 0
4320  ? static_cast<int>(
4321  cloud->getNumberOfScalarFields()) -
4322  1
4323  : sfIndex,
4324  false)) {
4325  return cmd.error(
4326  QObject::tr("Failed top apply operation on cloud '%1'")
4327  .arg(cloud->getName()));
4328  } else if (cmd.autoSaveMode()) {
4329  QString errorStr =
4330  cmd.exportEntity(cmd.clouds()[i], "SF_ARITHMETIC");
4331  if (!errorStr.isEmpty()) {
4332  return cmd.error(errorStr);
4333  }
4334  }
4335  }
4336  }
4337 
4338  // and meshes!
4339  for (size_t j = 0; j < cmd.meshes().size(); ++j) {
4340  bool isLocked = false;
4341  ccGenericMesh* mesh = cmd.meshes()[j].mesh;
4342  ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(mesh, &isLocked);
4343  if (cloud && !isLocked && cloud->getNumberOfScalarFields() != 0 &&
4344  sfIndex < static_cast<int>(cloud->getNumberOfScalarFields())) {
4346  cloud, operation,
4347  sfIndex < 0
4348  ? static_cast<int>(
4349  cloud->getNumberOfScalarFields()) -
4350  1
4351  : sfIndex,
4352  false)) {
4353  return cmd.error(
4354  QObject::tr("Failed top apply operation on mesh '%1'")
4355  .arg(mesh->getName()));
4356  } else if (cmd.autoSaveMode()) {
4357  QString errorStr =
4358  cmd.exportEntity(cmd.meshes()[j], "SF_ARITHMETIC");
4359  if (!errorStr.isEmpty()) {
4360  return cmd.error(errorStr);
4361  }
4362  }
4363  }
4364  }
4365 
4366  return true;
4367 }
4368 
4370  : ccCommandLineInterface::Command(QObject::tr("SF operation"),
4371  COMMAND_SF_OP) {}
4372 
4374  cmd.print(QObject::tr("[SF OPERATION]"));
4375 
4376  if (cmd.arguments().size() < 3) {
4377  return cmd.error(
4378  QObject::tr(
4379  "Missing parameter(s): SF index and/or operation "
4380  "and/or scalar value after '%1' (3 values expected)")
4381  .arg(COMMAND_SF_OP));
4382  }
4383 
4384  // read sf index
4385  int sfIndex = -1;
4386  bool ok = true;
4387  QString sfIndexStr = cmd.arguments().takeFirst();
4388  if (sfIndexStr.toUpper() == OPTION_LAST) {
4389  sfIndex = -2;
4390  } else {
4391  sfIndex = sfIndexStr.toInt(&ok);
4392  }
4393 
4394  if (!ok || sfIndex < 0) {
4395  return cmd.error(
4396  QObject::tr("Invalid SF index! (after %1)").arg(COMMAND_SF_OP));
4397  }
4398 
4399  // read operation type
4402  {
4403  QString opName = cmd.arguments().takeFirst();
4405  if (operation == ccScalarFieldArithmeticsDlg::INVALID) {
4406  return cmd.error(
4407  QObject::tr("Unknown operation! (%1)").arg(opName));
4408  } else if (operation > ccScalarFieldArithmeticsDlg::DIVIDE) {
4409  return cmd.error(
4410  QObject::tr("Operation %1 can't be applied with %2")
4411  .arg(opName, COMMAND_SF_OP));
4412  }
4413  }
4414 
4415  // read scalar value
4416  double value = 1.0;
4417  {
4418  bool ok = true;
4419  value = cmd.arguments().takeFirst().toDouble(&ok);
4420  if (!ok) {
4421  return cmd.error(QObject::tr("Invalid scalar value! (after %1)")
4422  .arg(COMMAND_SF_OP));
4423  }
4424  }
4425 
4427  {
4428  sf2.isConstantValue = true;
4429  sf2.constantValue = value;
4430  }
4431 
4432  // apply operation on clouds
4433  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
4434  ccPointCloud* cloud = cmd.clouds()[i].pc;
4435  if (cloud && cloud->getNumberOfScalarFields() != 0 &&
4436  sfIndex < static_cast<int>(cloud->getNumberOfScalarFields())) {
4438  cloud, operation,
4439  sfIndex < 0
4440  ? static_cast<int>(
4441  cloud->getNumberOfScalarFields()) -
4442  1
4443  : sfIndex,
4444  true, &sf2)) {
4445  return cmd.error(
4446  QObject::tr("Failed top apply operation on cloud '%1'")
4447  .arg(cloud->getName()));
4448  } else if (cmd.autoSaveMode()) {
4449  QString errorStr = cmd.exportEntity(cmd.clouds()[i], "SF_OP");
4450  if (!errorStr.isEmpty()) {
4451  return cmd.error(errorStr);
4452  }
4453  }
4454  }
4455  }
4456 
4457  // and meshes!
4458  for (size_t j = 0; j < cmd.meshes().size(); ++j) {
4459  bool isLocked = false;
4460  ccGenericMesh* mesh = cmd.meshes()[j].mesh;
4461  ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(mesh, &isLocked);
4462  if (cloud && !isLocked && cloud->getNumberOfScalarFields() != 0 &&
4463  sfIndex < static_cast<int>(cloud->getNumberOfScalarFields())) {
4465  cloud, operation,
4466  sfIndex < 0
4467  ? static_cast<int>(
4468  cloud->getNumberOfScalarFields()) -
4469  1
4470  : sfIndex,
4471  true, &sf2)) {
4472  return cmd.error(
4473  QObject::tr("Failed top apply operation on mesh '%1'")
4474  .arg(mesh->getName()));
4475  } else if (cmd.autoSaveMode()) {
4476  QString errorStr = cmd.exportEntity(cmd.meshes()[j], "SF_OP");
4477  if (!errorStr.isEmpty()) {
4478  return cmd.error(errorStr);
4479  }
4480  }
4481  }
4482  }
4483 
4484  return true;
4485 }
4486 
4488  : ccCommandLineInterface::Command(QObject::tr("Rename SF"),
4489  COMMAND_RENAME_SF) {}
4490 
4492  cmd.print(QObject::tr("[RENAME SF]"));
4493 
4494  if (cmd.arguments().size() < 2) {
4495  return cmd.error(
4496  QObject::tr("Missing parameter(s): SF index and/or scalar "
4497  "field name after '%1' (2 values expected)")
4498  .arg(COMMAND_RENAME_SF));
4499  }
4500 
4501  // read sf index
4502  int sfIndex = -1;
4503  bool ok = true;
4504  QString sfIndexStr = cmd.arguments().takeFirst();
4505  if (sfIndexStr.toUpper() == OPTION_LAST) {
4506  sfIndex = -2;
4507  } else {
4508  sfIndex = sfIndexStr.toInt(&ok);
4509  }
4510 
4511  if (!ok || sfIndex == -1) {
4512  return cmd.error(
4513  QObject::tr("Invalid SF index! (after %1)").arg(COMMAND_SF_OP));
4514  }
4515 
4516  // read the SF name
4517  QString sfName = cmd.arguments().takeFirst();
4518 
4519  // apply operation on clouds
4520  for (CLCloudDesc& cloudDesc : cmd.clouds()) {
4521  ccPointCloud* cloud = cloudDesc.pc;
4522  if (cloud && cloud->getNumberOfScalarFields() != 0 &&
4523  sfIndex < static_cast<int>(cloud->getNumberOfScalarFields())) {
4524  int thisSFIndex =
4525  (sfIndex < 0 ? static_cast<int>(
4526  cloud->getNumberOfScalarFields()) -
4527  1
4528  : sfIndex);
4529  int indexOfSFWithSameName =
4530  cloud->getScalarFieldIndexByName(qPrintable(sfName));
4531  if (indexOfSFWithSameName >= 0 &&
4532  thisSFIndex != indexOfSFWithSameName) {
4533  return cmd.error(
4534  "A SF with the same name is already defined on cloud " +
4535  cloud->getName());
4536  }
4537  cloudViewer::ScalarField* sf = cloud->getScalarField(thisSFIndex);
4538  if (!sf) {
4539  assert(false);
4540  return cmd.error("Internal error: invalid SF index");
4541  }
4542  sf->setName(qPrintable(sfName));
4543 
4544  if (cmd.autoSaveMode()) {
4545  QString errorStr = cmd.exportEntity(cloudDesc, "SF_RENAMED");
4546  if (!errorStr.isEmpty()) {
4547  return cmd.error(errorStr);
4548  }
4549  }
4550  }
4551  }
4552 
4553  // and meshes!
4554  for (CLMeshDesc& meshDesc : cmd.meshes()) {
4555  bool isLocked = false;
4556  ccGenericMesh* mesh = meshDesc.mesh;
4557  ccPointCloud* cloud = ccHObjectCaster::ToPointCloud(mesh, &isLocked);
4558  if (cloud && !isLocked && cloud->getNumberOfScalarFields() != 0 &&
4559  sfIndex < static_cast<int>(cloud->getNumberOfScalarFields())) {
4560  int thisSFIndex =
4561  (sfIndex < 0 ? static_cast<int>(
4562  cloud->getNumberOfScalarFields()) -
4563  1
4564  : sfIndex);
4565  int indexOfSFWithSameName =
4566  cloud->getScalarFieldIndexByName(qPrintable(sfName));
4567  if (indexOfSFWithSameName >= 0 &&
4568  thisSFIndex != indexOfSFWithSameName) {
4569  return cmd.error(
4570  "A SF with the same name is already defined on cloud " +
4571  cloud->getName());
4572  }
4573  cloudViewer::ScalarField* sf = cloud->getScalarField(thisSFIndex);
4574  if (!sf) {
4575  assert(false);
4576  return cmd.error("Internal error: invalid SF index");
4577  }
4578  sf->setName(qPrintable(sfName));
4579 
4580  if (cmd.autoSaveMode()) {
4581  QString errorStr = cmd.exportEntity(meshDesc, "SF_RENAMED");
4582  if (!errorStr.isEmpty()) {
4583  return cmd.error(errorStr);
4584  }
4585  }
4586  }
4587  }
4588 
4589  return true;
4590 }
4591 
4593  : ccCommandLineInterface::Command("ICP", COMMAND_ICP) {}
4594 
4596  cmd.print(QObject::tr("[ICP]"));
4597 
4598  // look for local options
4599  bool referenceIsFirst = false;
4600  bool adjustScale = false;
4601  bool enableFarthestPointRemoval = false;
4602  double minErrorDiff = 1.0e-6;
4603  unsigned iterationCount = 0;
4604  unsigned randomSamplingLimit = 20000;
4605  unsigned overlap = 100;
4606  int modelSFAsWeights = -1;
4607  int dataSFAsWeights = -1;
4608  int maxThreadCount = 0;
4609  int transformationFilters = 0;
4610 
4611  while (!cmd.arguments().empty()) {
4612  QString argument = cmd.arguments().front();
4613  if (ccCommandLineInterface::IsCommand(argument,
4615  // local option confirmed, we can move on
4616  cmd.arguments().pop_front();
4617 
4618  referenceIsFirst = true;
4620  argument, COMMAND_ICP_ADJUST_SCALE)) {
4621  // local option confirmed, we can move on
4622  cmd.arguments().pop_front();
4623 
4624  adjustScale = true;
4627  // local option confirmed, we can move on
4628  cmd.arguments().pop_front();
4629 
4630  enableFarthestPointRemoval = true;
4632  argument, COMMAND_ICP_MIN_ERROR_DIIF)) {
4633  // local option confirmed, we can move on
4634  cmd.arguments().pop_front();
4635 
4636  if (cmd.arguments().empty()) {
4637  return cmd.error(QObject::tr("Missing parameter: min error "
4638  "difference after '%1'")
4640  }
4641  bool ok;
4642  minErrorDiff = cmd.arguments().takeFirst().toDouble(&ok);
4643  if (!ok || minErrorDiff <= 0) {
4644  return cmd.error(QObject::tr("Invalid value for min. error "
4645  "difference! (after %1)")
4647  }
4649  argument, COMMAND_ICP_ITERATION_COUNT)) {
4650  // local option confirmed, we can move on
4651  cmd.arguments().pop_front();
4652 
4653  if (cmd.arguments().empty()) {
4654  return cmd.error(QObject::tr("Missing parameter: number of "
4655  "iterations after '%1'")
4657  }
4658  bool ok;
4659  QString arg = cmd.arguments().takeFirst();
4660  iterationCount = arg.toUInt(&ok);
4661  if (!ok || iterationCount == 0)
4662  return cmd.error(
4663  QObject::tr("Invalid number of iterations! (%1)")
4664  .arg(arg));
4665  } else if (ccCommandLineInterface::IsCommand(argument,
4667  // local option confirmed, we can move on
4668  cmd.arguments().pop_front();
4669 
4670  if (cmd.arguments().empty()) {
4671  return cmd.error(QObject::tr("Missing parameter: overlap "
4672  "percentage after '%1'")
4673  .arg(COMMAND_ICP_OVERLAP));
4674  }
4675  bool ok;
4676  QString arg = cmd.arguments().takeFirst();
4677  overlap = arg.toUInt(&ok);
4678  if (!ok || overlap < 10 || overlap > 100) {
4679  return cmd.error(QObject::tr("Invalid overlap value! (%1 --> "
4680  "should be between 10 and 100)")
4681  .arg(arg));
4682  }
4684  argument, COMMAND_ICP_RANDOM_SAMPLING_LIMIT)) {
4685  // local option confirmed, we can move on
4686  cmd.arguments().pop_front();
4687 
4688  if (cmd.arguments().empty()) {
4689  return cmd.error(
4690  QObject::tr("Missing parameter: random sampling limit "
4691  "value after '%1'")
4693  }
4694  bool ok;
4695  randomSamplingLimit = cmd.arguments().takeFirst().toUInt(&ok);
4696  if (!ok || randomSamplingLimit < 3) {
4697  return cmd.error(
4698  QObject::tr("Invalid random sampling limit! (after %1)")
4700  }
4703  // local option confirmed, we can move on
4704  cmd.arguments().pop_front();
4705 
4706  if (cmd.arguments().empty()) {
4707  return cmd.error(
4708  QObject::tr("Missing parameter: SF index after '%1'")
4710  }
4711  QString sfIndex = cmd.arguments().takeFirst();
4712  if (sfIndex.toUpper() == OPTION_LAST) {
4713  modelSFAsWeights = -2;
4714  } else {
4715  bool ok;
4716  modelSFAsWeights = sfIndex.toInt(&ok);
4717  if (!ok || modelSFAsWeights < 0) {
4718  return cmd.error(
4719  QObject::tr("Invalid SF index! (after %1)")
4721  }
4722  }
4724  argument, COMMAND_ICP_USE_DATA_SF_AS_WEIGHT)) {
4725  // local option confirmed, we can move on
4726  cmd.arguments().pop_front();
4727 
4728  if (cmd.arguments().empty()) {
4729  return cmd.error(
4730  QObject::tr("Missing parameter: SF index after '%1'")
4732  }
4733  QString sfIndex = cmd.arguments().takeFirst();
4734  if (sfIndex.toUpper() == OPTION_LAST) {
4735  dataSFAsWeights = -2;
4736  } else {
4737  bool ok;
4738  dataSFAsWeights = sfIndex.toInt(&ok);
4739  if (!ok || dataSFAsWeights < 0) {
4740  return cmd.error(
4741  QObject::tr("Invalid SF index! (after %1)")
4743  }
4744  }
4746  argument, COMMAND_MAX_THREAD_COUNT)) {
4747  // local option confirmed, we can move on
4748  cmd.arguments().pop_front();
4749 
4750  if (cmd.arguments().empty()) {
4751  return cmd.error(QObject::tr("Missing parameter: max thread "
4752  "count after '%1'")
4753  .arg(COMMAND_MAX_THREAD_COUNT));
4754  }
4755 
4756  bool ok;
4757  maxThreadCount = cmd.arguments().takeFirst().toInt(&ok);
4758  if (!ok || maxThreadCount < 0) {
4759  return cmd.error(QObject::tr("Invalid thread count! (after %1)")
4760  .arg(COMMAND_MAX_THREAD_COUNT));
4761  }
4762  } else if (ccCommandLineInterface::IsCommand(argument,
4763  COMMAND_ICP_ROT)) {
4764  // local option confirmed, we can move on
4765  cmd.arguments().pop_front();
4766 
4767  if (!cmd.arguments().empty()) {
4768  QString rotation = cmd.arguments().takeFirst().toUpper();
4769  if (rotation == "XYZ") {
4770  transformationFilters = 0;
4771  } else if (rotation == "X") {
4772  transformationFilters = 2;
4773  } else if (rotation == "Y") {
4774  transformationFilters = 4;
4775  } else if (rotation == "Z") {
4776  transformationFilters = 1;
4777  } else if (rotation == "NONE") {
4778  transformationFilters = 7;
4779  } else {
4780  return cmd.error(QObject::tr("Invalid parameter: unknown "
4781  "rotation filter \"%1\"")
4782  .arg(rotation));
4783  }
4784  } else {
4785  return cmd.error(
4786  QObject::tr("Missing parameter: rotation filter after "
4787  "\"-%1\" (XYZ/X/Y/Z/NONE)")
4788  .arg(COMMAND_ICP_ROT));
4789  }
4790  } else {
4791  break; // as soon as we encounter an unrecognized argument, we
4792  // break the local loop to go back to the main one!
4793  }
4794  }
4795 
4796  // we'll get the first two entities
4797  CLEntityDesc* dataAndModel[2] = {nullptr, nullptr};
4798  {
4799  int index = 0;
4800  if (!cmd.clouds().empty()) {
4801  dataAndModel[index++] = &cmd.clouds()[0];
4802  if (cmd.clouds().size() > 1) {
4803  dataAndModel[index++] = &cmd.clouds()[1];
4804  }
4805  }
4806  if (index < 2 && !cmd.meshes().empty()) {
4807  dataAndModel[index++] = &cmd.meshes()[0];
4808  if (index < 2 && cmd.meshes().size() > 1) {
4809  dataAndModel[index++] = &cmd.meshes()[1];
4810  }
4811  }
4812 
4813  if (index < 2) {
4814  return cmd.error(QObject::tr(
4815  "Not enough loaded entities (expect at least 2!)"));
4816  }
4817  }
4818 
4819  // put them in the right order (data first, model next)
4820  if (referenceIsFirst) {
4821  std::swap(dataAndModel[0], dataAndModel[1]);
4822  }
4823 
4824  // check that the scalar fields (weights) exist
4825  if (dataSFAsWeights != -1) {
4826  ccPointCloud* cloud =
4827  ccHObjectCaster::ToPointCloud(dataAndModel[0]->getEntity());
4828  if (!cloud || cloud->getNumberOfScalarFields() == 0 ||
4829  dataSFAsWeights >=
4830  static_cast<int>(cloud->getNumberOfScalarFields())) {
4831  return cmd.error(
4832  QObject::tr("Invalid SF index for data entity! (%1)")
4833  .arg(dataSFAsWeights));
4834  } else {
4835  if (dataSFAsWeights < 0) // last SF
4836  {
4837  dataSFAsWeights =
4838  static_cast<int>(cloud->getNumberOfScalarFields()) - 1;
4839  }
4840  cmd.print(QObject::tr("[ICP] SF #%1 (data entity) will be used as "
4841  "weights")
4842  .arg(dataSFAsWeights));
4843  cloud->setCurrentDisplayedScalarField(dataSFAsWeights);
4844  }
4845  }
4846 
4847  if (modelSFAsWeights != -1) {
4848  ccPointCloud* cloud =
4849  ccHObjectCaster::ToPointCloud(dataAndModel[1]->getEntity());
4850  if (!cloud || cloud->getNumberOfScalarFields() == 0 ||
4851  modelSFAsWeights >=
4852  static_cast<int>(cloud->getNumberOfScalarFields())) {
4853  return cmd.error(
4854  QObject::tr("Invalid SF index for model entity! (%1)")
4855  .arg(modelSFAsWeights));
4856  } else {
4857  if (modelSFAsWeights < 0) // last SF
4858  {
4859  modelSFAsWeights =
4860  static_cast<int>(cloud->getNumberOfScalarFields()) - 1;
4861  }
4862  cmd.print(QObject::tr("[ICP] SF #%1 (model entity) will be used as "
4863  "weights")
4864  .arg(modelSFAsWeights));
4865  cloud->setCurrentDisplayedScalarField(modelSFAsWeights);
4866  }
4867  }
4868 
4869  ccGLMatrix transMat;
4870  double finalError = 0.0;
4871  double finalScale = 1.0;
4872  unsigned finalPointCount = 0;
4873 
4875  {
4876  parameters.convType =
4877  (iterationCount != 0 ? cloudViewer::ICPRegistrationTools::
4881  parameters.minRMSDecrease = minErrorDiff;
4882  parameters.nbMaxIterations = iterationCount;
4883  parameters.adjustScale = adjustScale;
4884  parameters.filterOutFarthestPoints = enableFarthestPointRemoval;
4885  parameters.samplingLimit = randomSamplingLimit;
4886  parameters.finalOverlapRatio = overlap / 100.0;
4887  parameters.transformationFilters = transformationFilters;
4888  parameters.maxThreadCount = maxThreadCount;
4889  parameters.useC2MSignedDistances = false; // TODO
4890  parameters.normalsMatching =
4892  }
4893 
4894  if (ccRegistrationTools::ICP(dataAndModel[0]->getEntity(),
4895  dataAndModel[1]->getEntity(), transMat,
4896  finalScale, finalError, finalPointCount,
4897  parameters, dataSFAsWeights >= 0,
4898  modelSFAsWeights >= 0, cmd.widgetParent())) {
4899  ccHObject* data = dataAndModel[0]->getEntity();
4900  data->applyGLTransformation_recursive(&transMat);
4901  cmd.print(QObject::tr("Entity '%1' has been registered")
4902  .arg(data->getName()));
4903  cmd.print(QObject::tr("RMS: %1").arg(finalError));
4904  cmd.print(QObject::tr("Number of points used for final step: %1")
4905  .arg(finalPointCount));
4906 
4907  // save matrix in a separate text file
4908  {
4909  QString txtFilename = QObject::tr("%1/%2_REGISTRATION_MATRIX")
4910  .arg(dataAndModel[0]->path,
4911  dataAndModel[0]->basename);
4912  if (cmd.addTimestamp())
4913  txtFilename += QObject::tr("_%1").arg(
4914  QDateTime::currentDateTime().toString(
4915  "yyyy-MM-dd_hh'h'mm"));
4916  txtFilename += QObject::tr(".txt");
4917  QFile txtFile(txtFilename);
4918  txtFile.open(QIODevice::WriteOnly | QIODevice::Text);
4919  QTextStream txtStream(&txtFile);
4920  txtStream << transMat.toString(cmd.numericalPrecision(), ' ')
4921  << QtCompat::endl;
4922  txtFile.close();
4923  }
4924 
4925  dataAndModel[0]->basename += QObject::tr("_REGISTERED");
4926  if (cmd.autoSaveMode()) {
4927  QString errorStr = cmd.exportEntity(*dataAndModel[0]);
4928  if (!errorStr.isEmpty()) {
4929  return cmd.error(errorStr);
4930  }
4931  }
4932  } else {
4933  return false;
4934  }
4935 
4936  return true;
4937 }
4938 
4940  : ccCommandLineInterface::Command(QObject::tr("Change PLY output format"),
4942 
4944  if (cmd.arguments().empty()) {
4945  return cmd.error(QObject::tr("Missing parameter: format (ASCII, "
4946  "BINARY_LE, or BINARY_BE) after '%1'")
4948  }
4949 
4950  // if (fileFilter != PlyFilter::GetFileFilter())
4951  // cmd.warning(QObject::tr("Argument '%1' is only applicable to PLY
4952  // format!").arg(argument));
4953 
4954  QString plyFormat = cmd.arguments().takeFirst().toUpper();
4955  // printf("%s\n",qPrintable(plyFormat));
4956 
4957  if (plyFormat == "ASCII") {
4959  } else if (plyFormat == "BINARY_BE") {
4960  PlyFilter::SetDefaultOutputFormat(PLY_BIG_ENDIAN);
4961  } else if (plyFormat == "BINARY_LE") {
4962  PlyFilter::SetDefaultOutputFormat(PLY_LITTLE_ENDIAN);
4963  } else {
4964  return cmd.error(
4965  QObject::tr("Invalid PLY format! ('%1')").arg(plyFormat));
4966  }
4967 
4968  return true;
4969 }
4970 
4972  : ccCommandLineInterface::Command(
4973  QObject::tr("Compute structured cloud normals"),
4975 
4977  // simply change the default filter behavior
4979 
4980  return true;
4981 }
4982 
4983 CommandSave::CommandSave(const QString& name, const QString& keyword)
4984  : ccCommandLineInterface::Command(name, keyword) {}
4985 
4987  QStringList& fileNames) {
4988  //
4989  // File list is space separated, but can use quotes to include spaces in the
4990  // file names
4991  //
4992  auto argument = cmd.arguments().takeFirst();
4993  while (!argument.isEmpty()) {
4994  auto firstChar = argument.at(0);
4995  if (firstChar == '\'' || firstChar == '\"') {
4996  auto end = argument.indexOf(firstChar, 1);
4997  if (end == -1) {
4998  return cmd.error(QObject::tr("A file starting with %1 does not "
4999  "have a closing %1")
5000  .arg(firstChar));
5001  }
5002 
5003  fileNames.push_back(argument.mid(1, end - 1));
5004  argument.remove(0, end + 1);
5005  if (argument.startsWith(' ')) {
5006  argument.remove(0, 1);
5007  }
5008  } else {
5009  auto end = argument.indexOf(' ');
5010  if (end == -1) {
5011  end = argument.length();
5012  }
5013  fileNames.push_back(argument.left(end));
5014  argument.remove(0, end + 1);
5015  }
5016  }
5017  return true;
5018 }
5019 
5020 void CommandSave::SetFileDesc(CLEntityDesc& desc, const QString& fileName) {
5021  QFileInfo fInfo(fileName);
5022  desc.basename = fInfo.fileName();
5023  desc.path = fInfo.filePath().left(fInfo.filePath().length() -
5024  fInfo.fileName().length());
5025 }
5026 
5028  : CommandSave(QObject::tr("Save clouds"), COMMAND_SAVE_CLOUDS) {}
5029 
5031  bool allAtOnce = false;
5032  bool setFileNames = false;
5033  QStringList fileNames;
5034 
5035  // look for additional parameters
5036  while (!cmd.arguments().empty()) {
5037  QString argument = cmd.arguments().front();
5038  if (argument.toUpper() == OPTION_ALL_AT_ONCE) {
5039  // local option confirmed, we can move on
5040  cmd.arguments().pop_front();
5041  allAtOnce = true;
5042  } else if (argument.left(sizeof(OPTION_FILE_NAMES) - 1).toUpper() ==
5044  cmd.arguments().pop_front();
5045  setFileNames = true;
5046  if (!ParseFileNames(cmd, fileNames)) {
5047  return false;
5048  }
5049  } else {
5050  break; // as soon as we encounter an unrecognized argument, we
5051  // break the local loop to go back to the main one!
5052  }
5053  }
5054 
5055  if (setFileNames && allAtOnce && fileNames.size() != 1) {
5056  return cmd.error(QObject::tr("Invalid parameter: specified %1 file "
5057  "names, but ALL_AT_ONCE is on")
5058  .arg(fileNames.size()));
5059  }
5060  if (setFileNames && !allAtOnce && fileNames.size() != cmd.clouds().size()) {
5061  return cmd.error(QObject::tr("Invalid parameter: specified %1 file "
5062  "names, but there are %2 clouds")
5063  .arg(fileNames.size())
5064  .arg(cmd.clouds().size()));
5065  }
5066 
5067  QString ext = cmd.cloudExportExt();
5068  bool timestamp = cmd.addTimestamp();
5069  if (setFileNames) {
5070  cmd.toggleAddTimestamp(false);
5071  cmd.setCloudExportFormat(cmd.cloudExportFormat(), QString());
5072 
5073  if (!allAtOnce) {
5074  for (int i = 0; i < fileNames.size(); ++i) {
5075  SetFileDesc(cmd.clouds()[i], fileNames[i]);
5076  }
5077  }
5078  }
5079 
5080  auto res = cmd.saveClouds(QString(), allAtOnce,
5081  setFileNames ? &fileNames[0] : nullptr);
5082 
5083  if (setFileNames) {
5084  cmd.toggleAddTimestamp(timestamp);
5085  cmd.setCloudExportFormat(cmd.cloudExportFormat(), ext);
5086  }
5087 
5088  return res;
5089 }
5090 
5092  : CommandSave(QObject::tr("Save meshes"), COMMAND_SAVE_MESHES) {}
5093 
5095  bool allAtOnce = false;
5096  bool setFileNames = false;
5097  QStringList fileNames;
5098 
5099  // look for additional parameters
5100  while (!cmd.arguments().empty()) {
5101  QString argument = cmd.arguments().front();
5102  if (argument.toUpper() == OPTION_ALL_AT_ONCE) {
5103  // local option confirmed, we can move on
5104  cmd.arguments().pop_front();
5105  allAtOnce = true;
5106  } else if (argument.left(sizeof(OPTION_FILE_NAMES) - 1).toUpper() ==
5108  cmd.arguments().pop_front();
5109  setFileNames = true;
5110  if (!ParseFileNames(cmd, fileNames)) {
5111  return false;
5112  }
5113  } else {
5114  break; // as soon as we encounter an unrecognized argument, we
5115  // break the local loop to go back to the main one!
5116  }
5117  }
5118 
5119  if (setFileNames && allAtOnce && fileNames.size() != 1) {
5120  return cmd.error(QObject::tr("Invalid parameter: specified %1 file "
5121  "names, but ALL_AT_ONCE is on")
5122  .arg(fileNames.size()));
5123  }
5124  if (setFileNames && !allAtOnce && fileNames.size() != cmd.meshes().size()) {
5125  return cmd.error(QObject::tr("Invalid parameter: specified %1 file "
5126  "names, but there are %2 meshes")
5127  .arg(fileNames.size())
5128  .arg(cmd.meshes().size()));
5129  }
5130 
5131  QString ext = cmd.meshExportExt();
5132  bool timestamp = cmd.addTimestamp();
5133  if (setFileNames) {
5134  cmd.toggleAddTimestamp(false);
5135  cmd.setMeshExportFormat(cmd.meshExportFormat(), QString());
5136 
5137  if (!allAtOnce) {
5138  for (int i = 0; i < fileNames.size(); ++i) {
5139  SetFileDesc(cmd.meshes()[i], fileNames[i]);
5140  }
5141  }
5142  }
5143 
5144  auto res = cmd.saveMeshes(QString(), allAtOnce,
5145  setFileNames ? &fileNames[0] : nullptr);
5146 
5147  if (setFileNames) {
5148  cmd.toggleAddTimestamp(timestamp);
5149  cmd.setMeshExportFormat(cmd.meshExportFormat(), ext);
5150  }
5151 
5152  return res;
5153 }
5154 
5156  : ccCommandLineInterface::Command(QObject::tr("Auto save state"),
5157  COMMAND_AUTO_SAVE) {}
5158 
5160  if (cmd.arguments().empty()) {
5161  return cmd.error(
5162  QObject::tr("Missing parameter: option after '%1' (%2/%3)")
5164  }
5165 
5166  QString option = cmd.arguments().takeFirst().toUpper();
5167  if (option == OPTION_ON) {
5168  cmd.print(QObject::tr("Auto-save is enabled"));
5169  cmd.toggleAutoSaveMode(true);
5170  } else if (option == OPTION_OFF) {
5171  cmd.print(QObject::tr("Auto-save is disabled"));
5172  cmd.toggleAutoSaveMode(false);
5173  } else {
5174  return cmd.error(
5175  QObject::tr(
5176  "Unrecognized option after '%1' (%2 or %3 expected)")
5178  }
5179 
5180  return true;
5181 }
5182 
5184  : ccCommandLineInterface::Command(QObject::tr("Set log file"),
5185  COMMAND_LOG_FILE) {}
5186 
5188  if (cmd.arguments().empty()) {
5189  return cmd.error(QObject::tr("Missing parameter: filename after '%1'")
5190  .arg(COMMAND_LOG_FILE));
5191  }
5192 
5193  QString filename = cmd.arguments().takeFirst();
5194  if (!ecvConsole::TheInstance(false)) {
5195  assert(cmd.silentMode());
5196  ecvConsole::Init();
5197  }
5198 
5199  return ecvConsole::TheInstance()->setLogFile(filename);
5200 }
5201 
5203  : ccCommandLineInterface::Command(QObject::tr("Clear"), COMMAND_CLEAR) {}
5204 
5206  cmd.removeClouds(false);
5207  cmd.removeMeshes(false);
5208  return true;
5209 }
5210 
5212  : ccCommandLineInterface::Command(QObject::tr("Clear clouds"),
5214 
5216  cmd.removeClouds(false);
5217  return true;
5218 }
5219 
5221  : ccCommandLineInterface::Command(QObject::tr("Pop clouds"),
5222  COMMAND_POP_CLOUDS) {}
5223 
5225  cmd.removeClouds(true);
5226  return true;
5227 }
5228 
5230  : ccCommandLineInterface::Command(QObject::tr("Clear meshes"),
5232 
5234  cmd.removeMeshes(false);
5235  return true;
5236 }
5237 
5239  : ccCommandLineInterface::Command(QObject::tr("Pop meshes"),
5240  COMMAND_POP_MESHES) {}
5241 
5243  cmd.removeMeshes(true);
5244  return true;
5245 }
5246 
5248  : ccCommandLineInterface::Command(QObject::tr("No timestamp"),
5250 
5252  cmd.toggleAddTimestamp(false);
5253  return true;
5254 }
5255 
5257  : ccCommandLineInterface::Command(QObject::tr("1st order moment"),
5258  COMMAND_MOMENT) {}
5259 
5261  if (cmd.arguments().empty()) {
5262  return cmd.error(QObject::tr("Missing parameter: kernel size after %1")
5263  .arg(COMMAND_MOMENT));
5264  }
5265 
5266  bool paramOk = false;
5267  QString kernelStr = cmd.arguments().takeFirst();
5268  PointCoordinateType kernelSize =
5269  static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
5270  if (!paramOk) {
5271  return cmd.error(QObject::tr("Failed to read a numerical parameter: "
5272  "kernel size. Got '%1' instead.")
5273  .arg(kernelStr));
5274  }
5275  cmd.print(QObject::tr("\tKernel size: %1").arg(kernelSize));
5276 
5277  if (cmd.clouds().empty()) {
5278  return cmd.error(QObject::tr("No point cloud on which to compute first "
5279  "order moment! (be sure to open one with "
5280  "\"-%1 [cloud filename]\" before \"-%2\")")
5281  .arg(COMMAND_OPEN, COMMAND_MOMENT));
5282  }
5283 
5284  // Call MainWindow generic method
5285  ccHObject::Container entities;
5286  entities.resize(cmd.clouds().size());
5287  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
5288  entities[i] = cmd.clouds()[i].pc;
5289  }
5290 
5293  kernelSize, entities, nullptr, cmd.widgetParent())) {
5294  // save output
5295  if (cmd.autoSaveMode() &&
5296  !cmd.saveClouds(QObject::tr("MOMENT_KERNEL_%2").arg(kernelSize))) {
5297  return false;
5298  }
5299  }
5300  return true;
5301 }
5302 
5304  : ccCommandLineInterface::Command(QObject::tr("Feature"), COMMAND_FEATURE) {
5305 }
5306 
5308  cmd.print(QObject::tr("[FEATURE]"));
5309 
5310  if (cmd.arguments().empty()) {
5311  return cmd.error(
5312  QObject::tr("Missing parameter: feature type after \"-%1\"")
5313  .arg(COMMAND_FEATURE));
5314  }
5315 
5316  QString featureTypeStr = cmd.arguments().takeFirst().toUpper();
5318 
5319  if (featureTypeStr == "SUM_OF_EIGENVALUES") {
5321  } else if (featureTypeStr == "OMNIVARIANCE") {
5323  } else if (featureTypeStr == "EIGENTROPY") {
5325  } else if (featureTypeStr == "ANISOTROPY") {
5327  } else if (featureTypeStr == "PLANARITY") {
5329  } else if (featureTypeStr == "LINEARITY") {
5331  } else if (featureTypeStr == "PCA1") {
5332  featureType = cloudViewer::Neighbourhood::PCA1;
5333  } else if (featureTypeStr == "PCA2") {
5334  featureType = cloudViewer::Neighbourhood::PCA2;
5335  } else if (featureTypeStr == "SURFACE_VARIATION") {
5337  } else if (featureTypeStr == "SPHERICITY") {
5339  } else if (featureTypeStr == "VERTICALITY") {
5341  } else if (featureTypeStr == "EIGENVALUE1") {
5343  } else if (featureTypeStr == "EIGENVALUE2") {
5345  } else if (featureTypeStr == "EIGENVALUE3") {
5347  } else {
5348  return cmd.error(
5349  QObject::tr(
5350  "Invalid feature type after \"-%1\". Got '%2' instead of:\n\
5351 - SUM_OF_EIGENVALUES\n\
5352 - OMNIVARIANCE\n\
5353 - EIGENTROPY\n\
5354 - ANISOTROPY\n\
5355 - PLANARITY\n\
5356 - LINEARITY\n\
5357 - PCA1\n\
5358 - PCA2\n\
5359 - SURFACE_VARIATION\n\
5360 - SPHERICITY\n\
5361 - VERTICALITY\n\
5362 - EIGENVALUE1\n\
5363 - EIGENVALUE2\n\
5364 - EIGENVALUE3")
5365  .arg(COMMAND_FEATURE, featureTypeStr));
5366  }
5367 
5368  if (cmd.arguments().empty()) {
5369  return cmd.error(QObject::tr(
5370  "Missing parameter: kernel size after feature type"));
5371  }
5372 
5373  bool paramOk = false;
5374  QString kernelStr = cmd.arguments().takeFirst();
5375  PointCoordinateType kernelSize =
5376  static_cast<PointCoordinateType>(kernelStr.toDouble(&paramOk));
5377  if (!paramOk) {
5378  return cmd.error(QObject::tr("Failed to read a numerical parameter: "
5379  "kernel size. Got '%1' instead.")
5380  .arg(kernelStr));
5381  }
5382  cmd.print(QObject::tr("\tKernel size: %1").arg(kernelSize));
5383 
5384  if (cmd.clouds().empty()) {
5385  return cmd.error(QObject::tr("No point cloud on which to compute "
5386  "feature! (be sure to open one with \"-%1 "
5387  "[cloud filename]\" before \"-%2\")")
5388  .arg(COMMAND_OPEN, COMMAND_FEATURE));
5389  }
5390 
5391  // Call MainWindow generic method
5392  ccHObject::Container entities;
5393  entities.resize(cmd.clouds().size());
5394  for (size_t i = 0; i < cmd.clouds().size(); ++i) {
5395  entities[i] = cmd.clouds()[i].pc;
5396  }
5397 
5400  kernelSize, entities, nullptr, cmd.widgetParent())) {
5401  // save output
5402  if (cmd.autoSaveMode() &&
5403  !cmd.saveClouds(QObject::tr("%1_FEATURE_KERNEL_%2")
5404  .arg(featureTypeStr)
5405  .arg(kernelSize))) {
5406  return false;
5407  }
5408  }
5409  return true;
5410 }
constexpr PointCoordinateType PC_ONE
'1' as a PointCoordinateType value
Definition: CVConst.h:67
CV_LOCAL_MODEL_TYPES
Definition: CVConst.h:121
@ LS
Definition: CVConst.h:123
@ TRI
Definition: CVConst.h:124
@ QUADRIC
Definition: CVConst.h:125
constexpr ScalarType NAN_VALUE
NaN as a ScalarType value.
Definition: CVConst.h:76
Vector3Tpl< PointCoordinateType > CCVector3
Default 3D Vector.
Definition: CVGeom.h:798
float PointCoordinateType
Type of the coordinates of a (N-D) point.
Definition: CVTypes.h:16
std::string filename
int size
std::string name
int count
#define PLY_ASCII
Definition: Ply.h:51
cmdLineReadable * params[]
core::Tensor result
Definition: VtkUtils.cpp:76
static void SaveColumnsNamesHeader(bool state)
static void SavePointCountHeader(bool state)
static void SaveSFBeforeColor(bool state)
Sets whether color and SF should be swapped (default is color then SF)
static void SetOutputSFPrecision(int prec)
Sets the default output scalar values precision (as saving time)
static QString GetFileFilter()
Definition: AsciiFilter.h:26
static void SetOutputSeparatorIndex(int separatorIndex)
Sets the default output separator (as saving time)
static void SetOutputCoordsPrecision(int prec)
Sets the default output coords precision (as saving time)
static void SetDefaultSkippedLineCount(int count)
Sets the default number of skipped lines (at loading time)
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
static CVLog * TheInstance()
Returns the static and unique instance.
Definition: CVLog.cpp:53
static bool Error(const char *format,...)
Display an error dialog with formatted message.
Definition: CVLog.cpp:143
static const FilterContainer & GetFilters()
Returns the set of all registered filters.
std::vector< FileIOFilter::Shared > FilterContainer
Type of a I/O filters container.
Definition: FileIOFilter.h:284
static void SetDefaultOutputFormat(e_ply_storage_mode format)
Type y
Definition: CVGeom.h:137
Type u[3]
Definition: CVGeom.h:139
Type x
Definition: CVGeom.h:137
Type z
Definition: CVGeom.h:137
Bounding box structure.
Definition: ecvBBox.h:25
static Shared LoadFromXML(QString filename)
Loads a color scale from an XML file.
QSharedPointer< ccColorScale > Shared
Shared pointer type.
Definition: ecvColorScale.h:74
Command line interface.
virtual QString hierarchyExportExt() const =0
virtual QString cloudExportExt() const =0
Returns the current cloud(s) export extension (warning: can be anything)
virtual QStringList & arguments()=0
Returns the list of arguments.
virtual bool saveClouds(QString suffix=QString(), bool allAtOnce=false, const QString *allAtOnceFileName=nullptr)=0
Saves all clouds.
virtual void warning(const QString &message) const =0
virtual QString hierarchyExportFormat() const =0
Returns the current hierarchy(ies) export format.
bool nextCommandIsGlobalShift() const
Returns whether the nex command is the '-GLOBAL_SHIFT' option.
bool processGlobalShiftCommand(GlobalShiftOptions &options)
virtual void print(const QString &message) const =0
virtual QString meshExportExt() const =0
Returns the current mesh(es) export extension (warning: can be anything)
virtual bool error(const QString &message) const =0
virtual QString meshExportFormat() const =0
Returns the current mesh(es) export format.
static bool IsCommand(const QString &token, const char *command)
Test whether a command line token is a valid command keyword or not.
virtual QString cloudExportFormat() const =0
Returns the current cloud(s) export format.
virtual bool importFile(QString filename, const GlobalShiftOptions &globalShiftOptions, FileIOFilter::Shared filter=FileIOFilter::Shared(nullptr))=0
Loads a file with a specific filter.
virtual void setCloudExportFormat(QString format, QString ext)=0
Sets the current cloud(s) export format and extension.
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 std::vector< CLMeshDesc > & meshes()
Currently opened meshes and their filename.
virtual ecvProgressDialog * progressDialog()
Returns a (shared) progress dialog (if any is available)
virtual void removeClouds(bool onlyLast=false)=0
Removes all clouds (or only the last one ;)
virtual CLLoadParameters & fileLoadingParams()
File loading parameters.
virtual void removeMeshes(bool onlyLast=false)=0
Removes all meshes (or only the last one ;)
virtual bool saveMeshes(QString suffix=QString(), bool allAtOnce=false, const QString *allAtOnceFileName=nullptr)=0
Saves all meshes.
int numericalPrecision() const
Returns the numerical precision.
virtual void setHierarchyExportFormat(QString format, QString ext)=0
Sets the current hierarchy(ies) export format and extension.
virtual QDialog * widgetParent()
Returns a (widget) parent (if any is available)
virtual void setMeshExportFormat(QString format, QString ext)=0
Sets the current mesh(es) export format and extension.
virtual std::vector< CLCloudDesc > & clouds()
Currently opened point clouds and their filename.
Dialog for cloud/cloud or cloud/mesh comparison setting.
bool initDialog()
Should be called once after the dialog is created.
static ccHObject * Crop(ccHObject *entity, const ccBBox &box, bool inside=true, const ccGLMatrix *meshRotation=0)
Crops the input entity.
Definition: ecvCropTool.cpp:23
virtual void showNormals(bool state)
Sets normals visibility.
virtual void showColors(bool state)
Sets colors visibility.
virtual void showSF(bool state)
Sets active scalarfield visibility.
QString toString(int precision=12, QChar separator=' ') const
Returns matrix as a string.
void applyRotation(Vector3Tpl< float > &vec) const
Applies rotation only to a 3D vector (in place) - float version.
static ccGLMatrixTpl< float > FromToRotation(const Vector3Tpl< float > &from, const Vector3Tpl< float > &to)
Creates a transformation matrix that rotates a vector to another.
virtual bool fromAsciiFile(QString filename)
Loads matrix from an ASCII file.
void setTranslation(const Vector3Tpl< float > &Tr)
Sets translation (float version)
Float version of ccGLMatrixTpl.
Definition: ecvGLMatrix.h:19
Generic mesh interface.
void showNormals(bool state) override
Sets normals visibility.
virtual ccGenericPointCloud * getAssociatedCloud() const =0
Returns the vertices cloud.
A 3D cloud interface with associated features (color, normals, octree, etc.)
virtual ccOctree::Shared computeOctree(cloudViewer::GenericProgressCallback *progressCb=nullptr, bool autoAddChild=true)
Computes the cloud octree.
virtual ccOctree::Shared getOctree() const
Returns the associated octree (if any)
static ccMesh * ToMesh(ccHObject *obj)
Converts current object to ccMesh (if possible)
static ccPointCloud * ToPointCloud(ccHObject *obj, bool *isLockedVertices=nullptr)
Converts current object to 'equivalent' ccPointCloud.
static ccShiftedObject * ToShifted(ccHObject *obj, bool *isLockedVertices=nullptr)
Converts current object to 'equivalent' ccShiftedObject.
Hierarchical CLOUDVIEWER Object.
Definition: ecvHObject.h:25
virtual ccBBox getOwnBB(bool withGLFeatures=false)
Returns the entity's own bounding-box.
void detachChild(ccHObject *child)
Detaches a specific child.
void applyGLTransformation_recursive(const ccGLMatrix *trans=nullptr)
Applies the active OpenGL transformation to the entity (recursive)
virtual bool addChild(ccHObject *child, int dependencyFlags=DP_PARENT_OF_OTHER, int insertIndex=-1)
Adds a child.
ccHObject * getParent() const
Returns parent object.
Definition: ecvHObject.h:245
std::vector< ccHObject * > Container
Standard instances container (for children, etc.)
Definition: ecvHObject.h:337
Triangular mesh.
Definition: ecvMesh.h:35
static ccMesh * Triangulate(ccGenericPointCloud *cloud, cloudViewer::TRIANGULATION_TYPES type, bool updateNormals=false, PointCoordinateType maxEdgeLength=0, unsigned char dim=2)
Creates a Delaunay 2.5D mesh from a point cloud.
void clearTriNormals()
Removes per-triangle normals.
Definition: ecvMesh.h:353
ccGenericPointCloud * getAssociatedCloud() const override
Returns the vertices cloud.
Definition: ecvMesh.h:143
bool hasNormals() const override
Returns whether normals are enabled or not.
virtual unsigned size() const override
Returns the number of triangles.
void invertNormals()
Inverts normals (if any)
static PointCoordinateType GuessBestRadius(ccGenericPointCloud *cloud, cloudViewer::DgmOctree *cloudOctree=nullptr, cloudViewer::GenericProgressCallback *progressCb=nullptr)
static QString ConvertDipAndDipDirToString(PointCoordinateType dip_deg, PointCoordinateType dipDir_deg)
Converts geological 'dip direction & dip' parameters to a string.
static void ConvertNormalToDipAndDipDir(const CCVector3 &N, PointCoordinateType &dip_deg, PointCoordinateType &dipDir_deg)
Converts a normal vector to geological 'dip direction & dip' parameters.
Orientation
'Default' orientations
virtual QString getName() const
Returns object name.
Definition: ecvObject.h:72
bool isA(CV_CLASS_ENUM type) const
Definition: ecvObject.h:131
virtual void setName(const QString &name)
Sets object name.
Definition: ecvObject.h:75
virtual void setEnabled(bool state)
Sets the "enabled" property.
Definition: ecvObject.h:102
QSharedPointer< ccOctree > Shared
Shared pointer.
Definition: ecvOctree.h:32
Plane (primitive)
Definition: ecvPlane.h:18
static ccPlane * Fit(cloudViewer::GenericIndexedCloudPersist *cloud, double *rms=0)
Fits a plane primitive on a cloud.
CCVector3 getNormal() const override
Returns the entity normal.
Definition: ecvPlane.h:73
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
void setCurrentDisplayedScalarField(int index)
Sets the currently displayed scalar field.
bool setRGBColorByBanding(unsigned char dim, double freq)
Assigns color to points by 'banding'.
int addScalarField(const char *uniqueName) override
Creates a new scalar field and registers it.
bool hasNormals() const override
Returns whether normals are enabled or not.
bool computeNormalsWithOctree(CV_LOCAL_MODEL_TYPES model, ccNormalVectors::Orientation preferredOrientation, PointCoordinateType defaultRadius, ecvProgressDialog *pDlg=nullptr)
Compute the normals by approximating the local surface around each point.
bool reserve(unsigned numberOfPoints) override
Reserves memory for all the active features.
bool exportCoordToSF(bool exportDims[3])
Exports the specified coordinate dimension(s) to scalar field(s)
bool orientNormalsWithMST(unsigned kNN=6, ecvProgressDialog *pDlg=nullptr)
Orient the normals with a Minimum Spanning Tree.
int getCurrentDisplayedScalarFieldIndex() const
Returns the currently displayed scalar field index (or -1 if none)
void unallocateNorms()
Erases the cloud normals.
void deleteScalarField(int index) override
Deletes a specific scalar field.
bool convertCurrentScalarFieldToColors(bool mixWithExistingColor=false)
ccPointCloud * partialClone(const cloudViewer::ReferenceCloud *selection, int *warnings=nullptr, bool withChildEntities=true) const
Creates a new point cloud object from a ReferenceCloud (selection)
bool hasScalarFields() const override
Returns whether one or more scalar fields are instantiated.
void invertNormals()
Inverts normals (if any)
Colored polyline.
Definition: ecvPolyline.h:24
static bool ICP(ccHObject *data, ccHObject *model, ccGLMatrix &transMat, double &finalScale, double &finalRMS, unsigned &finalPointCount, const cloudViewer::ICPRegistrationTools::Parameters &inputParameters, bool useDataSFAsWeights=false, bool useModelSFAsWeights=false, QWidget *parent=nullptr)
Applies ICP registration on two entities.
static Operation GetOperationByName(QString name)
Returns the operation enumerator based on its name.
static bool Apply(ccPointCloud *cloud, Operation op, int sf1Idx, bool inplace, SF2 *sf2=0, QWidget *parent=0)
Applies operation on a given cloud.
A scalar field associated to display-related parameters.
void setMinDisplayed(ScalarType val)
Sets the minimum displayed value.
void setColorScale(ccColorScale::Shared scale)
Sets associated color scale.
void setSymmetricalScale(bool state)
Sets whether the color scale should be symmetrical or not.
void computeMinAndMax() override
Determines the min and max values.
void setSaturationStart(ScalarType val)
Sets the value at which to start color gradient.
Shifted entity interface.
virtual void setGlobalScale(double scale)
virtual void setGlobalShift(double x, double y, double z)
Sets shift applied to original coordinates (information storage only)
virtual const CCVector3d & getGlobalShift() const
Returns the shift applied to original coordinates.
virtual double getGlobalScale() const
Returns the scale applied to original coordinates.
static int labelConnectedComponents(GenericIndexedCloudPersist *theCloud, unsigned char level, bool sixConnexity=false, cloudViewer::GenericProgressCallback *progressCb=nullptr, cloudViewer::DgmOctree *inputOctree=nullptr)
Labels connected components from a point cloud.
static bool extractConnectedComponents(GenericIndexedCloudPersist *theCloud, ReferenceCloudContainer &ccc)
Extracts connected components from a point cloud.
Vector3Tpl< T > getCenter() const
Returns center.
Definition: BoundingBox.h:164
Several point cloud resampling algorithms (octree-based, random, etc.)
static ReferenceCloud * subsampleCloudRandomly(GenericIndexedCloudPersist *cloud, unsigned newNumberOfPoints, GenericProgressCallback *progressCb=nullptr)
Subsamples a point cloud (process based on random selections)
static ReferenceCloud * subsampleCloudWithOctreeAtLevel(GenericIndexedCloudPersist *cloud, unsigned char octreeLevel, SUBSAMPLING_CELL_METHOD subsamplingMethod, GenericProgressCallback *progressCb=nullptr, DgmOctree *inputOctree=nullptr)
Subsamples a point cloud (process based on the octree)
static ReferenceCloud * sorFilter(GenericIndexedCloudPersist *cloud, int knn=6, double nSigma=1.0, DgmOctree *octree=nullptr, GenericProgressCallback *progressCb=nullptr)
Statistical Outliers Removal (SOR) filter.
static ReferenceCloud * resampleCloudSpatially(GenericIndexedCloudPersist *cloud, PointCoordinateType minDistance, const SFModulationParams &modParams, DgmOctree *octree=nullptr, GenericProgressCallback *progressCb=nullptr)
Resamples a point cloud (process based on inter point distance)
static const int MAX_OCTREE_LEVEL
Max octree subdivision level.
Definition: DgmOctree.h:67
static int computeCloud2CloudDistances(GenericIndexedCloudPersist *comparedCloud, GenericIndexedCloudPersist *referenceCloud, Cloud2CloudDistancesComputationParams &params, GenericProgressCallback *progressCb=nullptr, DgmOctree *compOctree=nullptr, DgmOctree *refOctree=nullptr)
virtual unsigned size() const =0
Returns the number of points.
A generic class to handle a probability distribution.
virtual const char * getName() const =0
Returns distribution name.
static double computeMeshVolume(GenericMesh *mesh)
Computes the mesh volume.
const CCVector3 * getGravityCenter()
Returns gravity center.
GeomFeature
Geometric feature computed from eigen values/vectors.
CurvatureType
Curvature type.
Definition: Neighbourhood.h:42
The Normal/Gaussian statistical distribution.
bool setParameters(ScalarType _mu, ScalarType _sigma2)
Sets the distribution parameters.
ScalarField * getCurrentOutScalarField() const
Returns the scalar field currently associated to the cloud output.
int getScalarFieldIndexByName(const char *name) const
Returns the index of a scalar field represented by its name.
int getCurrentInScalarFieldIndex()
Returns current INPUT scalar field index (or -1 if none)
ScalarField * getScalarField(int index) const
Returns a pointer to a specific scalar field.
unsigned getNumberOfScalarFields() const
Returns the number of associated (and active) scalar fields.
void setCurrentScalarField(int index)
Sets both the INPUT & OUTPUT scalar field.
void addPoint(const CCVector3 &P)
Adds a 3D point to the database.
int getCurrentOutScalarFieldIndex()
Returns current OUTPUT scalar field index (or -1 if none)
unsigned size() const override
Definition: PointCloudTpl.h:38
void setCurrentInScalarField(int index)
Sets the INPUT scalar field.
ScalarField * getCurrentInScalarField() const
Returns the scalar field currently associated to the cloud input.
void setClosed(bool state)
Sets whether the polyline is closed or not.
Definition: Polyline.h:29
A very simple point cloud (no point duplication)
virtual bool addPointIndex(unsigned globalIndex)
Point global index insertion mechanism.
unsigned size() const override
Returns the number of points.
virtual void clear(bool releaseMemory=false)
Clears the cloud.
A simple scalar field (to be associated to a point cloud)
Definition: ScalarField.h:25
virtual void computeMinAndMax()
Determines the min and max values.
Definition: ScalarField.h:123
ScalarType getMin() const
Returns the minimum value.
Definition: ScalarField.h:72
std::size_t countValidValues() const
Returns the number of valid values in this scalar field.
Definition: ScalarField.cpp:29
void computeMeanAndVariance(ScalarType &mean, ScalarType *variance=nullptr) const
Definition: ScalarField.cpp:41
const char * getName() const
Returns scalar field name.
Definition: ScalarField.h:43
void setName(const char *name)
Sets scalar field name.
Definition: ScalarField.cpp:22
static bool ValidValue(ScalarType value)
Returns whether a scalar value is valid or not.
Definition: ScalarField.h:61
ScalarType getMax() const
Returns the maximum value.
Definition: ScalarField.h:74
static double testCloudWithStatisticalModel(const GenericDistribution *distrib, GenericIndexedCloudPersist *theCloud, unsigned numberOfNeighbours, double pTrust, GenericProgressCallback *progressCb=nullptr, DgmOctree *inputOctree=nullptr)
The Weibull statistical parametric distribution.
bool setParameters(ScalarType a, ScalarType b, ScalarType valueShift=0)
Sets the distribution parameters.
static void Init(QListWidget *textDisplay=nullptr, QWidget *parentWidget=nullptr, MainWindow *parentWindow=nullptr, bool redirectToStdOut=false)
Inits console (and optionaly associates it with a text output widget)
Definition: ecvConsole.cpp:178
Graphical progress indicator (thread-safe)
constexpr char COMMAND_FLIP_TRIANGLES[]
constexpr char COMMAND_CLOSEST_POINT_SET[]
constexpr char COMMAND_REMOVE_SCAN_GRIDS[]
constexpr char COMMAND_DENSITY_TYPE[]
constexpr char COMMAND_APPLY_TRANSFORMATION[]
constexpr char COMMAND_SAVE_MESHES[]
constexpr char OPTION_ALL[]
constexpr char COMMAND_REMOVE_NORMALS[]
constexpr char COMMAND_SAMPLE_MESH[]
constexpr char COMMAND_COMPUTE_GRIDDED_NORMALS[]
constexpr char OPTION_MESH[]
constexpr char COMMAND_RENAME_SF[]
constexpr char COMMAND_PLY_EXPORT_FORMAT[]
constexpr char COMMAND_ASCII_EXPORT_PRECISION[]
constexpr char OPTION_FORCE[]
constexpr char COMMAND_CLOUD_EXPORT_FORMAT[]
constexpr char OPTION_PERCENT[]
constexpr char COMMAND_MERGE_MESHES[]
constexpr char COMMAND_REMOVE_ALL_SFS[]
constexpr char COMMAND_CROP_OUTSIDE[]
constexpr char COMMAND_POP_MESHES[]
constexpr char COMMAND_DELAUNAY_MAX_EDGE_LENGTH[]
constexpr char COMMAND_CONVERT_NORMALS_TO_DIP[]
constexpr char COMMAND_VERBOSITY[]
constexpr char OPTION_FIRST[]
constexpr char COMMAND_ICP_USE_DATA_SF_AS_WEIGHT[]
constexpr char COMMAND_REMOVE_RGB[]
constexpr char OPTION_REGEX[]
constexpr char COMMAND_DELAUNAY_AA[]
constexpr char OPTION_FILE_NAMES[]
constexpr char COMMAND_EXTRACT_CC[]
spatial step / octree level)
constexpr char COMMAND_MESH_EXPORT_FORMAT[]
constexpr char OPTION_MODEL[]
constexpr char COMMAND_EXTRACT_VERTICES[]
constexpr char OPTION_ALL_AT_ONCE[]
constexpr char COMMAND_ASCII_EXPORT_ADD_PTS_COUNT[]
constexpr char COMMAND_CLEAR_MESHES[]
int GetScalarFieldIndex(ccPointCloud *cloud, int sfIndex, const QString &sfName, bool minusOneMeansCurrent=false)
constexpr char COMMAND_APPROX_DENSITY[]
constexpr char OPTION_SIGMA_SF[]
constexpr char COMMAND_ICP_MIN_ERROR_DIIF[]
constexpr char COMMAND_ICP_RANDOM_SAMPLING_LIMIT[]
constexpr char COMMAND_CROP_2D[]
constexpr char COMMAND_OPEN[]
cloudViewer::ScalarField * GetScalarField(ccPointCloud *cloud, int sfIndex, const QString &sfName, bool minusOneMeansCurrent=false)
constexpr char COMMAND_EXPORT_EXTENSION[]
constexpr char COMMAND_AUTO_SAVE[]
static bool GetSFIndexOrName(ccCommandLineInterface &cmd, int &sfIndex, QString &sfName, bool allowMinusOne=false)
constexpr char OPTION_ON[]
constexpr char COMMAND_ICP[]
constexpr char COMMAND_ICP_USE_MODEL_SF_AS_WEIGHT[]
constexpr char COMMAND_DELAUNAY_BF[]
constexpr char COMMAND_SAVE_CLOUDS[]
constexpr char COMMAND_SUBSAMPLE[]
constexpr char COMMAND_ORIENT_NORMALS[]
constexpr char COMMAND_POP_CLOUDS[]
constexpr char COMMAND_MOMENT[]
constexpr char COMMAND_MAX_THREAD_COUNT[]
constexpr char COMMAND_BEST_FIT_PLANE_KEEP_LOADED[]
constexpr char COMMAND_DEBUG[]
constexpr char COMMAND_MESH_VOLUME[]
constexpr char OPTION_BILATERAL[]
constexpr char OPTION_BLEND_GRAYSCALE[]
constexpr char COMMAND_LOG_FILE[]
constexpr char COMMAND_COMPUTE_OCTREE_NORMALS[]
constexpr char OPTION_MEAN[]
constexpr char COMMAND_CURVATURE[]
constexpr char COMMAND_C2M_DIST[]
constexpr char COMMAND_BEST_FIT_PLANE_MAKE_HORIZ[]
constexpr char OPTION_SF[]
constexpr char OPTION_BURNT_COLOR_THRESHOLD[]
constexpr char COMMAND_BEST_FIT_PLANE[]
constexpr char COMMAND_SF_ARITHMETIC[]
constexpr char COMMAND_FILTER[]
constexpr char COMMAND_ICP_ROT[]
constexpr char COMMAND_FILTER_SF_BY_VALUE[]
constexpr char COMMAND_ICP_ENABLE_FARTHEST_REMOVAL[]
constexpr char COMMAND_SOR_FILTER[]
constexpr char COMMAND_INVERT_NORMALS[]
constexpr char COMMAND_ICP_REFERENCE_IS_FIRST[]
constexpr char COMMAND_FEATURE[]
constexpr char COMMAND_REMOVE_SF[]
constexpr char OPTION_NOT[]
constexpr char COMMAND_ROUGHNESS_UP_DIR[]
constexpr char COMMAND_SF_GRADIENT[]
constexpr char COMMAND_DELAUNAY[]
constexpr char COMMAND_HIERARCHY_EXPORT_FORMAT[]
constexpr char COMMAND_ICP_ITERATION_COUNT[]
constexpr char COMMAND_CONVERT_NORMALS_TO_SFS[]
constexpr char COMMAND_RGB_CONVERT_TO_SF[]
constexpr char COMMAND_SF_COLOR_SCALE[]
constexpr char COMMAND_ASCII_EXPORT_ADD_COL_HEADER[]
constexpr char COMMAND_C2X_MAX_DISTANCE[]
constexpr char COMMAND_STAT_TEST[]
constexpr char COMMAND_ASCII_EXPORT_SEPARATOR[]
constexpr char OPTION_ORIENT[]
constexpr char COMMAND_ROUGHNESS[]
constexpr char COMMAND_C2C_DIST[]
constexpr char OPTION_SIGMA[]
constexpr char COMMAND_C2C_LOCAL_MODEL[]
constexpr char COMMAND_NO_TIMESTAMP[]
constexpr char COMMAND_CLEAR[]
constexpr char COMMAND_CLEAR_CLOUDS[]
constexpr char COMMAND_SET_ACTIVE_SF[]
constexpr char COMMAND_C2M_DIST_FLIP_NORMALS[]
constexpr char OPTION_LAST[]
constexpr char COMMAND_COORD_TO_SF[]
constexpr char COMMAND_SF_CONVERT_TO_RGB[]
constexpr char COMMAND_ICP_ADJUST_SCALE[]
constexpr char COMMAND_MATCH_BB_CENTERS[]
constexpr char OPTION_GAUSSIAN[]
constexpr char OPTION_OFF[]
constexpr char COMMAND_OPEN_SKIP_LINES[]
static bool ReadDensityType(ccCommandLineInterface &cmd, cloudViewer::GeometricalAnalysisTools::Density &density)
constexpr char COMMAND_CLEAR_NORMALS[]
constexpr char COMMAND_C2X_OCTREE_LEVEL[]
constexpr char COMMAND_SF_OP[]
constexpr char OPTION_NUMBER_OF_POINTS[]
constexpr char OPTION_RGB[]
constexpr char COMMAND_CROP[]
constexpr char COMMAND_COLOR_BANDING[]
constexpr char COMMAND_DROP_GLOBAL_SHIFT[]
constexpr char COMMAND_ICP_OVERLAP[]
constexpr char OPTION_MEDIAN[]
constexpr char OPTION_CLOUD[]
constexpr char COMMAND_MERGE_CLOUDS[]
constexpr char COMMAND_VOLUME_TO_FILE[]
constexpr char COMMAND_DENSITY[]
constexpr char COMMAND_C2C_SPLIT_XYZ[]
constexpr char OPTION_USE_ACTIVE_SF[]
#define CC_CHI2_DISTANCES_DEFAULT_SF_NAME
Definition: ecvCommon.h:22
#define CC_CONNECTED_COMPONENTS_DEFAULT_LABEL_NAME
Definition: ecvCommon.h:23
@ MESH
Definition: CVTypes.h:105
@ POINT_CLOUD
Definition: CVTypes.h:104
QTextStream & endl(QTextStream &stream)
Definition: QtCompat.h:718
bool exportNormalToSF(const ccHObject::Container &selectedEntities, QWidget *parent, bool *exportDimensions)
bool convertNormalsTo(const ccHObject::Container &selectedEntities, NORMAL_CONVERSION_DEST dest)
Converts a cloud's normals.
bool ApplyCCLibAlgorithm(CC_LIB_ALGORITHM algo, ccHObject::Container &entities, QWidget *parent, void **additionalParameters)
bool ComputeGeomCharacteristic(cloudViewer::GeometricalAnalysisTools::GeomCharacteristic c, int subOption, PointCoordinateType radius, ccHObject::Container &entities, const CCVector3 *roughnessUpDir, QWidget *parent, ecvProgressDialog *progressDialog)
static const std::string path
Definition: PointCloud.cpp:59
bool GreaterThanEpsilon(float x)
Test a floating point number against our epsilon (a very small number).
Definition: CVMath.h:37
std::vector< ReferenceCloud * > ReferenceCloudContainer
A standard container to store several subsets of points.
std::string toString(T x)
Definition: Common.h:80
void swap(cloudViewer::core::SmallVectorImpl< T > &LHS, cloudViewer::core::SmallVectorImpl< T > &RHS)
Implement std::swap in terms of SmallVector swap.
Definition: SmallVector.h:1370
unsigned char octreeLevel
Loaded cloud description.
ccPointCloud * pc
Loaded entity description.
virtual ccHObject * getEntity()=0
Loaded mesh description.
ccGenericMesh * mesh
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
QString getFileFormatFilter(ccCommandLineInterface &cmd, QString &defaultExt)
CommandChangeOutputFormat(const QString &name, const QString &keyword)
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
CommandDist(bool cloud2meshDist, const QString &name, const QString &keyword)
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool removeSF(int sfIndex, ccPointCloud &pc)
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
CommandSave(const QString &name, const QString &keyword)
static void SetFileDesc(CLEntityDesc &desc, const QString &fileName)
static bool ParseFileNames(ccCommandLineInterface &cmd, QStringList &fileNames)
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.
Parameters for the scalar-field based modulation of a parameter.
bool enabled
Whether the modulation is enabled or not.
double b
Modulation scheme: y = a.sf + b.
double a
Modulation scheme: y = a.sf + b.
Cloud-to-cloud "Hausdorff" distance computation parameters.
CONVERGENCE_TYPE convType
Convergence type.
NORMALS_MATCHING normalsMatching
Normals matching method.
int maxThreadCount
Maximum number of threads to use (0 = max)
bool useC2MSignedDistances
Whether to compute signed C2M distances.