ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
dense_reconstruction_widget.cc
Go to the documentation of this file.
1 // Copyright (c) 2018, ETH Zurich and UNC Chapel Hill.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are met:
6 //
7 // * Redistributions of source code must retain the above copyright
8 // notice, this list of conditions and the following disclaimer.
9 //
10 // * Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 //
14 // * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
15 // its contributors may be used to endorse or promote products derived
16 // from this software without specific prior written permission.
17 //
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
22 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 // POSSIBILITY OF SUCH DAMAGE.
29 //
30 // Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)
31 
33 
34 #include "base/undistortion.h"
36 #include "mvs/fusion.h"
37 #include "mvs/meshing.h"
38 #include "mvs/patch_match.h"
39 #include "ui/main_window.h"
40 
41 namespace colmap {
42 namespace {
43 
44 const static std::string kFusedFileName = "fused.ply";
45 const static std::string kPoissonMeshedFileName = "meshed-poisson.ply";
46 const static std::string kDelaunayMeshedFileName = "meshed-delaunay.ply";
47 
48 class StereoOptionsTab : public OptionsWidget {
49  public:
50  StereoOptionsTab(QWidget* parent, OptionManager* options)
51  : OptionsWidget(parent) {
52  // Set a relatively small default image size to avoid too long computation.
53  if (options->patch_match_stereo->max_image_size == -1) {
54  options->patch_match_stereo->max_image_size = 2000;
55  }
56 
57  AddOptionInt(&options->patch_match_stereo->max_image_size, "max_image_size",
58  -1);
59  AddOptionText(&options->patch_match_stereo->gpu_index, "gpu_index");
60  AddOptionDouble(&options->patch_match_stereo->depth_min, "depth_min", -1);
61  AddOptionDouble(&options->patch_match_stereo->depth_max, "depth_max", -1);
62  AddOptionInt(&options->patch_match_stereo->window_radius, "window_radius");
63  AddOptionInt(&options->patch_match_stereo->window_step, "window_step");
64  AddOptionDouble(&options->patch_match_stereo->sigma_spatial,
65  "sigma_spatial", -1);
66  AddOptionDouble(&options->patch_match_stereo->sigma_color, "sigma_color");
67  AddOptionInt(&options->patch_match_stereo->num_samples, "num_samples");
68  AddOptionDouble(&options->patch_match_stereo->ncc_sigma, "ncc_sigma");
69  AddOptionDouble(&options->patch_match_stereo->min_triangulation_angle,
70  "min_triangulation_angle");
71  AddOptionDouble(&options->patch_match_stereo->incident_angle_sigma,
72  "incident_angle_sigma");
73  AddOptionInt(&options->patch_match_stereo->num_iterations,
74  "num_iterations");
75  AddOptionBool(&options->patch_match_stereo->geom_consistency,
76  "geom_consistency");
77  AddOptionDouble(&options->patch_match_stereo->geom_consistency_regularizer,
78  "geom_consistency_regularizer");
79  AddOptionDouble(&options->patch_match_stereo->geom_consistency_max_cost,
80  "geom_consistency_max_cost");
81  AddOptionBool(&options->patch_match_stereo->filter, "filter");
82  AddOptionDouble(&options->patch_match_stereo->filter_min_ncc,
83  "filter_min_ncc");
84  AddOptionDouble(
85  &options->patch_match_stereo->filter_min_triangulation_angle,
86  "filter_min_triangulation_angle");
87  AddOptionInt(&options->patch_match_stereo->filter_min_num_consistent,
88  "filter_min_num_consistent");
89  AddOptionDouble(
90  &options->patch_match_stereo->filter_geom_consistency_max_cost,
91  "filter_geom_consistency_max_cost");
92  AddOptionDouble(&options->patch_match_stereo->cache_size,
93  "cache_size [gigabytes]", 0,
94  std::numeric_limits<double>::max(), 0.1, 1);
95  AddOptionBool(&options->patch_match_stereo->write_consistency_graph,
96  "write_consistency_graph");
97  }
98 };
99 
100 class FusionOptionsTab : public OptionsWidget {
101  public:
102  FusionOptionsTab(QWidget* parent, OptionManager* options)
103  : OptionsWidget(parent) {
104  AddOptionInt(&options->stereo_fusion->max_image_size, "max_image_size", -1);
105  AddOptionInt(&options->stereo_fusion->min_num_pixels, "min_num_pixels", 0);
106  AddOptionInt(&options->stereo_fusion->max_num_pixels, "max_num_pixels", 0);
107  AddOptionInt(&options->stereo_fusion->max_traversal_depth,
108  "max_traversal_depth", 1);
109  AddOptionDouble(&options->stereo_fusion->max_reproj_error,
110  "max_reproj_error", 0);
111  AddOptionDouble(&options->stereo_fusion->max_depth_error, "max_depth_error",
112  0, 1, 0.0001, 4);
113  AddOptionDouble(&options->stereo_fusion->max_normal_error,
114  "max_normal_error", 0, 180);
115  AddOptionInt(&options->stereo_fusion->check_num_images, "check_num_images",
116  1);
117  AddOptionDouble(&options->stereo_fusion->cache_size,
118  "cache_size [gigabytes]", 0,
119  std::numeric_limits<double>::max(), 0.1, 1);
120  }
121 };
122 
123 class MeshingOptionsTab : public OptionsWidget {
124  public:
125  MeshingOptionsTab(QWidget* parent, OptionManager* options)
126  : OptionsWidget(parent) {
127  AddSection("Poisson Meshing");
128  AddOptionDouble(&options->poisson_meshing->point_weight, "point_weight", 0);
129  AddOptionInt(&options->poisson_meshing->depth, "depth", 1);
130  AddOptionDouble(&options->poisson_meshing->color, "color", 0);
131  AddOptionDouble(&options->poisson_meshing->trim, "trim", 0);
132  AddOptionInt(&options->poisson_meshing->num_threads, "num_threads", -1);
133 
134  AddSection("Delaunay Meshing");
135  AddOptionDouble(&options->delaunay_meshing->max_proj_dist, "max_proj_dist",
136  0);
137  AddOptionDouble(&options->delaunay_meshing->max_depth_dist,
138  "max_depth_dist", 0);
139  AddOptionDouble(&options->delaunay_meshing->distance_sigma_factor,
140  "distance_sigma_factor", 0);
141  AddOptionDouble(&options->delaunay_meshing->quality_regularization,
142  "quality_regularization", 0);
143  AddOptionDouble(&options->delaunay_meshing->max_side_length_factor,
144  "max_side_length_factor", 0);
145  AddOptionDouble(&options->delaunay_meshing->max_side_length_percentile,
146  "max_side_length_percentile", 0);
147  AddOptionInt(&options->delaunay_meshing->num_threads, "num_threads", -1);
148  }
149 };
150 
151 class TexturingOptionsTab : public 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), [this](int index) {
168  QString data = mesh_source_combo_->itemData(index).toString();
169  options_->texturing->mesh_source = data.toStdString();
170  });
171 
172  AddWidgetRow("mesh_source", mesh_source_combo_);
173 
174  AddOptionFilePath(&options->texturing->meshed_file_path,
175  "meshed_file_path");
176  AddOptionDirPath(&options->texturing->textured_file_path,
177  "textured_file_path");
178 
179  AddSection("Advanced Options");
180  AddOptionBool(&options->texturing->use_depth_normal_maps,
181  "use_depth_normal_maps");
182 
183  // Add depth_map_type selection
184  QComboBox* depth_type_combo = new QComboBox(this);
185  depth_type_combo->addItem("Geometric", "geometric");
186  depth_type_combo->addItem("Photometric", "photometric");
187  connect(depth_type_combo,
188  QOverload<int>::of(&QComboBox::currentIndexChanged),
189  [this, depth_type_combo, options](int index) {
190  QString data = depth_type_combo->itemData(index).toString();
191  options->texturing->depth_map_type = data.toStdString();
192  });
193  // Set initial value
194  if (options->texturing->depth_map_type == "geometric") {
195  depth_type_combo->setCurrentIndex(0);
196  } else {
197  depth_type_combo->setCurrentIndex(1);
198  }
199  AddWidgetRow("depth_map_type", depth_type_combo);
200 
201  AddOptionDouble(&options->texturing->max_depth_error, "max_depth_error",
202  0, 1, 0.001, 3);
203  AddOptionDouble(&options->texturing->min_normal_consistency,
204  "min_normal_consistency", -1, 1, 0.01, 2);
205  AddOptionDouble(&options->texturing->max_viewing_angle_deg,
206  "max_viewing_angle_deg", 0, 180, 1, 1);
207  AddOptionBool(&options->texturing->use_gradient_magnitude,
208  "use_gradient_magnitude");
209  }
210 
211  protected:
212  void showEvent(QShowEvent* event) override {
214  // Sync combobox with current option value
215  if (mesh_source_combo_) {
216  std::string current_source = options_->texturing->mesh_source;
217  if (current_source == "poisson") {
218  mesh_source_combo_->setCurrentIndex(2);
219  } else if (current_source == "delaunay") {
220  mesh_source_combo_->setCurrentIndex(1);
221  } else {
222  mesh_source_combo_->setCurrentIndex(0);
223  }
224  }
225  }
226 
227  private:
228  OptionManager* options_;
229  QComboBox* mesh_source_combo_;
230 };
231 
232 // Read the specified reference image names from a patch match configuration.
233 std::vector<std::pair<std::string, std::string>> ReadPatchMatchConfig(
234  const std::string& config_path) {
235  std::ifstream file(config_path);
236  CHECK(file.is_open()) << config_path;
237 
238  std::string line;
239  std::string ref_image_name;
240  std::vector<std::pair<std::string, std::string>> images;
241  while (std::getline(file, line)) {
242  StringTrim(&line);
243 
244  if (line.empty() || line[0] == '#') {
245  continue;
246  }
247 
248  if (ref_image_name.empty()) {
249  ref_image_name = line;
250  } else {
251  images.emplace_back(ref_image_name, line);
252  ref_image_name.clear();
253  }
254  }
255 
256  return images;
257 }
258 
259 } // namespace
260 
262  QWidget* parent, OptionManager* options)
263  : QWidget(parent) {
264  setWindowFlags(Qt::Dialog);
265  setWindowModality(Qt::ApplicationModal);
266  setWindowTitle("Dense reconstruction options");
267 
268  QGridLayout* grid = new QGridLayout(this);
269 
270  QTabWidget* tab_widget = new QTabWidget(this);
271  tab_widget->setElideMode(Qt::TextElideMode::ElideRight);
272  tab_widget->addTab(new StereoOptionsTab(this, options), "Stereo");
273  tab_widget->addTab(new FusionOptionsTab(this, options), "Fusion");
274  tab_widget->addTab(new MeshingOptionsTab(this, options), "Meshing");
275  tab_widget->addTab(new TexturingOptionsTab(this, options), "Texturing");
276 
277  grid->addWidget(tab_widget, 0, 0);
278 }
279 
281  OptionManager* options)
282  : QWidget(main_window),
283  main_window_(main_window),
284  options_(options),
285  reconstruction_(nullptr),
286  thread_control_widget_(new ThreadControlWidget(this)),
287  options_widget_(new DenseReconstructionOptionsWidget(this, options)),
288  photometric_done_(false),
289  geometric_done_(false) {
290  setWindowFlags(Qt::Dialog);
291  setWindowModality(Qt::ApplicationModal);
292  setWindowTitle("Dense reconstruction");
293  resize(main_window_->size().width() - 20, main_window_->size().height() - 20);
294 
295  QGridLayout* grid = new QGridLayout(this);
296 
297  undistortion_button_ = new QPushButton(tr("Undistortion"), this);
298  connect(undistortion_button_, &QPushButton::released, this,
299  &DenseReconstructionWidget::Undistort);
300  grid->addWidget(undistortion_button_, 0, 0, Qt::AlignLeft);
301 
302  stereo_button_ = new QPushButton(tr("Stereo"), this);
303  connect(stereo_button_, &QPushButton::released, this,
304  &DenseReconstructionWidget::Stereo);
305  grid->addWidget(stereo_button_, 0, 1, Qt::AlignLeft);
306 
307  fusion_button_ = new QPushButton(tr("Fusion"), this);
308  connect(fusion_button_, &QPushButton::released, this,
309  &DenseReconstructionWidget::Fusion);
310  grid->addWidget(fusion_button_, 0, 2, Qt::AlignLeft);
311 
312  poisson_meshing_button_ = new QPushButton(tr("Poisson"), this);
313  connect(poisson_meshing_button_, &QPushButton::released, this,
314  &DenseReconstructionWidget::PoissonMeshing);
315  grid->addWidget(poisson_meshing_button_, 0, 3, Qt::AlignLeft);
316 
317  delaunay_meshing_button_ = new QPushButton(tr("Delaunay"), this);
318  connect(delaunay_meshing_button_, &QPushButton::released, this,
319  &DenseReconstructionWidget::DelaunayMeshing);
320  grid->addWidget(delaunay_meshing_button_, 0, 4, Qt::AlignLeft);
321 
322  texturing_button_ = new QPushButton(tr("Texturing"), this);
323  connect(texturing_button_, &QPushButton::released, this,
324  &DenseReconstructionWidget::Texturing);
325  grid->addWidget(texturing_button_, 0, 5, Qt::AlignLeft);
326 
327  QPushButton* options_button = new QPushButton(tr("Options"), this);
328  connect(options_button, &QPushButton::released, options_widget_,
329  &OptionsWidget::show);
330  grid->addWidget(options_button, 0, 6, Qt::AlignLeft);
331 
332  QLabel* workspace_path_label = new QLabel("Workspace", this);
333  grid->addWidget(workspace_path_label, 0, 7, Qt::AlignRight);
334 
335  workspace_path_text_ = new QLineEdit(this);
336  grid->addWidget(workspace_path_text_, 0, 8, Qt::AlignRight);
337  connect(workspace_path_text_, &QLineEdit::textChanged, this,
338  &DenseReconstructionWidget::RefreshWorkspace, Qt::QueuedConnection);
339 
340  QPushButton* refresh_path_button = new QPushButton(tr("Refresh"), this);
341  connect(refresh_path_button, &QPushButton::released, this,
342  &DenseReconstructionWidget::RefreshWorkspace, Qt::QueuedConnection);
343  grid->addWidget(refresh_path_button, 0, 9, Qt::AlignRight);
344 
345  QPushButton* workspace_path_button = new QPushButton(tr("Select"), this);
346  connect(workspace_path_button, &QPushButton::released, this,
347  &DenseReconstructionWidget::SelectWorkspacePath,
348  Qt::QueuedConnection);
349  grid->addWidget(workspace_path_button, 0, 10, Qt::AlignRight);
350 
351  QStringList table_header;
352  table_header << "image_name"
353  << ""
354  << "photometric"
355  << "geometric"
356  << "src_images";
357 
358  table_widget_ = new QTableWidget(this);
359  table_widget_->setColumnCount(table_header.size());
360  table_widget_->setHorizontalHeaderLabels(table_header);
361 
362  table_widget_->setShowGrid(true);
363  table_widget_->setSelectionBehavior(QAbstractItemView::SelectRows);
364  table_widget_->setSelectionMode(QAbstractItemView::SingleSelection);
365  table_widget_->setEditTriggers(QAbstractItemView::NoEditTriggers);
366  table_widget_->verticalHeader()->setDefaultSectionSize(25);
367 
368  grid->addWidget(table_widget_, 1, 0, 1, 10);
369 
370  grid->setColumnStretch(4, 1);
371 
372  image_viewer_widget_ = new ImageViewerWidget(this);
373  image_viewer_widget_->setWindowModality(Qt::ApplicationModal);
374 
375  refresh_workspace_action_ = new QAction(this);
376  connect(refresh_workspace_action_, &QAction::triggered, this,
377  &DenseReconstructionWidget::RefreshWorkspace);
378 
379  write_fused_points_action_ = new QAction(this);
380  connect(write_fused_points_action_, &QAction::triggered, this,
381  &DenseReconstructionWidget::WriteFusedPoints);
382 
383  show_meshing_info_action_ = new QAction(this);
384  connect(show_meshing_info_action_, &QAction::triggered, this,
385  &DenseReconstructionWidget::ShowMeshingInfo);
386 
387  RefreshWorkspace();
388 }
389 
390 void DenseReconstructionWidget::showEvent(QShowEvent* event) {
391  Q_UNUSED(event);
392 
393  // Auto-load workspace path from configuration if not already set
394  if (workspace_path_text_->text().isEmpty()) {
395  std::string default_workspace;
396 
397  // Try multiple sources for workspace path (in order of priority)
398  // 1. Try project_path's parent directory
399  if (options_->project_path && !options_->project_path->empty()) {
400  default_workspace = GetParentDir(*options_->project_path);
401  if (ExistsDir(default_workspace)) {
402  workspace_path_text_->setText(QString::fromStdString(default_workspace));
403  std::cout << "Auto-loaded workspace from project_path: " << default_workspace << std::endl;
404  RefreshWorkspace();
405  return;
406  }
407  }
408 
409  // 2. Try database_path's parent directory
410  if (options_->database_path && !options_->database_path->empty()) {
411  default_workspace = GetParentDir(*options_->database_path);
412  if (ExistsDir(default_workspace)) {
413  workspace_path_text_->setText(QString::fromStdString(default_workspace));
414  std::cout << "Auto-loaded workspace from database_path: " << default_workspace << std::endl;
415  RefreshWorkspace();
416  return;
417  }
418  }
419 
420  // 3. Try image_path (it's usually already a directory)
421  if (options_->image_path && !options_->image_path->empty()) {
422  // First try image_path itself
423  default_workspace = *options_->image_path;
424  if (ExistsDir(default_workspace)) {
425  // Use parent of image_path as workspace
426  default_workspace = GetParentDir(default_workspace);
427  if (ExistsDir(default_workspace)) {
428  workspace_path_text_->setText(QString::fromStdString(default_workspace));
429  std::cout << "Auto-loaded workspace from image_path: " << default_workspace << std::endl;
430  RefreshWorkspace();
431  return;
432  }
433  }
434  }
435  std::cout << "Could not auto-load workspace path. Please select manually." << std::endl;
436  }
437 
438  RefreshWorkspace();
439 }
440 
442  reconstruction_ = reconstruction;
443  show();
444  raise();
445 }
446 
447 void DenseReconstructionWidget::Undistort() {
448  const std::string workspace_path = GetWorkspacePath();
449  if (workspace_path.empty()) {
450  return;
451  }
452 
453  if (reconstruction_ == nullptr || reconstruction_->NumRegImages() < 2) {
454  QMessageBox::critical(this, "",
455  tr("No reconstruction selected in main window"));
456  return;
457  }
458 
459  COLMAPUndistorter* undistorter =
460  new COLMAPUndistorter(UndistortCameraOptions(), reconstruction_,
461  *options_->image_path, workspace_path);
462  undistorter->AddCallback(Thread::FINISHED_CALLBACK,
463  [this]() { refresh_workspace_action_->trigger(); });
464  thread_control_widget_->StartThread("Undistorting...", true, undistorter);
465 }
466 
467 void DenseReconstructionWidget::Stereo() {
468  const std::string workspace_path = GetWorkspacePath();
469  if (workspace_path.empty()) {
470  return;
471  }
472 
473 #ifdef CUDA_ENABLED
474  mvs::PatchMatchController* processor = new mvs::PatchMatchController(
475  *options_->patch_match_stereo, workspace_path, "COLMAP", "");
476  processor->AddCallback(Thread::FINISHED_CALLBACK,
477  [this]() { refresh_workspace_action_->trigger(); });
478  thread_control_widget_->StartThread("Stereo...", true, processor);
479 #else
480  QMessageBox::critical(this, "",
481  tr("Dense stereo reconstruction requires CUDA, which "
482  "is not available on your system."));
483 #endif
484 }
485 
486 void DenseReconstructionWidget::Fusion() {
487  const std::string workspace_path = GetWorkspacePath();
488  if (workspace_path.empty()) {
489  return;
490  }
491 
492  std::string input_type;
493  if (geometric_done_) {
494  input_type = "geometric";
495  } else if (photometric_done_) {
496  input_type = "photometric";
497  } else {
498  QMessageBox::critical(this, "",
499  tr("All images must be processed prior to fusion"));
500  }
501 
502  mvs::StereoFusion* fuser = new mvs::StereoFusion(
503  *options_->stereo_fusion, workspace_path, "COLMAP", "", input_type);
504  fuser->AddCallback(Thread::FINISHED_CALLBACK, [this, fuser]() {
505  fused_points_ = fuser->GetFusedPoints();
506  fused_points_visibility_ = fuser->GetFusedPointsVisibility();
507  write_fused_points_action_->trigger();
508  });
509  thread_control_widget_->StartThread("Fusion...", true, fuser);
510 }
511 
512 void DenseReconstructionWidget::PoissonMeshing() {
513  const std::string workspace_path = GetWorkspacePath();
514  if (workspace_path.empty()) {
515  return;
516  }
517 
518  if (ExistsFile(JoinPaths(workspace_path, kFusedFileName))) {
519  thread_control_widget_->StartFunction(
520  "Poisson Meshing...", [this, workspace_path]() {
522  *options_->poisson_meshing,
523  JoinPaths(workspace_path, kFusedFileName),
524  JoinPaths(workspace_path, kPoissonMeshedFileName));
525  refresh_workspace_action_->trigger();
526  show_meshing_info_action_->trigger();
527  });
528  }
529 }
530 
531 void DenseReconstructionWidget::DelaunayMeshing() {
532 #ifdef CGAL_ENABLED
533  const std::string workspace_path = GetWorkspacePath();
534  if (workspace_path.empty()) {
535  return;
536  }
537 
538  if (ExistsFile(JoinPaths(workspace_path, kFusedFileName))) {
539  thread_control_widget_->StartFunction(
540  "Delaunay Meshing...", [this, workspace_path]() {
541  mvs::DenseDelaunayMeshing(
542  *options_->delaunay_meshing, workspace_path,
543  JoinPaths(workspace_path, kDelaunayMeshedFileName));
544  refresh_workspace_action_->trigger();
545  show_meshing_info_action_->trigger();
546  });
547  }
548 #else
549  QMessageBox::critical(this, "",
550  tr("Delaunay meshing requires CGAL, which "
551  "is not available on your system."));
552 #endif
553 }
554 
555 void DenseReconstructionWidget::Texturing() {
556  const std::string workspace_path = GetWorkspacePath();
557  if (workspace_path.empty()) {
558  return;
559  }
560 
561  if (reconstruction_ == nullptr || reconstruction_->NumRegImages() < 2) {
562  QMessageBox::critical(this, "",
563  tr("No reconstruction selected in main window"));
564  return;
565  }
566 
567  // Determine which mesh file to use based on mesh_source option
568  if (!ExistsFile(options_->texturing->meshed_file_path)) {
569  const std::string poisson_path =
570  JoinPaths(workspace_path, kPoissonMeshedFileName);
571  const std::string delaunay_path =
572  JoinPaths(workspace_path, kDelaunayMeshedFileName);
573  const bool poisson_exists = ExistsFile(poisson_path);
574  const bool delaunay_exists = ExistsFile(delaunay_path);
575 
576  if (options_->texturing->mesh_source == "delaunayx") {
577  if (delaunay_exists) {
578  options_->texturing->meshed_file_path = delaunay_path;
579  } else {
580  QMessageBox::critical(this, "",
581  tr("Delaunay mesh file not found. Please "
582  "run Delaunay meshing first."));
583  return;
584  }
585  } else if (options_->texturing->mesh_source == "poisson") {
586  if (poisson_exists) {
587  options_->texturing->meshed_file_path = poisson_path;
588  } else {
589  QMessageBox::critical(this, "",
590  tr("Poisson mesh file not found. Please "
591  "run Poisson meshing first."));
592  return;
593  }
594  } else { // "auto" - prefer Delaunay, fallback to Poisson
595  if (delaunay_exists) {
596  options_->texturing->meshed_file_path = delaunay_path;
597  } else if (poisson_exists) {
598  options_->texturing->meshed_file_path = poisson_path;
599  } else {
600  QMessageBox::critical(this, "",
601  tr("No mesh file found. Please run "
602  "Poisson or Delaunay meshing first."));
603  return;
604  }
605  }
606  }
607 
608  // Use default textured file path in local workspace path
609  if (options_->texturing->textured_file_path.empty()) {
610  options_->texturing->textured_file_path =
611  JoinPaths(workspace_path, "textured-mesh.obj");
612  }
613 
614  TexturingReconstruction* texturing_tool =
615  new TexturingReconstruction(*options_->texturing, *reconstruction_,
616  *options_->image_path, workspace_path);
617  texturing_tool->AddCallback(Thread::FINISHED_CALLBACK, [this]() {
618  show_meshing_info_action_->trigger();
619  });
620  thread_control_widget_->StartThread("Texturing...", true, texturing_tool);
621 }
622 
623 void DenseReconstructionWidget::SelectWorkspacePath() {
624  std::string workspace_path;
625  if (workspace_path_text_->text().isEmpty()) {
626  workspace_path = GetParentDir(*options_->project_path);
627  } else {
628  workspace_path = workspace_path_text_->text().toUtf8().constData();
629  }
630 
631  workspace_path_text_->setText(QFileDialog::getExistingDirectory(
632  this, tr("Select workspace path..."),
633  QString::fromStdString(workspace_path), QFileDialog::ShowDirsOnly));
634 
635  RefreshWorkspace();
636 }
637 
638 std::string DenseReconstructionWidget::GetWorkspacePath() {
639  const std::string workspace_path =
640  workspace_path_text_->text().toUtf8().constData();
641  if (ExistsDir(workspace_path)) {
642  return workspace_path;
643  } else {
644  QMessageBox::critical(this, "", tr("Invalid workspace path"));
645  return "";
646  }
647 }
648 
649 void DenseReconstructionWidget::RefreshWorkspace() {
650  table_widget_->clearContents();
651  table_widget_->setRowCount(0);
652 
653  const std::string workspace_path =
654  workspace_path_text_->text().toUtf8().constData();
655  if (ExistsDir(workspace_path)) {
656  undistortion_button_->setEnabled(true);
657  } else {
658  undistortion_button_->setEnabled(false);
659  stereo_button_->setEnabled(false);
660  fusion_button_->setEnabled(false);
661  poisson_meshing_button_->setEnabled(false);
662  delaunay_meshing_button_->setEnabled(false);
663  texturing_button_->setEnabled(false);
664  return;
665  }
666 
667  images_path_ = JoinPaths(workspace_path, "images");
668  depth_maps_path_ = JoinPaths(workspace_path, "stereo/depth_maps");
669  normal_maps_path_ = JoinPaths(workspace_path, "stereo/normal_maps");
670  const std::string config_path =
671  JoinPaths(workspace_path, "stereo/patch-match.cfg");
672 
673  if (ExistsDir(images_path_) && ExistsDir(depth_maps_path_) &&
674  ExistsDir(normal_maps_path_) &&
675  ExistsDir(JoinPaths(workspace_path, "sparse")) &&
676  ExistsDir(JoinPaths(workspace_path, "stereo/consistency_graphs")) &&
677  ExistsFile(config_path)) {
678  stereo_button_->setEnabled(true);
679  } else {
680  stereo_button_->setEnabled(false);
681  fusion_button_->setEnabled(false);
682  poisson_meshing_button_->setEnabled(false);
683  delaunay_meshing_button_->setEnabled(false);
684  texturing_button_->setEnabled(false);
685  return;
686  }
687 
688  const auto images = ReadPatchMatchConfig(config_path);
689  table_widget_->setRowCount(images.size());
690 
691  for (size_t i = 0; i < images.size(); ++i) {
692  const std::string image_name = images[i].first;
693  const std::string src_images = images[i].second;
694  const std::string image_path = JoinPaths(images_path_, image_name);
695 
696  QTableWidgetItem* image_name_item =
697  new QTableWidgetItem(QString::fromStdString(image_name));
698  table_widget_->setItem(i, 0, image_name_item);
699 
700  QPushButton* image_button = new QPushButton("Image");
701  connect(image_button, &QPushButton::released,
702  [this, image_name, image_path]() {
703  image_viewer_widget_->setWindowTitle(
704  QString("Image for %1").arg(image_name.c_str()));
705  image_viewer_widget_->ReadAndShow(image_path);
706  });
707  table_widget_->setCellWidget(i, 1, image_button);
708 
709  table_widget_->setCellWidget(
710  i, 2, GenerateTableButtonWidget(image_name, "photometric"));
711  table_widget_->setCellWidget(
712  i, 3, GenerateTableButtonWidget(image_name, "geometric"));
713 
714  QTableWidgetItem* src_images_item =
715  new QTableWidgetItem(QString::fromStdString(src_images));
716  table_widget_->setItem(i, 4, src_images_item);
717  }
718 
719  table_widget_->resizeColumnsToContents();
720 
721  fusion_button_->setEnabled(photometric_done_ || geometric_done_);
722  poisson_meshing_button_->setEnabled(
723  ExistsFile(JoinPaths(workspace_path, kFusedFileName)));
724  delaunay_meshing_button_->setEnabled(
725  ExistsFile(JoinPaths(workspace_path, kFusedFileName)));
726  texturing_button_->setEnabled(
727  ExistsFile(JoinPaths(workspace_path, kPoissonMeshedFileName)) ||
728  ExistsFile(JoinPaths(workspace_path, kDelaunayMeshedFileName)));
729 }
730 
731 void DenseReconstructionWidget::WriteFusedPoints() {
732  const int reply = QMessageBox::question(
733  this, "",
734  tr("Do you want to visualize the point cloud? Otherwise, to visualize "
735  "the reconstructed dense point cloud later, navigate to the "
736  "<i>dense</i> sub-folder in your workspace with <i>File > Import "
737  "model from...</i>."),
738  QMessageBox::Yes | QMessageBox::No);
739  if (reply == QMessageBox::Yes) {
740  const size_t reconstruction_idx =
741  main_window_->reconstruction_manager_.Add();
742  auto& reconstruction =
743  main_window_->reconstruction_manager_.Get(reconstruction_idx);
744 
745  for (const auto& point : fused_points_) {
746  const Eigen::Vector3d xyz(point.x, point.y, point.z);
747  reconstruction.AddPoint3D(xyz, Track(),
748  Eigen::Vector3ub(point.r, point.g, point.b));
749  }
750 
751  options_->render->min_track_len = 0;
752  main_window_->reconstruction_manager_widget_->Update();
753  main_window_->reconstruction_manager_widget_->SelectReconstruction(
754  reconstruction_idx);
755  main_window_->RenderNow();
756  }
757 
758  const std::string workspace_path =
759  workspace_path_text_->text().toUtf8().constData();
760  if (workspace_path.empty()) {
761  fused_points_ = {};
762  fused_points_visibility_ = {};
763  return;
764  }
765 
766  thread_control_widget_->StartFunction("Exporting...", [this,
767  workspace_path]() {
768  const std::string output_path = JoinPaths(workspace_path, kFusedFileName);
769  WriteBinaryPlyPoints(output_path, fused_points_);
770  mvs::WritePointsVisibility(output_path + ".vis", fused_points_visibility_);
771  fused_points_ = {};
772  fused_points_visibility_ = {};
773  poisson_meshing_button_->setEnabled(true);
774  delaunay_meshing_button_->setEnabled(true);
775  });
776 }
777 
778 void DenseReconstructionWidget::ShowMeshingInfo() {
779  QMessageBox::information(
780  this, "",
781  tr("To visualize the meshed model, you must use an external viewer such "
782  "as Meshlab. The model is located in the workspace folder."));
783 }
784 
785 QWidget* DenseReconstructionWidget::GenerateTableButtonWidget(
786  const std::string& image_name, const std::string& type) {
787  CHECK(type == "photometric" || type == "geometric");
788  const bool photometric = type == "photometric";
789 
790  if (photometric) {
791  photometric_done_ = true;
792  } else {
793  geometric_done_ = true;
794  }
795 
796  const std::string depth_map_path =
797  JoinPaths(depth_maps_path_,
798  StringPrintf("%s.%s.bin", image_name.c_str(), type.c_str()));
799  const std::string normal_map_path =
800  JoinPaths(normal_maps_path_,
801  StringPrintf("%s.%s.bin", image_name.c_str(), type.c_str()));
802 
803  QWidget* button_widget = new QWidget();
804  QGridLayout* button_layout = new QGridLayout(button_widget);
805  button_layout->setContentsMargins(0, 0, 0, 0);
806 
807  QPushButton* depth_map_button = new QPushButton("Depth map", button_widget);
808  if (ExistsFile(depth_map_path)) {
809  connect(depth_map_button, &QPushButton::released,
810  [this, image_name, depth_map_path]() {
811  mvs::DepthMap depth_map;
812  depth_map.Read(depth_map_path);
813  image_viewer_widget_->setWindowTitle(
814  QString("Depth map for %1").arg(image_name.c_str()));
815  image_viewer_widget_->ShowBitmap(depth_map.ToBitmap(2, 98));
816  });
817  } else {
818  depth_map_button->setEnabled(false);
819  if (photometric) {
820  photometric_done_ = false;
821  } else {
822  geometric_done_ = false;
823  }
824  }
825  button_layout->addWidget(depth_map_button, 0, 1, Qt::AlignLeft);
826 
827  QPushButton* normal_map_button = new QPushButton("Normal map", button_widget);
828  if (ExistsFile(normal_map_path)) {
829  connect(normal_map_button, &QPushButton::released,
830  [this, image_name, normal_map_path]() {
831  mvs::NormalMap normal_map;
832  normal_map.Read(normal_map_path);
833  image_viewer_widget_->setWindowTitle(
834  QString("Normal map for %1").arg(image_name.c_str()));
835  image_viewer_widget_->ShowBitmap(normal_map.ToBitmap());
836  });
837  } else {
838  normal_map_button->setEnabled(false);
839  if (photometric) {
840  photometric_done_ = false;
841  } else {
842  geometric_done_ = false;
843  }
844  }
845  button_layout->addWidget(normal_map_button, 0, 2, Qt::AlignLeft);
846 
847  return button_widget;
848 }
849 
850 } // namespace colmap
MouseEvent event
char type
DenseReconstructionOptionsWidget(QWidget *parent, OptionManager *options)
DenseReconstructionWidget(MainWindow *main_window, OptionManager *options)
void Show(Reconstruction *reconstruction)
void ShowBitmap(const Bitmap &bitmap)
void ReadAndShow(const std::string &path)
std::shared_ptr< mvs::PoissonMeshingOptions > poisson_meshing
std::shared_ptr< mvs::DelaunayMeshingOptions > delaunay_meshing
std::shared_ptr< TexturingOptions > texturing
std::shared_ptr< mvs::PatchMatchOptions > patch_match_stereo
std::shared_ptr< RenderOptions > render
std::shared_ptr< std::string > database_path
std::shared_ptr< mvs::StereoFusionOptions > stereo_fusion
std::shared_ptr< std::string > project_path
std::shared_ptr< std::string > image_path
void showEvent(QShowEvent *event)
const Reconstruction & Get(const size_t idx) const
size_t NumRegImages() const
void StartThread(const QString &progress_text, const bool stoppable, Thread *thread)
void StartFunction(const QString &progress_text, const std::function< void()> &func)
GraphType data
Definition: graph_cut.cc:138
Matrix< uint8_t, 3, 1 > Vector3ub
Definition: types.h:42
QTextStream & endl(QTextStream &stream)
Definition: QtCompat.h:718
colmap::OptionManager OptionManager
void WritePointsVisibility(const std::string &path, const std::vector< std::vector< int >> &points_visibility)
Definition: fusion.cc:635
bool PoissonMeshing(const PoissonMeshingOptions &options, const std::string &input_path, const std::string &output_path)
Definition: meshing.cc:123
void StringTrim(std::string *str)
Definition: string.cc:188
void WriteBinaryPlyPoints(const std::string &path, const std::vector< PlyPoint > &points, const bool write_normal, const bool write_rgb)
Definition: ply.cc:369
bool ExistsDir(const std::string &path)
Definition: misc.cc:104
bool ExistsFile(const std::string &path)
Definition: misc.cc:100
std::string JoinPaths(T const &... paths)
Definition: misc.h:128
std::string StringPrintf(const char *format,...)
Definition: string.cc:131
std::string GetParentDir(const std::string &path)
Definition: misc.cc:128
std::string toString(T x)
Definition: Common.h:80