Skip to content

Commit 8a95fbf

Browse files
authored
Merge pull request #2515 from SCIInstitute/amorris/2514-multi-domain
Fix #2514 - Multi-domain overlap when pre-aligned data skips grooming alignment
2 parents 2cbd705 + 1def474 commit 8a95fbf

9 files changed

Lines changed: 122 additions & 3 deletions

File tree

Libs/Analyze/Analyze.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "Analyze.h"
22

3+
#include <Groom/GroomParameters.h>
34
#include <Logging.h>
45
#include <MeshWarper.h>
56
#include <Particles/ParticleNormalEvaluation.h>
@@ -443,6 +444,33 @@ bool Analyze::update_shapes() {
443444
shapes_.push_back(shape);
444445
}
445446

447+
// Compute mean groomed centroid per domain across all shapes.
448+
// Only applies when grooming alignment was NOT performed (pre-aligned data).
449+
// This restores world-space positioning after Procrustes centers everything to the origin.
450+
GroomParameters groom_params(project_);
451+
bool has_grooming_alignment = groom_params.get_alignment_enabled() && !groom_params.get_skip_grooming();
452+
if (!has_grooming_alignment) {
453+
unsigned int num_domains = domain_names.size();
454+
std::vector<Eigen::Vector3d> centroid_sum(num_domains, Eigen::Vector3d::Zero());
455+
int centroid_count = 0;
456+
for (auto& shape : shapes_) {
457+
auto centroids = shape->get_groomed_centroids();
458+
for (unsigned int d = 0; d < num_domains && d < centroids.size(); d++) {
459+
centroid_sum[d] += centroids[d];
460+
}
461+
centroid_count++;
462+
}
463+
if (centroid_count > 0) {
464+
std::vector<Eigen::Vector3d> mean_centroids(num_domains);
465+
for (unsigned int d = 0; d < num_domains; d++) {
466+
mean_centroids[d] = centroid_sum[d] / centroid_count;
467+
}
468+
for (auto& shape : shapes_) {
469+
shape->set_groomed_centroids(mean_centroids);
470+
}
471+
}
472+
}
473+
446474
SW_DEBUG("Successfully loaded shapes");
447475

448476
return true;

Libs/Analyze/Particles.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ void Particles::set_procrustes_transforms(const std::vector<vtkSmartPointer<vtkT
163163
procrustes_transforms_ = transforms;
164164
}
165165

166+
//---------------------------------------------------------------------------
167+
void Particles::set_groomed_centroids(const std::vector<Eigen::Vector3d>& centroids) {
168+
groomed_centroids_ = centroids;
169+
}
170+
166171
//---------------------------------------------------------------------------
167172
Eigen::VectorXd Particles::get_difference_vectors(const Particles& other) const {
168173
auto combined = get_combined_global_particles();
@@ -178,6 +183,20 @@ void Particles::transform_global_particles() {
178183
transformed_global_particles_.clear();
179184
if (!transform_) {
180185
transformed_global_particles_ = global_particles_;
186+
187+
// Apply groomed mesh centroid offsets to restore world-space positioning.
188+
// This only applies when transform_ is null (local alignment), where global_particles_
189+
// are Procrustes-centered at the origin. When grooming alignment was performed,
190+
// centroids are near zero (no effect). When grooming was skipped, this restores the
191+
// original spatial positions, preventing multi-domain shapes from overlapping at the origin.
192+
for (int d = 0; d < transformed_global_particles_.size() && d < groomed_centroids_.size(); d++) {
193+
Eigen::VectorXd& eigen = transformed_global_particles_[d];
194+
for (size_t i = 0; i < eigen.size(); i += 3) {
195+
eigen[i] += groomed_centroids_[d][0];
196+
eigen[i + 1] += groomed_centroids_[d][1];
197+
eigen[i + 2] += groomed_centroids_[d][2];
198+
}
199+
}
181200
} else {
182201
for (int d = 0; d < local_particles_.size(); d++) {
183202
Eigen::VectorXd eigen = local_particles_[d];
@@ -235,6 +254,7 @@ void Particles::transform_global_particles() {
235254
transformed_global_particles_.push_back(eigen);
236255
}
237256
}
257+
238258
}
239259

240260
//---------------------------------------------------------------------------

Libs/Analyze/Particles.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class Particles {
4949
void set_transform(vtkSmartPointer<vtkTransform> transform);
5050
void set_procrustes_transforms(const std::vector<vtkSmartPointer<vtkTransform>>& transforms);
5151
void set_alignment_type(int alignment);
52+
void set_groomed_centroids(const std::vector<Eigen::Vector3d>& centroids);
5253

5354
Eigen::VectorXd get_difference_vectors(const Particles& other) const;
5455

@@ -72,6 +73,7 @@ class Particles {
7273

7374
vtkSmartPointer<vtkTransform> transform_;
7475
std::vector<vtkSmartPointer<vtkTransform>> procrustes_transforms_;
76+
std::vector<Eigen::Vector3d> groomed_centroids_;
7577
int alignment_type_ = -3; // not a valid value
7678

7779
};

Libs/Analyze/Shape.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,26 @@ vtkSmartPointer<vtkTransform> Shape::get_groomed_transform(int domain) {
773773
return nullptr;
774774
}
775775

776+
//---------------------------------------------------------------------------
777+
std::vector<Eigen::Vector3d> Shape::get_groomed_centroids() {
778+
std::vector<Eigen::Vector3d> centroids;
779+
auto meshes = get_groomed_meshes(true);
780+
for (int i = 0; i < meshes.meshes().size(); i++) {
781+
auto mesh = meshes.meshes()[i];
782+
if (mesh && mesh->get_poly_data() && mesh->get_poly_data()->GetNumberOfPoints() > 0) {
783+
auto com = vtkSmartPointer<vtkCenterOfMass>::New();
784+
com->SetInputData(mesh->get_poly_data());
785+
com->Update();
786+
double center[3];
787+
com->GetCenter(center);
788+
centroids.push_back(Eigen::Vector3d(center[0], center[1], center[2]));
789+
} else {
790+
centroids.push_back(Eigen::Vector3d::Zero());
791+
}
792+
}
793+
return centroids;
794+
}
795+
776796
//---------------------------------------------------------------------------
777797
vtkSmartPointer<vtkTransform> Shape::get_procrustes_transform(int domain) {
778798
auto transforms = subject_->get_procrustes_transforms();
@@ -817,6 +837,11 @@ void Shape::set_particle_transform(vtkSmartPointer<vtkTransform> transform) {
817837
particles_.set_transform(transform);
818838
}
819839

840+
//---------------------------------------------------------------------------
841+
void Shape::set_groomed_centroids(const std::vector<Eigen::Vector3d>& centroids) {
842+
particles_.set_groomed_centroids(centroids);
843+
}
844+
820845
//---------------------------------------------------------------------------
821846
void Shape::set_alignment_type(int alignment) { particles_.set_alignment_type(alignment); }
822847

Libs/Analyze/Shape.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ class Shape {
107107
//! Set the particle transform (alignment)
108108
void set_particle_transform(vtkSmartPointer<vtkTransform> transform);
109109

110+
//! Set per-domain centroid offsets for world-space positioning
111+
void set_groomed_centroids(const std::vector<Eigen::Vector3d>& centroids);
112+
110113
//! Set the alignment type
111114
void set_alignment_type(int alignment);
112115

@@ -158,6 +161,9 @@ class Shape {
158161

159162
vtkSmartPointer<vtkTransform> get_groomed_transform(int domain = 0);
160163

164+
//! Get the centroid of each groomed mesh domain
165+
std::vector<Eigen::Vector3d> get_groomed_centroids();
166+
161167
vtkSmartPointer<vtkTransform> get_procrustes_transform(int domain = 0);
162168
std::vector<vtkSmartPointer<vtkTransform>> get_procrustes_transforms();
163169

Studio/Analysis/AnalysisTool.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <Job/ParticleNormalEvaluationJob.h>
1313
#include <Job/StatsGroupLDAJob.h>
1414
#include <Libs/Application/Job/PythonWorker.h>
15+
#include <Groom/GroomParameters.h>
1516
#include <Logging.h>
1617
#include <QMeshWarper.h>
1718
#include <Shape.h>
@@ -1205,6 +1206,38 @@ void AnalysisTool::reset_stats() {
12051206
evals_ready_ = false;
12061207
stats_ready_ = false;
12071208

1209+
// Compute mean groomed centroid per domain to restore world-space positioning.
1210+
// Only applies when grooming alignment was NOT performed (i.e., pre-aligned data).
1211+
// When grooming alignment was performed, Procrustes centering to origin is correct
1212+
// and we don't need to restore original positions.
1213+
if (session_ && session_->particles_present()) {
1214+
auto shapes = session_->get_non_excluded_shapes();
1215+
GroomParameters groom_params(session_->get_project());
1216+
bool has_grooming_alignment = groom_params.get_alignment_enabled() && !groom_params.get_skip_grooming();
1217+
if (!has_grooming_alignment) {
1218+
auto domain_names = session_->get_project()->get_domain_names();
1219+
unsigned int num_domains = domain_names.size();
1220+
std::vector<Eigen::Vector3d> centroid_sum(num_domains, Eigen::Vector3d::Zero());
1221+
int centroid_count = 0;
1222+
for (auto& shape : shapes) {
1223+
auto centroids = shape->get_groomed_centroids();
1224+
for (unsigned int d = 0; d < num_domains && d < centroids.size(); d++) {
1225+
centroid_sum[d] += centroids[d];
1226+
}
1227+
centroid_count++;
1228+
}
1229+
if (centroid_count > 0) {
1230+
std::vector<Eigen::Vector3d> mean_centroids(num_domains);
1231+
for (unsigned int d = 0; d < num_domains; d++) {
1232+
mean_centroids[d] = centroid_sum[d] / centroid_count;
1233+
}
1234+
for (auto& shape : shapes) {
1235+
shape->set_groomed_centroids(mean_centroids);
1236+
}
1237+
}
1238+
}
1239+
}
1240+
12081241
ui_->pca_scalar_combo->clear();
12091242
if (session_) {
12101243
for (const auto& feature : session_->get_project()->get_feature_names()) {

docs/studio/multiple-domains.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@ In the presence of multiple anatomies, there are multiple alignment strategies t
5151
Below is an example of these four options with a pelvis and femur model.
5252

5353
<p><video src="https://sci.utah.edu/~shapeworks/doc-resources/mp4s/multiple-domains-mixed-types.mp4" autoplay muted loop controls style="width:100%"></p>
54+
55+
For a detailed explanation of alignment options, Multi-Level Component Analysis (MCA), their interactions, and how they affect group p-values, see [Multi-Domain Reference Frames](multi-domain-analysis-reference-frames.md).

docs/studio/studio-analyze.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@ The PCA tab of the View panel shows reconstructed shapes (surface meshes) along
7373
The PCA tab of the View panel shows options to select modes of variation in different subspaces when a multiple domain shape model is loaded:
7474
![ShapeWorks Studio Analysis View Panel PCA Display for Multiple-Domain Shape Model](../img/studio/studio_analyze_view_pca_multiple_domain.png)
7575

76-
`Shape and Relative Pose` - Selecting this option shows reconstructed shapes and it's eigenvalue and lambda, along ordinary PCA modes of variation. PCA is done in the shared space of the multi-object shape structure and thus the shsape and pose variations are entangled here.
76+
`Shape and Relative Pose` - Selecting this option shows reconstructed shapes and its eigenvalue and lambda along ordinary PCA modes of variation. PCA is done in the shared space of the multi-object shape structure and thus shape and pose variations are entangled here.
7777

78-
`Shape` - Selecting this option shows reconstructed shapes and it's eigenvalue and lambda, along only morphological modes of variation. Multi-Level Component Analysis is done in the shape subspace (within-object) of the multi-object shape structure. Shape and pose variations are disentangled here and we only see morphological changes of each object in the shape structure.
78+
`Shape` - Selecting this option shows reconstructed shapes and its eigenvalue and lambda along morphological modes of variation. Multi-Level Component Analysis subtracts each domain's centroid per subject, removing translational pose differences. Note that rotational pose differences remain in the shape component.
7979

80-
`Relative Pose` - Selecting this option shows reconstructed shapes and it's eigenvalue and lambda, along only relative pose modes of variation. Multi-Level Component Analysis is done in the relative pose subspace (between-objects) of the multi-object shape structure. Shape and pose variations are disentangled here and we only see alignment changes between the objects in the multi-object shape structure.
80+
`Relative Pose` - Selecting this option shows reconstructed shapes and its eigenvalue and lambda along relative pose modes of variation. Multi-Level Component Analysis keeps only per-domain centroids, showing translational relationships between domains. Note that rotational pose is not captured by this mode.
81+
82+
For a detailed explanation of these modes, their limitations, and how they interact with alignment settings, see [Multi-Domain Reference Frames](multi-domain-analysis-reference-frames.md).
8183

8284
### Show Difference to Mean
8385

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ nav:
4949
- 'Surface Reconstruction': 'studio/surface-reconstruction.md'
5050
- 'DeepSSM Module': 'studio/deepssm-in-studio.md'
5151
- 'Multiple Domains SSM': 'studio/multiple-domains.md'
52+
- 'Multi-Domain Reference Frames': 'studio/multi-domain-analysis-reference-frames.md'
5253
- 'Shared Boundaries': 'studio/studio-shared-boundary.md'
5354
- 'Segmentation Tool': 'studio/segmentation-tool.md'
5455
- 'AI Assisted Segmentation': 'studio/ai-assisted-segmentation.md'

0 commit comments

Comments
 (0)