ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
AutomaticReconstructionWidget.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 <ecvPointCloud.h>
11 
12 #include <QApplication>
13 #include <QMessageBox>
14 #include <QProgressDialog>
15 #include <QShowEvent>
16 
17 #include "MainWindow.h"
18 #include "ReconstructionWidget.h"
19 #include "ThreadControlWidget.h"
20 #include "retrieval/resources.h"
21 #include "util/download.h"
22 
23 namespace cloudViewer {
24 
25 using namespace colmap;
27  ReconstructionWidget* main_window)
28  : OptionsWidget(main_window),
29  main_window_(main_window),
30  thread_control_widget_(new ThreadControlWidget(this)) {
31  setWindowTitle("Automatic reconstruction");
32 
33  AddOptionDirPath(&options_.workspace_path, "Workspace folder");
34  AddSpacer();
35  AddOptionDirPath(&options_.image_path, "Image folder");
36  AddSpacer();
37  AddOptionDirPath(&options_.mask_path, "Mask folder");
38  AddSpacer();
39 
40  AddOptionFilePath(&options_.vocab_tree_path,
41  "Vocabulary tree<br>(optional)");
42 
43  AddSpacer();
44 
45  QLabel* data_type_label = new QLabel(tr("Data type"), this);
46  data_type_label->setFont(font());
47  data_type_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
48  grid_layout_->addWidget(data_type_label, grid_layout_->rowCount(), 0);
49 
50  data_type_cb_ = new QComboBox(this);
51  data_type_cb_->addItem("Individual images");
52  data_type_cb_->addItem("Video frames");
53  data_type_cb_->addItem("Internet images");
54  grid_layout_->addWidget(data_type_cb_, grid_layout_->rowCount() - 1, 1);
55 
56  QLabel* quality_label = new QLabel(tr("Quality"), this);
57  quality_label->setFont(font());
58  quality_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
59  grid_layout_->addWidget(quality_label, grid_layout_->rowCount(), 0);
60 
61  quality_cb_ = new QComboBox(this);
62  quality_cb_->addItem("Low");
63  quality_cb_->addItem("Medium");
64  quality_cb_->addItem("High");
65  quality_cb_->addItem("Extreme");
66  quality_cb_->setCurrentIndex(2);
67  grid_layout_->addWidget(quality_cb_, grid_layout_->rowCount() - 1, 1);
68 
69  AddSpacer();
70 
71  AddOptionBool(&options_.single_camera, "Shared intrinsics");
72  AddOptionBool(&options_.sparse, "Sparse model");
73  AddOptionBool(&options_.dense, "Dense model");
74  AddOptionBool(&options_.texturing, "Mesh texturing");
75  AddOptionBool(&options_.autoVisualization, "Auto visualization");
76 
77  QLabel* mesher_label = new QLabel(tr("Mesher"), this);
78  mesher_label->setFont(font());
79  mesher_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
80  grid_layout_->addWidget(mesher_label, grid_layout_->rowCount(), 0);
81 
82  mesher_cb_ = new QComboBox(this);
83  mesher_cb_->addItem("Delaunay");
84  mesher_cb_->addItem("Poisson");
85  mesher_cb_->setCurrentIndex(0);
86  grid_layout_->addWidget(mesher_cb_, grid_layout_->rowCount() - 1, 1);
87 
88  AddSpacer();
89 
90  AddOptionInt(&options_.num_threads, "num_threads", -1);
91  AddOptionBool(&options_.use_gpu, "GPU");
92  AddOptionText(&options_.gpu_index, "gpu_index");
93 
94  AddSpacer();
95 
96  QPushButton* run_button = new QPushButton(tr("Run"), this);
97  grid_layout_->addWidget(run_button, grid_layout_->rowCount(), 1);
98  connect(run_button, &QPushButton::released, this,
100 
101  render_result_ = new QAction(this);
102  connect(render_result_, &QAction::triggered, this,
103  &AutomaticReconstructionWidget::RenderResult, Qt::QueuedConnection);
104 }
105 
107  WriteOptions();
108 
109  if (!ExistsDir(options_.workspace_path)) {
110  QMessageBox::critical(this, "", tr("Invalid workspace folder"));
111  return;
112  }
113 
114  if (!ExistsDir(options_.image_path)) {
115  QMessageBox::critical(this, "", tr("Invalid image folder"));
116  return;
117  }
118 
119  switch (data_type_cb_->currentIndex()) {
120  case 0:
121  options_.data_type =
122  AutomaticReconstructionController::DataType::INDIVIDUAL;
123  break;
124  case 1:
125  options_.data_type =
126  AutomaticReconstructionController::DataType::VIDEO;
127  break;
128  case 2:
129  options_.data_type =
130  AutomaticReconstructionController::DataType::INTERNET;
131  break;
132  default:
133  options_.data_type =
134  AutomaticReconstructionController::DataType::INDIVIDUAL;
135  break;
136  }
137 
138  switch (quality_cb_->currentIndex()) {
139  case 0:
140  options_.quality = AutomaticReconstructionController::Quality::LOW;
141  break;
142  case 1:
143  options_.quality =
144  AutomaticReconstructionController::Quality::MEDIUM;
145  break;
146  case 2:
147  options_.quality = AutomaticReconstructionController::Quality::HIGH;
148  break;
149  case 3:
150  options_.quality =
151  AutomaticReconstructionController::Quality::EXTREME;
152  break;
153  default:
154  options_.quality = AutomaticReconstructionController::Quality::HIGH;
155  break;
156  }
157 
158  switch (mesher_cb_->currentIndex()) {
159  case 0:
160  options_.mesher =
161  AutomaticReconstructionController::Mesher::DELAUNAY;
162  break;
163  case 1:
164  options_.mesher =
165  AutomaticReconstructionController::Mesher::POISSON;
166  break;
167  default:
168  options_.mesher =
169  AutomaticReconstructionController::Mesher::DELAUNAY;
170  break;
171  }
172 
173  // Check if vocab_tree_path is a URI and needs to be downloaded
174  std::string vocab_tree_path = options_.vocab_tree_path;
175  if (vocab_tree_path.empty()) {
176  vocab_tree_path = retrieval::kDefaultVocabTreeUri;
177  }
178 
179  // If it's a URI, check if it's already cached or needs to be downloaded
180  if (!vocab_tree_path.empty() && colmap::IsURI(vocab_tree_path)) {
181 #ifdef COLMAP_DOWNLOAD_ENABLED
182  // First, check if the file is already cached
183  std::filesystem::path cached_path =
184  colmap::GetCachedFilePath(vocab_tree_path);
185  if (!cached_path.empty() && std::filesystem::exists(cached_path)) {
186  // File already exists in cache, use it directly
187  LOG(INFO) << "Using cached vocabulary tree file: " << cached_path;
188  options_.vocab_tree_path = cached_path.string();
189  } else {
190  // File doesn't exist, show download progress dialog
191  QProgressDialog progress_dialog(
192  tr("Downloading vocabulary tree..."), tr("Cancel"), 0, 100,
193  this);
194  progress_dialog.setWindowModality(Qt::ApplicationModal);
195  progress_dialog.setWindowTitle(tr("Downloading"));
196  progress_dialog.setAutoClose(false);
197  progress_dialog.setAutoReset(false);
198  progress_dialog.setMinimumDuration(0);
199  progress_dialog.show();
200  QApplication::processEvents();
201 
202  bool download_canceled = false;
203  colmap::DownloadProgressCallback progress_callback =
204  [&progress_dialog, &download_canceled](int64_t downloaded,
205  int64_t total) {
206  QApplication::processEvents();
207  if (progress_dialog.wasCanceled()) {
208  download_canceled = true;
209  return;
210  }
211 
212  if (total > 0) {
213  int percent = static_cast<int>((downloaded * 100) /
214  total);
215  progress_dialog.setValue(percent);
216 
217  // Update label with size information
218  double downloaded_mb =
219  static_cast<double>(downloaded) /
220  (1024.0 * 1024.0);
221  double total_mb = static_cast<double>(total) /
222  (1024.0 * 1024.0);
223  progress_dialog.setLabelText(
224  tr("Downloading vocabulary tree...\n%1 MB "
225  "/ %2 MB (%3%)")
226  .arg(downloaded_mb, 0, 'f', 2)
227  .arg(total_mb, 0, 'f', 2)
228  .arg(percent));
229  } else {
230  progress_dialog.setValue(0);
231  double downloaded_mb =
232  static_cast<double>(downloaded) /
233  (1024.0 * 1024.0);
234  progress_dialog.setLabelText(
235  tr("Downloading vocabulary tree...\n%1 MB")
236  .arg(downloaded_mb, 0, 'f', 2));
237  }
238  QApplication::processEvents();
239  };
240 
241  try {
242  std::string downloaded_path = colmap::DownloadAndCacheFile(
243  vocab_tree_path, progress_callback);
244  progress_dialog.close();
245 
246  if (download_canceled || downloaded_path.empty()) {
247  QMessageBox::warning(
248  this, tr("Download Canceled"),
249  tr("Vocabulary tree download was canceled. Please "
250  "provide a local path."));
251  return;
252  }
253 
254  // Update options with the downloaded path
255  options_.vocab_tree_path = downloaded_path;
256  } catch (const std::exception& e) {
257  progress_dialog.close();
258  QMessageBox::critical(
259  this, tr("Download Failed"),
260  tr("Failed to download vocabulary tree: %1")
261  .arg(e.what()));
262  return;
263  }
264  }
265 #else
266  QMessageBox::warning(this, tr("Download Disabled"),
267  tr("Download support is disabled. Please provide "
268  "a local vocabulary tree path."));
269  return;
270 #endif
271  }
272 
273  main_window_->reconstruction_manager_.Clear();
274  main_window_->reconstruction_manager_widget_->Update();
275  main_window_->RenderClear();
276  main_window_->RenderNow();
277 
280  options_, &main_window_->reconstruction_manager_);
281 
282  controller->AddCallback(Thread::FINISHED_CALLBACK, [this, controller]() {
283  fused_points_ = controller->fused_points_;
284  meshing_paths_ = controller->meshing_paths_;
285  textured_paths_ = controller->textured_paths_;
286  texturing_success_ = controller->texturing_success_;
287  controller->fused_points_.clear();
288  controller->meshing_paths_.clear();
289  controller->textured_paths_.clear();
290  render_result_->trigger();
291  });
292 
293  thread_control_widget_->StartThread("Reconstructing...", true, controller);
294 }
295 
297  // Ensure vocab_tree_path has default value before reading options
298  // This ensures that even if WriteOptions() previously saved an empty value,
299  // we restore the default value when the window is shown
300  if (options_.vocab_tree_path.empty()) {
301  options_.vocab_tree_path = retrieval::kDefaultVocabTreeUri;
302  }
303 
304  // Call base class showEvent to read all options (including the default
305  // value)
306  OptionsWidget::showEvent(event);
307 
308  // Double-check: if UI is still empty after ReadOptions, set it explicitly
309  // This handles the case where options_.vocab_tree_path was empty before
310  for (auto& option : options_path_) {
311  if (option.second == &options_.vocab_tree_path) {
312  if (option.first->text().isEmpty() &&
313  !options_.vocab_tree_path.empty()) {
314  option.first->setText(
315  QString::fromStdString(options_.vocab_tree_path));
316  }
317  break;
318  }
319  }
320 }
321 
322 void AutomaticReconstructionWidget::RenderResult() {
323  if (main_window_->reconstruction_manager_.Size() > 0) {
324  main_window_->reconstruction_manager_widget_->Update();
325  main_window_->RenderClear();
326  main_window_->RenderNow();
327  }
328 
329  if (options_.sparse) {
330  QMessageBox::information(this, "",
331  tr("Imported the reconstructed sparse models "
332  "for visualization. The "
333  "models were also exported to the "
334  "<i>sparse</i> sub-folder in the "
335  "workspace."));
336  }
337 
338  if (options_.dense) {
339  if (options_.autoVisualization) {
340  // add dense point cloud
341  if (!fused_points_.empty()) {
342  // we create a new group to store all fused dense point cloud
343  ccHObject* fusedCloudGroup = new ccHObject("fusedCloudGroup");
344  fusedCloudGroup->setVisible(true);
345 
346  // for each cluster
347  for (std::size_t i = 0; i < fused_points_.size(); ++i) {
348  ccPointCloud* cloud =
349  new ccPointCloud(QString("%1-denseCloud").arg(i));
350  if (cloud) {
351  unsigned nPoints =
352  static_cast<unsigned>(fused_points_.size());
353  if (nPoints > 0 &&
354  cloud->reserveThePointsTable(nPoints)) {
355  if (cloud->reserveTheRGBTable()) {
356  for (const auto& point : fused_points_[i]) {
357  cloud->addPoint(CCVector3(point.x, point.y,
358  point.z));
359  cloud->addRGBColor(ecvColor::Rgb(
360  point.r, point.g, point.b));
361  }
362  cloud->showColors(true);
363  fusedCloudGroup->addChild(cloud);
364  } else {
365  CVLog::Error(
366  "[AutomaticReconstructionWidget::"
367  "RenderResult] Not enough memory!");
368  }
369  } else {
371  "[RenderResult] Ignore empty fused points "
372  "for index %i!",
373  i);
374  }
375  } else {
376  CVLog::Error(
377  "[AutomaticReconstructionWidget::RenderResult] "
378  "Not enough memory!");
379  }
380  }
381 
382  if (fusedCloudGroup->getChildrenNumber() == 0) {
383  delete fusedCloudGroup;
384  fusedCloudGroup = nullptr;
386  "[AutomaticReconstructionWidget::RenderResult] "
387  "some unknown error!");
388  } else {
389  if (main_window_->app_) {
390  main_window_->app_->addToDB(fusedCloudGroup);
391  }
392  }
393  }
394 
395  // Add meshed model or textured model
396  if (main_window_->app_) {
397  QStringList filenames;
398 
399  // If texturing was enabled and successful, add only textured
400  // meshes
401  if (options_.texturing && texturing_success_ &&
402  !textured_paths_.empty()) {
403  for (const std::string& path : textured_paths_) {
404  if (!ExistsFile(path)) {
406  "[RenderResult] Ignore invalid textured "
407  "mesh for file [%s]",
408  path.c_str());
409  continue;
410  }
411  filenames.push_back(path.c_str());
412  }
413  if (!filenames.isEmpty()) {
414  CVLog::Print("Adding %d textured mesh(es) to scene",
415  filenames.size());
416  main_window_->app_->addToDBAuto(filenames, false);
417  }
418  }
419  // Otherwise, add non-textured meshes
420  else if (!meshing_paths_.empty()) {
421  for (const std::string& path : meshing_paths_) {
422  if (!ExistsFile(path)) {
424  "[RenderResult] Ignore invalid meshed "
425  "model for file [%s]",
426  path.c_str());
427  continue;
428  }
429  filenames.push_back(path.c_str());
430  }
431  if (!filenames.isEmpty()) {
432  CVLog::Print("Adding %d mesh(es) to scene",
433  filenames.size());
434  main_window_->app_->addToDBAuto(filenames, false);
435  }
436  }
437  }
438  } else {
439  QMessageBox::information(
440  this, "",
441  tr("To visualize the reconstructed dense point cloud, "
442  "navigate to the "
443  "<i>dense</i> sub-folder in your workspace with <i>File "
444  "> Import "
445  "model from...</i>. To visualize the meshed model, "
446  "you can only drop meshed file into the main window."));
447  }
448  }
449 }
450 
451 } // namespace cloudViewer
MouseEvent event
Vector3Tpl< PointCoordinateType > CCVector3
Default 3D Vector.
Definition: CVGeom.h:798
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
static bool Print(const char *format,...)
Prints out a formatted message in console.
Definition: CVLog.cpp:113
static bool Error(const char *format,...)
Display an error dialog with formatted message.
Definition: CVLog.cpp:143
void addToDB(const QStringList &filenames, QString fileFilter=QString(), bool displayDialog=true)
void addToDBAuto(const QStringList &filenames, bool displayDialog=true)
virtual void setVisible(bool state)
Sets entity visibility.
virtual void showColors(bool state)
Sets colors visibility.
Hierarchical CLOUDVIEWER Object.
Definition: ecvHObject.h:25
unsigned getChildrenNumber() const
Returns the number of children.
Definition: ecvHObject.h:312
virtual bool addChild(ccHObject *child, int dependencyFlags=DP_PARENT_OF_OTHER, int insertIndex=-1)
Adds a child.
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
bool reserveTheRGBTable()
Reserves memory to store the RGB colors.
void addRGBColor(const ecvColor::Rgb &C)
Pushes an RGB color on stack.
bool reserveThePointsTable(unsigned _numberOfPoints)
Reserves memory to store the points coordinates.
std::vector< std::vector< colmap::PlyPoint > > fused_points_
AutomaticReconstructionWidget(ReconstructionWidget *main_window)
void addPoint(const CCVector3 &P)
Adds a 3D point to the database.
void StartThread(const QString &progress_text, const bool stoppable, colmap::Thread *thread)
RGB color structure.
Definition: ecvColorTypes.h:49
static const std::string path
Definition: PointCloud.cpp:59
Generic file read and write utility for python interface.
Definition: lsd.c:149
int y
Definition: lsd.c:149
int x
Definition: lsd.c:149