ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
ecvCommandCrossSection.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 
9 
10 #include <ecvHObjectCaster.h>
11 #include <ecvMesh.h>
12 
13 #include "ecvCommandLineCommands.h"
14 #include "ecvCropTool.h"
15 
16 // QT
17 #include <QDir>
18 // to read the 'Cross Section' tool XML parameters file
19 #include <QXmlStreamReader>
20 
21 constexpr char COMMAND_CROSS_SECTION[] = "CROSS_SECTION";
22 
24  : ccCommandLineInterface::Command("Cross section", COMMAND_CROSS_SECTION) {}
25 
27  cmd.print("[CROSS SECTION]");
28 
29  static QString s_xmlACloudViewer = "ACloudViewer";
30  static QString s_xmlCloudCompare = "CloudCompare";
31  static QString s_xmlBoxThickness = "BoxThickness";
32  static QString s_xmlBoxCenter = "BoxCenter";
33  static QString s_xmlRepeatDim = "RepeatDim";
34  static QString s_xmlRepeatGap = "RepeatGap";
35  static QString s_xmlFilePath = "FilePath";
36  static QString s_outputXmlFilePath = "OutputFilePath";
37 
38  // expected argument: XML file
39  if (cmd.arguments().empty())
40  return cmd.error(
41  QString("Missing parameter: XML parameters file after \"-%1\"")
42  .arg(COMMAND_CROSS_SECTION));
43  QString xmlFilename = cmd.arguments().takeFirst();
44 
45  // read the XML file
46  CCVector3 boxCenter(0, 0, 0);
47  CCVector3 boxThickness(0, 0, 0);
48  bool repeatDim[3] = {false, false, false};
49  double repeatGap = 0.0;
50  bool inside = true;
51  bool autoCenter = true;
52  QString inputFilePath;
53  QString outputFilePath;
54  {
55  QFile file(xmlFilename);
56  if (!file.open(QFile::ReadOnly | QFile::Text)) {
57  return cmd.error(
58  QString("Couldn't open XML file '%1'").arg(xmlFilename));
59  }
60 
61  // read file content
62  QXmlStreamReader stream(&file);
63 
64  // expected: ACloudViewer or CloudCompare
65  if (!stream.readNextStartElement() ||
66  (stream.name() != s_xmlACloudViewer &&
67  stream.name() != s_xmlCloudCompare)) {
68  return cmd.error(
69  QString("Invalid XML file (should start by '<%1 or %2>')")
70  .arg(s_xmlACloudViewer, s_xmlCloudCompare));
71  }
72 
73  unsigned mandatoryCount = 0;
74  while (stream.readNextStartElement()) // loop over the elements
75  {
76  if (stream.name() == s_xmlBoxThickness) {
77  QXmlStreamAttributes attributes = stream.attributes();
78  if (!readVector(attributes, boxThickness, s_xmlBoxThickness,
79  cmd))
80  return false;
81  stream.skipCurrentElement();
82  ++mandatoryCount;
83  } else if (stream.name() == s_xmlBoxCenter) {
84  QXmlStreamAttributes attributes = stream.attributes();
85  if (!readVector(attributes, boxCenter, s_xmlBoxCenter, cmd))
86  return false;
87  stream.skipCurrentElement();
88  autoCenter = false;
89  } else if (stream.name() == s_xmlRepeatDim) {
90  QString itemValue = stream.readElementText();
91  bool ok = false;
92  int dim = itemValue.toInt(&ok);
93  if (!ok || dim < 0 || dim > 2) {
94  return cmd.error(QString("Invalid XML file (invalid value "
95  "for '<%1>')")
96  .arg(s_xmlRepeatDim));
97  }
98  repeatDim[dim] = true;
99  } else if (stream.name() == s_xmlRepeatGap) {
100  QString itemValue = stream.readElementText();
101  bool ok = false;
102  repeatGap = itemValue.toDouble(&ok);
103  if (!ok) {
104  return cmd.error(QString("Invalid XML file (invalid value "
105  "for '<%1>')")
106  .arg(s_xmlRepeatGap));
107  }
108  } else if (stream.name() == s_xmlFilePath) {
109  inputFilePath = stream.readElementText();
110  if (!QDir(inputFilePath).exists()) {
111  return cmd.error(QString("Invalid file path (directory "
112  "pointed by '<%1>' doesn't exist)")
113  .arg(s_xmlFilePath));
114  }
115  //++mandatoryCount;
116  } else if (stream.name() == s_outputXmlFilePath) {
117  outputFilePath = stream.readElementText();
118  if (!QDir(outputFilePath).exists()) {
119  return cmd.error(
120  QString("Invalid output file path (directory "
121  "pointed by '<%1>' doesn't exist)")
122  .arg(s_outputXmlFilePath));
123  }
124  //++mandatoryCount;
125  } else {
126  cmd.warning(QString("Unknown element: %1")
127  .arg(stream.name().toString()));
128  stream.skipCurrentElement();
129  }
130  }
131 
132  if (mandatoryCount < 1 ||
133  (!repeatDim[0] && !repeatDim[1] && !repeatDim[2])) {
134  return cmd.error(
135  QString("Some mandatory elements are missing in the XML "
136  "file (see documentation)"));
137  }
138  }
139 
140  // safety checks
141  if (cloudViewer::LessThanEpsilon(boxThickness.x) ||
142  cloudViewer::LessThanEpsilon(boxThickness.y) ||
143  cloudViewer::LessThanEpsilon(boxThickness.z)) {
144  return cmd.error(QString("Invalid box thickness"));
145  }
146 
147  CCVector3 repeatStep =
148  boxThickness + CCVector3(repeatGap, repeatGap, repeatGap);
149  if ((repeatDim[0] && cloudViewer::LessThanEpsilon(repeatStep.x)) ||
150  (repeatDim[1] && cloudViewer::LessThanEpsilon(repeatStep.y)) ||
151  (repeatDim[2] && cloudViewer::LessThanEpsilon(repeatStep.z))) {
152  return cmd.error(QString(
153  "Repeat gap can't be equal or smaller than 'minus' box width"));
154  }
155 
156  if (outputFilePath.isEmpty()) {
157  outputFilePath = inputFilePath;
158  }
159 
160  int iterationCount = 1;
161 
162  // shall we load the entities?
163  QStringList files;
164  QDir dir;
165  bool fromFiles = false;
166  if (!inputFilePath.isEmpty()) {
167  // look for all files in the input directory
168  dir = QDir(inputFilePath);
169  assert(dir.exists());
170  files = dir.entryList(QDir::Files);
171  iterationCount = files.size();
172  fromFiles = true;
173 
174  // remove any cloud or mesh in memory!
175  cmd.removeClouds();
176  cmd.removeMeshes();
177  }
178 
179  for (int f = 0; f < iterationCount; ++f) {
180  // shall we load files?
181  QString filename;
182  if (fromFiles) {
183  assert(f < files.size());
184  filename = dir.absoluteFilePath(files[f]);
185  QFileInfo fileinfo(filename);
186  if (!fileinfo.isFile() || fileinfo.suffix().toUpper() == "XML") {
187  continue;
188  }
189 
190  // let's try to load the file
191  cmd.print(QString("Processing file: '%1'").arg(files[f]));
192 
193  bool result = false;
194  {
195  // hack: replace the current argument list by a fake 'load file'
196  // sequence
197  QStringList realArguments = cmd.arguments();
198 
199  QStringList loadArguments;
200  loadArguments << filename;
201  cmd.arguments() = loadArguments;
202  result = CommandLoad().process(cmd);
203 
204  // end of hack: restore the current argument list
205  cmd.arguments() = realArguments;
206  }
207 
208  if (!result) {
209  cmd.warning("\tFailed to load file!");
210  continue;
211  }
212  } else {
213  assert(iterationCount == 1);
214  }
215 
216  // repeat crop process on each file (or do it only once on the currently
217  // loaded entities)
218  {
219  ccHObject::Container entities;
220  try {
221  for (size_t i = 0; i < cmd.clouds().size(); ++i)
222  entities.push_back(cmd.clouds()[i].pc);
223  for (size_t j = 0; j < cmd.meshes().size(); ++j)
224  entities.push_back(cmd.meshes()[j].mesh);
225  } catch (const std::bad_alloc &) {
226  return cmd.error("Not enough memory!");
227  }
228 
229  for (size_t i = 0; i < entities.size(); ++i) {
230  // check entity bounding-box
231  ccHObject *ent = entities[i];
232  ccBBox bbox = ent->getOwnBB();
233  if (!bbox.isValid()) {
234  cmd.warning(
235  QString("Entity '%1' has an invalid bounding-box!")
236  .arg(ent->getName()));
237  continue;
238  }
239 
240  // browse to/create a subdirectory with the (base) filename as
241  // name
242  QString basename;
243  if (fromFiles) {
244  basename = QFileInfo(filename).baseName();
245  } else {
246  basename = i < cmd.clouds().size()
247  ? cmd.clouds()[i].basename
248  : cmd.meshes()[i - cmd.clouds().size()]
249  .basename;
250  }
251 
252  if (entities.size() > 1) basename += QString("_%1").arg(i + 1);
253 
254  QDir outputDir(outputFilePath);
255  if (outputFilePath.isEmpty()) {
256  if (fromFiles) {
257  assert(false);
258  outputDir = QDir::current();
259  } else {
260  outputDir = QDir(
261  i < cmd.clouds().size()
262  ? cmd.clouds()[i].path
263  : cmd.meshes()[i - cmd.clouds().size()]
264  .path);
265  }
266  }
267 
268  assert(outputDir.exists());
269  if (outputDir.cd(basename)) {
270  // if the directory already exists...
271  cmd.warning(QString("Subdirectory '%1' already exists")
272  .arg(basename));
273  } else if (outputDir.mkdir(basename)) {
274  outputDir.cd(basename);
275  } else {
276  cmd.warning(
277  QString("Failed to create subdirectory '%1' (check "
278  "access rights and base name validity!)")
279  .arg(basename));
280  continue;
281  }
282 
283  int toto = ceil(-0.4);
284  int toto2 = ceil(-0.6);
285 
286  // place the initial box at the beginning of the entity bounding
287  // box
288  CCVector3 C0 = autoCenter ? bbox.getCenter() : boxCenter;
289  unsigned steps[3] = {1, 1, 1};
290  for (unsigned d = 0; d < 3; ++d) {
291  if (repeatDim[d]) {
292  PointCoordinateType boxHalfWidth =
293  boxThickness.u[d] / 2;
294  PointCoordinateType distToMinBorder =
295  C0.u[d] - boxHalfWidth - bbox.minCorner().u[d];
296  int stepsToMinBorder = static_cast<int>(
297  ceil(distToMinBorder / repeatStep.u[d]));
298  C0.u[d] -= stepsToMinBorder * repeatStep.u[d];
299 
300  PointCoordinateType distToMaxBorder =
301  bbox.maxCorner().u[d] - C0.u[d] - boxHalfWidth;
302  int stepsToMaxBoder = static_cast<int>(
303  ceil(distToMaxBorder / repeatStep.u[d]) + 1);
304  assert(stepsToMaxBoder >= 0);
305  steps[d] = std::max<unsigned>(stepsToMaxBoder, 1);
306  }
307  }
308 
309  cmd.print(QString("Will extract up to (%1 x %2 x %3) = %4 "
310  "sections")
311  .arg(steps[0])
312  .arg(steps[1])
313  .arg(steps[2])
314  .arg(steps[0] * steps[1] * steps[2]));
315 
316  // now extract the slices
317  for (unsigned dx = 0; dx < steps[0]; ++dx) {
318  for (unsigned dy = 0; dy < steps[1]; ++dy) {
319  for (unsigned dz = 0; dz < steps[2]; ++dz) {
320  CCVector3 C = C0 + CCVector3(dx * repeatStep.x,
321  dy * repeatStep.y,
322  dz * repeatStep.z);
323  ccBBox cropBox(C - boxThickness / 2,
324  C + boxThickness / 2);
325  cmd.print(QString("Box (%1;%2;%3) --> (%4;%5;%6)")
326  .arg(cropBox.minCorner().x)
327  .arg(cropBox.minCorner().y)
328  .arg(cropBox.minCorner().z)
329  .arg(cropBox.maxCorner().x)
330  .arg(cropBox.maxCorner().y)
331  .arg(cropBox.maxCorner().z));
332  ccHObject *croppedEnt =
333  ccCropTool::Crop(ent, cropBox, inside);
334  if (croppedEnt) {
335  QString outputBasename =
336  basename + QString("_%1_%2_%3")
337  .arg(C.x)
338  .arg(C.y)
339  .arg(C.z);
340  QString errorStr;
341  // original entity is a cloud?
342  if (i < cmd.clouds().size()) {
343  CLCloudDesc desc(
344  static_cast<ccPointCloud *>(
345  croppedEnt),
346  outputBasename,
347  outputDir.absolutePath(),
348  entities.size() > 1
349  ? static_cast<int>(i)
350  : -1);
351  errorStr = cmd.exportEntity(desc);
352  } else // otherwise it's a mesh
353  {
354  CLMeshDesc desc(
355  static_cast<ccMesh *>(croppedEnt),
356  outputBasename,
357  outputDir.absolutePath(),
358  entities.size() > 1
359  ? static_cast<int>(i)
360  : -1);
361  errorStr = cmd.exportEntity(desc);
362  }
363 
364  delete croppedEnt;
365  croppedEnt = nullptr;
366 
367  if (!errorStr.isEmpty())
368  return cmd.error(errorStr);
369  }
370  }
371  }
372  }
373  }
374 
375  if (fromFiles) {
376  // unload entities
377  cmd.removeClouds();
378  cmd.removeMeshes();
379  }
380  }
381  }
382 
383  return true;
384 }
385 
386 bool CommandCrossSection::readVector(const QXmlStreamAttributes &attributes,
387  CCVector3 &P,
388  QString element,
389  const ccCommandLineInterface &cmd) {
390  if (attributes.size() < 3) {
391  return cmd.error(QString("Invalid XML file (3 attributes expected for "
392  "element '<%1>')")
393  .arg(element));
394  }
395 
396  int count = 0;
397  for (int i = 0; i < attributes.size(); ++i) {
398  QString name = attributes[i].name().toString().toUpper();
399  QString value = attributes[i].value().toString();
400 
401  bool ok = false;
402  if (name == "X") {
403  P.x = value.toDouble(&ok);
404  ++count;
405  } else if (name == "Y") {
406  P.y = value.toDouble(&ok);
407  ++count;
408  } else if (name == "Z") {
409  P.z = value.toDouble(&ok);
410  ++count;
411  } else {
412  ok = true;
413  }
414 
415  if (!ok) {
416  return cmd.error(
417  QString("Invalid XML file (numerical attribute expected "
418  "for attribute '%1' of element '<%2>')")
419  .arg(name, element));
420  }
421  }
422 
423  if (count < 3) {
424  return cmd.error(QString("Invalid XML file (attributes 'X','Y' and 'Z' "
425  "are mandatory for element '<%1>')")
426  .arg(element));
427  }
428 
429  return true;
430 }
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
core::Tensor result
Definition: VtkUtils.cpp:76
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
Command line interface.
virtual QStringList & arguments()=0
Returns the list of arguments.
virtual void warning(const QString &message) const =0
virtual void print(const QString &message) const =0
virtual bool error(const QString &message) const =0
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 void removeClouds(bool onlyLast=false)=0
Removes all clouds (or only the last one ;)
virtual void removeMeshes(bool onlyLast=false)=0
Removes all meshes (or only the last one ;)
virtual std::vector< CLCloudDesc > & clouds()
Currently opened point clouds and their filename.
static ccHObject * Crop(ccHObject *entity, const ccBBox &box, bool inside=true, const ccGLMatrix *meshRotation=0)
Crops the input entity.
Definition: ecvCropTool.cpp:23
Hierarchical CLOUDVIEWER Object.
Definition: ecvHObject.h:25
virtual ccBBox getOwnBB(bool withGLFeatures=false)
Returns the entity's own bounding-box.
std::vector< ccHObject * > Container
Standard instances container (for children, etc.)
Definition: ecvHObject.h:337
Triangular mesh.
Definition: ecvMesh.h:35
virtual unsigned size() const override
Returns the number of triangles.
virtual QString getName() const
Returns object name.
Definition: ecvObject.h:72
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
Vector3Tpl< T > getCenter() const
Returns center.
Definition: BoundingBox.h:164
const Vector3Tpl< T > & maxCorner() const
Returns max corner (const)
Definition: BoundingBox.h:156
const Vector3Tpl< T > & minCorner() const
Returns min corner (const)
Definition: BoundingBox.h:154
bool isValid() const
Returns whether bounding box is valid or not.
Definition: BoundingBox.h:203
unsigned size() const override
Definition: PointCloudTpl.h:38
constexpr char COMMAND_CROSS_SECTION[]
MiniVec< float, N > ceil(const MiniVec< float, N > &a)
Definition: MiniVec.h:89
bool LessThanEpsilon(float x)
Test a floating point number against our epsilon (a very small number).
Definition: CVMath.h:23
Loaded cloud description.
Loaded mesh description.
bool process(ccCommandLineInterface &cmd) override
Main process.
bool process(ccCommandLineInterface &cmd) override
Main process.