40 const std::string& image_path,
41 const std::string& output_path,
42 const std::vector<image_t>& image_ids)
44 image_path_(image_path),
45 output_path_(output_path),
46 image_ids_(image_ids),
47 reconstruction_(reconstruction) {
48 camera_trajectory_ = std::make_shared<cloudViewer::camera::PinholeCameraTrajectory>();
51 void TexturingReconstruction::Run() {
75 workspace_ = std::make_unique<colmap::mvs::CachedWorkspace>(
83 ThreadPool thread_pool;
84 std::vector<std::future<bool>> futures;
86 camera_trajectory_->parameters_.clear();
87 if (image_ids_.empty()) {
88 camera_trajectory_->parameters_.resize(reconstruction_.
NumRegImages());
89 for (
size_t i = 0; i < reconstruction_.
NumRegImages(); ++i) {
91 futures.push_back(thread_pool.AddTask(
92 &TexturingReconstruction::Texturing,
this, image_id, i));
95 std::size_t index = 0;
96 camera_trajectory_->parameters_.resize(image_ids_.size());
97 for (
const image_t image_id : image_ids_) {
99 thread_pool.AddTask(&TexturingReconstruction::Texturing,
100 this, image_id, index));
107 image_names_.clear();
108 for (
size_t i = 0; i < futures.size(); ++i) {
114 std::cout <<
StringPrintf(
"texture image [%d/%d]\n", i + 1, futures.size());
117 if (futures[i].get()) {
118 if (image_ids_.empty()) {
120 image_names_.push_back(reconstruction_.
Image(image_id).
Name());
122 image_names_.push_back(
123 reconstruction_.
Image(image_ids_[i]).
Name());
129 for (
size_t i = 0; i < camera_trajectory_->parameters_.size(); ++i) {
130 auto& cameraParams = camera_trajectory_->parameters_[i];
131 if (!cameraParams.intrinsic_.IsValid()) {
133 "Invalid camera intrinsic parameters found and ignore "
141 bool mesh_loaded =
false;
144 if (!mesh.CreateInternalCloud()) {
145 std::cerr <<
"ERROR: " <<
StringPrintf(
"creating internal cloud failed!\n");
157 std::cout <<
StringPrintf(
"Loaded mesh: %zu vertices, %zu triangles\n",
165 std::cerr <<
"WARNING: " <<
StringPrintf(
"Failed to load mesh from %s\n",
170 std::cerr <<
"ERROR: " <<
StringPrintf(
"Mesh file path is empty or mesh file does not exist: %s\n",
176 std::shared_ptr<cloudViewer::camera::PinholeCameraTrajectory> filtered_trajectory =
179 filtered_trajectory = FilterCameraTrajectory(&mesh);
181 std::cout <<
StringPrintf(
"Filtered camera trajectory: %zu -> %zu cameras\n",
182 camera_trajectory_->parameters_.size(),
183 filtered_trajectory->parameters_.size());
187 if (filtered_trajectory->parameters_.empty()) {
189 "No valid cameras after filtering! Aborting texturing.\n");
194 for (
size_t i = 0; i < filtered_trajectory->parameters_.size(); ++i) {
195 const auto& params = filtered_trajectory->parameters_[i];
196 if (!params.intrinsic_.IsValid()) {
198 "Camera %zu in filtered trajectory has invalid "
204 for (
int row = 0; row < 4; ++row) {
205 for (
int col = 0; col < 4; ++col) {
206 if (!std::isfinite(params.extrinsic_(row, col))) {
208 "Camera %zu in filtered trajectory has invalid "
219 mvs::MvsTexturing::Options mvs_options;
220 mvs_options.verbose = options_.
verbose;
222 mvs_options.max_viewing_angle_deg = 75.0f;
225 mvs_options.use_gradient_magnitude =
false;
227 mvs::MvsTexturing texturing(mvs_options, reconstruction_, workspace_.get(),
232 std::cerr <<
"ERROR: " <<
StringPrintf(
"Mesh must be loaded before texturing!\n");
235 if (texturing.TextureMesh(mesh, *filtered_trajectory,
237 std::cout <<
StringPrintf(
"Save textured mesh to %s successfully!\n",
240 std::cerr <<
"WARNING: " <<
StringPrintf(
"Texturing reconstruction failed!\n");
245 bool TexturingReconstruction::Texturing(
const image_t image_id,
250 const std::string input_image_path =
JoinPaths(image_path_,
image.Name());
251 const std::string texture_file =
JoinPaths(
"images",
image.Name());
253 std::string target_file_path =
263 int workspace_image_idx = GetWorkspaceImageIdx(image_id);
266 "[Texturing] Processing image_id=%u, image_name='%s', "
267 "workspace_image_idx=%d\n",
268 image_id,
image.Name().c_str(), workspace_image_idx);
273 auto& cameraParams = camera_trajectory_->parameters_[index];
274 cameraParams.texture_file_ = texture_file;
276 Eigen::Matrix3d
rotation = proj_matrix.leftCols<3>();
277 Eigen::Vector3d translation = proj_matrix.rightCols<1>();
281 std::string model_name = camera.
ModelName();
283 if (model_name ==
"SIMPLE_PINHOLE" || model_name ==
"SIMPLE_RADIAL" ||
284 model_name ==
"SIMPLE_RADIAL_FISHEYE" || model_name ==
"RADIAL" ||
285 model_name ==
"RADIAL_FISHEYE") {
287 cameraParams.intrinsic_.SetIntrinsics(
288 static_cast<int>(camera.
Width()),
292 }
else if (model_name ==
"PINHOLE" || model_name ==
"OPENCV" ||
293 model_name ==
"OPENCV_FISHEYE" || model_name ==
"FULL_OPENCV" ||
294 model_name ==
"FOV" || model_name ==
"THIN_PRISM_FISHEYE") {
296 cameraParams.intrinsic_.SetIntrinsics(
297 static_cast<int>(camera.
Width()),
315 "Unsupported camera model with texturing "
316 "detected! If possible, re-run the SfM reconstruction with the "
317 "SIMPLE_PINHOLE or the PINHOLE camera model (recommended). "
318 "Otherwise, use the undistortion step in Colmap to obtain "
319 "undistorted images and corresponding camera models without "
322 std::cerr <<
"ERROR: " <<
StringPrintf(msg.c_str()) <<
"\n";
336 #define PrintOption(option) \
337 std::cout << " " << #option ": " << option << std::endl
352 int TexturingReconstruction::GetWorkspaceImageIdx(
353 const image_t image_id)
const {
356 "[GetWorkspaceImageIdx] workspace_ is null for image_id=%u\n",
362 const std::string& image_name =
image.Name();
364 const auto& model = workspace_->GetModel();
367 "[GetWorkspaceImageIdx] Looking up image_name='%s' (image_id=%u) "
368 "in workspace model (model has %zu images)\n",
369 image_name.c_str(), image_id, model.images.size());
374 int workspace_image_idx = model.GetImageIdx(image_name);
377 "[GetWorkspaceImageIdx] Found workspace_image_idx=%d for "
379 workspace_image_idx, image_name.c_str());
382 if (workspace_image_idx < 0) {
384 "[GetWorkspaceImageIdx] Invalid negative "
385 "workspace_image_idx=%d for image_name='%s'\n",
386 workspace_image_idx, image_name.c_str());
390 if (
static_cast<size_t>(workspace_image_idx) >= model.images.size()) {
392 "[GetWorkspaceImageIdx] workspace_image_idx=%d >= "
393 "model.images.size()=%zu for image_name='%s'\n",
394 workspace_image_idx, model.images.size(),
400 "[GetWorkspaceImageIdx] Successfully mapped image_name='%s' -> "
401 "workspace_image_idx=%d\n",
402 image_name.c_str(), workspace_image_idx);
403 return workspace_image_idx;
404 }
catch (
const std::exception&
e) {
407 "[GetWorkspaceImageIdx] Exception when looking up "
408 "image_name='%s': %s\n",
409 image_name.c_str(),
e.what());
414 bool TexturingReconstruction::IsPointVisible(
const Eigen::Vector3d& point3d,
416 const Camera& camera,
417 int workspace_image_idx)
const {
419 workspace_image_idx < 0) {
425 const auto& model = workspace_->GetModel();
426 if (
static_cast<size_t>(workspace_image_idx) >= model.images.size()) {
430 if (!workspace_->HasDepthMap(workspace_image_idx)) {
435 const auto& depth_map = workspace_->GetDepthMap(workspace_image_idx);
440 image.ProjectionMatrix();
441 Eigen::Vector4d point_homogeneous(point3d.x(), point3d.y(), point3d.z(),
443 Eigen::Vector3d cam_coords = world_to_cam * point_homogeneous;
446 if (cam_coords.z() <= 0) {
451 Eigen::Matrix3d K = Eigen::Matrix3d::Identity();
452 K(0, 0) = camera.MeanFocalLength();
453 K(1, 1) = camera.MeanFocalLength();
454 K(0, 2) = camera.PrincipalPointX();
455 K(1, 2) = camera.PrincipalPointY();
458 Eigen::Vector3d proj = K * cam_coords;
459 const double u = proj.x() / proj.z();
460 const double v = proj.y() / proj.z();
463 if (u < 0 || u >= camera.Width() || v < 0 || v >= camera.Height()) {
467 const size_t row =
static_cast<size_t>(std::round(v));
468 const size_t col =
static_cast<size_t>(std::round(u));
471 if (row >= depth_map.GetHeight() || col >= depth_map.GetWidth()) {
475 const float depth_map_value = depth_map.Get(row, col);
476 const float point_depth =
static_cast<float>(proj(2));
479 if (depth_map_value <= 0) {
485 const float depth_error =
486 std::abs(point_depth - depth_map_value) / depth_map_value;
488 }
catch (
const std::exception&) {
494 float TexturingReconstruction::ComputeViewQuality(
495 const Eigen::Vector3d& point3d,
496 const Eigen::Vector3d& face_normal,
498 const Camera& camera,
499 int workspace_image_idx)
const {
501 workspace_image_idx < 0) {
503 const Eigen::Vector3d camera_pos =
image.ProjectionCenter();
504 const Eigen::Vector3d view_dir = (point3d - camera_pos).normalized();
505 const double cos_angle = face_normal.normalized().dot(view_dir);
506 return static_cast<float>(std::max(0.0, cos_angle));
510 const auto& model = workspace_->GetModel();
511 if (
static_cast<size_t>(workspace_image_idx) >= model.images.size()) {
513 const Eigen::Vector3d camera_pos =
image.ProjectionCenter();
514 const Eigen::Vector3d view_dir = (point3d - camera_pos).normalized();
515 const double cos_angle = face_normal.normalized().dot(view_dir);
516 return static_cast<float>(std::max(0.0, cos_angle));
519 if (!workspace_->HasNormalMap(workspace_image_idx)) {
521 const Eigen::Vector3d camera_pos =
image.ProjectionCenter();
522 const Eigen::Vector3d view_dir = (point3d - camera_pos).normalized();
523 const double cos_angle = face_normal.normalized().dot(view_dir);
524 return static_cast<float>(std::max(0.0, cos_angle));
528 const auto& normal_map = workspace_->GetNormalMap(workspace_image_idx);
533 image.ProjectionMatrix();
534 Eigen::Vector4d point_homogeneous(point3d.x(), point3d.y(), point3d.z(),
536 Eigen::Vector3d cam_coords = world_to_cam * point_homogeneous;
538 if (cam_coords.z() <= 0) {
543 Eigen::Matrix3d K = Eigen::Matrix3d::Identity();
544 K(0, 0) = camera.MeanFocalLength();
545 K(1, 1) = camera.MeanFocalLength();
546 K(0, 2) = camera.PrincipalPointX();
547 K(1, 2) = camera.PrincipalPointY();
550 Eigen::Vector3d proj = K * cam_coords;
551 const double u = proj.x() / proj.z();
552 const double v = proj.y() / proj.z();
554 if (u < 0 || u >= camera.Width() || v < 0 || v >= camera.Height()) {
558 const size_t row =
static_cast<size_t>(std::round(v));
559 const size_t col =
static_cast<size_t>(std::round(u));
561 if (row >= normal_map.GetHeight() || col >= normal_map.GetWidth()) {
566 Eigen::Vector3d normal_map_normal(normal_map.Get(row, col, 0),
567 normal_map.Get(row, col, 1),
568 normal_map.Get(row, col, 2));
572 const Eigen::Matrix3d R =
image.RotationMatrix();
573 const Eigen::Vector3d face_normal_cam = R * face_normal.normalized();
576 const double cos_angle = face_normal_cam.normalized().dot(
577 normal_map_normal.normalized());
579 return static_cast<float>(cos_angle);
580 }
catch (
const std::exception&) {
582 const Eigen::Vector3d camera_pos =
image.ProjectionCenter();
583 const Eigen::Vector3d view_dir = (point3d - camera_pos).normalized();
584 const double cos_angle = face_normal.normalized().dot(view_dir);
585 return static_cast<float>(std::max(0.0, cos_angle));
589 std::shared_ptr<cloudViewer::camera::PinholeCameraTrajectory>
590 TexturingReconstruction::FilterCameraTrajectory(
ccMesh* mesh)
const {
591 auto filtered_trajectory =
592 std::make_shared<::cloudViewer::camera::PinholeCameraTrajectory>();
595 std::vector<Eigen::Vector3d> mesh_vertices;
597 std::cerr <<
"ERROR: " <<
StringPrintf(
"Mesh is null or has no associated cloud!\n");
598 return filtered_trajectory;
604 if (cloud && cloud->
size() > 0) {
608 const unsigned point_count = cloud->
size();
609 sample_step = std::max(1,
static_cast<int>(point_count / 1000));
610 mesh_vertices.reserve(point_count / sample_step);
612 bool mesh_is_shifted = cloud->
isShifted();
613 for (
unsigned i = 0; i < point_count; i += sample_step) {
615 if (mesh_is_shifted) {
618 mesh_vertices.emplace_back(pt_global.
x, pt_global.
y,
621 mesh_vertices.emplace_back(pt->
x, pt->
y, pt->
z);
627 "Sampled %zu vertices from mesh for visibility testing\n",
628 mesh_vertices.size());
632 std::cerr <<
"WARNING: " <<
StringPrintf(
"Mesh has no valid vertices for visibility testing\n");
636 for (
size_t i = 0; i < camera_trajectory_->parameters_.size(); ++i) {
637 const auto& cameraParams = camera_trajectory_->parameters_[i];
641 if (image_ids_.empty()) {
646 if (i < image_ids_.size()) {
647 image_id = image_ids_[i];
657 int workspace_image_idx = GetWorkspaceImageIdx(image_id);
662 bool has_valid_maps =
false;
663 if (workspace_image_idx >= 0 && workspace_) {
664 has_valid_maps = workspace_->HasDepthMap(workspace_image_idx) &&
665 workspace_->HasNormalMap(workspace_image_idx);
669 bool should_include =
true;
671 if (!has_valid_maps) {
672 should_include =
false;
675 "Excluding camera %s from texturing (no "
676 "depth/normal maps)\n",
677 image.Name().c_str());
679 }
else if (!mesh_vertices.empty()) {
681 size_t visible_count = 0;
682 size_t quality_count = 0;
687 for (
size_t idx = 0; idx < mesh_vertices.size(); ++idx) {
688 const auto& vertex = mesh_vertices[idx];
690 if (IsPointVisible(vertex,
image, camera,
691 workspace_image_idx)) {
695 Eigen::Vector3d face_normal(0, 0, 1);
698 size_t vertex_idx = idx * sample_step;
699 if (vertex_idx < cloud->
size()) {
702 face_normal = Eigen::Vector3d(n.
x, n.
y, n.
z);
713 ComputeViewQuality(vertex, face_normal,
image,
714 camera, workspace_image_idx);
719 if (std::abs(quality) >=
728 const double visibility_ratio =
729 static_cast<double>(visible_count) /
730 mesh_vertices.size();
731 const double quality_ratio =
732 visible_count > 0 ?
static_cast<double>(quality_count) /
736 const double min_visibility_ratio =
741 const double min_quality_ratio = 0.05;
743 should_include = visibility_ratio >= min_visibility_ratio &&
744 quality_ratio >= min_quality_ratio;
747 if (should_include) {
749 "Camera %s: visibility=%.2f%%, quality=%.2f%% "
751 image.Name().c_str(), visibility_ratio * 100.0,
752 quality_ratio * 100.0);
755 "Camera %s: visibility=%.2f%%, quality=%.2f%% "
757 image.Name().c_str(), visibility_ratio * 100.0,
758 quality_ratio * 100.0);
764 if (should_include) {
766 if (!cameraParams.intrinsic_.IsValid()) {
769 "Excluding camera %s from texturing (invalid "
770 "intrinsic parameters)\n",
771 image.Name().c_str());
777 bool extrinsic_valid =
true;
778 for (
int row = 0; row < 4; ++row) {
779 for (
int col = 0; col < 4; ++col) {
780 const double val = cameraParams.extrinsic_(row, col);
781 if (!std::isfinite(val)) {
782 extrinsic_valid =
false;
786 if (!extrinsic_valid)
break;
789 if (!extrinsic_valid) {
792 "Excluding camera %s from texturing (invalid "
793 "extrinsic matrix)\n",
794 image.Name().c_str());
799 filtered_trajectory->parameters_.push_back(cameraParams);
804 if (filtered_trajectory->parameters_.empty()) {
806 "No valid cameras found after filtering! Using original "
808 return camera_trajectory_;
811 return filtered_trajectory;
std::shared_ptr< core::Tensor > image
static bool PrintDebug(const char *format,...)
Same as Print, but works only in Debug mode.
static ccGLMatrixTpl< double > FromEigenMatrix3(const Eigen::Matrix< double, 3, 3 > &mat)
void setTranslation(const Vector3Tpl< float > &Tr)
Sets translation (float version)
static Eigen::Matrix< double, 4, 4 > ToEigenMatrix4(const ccGLMatrixTpl< float > &mat)
Double version of ccGLMatrixTpl.
A 3D cloud interface with associated features (color, normals, octree, etc.)
static ccPointCloud * ToPointCloud(ccHObject *obj, bool *isLockedVertices=nullptr)
Converts current object to 'equivalent' ccPointCloud.
virtual unsigned size() const override
Returns the number of triangles.
ccGenericPointCloud * getAssociatedCloud() const override
Returns the vertices cloud.
bool hasNormals() const override
Returns whether normals are enabled or not.
bool computeNormals(bool perVertex)
Computes normals.
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
bool hasNormals() const override
Returns whether normals are enabled or not.
const CCVector3 & getPointNormal(unsigned pointIndex) const override
Returns normal corresponding to a given point.
CCVector3d toGlobal3d(const Vector3Tpl< T > &Plocal) const
Returns the point back-projected into the original coordinates system.
bool isShifted() const
Returns whether the cloud is shifted or not.
virtual unsigned size() const =0
Returns the number of points.
unsigned size() const override
const CCVector3 * getPoint(unsigned index) const override
double FocalLengthY() const
std::string ModelName() const
double FocalLength() const
double FocalLengthX() const
double PrincipalPointX() const
double PrincipalPointY() const
const std::string & Name() const
size_t NumRegImages() const
const class Image & Image(const image_t image_id) const
const class Camera & Camera(const camera_t camera_id) const
const std::vector< image_t > & RegImageIds() const
TexturingReconstruction(const TexturingOptions &options, const Reconstruction &reconstruction, const std::string &image_path, const std::string &output_path, const std::vector< image_t > &image_ids=std::vector< image_t >())
const Timer & GetTimer() const
void PrintMinutes() const
Matrix< double, 3, 4 > Matrix3x4d
bool AutoReadMesh(const std::string &filename, ccMesh &mesh, const ReadTriangleMeshOptions ¶ms={})
std::string GetFileParentDirectory(const std::string &filename)
void PrintHeading2(const std::string &heading)
bool ExistsFile(const std::string &path)
void CreateDirIfNotExists(const std::string &path)
std::string JoinPaths(T const &... paths)
void PrintHeading1(const std::string &heading)
std::string StringPrintf(const char *format,...)
const image_t kInvalidImageId
void FileCopy(const std::string &src_path, const std::string &dst_path, CopyType type)
bool use_depth_normal_maps
double min_normal_consistency
std::string meshed_file_path
std::string depth_map_type
std::string textured_file_path
double max_viewing_angle_deg
bool use_gradient_magnitude
std::string workspace_format
std::string stereo_folder
std::string workspace_path
#define PrintOption(option)