Skip to content

Commit 940bb23

Browse files
committed
Fix shared boundary donut and contour correspondence issues
- MeshUtils: replace fragile is_clockwise atan2 test with summed signed-area vector (uses all vertices, immune to branch-cut flips); rotate boundary loops to start at canonical max-Y vertex for consistent ordering across subjects. - ContourDomain: return length² from GetSurfaceArea so contours participate in sampling-scale auto-scaling at a magnitude comparable to meshes. - SamplingFunction: apply auto-scale uniformly; contours no longer require special-case skipping. - GroomParameters: skip phantom shared-boundary entry on migration when domain names are empty. - Regenerate shared boundary test ground truth for canonical start vertex.
1 parent 9f27f3d commit 940bb23

File tree

6 files changed

+61
-16
lines changed

6 files changed

+61
-16
lines changed

Libs/Groom/GroomParameters.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,9 @@ std::vector<GroomParameters::SharedBoundary> GroomParameters::get_shared_boundar
589589
boundary.second_domain =
590590
std::string(params_.get(Keys::SHARED_BOUNDARY_SECOND_DOMAIN, Defaults::shared_boundary_second_domain));
591591
boundary.tolerance = params_.get(Keys::SHARED_BOUNDARY_TOLERANCE, Defaults::shared_boundary_tolerance);
592-
boundaries.push_back(boundary);
592+
if (!boundary.first_domain.empty() && !boundary.second_domain.empty()) {
593+
boundaries.push_back(boundary);
594+
}
593595

594596
// Migrate to new format
595597
set_shared_boundaries(boundaries);

Libs/Mesh/MeshUtils.cpp

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,20 +266,31 @@ int MeshUtils::findReferenceMesh(std::vector<Mesh>& meshes, int random_subset_si
266266
*/
267267

268268
//---------------------------------------------------------------------------
269+
// Robust orientation test: compute the loop's signed-area vector by summing edge
270+
// cross products. The dominant axis of this vector gives the loop's normal; its sign
271+
// determines orientation. Uses ALL vertices, so it is insensitive to where loop[0]
272+
// happens to start and does not suffer from atan2 branch-cut flips.
269273
static bool is_clockwise(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, const std::vector<int>& loop) {
270274
Eigen::RowVector3d centroid{0.0, 0.0, 0.0};
271275
for (const auto& i : loop) {
272276
centroid += V.row(i);
273277
}
274278
centroid /= loop.size();
275279

276-
// todo this is arbitrary and works for the peanut data and initial tests on LA+Septum data
277-
// it enforces a consistent ordering in the boundary loop
278-
const auto v0 = V.row(loop[0]) - centroid;
279-
const auto v1 = V.row(loop[1]) - centroid;
280-
const double angle0 = atan2(v0.z(), v0.y());
281-
const double angle1 = atan2(v1.z(), v1.y());
282-
return angle0 > angle1;
280+
Eigen::RowVector3d area_vec{0.0, 0.0, 0.0};
281+
for (size_t i = 0; i < loop.size(); i++) {
282+
const Eigen::RowVector3d v0 = V.row(loop[i]) - centroid;
283+
const Eigen::RowVector3d v1 = V.row(loop[(i + 1) % loop.size()]) - centroid;
284+
area_vec += v0.cross(v1);
285+
}
286+
287+
// Use the dominant axis of the area vector as the reference normal. Calling "clockwise"
288+
// the case where the area vector points in the negative dominant-axis direction yields
289+
// consistent results across samples that share the same boundary plane.
290+
int max_axis = 0;
291+
if (std::abs(area_vec[1]) > std::abs(area_vec[max_axis])) max_axis = 1;
292+
if (std::abs(area_vec[2]) > std::abs(area_vec[max_axis])) max_axis = 2;
293+
return area_vec[max_axis] < 0;
283294
}
284295

285296
//---------------------------------------------------------------------------
@@ -293,7 +304,32 @@ Mesh MeshUtils::extract_boundary_loop(Mesh mesh) {
293304
throw std::runtime_error("Expected at least one boundary loop in the mesh");
294305
}
295306

296-
const auto& loop = loops[0];
307+
auto loop = loops[0]; // copy so we can rotate it to a canonical start vertex
308+
309+
// Rotate the loop so it always starts at a canonical vertex (the one with the
310+
// largest Y coordinate; lexicographic tiebreakers on Z then X). igl::boundary_loop
311+
// returns loops starting at an arbitrary vertex, which produces per-subject
312+
// rotational offsets that destroy inter-subject correspondence on contour domains.
313+
{
314+
size_t canonical = 0;
315+
for (size_t i = 1; i < loop.size(); i++) {
316+
const double cur_y = V(loop[i], 1);
317+
const double cur_z = V(loop[i], 2);
318+
const double cur_x = V(loop[i], 0);
319+
const double best_y = V(loop[canonical], 1);
320+
const double best_z = V(loop[canonical], 2);
321+
const double best_x = V(loop[canonical], 0);
322+
if (cur_y > best_y ||
323+
(cur_y == best_y && cur_z > best_z) ||
324+
(cur_y == best_y && cur_z == best_z && cur_x > best_x)) {
325+
canonical = i;
326+
}
327+
}
328+
if (canonical != 0) {
329+
std::rotate(loop.begin(), loop.begin() + canonical, loop.end());
330+
}
331+
}
332+
297333
const auto is_cw = is_clockwise(V, F, loop);
298334

299335
auto pts = vtkSmartPointer<vtkPoints>::New();

Libs/Optimize/Domain/ContourDomain.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,13 @@ int ContourDomain::NumberOfLinesIncidentOnPoint(int i) const {
344344
}
345345

346346
void ContourDomain::ComputeAvgEdgeLength() {
347-
const double total_length = std::accumulate(lines_.begin(), lines_.end(),
347+
total_length_ = std::accumulate(lines_.begin(), lines_.end(),
348348
0.0, [&](double s, const vtkSmartPointer<vtkLine> &line) {
349349
const auto pt_a = GetPoint(line->GetPointId(0));
350350
const auto pt_b = GetPoint(line->GetPointId(1));
351351
return s + (pt_a - pt_b).norm();
352352
});
353-
avg_edge_length_ = total_length / lines_.size();
353+
avg_edge_length_ = total_length_ / lines_.size();
354354
}
355355

356356
}

Libs/Optimize/Domain/ContourDomain.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ class ContourDomain : public ParticleDomain {
8686
}
8787

8888
double GetSurfaceArea() const override {
89-
// TODO: Implement something analogous for scaling purposes
90-
return 1.0;
89+
// Return length² as an area-equivalent for a contour so it participates in the
90+
// sampling-scale auto-scaling. For a circle of radius r this is (2πr)² = 4π²r²,
91+
// which is π times the matching sphere's surface area — comparable magnitude so
92+
// the scale factor doesn't crush the contour gradient.
93+
return total_length_ * total_length_;
9194
}
9295

9396
void DeleteImages() override {
@@ -132,6 +135,7 @@ class ContourDomain : public ParticleDomain {
132135
mutable double geo_lq_dist_ = -1;
133136

134137
double avg_edge_length_{0.0};
138+
double total_length_{0.0};
135139

136140
void ComputeBounds();
137141
void ComputeGeodesics(vtkSmartPointer<vtkPolyData> poly_data);

Libs/Optimize/Function/SamplingFunction.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,10 @@ SamplingFunction::VectorType SamplingFunction::evaluate(unsigned int idx, unsign
250250

251251
gradE = gradE / m_avgKappa;
252252

253-
// Apply sampling scale if enabled
253+
// Apply sampling scale if enabled. Contour domains return length² from GetSurfaceArea,
254+
// giving a scale factor comparable in magnitude to the equivalent mesh's — without this,
255+
// contour particles would move at native gradient magnitudes (~1000× stronger than scaled
256+
// mesh particles), causing them to spin rapidly around the loop instead of settling.
254257
if (m_SamplingScale) {
255258
double scale_factor = 1.0;
256259

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:06fb0a7bfbf2fe03d29f855a7156adacd2bd46596f26a36409df0531995daff6
3-
size 2851
2+
oid sha256:95657cfe31442ba0bdeaeb7116752883c8dae291077d5e0c5cf95cee52c41a23
3+
size 4074

0 commit comments

Comments
 (0)