ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
DenseReconstructionWidget.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 "ReconstructionWidget.h"
11 #include "base/undistortion.h"
12 #include "controllers/texturing_controller.h"
13 #include "mvs/fusion.h"
14 #include "mvs/meshing.h"
15 #include "mvs/patch_match.h"
16 #include "ui/render_options.h"
17 #include "util/option_manager.h"
18 
19 // CV_CORE_LIB
20 #include <FileSystem.h>
21 
22 // CV_DB_LIB
23 #include <ecvDisplayTools.h>
24 #include <ecvPointCloud.h>
25 
26 // LOCAL
27 #include "MainWindow.h"
28 
29 namespace cloudViewer {
30 namespace {
31 
32 const static std::string kFusedFileName = "fused.ply";
33 const static std::string kPoissonMeshedFileName = "meshed-poisson.ply";
34 const static std::string kDelaunayMeshedFileName = "meshed-delaunay.ply";
35 const static std::string kTexturedMeshFileName = "textured-mesh.obj";
36 
37 class StereoOptionsTab : public colmap::OptionsWidget {
38 public:
39  StereoOptionsTab(QWidget* parent, OptionManager* options)
40  : OptionsWidget(parent) {
41  // Set a relatively small default image size to avoid too long
42  // computation.
43  if (options->patch_match_stereo->max_image_size == -1) {
44  options->patch_match_stereo->max_image_size = 2000;
45  }
46 
47  AddOptionInt(&options->patch_match_stereo->max_image_size,
48  "max_image_size", -1);
49  AddOptionText(&options->patch_match_stereo->gpu_index, "gpu_index");
50  AddOptionDouble(&options->patch_match_stereo->depth_min, "depth_min",
51  -1);
52  AddOptionDouble(&options->patch_match_stereo->depth_max, "depth_max",
53  -1);
54  AddOptionInt(&options->patch_match_stereo->window_radius,
55  "window_radius");
56  AddOptionInt(&options->patch_match_stereo->window_step, "window_step");
57  AddOptionDouble(&options->patch_match_stereo->sigma_spatial,
58  "sigma_spatial", -1);
59  AddOptionDouble(&options->patch_match_stereo->sigma_color,
60  "sigma_color");
61  AddOptionInt(&options->patch_match_stereo->num_samples, "num_samples");
62  AddOptionDouble(&options->patch_match_stereo->ncc_sigma, "ncc_sigma");
63  AddOptionDouble(&options->patch_match_stereo->min_triangulation_angle,
64  "min_triangulation_angle");
65  AddOptionDouble(&options->patch_match_stereo->incident_angle_sigma,
66  "incident_angle_sigma");
67  AddOptionInt(&options->patch_match_stereo->num_iterations,
68  "num_iterations");
69  AddOptionBool(&options->patch_match_stereo->geom_consistency,
70  "geom_consistency");
71  AddOptionDouble(
72  &options->patch_match_stereo->geom_consistency_regularizer,
73  "geom_consistency_regularizer");
74  AddOptionDouble(&options->patch_match_stereo->geom_consistency_max_cost,
75  "geom_consistency_max_cost");
76  AddOptionBool(&options->patch_match_stereo->filter, "filter");
77  AddOptionDouble(&options->patch_match_stereo->filter_min_ncc,
78  "filter_min_ncc");
79  AddOptionDouble(
80  &options->patch_match_stereo->filter_min_triangulation_angle,
81  "filter_min_triangulation_angle");
82  AddOptionInt(&options->patch_match_stereo->filter_min_num_consistent,
83  "filter_min_num_consistent");
84  AddOptionDouble(
85  &options->patch_match_stereo->filter_geom_consistency_max_cost,
86  "filter_geom_consistency_max_cost");
87  AddOptionDouble(&options->patch_match_stereo->cache_size,
88  "cache_size [gigabytes]", 0,
90  AddOptionBool(&options->patch_match_stereo->write_consistency_graph,
91  "write_consistency_graph");
92  }
93 };
94 
95 class FusionOptionsTab : public colmap::OptionsWidget {
96 public:
97  FusionOptionsTab(QWidget* parent, OptionManager* options)
98  : OptionsWidget(parent) {
99  AddOptionInt(&options->stereo_fusion->max_image_size, "max_image_size",
100  -1);
101  AddOptionInt(&options->stereo_fusion->min_num_pixels, "min_num_pixels",
102  0);
103  AddOptionInt(&options->stereo_fusion->max_num_pixels, "max_num_pixels",
104  0);
105  AddOptionInt(&options->stereo_fusion->max_traversal_depth,
106  "max_traversal_depth", 1);
107  AddOptionDouble(&options->stereo_fusion->max_reproj_error,
108  "max_reproj_error", 0);
109  AddOptionDouble(&options->stereo_fusion->max_depth_error,
110  "max_depth_error", 0, 1, 0.0001, 4);
111  AddOptionDouble(&options->stereo_fusion->max_normal_error,
112  "max_normal_error", 0, 180);
113  AddOptionInt(&options->stereo_fusion->check_num_images,
114  "check_num_images", 1);
115  AddOptionDouble(&options->stereo_fusion->cache_size,
116  "cache_size [gigabytes]", 0,
118  }
119 };
120 
121 class MeshingOptionsTab : public colmap::OptionsWidget {
122 public:
123  MeshingOptionsTab(QWidget* parent, OptionManager* options)
124  : OptionsWidget(parent) {
125  AddSection("Poisson Meshing");
126  AddOptionDouble(&options->poisson_meshing->point_weight, "point_weight",
127  0);
128  AddOptionInt(&options->poisson_meshing->depth, "depth", 1);
129  AddOptionDouble(&options->poisson_meshing->color, "color", 0);
130  AddOptionDouble(&options->poisson_meshing->trim, "trim", 0);
131  AddOptionInt(&options->poisson_meshing->num_threads, "num_threads", -1);
132 
133  AddSection("Delaunay Meshing");
134  AddOptionDouble(&options->delaunay_meshing->max_proj_dist,
135  "max_proj_dist", 0);
136  AddOptionDouble(&options->delaunay_meshing->max_depth_dist,
137  "max_depth_dist", 0);
138  AddOptionDouble(&options->delaunay_meshing->distance_sigma_factor,
139  "distance_sigma_factor", 0);
140  AddOptionDouble(&options->delaunay_meshing->quality_regularization,
141  "quality_regularization", 0);
142  AddOptionDouble(&options->delaunay_meshing->max_side_length_factor,
143  "max_side_length_factor", 0);
144  AddOptionDouble(&options->delaunay_meshing->max_side_length_percentile,
145  "max_side_length_percentile", 0);
146  AddOptionInt(&options->delaunay_meshing->num_threads, "num_threads",
147  -1);
148  }
149 };
150 
151 class TexturingOptionsTab : public colmap::OptionsWidget {
152 public:
153  TexturingOptionsTab(QWidget* parent, OptionManager* options)
154  : OptionsWidget(parent),
155  options_(options),
156  mesh_source_combo_(nullptr) {
157  AddOptionBool(&options->texturing->verbose, "verbose");
158 
159  // Add mesh source selection combobox
160  mesh_source_combo_ = new QComboBox(this);
161  mesh_source_combo_->addItem("Auto (Delaunay preferred)", "auto");
162  mesh_source_combo_->addItem("Delaunay", "delaunay");
163  mesh_source_combo_->addItem("Poisson", "poisson");
164 
165  // Connect signal to update option value
166  connect(mesh_source_combo_,
167  QOverload<int>::of(&QComboBox::currentIndexChanged),
168  [this](int index) {
169  QString data =
170  mesh_source_combo_->itemData(index).toString();
171  options_->texturing->mesh_source = data.toStdString();
172  });
173 
174  AddWidgetRow("mesh_source", mesh_source_combo_);
175 
176  AddOptionFilePath(&options->texturing->meshed_file_path,
177  "meshed_file_path");
178  AddOptionDirPath(&options->texturing->textured_file_path,
179  "textured_file_path");
180 
181  AddSection("Advanced Options");
182  AddOptionBool(&options->texturing->use_depth_normal_maps,
183  "use_depth_normal_maps");
184 
185  // Add depth_map_type selection
186  QComboBox* depth_type_combo = new QComboBox(this);
187  depth_type_combo->addItem("Geometric", "geometric");
188  depth_type_combo->addItem("Photometric", "photometric");
189  connect(depth_type_combo,
190  QOverload<int>::of(&QComboBox::currentIndexChanged),
191  [this, depth_type_combo, options](int index) {
192  QString data = depth_type_combo->itemData(index).toString();
193  options->texturing->depth_map_type = data.toStdString();
194  });
195  // Set initial value
196  if (options->texturing->depth_map_type == "geometric") {
197  depth_type_combo->setCurrentIndex(0);
198  } else {
199  depth_type_combo->setCurrentIndex(1);
200  }
201  AddWidgetRow("depth_map_type", depth_type_combo);
202 
203  AddOptionDouble(&options->texturing->max_depth_error, "max_depth_error",
204  0, 1, 0.001, 3);
205  AddOptionDouble(&options->texturing->min_normal_consistency,
206  "min_normal_consistency", -1, 1, 0.01, 2);
207  AddOptionDouble(&options->texturing->max_viewing_angle_deg,
208  "max_viewing_angle_deg", 0, 180, 1, 1);
209  AddOptionBool(&options->texturing->use_gradient_magnitude,
210  "use_gradient_magnitude");
211  }
212 
213 protected:
214  void showEvent(QShowEvent* event) override {
215  OptionsWidget::showEvent(event);
216  // Sync combobox with current option value
217  if (mesh_source_combo_) {
218  std::string current_source = options_->texturing->mesh_source;
219  if (current_source == "poisson") {
220  mesh_source_combo_->setCurrentIndex(2);
221  } else if (current_source == "delaunay") {
222  mesh_source_combo_->setCurrentIndex(1);
223  } else {
224  mesh_source_combo_->setCurrentIndex(0);
225  }
226  }
227  }
228 
229 private:
230  OptionManager* options_;
231  QComboBox* mesh_source_combo_;
232 };
233 
234 // Read the specified reference image names from a patch match configuration.
235 std::vector<std::pair<std::string, std::string>> ReadPatchMatchConfig(
236  const std::string& config_path) {
237  std::ifstream file(config_path);
238  CHECK(file.is_open()) << config_path;
239 
240  std::string line;
241  std::string ref_image_name;
242  std::vector<std::pair<std::string, std::string>> images;
243  while (std::getline(file, line)) {
244  colmap::StringTrim(&line);
245 
246  if (line.empty() || line[0] == '#') {
247  continue;
248  }
249 
250  if (ref_image_name.empty()) {
251  ref_image_name = line;
252  } else {
253  images.emplace_back(ref_image_name, line);
254  ref_image_name.clear();
255  }
256  }
257 
258  return images;
259 }
260 
261 } // namespace
262 
263 using namespace colmap;
265  QWidget* parent, OptionManager* options)
266  : QWidget(parent) {
267  setWindowFlags(Qt::Dialog);
268  setWindowModality(Qt::ApplicationModal);
269  setWindowTitle("Dense reconstruction options");
270 
271  QGridLayout* grid = new QGridLayout(this);
272 
273  QTabWidget* tab_widget = new QTabWidget(this);
274  tab_widget->setElideMode(Qt::TextElideMode::ElideRight);
275  tab_widget->addTab(new StereoOptionsTab(this, options), "Stereo");
276  tab_widget->addTab(new FusionOptionsTab(this, options), "Fusion");
277  tab_widget->addTab(new MeshingOptionsTab(this, options), "Meshing");
278  tab_widget->addTab(new TexturingOptionsTab(this, options), "Texturing");
279 
280  grid->addWidget(tab_widget, 0, 0);
281 }
282 
284  ReconstructionWidget* main_window, OptionManager* options)
285  : QWidget(main_window),
286  main_window_(main_window),
287  options_(options),
288  reconstruction_(nullptr),
289  thread_control_widget_(new ThreadControlWidget(this)),
290  options_widget_(new DenseReconstructionOptionsWidget(this, options)),
291  photometric_done_(false),
292  geometric_done_(false) {
293  setWindowFlags(Qt::Dialog);
294  setWindowModality(Qt::ApplicationModal);
295  setWindowTitle("Dense reconstruction");
296  resize(main_window_->size().width() - 20,
297  main_window_->size().height() - 20);
298 
299  QGridLayout* grid = new QGridLayout(this);
300 
301  undistortion_button_ = new QPushButton(tr("Undistortion"), this);
302  connect(undistortion_button_, &QPushButton::released, this,
303  &DenseReconstructionWidget::Undistort);
304  grid->addWidget(undistortion_button_, 0, 0, Qt::AlignLeft);
305 
306  stereo_button_ = new QPushButton(tr("Stereo"), this);
307  connect(stereo_button_, &QPushButton::released, this,
308  &DenseReconstructionWidget::Stereo);
309  grid->addWidget(stereo_button_, 0, 1, Qt::AlignLeft);
310 
311  fusion_button_ = new QPushButton(tr("Fusion"), this);
312  connect(fusion_button_, &QPushButton::released, this,
313  &DenseReconstructionWidget::Fusion);
314  grid->addWidget(fusion_button_, 0, 2, Qt::AlignLeft);
315 
316  poisson_meshing_button_ = new QPushButton(tr("Poisson"), this);
317  connect(poisson_meshing_button_, &QPushButton::released, this,
318  &DenseReconstructionWidget::PoissonMeshing);
319  grid->addWidget(poisson_meshing_button_, 0, 3, Qt::AlignLeft);
320 
321  delaunay_meshing_button_ = new QPushButton(tr("Delaunay"), this);
322  connect(delaunay_meshing_button_, &QPushButton::released, this,
323  &DenseReconstructionWidget::DelaunayMeshing);
324  grid->addWidget(delaunay_meshing_button_, 0, 4, Qt::AlignLeft);
325 
326  texturing_button_ = new QPushButton(tr("Texturing"), this);
327  connect(texturing_button_, &QPushButton::released, this,
328  &DenseReconstructionWidget::Texturing);
329  grid->addWidget(texturing_button_, 0, 5, Qt::AlignLeft);
330 
331  QPushButton* options_button = new QPushButton(tr("Options"), this);
332  connect(options_button, &QPushButton::released, options_widget_,
333  &OptionsWidget::show);
334  grid->addWidget(options_button, 0, 6, Qt::AlignLeft);
335 
336  QLabel* workspace_path_label = new QLabel("Workspace", this);
337  grid->addWidget(workspace_path_label, 0, 7, Qt::AlignRight);
338 
339  workspace_path_text_ = new QLineEdit(this);
340  grid->addWidget(workspace_path_text_, 0, 8, Qt::AlignRight);
341  connect(workspace_path_text_, &QLineEdit::textChanged, this,
342  &DenseReconstructionWidget::RefreshWorkspace, Qt::QueuedConnection);
343 
344  QPushButton* refresh_path_button = new QPushButton(tr("Refresh"), this);
345  connect(refresh_path_button, &QPushButton::released, this,
346  &DenseReconstructionWidget::RefreshWorkspace, Qt::QueuedConnection);
347  grid->addWidget(refresh_path_button, 0, 9, Qt::AlignRight);
348 
349  QPushButton* workspace_path_button = new QPushButton(tr("Select"), this);
350  connect(workspace_path_button, &QPushButton::released, this,
351  &DenseReconstructionWidget::SelectWorkspacePath,
352  Qt::QueuedConnection);
353  grid->addWidget(workspace_path_button, 0, 10, Qt::AlignRight);
354 
355  QStringList table_header;
356  table_header << "image_name"
357  << ""
358  << "photometric"
359  << "geometric"
360  << "src_images";
361 
362  table_widget_ = new QTableWidget(this);
363  table_widget_->setColumnCount(table_header.size());
364  table_widget_->setHorizontalHeaderLabels(table_header);
365 
366  table_widget_->setShowGrid(true);
367  table_widget_->setSelectionBehavior(QAbstractItemView::SelectRows);
368  table_widget_->setSelectionMode(QAbstractItemView::SingleSelection);
369  table_widget_->setEditTriggers(QAbstractItemView::NoEditTriggers);
370  table_widget_->verticalHeader()->setDefaultSectionSize(25);
371 
372  grid->addWidget(table_widget_, 1, 0, 1, 11);
373 
374  grid->setColumnStretch(4, 1);
375 
376  image_viewer_widget_ = new ImageViewerWidget(this);
377  image_viewer_widget_->setWindowModality(Qt::ApplicationModal);
378 
379  refresh_workspace_action_ = new QAction(this);
380  connect(refresh_workspace_action_, &QAction::triggered, this,
381  &DenseReconstructionWidget::RefreshWorkspace);
382 
383  write_fused_points_action_ = new QAction(this);
384  connect(write_fused_points_action_, &QAction::triggered, this,
385  &DenseReconstructionWidget::WriteFusedPoints);
386 
387  show_meshing_info_action_ = new QAction(this);
388  connect(show_meshing_info_action_, &QAction::triggered, this,
389  &DenseReconstructionWidget::ShowMeshingInfo);
390 
391  RefreshWorkspace();
392 }
393 
394 void DenseReconstructionWidget::showEvent(QShowEvent* event) {
395  Q_UNUSED(event);
396 
397  // Auto-load workspace path from configuration if not already set
398  if (workspace_path_text_->text().isEmpty()) {
399  std::string default_workspace;
400 
401  // Try multiple sources for workspace path (in order of priority)
402  // 1. Try project_path's parent directory
403  if (options_->project_path && !options_->project_path->empty()) {
404  default_workspace = GetParentDir(*options_->project_path);
405  if (ExistsDir(default_workspace)) {
406  workspace_path_text_->setText(
407  QString::fromStdString(default_workspace));
408  CVLog::Print(
409  QString("Auto-loaded workspace from project_path: %1")
410  .arg(default_workspace.c_str()));
411  RefreshWorkspace();
412  return;
413  }
414  }
415 
416  // 2. Try database_path's parent directory
417  if (options_->database_path && !options_->database_path->empty()) {
418  default_workspace = GetParentDir(*options_->database_path);
419  if (ExistsDir(default_workspace)) {
420  workspace_path_text_->setText(
421  QString::fromStdString(default_workspace));
422  CVLog::Print(
423  QString("Auto-loaded workspace from database_path: %1")
424  .arg(default_workspace.c_str()));
425  RefreshWorkspace();
426  return;
427  }
428  }
429 
430  // 3. Try image_path (it's usually already a directory)
431  if (options_->image_path && !options_->image_path->empty()) {
432  // First try image_path itself
433  default_workspace = *options_->image_path;
434  if (ExistsDir(default_workspace)) {
435  // Use parent of image_path as workspace
436  default_workspace = GetParentDir(default_workspace);
437  if (ExistsDir(default_workspace)) {
438  workspace_path_text_->setText(
439  QString::fromStdString(default_workspace));
440  CVLog::Print(
441  QString("Auto-loaded workspace from image_path: %1")
442  .arg(default_workspace.c_str()));
443  RefreshWorkspace();
444  return;
445  }
446  }
447  }
448  CVLog::Warning(QString(
449  "Could not auto-load workspace path. Please select manually."));
450  }
451 
452  RefreshWorkspace();
453 }
454 
455 void DenseReconstructionWidget::Show(Reconstruction* reconstruction) {
456  reconstruction_ = reconstruction;
457  show();
458  raise();
459 }
460 
461 void DenseReconstructionWidget::Undistort() {
462  const std::string workspace_path = GetWorkspacePath();
463  if (workspace_path.empty()) {
464  return;
465  }
466 
467  if (reconstruction_ == nullptr || reconstruction_->NumRegImages() < 2) {
468  QMessageBox::critical(this, "",
469  tr("No reconstruction selected in main window"));
470  return;
471  }
472 
473  // Pass reconstruction pointer for in-place undistortion
474  COLMAPUndistorter* undistorter =
475  new COLMAPUndistorter(UndistortCameraOptions(), reconstruction_,
476  *options_->image_path, workspace_path);
477  undistorter->AddCallback(Thread::FINISHED_CALLBACK, [this]() {
478  refresh_workspace_action_->trigger();
479  });
480  thread_control_widget_->StartThread("Undistorting...", true, undistorter);
481 }
482 
483 void DenseReconstructionWidget::Stereo() {
484  const std::string workspace_path = GetWorkspacePath();
485  if (workspace_path.empty()) {
486  return;
487  }
488 
489 #ifdef CUDA_ENABLED
490  mvs::PatchMatchController* processor = new mvs::PatchMatchController(
491  *options_->patch_match_stereo, workspace_path, "COLMAP", "");
492  processor->AddCallback(Thread::FINISHED_CALLBACK,
493  [this]() { refresh_workspace_action_->trigger(); });
494  thread_control_widget_->StartThread("Stereo...", true, processor);
495 #else
496  QMessageBox::critical(this, "",
497  tr("Dense stereo reconstruction requires CUDA, which "
498  "is not available on your system."));
499 #endif
500 }
501 
502 void DenseReconstructionWidget::Fusion() {
503  const std::string workspace_path = GetWorkspacePath();
504  if (workspace_path.empty()) {
505  return;
506  }
507 
508  std::string input_type;
509  if (geometric_done_) {
510  input_type = "geometric";
511  } else if (photometric_done_) {
512  input_type = "photometric";
513  } else {
514  QMessageBox::critical(
515  this, "", tr("All images must be processed prior to fusion"));
516  }
517 
518  mvs::StereoFusion* fuser = new mvs::StereoFusion(
519  *options_->stereo_fusion, workspace_path, "COLMAP", "", input_type);
520  fuser->AddCallback(Thread::FINISHED_CALLBACK, [this, fuser]() {
521  fused_points_ = fuser->GetFusedPoints();
522  fused_points_visibility_ = fuser->GetFusedPointsVisibility();
523  write_fused_points_action_->trigger();
524  });
525  thread_control_widget_->StartThread("Fusion...", true, fuser);
526 }
527 
528 void DenseReconstructionWidget::PoissonMeshing() {
529  const std::string workspace_path = GetWorkspacePath();
530  if (workspace_path.empty()) {
531  return;
532  }
533 
534  if (ExistsFile(JoinPaths(workspace_path, kFusedFileName))) {
535  thread_control_widget_->StartFunction(
536  "Poisson Meshing...", [this, workspace_path]() {
537  mvs::PoissonMeshing(
538  *options_->poisson_meshing,
539  JoinPaths(workspace_path, kFusedFileName),
540  JoinPaths(workspace_path, kPoissonMeshedFileName));
541  out_mesh_path_ =
542  JoinPaths(workspace_path, kPoissonMeshedFileName);
543  refresh_workspace_action_->trigger();
544  show_meshing_info_action_->trigger();
545  });
546  }
547 }
548 
549 void DenseReconstructionWidget::DelaunayMeshing() {
550 #ifdef CGAL_ENABLED
551  const std::string workspace_path = GetWorkspacePath();
552  if (workspace_path.empty()) {
553  return;
554  }
555 
556  if (ExistsFile(JoinPaths(workspace_path, kFusedFileName))) {
557  thread_control_widget_->StartFunction(
558  "Delaunay Meshing...", [this, workspace_path]() {
559  mvs::DenseDelaunayMeshing(
560  *options_->delaunay_meshing, workspace_path,
561  JoinPaths(workspace_path, kDelaunayMeshedFileName));
562  out_mesh_path_ =
563  JoinPaths(workspace_path, kDelaunayMeshedFileName);
564  refresh_workspace_action_->trigger();
565  show_meshing_info_action_->trigger();
566  });
567  }
568 #else
569  QMessageBox::critical(this, "",
570  tr("Delaunay meshing requires CGAL, which "
571  "is not available on your system."));
572 #endif
573 }
574 
575 void DenseReconstructionWidget::Texturing() {
576  const std::string workspace_path = GetWorkspacePath();
577  if (workspace_path.empty()) {
578  return;
579  }
580 
581  if (reconstruction_ == nullptr || reconstruction_->NumRegImages() < 2) {
582  QMessageBox::critical(this, "",
583  tr("No reconstruction selected in main window"));
584  return;
585  }
586 
587  // Determine which mesh file to use based on mesh_source option
588  if (!ExistsFile(options_->texturing->meshed_file_path)) {
589  const std::string poisson_path =
590  JoinPaths(workspace_path, kPoissonMeshedFileName);
591  const std::string delaunay_path =
592  JoinPaths(workspace_path, kDelaunayMeshedFileName);
593  const bool poisson_exists = ExistsFile(poisson_path);
594  const bool delaunay_exists = ExistsFile(delaunay_path);
595 
596  if (options_->texturing->mesh_source == "delaunay") {
597  if (delaunay_exists) {
598  options_->texturing->meshed_file_path = delaunay_path;
599  } else {
600  QMessageBox::critical(this, "",
601  tr("Delaunay mesh file not found. Please "
602  "run Delaunay meshing first."));
603  return;
604  }
605  } else if (options_->texturing->mesh_source == "poisson") {
606  if (poisson_exists) {
607  options_->texturing->meshed_file_path = poisson_path;
608  } else {
609  QMessageBox::critical(this, "",
610  tr("Poisson mesh file not found. Please "
611  "run Poisson meshing first."));
612  return;
613  }
614  } else { // "auto" - prefer Delaunay, fallback to Poisson
615  if (delaunay_exists) {
616  options_->texturing->meshed_file_path = delaunay_path;
617  } else if (poisson_exists) {
618  options_->texturing->meshed_file_path = poisson_path;
619  } else {
620  QMessageBox::critical(this, "",
621  tr("No mesh file found. Please run "
622  "Poisson or Delaunay meshing first."));
623  return;
624  }
625  }
626  }
627 
628  // use default textured file path in local worksapce path
629  if (options_->texturing->textured_file_path.empty()) {
630  options_->texturing->textured_file_path =
631  JoinPaths(workspace_path, kTexturedMeshFileName);
632  } else {
633  std::string parent_path = utility::filesystem::GetFileParentDirectory(
634  options_->texturing->textured_file_path);
635  CreateDirIfNotExists(parent_path);
636  std::string name, ext;
637  SplitFileExtension(options_->texturing->textured_file_path, &name,
638  &ext);
639  // only support obj textured mesh file extention
640  if (ext != ".obj" && ext != ".OBJ") {
641  options_->texturing->textured_file_path =
642  JoinPaths(parent_path, kTexturedMeshFileName);
643  }
644  }
645 
646  colmap::TexturingReconstruction* texturingTool =
647  new colmap::TexturingReconstruction(
648  *options_->texturing, *reconstruction_,
649  *options_->image_path, workspace_path);
650  texturingTool->AddCallback(Thread::FINISHED_CALLBACK, [this]() {
651  out_mesh_path_ = options_->texturing->textured_file_path;
652  show_meshing_info_action_->trigger();
653  });
654  thread_control_widget_->StartThread("Texturing...", true, texturingTool);
655 }
656 
657 void DenseReconstructionWidget::SelectWorkspacePath() {
658  std::string workspace_path;
659  if (workspace_path_text_->text().isEmpty()) {
660  workspace_path = GetParentDir(*options_->project_path);
661  } else {
662  workspace_path = workspace_path_text_->text().toUtf8().constData();
663  }
664 
665  workspace_path_text_->setText(QFileDialog::getExistingDirectory(
666  this, tr("Select workspace path..."),
667  QString::fromStdString(workspace_path), QFileDialog::ShowDirsOnly));
668 
669  RefreshWorkspace();
670 }
671 
672 std::string DenseReconstructionWidget::GetWorkspacePath() {
673  const std::string workspace_path =
674  workspace_path_text_->text().toUtf8().constData();
675  if (ExistsDir(workspace_path)) {
676  return workspace_path;
677  } else {
678  QMessageBox::critical(this, "", tr("Invalid workspace path"));
679  return "";
680  }
681 }
682 
683 void DenseReconstructionWidget::RefreshWorkspace() {
684  table_widget_->clearContents();
685  table_widget_->setRowCount(0);
686 
687  const std::string workspace_path =
688  workspace_path_text_->text().toUtf8().constData();
689  if (ExistsDir(workspace_path)) {
690  undistortion_button_->setEnabled(true);
691  } else {
692  undistortion_button_->setEnabled(false);
693  stereo_button_->setEnabled(false);
694  fusion_button_->setEnabled(false);
695  poisson_meshing_button_->setEnabled(false);
696  delaunay_meshing_button_->setEnabled(false);
697  texturing_button_->setEnabled(false);
698  return;
699  }
700 
701  images_path_ = JoinPaths(workspace_path, "images");
702  depth_maps_path_ = JoinPaths(workspace_path, "stereo/depth_maps");
703  normal_maps_path_ = JoinPaths(workspace_path, "stereo/normal_maps");
704  const std::string config_path =
705  JoinPaths(workspace_path, "stereo/patch-match.cfg");
706 
707  if (ExistsDir(images_path_) && ExistsDir(depth_maps_path_) &&
708  ExistsDir(normal_maps_path_) &&
709  ExistsDir(JoinPaths(workspace_path, "sparse")) &&
710  ExistsDir(JoinPaths(workspace_path, "stereo/consistency_graphs")) &&
711  ExistsFile(config_path)) {
712  stereo_button_->setEnabled(true);
713  } else {
714  stereo_button_->setEnabled(false);
715  fusion_button_->setEnabled(false);
716  poisson_meshing_button_->setEnabled(false);
717  delaunay_meshing_button_->setEnabled(false);
718  texturing_button_->setEnabled(false);
719  return;
720  }
721 
722  const auto images = ReadPatchMatchConfig(config_path);
723  table_widget_->setRowCount(images.size());
724 
725  for (size_t i = 0; i < images.size(); ++i) {
726  const std::string image_name = images[i].first;
727  const std::string src_images = images[i].second;
728  const std::string image_path = JoinPaths(images_path_, image_name);
729 
730  QTableWidgetItem* image_name_item =
731  new QTableWidgetItem(QString::fromStdString(image_name));
732  table_widget_->setItem(i, 0, image_name_item);
733 
734  QPushButton* image_button = new QPushButton("Image");
735  connect(image_button, &QPushButton::released,
736  [this, image_name, image_path]() {
737  image_viewer_widget_->setWindowTitle(
738  QString("Image for %1").arg(image_name.c_str()));
739  image_viewer_widget_->ReadAndShow(image_path);
740  });
741  table_widget_->setCellWidget(i, 1, image_button);
742 
743  table_widget_->setCellWidget(
744  i, 2, GenerateTableButtonWidget(image_name, "photometric"));
745  table_widget_->setCellWidget(
746  i, 3, GenerateTableButtonWidget(image_name, "geometric"));
747 
748  QTableWidgetItem* src_images_item =
749  new QTableWidgetItem(QString::fromStdString(src_images));
750  table_widget_->setItem(i, 4, src_images_item);
751  }
752 
753  table_widget_->resizeColumnsToContents();
754 
755  fusion_button_->setEnabled(photometric_done_ || geometric_done_);
756  poisson_meshing_button_->setEnabled(
757  ExistsFile(JoinPaths(workspace_path, kFusedFileName)));
758  delaunay_meshing_button_->setEnabled(
759  ExistsFile(JoinPaths(workspace_path, kFusedFileName)));
760 
761  texturing_button_->setEnabled(
762  ExistsFile(JoinPaths(workspace_path, kPoissonMeshedFileName)) ||
763  ExistsFile(JoinPaths(workspace_path, kDelaunayMeshedFileName)));
764 }
765 
766 void DenseReconstructionWidget::WriteFusedPoints() {
767  const int reply = QMessageBox::question(
768  this, "",
769  tr("Do you want to add the dense fused point cloud to DBRoot? "
770  "Otherwise, to visualize "
771  "the reconstructed dense point cloud later, navigate to the "
772  "<i>dense</i> sub-folder in your workspace with <i>File > "
773  "Import "
774  "model from...</i>."),
775  QMessageBox::Yes | QMessageBox::No);
776  if (reply == QMessageBox::Yes) {
777  ccPointCloud* cloud = new ccPointCloud("denseCloud");
778  unsigned nPoints = static_cast<unsigned>(fused_points_.size());
779  if (nPoints > 0 && cloud->reserveThePointsTable(nPoints)) {
780  if (cloud->reserveTheRGBTable()) {
781  cloud->showColors(true);
782  for (const auto& point : fused_points_) {
783  cloud->addPoint(CCVector3(point.x, point.y, point.z));
784  cloud->addRGBColor(
785  ecvColor::Rgb(point.r, point.g, point.b));
786  }
787  if (main_window_->app_) {
788  main_window_->app_->addToDB(cloud);
789  }
790  } else {
792  "[DenseReconstructionWidget::WriteFusedPoints] Not "
793  "enough memory!");
794  }
795  }
796  }
797 
798  const std::string workspace_path =
799  workspace_path_text_->text().toUtf8().constData();
800  if (workspace_path.empty()) {
801  fused_points_ = {};
802  fused_points_visibility_ = {};
803  return;
804  }
805 
806  thread_control_widget_->StartFunction(
807  "Exporting...", [this, workspace_path]() {
808  const std::string output_path =
809  JoinPaths(workspace_path, kFusedFileName);
810  WriteBinaryPlyPoints(output_path, fused_points_);
811  mvs::WritePointsVisibility(output_path + ".vis",
812  fused_points_visibility_);
813  fused_points_ = {};
814  fused_points_visibility_ = {};
815  poisson_meshing_button_->setEnabled(true);
816  delaunay_meshing_button_->setEnabled(true);
817  });
818 }
819 
820 void DenseReconstructionWidget::ShowMeshingInfo() {
821  if (!ExistsFile(out_mesh_path_)) {
822  return;
823  }
824 
825  const int reply = QMessageBox::question(
826  this, "",
827  tr("Do you want to add the meshed model to DBRoot, Otherwise, to "
828  "visualize "
829  "the reconstructed dense point cloud later, navigate to the "
830  "File-->Open. "
831  "The model is located in the workspace folder."),
832  QMessageBox::Yes | QMessageBox::No);
833  if (reply == QMessageBox::Yes) {
834  if (main_window_->app_) {
835  main_window_->app_->addToDBAuto(QStringList(out_mesh_path_.c_str()),
836  false);
837  }
838  }
839 }
840 
841 QWidget* DenseReconstructionWidget::GenerateTableButtonWidget(
842  const std::string& image_name, const std::string& type) {
843  CHECK(type == "photometric" || type == "geometric");
844  const bool photometric = type == "photometric";
845 
846  if (photometric) {
847  photometric_done_ = true;
848  } else {
849  geometric_done_ = true;
850  }
851 
852  const std::string depth_map_path = JoinPaths(
853  depth_maps_path_,
854  StringPrintf("%s.%s.bin", image_name.c_str(), type.c_str()));
855  const std::string normal_map_path = JoinPaths(
856  normal_maps_path_,
857  StringPrintf("%s.%s.bin", image_name.c_str(), type.c_str()));
858 
859  QWidget* button_widget = new QWidget();
860  QGridLayout* button_layout = new QGridLayout(button_widget);
861  button_layout->setContentsMargins(0, 0, 0, 0);
862 
863  QPushButton* depth_map_button = new QPushButton("Depth map", button_widget);
864  if (ExistsFile(depth_map_path)) {
865  connect(depth_map_button, &QPushButton::released,
866  [this, image_name, depth_map_path]() {
867  mvs::DepthMap depth_map;
868  depth_map.Read(depth_map_path);
869  image_viewer_widget_->setWindowTitle(
870  QString("Depth map for %1")
871  .arg(image_name.c_str()));
872  image_viewer_widget_->ShowBitmap(depth_map.ToBitmap(2, 98));
873  });
874  } else {
875  depth_map_button->setEnabled(false);
876  if (photometric) {
877  photometric_done_ = false;
878  } else {
879  geometric_done_ = false;
880  }
881  }
882  button_layout->addWidget(depth_map_button, 0, 1, Qt::AlignLeft);
883 
884  QPushButton* normal_map_button =
885  new QPushButton("Normal map", button_widget);
886  if (ExistsFile(normal_map_path)) {
887  connect(normal_map_button, &QPushButton::released,
888  [this, image_name, normal_map_path]() {
889  mvs::NormalMap normal_map;
890  normal_map.Read(normal_map_path);
891  image_viewer_widget_->setWindowTitle(
892  QString("Normal map for %1")
893  .arg(image_name.c_str()));
894  image_viewer_widget_->ShowBitmap(normal_map.ToBitmap());
895  });
896  } else {
897  normal_map_button->setEnabled(false);
898  if (photometric) {
899  photometric_done_ = false;
900  } else {
901  geometric_done_ = false;
902  }
903  }
904  button_layout->addWidget(normal_map_button, 0, 2, Qt::AlignLeft);
905 
906  return button_widget;
907 }
908 
909 } // namespace cloudViewer
MouseEvent event
Vector3Tpl< PointCoordinateType > CCVector3
Default 3D Vector.
Definition: CVGeom.h:798
std::string name
char type
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
void addToDB(const QStringList &filenames, QString fileFilter=QString(), bool displayDialog=true)
void addToDBAuto(const QStringList &filenames, bool displayDialog=true)
virtual void showColors(bool state)
Sets colors visibility.
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.
DenseReconstructionOptionsWidget(QWidget *parent, OptionManager *options)
void Show(colmap::Reconstruction *reconstruction)
DenseReconstructionWidget(ReconstructionWidget *main_window, OptionManager *options)
void ShowBitmap(const colmap::Bitmap &bitmap)
void ReadAndShow(const std::string &path)
void addPoint(const CCVector3 &P)
Adds a 3D point to the database.
void StartThread(const QString &progress_text, const bool stoppable, colmap::Thread *thread)
void StartFunction(const QString &progress_text, const std::function< void()> &func)
RGB color structure.
Definition: ecvColorTypes.h:49
int max(int a, int b)
Definition: cutil_math.h:48
std::string GetFileParentDirectory(const std::string &filename)
Definition: FileSystem.cpp:314
void SplitFileExtension(const std::string &path, std::string *root, std::string *ext)
Definition: FileSystem.cpp:137
std::string JoinPaths(T const &...paths)
Definition: FileSystem.h:23
std::string StringPrintf(const char *format,...)
Definition: Helper.cpp:110
Generic file read and write utility for python interface.
colmap::OptionManager OptionManager
std::string toString(T x)
Definition: Common.h:80
Definition: lsd.c:149
int y
Definition: lsd.c:149
int x
Definition: lsd.c:149