Skip to content

Commit 75a5215

Browse files
authored
Merge pull request #2535 from SCIInstitute/amorris/fix-shared-boundary
Fix #2534 - shared boundary issue: skip extract-largest-component for shared boundary domains
2 parents 4fae05f + 072aa5f commit 75a5215

12 files changed

Lines changed: 322 additions & 27 deletions

File tree

Libs/Groom/Groom.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,10 @@ bool Groom::mesh_pipeline(std::shared_ptr<Subject> subject, size_t domain) {
385385

386386
//---------------------------------------------------------------------------
387387
bool Groom::run_mesh_pipeline(Mesh& mesh, GroomParameters params, const std::string& filename) {
388-
// Repair mesh: triangulate, extract largest component, clean, fix non-manifold, remove zero-area triangles
389-
mesh = Mesh(MeshUtils::repair_mesh(mesh.getVTKMesh()));
388+
// Repair mesh: triangulate, clean, fix non-manifold, remove zero-area triangles
389+
// Skip extract-largest-component for shared boundary domains to avoid removing fragments at the cut surface
390+
bool extract_largest = !params.is_shared_boundary_domain();
391+
mesh = Mesh(MeshUtils::repair_mesh(mesh.getVTKMesh(), extract_largest));
390392

391393
if (params.get_fill_mesh_holes_tool()) {
392394
mesh.fillHoles();

Libs/Groom/GroomParameters.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,19 @@ bool GroomParameters::get_shared_boundaries_enabled() {
544544
//---------------------------------------------------------------------------
545545
void GroomParameters::set_shared_boundaries_enabled(bool enabled) { params_.set(Keys::SHARED_BOUNDARY, enabled); }
546546

547+
//---------------------------------------------------------------------------
548+
bool GroomParameters::is_shared_boundary_domain() {
549+
if (!get_shared_boundaries_enabled()) {
550+
return false;
551+
}
552+
for (const auto& boundary : get_shared_boundaries()) {
553+
if (boundary.first_domain == domain_name_ || boundary.second_domain == domain_name_) {
554+
return true;
555+
}
556+
}
557+
return false;
558+
}
559+
547560
//---------------------------------------------------------------------------
548561
std::vector<GroomParameters::SharedBoundary> GroomParameters::get_shared_boundaries() {
549562
std::vector<SharedBoundary> boundaries;
@@ -576,7 +589,9 @@ std::vector<GroomParameters::SharedBoundary> GroomParameters::get_shared_boundar
576589
boundary.second_domain =
577590
std::string(params_.get(Keys::SHARED_BOUNDARY_SECOND_DOMAIN, Defaults::shared_boundary_second_domain));
578591
boundary.tolerance = params_.get(Keys::SHARED_BOUNDARY_TOLERANCE, Defaults::shared_boundary_tolerance);
579-
boundaries.push_back(boundary);
592+
if (!boundary.first_domain.empty() && !boundary.second_domain.empty()) {
593+
boundaries.push_back(boundary);
594+
}
580595

581596
// Migrate to new format
582597
set_shared_boundaries(boundaries);

Libs/Groom/GroomParameters.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ class GroomParameters {
151151
bool get_shared_boundaries_enabled();
152152
void set_shared_boundaries_enabled(bool enabled);
153153

154+
//! Check if the current domain participates in any shared boundary
155+
bool is_shared_boundary_domain();
156+
154157
std::vector<SharedBoundary> get_shared_boundaries();
155158
void set_shared_boundaries(const std::vector<SharedBoundary>& boundaries);
156159
void add_shared_boundary(const std::string& first_domain, const std::string& second_domain, double tolerance);

Libs/Mesh/MeshUtils.cpp

Lines changed: 65 additions & 14 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();
@@ -389,6 +425,12 @@ static std::tuple<Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXd, Eigen::Matr
389425
Eigen::MatrixXi out_F;
390426
Eigen::MatrixXd rem_V;
391427
Eigen::MatrixXi rem_F;
428+
429+
// If either mesh is empty, there can be no shared surface
430+
if (is_empty(src_V, src_F) || is_empty(other_V, other_F)) {
431+
return std::make_tuple(out_V, out_F, src_V, src_F);
432+
}
433+
392434
igl::AABB<Eigen::MatrixXd, 3> tree;
393435
tree.init(other_V, other_F);
394436

@@ -482,6 +524,10 @@ std::array<Mesh, 3> MeshUtils::shared_boundary_extractor(const Mesh& mesh_l, con
482524
V_r = mesh_r.points();
483525
F_r = mesh_r.faces();
484526

527+
if (is_empty(V_l, F_l) || is_empty(V_r, F_r)) {
528+
throw std::runtime_error("Input mesh is empty. Cannot extract shared boundary from empty meshes");
529+
}
530+
485531
Eigen::MatrixXd shared_V_l, shared_V_r, rem_V_l, rem_V_r;
486532
Eigen::MatrixXi shared_F_l, shared_F_r, rem_F_l, rem_F_r;
487533
std::tie(shared_V_l, shared_F_l, rem_V_l, rem_F_l) = find_shared_surface(V_l, F_l, V_r, F_r, tol);
@@ -1073,18 +1119,23 @@ vtkSmartPointer<vtkPolyData> MeshUtils::recreate_mesh(vtkSmartPointer<vtkPolyDat
10731119
}
10741120

10751121
//---------------------------------------------------------------------------
1076-
vtkSmartPointer<vtkPolyData> MeshUtils::repair_mesh(vtkSmartPointer<vtkPolyData> mesh) {
1122+
vtkSmartPointer<vtkPolyData> MeshUtils::repair_mesh(vtkSmartPointer<vtkPolyData> mesh, bool extract_largest) {
10771123
auto triangle_filter = vtkSmartPointer<vtkTriangleFilter>::New();
10781124
triangle_filter->SetInputData(mesh);
10791125
triangle_filter->PassLinesOff();
10801126
triangle_filter->Update();
10811127

1082-
auto connectivity = vtkSmartPointer<vtkPolyDataConnectivityFilter>::New();
1083-
connectivity->SetInputConnection(triangle_filter->GetOutputPort());
1084-
connectivity->SetExtractionModeToLargestRegion();
1085-
connectivity->Update();
1128+
vtkSmartPointer<vtkPolyData> triangulated = triangle_filter->GetOutput();
1129+
1130+
if (extract_largest) {
1131+
auto connectivity = vtkSmartPointer<vtkPolyDataConnectivityFilter>::New();
1132+
connectivity->SetInputData(triangulated);
1133+
connectivity->SetExtractionModeToLargestRegion();
1134+
connectivity->Update();
1135+
triangulated = connectivity->GetOutput();
1136+
}
10861137

1087-
auto cleaned = MeshUtils::clean_mesh(connectivity->GetOutput());
1138+
auto cleaned = MeshUtils::clean_mesh(triangulated);
10881139

10891140
auto fixed = Mesh(cleaned).fixNonManifold();
10901141

Libs/Mesh/MeshUtils.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class MeshUtils {
8686
/// Recreate mesh, dropping deleted cells
8787
static vtkSmartPointer<vtkPolyData> recreate_mesh(vtkSmartPointer<vtkPolyData> mesh);
8888

89-
/// Repair mesh: triangulate, extract largest component, clean, fix non-manifold, remove zero-area triangles
90-
static vtkSmartPointer<vtkPolyData> repair_mesh(vtkSmartPointer<vtkPolyData> mesh);
89+
/// Repair mesh: triangulate, optionally extract largest component, clean, fix non-manifold, remove zero-area triangles
90+
static vtkSmartPointer<vtkPolyData> repair_mesh(vtkSmartPointer<vtkPolyData> mesh, bool extract_largest = true);
9191
};
9292
} // namespace shapeworks

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

Libs/Optimize/OptimizeParameters.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ std::vector<std::vector<itk::Point<double>>> OptimizeParameters::get_initial_poi
328328
for (auto s : subjects) {
329329
if (s->is_fixed()) {
330330
count++;
331+
if (d >= s->get_world_particle_filenames().size()) {
332+
throw std::runtime_error("Subject " + s->get_display_name() + " does not have enough world particle files");
333+
}
331334
// read the world points that are in the shared coordinate space
332335
auto filename = s->get_world_particle_filenames()[d];
333336
auto particles = read_particles_as_vector(filename);

Studio/Optimize/OptimizeTool.cpp

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
// qt
44
#include <QFileDialog>
5+
#include <QFontMetrics>
56
#include <QIntValidator>
7+
#include <QLabel>
68
#include <QMessageBox>
9+
#include <QResizeEvent>
710
#include <QThread>
811
#include <QTimer>
912

@@ -22,6 +25,47 @@
2225

2326
using namespace shapeworks;
2427

28+
namespace {
29+
// QLabel that elides its text with "..." when it's narrower than the text.
30+
// The full text is kept as a tooltip so the user can see it on hover.
31+
// sizeHint() reports the full-text width so the layout gives the label its natural
32+
// space when available; elision only happens when the layout must shrink it.
33+
class ElidedLabel : public QLabel {
34+
public:
35+
explicit ElidedLabel(const QString& text, QWidget* parent = nullptr) : QLabel(parent), full_text_(text) {
36+
setToolTip(text);
37+
updateElidedText();
38+
}
39+
40+
QSize sizeHint() const override {
41+
QFontMetrics metrics(font());
42+
return QSize(metrics.horizontalAdvance(full_text_) + 4, QLabel::sizeHint().height());
43+
}
44+
45+
QSize minimumSizeHint() const override {
46+
// Minimum: enough for ellipsis plus a couple characters so the label can shrink
47+
// further if the panel is very narrow, but not to zero width.
48+
QFontMetrics metrics(font());
49+
return QSize(metrics.horizontalAdvance(QStringLiteral("X...")), QLabel::minimumSizeHint().height());
50+
}
51+
52+
protected:
53+
void resizeEvent(QResizeEvent* event) override {
54+
QLabel::resizeEvent(event);
55+
updateElidedText();
56+
}
57+
58+
private:
59+
void updateElidedText() {
60+
QFontMetrics metrics(font());
61+
QString elided = metrics.elidedText(full_text_, Qt::ElideRight, width());
62+
QLabel::setText(elided);
63+
}
64+
65+
QString full_text_;
66+
};
67+
} // namespace
68+
2569
//---------------------------------------------------------------------------
2670
OptimizeTool::OptimizeTool(Preferences& prefs, Telemetry& telemetry) : preferences_(prefs), telemetry_(telemetry) {
2771
ui_ = new Ui_OptimizeTool;
@@ -473,6 +517,8 @@ void OptimizeTool::setup_domain_boxes() {
473517
QLineEdit* box = new QLineEdit(this);
474518
last_box = box;
475519
box->setAlignment(Qt::AlignHCenter);
520+
box->setMinimumWidth(50);
521+
box->setMaximumWidth(100);
476522
box->setValidator(above_zero);
477523
box->setText(ui_->number_of_particles->text());
478524
connect(box, &QLineEdit::textChanged, this, &OptimizeTool::update_run_button);
@@ -482,7 +528,7 @@ void OptimizeTool::setup_domain_boxes() {
482528
} else {
483529
auto domain_names = session_->get_project()->get_domain_names();
484530
for (int i = 0; i < domain_names.size(); i++) {
485-
auto label = new QLabel(QString::fromStdString(domain_names[i]), this);
531+
auto label = new ElidedLabel(QString::fromStdString(domain_names[i]), this);
486532
label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
487533
grid->addWidget(label, i, 1);
488534
domain_grid_widgets_.push_back(label);

0 commit comments

Comments
 (0)