mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 02:11:18 +00:00
Port colored OBJ import pipeline from BambuStudio
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <set>
|
||||
|
||||
#include <boost/next_prior.hpp>
|
||||
#include "CgalUtils.hpp"
|
||||
@@ -379,6 +378,413 @@ static bool linear_subdivision(TriMesh& mesh, std::vector<FaceUVArray>& uv_coord
|
||||
return true;
|
||||
}
|
||||
|
||||
using VertexColor = std::array<float, 4>;
|
||||
|
||||
// Quantize continuous per-vertex colors into a small palette of cluster centers.
|
||||
// The legacy OBJ vertex-color import consumed discrete filament ids, so split
|
||||
// decisions could be made by comparing integers. Quantizing up front restores
|
||||
// that property for the adaptive splitter below.
|
||||
static bool quantize_vertex_colors(
|
||||
const std::vector<VertexColor>& vertex_colors,
|
||||
const TextureToColorSettings& settings,
|
||||
AlgoCancelCallback cancel_callback,
|
||||
std::vector<RGB>& out_centers,
|
||||
std::vector<std::size_t>& out_vertex_cluster_ids)
|
||||
{
|
||||
out_centers.clear();
|
||||
out_vertex_cluster_ids.clear();
|
||||
if (vertex_colors.empty())
|
||||
return false;
|
||||
|
||||
std::vector<RGB> vertex_rgb(vertex_colors.size());
|
||||
for (std::size_t i = 0; i < vertex_colors.size(); ++i) {
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
float v = std::clamp(vertex_colors[i][c] * 255.0f, 0.0f, 255.0f);
|
||||
vertex_rgb[i][c] = static_cast<std::size_t>(v);
|
||||
}
|
||||
}
|
||||
|
||||
ClusterParameters para;
|
||||
para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{};
|
||||
if (settings.target_colors_num == 0) {
|
||||
para.max_color_distance = settings.max_color_distance;
|
||||
para.max_cluster_k = settings.max_cluster_k;
|
||||
out_centers = cluster_adaptive(vertex_rgb, para);
|
||||
} else {
|
||||
para.cluster_k = settings.target_colors_num;
|
||||
out_centers = cluster_k_means(vertex_rgb, para);
|
||||
}
|
||||
if (out_centers.empty()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "quantize_vertex_colors: no cluster center generated.";
|
||||
return false;
|
||||
}
|
||||
|
||||
out_vertex_cluster_ids.resize(vertex_rgb.size());
|
||||
for (std::size_t i = 0; i < vertex_rgb.size(); ++i) {
|
||||
std::size_t nearest_id = 0;
|
||||
if (!calc_nearest_color_id(out_centers, vertex_rgb[i], nearest_id))
|
||||
nearest_id = 0;
|
||||
out_vertex_cluster_ids[i] = nearest_id;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(debug) << "quantize_vertex_colors: quantized " << vertex_rgb.size()
|
||||
<< " vertex colors into " << out_centers.size() << " clusters.";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Single-level adaptive subdivision driven by per-vertex cluster ids.
|
||||
//
|
||||
// Reproduces the split topology that the legacy OBJ vertex-color import encoded
|
||||
// into mmu_segmentation_facets (TriangleSelector::perform_split cases 1/2/3), but
|
||||
// materializes it as real geometry. An edge is split at its midpoint if and only
|
||||
// if its two endpoints belong to different clusters. Because that predicate reads
|
||||
// only the shared endpoints, adjacent faces always reach the same conclusion and
|
||||
// no T-junctions can appear.
|
||||
static bool adaptive_split_by_vertex_clusters(
|
||||
TriMesh& mesh,
|
||||
const std::vector<std::size_t>& vertex_cluster_ids,
|
||||
const std::vector<RGB>& cluster_centers,
|
||||
std::vector<RGB>& out_face_colors)
|
||||
{
|
||||
const TriVertices original_vertices = mesh.vertices;
|
||||
const TriFaces original_faces = mesh.indices;
|
||||
if (original_vertices.empty() || original_faces.empty() || cluster_centers.empty())
|
||||
return false;
|
||||
if (vertex_cluster_ids.size() != original_vertices.size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "adaptive_split_by_vertex_clusters: cluster id count ("
|
||||
<< vertex_cluster_ids.size() << ") != vertex count ("
|
||||
<< original_vertices.size() << ").";
|
||||
return false;
|
||||
}
|
||||
if (original_vertices.size() >= (1ULL << 32)) [[unlikely]] {
|
||||
BOOST_LOG_TRIVIAL(warning) << "adaptive_split_by_vertex_clusters: vertex_count="
|
||||
<< original_vertices.size() << " exceeds 32-bit edge_key range.";
|
||||
return false;
|
||||
}
|
||||
|
||||
TriVertices out_vertices = original_vertices;
|
||||
TriFaces out_faces;
|
||||
out_faces.reserve(original_faces.size() * 5);
|
||||
out_face_colors.clear();
|
||||
out_face_colors.reserve(original_faces.size() * 5);
|
||||
|
||||
auto edge_key = [](std::size_t a, std::size_t b) -> uint64_t {
|
||||
return a < b ? ((static_cast<uint64_t>(a) << 32) | b)
|
||||
: ((static_cast<uint64_t>(b) << 32) | a);
|
||||
};
|
||||
std::unordered_map<uint64_t, std::size_t> edge_to_mid;
|
||||
edge_to_mid.reserve(original_faces.size() * 3 / 2);
|
||||
|
||||
// Midpoints on shared edges must be deduplicated so that neighbouring faces
|
||||
// reference the same vertex instead of coincident duplicates.
|
||||
auto midpoint_of_edge = [&](std::size_t a, std::size_t b) -> std::size_t {
|
||||
const uint64_t key = edge_key(a, b);
|
||||
auto it = edge_to_mid.find(key);
|
||||
if (it != edge_to_mid.end())
|
||||
return it->second;
|
||||
const std::size_t idx = out_vertices.size();
|
||||
out_vertices.push_back((original_vertices[a] + original_vertices[b]) * 0.5f);
|
||||
edge_to_mid.emplace(key, idx);
|
||||
return idx;
|
||||
};
|
||||
// Points strictly inside an original face are never shared, so they skip the map.
|
||||
// The midpoint is computed before push_back so a reallocation cannot dangle it.
|
||||
auto append_interior_midpoint = [&](std::size_t a, std::size_t b) -> std::size_t {
|
||||
const TriVertex mid = (out_vertices[a] + out_vertices[b]) * 0.5f;
|
||||
const std::size_t idx = out_vertices.size();
|
||||
out_vertices.push_back(mid);
|
||||
return idx;
|
||||
};
|
||||
auto emit = [&](std::size_t a, std::size_t b, std::size_t c, std::size_t cluster_id) {
|
||||
out_faces.push_back(Vec3i32(static_cast<int>(a), static_cast<int>(b), static_cast<int>(c)));
|
||||
out_face_colors.push_back(cluster_centers[cluster_id]);
|
||||
};
|
||||
|
||||
for (const auto& f : original_faces) {
|
||||
const std::size_t v[3] = {static_cast<std::size_t>(f[0]), static_cast<std::size_t>(f[1]), static_cast<std::size_t>(f[2])};
|
||||
const std::size_t c[3] = {vertex_cluster_ids[v[0]], vertex_cluster_ids[v[1]], vertex_cluster_ids[v[2]]};
|
||||
|
||||
// Case A: uniform cluster, keep the face untouched.
|
||||
if (c[0] == c[1] && c[1] == c[2]) {
|
||||
emit(v[0], v[1], v[2], c[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case B: two vertices share a cluster and the third is isolated. Split the
|
||||
// two edges incident to the isolated vertex, which are exactly the
|
||||
// cross-cluster ones; the opposite edge stays intact.
|
||||
int iso = -1;
|
||||
if (c[1] == c[2]) iso = 0;
|
||||
else if (c[2] == c[0]) iso = 1;
|
||||
else if (c[0] == c[1]) iso = 2;
|
||||
if (iso >= 0) {
|
||||
const int i = iso, j = (iso + 1) % 3, k = (iso + 2) % 3;
|
||||
const std::size_t m_ij = midpoint_of_edge(v[i], v[j]);
|
||||
const std::size_t m_ki = midpoint_of_edge(v[k], v[i]);
|
||||
emit(v[i], m_ij, m_ki, c[i]);
|
||||
emit(m_ij, v[j], m_ki, c[j]);
|
||||
emit(v[j], v[k], m_ki, c[j]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case C: all three clusters differ. Split every edge, then cut the centre
|
||||
// triangle once more. The centre is equidistant from all three clusters, so
|
||||
// the legacy heuristic selects the cut by widest interior angle, which is
|
||||
// the vertex opposite the longest edge.
|
||||
const std::size_t m01 = midpoint_of_edge(v[0], v[1]);
|
||||
const std::size_t m12 = midpoint_of_edge(v[1], v[2]);
|
||||
const std::size_t m20 = midpoint_of_edge(v[2], v[0]);
|
||||
emit(v[0], m01, m20, c[0]);
|
||||
emit(m01, v[1], m12, c[1]);
|
||||
emit(m12, v[2], m20, c[2]);
|
||||
|
||||
const TriVertex& p0 = original_vertices[v[0]];
|
||||
const TriVertex& p1 = original_vertices[v[1]];
|
||||
const TriVertex& p2 = original_vertices[v[2]];
|
||||
const float sq_opposite_v0 = (p2 - p1).squaredNorm();
|
||||
const float sq_opposite_v1 = (p0 - p2).squaredNorm();
|
||||
const float sq_opposite_v2 = (p1 - p0).squaredNorm();
|
||||
int widest = 0;
|
||||
float widest_len = sq_opposite_v0;
|
||||
if (sq_opposite_v1 > widest_len) { widest = 1; widest_len = sq_opposite_v1; }
|
||||
if (sq_opposite_v2 > widest_len) { widest = 2; }
|
||||
|
||||
if (widest == 0) {
|
||||
const std::size_t mc = append_interior_midpoint(m20, m01);
|
||||
emit(m12, m20, mc, c[1]);
|
||||
emit(mc, m01, m12, c[2]);
|
||||
} else if (widest == 1) {
|
||||
const std::size_t mc = append_interior_midpoint(m01, m12);
|
||||
emit(m20, m01, mc, c[0]);
|
||||
emit(mc, m12, m20, c[2]);
|
||||
} else {
|
||||
const std::size_t mc = append_interior_midpoint(m12, m20);
|
||||
emit(m01, m12, mc, c[1]);
|
||||
emit(mc, m20, m01, c[0]);
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "adaptive_split_by_vertex_clusters: faces " << original_faces.size()
|
||||
<< " -> " << out_faces.size() << ", vertices " << original_vertices.size()
|
||||
<< " -> " << out_vertices.size();
|
||||
mesh = TriMesh(out_faces, out_vertices);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Shared pipeline: mesh repair -> color clustering -> label assignment -> smoothing.
|
||||
// Called by both TextureToColor (after UV sampling) and ClusterAndSmooth (after vertex-color oversample).
|
||||
// progress_callback reports 0~100 within this function; the caller maps it to its own global range.
|
||||
static bool repair_cluster_smooth(
|
||||
TriMesh& mesh,
|
||||
std::vector<RGB>& face_colors,
|
||||
std::vector<RGB>& out_clustered_face_colors,
|
||||
const TextureToColorSettings& settings,
|
||||
AlgoProgressCallback progress_callback,
|
||||
AlgoCancelCallback cancel_callback,
|
||||
const char* log_prefix)
|
||||
{
|
||||
auto report = [&](int pct, const char* msg) {
|
||||
if (progress_callback)
|
||||
progress_callback({pct, msg});
|
||||
};
|
||||
auto cancelled = [&]() -> bool {
|
||||
if (cancel_callback && cancel_callback()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << " cancelled";
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
report(0, "Repairing mesh");
|
||||
if (cancelled()) return false;
|
||||
|
||||
// Resample face colors onto a repaired mesh via centroid nearest-neighbor.
|
||||
auto resample_face_colors = [&](TriMesh&& repaired_mesh) -> bool {
|
||||
TriVertices old_vertices = std::move(mesh.vertices);
|
||||
TriFaces old_indices = std::move(mesh.indices);
|
||||
auto aabb_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices);
|
||||
mesh = std::move(repaired_mesh);
|
||||
|
||||
if (is_closed(mesh)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << ": repaired mesh is closed.";
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << ": repaired mesh is open.";
|
||||
}
|
||||
|
||||
std::vector<RGB> new_face_colors(mesh.facets_count());
|
||||
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, mesh.facets_count()), [&](const tbb::blocked_range<size_t>& range) {
|
||||
for (std::size_t fid = range.begin(); fid < range.end(); ++fid) {
|
||||
const auto& face = mesh.indices[fid];
|
||||
Vec3f center = (mesh.vertices[face[0]] + mesh.vertices[face[1]] + mesh.vertices[face[2]]) / 3.0f;
|
||||
size_t hit_idx = 0;
|
||||
Vec3f closest;
|
||||
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
|
||||
old_vertices, old_indices, aabb_tree, center, hit_idx, closest);
|
||||
new_face_colors[fid] = face_colors[hit_idx];
|
||||
}
|
||||
});
|
||||
face_colors = std::move(new_face_colors);
|
||||
return true;
|
||||
};
|
||||
|
||||
auto repair_and_resample = [&]() -> bool {
|
||||
std::shared_ptr<TriMesh> repaired_mesh;
|
||||
if (!RepairMesh(mesh, repaired_mesh)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << ": RepairMesh failed.";
|
||||
return false;
|
||||
}
|
||||
if (cancelled()) return false;
|
||||
return resample_face_colors(std::move(*repaired_mesh));
|
||||
};
|
||||
|
||||
{
|
||||
TriangleMesh stats_mesh(static_cast<const indexed_triangle_set&>(mesh));
|
||||
const auto& stats = stats_mesh.stats();
|
||||
// Orca's TriangleMeshStats defines manifold() as open_edges == 0 and does not track
|
||||
// non-manifold edges/vertices separately, so BBS's "!manifold() || has_open_edges()"
|
||||
// collapses to this single test and the extra counters drop out of the log.
|
||||
if (!stats.manifold()) {
|
||||
BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh has non-manifold geometry or open boundaries, open_edges="
|
||||
<< stats.open_edges;
|
||||
if (settings.mesh_repair_decision == MeshRepairDecision::Ask) {
|
||||
if (settings.mesh_repair_decision_required)
|
||||
*settings.mesh_repair_decision_required = true;
|
||||
return false;
|
||||
}
|
||||
if (settings.mesh_repair_decision == MeshRepairDecision::RepairAndImport) {
|
||||
indexed_triangle_set repaired_its;
|
||||
std::string repair_error;
|
||||
bool repaired = settings.mesh_repair_callback && settings.mesh_repair_callback(
|
||||
static_cast<const indexed_triangle_set&>(mesh), repaired_its,
|
||||
[&](const char* message, unsigned /*percent*/) {
|
||||
report(5, message ? message : "Repairing mesh");
|
||||
},
|
||||
[&]() { return cancelled(); }, &repair_error);
|
||||
if (repaired) {
|
||||
if (cancelled()) return false;
|
||||
BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished.";
|
||||
if (!resample_face_colors(TriMesh(std::move(repaired_its))))
|
||||
return false;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error;
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cgalutils::is_mesh_halfedge_compatible(mesh)) {
|
||||
BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh not halfedge-compatible, attempting RepairMesh.";
|
||||
if (!repair_and_resample())
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef OUTPUT_TEST_RESULT
|
||||
SaveToOFF(std::string(log_prefix) + "_1_repair.off", mesh, face_colors);
|
||||
#endif
|
||||
|
||||
report(20, "Color clustering");
|
||||
if (cancelled()) return false;
|
||||
|
||||
// Clustering
|
||||
std::vector<RGB> cluster_centers;
|
||||
out_clustered_face_colors = face_colors;
|
||||
std::vector<std::size_t> clustered_face_labels(face_colors.size());
|
||||
const bool adaptive_cluster = settings.target_colors_num == 0;
|
||||
|
||||
if (adaptive_cluster) {
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << ": use cluster adaptive method.";
|
||||
ClusterParameters para;
|
||||
para.max_color_distance = settings.max_color_distance;
|
||||
para.max_cluster_k = settings.max_cluster_k;
|
||||
para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{};
|
||||
cluster_centers = cluster_adaptive(face_colors, para);
|
||||
if (cancelled()) return false;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << ": use cluster k-means method.";
|
||||
ClusterParameters para;
|
||||
para.cluster_k = settings.target_colors_num;
|
||||
para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{};
|
||||
cluster_centers = cluster_k_means(face_colors, para);
|
||||
if (cancelled()) return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << ": k = " << cluster_centers.size() << ".";
|
||||
if (cluster_centers.empty()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << ": no cluster center generated.";
|
||||
return false;
|
||||
}
|
||||
|
||||
report(40, "Assigning cluster labels");
|
||||
if (cancelled()) return false;
|
||||
|
||||
// Assign each face to nearest cluster center
|
||||
{
|
||||
std::atomic<size_t> done{0};
|
||||
std::atomic<bool> cancel_requested{false};
|
||||
const size_t total = mesh.indices.size();
|
||||
const size_t interval = std::max<size_t>(total / 20, 1);
|
||||
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, total), [&](const tbb::blocked_range<size_t>& range) {
|
||||
for (std::size_t fid = range.begin(); fid < range.end(); ++fid) {
|
||||
if (cancel_requested.load(std::memory_order_relaxed)) return;
|
||||
std::size_t nearest_id = 0;
|
||||
calc_nearest_color_id(cluster_centers, face_colors[fid], nearest_id);
|
||||
clustered_face_labels[fid] = nearest_id;
|
||||
out_clustered_face_colors[fid] = cluster_centers[nearest_id];
|
||||
size_t cnt = done.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
if (cnt % interval == 0) {
|
||||
if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; }
|
||||
}
|
||||
}
|
||||
});
|
||||
if (cancel_requested.load() || cancelled()) return false;
|
||||
}
|
||||
if (adaptive_cluster) {
|
||||
if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "cluster assignment"))
|
||||
return false;
|
||||
} else {
|
||||
ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "cluster assignment");
|
||||
}
|
||||
|
||||
#ifdef OUTPUT_TEST_RESULT
|
||||
{
|
||||
std::vector<RGB> tmp = out_clustered_face_colors;
|
||||
for (std::size_t i = 0; i < tmp.size(); ++i)
|
||||
tmp[i] = cluster_centers[clustered_face_labels[i]];
|
||||
SaveToOFF(std::string(log_prefix) + "_3_cluster.off", mesh, tmp);
|
||||
}
|
||||
#endif
|
||||
|
||||
report(65, "Smoothing colors");
|
||||
if (cancelled()) return false;
|
||||
|
||||
SmoothParameters smooth_parameters;
|
||||
smooth_parameters.smooth_weight = settings.smooth_weight;
|
||||
if (!smooth_region(mesh, clustered_face_labels, smooth_parameters)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << log_prefix << ": smooth region failed.";
|
||||
return false;
|
||||
}
|
||||
if (adaptive_cluster) {
|
||||
if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "color smoothing"))
|
||||
return false;
|
||||
} else {
|
||||
ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "color smoothing");
|
||||
}
|
||||
|
||||
report(90, "Updating face colors");
|
||||
if (cancelled()) return false;
|
||||
|
||||
for (std::size_t i = 0; i < out_clustered_face_colors.size(); ++i)
|
||||
out_clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]];
|
||||
|
||||
#ifdef OUTPUT_TEST_RESULT
|
||||
SaveToOFF(std::string(log_prefix) + "_4_smooth.off", mesh, out_clustered_face_colors);
|
||||
#endif
|
||||
|
||||
report(100, "Completed");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<Vec2f>>& texture_mesh_uv_coords, const cv::Mat& texture, TriMesh& color_mesh,
|
||||
std::vector<std::array<std::size_t, 3>>& face_colors, const TextureToColorSettings& settings, AlgoProgressCallback progress_callback,
|
||||
AlgoCancelCallback cancel_callback) {
|
||||
@@ -525,259 +931,22 @@ bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<V
|
||||
SaveToOFF("texture_to_color_0_initialize.off", color_mesh, face_colors);
|
||||
#endif
|
||||
|
||||
report(40, "Repairing mesh");
|
||||
if (cancelled()) {
|
||||
// Map progress from repair_cluster_smooth's [0,100] to TextureToColor's [40,100]
|
||||
AlgoProgressCallback rcs_progress = nullptr;
|
||||
if (progress_callback) {
|
||||
rcs_progress = [&](AlgoProgress p) {
|
||||
int mapped_pct = 40 + p.percent * 60 / 100;
|
||||
progress_callback({mapped_pct, p.message});
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<RGB> clustered_face_colors;
|
||||
if (!repair_cluster_smooth(color_mesh, face_colors, clustered_face_colors,
|
||||
settings, rcs_progress, cancel_callback, "TextureToColor"))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sub-stage timing helper for the "Repairing mesh" outer lap. Logs each
|
||||
// sub-phase under a [timing][Repairing mesh] prefix so that regressions in
|
||||
// mesh inspection, RepairMesh, AABB resampling, etc. can be attributed
|
||||
// to a specific sub-stage without changing the outer lap structure.
|
||||
auto sub_lap = [&](const char* sub_name, Clock::time_point t0) {
|
||||
double ms = std::chrono::duration<double, std::milli>(Clock::now() - t0).count();
|
||||
BOOST_LOG_TRIVIAL(debug) << "[timing][Repairing mesh] " << sub_name << ": " << ms << "ms";
|
||||
};
|
||||
|
||||
// Step 3: Repair mesh
|
||||
// Many textured models have non-manifold, non-closed, or other issues that need to be fixed beforehand
|
||||
auto resample_repaired_mesh = [&](TriMesh&& repaired_mesh) -> bool {
|
||||
// AABBTreeIndirect references vertices/faces externally, so snapshot the
|
||||
// pre-repair geometry by moving them out of color_mesh before it gets
|
||||
// overwritten with the repaired mesh below. std::move on std::vector is
|
||||
// O(1) (pointer adoption), no element copy.
|
||||
const auto t_aabb = Clock::now();
|
||||
TriVertices old_vertices = std::move(color_mesh.vertices);
|
||||
TriFaces old_indices = std::move(color_mesh.indices);
|
||||
auto before_repair_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(old_vertices, old_indices);
|
||||
sub_lap("resample.aabb_build", t_aabb);
|
||||
|
||||
color_mesh = std::move(repaired_mesh);
|
||||
|
||||
const auto t_is_closed = Clock::now();
|
||||
if (is_closed(color_mesh)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repaired mesh is closed.";
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repaired mesh is open.";
|
||||
}
|
||||
sub_lap("resample.is_closed", t_is_closed);
|
||||
|
||||
// New faces after repair inherit old face colors via centroid nearest-neighbor lookup.
|
||||
// Since the mesh barely changes after repair, resampling via centroid nearest-neighbor is sufficient.
|
||||
const auto t_resample = Clock::now();
|
||||
std::vector<RGB> new_face_colors(color_mesh.facets_count());
|
||||
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, color_mesh.facets_count()), [&](const tbb::blocked_range<size_t>& range) {
|
||||
for (std::size_t fid = range.begin(); fid < range.end(); ++fid) {
|
||||
const auto& face = color_mesh.indices[fid];
|
||||
Vec3f center = (color_mesh.vertices[face[0]] + color_mesh.vertices[face[1]] + color_mesh.vertices[face[2]]) / 3.0f;
|
||||
size_t hit_idx = 0;
|
||||
Vec3f closest;
|
||||
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
|
||||
old_vertices, old_indices, before_repair_tree, center, hit_idx, closest);
|
||||
new_face_colors[fid] = face_colors[hit_idx];
|
||||
}
|
||||
});
|
||||
face_colors = std::move(new_face_colors);
|
||||
sub_lap("resample.parallel_nearest", t_resample);
|
||||
return true;
|
||||
};
|
||||
|
||||
auto repair_and_resample_mesh = [&]() -> bool {
|
||||
std::shared_ptr<TriMesh> repaired_mesh;
|
||||
const auto t_repair = Clock::now();
|
||||
bool success = RepairMesh(color_mesh, repaired_mesh);
|
||||
sub_lap("RepairMesh", t_repair);
|
||||
if (success == false) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repair mesh failed.";
|
||||
return false;
|
||||
}
|
||||
if (cancelled()) return false;
|
||||
return resample_repaired_mesh(std::move(*repaired_mesh));
|
||||
};
|
||||
|
||||
{
|
||||
const auto t_stats = Clock::now();
|
||||
TriangleMesh stats_mesh(static_cast<const indexed_triangle_set&>(color_mesh));
|
||||
const auto& stats = stats_mesh.stats();
|
||||
sub_lap("stats_check", t_stats);
|
||||
// Orca's TriangleMeshStats defines manifold() as open_edges == 0 and does not track
|
||||
// non-manifold edges/vertices separately, so BBS's "!manifold() || has_open_edges()"
|
||||
// collapses to this single test and the extra counters drop out of the log.
|
||||
if (!stats.manifold()) {
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: mesh has non-manifold geometry or open boundaries, open_edges="
|
||||
<< stats.open_edges;
|
||||
if (settings.mesh_repair_decision == MeshRepairDecision::Ask) {
|
||||
if (settings.mesh_repair_decision_required)
|
||||
*settings.mesh_repair_decision_required = true;
|
||||
return false;
|
||||
}
|
||||
if (settings.mesh_repair_decision == MeshRepairDecision::RepairAndImport) {
|
||||
indexed_triangle_set repaired_its;
|
||||
std::string repair_error;
|
||||
const auto t_win3d = Clock::now();
|
||||
bool repaired = settings.mesh_repair_callback && settings.mesh_repair_callback(static_cast<const indexed_triangle_set&>(color_mesh), repaired_its,
|
||||
[&](const char* message, unsigned percent) {
|
||||
sub_report(static_cast<int>(percent), 40, 60, message ? message : "Repairing mesh");
|
||||
},
|
||||
[&]() { return cancelled(); }, &repair_error);
|
||||
sub_lap("windows_3d_repair", t_win3d);
|
||||
if (repaired) {
|
||||
if (cancelled()) return false;
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: Windows 3D mesh repair finished.";
|
||||
if (!resample_repaired_mesh(TriMesh(std::move(repaired_its))))
|
||||
return false;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: Windows 3D mesh repair failed: " << repair_error;
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureToColor: importing mesh without Windows 3D repair.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto t_halfedge = Clock::now();
|
||||
const bool halfedge_ok = cgalutils::is_mesh_halfedge_compatible(color_mesh);
|
||||
sub_lap("is_mesh_halfedge_compatible", t_halfedge);
|
||||
if (!halfedge_ok && repair_and_resample_mesh() == false) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: repair and resample mesh failed.";
|
||||
return false;
|
||||
}
|
||||
lap("Repairing mesh");
|
||||
#ifdef OUTPUT_TEST_RESULT
|
||||
SaveToOFF("texture_to_color_1_repair.off", color_mesh, face_colors);
|
||||
#endif
|
||||
|
||||
report(65, "Color clustering");
|
||||
if (cancelled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 5: Color clustering
|
||||
std::vector<RGB> cluster_centers;
|
||||
std::vector<RGB> clustered_face_colors = face_colors;
|
||||
std::vector<std::size_t> clustered_face_labels(face_colors.size());
|
||||
const bool adaptive_cluster = settings.target_colors_num == 0;
|
||||
|
||||
// Compute cluster centers
|
||||
if (adaptive_cluster) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: use cluster adaptive method.";
|
||||
ClusterParameters para;
|
||||
para.max_color_distance = settings.max_color_distance;
|
||||
para.max_cluster_k = settings.max_cluster_k;
|
||||
para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{};
|
||||
cluster_centers = cluster_adaptive(face_colors, para);
|
||||
if (cancelled()) return false;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: use cluster k-means method.";
|
||||
ClusterParameters para;
|
||||
para.cluster_k = settings.target_colors_num;
|
||||
para.cancel_callback = cancel_callback ? [&]() { return cancel_callback(); } : std::function<bool()>{};
|
||||
cluster_centers = cluster_k_means(face_colors, para);
|
||||
if (cancelled()) return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: the k is " << cluster_centers.size() << ".";
|
||||
if (cluster_centers.empty()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: no cluster center generated.";
|
||||
return false;
|
||||
}
|
||||
const std::set<RGB> unique_cluster_centers(cluster_centers.begin(), cluster_centers.end());
|
||||
if (unique_cluster_centers.size() != cluster_centers.size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: cluster centers contain duplicated RGB values, unique exported colors may be fewer than centers.";
|
||||
}
|
||||
|
||||
report(70, "Assigning cluster labels");
|
||||
if (cancelled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assign each face's color to the nearest cluster center
|
||||
constexpr bool use_simple_cluster = true; // Complex algorithm is still being optimized; use simple assignment for now
|
||||
if (use_simple_cluster) {
|
||||
std::atomic<size_t> done_cluster{0};
|
||||
std::atomic<bool> cancel_requested{false};
|
||||
const size_t total_cluster = color_mesh.indices.size();
|
||||
const size_t cluster_interval = std::max<size_t>(total_cluster / 20, 1);
|
||||
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, total_cluster), [&](const tbb::blocked_range<size_t>& range) {
|
||||
for (std::size_t fid = range.begin(); fid < range.end(); ++fid) {
|
||||
if (cancel_requested.load(std::memory_order_relaxed)) return;
|
||||
auto& face_color = face_colors[fid];
|
||||
auto nearest_color_id = std::numeric_limits<std::size_t>::max();
|
||||
bool success = calc_nearest_color_id(cluster_centers, face_color, nearest_color_id);
|
||||
if (success == false) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: calc nearest color id failed.";
|
||||
continue;
|
||||
}
|
||||
clustered_face_labels[fid] = nearest_color_id;
|
||||
clustered_face_colors[fid] = cluster_centers[nearest_color_id];
|
||||
size_t cnt = done_cluster.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
if (cnt % cluster_interval == 0) {
|
||||
if (cancelled()) { cancel_requested.store(true, std::memory_order_relaxed); return; }
|
||||
sub_report(static_cast<int>(cnt * 100 / total_cluster), 70, 85, "Assigning cluster labels");
|
||||
}
|
||||
}
|
||||
});
|
||||
if (cancel_requested.load() || cancelled()) return false;
|
||||
} else {
|
||||
bool success = mesh_cluster(color_mesh, cluster_centers, clustered_face_colors, clustered_face_labels);
|
||||
if (success == false) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: mesh cluster failed.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (adaptive_cluster) {
|
||||
if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "cluster assignment")) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "cluster assignment");
|
||||
}
|
||||
lap("Color clustering & labeling");
|
||||
#ifdef OUTPUT_TEST_RESULT
|
||||
for (std::size_t i = 0; i < clustered_face_colors.size(); ++i) {
|
||||
clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]];
|
||||
}
|
||||
SaveToOFF("texture_to_color_3_cluster.off", color_mesh, clustered_face_colors);
|
||||
#endif
|
||||
|
||||
report(85, "Smoothing colors");
|
||||
if (cancelled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 6: Post-process colors
|
||||
SmoothParameters smooth_parameters;
|
||||
smooth_parameters.smooth_weight = settings.smooth_weight;
|
||||
if (!smooth_region(color_mesh, clustered_face_labels, smooth_parameters)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: smooth region failed.";
|
||||
return false;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(debug) << "TextureToColor: smooth region success.";
|
||||
if (adaptive_cluster) {
|
||||
if (!discard_unused_cluster_centers(cluster_centers, clustered_face_labels, "color smoothing")) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
ensure_all_cluster_centers_used(face_colors, cluster_centers, clustered_face_labels, "color smoothing");
|
||||
}
|
||||
report(95, "Updating face colors");
|
||||
if (cancelled()) {
|
||||
return false;
|
||||
}
|
||||
for (std::size_t i = 0; i < clustered_face_colors.size(); ++i) {
|
||||
clustered_face_colors[i] = cluster_centers[clustered_face_labels[i]];
|
||||
}
|
||||
const std::set<RGB> unique_exported_colors(clustered_face_colors.begin(), clustered_face_colors.end());
|
||||
if (unique_exported_colors.size() < cluster_centers.size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "TextureToColor: final exported unique colors (" << unique_exported_colors.size()
|
||||
<< ") are fewer than cluster centers (" << cluster_centers.size()
|
||||
<< "), likely due to duplicate centers or unsatisfied seed assignment.";
|
||||
}
|
||||
#ifdef OUTPUT_TEST_RESULT
|
||||
SaveToOFF("texture_to_color_4_smooth.off", color_mesh, clustered_face_colors);
|
||||
#endif
|
||||
|
||||
face_colors = std::move(clustered_face_colors);
|
||||
lap("Smoothing colors");
|
||||
lap("Repair + Clustering + Smoothing");
|
||||
double total_ms = std::chrono::duration<double, std::milli>(Clock::now() - t_total_start).count();
|
||||
BOOST_LOG_TRIVIAL(debug) << "[timing] TextureToColor total: " << total_ms << "ms"
|
||||
<< " faces=" << color_mesh.facets_count();
|
||||
@@ -785,5 +954,91 @@ bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<V
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClusterAndSmooth(const TriMesh& mesh,
|
||||
const std::vector<std::array<std::size_t, 3>>& input_face_colors,
|
||||
TriMesh& out_mesh,
|
||||
std::vector<std::array<std::size_t, 3>>& out_face_colors,
|
||||
const TextureToColorSettings& settings,
|
||||
AlgoProgressCallback progress_callback,
|
||||
AlgoCancelCallback cancel_callback,
|
||||
const std::vector<std::array<float, 4>>& vertex_colors)
|
||||
{
|
||||
auto report = [&](int pct, const char* msg) {
|
||||
if (progress_callback)
|
||||
progress_callback({pct, msg});
|
||||
};
|
||||
auto cancelled = [&]() -> bool {
|
||||
if (cancel_callback && cancel_callback()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " cancelled";
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
out_mesh = mesh;
|
||||
out_face_colors.clear();
|
||||
|
||||
if (mesh.indices.empty() || input_face_colors.empty()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: empty mesh or face colors.";
|
||||
return false;
|
||||
}
|
||||
if (input_face_colors.size() != mesh.indices.size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "ClusterAndSmooth: face_colors size ("
|
||||
<< input_face_colors.size() << ") != indices size ("
|
||||
<< mesh.indices.size() << "), clamping.";
|
||||
}
|
||||
|
||||
report(0, "Initializing");
|
||||
if (cancelled()) return false;
|
||||
|
||||
// Prepare face colors aligned to mesh size
|
||||
std::vector<RGB> face_colors(out_mesh.indices.size());
|
||||
for (size_t i = 0; i < out_mesh.indices.size(); ++i) {
|
||||
if (i < input_face_colors.size())
|
||||
face_colors[i] = input_face_colors[i];
|
||||
else
|
||||
face_colors[i] = {128, 128, 128};
|
||||
}
|
||||
|
||||
// Low-poly vertex-color meshes take the legacy OBJ import route: quantize the
|
||||
// vertex colors, then split only across cluster boundaries. Colors are exact
|
||||
// cluster centers afterwards, so repair / re-clustering / smoothing are skipped
|
||||
// to match the legacy behaviour, which never touched the mesh either.
|
||||
// A vertex color count that disagrees with the mesh falls through to the generic
|
||||
// pipeline below rather than failing the import outright.
|
||||
if (!vertex_colors.empty() &&
|
||||
vertex_colors.size() == out_mesh.vertices.size() &&
|
||||
out_mesh.facets_count() < settings.oversampling_min_face_count) {
|
||||
report(10, "Quantizing vertex colors");
|
||||
std::vector<RGB> cluster_centers;
|
||||
std::vector<std::size_t> vertex_cluster_ids;
|
||||
if (!quantize_vertex_colors(vertex_colors, settings, cancel_callback, cluster_centers, vertex_cluster_ids)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: vertex color quantization failed.";
|
||||
return false;
|
||||
}
|
||||
if (cancelled()) return false;
|
||||
|
||||
report(50, "Splitting color boundaries");
|
||||
if (!adaptive_split_by_vertex_clusters(out_mesh, vertex_cluster_ids, cluster_centers, face_colors)) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "ClusterAndSmooth: adaptive vertex-color split failed.";
|
||||
return false;
|
||||
}
|
||||
if (cancelled()) return false;
|
||||
|
||||
out_face_colors = std::move(face_colors);
|
||||
report(100, "Completed");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<RGB> clustered_face_colors;
|
||||
if (!repair_cluster_smooth(out_mesh, face_colors, clustered_face_colors,
|
||||
settings, progress_callback, cancel_callback,
|
||||
"ClusterAndSmooth"))
|
||||
return false;
|
||||
|
||||
out_face_colors = std::move(clustered_face_colors);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace tex2color
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -61,5 +61,43 @@ bool TextureToColor(const TriMesh& texture_mesh, const std::vector<std::vector<V
|
||||
std::vector<std::array<std::size_t, 3>>& face_colors, const TextureToColorSettings& settings = TextureToColorSettings(),
|
||||
AlgoProgressCallback progress_callback = nullptr, AlgoCancelCallback cancel_callback = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Turn pre-computed per-face colors into a clustered color mesh (no texture/UV).
|
||||
*
|
||||
* Used for OBJ vertex colors and MTL face colors, which bypass texture sampling.
|
||||
* Two routes are possible:
|
||||
* - Low-poly meshes carrying per-vertex colors: the vertex colors are quantized
|
||||
* into a small palette and the mesh is geometrically split along cluster
|
||||
* boundaries, reproducing the split topology of the legacy OBJ vertex-color
|
||||
* import. Output colors are then exact cluster centers, so mesh repair,
|
||||
* re-clustering and smoothing are skipped.
|
||||
* - Everything else: mesh repair, color clustering (K-Means or adaptive) and
|
||||
* region smoothing, sharing the same pipeline as TextureToColor.
|
||||
*
|
||||
* @param[in] mesh Input triangle mesh
|
||||
* @param[in] input_face_colors Pre-computed per-face RGB colors [0..255]
|
||||
* @param[out] out_mesh Output mesh. Geometry is subdivided on the
|
||||
* vertex-color route, and may still be replaced
|
||||
* by mesh repair on the generic route.
|
||||
* @param[out] out_face_colors Output per-face colors, one entry per out_mesh face
|
||||
* @param[in] settings Algorithm parameters (target_colors_num, smooth_weight;
|
||||
* oversampling_min_face_count doubles as the low-poly
|
||||
* threshold for the vertex-color route)
|
||||
* @param[in] progress_callback Progress callback
|
||||
* @param[in] cancel_callback Cancel callback
|
||||
* @param[in] vertex_colors Optional per-vertex RGBA [0..1]. Must match
|
||||
* mesh.vertices in size to enable the vertex-color
|
||||
* route; otherwise it is ignored.
|
||||
* @return true on success, false on failure or cancellation
|
||||
*/
|
||||
bool ClusterAndSmooth(const TriMesh& mesh,
|
||||
const std::vector<std::array<std::size_t, 3>>& input_face_colors,
|
||||
TriMesh& out_mesh,
|
||||
std::vector<std::array<std::size_t, 3>>& out_face_colors,
|
||||
const TextureToColorSettings& settings = TextureToColorSettings(),
|
||||
AlgoProgressCallback progress_callback = nullptr,
|
||||
AlgoCancelCallback cancel_callback = nullptr,
|
||||
const std::vector<std::array<float, 4>>& vertex_colors = {});
|
||||
|
||||
} // namespace tex2color
|
||||
} // namespace Slic3r
|
||||
|
||||
Reference in New Issue
Block a user