11 #include <QImageReader>
22 #include <unordered_map>
23 #include <unordered_set>
55 const std::string& image_file,
56 const Eigen::Matrix3f& projection,
57 const Eigen::Matrix4f& world_to_cam,
58 const Eigen::Vector3f& pos,
59 const Eigen::Vector3f& viewdir,
63 image_file(image_file),
66 projection(projection),
67 world_to_cam(world_to_cam),
72 const Eigen::Vector3f& vertex)
const {
76 Eigen::Vector4f vertex_homogeneous(vertex.x(), vertex.y(), vertex.z(),
80 Eigen::Vector3f cam_coords =
84 if (cam_coords.z() <= 0) {
85 return Eigen::Vector2f(-1, -1);
89 Eigen::Vector3f proj =
projection * cam_coords;
92 Eigen::Vector2f pixel(proj.x() / proj.z(), proj.y() / proj.z());
97 return Eigen::Vector2f(pixel.x() - 0.5f, pixel.y() - 0.5f);
101 return pixel.x() >= 0 && pixel.x() <
width - 1 && pixel.y() >= 0 &&
106 const Eigen::Vector3f& v2,
107 const Eigen::Vector3f& v3)
const {
115 const std::vector<size_t>& faces,
116 const std::vector<Eigen::Vector2f>& texcoords,
117 std::shared_ptr<QImage>
image,
124 texcoords(texcoords),
134 const std::string& image_path)
136 reconstruction_(reconstruction),
137 workspace_(workspace),
138 image_path_(image_path) {}
140 int MvsTexturing::GetWorkspaceImageIdx(
const colmap::image_t image_id)
const {
145 const std::string& image_name =
image.Name();
147 const auto& model = workspace_->
GetModel();
152 int workspace_image_idx = model.
GetImageIdx(image_name);
155 if (workspace_image_idx < 0 ||
156 static_cast<size_t>(workspace_image_idx) >= model.images.size()) {
160 return workspace_image_idx;
161 }
catch (
const std::exception&) {
167 bool MvsTexturing::IsPointVisible(
const Eigen::Vector3d& point3d,
170 int workspace_image_idx)
const {
172 workspace_image_idx < 0) {
177 const auto& model = workspace_->
GetModel();
178 if (
static_cast<size_t>(workspace_image_idx) >= model.images.size()) {
182 if (!workspace_->
HasDepthMap(workspace_image_idx)) {
187 const auto& depth_map = workspace_->
GetDepthMap(workspace_image_idx);
190 const Eigen::Vector3d proj = proj_matrix * point3d.homogeneous();
196 const double u = proj(0) / proj(2);
197 const double v = proj(1) / proj(2);
199 if (u < 0 || u >= camera.
Width() || v < 0 || v >= camera.
Height()) {
203 const size_t row =
static_cast<size_t>(std::round(v));
204 const size_t col =
static_cast<size_t>(std::round(u));
206 if (row >= depth_map.GetHeight() || col >= depth_map.GetWidth()) {
210 const float depth_map_value = depth_map.Get(row, col);
211 const float point_depth =
static_cast<float>(proj(2));
213 if (depth_map_value <= 0) {
220 const float depth_error =
221 std::abs(point_depth - depth_map_value) / depth_map_value;
226 const float absolute_tolerance = 0.1f;
227 return depth_error <= relative_tolerance ||
228 std::abs(point_depth - depth_map_value) <=
229 depth_map_value * absolute_tolerance;
230 }
catch (
const std::exception&) {
238 const ::cloudViewer::camera::PinholeCameraTrajectory& camera_trajectory,
239 const std::string& output_path) {
241 std::cout <<
StringPrintf(
"Starting mvs-texturing based texture mapping...\n");
246 if (!generic_cloud) {
248 "Mesh has no associated cloud! "
249 "This may be due to mesh merge failure. "
250 "Please ensure the mesh file has valid vertices.\n");
255 if (!cloud || cloud->
size() == 0) {
256 std::cerr <<
"ERROR: " <<
StringPrintf(
"Mesh has invalid or empty associated cloud!\n");
260 if (mesh.
size() == 0) {
261 std::cerr <<
"ERROR: " <<
StringPrintf(
"Mesh has no triangles!\n");
266 std::cout <<
StringPrintf(
"Using mesh: %u vertices, %u triangles\n", cloud->
size(),
276 "Mesh has no normals, computing per-vertex normals...\n");
285 "Failed to get associated cloud after computing "
290 std::cout <<
StringPrintf(
"Computed per-vertex normals for %u vertices",
291 cloud->
size()) <<
"\n";
297 CreateTextureViews(camera_trajectory);
299 if (texture_views_.empty()) {
300 std::cerr <<
"ERROR: " <<
StringPrintf(
"No valid texture views created!\n");
305 std::cout <<
StringPrintf(
"Created %zu texture views\n", texture_views_.size());
309 BuildAdjacencyGraph(mesh);
312 CalculateDataCosts(mesh);
318 GenerateTexturePatches(mesh);
324 GenerateTextureAtlases();
327 if (!SaveOBJModel(output_path, mesh)) {
328 std::cerr <<
"ERROR: " <<
StringPrintf(
"Failed to save OBJ model to %s\n", output_path.c_str());
335 void MvsTexturing::CreateTextureViews(
336 const ::cloudViewer::camera::PinholeCameraTrajectory& camera_trajectory) {
337 texture_views_.clear();
338 view_to_image_id_.clear();
339 texture_views_.reserve(camera_trajectory.parameters_.size());
340 view_to_image_id_.reserve(camera_trajectory.parameters_.size());
343 for (
size_t i = 0; i < camera_trajectory.parameters_.size(); ++i) {
344 const auto& params = camera_trajectory.parameters_[i];
349 std::string texture_file_base =
359 view_to_image_id_.push_back(image_id);
363 Eigen::Matrix3f projection = Eigen::Matrix3f::Identity();
364 Eigen::Matrix4f world_to_cam = Eigen::Matrix4f::Identity();
366 Eigen::Vector3f viewdir;
377 projection(0, 0) = params.intrinsic_.GetFocalLength().first;
378 projection(1, 1) = params.intrinsic_.GetFocalLength().second;
379 projection(0, 2) = params.intrinsic_.GetPrincipalPoint().first;
380 projection(1, 2) = params.intrinsic_.GetPrincipalPoint().second;
384 world_to_cam.block<3, 3>(0, 0) =
385 Rt_matrix.leftCols<3>().cast<
float>();
386 world_to_cam.block<3, 1>(0, 3) =
387 Rt_matrix.rightCols<1>().cast<
float>();
388 world_to_cam(3, 0) = 0.0f;
389 world_to_cam(3, 1) = 0.0f;
390 world_to_cam(3, 2) = 0.0f;
391 world_to_cam(3, 3) = 1.0f;
394 Eigen::Matrix3f R = world_to_cam.block<3, 3>(0, 0);
395 Eigen::Vector3f t = world_to_cam.block<3, 1>(0, 3);
396 pos = -R.transpose() * t;
400 viewdir = -R.transpose() * Eigen::Vector3f(0, 0, 1);
407 projection(0, 0) = params.intrinsic_.GetFocalLength().first;
408 projection(1, 1) = params.intrinsic_.GetFocalLength().second;
409 projection(0, 2) = params.intrinsic_.GetPrincipalPoint().first;
410 projection(1, 2) = params.intrinsic_.GetPrincipalPoint().second;
413 Eigen::Matrix4f cam_to_world = params.extrinsic_.cast<
float>();
414 world_to_cam = cam_to_world.inverse();
416 pos = Eigen::Vector3f(cam_to_world(0, 3), cam_to_world(1, 3),
418 viewdir = -cam_to_world.block<3, 1>(0, 2).normalized();
422 std::string image_file = params.texture_file_;
429 image_file = candidate;
435 std::string workspace_path =
438 workspace_path,
"images",
441 image_file = candidate;
447 image_file = candidate;
455 "Image file does not exist: %s (will try to load anyway)",
456 image_file.c_str()) <<
"\n";
459 auto view = std::make_unique<TextureView>(
460 i, image_file, projection, world_to_cam, pos, viewdir,
461 params.intrinsic_.width_, params.intrinsic_.height_);
463 texture_views_.push_back(std::move(view));
467 void MvsTexturing::CalculateDataCosts(
const ccMesh& mesh) {
469 std::cout <<
StringPrintf(
"Calculating data costs for %zu faces...\n", mesh.
size());
475 std::cerr <<
"ERROR: " <<
StringPrintf(
"Mesh has no associated cloud!\n");
483 bool mesh_is_shifted = cloud->
isShifted();
487 if (options_.
verbose && mesh_is_shifted) {
488 std::cout <<
StringPrintf(
"Mesh has global shift: (%.6f, %.6f, %.6f), scale: %.6f",
489 mesh_global_shift.
x, mesh_global_shift.
y,
490 mesh_global_shift.
z, mesh_global_scale) <<
"\n";
493 const unsigned num_faces = mesh.
size();
494 face_projection_infos_.clear();
495 face_projection_infos_.resize(num_faces);
498 size_t total_faces_processed = 0;
499 size_t faces_inside_view = 0;
500 size_t faces_backface_culled = 0;
501 size_t faces_viewing_angle_too_steep = 0;
502 size_t faces_behind_camera = 0;
503 size_t faces_depth_check_passed = 0;
504 size_t faces_depth_check_failed = 0;
505 size_t faces_zero_area = 0;
506 size_t faces_quality_ok = 0;
509 for (
size_t view_id = 0; view_id < texture_views_.size(); ++view_id) {
510 const auto& view = texture_views_[view_id];
515 std::cerr <<
"WARNING: " <<
StringPrintf(
"View %zu has invalid image_id, skipping",
523 int workspace_image_idx = GetWorkspaceImageIdx(image_id);
526 for (
unsigned face_id = 0; face_id < num_faces; ++face_id) {
527 total_faces_processed++;
532 if (tri_idx(0) >=
static_cast<int>(cloud->
size()) ||
533 tri_idx(1) >=
static_cast<int>(cloud->
size()) ||
534 tri_idx(2) >=
static_cast<int>(cloud->
size())) {
544 Eigen::Vector3f v1, v2, v3;
545 if (mesh_is_shifted) {
550 v1 = Eigen::Vector3f(
static_cast<float>(v1_global.
x),
551 static_cast<float>(v1_global.
y),
552 static_cast<float>(v1_global.
z));
553 v2 = Eigen::Vector3f(
static_cast<float>(v2_global.
x),
554 static_cast<float>(v2_global.
y),
555 static_cast<float>(v2_global.
z));
556 v3 = Eigen::Vector3f(
static_cast<float>(v3_global.
x),
557 static_cast<float>(v3_global.
y),
558 static_cast<float>(v3_global.
z));
560 v1 = Eigen::Vector3f(v1_ptr->
x, v1_ptr->
y, v1_ptr->
z);
561 v2 = Eigen::Vector3f(v2_ptr->
x, v2_ptr->
y, v2_ptr->
z);
562 v3 = Eigen::Vector3f(v3_ptr->
x, v3_ptr->
y, v3_ptr->
z);
566 if (!view->Inside(v1, v2, v3)) {
572 Eigen::Vector3f face_center = (v1 + v2 + v3) / 3.0f;
573 Eigen::Vector3f face_normal_vec = (v2 - v1).cross(v3 - v1);
574 float face_normal_norm = face_normal_vec.norm();
575 if (face_normal_norm < 1e-6f) {
578 Eigen::Vector3f face_normal =
579 face_normal_vec / face_normal_norm;
582 Eigen::Vector3d face_center_d = face_center.cast<
double>();
583 Eigen::Vector3d face_normal_d = face_normal.cast<
double>();
586 const Eigen::Vector3d camera_pos =
image.ProjectionCenter();
587 Eigen::Vector3d view_to_face_vec =
588 (face_center_d - camera_pos).normalized();
589 Eigen::Vector3d face_to_view_vec =
590 (camera_pos - face_center_d).normalized();
593 float viewing_angle =
594 static_cast<float>(face_to_view_vec.dot(face_normal_d));
595 if (viewing_angle < 0.0f) {
596 faces_backface_culled++;
602 const float max_viewing_angle_rad =
604 if (std::acos(viewing_angle) > max_viewing_angle_rad) {
605 faces_viewing_angle_too_steep++;
610 Eigen::Vector3d viewing_direction =
611 image.RotationMatrix()
614 if (viewing_direction.dot(view_to_face_vec) < 0.0) {
615 faces_behind_camera++;
631 bool depth_visible = IsPointVisible(
632 face_center_d,
image, camera, workspace_image_idx);
637 visible = depth_visible;
640 faces_depth_check_passed++;
642 faces_depth_check_failed++;
652 Eigen::Vector2f p1 = view->GetPixelCoords(v1);
653 Eigen::Vector2f p2 = view->GetPixelCoords(v2);
654 Eigen::Vector2f p3 = view->GetPixelCoords(v3);
657 float area = 0.5f * std::abs((p2(0) - p1(0)) * (p3(1) - p1(1)) -
658 (p3(0) - p1(0)) * (p2(1) - p1(1)));
662 const float min_area_threshold =
664 if (area < min_area_threshold) {
672 quality = CalculateGMIQuality(view_id, p1, p2, p3);
674 if (quality <= 0.0f) {
694 Eigen::Vector3f mean_color(0.5f, 0.5f,
697 FaceProjectionInfo info;
698 info.view_id = view_id;
699 info.quality = quality;
700 info.mean_color = mean_color;
702 face_projection_infos_[face_id].push_back(info);
705 if (options_.
verbose && (view_id + 1) % 10 == 0) {
706 std::cout <<
StringPrintf(
"Processed %zu/%zu views...\n", view_id + 1,
707 texture_views_.size());
713 "\nData cost calculation stats:\n"
714 " Total face-view pairs processed: %zu\n"
715 " Inside view: %zu\n"
716 " Backface culled: %zu\n"
717 " Viewing angle too steep: %zu\n"
718 " Behind camera: %zu\n"
719 " Depth check passed: %zu\n"
720 " Depth check not passed: %zu\n"
721 " Zero area projection: %zu\n"
722 " Quality OK (added to data costs): %zu\n",
723 total_faces_processed, faces_inside_view, faces_backface_culled,
724 faces_viewing_angle_too_steep, faces_behind_camera,
725 faces_depth_check_passed, faces_depth_check_failed,
726 faces_zero_area, faces_quality_ok);
731 for (
size_t face_id = 0; face_id < face_projection_infos_.size();
733 auto& infos = face_projection_infos_[face_id];
734 std::sort(infos.begin(), infos.end(),
735 [](
const FaceProjectionInfo&
a,
const FaceProjectionInfo& b) {
736 return a.view_id < b.view_id;
742 float max_quality = 0.0f;
743 std::vector<float> all_qualities;
744 for (
const auto& infos : face_projection_infos_) {
745 for (
const auto& info : infos) {
746 max_quality = std::max(max_quality, info.quality);
747 all_qualities.push_back(info.quality);
752 float percentile = max_quality;
753 if (!all_qualities.empty()) {
754 std::sort(all_qualities.begin(), all_qualities.end());
755 size_t percentile_idx =
static_cast<size_t>(
756 std::round(0.995f * (all_qualities.size() - 1)));
757 percentile_idx = std::min(percentile_idx, all_qualities.size() - 1);
758 percentile = all_qualities[percentile_idx];
762 std::cout <<
StringPrintf(
"Maximum quality: %.6f, 99.5%% percentile: %.6f",
763 max_quality, percentile) <<
"\n";
769 data_costs_.resize(face_projection_infos_.size());
770 for (
size_t face_id = 0; face_id < face_projection_infos_.size();
772 const auto& infos = face_projection_infos_[face_id];
773 data_costs_[face_id].reserve(infos.size());
775 for (
const auto& info : infos) {
777 float normalized_quality =
778 percentile > 0 ? std::min(1.0f, info.quality / percentile)
781 float cost = 1.0f - normalized_quality;
785 data_costs_[face_id].emplace_back(info.view_id, cost);
790 size_t total_projections = 0;
791 for (
const auto& infos : face_projection_infos_) {
792 total_projections += infos.size();
794 std::cout <<
StringPrintf(
"Calculated data costs: %zu total face-view projections",
795 total_projections) <<
"\n";
799 void MvsTexturing::BuildAdjacencyGraph(
const ccMesh& mesh) {
801 std::cout <<
StringPrintf(
"Building adjacency graph for mesh faces...\n");
804 const unsigned num_faces = mesh.
size();
805 face_adjacency_.clear();
806 face_adjacency_.resize(num_faces);
811 std::cerr <<
"ERROR: " <<
StringPrintf(
"Mesh has no associated cloud!\n");
816 std::map<std::pair<unsigned, unsigned>, std::vector<unsigned>>
818 for (
unsigned face_id = 0; face_id < num_faces; ++face_id) {
822 unsigned v1 = tri_idx(0), v2 = tri_idx(1), v3 = tri_idx(2);
825 auto add_edge = [&edge_to_faces](
unsigned a,
unsigned b,
828 edge_to_faces[{
a, b}].push_back(
face);
830 add_edge(v1, v2, face_id);
831 add_edge(v2, v3, face_id);
832 add_edge(v3, v1, face_id);
836 for (
const auto& [edge, faces] : edge_to_faces) {
837 for (
size_t i = 0; i < faces.size(); ++i) {
838 for (
size_t j = i + 1; j < faces.size(); ++j) {
839 unsigned face1 = faces[i];
840 unsigned face2 = faces[j];
843 if (std::find(face_adjacency_[face1].begin(),
844 face_adjacency_[face1].end(),
845 face2) == face_adjacency_[face1].end()) {
846 face_adjacency_[face1].push_back(face2);
848 if (std::find(face_adjacency_[face2].begin(),
849 face_adjacency_[face2].end(),
850 face1) == face_adjacency_[face2].end()) {
851 face_adjacency_[face2].push_back(face1);
858 size_t total_edges = 0;
859 for (
const auto& adj : face_adjacency_) {
860 total_edges += adj.size();
862 std::cout <<
StringPrintf(
"Built adjacency graph: %zu faces, %zu edges\n", num_faces,
867 float MvsTexturing::ComputePairwiseCost(
size_t face1,
870 size_t view2)
const {
872 if (view1 == view2) {
878 void MvsTexturing::SelectViews() {
880 std::cout <<
StringPrintf(
"Selecting views using graph cut algorithm...\n");
883 face_labels_.clear();
884 face_labels_.resize(face_projection_infos_.size(), SIZE_MAX);
887 for (
size_t face_id = 0; face_id < face_projection_infos_.size();
889 const auto& infos = face_projection_infos_[face_id];
895 float min_cost = std::numeric_limits<float>::max();
896 size_t best_view_id = SIZE_MAX;
897 for (
const auto& [view_id, cost] : data_costs_[face_id]) {
898 if (cost < min_cost) {
900 best_view_id = view_id;
904 if (best_view_id != SIZE_MAX) {
905 face_labels_[face_id] = best_view_id;
911 const float lambda = 0.5f;
914 bool changed =
false;
917 for (
size_t face_id = 0; face_id < face_labels_.size(); ++face_id) {
918 if (face_labels_[face_id] == SIZE_MAX)
continue;
919 if (data_costs_[face_id].empty())
continue;
921 float current_cost = std::numeric_limits<float>::max();
923 for (
const auto& [view_id, cost] : data_costs_[face_id]) {
924 if (view_id == face_labels_[face_id]) {
931 float current_pairwise = 0.0f;
932 for (
size_t adj_face : face_adjacency_[face_id]) {
933 if (adj_face < face_labels_.size() &&
934 face_labels_[adj_face] != SIZE_MAX) {
935 current_pairwise += ComputePairwiseCost(
936 face_id, adj_face, face_labels_[face_id],
937 face_labels_[adj_face]);
940 float current_total = current_cost + lambda * current_pairwise;
943 float best_total = current_total;
944 size_t best_label = face_labels_[face_id];
946 for (
const auto& [view_id, cost] : data_costs_[face_id]) {
947 if (view_id == face_labels_[face_id])
continue;
949 float pairwise = 0.0f;
950 for (
size_t adj_face : face_adjacency_[face_id]) {
951 if (adj_face < face_labels_.size() &&
952 face_labels_[adj_face] != SIZE_MAX) {
954 ComputePairwiseCost(face_id, adj_face, view_id,
955 face_labels_[adj_face]);
959 float total = cost + lambda * pairwise;
960 if (total < best_total) {
962 best_label = view_id;
967 face_labels_[face_id] = best_label;
972 if (options_.
verbose && iter % 2 == 0) {
973 std::cout <<
StringPrintf(
"Graph cut iteration %d...\n", iter + 1);
978 size_t labeled_faces = 0;
979 size_t unlabeled_faces = 0;
980 for (
size_t label : face_labels_) {
981 if (label != SIZE_MAX) {
989 std::cout <<
StringPrintf(
"Selected views: %zu/%zu faces have valid labels (%.1f%%)\n",
990 labeled_faces, face_labels_.size(),
991 100.0f * labeled_faces / face_labels_.size());
992 if (unlabeled_faces > 0) {
994 "%zu faces have no valid labels (will be handled as unseen "
1001 void MvsTexturing::GetSubgraphs(
1002 size_t label, std::vector<std::vector<size_t>>* subgraphs)
const {
1004 std::vector<bool> used(face_labels_.size(),
false);
1006 for (
size_t i = 0; i < face_labels_.size(); ++i) {
1007 if (face_labels_[i] == label && !used[i]) {
1008 subgraphs->push_back(std::vector<size_t>());
1010 std::list<size_t> queue;
1014 while (!queue.empty()) {
1015 size_t node = queue.front();
1018 subgraphs->back().push_back(node);
1021 for (
size_t adj_face : face_adjacency_[node]) {
1022 if (adj_face < face_labels_.size() &&
1023 face_labels_[adj_face] == label && !used[adj_face]) {
1024 queue.push_back(adj_face);
1025 used[adj_face] =
true;
1033 void MvsTexturing::MergeVertexProjectionInfos() {
1036 for (
size_t i = 0; i < vertex_projection_infos_.size(); ++i) {
1037 auto& infos = vertex_projection_infos_[i];
1039 std::map<size_t, VertexProjectionInfo> info_map;
1041 for (
const auto& info : infos) {
1042 size_t texture_patch_id = info.texture_patch_id;
1043 auto it = info_map.find(texture_patch_id);
1044 if (it == info_map.end()) {
1045 info_map[texture_patch_id] = info;
1048 it->second.faces.insert(it->second.faces.end(),
1049 info.faces.begin(), info.faces.end());
1054 infos.reserve(info_map.size());
1055 for (
const auto& [patch_id, info] : info_map) {
1056 infos.push_back(info);
1076 const std::vector<size_t>& faces,
1079 bool mesh_is_shifted =
false) {
1082 std::string image_path = texture_view->
image_file;
1085 std::vector<std::string> candidates;
1087 for (
const auto& candidate : candidates) {
1089 image_path = candidate;
1095 QImageReader reader(QString::fromStdString(image_path));
1096 QImage img = reader.read();
1097 if (!img.isNull()) {
1098 texture_view->
image_data = std::make_shared<QImage>(
1099 img.convertToFormat(QImage::Format_RGB888));
1104 std::cerr <<
"ERROR: " <<
StringPrintf(
"Failed to load image for texture view\n");
1111 int min_x = texture_view->
width, min_y = texture_view->
height;
1112 int max_x = 0, max_y = 0;
1113 std::vector<Eigen::Vector2f> texcoords;
1115 for (
size_t face_id : faces) {
1125 Eigen::Vector3f v1, v2, v3;
1126 if (mesh_is_shifted) {
1131 v1 = Eigen::Vector3f(
static_cast<float>(v1_global.
x),
1132 static_cast<float>(v1_global.
y),
1133 static_cast<float>(v1_global.
z));
1134 v2 = Eigen::Vector3f(
static_cast<float>(v2_global.
x),
1135 static_cast<float>(v2_global.
y),
1136 static_cast<float>(v2_global.
z));
1137 v3 = Eigen::Vector3f(
static_cast<float>(v3_global.
x),
1138 static_cast<float>(v3_global.
y),
1139 static_cast<float>(v3_global.
z));
1141 v1 = Eigen::Vector3f(v1_ptr->
x, v1_ptr->
y, v1_ptr->
z);
1142 v2 = Eigen::Vector3f(v2_ptr->
x, v2_ptr->
y, v2_ptr->
z);
1143 v3 = Eigen::Vector3f(v3_ptr->
x, v3_ptr->
y, v3_ptr->
z);
1150 texcoords.push_back(p1);
1151 texcoords.push_back(p2);
1152 texcoords.push_back(p3);
1154 min_x = std::min({min_x,
static_cast<int>(
std::floor(p1.x())),
1157 min_y = std::min({min_y,
static_cast<int>(
std::floor(p1.y())),
1160 max_x = std::max({max_x,
static_cast<int>(
std::ceil(p1.x())),
1163 max_y = std::max({max_y,
static_cast<int>(
std::ceil(p1.y())),
1169 if (min_x < 0 || min_y < 0 || max_x >= texture_view->
width ||
1170 max_y >= texture_view->
height) {
1171 std::cerr <<
"WARNING: " <<
StringPrintf(
"Bounding box out of bounds, clamping\n");
1172 min_x = std::max(0, min_x);
1173 min_y = std::max(0, min_y);
1174 max_x = std::min(texture_view->
width - 1, max_x);
1175 max_y = std::min(texture_view->
height - 1, max_y);
1178 int width = max_x - min_x + 1;
1179 int height = max_y - min_y + 1;
1188 const int texture_patch_border = 1;
1189 int patch_width =
width + 2 * texture_patch_border;
1190 int patch_height =
height + 2 * texture_patch_border;
1191 int patch_min_x = min_x - texture_patch_border;
1192 int patch_min_y = min_y - texture_patch_border;
1195 Eigen::Vector2f
offset(patch_min_x, patch_min_y);
1196 for (
auto& tc : texcoords) {
1201 QImage
patch(patch_width, patch_height, QImage::Format_RGB888);
1202 patch.fill(QColor(255, 0, 255));
1204 int src_x_start = std::max(0, patch_min_x);
1205 int src_y_start = std::max(0, patch_min_y);
1206 int src_x_end = std::min(texture_view->
width, patch_min_x + patch_width);
1207 int src_y_end = std::min(texture_view->
height, patch_min_y + patch_height);
1209 int dst_x_start = src_x_start - patch_min_x;
1210 int dst_y_start = src_y_start - patch_min_y;
1211 int copy_width = src_x_end - src_x_start;
1212 int copy_height = src_y_end - src_y_start;
1214 if (copy_width > 0 && copy_height > 0) {
1215 QImage src_region = texture_view->
image_data->copy(
1216 src_x_start, src_y_start, copy_width, copy_height);
1217 QPainter painter(&
patch);
1218 painter.drawImage(dst_x_start, dst_y_start, src_region);
1222 auto patch_ptr = std::make_shared<QImage>(
patch);
1223 auto texture_patch = std::make_unique<TexturePatch>(
1224 label, faces, texcoords, patch_ptr, patch_min_x, patch_min_y,
1225 patch_min_x + patch_width - 1, patch_min_y + patch_height - 1);
1228 candidate.
min_x = patch_min_x;
1229 candidate.
min_y = patch_min_y;
1230 candidate.
max_x = patch_min_x + patch_width - 1;
1231 candidate.
max_y = patch_min_y + patch_height - 1;
1237 void MvsTexturing::GenerateTexturePatches(
const ccMesh& mesh) {
1240 std::cout <<
StringPrintf(
"Generating texture patches...\n");
1243 texture_patches_.clear();
1247 std::cerr <<
"ERROR: " <<
StringPrintf(
"Mesh has no associated cloud!\n");
1252 vertex_projection_infos_.clear();
1253 vertex_projection_infos_.resize(cloud->
size());
1256 bool mesh_is_shifted = cloud->
isShifted();
1258 size_t num_patches = 0;
1263 for (
size_t i = 0; i < texture_views_.size(); ++i) {
1265 int label =
static_cast<int>(
1272 std::vector<std::vector<size_t>> subgraphs;
1273 GetSubgraphs(view_id, &subgraphs);
1275 if (subgraphs.empty())
continue;
1277 TextureView* texture_view = texture_views_[view_id].get();
1280 if (!texture_view->image_data) {
1281 std::string image_path = texture_view->image_file;
1285 std::vector<std::string> candidates;
1289 std::string workspace_path =
1292 workspace_path,
"images",
1297 candidates.push_back(
1305 for (
const auto& candidate : candidates) {
1307 image_path = candidate;
1313 QImageReader reader(QString::fromStdString(image_path));
1314 QImage img = reader.read();
1315 if (!img.isNull()) {
1316 texture_view->image_data = std::make_shared<QImage>(
1317 img.convertToFormat(QImage::Format_RGB888));
1319 std::cerr <<
"WARNING: " <<
StringPrintf(
"Failed to load image: %s\n", image_path.c_str());
1325 std::list<TexturePatchCandidate> candidates;
1326 for (
size_t j = 0; j < subgraphs.size(); ++j) {
1327 TexturePatchCandidate candidate =
1329 cloud, mesh_is_shifted);
1330 if (candidate.texture_patch) {
1331 candidates.push_back(std::move(candidate));
1340 for (
auto it = candidates.begin(); it != candidates.end(); ++it) {
1341 for (
auto sit = candidates.begin(); sit != candidates.end();) {
1344 bool sit_inside_it = (sit->min_x >= it->min_x &&
1345 sit->max_x <= it->max_x &&
1346 sit->min_y >= it->min_y &&
1347 sit->max_y <= it->max_y);
1349 if (sit_inside_it) {
1352 auto& it_faces = it->texture_patch->faces;
1353 auto& sit_faces = sit->texture_patch->faces;
1354 it_faces.insert(it_faces.end(), sit_faces.begin(),
1357 auto& it_texcoords = it->texture_patch->texcoords;
1358 auto& sit_texcoords = sit->texture_patch->texcoords;
1359 Eigen::Vector2f
offset(sit->min_x - it->min_x,
1360 sit->min_y - it->min_y);
1361 for (
const auto& tc : sit_texcoords) {
1362 it_texcoords.push_back(tc +
offset);
1365 sit = candidates.erase(sit);
1377 for (
auto it = candidates.begin(); it != candidates.end(); ++it) {
1378 size_t texture_patch_id;
1381 texture_patches_.push_back(std::move(it->texture_patch));
1382 texture_patch_id = num_patches++;
1385 const auto&
patch = texture_patches_.back();
1386 const auto& faces =
patch->faces;
1387 const auto& texcoords =
patch->texcoords;
1389 for (
size_t face_idx = 0; face_idx < faces.size(); ++face_idx) {
1390 size_t face_id = faces[face_idx];
1394 for (
size_t j = 0; j < 3; ++j) {
1395 size_t vertex_id = tri_idx(j);
1396 if (vertex_id >= vertex_projection_infos_.size())
continue;
1398 Eigen::Vector2f projection = texcoords[face_idx * 3 + j];
1400 VertexProjectionInfo info;
1401 info.texture_patch_id = texture_patch_id;
1402 info.projection = projection;
1403 info.faces = {face_id};
1405 vertex_projection_infos_[vertex_id].push_back(info);
1412 MergeVertexProjectionInfos();
1418 std::vector<size_t> unseen_faces;
1419 std::vector<std::vector<size_t>> subgraphs;
1424 for (
size_t face_id = 0; face_id < face_labels_.size(); ++face_id) {
1425 if (face_labels_[face_id] == SIZE_MAX) {
1426 unseen_faces.push_back(face_id);
1431 if (!unseen_faces.empty()) {
1432 std::vector<bool> used(face_labels_.size(),
false);
1433 for (
size_t face_id : unseen_faces) {
1434 if (used[face_id])
continue;
1436 subgraphs.push_back(std::vector<size_t>());
1437 std::list<size_t> queue;
1438 queue.push_back(face_id);
1439 used[face_id] =
true;
1441 while (!queue.empty()) {
1442 size_t node = queue.front();
1444 subgraphs.back().push_back(node);
1446 for (
size_t adj_face : face_adjacency_[node]) {
1447 if (adj_face < face_labels_.size() &&
1448 face_labels_[adj_face] == SIZE_MAX &&
1450 queue.push_back(adj_face);
1451 used[adj_face] =
true;
1462 if (!unseen_faces.empty() && options_.
verbose) {
1463 std::cout <<
StringPrintf(
"Found %zu unseen faces in %zu subgraphs\n",
1464 unseen_faces.size(), subgraphs.size());
1468 QImage default_image(3, 3, QImage::Format_RGB888);
1469 default_image.fill(QColor(128, 128, 128));
1471 std::vector<Eigen::Vector2f> texcoords;
1472 for (
size_t i = 0; i < unseen_faces.size(); ++i) {
1475 texcoords.push_back(Eigen::Vector2f(2.0f, 1.0f));
1476 texcoords.push_back(Eigen::Vector2f(1.0f, 1.0f));
1477 texcoords.push_back(Eigen::Vector2f(1.0f, 2.0f));
1480 auto patch_ptr = std::make_shared<QImage>(default_image);
1481 auto texture_patch = std::make_unique<TexturePatch>(
1482 0, unseen_faces, texcoords, patch_ptr, 0, 0, 2, 2);
1484 size_t texture_patch_id = texture_patches_.size();
1485 texture_patches_.push_back(std::move(texture_patch));
1488 for (
size_t i = 0; i < unseen_faces.size(); ++i) {
1489 size_t face_id = unseen_faces[i];
1493 for (
size_t j = 0; j < 3; ++j) {
1494 size_t vertex_id = tri_idx(j);
1495 if (vertex_id >= vertex_projection_infos_.size())
continue;
1497 Eigen::Vector2f projection = texcoords[i * 3 + j];
1499 VertexProjectionInfo info;
1500 info.texture_patch_id = texture_patch_id;
1501 info.projection = projection;
1502 info.faces = {face_id};
1504 vertex_projection_infos_[vertex_id].push_back(info);
1511 MergeVertexProjectionInfos();
1514 std::cout <<
StringPrintf(
"Generated %zu texture patches\n", texture_patches_.size());
1518 void MvsTexturing::SeamLeveling(
const ccMesh& mesh) {
1528 std::cout <<
StringPrintf(
"Performing seam leveling...\n");
1531 if (texture_patches_.empty()) {
1537 seam_edges_.clear();
1538 std::set<std::pair<size_t, size_t>> processed_vertex_edges;
1540 for (
size_t face_id = 0; face_id < face_labels_.size(); ++face_id) {
1541 if (face_labels_[face_id] == SIZE_MAX)
continue;
1543 for (
size_t adj_face : face_adjacency_[face_id]) {
1544 if (adj_face >= face_labels_.size() ||
1545 face_labels_[adj_face] == SIZE_MAX)
1547 if (face_labels_[face_id] == face_labels_[adj_face])
continue;
1556 std::vector<size_t> shared_vertices;
1557 for (
int i = 0; i < 3; ++i) {
1558 for (
int j = 0; j < 3; ++j) {
1559 if (tri1_idx(i) == tri2_idx(j)) {
1560 shared_vertices.push_back(tri1_idx(i));
1565 if (shared_vertices.size() == 2) {
1567 seam.face1 = face_id;
1568 seam.face2 = adj_face;
1569 seam.v1 = std::min(shared_vertices[0], shared_vertices[1]);
1570 seam.v2 = std::max(shared_vertices[0], shared_vertices[1]);
1573 if (processed_vertex_edges.find(edge_key) ==
1574 processed_vertex_edges.end()) {
1575 processed_vertex_edges.insert(edge_key);
1576 seam_edges_.push_back(seam);
1592 std::cout <<
StringPrintf(
"Seam leveling completed: %zu seam edges found\n",
1593 seam_edges_.size());
1597 void MvsTexturing::GenerateTextureAtlases() {
1599 std::cout <<
StringPrintf(
"Generating texture atlases with bin packing...\n");
1602 if (texture_patches_.empty()) {
1607 const unsigned int MAX_TEXTURE_SIZE = 8192;
1608 const unsigned int PREF_TEXTURE_SIZE = 4096;
1610 const unsigned int PADDING = 2;
1613 std::vector<size_t> patch_indices(texture_patches_.size());
1614 std::iota(patch_indices.begin(), patch_indices.end(), 0);
1615 std::sort(patch_indices.begin(), patch_indices.end(),
1616 [
this](
size_t a,
size_t b) {
1617 int size_a = texture_patches_[a]->image->width() *
1618 texture_patches_[a]->image->height();
1619 int size_b = texture_patches_[b]->image->width() *
1620 texture_patches_[b]->image->height();
1621 return size_a > size_b;
1633 std::vector<AtlasPatch> patches;
1636 std::vector<Atlas> atlases;
1637 unsigned int current_atlas_size = PREF_TEXTURE_SIZE;
1639 for (
size_t patch_idx : patch_indices) {
1640 const auto&
patch = texture_patches_[patch_idx];
1641 unsigned int patch_width =
patch->image->width() + 2 * PADDING;
1642 unsigned int patch_height =
patch->image->height() + 2 * PADDING;
1645 bool placed =
false;
1646 for (
auto& atlas : atlases) {
1649 for (
unsigned int y = 0;
y + patch_height <= atlas.height;
1651 for (
unsigned int x = 0;
x + patch_width <= atlas.width;
1655 bool overlaps =
false;
1656 for (
const auto& ap : atlas.patches) {
1657 const auto& existing_patch =
1658 texture_patches_[ap.patch_idx];
1660 existing_patch->image->width() + 2 * PADDING;
1662 existing_patch->image->height() + 2 * PADDING;
1664 if (!(
x + patch_width <= ap.x || ap.x + ew <=
x ||
1665 y + patch_height <= ap.y || ap.y + eh <=
y)) {
1673 ap.patch_idx = patch_idx;
1676 atlas.patches.push_back(ap);
1689 unsigned int atlas_size = current_atlas_size;
1690 if (patch_width > atlas_size || patch_height > atlas_size) {
1691 atlas_size = std::max(patch_width, patch_height);
1693 unsigned int next_power = 1;
1694 while (next_power < atlas_size &&
1695 next_power < MAX_TEXTURE_SIZE) {
1698 atlas_size = std::min(next_power, MAX_TEXTURE_SIZE);
1702 new_atlas.width = atlas_size;
1703 new_atlas.height = atlas_size;
1705 ap.patch_idx = patch_idx;
1708 new_atlas.patches.push_back(ap);
1709 atlases.push_back(new_atlas);
1714 texture_atlases_.clear();
1715 texture_atlases_.reserve(atlases.size());
1717 for (
size_t atlas_idx = 0; atlas_idx < atlases.size(); ++atlas_idx) {
1718 const auto& atlas_info = atlases[atlas_idx];
1721 atlas.width = atlas_info.width;
1722 atlas.height = atlas_info.height;
1723 atlas.image = std::make_shared<QImage>(
1724 atlas_info.width, atlas_info.height, QImage::Format_RGB888);
1730 for (
const auto& ap : atlas_info.patches) {
1731 const auto&
patch = texture_patches_[ap.patch_idx];
1734 QPainter painter(atlas.image.get());
1735 painter.drawImage(ap.x + PADDING, ap.y + PADDING, *
patch->image);
1740 for (
size_t i = 0; i <
patch->faces.size(); ++i) {
1741 size_t face_id =
patch->faces[i];
1742 atlas.face_ids.push_back(face_id);
1745 size_t face_texcoord_start = atlas.texcoords.size();
1746 for (
int j = 0; j < 3; ++j) {
1747 size_t texcoord_idx = i * 3 + j;
1748 Eigen::Vector2f local_tc =
patch->texcoords[texcoord_idx];
1752 (local_tc.x() + ap.x + PADDING) / atlas.width;
1754 (local_tc.y() + ap.y + PADDING) / atlas.height;
1756 atlas.texcoords.emplace_back(atlas_u, atlas_v);
1761 atlas.texcoord_ids.push_back(face_texcoord_start);
1762 atlas.texcoord_ids.push_back(face_texcoord_start + 1);
1763 atlas.texcoord_ids.push_back(face_texcoord_start + 2);
1767 texture_atlases_.push_back(atlas);
1771 std::cout <<
StringPrintf(
"Created %zu texture atlases from %zu patches",
1772 texture_atlases_.size(), texture_patches_.size()) <<
"\n";
1773 for (
size_t i = 0; i < texture_atlases_.size(); ++i) {
1774 std::cout <<
StringPrintf(
" Atlas %zu: %u x %u, %zu faces", i + 1,
1775 texture_atlases_[i].
width, texture_atlases_[i].
height,
1776 texture_atlases_[i].face_ids.size()) <<
"\n";
1787 QImage MvsTexturing::ComputeGradientMagnitudeImage(
const QImage&
image) {
1790 if (
image.format() == QImage::Format_Grayscale8) {
1793 gray =
image.convertToFormat(QImage::Format_Grayscale8);
1796 const int width = gray.width();
1797 const int height = gray.height();
1800 QImage gmi(
width,
height, QImage::Format_Grayscale8);
1808 std::vector<float> magnitudes;
1812 const uchar* row_prev = gray.constScanLine(
y - 1);
1813 const uchar* row_curr = gray.constScanLine(
y);
1814 const uchar* row_next = gray.constScanLine(
y + 1);
1816 for (
int x = 1;
x <
width - 1;
x++) {
1818 int gx = -
static_cast<int>(row_prev[
x - 1]) +
static_cast<int>(row_prev[
x + 1])
1819 - 2 *
static_cast<int>(row_curr[
x - 1]) + 2 *
static_cast<int>(row_curr[
x + 1])
1820 -
static_cast<int>(row_next[
x - 1]) +
static_cast<int>(row_next[
x + 1]);
1823 int gy = -
static_cast<int>(row_prev[
x - 1]) - 2 *
static_cast<int>(row_prev[
x]) -
static_cast<int>(row_prev[
x + 1])
1824 +
static_cast<int>(row_next[
x - 1]) + 2 *
static_cast<int>(row_next[
x]) +
static_cast<int>(row_next[
x + 1]);
1827 float mag = std::sqrt(
static_cast<float>(gx * gx + gy * gy));
1828 magnitudes.push_back(mag);
1833 float max_mag = 0.0f;
1834 for (
float mag : magnitudes) {
1835 max_mag = std::max(max_mag, mag);
1841 uchar* row = gmi.scanLine(
y);
1842 for (
int x = 1;
x <
width - 1;
x++) {
1843 float normalized = (max_mag > 0.0f) ? (magnitudes[idx] / max_mag) : 0.0f;
1844 row[
x] =
static_cast<uchar>(normalized * 255.0f);
1853 bool MvsTexturing::IsPointInTriangle(
float px,
float py,
1854 const Eigen::Vector2f& p1,
1855 const Eigen::Vector2f& p2,
1856 const Eigen::Vector2f& p3)
const {
1858 float denom = (p2.y() - p3.y()) * (p1.x() - p3.x()) +
1859 (p3.x() - p2.x()) * (p1.y() - p3.y());
1861 if (std::abs(denom) < 1e-10f) {
1865 float a = ((p2.y() - p3.y()) * (px - p3.x()) +
1866 (p3.x() - p2.x()) * (
py - p3.y())) / denom;
1867 float b = ((p3.y() - p1.y()) * (px - p3.x()) +
1868 (p1.x() - p3.x()) * (
py - p3.y())) / denom;
1869 float c = 1.0f -
a - b;
1871 return a >= 0.0f && b >= 0.0f && c >= 0.0f;
1876 float MvsTexturing::CalculateGMIQuality(
size_t view_id,
1877 const Eigen::Vector2f& p1,
1878 const Eigen::Vector2f& p2,
1879 const Eigen::Vector2f& p3) {
1881 if (view_id >= gradient_magnitude_images_.size() ||
1882 gradient_magnitude_images_[view_id].isNull()) {
1884 if (view_id < texture_views_.size()) {
1885 const auto& view = texture_views_[view_id];
1886 if (view && view->image_data && !view->image_data->isNull()) {
1887 if (view_id >= gradient_magnitude_images_.size()) {
1888 gradient_magnitude_images_.resize(texture_views_.size());
1890 gradient_magnitude_images_[view_id] =
1891 ComputeGradientMagnitudeImage(*view->image_data);
1900 const QImage& gmi = gradient_magnitude_images_[view_id];
1906 float min_x = std::min({p1.x(), p2.x(), p3.x()});
1907 float max_x = std::max({p1.x(), p2.x(), p3.x()});
1908 float min_y = std::min({p1.y(), p2.y(), p3.y()});
1909 float max_y = std::max({p1.y(), p2.y(), p3.y()});
1912 min_x = std::max(0.0f, min_x);
1913 max_x = std::min(
static_cast<float>(gmi.width() - 1), max_x);
1914 min_y = std::max(0.0f, min_y);
1915 max_y = std::min(
static_cast<float>(gmi.height() - 1), max_y);
1918 std::vector<float> samples;
1919 samples.reserve(
static_cast<size_t>((max_x - min_x + 1) * (max_y - min_y + 1)));
1922 if (y < 0 || y >= gmi.height())
continue;
1923 const uchar* row = gmi.constScanLine(
y);
1926 if (x < 0 || x >= gmi.width())
continue;
1929 if (IsPointInTriangle(
static_cast<float>(
x),
static_cast<float>(
y), p1, p2, p3)) {
1930 float gmi_value =
static_cast<float>(row[
x]) / 255.0f;
1931 samples.push_back(gmi_value);
1936 if (samples.empty()) {
1941 std::sort(samples.begin(), samples.end());
1942 float median_gmi = samples[samples.size() / 2];
1945 float area = 0.5f * std::abs((p2.x() - p1.x()) * (p3.y() - p1.y()) -
1946 (p3.x() - p1.x()) * (p2.y() - p1.y()));
1951 return median_gmi * std::sqrt(std::max(area, 1.0f));
1954 bool MvsTexturing::SaveOBJModel(
const std::string& output_path,
1957 std::cout <<
StringPrintf(
"Exporting OBJ model with ObjFilter...\n");
1960 if (texture_atlases_.empty()) {
1961 std::cerr <<
"WARNING: No texture atlases to save!\n";
1967 std::cerr <<
"ERROR: Mesh has no associated cloud!\n";
1972 std::string root, ext;
1974 std::string prefix = (ext ==
".obj" || ext ==
".OBJ") ? root : output_path;
1979 auto format_material_name = [](
size_t n) -> std::string {
1980 std::string
name =
"material";
1982 while (num_str.length() < 4) {
1983 num_str =
"0" + num_str;
1985 return name + num_str;
1991 if (!export_cloud) {
1992 std::cerr <<
"ERROR: Failed to clone point cloud\n";
1999 export_mesh->
setName(
"textured-mesh");
2000 export_mesh->
addChild(export_cloud);
2003 size_t total_faces = 0;
2004 for (
const auto& atlas : texture_atlases_) {
2005 total_faces += atlas.face_ids.size();
2008 if (!export_mesh->
reserve(total_faces)) {
2009 std::cerr <<
"ERROR: Failed to reserve memory for export mesh\n";
2017 for (
size_t atlas_idx = 0; atlas_idx < texture_atlases_.size(); ++atlas_idx) {
2018 const auto& atlas = texture_atlases_[atlas_idx];
2019 std::string material_name = format_material_name(atlas_idx);
2022 std::string texture_filename = base_name +
"_" + material_name +
"_map_Kd.png";
2030 mat->setShininess(1.0f);
2034 mat->setTexture(*atlas.image, QString::fromStdString(texture_path),
false);
2044 size_t total_texcoords = 0;
2045 for (
const auto& atlas : texture_atlases_) {
2046 total_texcoords += atlas.texcoords.size();
2048 texCoords->reserve(total_texcoords);
2050 for (
const auto& atlas : texture_atlases_) {
2051 for (
const auto& tc : atlas.texcoords) {
2065 std::cerr <<
"ERROR: Failed to reserve per-triangle data\n";
2072 size_t texcoord_offset = 0;
2074 for (
size_t atlas_idx = 0; atlas_idx < texture_atlases_.size(); ++atlas_idx) {
2075 const auto& atlas = texture_atlases_[atlas_idx];
2076 int material_idx =
static_cast<int>(atlas_idx);
2078 const auto& atlas_faces = atlas.face_ids;
2079 const auto& atlas_texcoord_ids = atlas.texcoord_ids;
2081 for (
size_t i = 0; i < atlas_faces.size(); ++i) {
2082 size_t mesh_face_idx = atlas_faces[i];
2094 int tc_idx0 =
static_cast<int>(texcoord_offset + atlas_texcoord_ids[i * 3 + 0]);
2095 int tc_idx1 =
static_cast<int>(texcoord_offset + atlas_texcoord_ids[i * 3 + 1]);
2096 int tc_idx2 =
static_cast<int>(texcoord_offset + atlas_texcoord_ids[i * 3 + 2]);
2101 texcoord_offset += atlas.texcoords.size();
2106 std::cout <<
StringPrintf(
" Saving with ObjFilter (%zu materials, %zu usemtl switches)...\n",
2107 texture_atlases_.size(), texture_atlases_.size());
2115 QString qpath = QString::fromStdString(output_path);
2121 export_mesh =
nullptr;
2127 "Saved OBJ model to %s\n"
2128 " Vertices: %u, Faces: %zu, Materials: %zu, usemtl switches: %zu\n",
2129 output_path.c_str(), cloud->
size(), total_faces,
2130 texture_atlases_.size(), texture_atlases_.size());
2134 std::cerr <<
"ERROR: ObjFilter failed to save, error code: " <<
result <<
std::endl;
std::shared_ptr< core::Tensor > image
CC_FILE_ERROR
Typical I/O filter errors.
Wavefront meshes file I/O filter.
virtual CC_FILE_ERROR saveToFile(ccHObject *entity, const QString &filename, const SaveParameters ¶meters) override
Saves an entity (or a group of) to a file.
Array of 2D texture coordinates.
void addElement(const Type &value)
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 bool addChild(ccHObject *child, int dependencyFlags=DP_PARENT_OF_OTHER, int insertIndex=-1)
Adds a child.
Mesh (triangle) material.
int addMaterial(ccMaterial::CShared mat, bool allowDuplicateNames=false)
Adds a material.
Mesh (triangle) material.
QSharedPointer< ccMaterial > Shared
Shared type.
bool reservePerTriangleMtlIndexes()
Reserves memory to store per-triangle material index.
virtual unsigned size() const override
Returns the number of triangles.
void addTriangleMtlIndex(int mtlIndex)
Adds triangle material index for next triangle.
void setMaterialSet(ccMaterialSet *materialSet, bool autoReleaseOldMaterialSet=true)
Sets associated material set (may be shared)
bool reserve(std::size_t n)
Reserves the memory to store the vertex indexes (3 per triangle)
ccGenericPointCloud * getAssociatedCloud() const override
Returns the vertices cloud.
void addTriangleTexCoordIndexes(int i1, int i2, int i3)
Adds a triplet of tex coords indexes for next triangle.
void addTriangle(unsigned i1, unsigned i2, unsigned i3)
Adds a triangle to the mesh.
void setTexCoordinatesTable(TextureCoordsContainer *texCoordsTable, bool autoReleaseOldTable=true)
Sets per-triangle texture coordinates array (may be shared)
bool reservePerTriangleTexCoordIndexes()
Reserves memory to store per-triangle triplets of tex coords indexes.
ccMesh & ComputeVertexNormals(bool normalized=true)
Function to compute vertex normals, usually called before rendering.
cloudViewer::VerticesIndexes * getTriangleVertIndexes(unsigned triangleIndex) override
Returns the indexes of the vertices of a given triangle.
virtual void setName(const QString &name)
Sets object name.
virtual void setEnabled(bool state)
Sets the "enabled" property.
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
bool hasNormals() const override
Returns whether normals are enabled or not.
ccPointCloud * cloneThis(ccPointCloud *destCloud=nullptr, bool ignoreChildren=false)
Clones this entity.
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 const CCVector3d & getGlobalShift() const
Returns the shift applied to original coordinates.
virtual double getGlobalScale() const
Returns the scale applied to original coordinates.
unsigned size() const override
const CCVector3 * getPoint(unsigned index) const override
Eigen::Matrix3x4d ProjectionMatrix() const
const std::string & Name() 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
MvsTexturing(const Options &options, const Reconstruction &reconstruction, Workspace *workspace, const std::string &image_path)
bool TextureMesh(ccMesh &mesh, const ::cloudViewer::camera::PinholeCameraTrajectory &camera_trajectory, const std::string &output_path)
const Options & GetOptions() const
virtual const DepthMap & GetDepthMap(const int image_idx)
const Model & GetModel() const
bool HasDepthMap(const int image_idx) const
std::vector< unsigned int > face
Matrix< double, 3, 4 > Matrix3x4d
QTextStream & endl(QTextStream &stream)
CLOUDVIEWER_HOST_DEVICE Pair< First, Second > make_pair(const First &_first, const Second &_second)
static const int max_iterations
static const std::string path
MiniVec< float, N > floor(const MiniVec< float, N > &a)
MiniVec< float, N > ceil(const MiniVec< float, N > &a)
static TexturePatchCandidate GenerateCandidate(int label, TextureView *texture_view, const std::vector< size_t > &faces, const ccMesh &mesh, ccPointCloud *cloud, bool mesh_is_shifted=false)
std::string GetPathBaseName(const std::string &path)
void SplitFileExtension(const std::string &path, std::string *root, std::string *ext)
bool ExistsFile(const std::string &path)
std::string JoinPaths(T const &... paths)
std::string StringPrintf(const char *format,...)
const image_t kInvalidImageId
std::string GetParentDir(const std::string &path)
constexpr Rgb black(0, 0, 0)
Eigen::Matrix< Index, 3, 1 > Vector3i
std::string to_string(const T &n)
void swap(cloudViewer::core::SmallVectorImpl< T > &LHS, cloudViewer::core::SmallVectorImpl< T > &RHS)
Implement std::swap in terms of SmallVector swap.
Generic saving parameters.
QWidget * parentWidget
Parent widget (if any)
bool alwaysDisplaySaveDialog
Triangle described by the indexes of its 3 vertices.
int GetImageIdx(const std::string &name) const
bool use_depth_normal_maps
bool use_gradient_magnitude
float max_viewing_angle_deg
std::unique_ptr< TexturePatch > texture_patch
bool IsInside(const TexturePatchCandidate &other) const
TexturePatch(int label, const std::vector< size_t > &faces, const std::vector< Eigen::Vector2f > &texcoords, std::shared_ptr< QImage > image, int min_x, int min_y, int max_x, int max_y)
Eigen::Matrix4f world_to_cam
bool ValidPixel(const Eigen::Vector2f &pixel) const
bool Inside(const Eigen::Vector3f &v1, const Eigen::Vector3f &v2, const Eigen::Vector3f &v3) const
TextureView(std::size_t id, const std::string &image_file, const Eigen::Matrix3f &projection, const Eigen::Matrix4f &world_to_cam, const Eigen::Vector3f &pos, const Eigen::Vector3f &viewdir, int width, int height)
Eigen::Vector2f GetPixelCoords(const Eigen::Vector3f &vertex) const
Eigen::Matrix3f projection
std::shared_ptr< QImage > image_data