CPPxDIC
C++ Digital Image Correlation library for fingertip 3D reconstruction
Loading...
Searching...
No Matches
surface_stitching.cpp
Go to the documentation of this file.
1
6#include "dic_structures.h"
7#include "surface_stitching.h"
8#include "logging.h"
9#include <vector>
10#include <algorithm>
11#include <cmath>
12#include <limits>
13#include <iostream>
14#include <map>
15#include <set>
16#include <unordered_map>
17#include <Eigen/Dense>
18
19namespace cppxdic {
20
25DIC3Dcombined stitchPairsSimple(const std::vector<DIC3DpairResults>& all_pairs) {
26 if (all_pairs.empty()) {
27 return DIC3Dcombined();
28 }
29
30 if (all_pairs.size() == 1) {
31 // Single pair - convert to DIC3Dcombined with pair indices set
32 DIC3Dcombined result;
33 const auto& pair = all_pairs[0];
34
35 result.Points3D = pair.Points3D;
36 result.Faces = pair.Faces;
37 result.FaceColors = pair.FaceColors;
38 result.corrComb = pair.corrComb;
39 result.FaceCorrComb = pair.FaceCorrComb;
40 result.FaceCentroids = pair.FaceCentroids;
41 result.Disp = pair.Disp;
42
43 size_t nFaces = result.Faces.size() / 3;
44 size_t nPoints = result.Points3D.empty() ? 0 : result.Points3D[0].x.size();
45 result.FacePairInds.assign(nFaces, 1);
46 result.PointPairInds.assign(nPoints, 1);
47
48 // Set pairIndices from cameraPairInd
49 if (pair.cameraPairInd.size() >= 2) {
50 result.pairIndices = pair.cameraPairInd;
51 }
52
53 return result;
54 }
55
56 // Multi-pair: append all pairs together
57 DIC3Dcombined stitched;
58 size_t nFrames = all_pairs[0].Points3D.size();
59
60 // Initialize with first pair
61 const auto& first_pair = all_pairs[0];
62 stitched.Points3D = first_pair.Points3D;
63 stitched.Faces = first_pair.Faces;
64 stitched.FaceColors = first_pair.FaceColors;
65 stitched.corrComb = first_pair.corrComb;
66 stitched.FaceCorrComb = first_pair.FaceCorrComb;
67 stitched.FaceCentroids = first_pair.FaceCentroids;
68 stitched.Disp = first_pair.Disp;
69
70 size_t nFaces = stitched.Faces.size() / 3;
71 size_t nPoints = stitched.Points3D[0].x.size();
72 stitched.FacePairInds.assign(nFaces, 1); // Pair 1
73 stitched.PointPairInds.assign(nPoints, 1);
74
75 // Initialize pairIndices matrix (nPairs x 2)
76 stitched.pairIndices.resize(all_pairs.size() * 2);
77 if (first_pair.cameraPairInd.size() >= 2) {
78 stitched.pairIndices[0] = first_pair.cameraPairInd[0];
79 stitched.pairIndices[1] = first_pair.cameraPairInd[1];
80 }
81
82 LOG_INFO << " Pair 1: " << nPoints << " points, " << nFaces << " faces";
83
84 // Append subsequent pairs
85 for (size_t ipair = 1; ipair < all_pairs.size(); ++ipair) {
86 const auto& pair = all_pairs[ipair];
87 size_t vertex_offset = stitched.Points3D[0].x.size();
88
89 // Append faces with adjusted indices
90 size_t pair_nFaces = pair.Faces.size() / 3;
91 for (size_t i = 0; i < pair.Faces.size(); ++i) {
92 stitched.Faces.push_back(pair.Faces[i] + static_cast<int>(vertex_offset));
93 }
94
95 // Append face colors
96 stitched.FaceColors.insert(
97 stitched.FaceColors.end(),
98 pair.FaceColors.begin(),
99 pair.FaceColors.end()
100 );
101
102 // Append face pair indices (1-indexed)
103 stitched.FacePairInds.insert(
104 stitched.FacePairInds.end(),
105 pair_nFaces,
106 static_cast<int>(ipair + 1)
107 );
108
109 // Append points for all frames
110 size_t pair_nPoints = pair.Points3D[0].x.size();
111 for (size_t frame = 0; frame < nFrames; ++frame) {
112 // Append Points3D
113 stitched.Points3D[frame].x.insert(
114 stitched.Points3D[frame].x.end(),
115 pair.Points3D[frame].x.begin(),
116 pair.Points3D[frame].x.end()
117 );
118 stitched.Points3D[frame].y.insert(
119 stitched.Points3D[frame].y.end(),
120 pair.Points3D[frame].y.begin(),
121 pair.Points3D[frame].y.end()
122 );
123 stitched.Points3D[frame].z.insert(
124 stitched.Points3D[frame].z.end(),
125 pair.Points3D[frame].z.begin(),
126 pair.Points3D[frame].z.end()
127 );
128
129 // Append correlation
130 if (frame < pair.corrComb.size()) {
131 stitched.corrComb[frame].insert(
132 stitched.corrComb[frame].end(),
133 pair.corrComb[frame].begin(),
134 pair.corrComb[frame].end()
135 );
136 }
137
138 // Append face correlation
139 if (frame < pair.FaceCorrComb.size()) {
140 stitched.FaceCorrComb[frame].insert(
141 stitched.FaceCorrComb[frame].end(),
142 pair.FaceCorrComb[frame].begin(),
143 pair.FaceCorrComb[frame].end()
144 );
145 }
146
147 // Append face centroids (3 values per face)
148 if (frame < pair.FaceCentroids.size()) {
149 stitched.FaceCentroids[frame].insert(
150 stitched.FaceCentroids[frame].end(),
151 pair.FaceCentroids[frame].begin(),
152 pair.FaceCentroids[frame].end()
153 );
154 }
155 }
156
157 // Append displacement data
158 for (size_t frame = 0; frame < nFrames; ++frame) {
159 if (frame < pair.Disp.DispVec.size()) {
160 stitched.Disp.DispVec[frame].insert(
161 stitched.Disp.DispVec[frame].end(),
162 pair.Disp.DispVec[frame].begin(),
163 pair.Disp.DispVec[frame].end()
164 );
165 }
166 if (frame < pair.Disp.DispMgn.size()) {
167 stitched.Disp.DispMgn[frame].insert(
168 stitched.Disp.DispMgn[frame].end(),
169 pair.Disp.DispMgn[frame].begin(),
170 pair.Disp.DispMgn[frame].end()
171 );
172 }
173 }
174
175 // Append point pair indices
176 stitched.PointPairInds.insert(
177 stitched.PointPairInds.end(),
178 pair_nPoints,
179 static_cast<int>(ipair + 1)
180 );
181
182 // Update pairIndices matrix
183 if (pair.cameraPairInd.size() >= 2) {
184 stitched.pairIndices[(ipair) * 2] = pair.cameraPairInd[0];
185 stitched.pairIndices[(ipair) * 2 + 1] = pair.cameraPairInd[1];
186 }
187
188 LOG_INFO << " Pair " << (ipair + 1) << ": " << pair_nPoints << " points, "
189 << pair_nFaces << " faces (appended)";
190 }
191
192 // Merge calibration data from individual pairs
193 for (const auto& pair : all_pairs) {
194 // Each pair has DLTpath and DLTparameters as vectors (2 cameras)
195 // Convert to 2D structure for DIC3Dcombined
196 stitched.calibration.DLT_paths.push_back(pair.DLTpath);
197 stitched.calibration.DLT_params.push_back(pair.DLTparameters);
198 }
199
200 // Merge distortion data from individual pairs
201 for (const auto& pair : all_pairs) {
202 stitched.distortion.distortion_models.push_back(pair.distortionModel);
203 stitched.distortion.distortion_paths.push_back(pair.distortionPath);
204 }
205
206 size_t total_points = stitched.Points3D[0].x.size();
207 size_t total_faces = stitched.Faces.size() / 3;
208 LOG_INFO << " Total stitched: " << total_points << " points, "
209 << total_faces << " faces";
210
211 return stitched;
212}
213
214// ============================================================================
215// Helper Functions for Geometric Stitching
216// ============================================================================
217
218std::vector<int> computeMeshBoundary(const std::vector<int>& faces,
219 const std::vector<Eigen::Vector3d>& vertices) {
220 if (faces.size() % 3 != 0) {
221 LOG_ERROR << "faces must be divisible by 3";
222 return {};
223 }
224
225 size_t nFaces = faces.size() / 3;
226
227 // Count edge occurrences (boundary edges appear only once)
228 std::map<std::pair<int, int>, int> edge_count;
229
230 for (size_t i = 0; i < nFaces; ++i) {
231 int v0 = faces[i * 3 + 0];
232 int v1 = faces[i * 3 + 1];
233 int v2 = faces[i * 3 + 2];
234
235 if (static_cast<size_t>(v0) >= vertices.size() || static_cast<size_t>(v1) >= vertices.size() || static_cast<size_t>(v2) >= vertices.size()) {
236 continue;
237 }
238 if (vertices[v0].hasNaN() || vertices[v1].hasNaN() || vertices[v2].hasNaN()) {
239 continue;
240 }
241
242 auto add_edge = [&](int a, int b) {
243 auto key = (a < b) ? std::make_pair(a, b) : std::make_pair(b, a);
244 edge_count[key]++;
245 };
246
247 add_edge(v0, v1);
248 add_edge(v1, v2);
249 add_edge(v2, v0);
250 }
251
252 std::vector<int> boundary_edges;
253 for (const auto& [edge, count] : edge_count) {
254 if (count == 1) {
255 boundary_edges.push_back(edge.first);
256 boundary_edges.push_back(edge.second);
257 }
258 }
259
260 return boundary_edges;
261}
262
263std::vector<double> computeEdgeLengths(const std::vector<int>& faces,
264 const std::vector<Eigen::Vector3d>& vertices) {
265 std::vector<double> edge_lengths;
266 if (faces.size() % 3 != 0) return edge_lengths;
267
268 size_t nFaces = faces.size() / 3;
269 edge_lengths.reserve(nFaces * 3);
270
271 for (size_t i = 0; i < nFaces; ++i) {
272 int v0 = faces[i * 3 + 0];
273 int v1 = faces[i * 3 + 1];
274 int v2 = faces[i * 3 + 2];
275
276 if (static_cast<size_t>(v0) >= vertices.size() || static_cast<size_t>(v1) >= vertices.size() || static_cast<size_t>(v2) >= vertices.size()) {
277 edge_lengths.push_back(std::nan(""));
278 edge_lengths.push_back(std::nan(""));
279 edge_lengths.push_back(std::nan(""));
280 continue;
281 }
282
283 edge_lengths.push_back((vertices[v1] - vertices[v0]).norm());
284 edge_lengths.push_back((vertices[v2] - vertices[v1]).norm());
285 edge_lengths.push_back((vertices[v0] - vertices[v2]).norm());
286 }
287
288 return edge_lengths;
289}
290
291// ============================================================================
292// MATLAB-faithful overlap removal helpers
293// ============================================================================
294
295// Per-vertex area-weighted normal. Matches MATLAB patchNormal(F,V).
296// Each face's geometric normal (B-A)x(C-A) has magnitude == 2*area, so summing
297// over incident faces yields an area-weighted average; we then unit-normalize.
298static std::vector<Eigen::Vector3d> computeVertexNormals(
299 const std::vector<int>& faces,
300 const std::vector<Eigen::Vector3d>& vertices) {
301
302 std::vector<Eigen::Vector3d> normals(vertices.size(), Eigen::Vector3d::Zero());
303 size_t nF = faces.size() / 3;
304 for (size_t i = 0; i < nF; ++i) {
305 int v0 = faces[i*3], v1 = faces[i*3+1], v2 = faces[i*3+2];
306 if (v0 < 0 || v1 < 0 || v2 < 0) continue;
307 if (static_cast<size_t>(v0) >= vertices.size() ||
308 static_cast<size_t>(v1) >= vertices.size() ||
309 static_cast<size_t>(v2) >= vertices.size()) continue;
310 const auto& A = vertices[v0];
311 const auto& B = vertices[v1];
312 const auto& C = vertices[v2];
313 if (A.hasNaN() || B.hasNaN() || C.hasNaN()) continue;
314 Eigen::Vector3d n = (B - A).cross(C - A); // area-weighted
315 normals[v0] += n;
316 normals[v1] += n;
317 normals[v2] += n;
318 }
319 for (auto& n : normals) {
320 double nm = n.norm();
321 if (nm > 0.0) n /= nm;
322 }
323 return normals;
324}
325
326// Möller–Trumbore ray-triangle intersection. Two-sided triangle (no backface
327// culling); semi-infinite ray (t >= 0). Returns parametric t along D, or NaN.
328static double rayTriangleHit(const Eigen::Vector3d& O,
329 const Eigen::Vector3d& D,
330 const Eigen::Vector3d& A,
331 const Eigen::Vector3d& B,
332 const Eigen::Vector3d& C,
333 double eps) {
334 Eigen::Vector3d E1 = B - A;
335 Eigen::Vector3d E2 = C - A;
336 Eigen::Vector3d P = D.cross(E2);
337 double det = E1.dot(P);
338 if (std::abs(det) < eps) return std::nan(""); // ray parallel to triangle
339 double inv_det = 1.0 / det;
340 Eigen::Vector3d T = O - A;
341 double u = T.dot(P) * inv_det;
342 if (u < 0.0 || u > 1.0) return std::nan("");
343 Eigen::Vector3d Q = T.cross(E1);
344 double v = D.dot(Q) * inv_det;
345 if (v < 0.0 || u + v > 1.0) return std::nan("");
346 double t = E2.dot(Q) * inv_det;
347 if (t < 0.0) return std::nan(""); // semi-infinite ray
348 return t;
349}
350
351// ============================================================================
352// Optional axis-aligned bounding-box acceleration for the ray casts
353// ----------------------------------------------------------------------------
354// The overlap-removal erosion casts a ray from every boundary vertex of one
355// surface against EVERY face of the other surface, and it does so once per
356// removed vertex. On the 2-pair S09 rig that costs minutes; on a 4-pair 8-view
357// rig it ran for more than nine hours without finishing. A bounding-volume
358// hierarchy over the faces turns the per-ray cost from O(F) triangle tests into
359// O(log F) box tests plus a handful of triangle tests.
360//
361// The prefilter is EXACT, not an approximation: a ray that intersects a triangle
362// necessarily intersects that triangle's (epsilon-padded) axis-aligned box, so no
363// real hit can ever be pruned, and the surviving candidates are scored with the
364// same rayTriangleHit() test and the same "keep the smallest t" rule. The result
365// is therefore identical to the brute-force path.
366//
367// Gated by step_e_ray_bvh (default off) while the option is being validated
368// against the brute-force results.
369// ============================================================================
370static bool g_stitch_ray_bvh = false;
371
372void setStitchRayAccel(bool enabled) { g_stitch_ray_bvh = enabled; }
374
375namespace {
376
377// Per-face boxes plus a median-split hierarchy over them.
378struct FaceBVH {
379 struct Node {
380 Eigen::Vector3d lo, hi;
381 int left = -1, right = -1; // interior nodes
382 int first = 0, count = 0; // leaves: range into `order`
383 };
384 std::vector<Node> nodes;
385 std::vector<int> order; // face indices, permuted during the build
386 std::vector<Eigen::Vector3d> f_lo, f_hi, f_c; // indexed BY FACE INDEX
387};
388
389// Collect the boxes of the faces that the brute-force path would actually test
390// (same validity checks), so the traversal never sees a degenerate face.
391void bvhCollectFaces(FaceBVH& b, const std::vector<int>& faces,
392 const std::vector<Eigen::Vector3d>& vertices, double pad) {
393 const size_t nF = faces.size() / 3;
394 const Eigen::Vector3d nanv = Eigen::Vector3d::Constant(std::nan(""));
395 b.f_lo.assign(nF, nanv);
396 b.f_hi.assign(nF, nanv);
397 b.f_c.assign(nF, nanv);
398 b.order.reserve(nF);
399 const Eigen::Vector3d padv = Eigen::Vector3d::Constant(pad);
400 for (size_t i = 0; i < nF; ++i) {
401 const int v0 = faces[i * 3], v1 = faces[i * 3 + 1], v2 = faces[i * 3 + 2];
402 if (v0 < 0 || v1 < 0 || v2 < 0) continue;
403 if (static_cast<size_t>(v0) >= vertices.size() ||
404 static_cast<size_t>(v1) >= vertices.size() ||
405 static_cast<size_t>(v2) >= vertices.size()) continue;
406 const auto& A = vertices[v0];
407 const auto& B = vertices[v1];
408 const auto& C = vertices[v2];
409 if (A.hasNaN() || B.hasNaN() || C.hasNaN()) continue;
410 b.f_lo[i] = A.cwiseMin(B).cwiseMin(C) - padv;
411 b.f_hi[i] = A.cwiseMax(B).cwiseMax(C) + padv;
412 b.f_c[i] = 0.5 * (b.f_lo[i] + b.f_hi[i]);
413 b.order.push_back(static_cast<int>(i));
414 }
415}
416
417int bvhBuild(FaceBVH& b, int first, int count, int leaf_size) {
418 const double inf = std::numeric_limits<double>::infinity();
419 Eigen::Vector3d lo = Eigen::Vector3d::Constant(inf);
420 Eigen::Vector3d hi = Eigen::Vector3d::Constant(-inf);
421 Eigen::Vector3d clo = lo, chi = hi;
422 for (int k = first; k < first + count; ++k) {
423 const int f = b.order[k];
424 lo = lo.cwiseMin(b.f_lo[f]);
425 hi = hi.cwiseMax(b.f_hi[f]);
426 clo = clo.cwiseMin(b.f_c[f]);
427 chi = chi.cwiseMax(b.f_c[f]);
428 }
429 const int self = static_cast<int>(b.nodes.size());
430 b.nodes.emplace_back();
431 b.nodes[self].lo = lo;
432 b.nodes[self].hi = hi;
433
434 const Eigen::Vector3d ext = chi - clo;
435 int axis = (ext[0] > ext[1]) ? ((ext[0] > ext[2]) ? 0 : 2) : ((ext[1] > ext[2]) ? 1 : 2);
436 if (count <= leaf_size || !(ext[axis] > 0.0)) { // leaf, or all centroids coincide
437 b.nodes[self].first = first;
438 b.nodes[self].count = count;
439 return self;
440 }
441 const int mid = first + count / 2;
442 std::nth_element(b.order.begin() + first, b.order.begin() + mid,
443 b.order.begin() + first + count,
444 [&b, axis](int x, int y) { return b.f_c[x][axis] < b.f_c[y][axis]; });
445 const int l = bvhBuild(b, first, mid - first, leaf_size);
446 const int r = bvhBuild(b, mid, first + count - mid, leaf_size);
447 b.nodes[self].left = l;
448 b.nodes[self].right = r;
449 return self;
450}
451
452// Slab test over [0, t_max]; t_enter lets the traversal skip nodes that start
453// beyond the best hit found so far.
454bool rayBoxHit(const Eigen::Vector3d& O, const Eigen::Vector3d& D, const Eigen::Vector3d& lo,
455 const Eigen::Vector3d& hi, double t_max, double& t_enter) {
456 double tmin = 0.0, tmax = t_max;
457 for (int i = 0; i < 3; ++i) {
458 if (std::abs(D[i]) < 1e-300) { // parallel to this slab
459 if (O[i] < lo[i] || O[i] > hi[i]) return false;
460 continue;
461 }
462 const double inv = 1.0 / D[i];
463 double t0 = (lo[i] - O[i]) * inv;
464 double t1 = (hi[i] - O[i]) * inv;
465 if (t0 > t1) std::swap(t0, t1);
466 if (t0 > tmin) tmin = t0;
467 if (t1 < tmax) tmax = t1;
468 if (tmin > tmax) return false;
469 }
470 t_enter = tmin;
471 return true;
472}
473
474// Closest hit along the semi-infinite ray, or +inf when the ray misses.
475double bvhClosestHit(const FaceBVH& b, const std::vector<int>& faces,
476 const std::vector<Eigen::Vector3d>& vertices, const Eigen::Vector3d& O,
477 const Eigen::Vector3d& D, double eps) {
478 double best_t = std::numeric_limits<double>::infinity();
479 if (b.nodes.empty()) return best_t;
480 int stack[128];
481 int sp = 0;
482 stack[sp++] = 0;
483 while (sp > 0) {
484 const FaceBVH::Node& n = b.nodes[stack[--sp]];
485 double t_enter = 0.0;
486 if (!rayBoxHit(O, D, n.lo, n.hi, best_t, t_enter)) continue;
487 if (t_enter > best_t) continue;
488 if (n.left < 0) { // leaf
489 for (int k = n.first; k < n.first + n.count; ++k) {
490 const int i = b.order[k];
491 const double t = rayTriangleHit(O, D, vertices[faces[i * 3]],
492 vertices[faces[i * 3 + 1]],
493 vertices[faces[i * 3 + 2]], eps);
494 if (!std::isnan(t) && t < best_t) best_t = t;
495 }
496 } else if (sp + 2 <= 128) {
497 stack[sp++] = n.left;
498 stack[sp++] = n.right;
499 } else {
500 LOG_WARN << "stitch BVH traversal stack overflow; falling back to a full scan";
501 return -1.0; // caller re-runs this ray brute force
502 }
503 }
504 return best_t;
505}
506
507} // namespace
508
509// For each (origin, direction) pair, find closest hit point on the mesh.
510// Returns hit positions; NaN-filled Vector3d when no hit.
511// Matches MATLAB triSurfRaySetIntersect with optStruct.ray='ray', triangle='two sided'.
512static std::vector<Eigen::Vector3d> raySetTriangleIntersect(
513 const std::vector<Eigen::Vector3d>& origins,
514 const std::vector<Eigen::Vector3d>& dirs,
515 const std::vector<int>& faces,
516 const std::vector<Eigen::Vector3d>& vertices,
517 double eps = 1e-6) {
518
519 std::vector<Eigen::Vector3d> hits(origins.size(),
520 Eigen::Vector3d::Constant(std::nan("")));
521 size_t nF = faces.size() / 3;
522
523 // Fast path: prune candidate triangles with an AABB hierarchy (exact, see above).
524 FaceBVH bvh;
525 bool use_bvh = g_stitch_ray_bvh && nF > 0 && !origins.empty();
526 if (use_bvh) {
527 bvhCollectFaces(bvh, faces, vertices, 1e-9);
528 if (bvh.order.empty()) {
529 use_bvh = false;
530 } else {
531 bvh.nodes.reserve(2 * bvh.order.size());
532 bvhBuild(bvh, 0, static_cast<int>(bvh.order.size()), 8);
533 }
534 }
535
536 for (size_t r = 0; r < origins.size(); ++r) {
537 const auto& O = origins[r];
538 const auto& D = dirs[r];
539 if (O.hasNaN() || D.hasNaN() || D.norm() < 1e-12) continue;
540 double best_t = std::numeric_limits<double>::infinity();
541 if (use_bvh) {
542 const double t = bvhClosestHit(bvh, faces, vertices, O, D, eps);
543 if (t >= 0.0) { // negative marks a traversal bail-out -> full scan below
544 if (std::isfinite(t)) hits[r] = O + t * D;
545 continue;
546 }
547 }
548 for (size_t i = 0; i < nF; ++i) {
549 int v0 = faces[i*3], v1 = faces[i*3+1], v2 = faces[i*3+2];
550 if (v0 < 0 || v1 < 0 || v2 < 0) continue;
551 if (static_cast<size_t>(v0) >= vertices.size() ||
552 static_cast<size_t>(v1) >= vertices.size() ||
553 static_cast<size_t>(v2) >= vertices.size()) continue;
554 const auto& A = vertices[v0];
555 const auto& B = vertices[v1];
556 const auto& C = vertices[v2];
557 if (A.hasNaN() || B.hasNaN() || C.hasNaN()) continue;
558 double t = rayTriangleHit(O, D, A, B, C, eps);
559 if (!std::isnan(t) && t < best_t) best_t = t;
560 }
561 if (std::isfinite(best_t)) hits[r] = O + best_t * D;
562 }
563 return hits;
564}
565
566// triSurfSetDist(F1,V1,F2,V2,'ray'): per-vertex ray-trace distance from V1
567// (along its outward normal) onto surface (F2,V2). NaN if no hit.
568static std::vector<double> computeRaySurfaceDistance(
569 const std::vector<Eigen::Vector3d>& V1,
570 const std::vector<Eigen::Vector3d>& N1,
571 const std::vector<int>& F2,
572 const std::vector<Eigen::Vector3d>& V2) {
573
574 auto hits = raySetTriangleIntersect(V1, N1, F2, V2, 1e-6);
575 std::vector<double> Q(V1.size(), std::nan(""));
576 for (size_t i = 0; i < V1.size(); ++i) {
577 if (!hits[i].hasNaN()) Q[i] = (V1[i] - hits[i]).norm();
578 }
579 return Q;
580}
581
582// Mean of finite triangle edge lengths (NaN-safe).
583static double meanFiniteEdgeLength(const std::vector<int>& faces,
584 const std::vector<Eigen::Vector3d>& vertices) {
585 auto el = computeEdgeLengths(faces, vertices);
586 double sum = 0.0;
587 size_t n = 0;
588 for (double e : el) { if (std::isfinite(e)) { sum += e; ++n; } }
589 return n ? sum / static_cast<double>(n) : 0.0;
590}
591
592// Filter face list by per-face keep mask (flat [v0,v1,v2, ...] in/out).
593static std::vector<int> applyFaceMask(const std::vector<int>& F,
594 const std::vector<bool>& CT) {
595 std::vector<int> out;
596 size_t nF = F.size() / 3;
597 out.reserve(F.size());
598 for (size_t i = 0; i < nF; ++i) {
599 if (CT[i]) {
600 out.push_back(F[i*3]);
601 out.push_back(F[i*3+1]);
602 out.push_back(F[i*3+2]);
603 }
604 }
605 return out;
606}
607
608// Mark every still-active face that references v_remove as removed (CT[i]=false).
609// Matches MATLAB: logicF1Remove = ~any(logicV1Remove(F1),2); CT1 = CT1 & logicF1Remove.
610static bool removeFacesContainingVertex(const std::vector<int>& F,
611 std::vector<bool>& CT,
612 int v_remove) {
613 size_t nF = F.size() / 3;
614 bool removed_any = false;
615 for (size_t i = 0; i < nF; ++i) {
616 if (!CT[i]) continue;
617 if (F[i*3] == v_remove || F[i*3+1] == v_remove || F[i*3+2] == v_remove) {
618 CT[i] = false;
619 removed_any = true;
620 }
621 }
622 return removed_any;
623}
624
625// ============================================================================
626// Faithful port of removeOverlapSurface_temp_2018_10_30.m
627//
628// Two phase iterative erosion:
629// Phase 1: ray-trace boundary vertices along their normals onto the other
630// surface. A boundary vertex is "overlapping" if it ray-hits the other
631// surface within 2*meanEdgeLength. Each iteration removes ALL active faces
632// containing the worst-quality boundary vertex (the one with the largest
633// pre-computed ray distance Q). Removal happens on the surface whose worst
634// Q is larger; if pair_forced is true, we always remove from surface 2.
635// Phase 2: gap closure. Using Euclidean min-distance from each boundary
636// vertex to the other boundary's vertices PLUS its edge midpoints and
637// 1/4 / 3/4 points (closer-than-vertex sampling), remove faces while any
638// boundary vertex sits within min_gap of the other boundary.
639//
640// min_gap should be 0.4 * meanEdgeLength (MATLAB caller convention).
641// ============================================================================
642std::pair<std::vector<bool>, std::vector<bool>> removeOverlapSurfaces(
643 const std::vector<int>& faces1,
644 const std::vector<int>& faces2,
645 const std::vector<Eigen::Vector3d>& V1,
646 const std::vector<Eigen::Vector3d>& V2,
647 double min_gap,
648 bool pair_forced) {
649
650 const size_t nF1 = faces1.size() / 3;
651 const size_t nF2 = faces2.size() / 3;
652 std::vector<bool> CT1(nF1, true);
653 std::vector<bool> CT2(nF2, true);
654 if (nF1 == 0 || nF2 == 0) return {CT1, CT2};
655
656 // Per-vertex normals + quality measure Q (ray distance) — computed once on
657 // the original full meshes, matching MATLAB lines 22-25, 60-62.
658 auto N1 = computeVertexNormals(faces1, V1);
659 auto N2 = computeVertexNormals(faces2, V2);
660 auto Q1 = computeRaySurfaceDistance(V1, N1, faces2, V2);
661 auto Q2 = computeRaySurfaceDistance(V2, N2, faces1, V1);
662
663 const double meanEdge =
664 0.5 * (meanFiniteEdgeLength(faces1, V1) + meanFiniteEdgeLength(faces2, V2));
665 if (meanEdge <= 0.0) return {CT1, CT2};
666 const double rayCloseThresh = 2.0 * meanEdge;
667
668 std::vector<bool> logicNoHit1(V1.size(), false);
669 std::vector<bool> logicNoHit2(V2.size(), false);
670
671 // Safety bound: in the worst case each iteration removes one face.
672 const size_t maxIter = nF1 + nF2 + 16;
673 size_t removed_phase1_s1 = 0, removed_phase1_s2 = 0;
674 size_t removed_phase2_s1 = 0, removed_phase2_s2 = 0;
675
676 // ---- Phase 1: ray-based overlap removal ----
677 for (size_t iter = 0; iter < maxIter; ++iter) {
678 auto F1cur = applyFaceMask(faces1, CT1);
679 auto F2cur = applyFaceMask(faces2, CT2);
680 if (F1cur.empty() || F2cur.empty()) break;
681
682 auto Eb1 = computeMeshBoundary(F1cur, V1);
683 auto Eb2 = computeMeshBoundary(F2cur, V2);
684
685 // Boundary vertex sets (unique), excluding previously-no-hit vertices.
686 std::set<int> b1set, b2set;
687 for (int v : Eb1) b1set.insert(v);
688 for (int v : Eb2) b2set.insert(v);
689 std::vector<int> ib1, ib2;
690 ib1.reserve(b1set.size()); ib2.reserve(b2set.size());
691 for (int v : b1set)
692 if (static_cast<size_t>(v) < logicNoHit1.size() && !logicNoHit1[v])
693 ib1.push_back(v);
694 for (int v : b2set)
695 if (static_cast<size_t>(v) < logicNoHit2.size() && !logicNoHit2[v])
696 ib2.push_back(v);
697
698 if (ib1.empty() && ib2.empty()) break;
699
700 int worst_v1 = -1, first_close_v1 = -1;
701 int worst_v2 = -1, first_close_v2 = -1;
702 double worstQ1 = -std::numeric_limits<double>::infinity();
703 double worstQ2 = -std::numeric_limits<double>::infinity();
704
705 if (!ib1.empty()) {
706 std::vector<Eigen::Vector3d> O(ib1.size()), D(ib1.size());
707 for (size_t i = 0; i < ib1.size(); ++i) { O[i] = V1[ib1[i]]; D[i] = N1[ib1[i]]; }
708 auto trace = raySetTriangleIntersect(O, D, F2cur, V2, 1e-6);
709 for (size_t i = 0; i < ib1.size(); ++i) {
710 bool close = !trace[i].hasNaN() &&
711 (V1[ib1[i]] - trace[i]).norm() < rayCloseThresh;
712 if (!close) {
713 logicNoHit1[ib1[i]] = true;
714 } else {
715 if (first_close_v1 < 0) first_close_v1 = ib1[i];
716 double q = Q1[ib1[i]];
717 if (std::isfinite(q) && q > worstQ1) { worstQ1 = q; worst_v1 = ib1[i]; }
718 }
719 }
720 }
721 if (!ib2.empty()) {
722 std::vector<Eigen::Vector3d> O(ib2.size()), D(ib2.size());
723 for (size_t i = 0; i < ib2.size(); ++i) { O[i] = V2[ib2[i]]; D[i] = N2[ib2[i]]; }
724 auto trace = raySetTriangleIntersect(O, D, F1cur, V1, 1e-6);
725 for (size_t i = 0; i < ib2.size(); ++i) {
726 bool close = !trace[i].hasNaN() &&
727 (V2[ib2[i]] - trace[i]).norm() < rayCloseThresh;
728 if (!close) {
729 logicNoHit2[ib2[i]] = true;
730 } else {
731 if (first_close_v2 < 0) first_close_v2 = ib2[i];
732 double q = Q2[ib2[i]];
733 if (std::isfinite(q) && q > worstQ2) { worstQ2 = q; worst_v2 = ib2[i]; }
734 }
735 }
736 }
737
738 bool any_close_1 = (first_close_v1 >= 0);
739 bool any_close_2 = (first_close_v2 >= 0);
740 if (!any_close_1 && !any_close_2) break;
741
742 // Pick which surface to erode this iteration.
743 bool remove_from_1;
744 if (any_close_1 && any_close_2) {
745 // pair_forced -> always favor removing from surface 2 (MATLAB convention)
746 remove_from_1 = (!pair_forced) && (worstQ1 > worstQ2);
747 } else {
748 remove_from_1 = any_close_1;
749 }
750
751 int v_remove = remove_from_1 ? (worst_v1 >= 0 ? worst_v1 : first_close_v1)
752 : (worst_v2 >= 0 ? worst_v2 : first_close_v2);
753 if (v_remove < 0) break;
754
755 bool ok = remove_from_1
756 ? removeFacesContainingVertex(faces1, CT1, v_remove)
757 : removeFacesContainingVertex(faces2, CT2, v_remove);
758 if (!ok) break;
759 if (remove_from_1) ++removed_phase1_s1; else ++removed_phase1_s2;
760 }
761
762 // ---- Phase 2: gap closure ----
763 // Iteratively remove the worst boundary face while either boundary sits
764 // within min_gap of the other (Euclidean distance to vertices + edge
765 // midpoints + 1/4 / 3/4 points). pair_forced is NOT considered in phase 2
766 // (matches MATLAB line 324: `if worstQ1>worstQ2` without pair_forced guard).
767 {
768 auto F1cur = applyFaceMask(faces1, CT1);
769 auto F2cur = applyFaceMask(faces2, CT2);
770 if (F1cur.empty() || F2cur.empty()) {
771 // Nothing to do in phase 2.
772 } else {
773 for (size_t iter = 0; iter < maxIter; ++iter) {
774 F1cur = applyFaceMask(faces1, CT1);
775 F2cur = applyFaceMask(faces2, CT2);
776 if (F1cur.empty() || F2cur.empty()) break;
777
778 auto Eb1 = computeMeshBoundary(F1cur, V1);
779 auto Eb2 = computeMeshBoundary(F2cur, V2);
780 if (Eb1.empty() || Eb2.empty()) break;
781
782 std::set<int> b1set, b2set;
783 for (int v : Eb1) b1set.insert(v);
784 for (int v : Eb2) b2set.insert(v);
785 std::vector<int> ib1(b1set.begin(), b1set.end());
786 std::vector<int> ib2(b2set.begin(), b2set.end());
787
788 // Build extended target sets (boundary verts + edge midpoints + 1/4,3/4).
789 std::vector<Eigen::Vector3d> tgt1, tgt2;
790 tgt1.reserve(ib1.size() + (Eb1.size() / 2) * 3);
791 tgt2.reserve(ib2.size() + (Eb2.size() / 2) * 3);
792 for (int v : ib1) tgt1.push_back(V1[v]);
793 for (int v : ib2) tgt2.push_back(V2[v]);
794 size_t nEb1 = Eb1.size() / 2, nEb2 = Eb2.size() / 2;
795 for (size_t i = 0; i < nEb1; ++i) {
796 int a = Eb1[i*2], b = Eb1[i*2+1];
797 if (static_cast<size_t>(a) >= V1.size() ||
798 static_cast<size_t>(b) >= V1.size()) continue;
799 const auto& A = V1[a]; const auto& B = V1[b];
800 if (A.hasNaN() || B.hasNaN()) continue;
801 tgt1.push_back(0.5 * (A + B));
802 tgt1.push_back(0.25 * A + 0.75 * B);
803 tgt1.push_back(0.75 * A + 0.25 * B);
804 }
805 for (size_t i = 0; i < nEb2; ++i) {
806 int a = Eb2[i*2], b = Eb2[i*2+1];
807 if (static_cast<size_t>(a) >= V2.size() ||
808 static_cast<size_t>(b) >= V2.size()) continue;
809 const auto& A = V2[a]; const auto& B = V2[b];
810 if (A.hasNaN() || B.hasNaN()) continue;
811 tgt2.push_back(0.5 * (A + B));
812 tgt2.push_back(0.25 * A + 0.75 * B);
813 tgt2.push_back(0.75 * A + 0.25 * B);
814 }
815
816 int worst_v1 = -1, first_close_v1 = -1;
817 int worst_v2 = -1, first_close_v2 = -1;
818 double worstQ1 = -std::numeric_limits<double>::infinity();
819 double worstQ2 = -std::numeric_limits<double>::infinity();
820 size_t close1 = 0, close2 = 0;
821
822 for (int v : ib1) {
823 const auto& P = V1[v];
824 if (P.hasNaN()) continue;
825 double dmin = std::numeric_limits<double>::infinity();
826 for (const auto& p : tgt2) {
827 if (p.hasNaN()) continue;
828 double d = (P - p).norm();
829 if (d < dmin) dmin = d;
830 }
831 if (dmin < min_gap) {
832 ++close1;
833 if (first_close_v1 < 0) first_close_v1 = v;
834 double q = Q1[v];
835 if (std::isfinite(q) && q > worstQ1) { worstQ1 = q; worst_v1 = v; }
836 }
837 }
838 for (int v : ib2) {
839 const auto& P = V2[v];
840 if (P.hasNaN()) continue;
841 double dmin = std::numeric_limits<double>::infinity();
842 for (const auto& p : tgt1) {
843 if (p.hasNaN()) continue;
844 double d = (P - p).norm();
845 if (d < dmin) dmin = d;
846 }
847 if (dmin < min_gap) {
848 ++close2;
849 if (first_close_v2 < 0) first_close_v2 = v;
850 double q = Q2[v];
851 if (std::isfinite(q) && q > worstQ2) { worstQ2 = q; worst_v2 = v; }
852 }
853 }
854
855 if (close1 == 0 && close2 == 0) break;
856
857 bool remove_from_1;
858 if (close1 > 0 && close2 > 0) {
859 remove_from_1 = (worstQ1 > worstQ2);
860 } else {
861 remove_from_1 = (close1 > 0);
862 }
863
864 int v_remove = remove_from_1
865 ? (worst_v1 >= 0 ? worst_v1 : first_close_v1)
866 : (worst_v2 >= 0 ? worst_v2 : first_close_v2);
867 if (v_remove < 0) break;
868
869 bool ok = remove_from_1
870 ? removeFacesContainingVertex(faces1, CT1, v_remove)
871 : removeFacesContainingVertex(faces2, CT2, v_remove);
872 if (!ok) break;
873 if (remove_from_1) ++removed_phase2_s1; else ++removed_phase2_s2;
874 }
875 }
876 }
877
878 LOG_DEBUG << " removeOverlapSurfaces: phase1 removed " << removed_phase1_s1
879 << " faces from S1, " << removed_phase1_s2 << " from S2;"
880 << " phase2 removed " << removed_phase2_s1 << " from S1, "
881 << removed_phase2_s2 << " from S2 (meanEdge=" << meanEdge
882 << ", minGap=" << min_gap << ")";
883
884 return {CT1, CT2};
885}
886
887// Group boundary edges into connected components (matches MATLAB tesgroup)
888std::vector<std::vector<int>> groupBoundaryEdges(const std::vector<int>& boundary_edges) {
889 size_t nEdges = boundary_edges.size() / 2;
890 if (nEdges == 0) return {};
891
892 // Build adjacency: vertex -> list of edge indices
893 std::unordered_map<int, std::vector<int>> vert_to_edges;
894 for (size_t i = 0; i < nEdges; ++i) {
895 vert_to_edges[boundary_edges[i*2]].push_back(static_cast<int>(i));
896 vert_to_edges[boundary_edges[i*2+1]].push_back(static_cast<int>(i));
897 }
898
899 std::vector<bool> visited(nEdges, false);
900 std::vector<std::vector<int>> groups;
901
902 for (size_t start = 0; start < nEdges; ++start) {
903 if (visited[start]) continue;
904
905 // BFS from this edge
906 std::vector<int> group;
907 std::vector<int> stack = {static_cast<int>(start)};
908 visited[start] = true;
909
910 while (!stack.empty()) {
911 int eidx = stack.back();
912 stack.pop_back();
913 group.push_back(eidx);
914
915 // Visit edges connected through shared vertices
916 for (int k = 0; k < 2; ++k) {
917 int v = boundary_edges[eidx * 2 + k];
918 for (int neighbor : vert_to_edges[v]) {
919 if (!visited[neighbor]) {
920 visited[neighbor] = true;
921 stack.push_back(neighbor);
922 }
923 }
924 }
925 }
926 groups.push_back(std::move(group));
927 }
928
929 return groups;
930}
931
932// Convert unordered edge list to ordered vertex curve (matches MATLAB edgeListToCurve)
933std::vector<int> edgeListToCurve(const std::vector<int>& edges) {
934 size_t nEdges = edges.size() / 2;
935 if (nEdges == 0) return {};
936
937 // Build adjacency list
938 std::unordered_map<int, std::vector<int>> adj;
939 for (size_t i = 0; i < nEdges; ++i) {
940 int a = edges[i*2], b = edges[i*2+1];
941 adj[a].push_back(b);
942 adj[b].push_back(a);
943 }
944
945 // Start from first vertex of first edge
946 int start = edges[0];
947 std::vector<int> curve;
948 curve.push_back(start);
949
950 std::set<int> visited_verts;
951 visited_verts.insert(start);
952 int current = start;
953
954 while (true) {
955 bool found = false;
956 for (int next : adj[current]) {
957 if (visited_verts.find(next) == visited_verts.end()) {
958 curve.push_back(next);
959 visited_verts.insert(next);
960 current = next;
961 found = true;
962 break;
963 }
964 }
965 if (!found) break;
966 }
967
968 return curve;
969}
970
971// Find faces where all 3 edges are boundary edges
972std::vector<bool> findAllBoundaryFaces(const std::vector<int>& faces,
973 const std::vector<int>& boundary_edges) {
974 size_t nFaces = faces.size() / 3;
975
976 // Build set of boundary edges (as sorted pairs)
977 std::set<std::pair<int,int>> boundary_set;
978 for (size_t i = 0; i < boundary_edges.size() / 2; ++i) {
979 int a = boundary_edges[i*2], b = boundary_edges[i*2+1];
980 boundary_set.insert(a < b ? std::make_pair(a, b) : std::make_pair(b, a));
981 }
982
983 auto is_boundary_edge = [&](int a, int b) -> bool {
984 auto key = a < b ? std::make_pair(a, b) : std::make_pair(b, a);
985 return boundary_set.count(key) > 0;
986 };
987
988 std::vector<bool> keep(nFaces, true);
989 for (size_t i = 0; i < nFaces; ++i) {
990 int v0 = faces[i*3], v1 = faces[i*3+1], v2 = faces[i*3+2];
991 if (is_boundary_edge(v0, v1) && is_boundary_edge(v1, v2) && is_boundary_edge(v0, v2)) {
992 keep[i] = false;
993 }
994 }
995 return keep;
996}
997
998// Zip two boundary curves with a greedy advancing-front triangle strip
999std::vector<int> zipBoundaryCurves(const std::vector<int>& curve1,
1000 const std::vector<int>& curve2,
1001 const std::vector<Eigen::Vector3d>& V_combined) {
1002 if (curve1.size() < 2 || curve2.size() < 2) return {};
1003
1004 std::vector<int> new_faces;
1005 size_t i1 = 0, i2 = 0;
1006
1007 while (i1 < curve1.size() - 1 || i2 < curve2.size() - 1) {
1008 int c1_cur = curve1[i1];
1009 int c2_cur = curve2[i2];
1010
1011 bool can_advance_1 = (i1 < curve1.size() - 1);
1012 bool can_advance_2 = (i2 < curve2.size() - 1);
1013
1014 if (!can_advance_1 && !can_advance_2) break;
1015
1016 bool advance_1 = false;
1017 if (can_advance_1 && can_advance_2) {
1018 // Pick shorter diagonal
1019 int c1_next = curve1[i1 + 1];
1020 int c2_next = curve2[i2 + 1];
1021 double d1 = (V_combined[c1_next] - V_combined[c2_cur]).norm();
1022 double d2 = (V_combined[c2_next] - V_combined[c1_cur]).norm();
1023 advance_1 = (d1 <= d2);
1024 } else {
1025 advance_1 = can_advance_1;
1026 }
1027
1028 if (advance_1) {
1029 int c1_next = curve1[i1 + 1];
1030 new_faces.push_back(c1_cur);
1031 new_faces.push_back(c1_next);
1032 new_faces.push_back(c2_cur);
1033 ++i1;
1034 } else {
1035 int c2_next = curve2[i2 + 1];
1036 new_faces.push_back(c1_cur);
1037 new_faces.push_back(c2_next);
1038 new_faces.push_back(c2_cur);
1039 ++i2;
1040 }
1041 }
1042
1043 return new_faces;
1044}
1045
1046// ============================================================================
1047// Internal: Filter faces by keep mask, return new face list
1048// ============================================================================
1049static std::vector<int> filterFaces(const std::vector<int>& faces,
1050 const std::vector<bool>& keep) {
1051 std::vector<int> result;
1052 size_t nFaces = faces.size() / 3;
1053 for (size_t i = 0; i < nFaces; ++i) {
1054 if (keep[i]) {
1055 result.push_back(faces[i*3]);
1056 result.push_back(faces[i*3+1]);
1057 result.push_back(faces[i*3+2]);
1058 }
1059 }
1060 return result;
1061}
1062
1063// Filter a per-face vector by keep mask
1064template<typename T>
1065static std::vector<T> filterByMask(const std::vector<T>& data, const std::vector<bool>& keep) {
1066 std::vector<T> result;
1067 for (size_t i = 0; i < keep.size() && i < data.size(); ++i) {
1068 if (keep[i]) result.push_back(data[i]);
1069 }
1070 return result;
1071}
1072
1073// ============================================================================
1074// Geometric Stitching Implementation
1075// ============================================================================
1076
1077DIC3Dcombined stitchPairsGeometric(const std::vector<DIC3DpairResults>& all_pairs,
1078 const std::vector<int>& pair_order,
1079 bool pair_forced) {
1080 if (all_pairs.empty()) {
1081 return DIC3Dcombined();
1082 }
1083
1084 if (pair_order.empty()) {
1085 LOG_WARN << "Empty pair order, using simple stitching";
1086 return stitchPairsSimple(all_pairs);
1087 }
1088
1089 LOG_INFO << "=== Geometric Stitching (with overlap removal) ===";
1090 {
1091 std::ostringstream order_oss;
1092 for (int idx : pair_order) order_oss << idx << " ";
1093 LOG_INFO << " Stitching order: " << order_oss.str();
1094 }
1095 LOG_INFO << " pair_forced=" << (pair_forced ? "true" : "false");
1096
1097 size_t first_idx = pair_order[0] - 1;
1098 if (first_idx >= all_pairs.size()) {
1099 LOG_ERROR << "Invalid pair index in pair_order";
1100 return DIC3Dcombined();
1101 }
1102
1103 size_t nPairs = all_pairs.size();
1104 size_t nFrames = all_pairs[first_idx].Points3D.size();
1105
1106 // === Working state: faces, per-face data, vertices ===
1107 // We maintain a "current stitched" mesh and iteratively add pairs.
1108 // Faces index into the combined vertex array [V_stitched ; V_next].
1109
1110 // Initialize from first pair
1111 std::vector<int> cur_faces = all_pairs[first_idx].Faces;
1112 std::vector<double> cur_faceColors = all_pairs[first_idx].FaceColors;
1113 std::vector<int> cur_facePairInds(cur_faces.size() / 3, pair_order[0]);
1114 // Per-frame data: Points3D, corrComb stored as vectors-of-frames
1115 std::vector<Point3DFrame> cur_pts3d = all_pairs[first_idx].Points3D;
1116 std::vector<std::vector<double>> cur_corrComb = all_pairs[first_idx].corrComb;
1117 std::vector<int> cur_pointPairInds(cur_pts3d[0].x.size(), pair_order[0]);
1118
1119 // pairIndices matrix (nPairs x 2 flat)
1120 std::vector<int> pairIndices(nPairs * 2, 0);
1121 if (all_pairs[first_idx].cameraPairInd.size() >= 2) {
1122 pairIndices[first_idx * 2] = all_pairs[first_idx].cameraPairInd[0];
1123 pairIndices[first_idx * 2 + 1] = all_pairs[first_idx].cameraPairInd[1];
1124 }
1125
1126 LOG_INFO << " Pair " << pair_order[0] << " (base): "
1127 << cur_pts3d[0].x.size() << " points, "
1128 << cur_faces.size() / 3 << " faces";
1129
1130 // === Iteratively stitch remaining pairs ===
1131 for (size_t ipair = 1; ipair < pair_order.size(); ++ipair) {
1132 size_t pair_idx = pair_order[ipair] - 1;
1133 if (pair_idx >= all_pairs.size()) {
1134 LOG_ERROR << "Invalid pair index " << pair_order[ipair];
1135 continue;
1136 }
1137
1138 const auto& next = all_pairs[pair_idx];
1139 std::vector<int> next_faces = next.Faces;
1140 std::vector<double> next_faceColors = next.FaceColors;
1141
1142 // Convert to Eigen vectors (frame 0) for geometry operations
1143 size_t nV1 = cur_pts3d[0].x.size();
1144 size_t nV2 = next.Points3D[0].x.size();
1145 std::vector<Eigen::Vector3d> V1(nV1), V2(nV2);
1146 for (size_t i = 0; i < nV1; ++i)
1147 V1[i] = Eigen::Vector3d(cur_pts3d[0].x[i], cur_pts3d[0].y[i], cur_pts3d[0].z[i]);
1148 for (size_t i = 0; i < nV2; ++i)
1149 V2[i] = Eigen::Vector3d(next.Points3D[0].x[i], next.Points3D[0].y[i], next.Points3D[0].z[i]);
1150
1151 // ---- Step 1: Remove NaN faces ----
1152 {
1153 size_t nF1 = cur_faces.size() / 3;
1154 std::vector<bool> keep1(nF1, true);
1155 for (size_t i = 0; i < nF1; ++i) {
1156 int v0=cur_faces[i*3], v1=cur_faces[i*3+1], v2=cur_faces[i*3+2];
1157 if (static_cast<size_t>(v0)>=V1.size()||static_cast<size_t>(v1)>=V1.size()||static_cast<size_t>(v2)>=V1.size()||
1158 V1[v0].hasNaN()||V1[v1].hasNaN()||V1[v2].hasNaN())
1159 keep1[i] = false;
1160 }
1161 cur_faces = filterFaces(cur_faces, keep1);
1162 cur_faceColors = filterByMask(cur_faceColors, keep1);
1163 cur_facePairInds = filterByMask(cur_facePairInds, keep1);
1164 }
1165 {
1166 size_t nF2 = next_faces.size() / 3;
1167 std::vector<bool> keep2(nF2, true);
1168 for (size_t i = 0; i < nF2; ++i) {
1169 int v0=next_faces[i*3], v1=next_faces[i*3+1], v2=next_faces[i*3+2];
1170 if (static_cast<size_t>(v0)>=V2.size()||static_cast<size_t>(v1)>=V2.size()||static_cast<size_t>(v2)>=V2.size()||
1171 V2[v0].hasNaN()||V2[v1].hasNaN()||V2[v2].hasNaN())
1172 keep2[i] = false;
1173 }
1174 next_faces = filterFaces(next_faces, keep2);
1175 next_faceColors = filterByMask(next_faceColors, keep2);
1176 }
1177
1178 // ---- Step 2: Overlap removal ----
1179 auto edgeLens1 = computeEdgeLengths(cur_faces, V1);
1180 auto edgeLens2 = computeEdgeLengths(next_faces, V2);
1181 double meanEdge = 0.0;
1182 size_t cnt = 0;
1183 for (double e : edgeLens1) { if (!std::isnan(e)) { meanEdge += e; cnt++; } }
1184 for (double e : edgeLens2) { if (!std::isnan(e)) { meanEdge += e; cnt++; } }
1185 if (cnt > 0) meanEdge /= cnt;
1186
1187 double minGap = 0.4 * meanEdge;
1188 double minDistValue = 5.0 * minGap; // used later for boundary zipping
1189
1190 // MATLAB-faithful overlap removal: iterative ray-tracing erosion in phase 1,
1191 // gap closure with min_gap in phase 2. Pass minGap (NOT minDistValue).
1192 auto [keep1, keep2] = removeOverlapSurfaces(
1193 cur_faces, next_faces, V1, V2, minGap, pair_forced);
1194
1195 size_t rem1 = std::count(keep1.begin(), keep1.end(), false);
1196 size_t rem2 = std::count(keep2.begin(), keep2.end(), false);
1197 LOG_DEBUG << " Overlap removal: " << rem1 << " faces from S1, " << rem2 << " from S2";
1198
1199 // Apply overlap filter
1200 cur_faces = filterFaces(cur_faces, keep1);
1201 cur_faceColors = filterByMask(cur_faceColors, keep1);
1202 cur_facePairInds = filterByMask(cur_facePairInds, keep1);
1203 next_faces = filterFaces(next_faces, keep2);
1204 next_faceColors = filterByMask(next_faceColors, keep2);
1205
1206 // ---- Step 3: Remove all-boundary faces ----
1207 {
1208 auto bnd1 = computeMeshBoundary(cur_faces, V1);
1209 auto abf1 = findAllBoundaryFaces(cur_faces, bnd1);
1210 size_t remB1 = std::count(abf1.begin(), abf1.end(), false);
1211 if (remB1 > 0) {
1212 cur_faces = filterFaces(cur_faces, abf1);
1213 cur_faceColors = filterByMask(cur_faceColors, abf1);
1214 cur_facePairInds = filterByMask(cur_facePairInds, abf1);
1215 LOG_DEBUG << " Removed " << remB1 << " all-boundary faces from S1";
1216 }
1217 }
1218 {
1219 auto bnd2 = computeMeshBoundary(next_faces, V2);
1220 auto abf2 = findAllBoundaryFaces(next_faces, bnd2);
1221 size_t remB2 = std::count(abf2.begin(), abf2.end(), false);
1222 if (remB2 > 0) {
1223 next_faces = filterFaces(next_faces, abf2);
1224 next_faceColors = filterByMask(next_faceColors, abf2);
1225 LOG_DEBUG << " Removed " << remB2 << " all-boundary faces from S2";
1226 }
1227 }
1228
1229 // ---- Step 4: Boundary zipping ----
1230 // Recompute boundaries after filtering
1231 auto bnd1 = computeMeshBoundary(cur_faces, V1);
1232 auto bnd2 = computeMeshBoundary(next_faces, V2);
1233
1234 // Build combined vertex array for zipping
1235 // V2 vertices will be at offset nV1 in the combined array
1236 std::vector<Eigen::Vector3d> V_combined;
1237 V_combined.reserve(nV1 + nV2);
1238 V_combined.insert(V_combined.end(), V1.begin(), V1.end());
1239 V_combined.insert(V_combined.end(), V2.begin(), V2.end());
1240
1241 // Offset next_faces for combined vertex array
1242 std::vector<int> next_faces_offset;
1243 next_faces_offset.reserve(next_faces.size());
1244 for (int idx : next_faces) next_faces_offset.push_back(idx + static_cast<int>(nV1));
1245
1246 // Also offset bnd2 edges
1247 std::vector<int> bnd2_offset;
1248 bnd2_offset.reserve(bnd2.size());
1249 for (int idx : bnd2) bnd2_offset.push_back(idx + static_cast<int>(nV1));
1250
1251 // Group boundaries
1252 auto groups1 = groupBoundaryEdges(bnd1);
1253 auto groups2 = groupBoundaryEdges(bnd2_offset);
1254
1255 std::vector<int> zip_faces;
1256
1257 for (size_t ib1 = 0; ib1 < groups1.size(); ++ib1) {
1258 // Extract edges for this group
1259 std::vector<int> edges_g1;
1260 for (int eidx : groups1[ib1]) {
1261 edges_g1.push_back(bnd1[eidx*2]);
1262 edges_g1.push_back(bnd1[eidx*2+1]);
1263 }
1264 auto curve1 = edgeListToCurve(edges_g1);
1265 if (curve1.size() < 2) continue;
1266
1267 for (size_t ib2 = 0; ib2 < groups2.size(); ++ib2) {
1268 std::vector<int> edges_g2;
1269 for (int eidx : groups2[ib2]) {
1270 edges_g2.push_back(bnd2_offset[eidx*2]);
1271 edges_g2.push_back(bnd2_offset[eidx*2+1]);
1272 }
1273 auto curve2 = edgeListToCurve(edges_g2);
1274 if (curve2.size() < 2) continue;
1275
1276 // Find minimum distance between boundary vertices of the two groups
1277 double group_min_dist = std::numeric_limits<double>::infinity();
1278 for (int v1_idx : curve1) {
1279 if (V_combined[v1_idx].hasNaN()) continue;
1280 for (int v2_idx : curve2) {
1281 if (V_combined[v2_idx].hasNaN()) continue;
1282 double d = (V_combined[v1_idx] - V_combined[v2_idx]).norm();
1283 group_min_dist = std::min(group_min_dist, d);
1284 }
1285 }
1286
1287 // Only zip groups that are close enough
1288 if (group_min_dist > minDistValue) continue;
1289
1290 // Find the close boundary segment on curve1
1291 // For each vertex in curve1, compute distance to closest vertex in curve2
1292 std::vector<double> d12(curve1.size(), std::numeric_limits<double>::infinity());
1293 for (size_t ci = 0; ci < curve1.size(); ++ci) {
1294 if (V_combined[curve1[ci]].hasNaN()) continue;
1295 for (int v2_idx : curve2) {
1296 if (V_combined[v2_idx].hasNaN()) continue;
1297 double d = (V_combined[curve1[ci]] - V_combined[v2_idx]).norm();
1298 d12[ci] = std::min(d12[ci], d);
1299 }
1300 }
1301
1302 // Extract sub-curve of curve1 that is close to curve2
1303 std::vector<int> sub_curve1;
1304 for (size_t ci = 0; ci < curve1.size(); ++ci) {
1305 if (d12[ci] < minDistValue) {
1306 sub_curve1.push_back(curve1[ci]);
1307 }
1308 }
1309 if (sub_curve1.size() < 2) continue;
1310
1311 // Find corresponding sub-curve on curve2
1312 // For each vertex in sub_curve1, find closest vertex in curve2
1313 std::vector<size_t> closest_c2_indices;
1314 for (int v1_idx : sub_curve1) {
1315 double best_d = std::numeric_limits<double>::infinity();
1316 size_t best_j = 0;
1317 for (size_t j = 0; j < curve2.size(); ++j) {
1318 if (V_combined[curve2[j]].hasNaN()) continue;
1319 double d = (V_combined[v1_idx] - V_combined[curve2[j]]).norm();
1320 if (d < best_d) { best_d = d; best_j = j; }
1321 }
1322 closest_c2_indices.push_back(best_j);
1323 }
1324
1325 // Determine range of curve2 indices
1326 size_t c2_min = *std::min_element(closest_c2_indices.begin(), closest_c2_indices.end());
1327 size_t c2_max = *std::max_element(closest_c2_indices.begin(), closest_c2_indices.end());
1328
1329 std::vector<int> sub_curve2;
1330 for (size_t ci = c2_min; ci <= c2_max; ++ci) {
1331 sub_curve2.push_back(curve2[ci]);
1332 }
1333 if (sub_curve2.size() < 2) continue;
1334
1335 // Orient curve2 to match curve1 direction
1336 double d_start_start = (V_combined[sub_curve1.front()] - V_combined[sub_curve2.front()]).norm();
1337 double d_start_end = (V_combined[sub_curve1.front()] - V_combined[sub_curve2.back()]).norm();
1338 if (d_start_end < d_start_start) {
1339 std::reverse(sub_curve2.begin(), sub_curve2.end());
1340 }
1341
1342 // Trim endpoints: remove leading/trailing vertices if they make the gap worse
1343 while (sub_curve1.size() >= 2 && sub_curve2.size() >= 2) {
1344 double d_cur = (V_combined[sub_curve1.front()] - V_combined[sub_curve2.front()]).norm();
1345 double d_adv1 = (V_combined[sub_curve1[1]] - V_combined[sub_curve2.front()]).norm();
1346 double d_adv2 = (V_combined[sub_curve1.front()] - V_combined[sub_curve2[1]]).norm();
1347 if (d_adv1 < d_cur) { sub_curve1.erase(sub_curve1.begin()); }
1348 else if (d_adv2 < d_cur) { sub_curve2.erase(sub_curve2.begin()); }
1349 else break;
1350 }
1351 while (sub_curve1.size() >= 2 && sub_curve2.size() >= 2) {
1352 double d_cur = (V_combined[sub_curve1.back()] - V_combined[sub_curve2.back()]).norm();
1353 double d_adv1 = (V_combined[sub_curve1[sub_curve1.size()-2]] - V_combined[sub_curve2.back()]).norm();
1354 double d_adv2 = (V_combined[sub_curve1.back()] - V_combined[sub_curve2[sub_curve2.size()-2]]).norm();
1355 if (d_adv1 < d_cur) { sub_curve1.pop_back(); }
1356 else if (d_adv2 < d_cur) { sub_curve2.pop_back(); }
1357 else break;
1358 }
1359
1360 if (sub_curve1.size() < 2 || sub_curve2.size() < 2) continue;
1361
1362 // Zip the two sub-curves
1363 auto zipped = zipBoundaryCurves(sub_curve1, sub_curve2, V_combined);
1364 zip_faces.insert(zip_faces.end(), zipped.begin(), zipped.end());
1365 }
1366 }
1367
1368 size_t nZipFaces = zip_faces.size() / 3;
1369 if (nZipFaces > 0) {
1370 LOG_DEBUG << " Zipped " << nZipFaces << " new faces between boundaries";
1371 }
1372
1373 // ---- Step 5: Combine everything ----
1374 // Combined faces: cur_faces + next_faces_offset + zip_faces
1375 std::vector<int> combined_faces;
1376 combined_faces.insert(combined_faces.end(), cur_faces.begin(), cur_faces.end());
1377 combined_faces.insert(combined_faces.end(), next_faces_offset.begin(), next_faces_offset.end());
1378 combined_faces.insert(combined_faces.end(), zip_faces.begin(), zip_faces.end());
1379
1380 // Combined face colors
1381 std::vector<double> combined_faceColors;
1382 combined_faceColors.insert(combined_faceColors.end(), cur_faceColors.begin(), cur_faceColors.end());
1383 combined_faceColors.insert(combined_faceColors.end(), next_faceColors.begin(), next_faceColors.end());
1384 // Zip faces get average color
1385 if (!cur_faceColors.empty() && !next_faceColors.empty()) {
1386 double avg_col = 0.0;
1387 for (double c : cur_faceColors) avg_col += c;
1388 for (double c : next_faceColors) avg_col += c;
1389 avg_col /= (cur_faceColors.size() + next_faceColors.size());
1390 combined_faceColors.insert(combined_faceColors.end(), nZipFaces, avg_col);
1391 }
1392
1393 // Combined face pair indices
1394 std::vector<int> combined_facePairInds;
1395 combined_facePairInds.insert(combined_facePairInds.end(), cur_facePairInds.begin(), cur_facePairInds.end());
1396 combined_facePairInds.insert(combined_facePairInds.end(), next_faces.size() / 3, pair_order[ipair]);
1397 // Zip faces get a special pair index (nPairs + ipair, matching MATLAB)
1398 combined_facePairInds.insert(combined_facePairInds.end(), nZipFaces, static_cast<int>(nPairs + ipair));
1399
1400 // Combined Points3D and corrComb for all frames
1401 std::vector<Point3DFrame> combined_pts3d(nFrames);
1402 std::vector<std::vector<double>> combined_corrComb(nFrames);
1403 for (size_t f = 0; f < nFrames; ++f) {
1404 // Concatenate vertices: [current ; next]
1405 combined_pts3d[f].x = cur_pts3d[f].x;
1406 combined_pts3d[f].x.insert(combined_pts3d[f].x.end(), next.Points3D[f].x.begin(), next.Points3D[f].x.end());
1407 combined_pts3d[f].y = cur_pts3d[f].y;
1408 combined_pts3d[f].y.insert(combined_pts3d[f].y.end(), next.Points3D[f].y.begin(), next.Points3D[f].y.end());
1409 combined_pts3d[f].z = cur_pts3d[f].z;
1410 combined_pts3d[f].z.insert(combined_pts3d[f].z.end(), next.Points3D[f].z.begin(), next.Points3D[f].z.end());
1411
1412 // NaN out vertices not referenced by any face (MATLAB lines 112-121: V1r(~V1rlogic,:)=NaN)
1413 std::set<int> referenced_verts;
1414 for (size_t fi = 0; fi < combined_faces.size(); ++fi) {
1415 referenced_verts.insert(combined_faces[fi]);
1416 }
1417 size_t totalV = combined_pts3d[f].x.size();
1418 for (size_t vi = 0; vi < totalV; ++vi) {
1419 if (referenced_verts.find(static_cast<int>(vi)) == referenced_verts.end()) {
1420 combined_pts3d[f].x[vi] = std::nan("");
1421 combined_pts3d[f].y[vi] = std::nan("");
1422 combined_pts3d[f].z[vi] = std::nan("");
1423 }
1424 }
1425
1426 // Correlation
1427 if (f < cur_corrComb.size() && f < next.corrComb.size()) {
1428 combined_corrComb[f] = cur_corrComb[f];
1429 combined_corrComb[f].insert(combined_corrComb[f].end(), next.corrComb[f].begin(), next.corrComb[f].end());
1430 }
1431 }
1432
1433 // Combined point pair indices
1434 std::vector<int> combined_pointPairInds;
1435 combined_pointPairInds.insert(combined_pointPairInds.end(), cur_pointPairInds.begin(), cur_pointPairInds.end());
1436 combined_pointPairInds.insert(combined_pointPairInds.end(), nV2, pair_order[ipair]);
1437
1438 // Update working state
1439 cur_faces = std::move(combined_faces);
1440 cur_faceColors = std::move(combined_faceColors);
1441 cur_facePairInds = std::move(combined_facePairInds);
1442 cur_pts3d = std::move(combined_pts3d);
1443 cur_corrComb = std::move(combined_corrComb);
1444 cur_pointPairInds = std::move(combined_pointPairInds);
1445
1446 if (next.cameraPairInd.size() >= 2) {
1447 pairIndices[pair_idx * 2] = next.cameraPairInd[0];
1448 pairIndices[pair_idx * 2 + 1] = next.cameraPairInd[1];
1449 }
1450
1451 LOG_INFO << " Pair " << pair_order[ipair] << " stitched: "
1452 << cur_pts3d[0].x.size() << " points, "
1453 << cur_faces.size() / 3 << " faces";
1454 }
1455
1456 // ---- Step 6: Fill single-triangle holes ----
1457 {
1458 // Build combined V for boundary detection
1459 size_t totalV = cur_pts3d[0].x.size();
1460 std::vector<Eigen::Vector3d> V_final(totalV);
1461 for (size_t i = 0; i < totalV; ++i)
1462 V_final[i] = Eigen::Vector3d(cur_pts3d[0].x[i], cur_pts3d[0].y[i], cur_pts3d[0].z[i]);
1463
1464 auto final_bnd = computeMeshBoundary(cur_faces, V_final);
1465 auto final_groups = groupBoundaryEdges(final_bnd);
1466
1467 size_t filled = 0;
1468 for (const auto& group : final_groups) {
1469 // Extract edges for this group
1470 std::vector<int> edges_g;
1471 for (int eidx : group) {
1472 edges_g.push_back(final_bnd[eidx*2]);
1473 edges_g.push_back(final_bnd[eidx*2+1]);
1474 }
1475
1476 // If exactly 3 edges -> single triangle hole
1477 if (edges_g.size() == 6) { // 3 edges * 2 vertices
1478 std::set<int> verts_set;
1479 for (int v : edges_g) verts_set.insert(v);
1480 if (verts_set.size() == 3) {
1481 auto it = verts_set.begin();
1482 int v0 = *it++, v1 = *it++, v2 = *it;
1483 cur_faces.push_back(v0);
1484 cur_faces.push_back(v1);
1485 cur_faces.push_back(v2);
1486
1487 // Assign median pair index from adjacent faces
1488 int fill_pair_ind = 0;
1489 if (!cur_facePairInds.empty()) {
1490 fill_pair_ind = cur_facePairInds[cur_facePairInds.size() / 2];
1491 }
1492 cur_facePairInds.push_back(fill_pair_ind);
1493
1494 // Average face color
1495 double fill_color = 128.0;
1496 if (!cur_faceColors.empty()) {
1497 double sum = 0;
1498 for (double c : cur_faceColors) sum += c;
1499 fill_color = sum / cur_faceColors.size();
1500 }
1501 cur_faceColors.push_back(fill_color);
1502 filled++;
1503 }
1504 }
1505 }
1506 if (filled > 0) {
1507 LOG_DEBUG << " Filled " << filled << " single-triangle holes";
1508 }
1509 }
1510
1511 // ---- Step 7: Append non-stitched pairs ----
1512 std::set<int> stitched_pairs_set(pair_order.begin(), pair_order.end());
1513 for (size_t ip = 0; ip < nPairs; ++ip) {
1514 int pairId = static_cast<int>(ip + 1);
1515 if (stitched_pairs_set.count(pairId)) continue;
1516
1517 const auto& extra = all_pairs[ip];
1518 size_t voff = cur_pts3d[0].x.size();
1519 size_t extra_nFaces = extra.Faces.size() / 3;
1520 size_t extra_nVerts = extra.Points3D[0].x.size();
1521
1522 // Append faces with offset
1523 for (int idx : extra.Faces) {
1524 cur_faces.push_back(idx + static_cast<int>(voff));
1525 }
1526 cur_faceColors.insert(cur_faceColors.end(), extra.FaceColors.begin(), extra.FaceColors.end());
1527 cur_facePairInds.insert(cur_facePairInds.end(), extra_nFaces, pairId);
1528
1529 // Append points for all frames
1530 for (size_t f = 0; f < nFrames && f < extra.Points3D.size(); ++f) {
1531 cur_pts3d[f].x.insert(cur_pts3d[f].x.end(), extra.Points3D[f].x.begin(), extra.Points3D[f].x.end());
1532 cur_pts3d[f].y.insert(cur_pts3d[f].y.end(), extra.Points3D[f].y.begin(), extra.Points3D[f].y.end());
1533 cur_pts3d[f].z.insert(cur_pts3d[f].z.end(), extra.Points3D[f].z.begin(), extra.Points3D[f].z.end());
1534 if (f < extra.corrComb.size() && f < cur_corrComb.size()) {
1535 cur_corrComb[f].insert(cur_corrComb[f].end(), extra.corrComb[f].begin(), extra.corrComb[f].end());
1536 }
1537 }
1538 cur_pointPairInds.insert(cur_pointPairInds.end(), extra_nVerts, pairId);
1539
1540 if (extra.cameraPairInd.size() >= 2) {
1541 pairIndices[ip * 2] = extra.cameraPairInd[0];
1542 pairIndices[ip * 2 + 1] = extra.cameraPairInd[1];
1543 }
1544
1545 LOG_INFO << " Pair " << pairId << " appended (not stitched): "
1546 << extra_nVerts << " points, " << extra_nFaces << " faces";
1547 }
1548
1549 // ---- Build final DIC3Dcombined ----
1550 DIC3Dcombined result;
1551 result.Faces = std::move(cur_faces);
1552 result.FaceColors = std::move(cur_faceColors);
1553 result.FacePairInds = std::move(cur_facePairInds);
1554 result.Points3D = std::move(cur_pts3d);
1555 result.corrComb = std::move(cur_corrComb);
1556 result.PointPairInds = std::move(cur_pointPairInds);
1557 result.pairIndices = std::move(pairIndices);
1558
1559 // Recompute FaceCorrComb, FaceCentroids, Disp from the combined data
1560 size_t total_faces = result.Faces.size() / 3;
1561 size_t total_points = result.Points3D.empty() ? 0 : result.Points3D[0].x.size();
1562
1563 result.FaceCorrComb.resize(nFrames);
1564 result.FaceCentroids.resize(nFrames);
1565 result.Disp.DispVec.resize(nFrames);
1566 result.Disp.DispMgn.resize(nFrames);
1567
1568 for (size_t f = 0; f < nFrames; ++f) {
1569 // Face correlation (max of 3 vertices)
1570 result.FaceCorrComb[f].resize(total_faces, std::nan(""));
1571 for (size_t i = 0; i < total_faces; ++i) {
1572 int v0 = result.Faces[i*3], v1 = result.Faces[i*3+1], v2 = result.Faces[i*3+2];
1573 if (static_cast<size_t>(v0) < result.corrComb[f].size() &&
1574 static_cast<size_t>(v1) < result.corrComb[f].size() &&
1575 static_cast<size_t>(v2) < result.corrComb[f].size()) {
1576 result.FaceCorrComb[f][i] = std::max({result.corrComb[f][v0], result.corrComb[f][v1], result.corrComb[f][v2]});
1577 }
1578 }
1579
1580 // Face centroids (3 doubles per face)
1581 result.FaceCentroids[f].resize(total_faces * 3, std::nan(""));
1582 for (size_t i = 0; i < total_faces; ++i) {
1583 int v0 = result.Faces[i*3], v1 = result.Faces[i*3+1], v2 = result.Faces[i*3+2];
1584 if (static_cast<size_t>(v0) < total_points && static_cast<size_t>(v1) < total_points && static_cast<size_t>(v2) < total_points) {
1585 result.FaceCentroids[f][i*3+0] = (result.Points3D[f].x[v0]+result.Points3D[f].x[v1]+result.Points3D[f].x[v2])/3.0;
1586 result.FaceCentroids[f][i*3+1] = (result.Points3D[f].y[v0]+result.Points3D[f].y[v1]+result.Points3D[f].y[v2])/3.0;
1587 result.FaceCentroids[f][i*3+2] = (result.Points3D[f].z[v0]+result.Points3D[f].z[v1]+result.Points3D[f].z[v2])/3.0;
1588 }
1589 }
1590
1591 // Displacement from frame 0
1592 result.Disp.DispVec[f].resize(total_points * 3, std::nan(""));
1593 result.Disp.DispMgn[f].resize(total_points, std::nan(""));
1594 for (size_t k = 0; k < total_points; ++k) {
1595 double dx = result.Points3D[f].x[k] - result.Points3D[0].x[k];
1596 double dy = result.Points3D[f].y[k] - result.Points3D[0].y[k];
1597 double dz = result.Points3D[f].z[k] - result.Points3D[0].z[k];
1598 result.Disp.DispVec[f][k*3+0] = dx;
1599 result.Disp.DispVec[f][k*3+1] = dy;
1600 result.Disp.DispVec[f][k*3+2] = dz;
1601 result.Disp.DispMgn[f][k] = std::sqrt(dx*dx + dy*dy + dz*dz);
1602 }
1603 }
1604
1605 // Merge calibration and distortion data
1606 for (const auto& pair : all_pairs) {
1607 result.calibration.DLT_paths.push_back(pair.DLTpath);
1608 result.calibration.DLT_params.push_back(pair.DLTparameters);
1609 }
1610 for (const auto& pair : all_pairs) {
1611 result.distortion.distortion_models.push_back(pair.distortionModel);
1612 result.distortion.distortion_paths.push_back(pair.distortionPath);
1613 }
1614
1615 LOG_INFO << " Geometric stitching complete: "
1616 << total_points << " points, " << total_faces << " faces";
1617
1618 return result;
1619}
1620
1621} // namespace cppxdic
Lightweight, dependency-free logging facility for CPPxDIC.
#define LOG_DEBUG
Definition logging.h:169
#define LOG_ERROR
Definition logging.h:172
#define LOG_WARN
Definition logging.h:171
#define LOG_INFO
Definition logging.h:170
DataSerializer - Abstract base class for format-agnostic DIC data I/O.
DIC3Dcombined stitchPairsSimple(const std::vector< DIC3DpairResults > &all_pairs)
Simple append-based stitching for multiple stereo pairs.
static std::vector< T > filterByMask(const std::vector< T > &data, const std::vector< bool > &keep)
static std::vector< double > computeRaySurfaceDistance(const std::vector< Eigen::Vector3d > &V1, const std::vector< Eigen::Vector3d > &N1, const std::vector< int > &F2, const std::vector< Eigen::Vector3d > &V2)
std::vector< std::vector< int > > groupBoundaryEdges(const std::vector< int > &boundary_edges)
Group boundary edges into connected components.
static bool removeFacesContainingVertex(const std::vector< int > &F, std::vector< bool > &CT, int v_remove)
static std::vector< int > filterFaces(const std::vector< int > &faces, const std::vector< bool > &keep)
void setStitchRayAccel(bool enabled)
Enable/disable the AABB-hierarchy prefilter used by the overlap-removal ray casts.
std::vector< int > edgeListToCurve(const std::vector< int > &edges)
Convert an unordered list of edges into an ordered curve of vertex indices.
bool stitchRayAccel()
Current state of the ray-cast prefilter (see setStitchRayAccel).
std::vector< double > computeEdgeLengths(const std::vector< int > &faces, const std::vector< Eigen::Vector3d > &vertices)
Compute edge lengths for all triangles Matches MATLAB: patchEdgeLengths(F, V)
static double meanFiniteEdgeLength(const std::vector< int > &faces, const std::vector< Eigen::Vector3d > &vertices)
static double rayTriangleHit(const Eigen::Vector3d &O, const Eigen::Vector3d &D, const Eigen::Vector3d &A, const Eigen::Vector3d &B, const Eigen::Vector3d &C, double eps)
static std::vector< int > applyFaceMask(const std::vector< int > &F, const std::vector< bool > &CT)
static bool g_stitch_ray_bvh
static std::vector< Eigen::Vector3d > raySetTriangleIntersect(const std::vector< Eigen::Vector3d > &origins, const std::vector< Eigen::Vector3d > &dirs, const std::vector< int > &faces, const std::vector< Eigen::Vector3d > &vertices, double eps=1e-6)
DIC3Dcombined stitchPairsGeometric(const std::vector< DIC3DpairResults > &all_pairs, const std::vector< int > &pair_order, bool pair_forced=false)
Geometric stitching with overlap removal and boundary zipping Matches MATLAB's DIC3DsurfaceStitch....
std::vector< int > computeMeshBoundary(const std::vector< int > &faces, const std::vector< Eigen::Vector3d > &vertices)
Compute boundary edges of a triangular mesh Matches MATLAB: patchBoundary(F, V)
std::vector< int > zipBoundaryCurves(const std::vector< int > &curve1, const std::vector< int > &curve2, const std::vector< Eigen::Vector3d > &V_combined)
Zip two boundary curves together by creating a triangle strip.
std::pair< std::vector< bool >, std::vector< bool > > removeOverlapSurfaces(const std::vector< int > &faces1, const std::vector< int > &faces2, const std::vector< Eigen::Vector3d > &vertices1, const std::vector< Eigen::Vector3d > &vertices2, double min_gap, bool pair_forced=false)
Remove overlapping regions from two surfaces Matches MATLAB: removeOverlapSurface_temp_2018_10_30(F1,...
static std::vector< Eigen::Vector3d > computeVertexNormals(const std::vector< int > &faces, const std::vector< Eigen::Vector3d > &vertices)
std::vector< bool > findAllBoundaryFaces(const std::vector< int > &faces, const std::vector< int > &boundary_edges)
Remove faces where all 3 edges are on the boundary (degenerate boundary triangles).
std::vector< std::vector< std::string > > DLT_paths
std::vector< std::vector< std::vector< double > > > DLT_params
DIC 3D Combined Results Equivalent to MATLAB's DIC3Dcombined This represents stitched results from mu...
std::vector< int > FacePairInds
DistortionData distortion
std::vector< int > PointPairInds
std::vector< Point3DFrame > Points3D
std::vector< int > pairIndices
std::vector< double > FaceColors
std::vector< std::vector< double > > FaceCorrComb
std::vector< std::vector< double > > corrComb
CalibrationData calibration
std::vector< int > Faces
std::vector< std::vector< double > > FaceCentroids
std::vector< std::vector< double > > DispVec
std::vector< std::vector< double > > DispMgn
std::vector< std::vector< std::string > > distortion_models
std::vector< std::vector< std::string > > distortion_paths
std::vector< Eigen::Vector3d > f_hi
int right
std::vector< Eigen::Vector3d > f_lo
int first
std::vector< int > order
std::vector< Node > nodes
Eigen::Vector3d hi
Eigen::Vector3d lo
int count
std::vector< Eigen::Vector3d > f_c