Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Libs/Analyze/Analyze.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "Analyze.h"

#include <Groom/GroomParameters.h>
#include <Logging.h>
#include <MeshWarper.h>
#include <Particles/ParticleNormalEvaluation.h>
Expand Down Expand Up @@ -443,6 +444,33 @@ bool Analyze::update_shapes() {
shapes_.push_back(shape);
}

// Compute mean groomed centroid per domain across all shapes.
// Only applies when grooming alignment was NOT performed (pre-aligned data).
// This restores world-space positioning after Procrustes centers everything to the origin.
GroomParameters groom_params(project_);
bool has_grooming_alignment = groom_params.get_alignment_enabled() && !groom_params.get_skip_grooming();
if (!has_grooming_alignment) {
unsigned int num_domains = domain_names.size();
std::vector<Eigen::Vector3d> centroid_sum(num_domains, Eigen::Vector3d::Zero());
int centroid_count = 0;
for (auto& shape : shapes_) {
auto centroids = shape->get_groomed_centroids();
for (unsigned int d = 0; d < num_domains && d < centroids.size(); d++) {
centroid_sum[d] += centroids[d];
}
centroid_count++;
}
if (centroid_count > 0) {
std::vector<Eigen::Vector3d> mean_centroids(num_domains);
for (unsigned int d = 0; d < num_domains; d++) {
mean_centroids[d] = centroid_sum[d] / centroid_count;
}
for (auto& shape : shapes_) {
shape->set_groomed_centroids(mean_centroids);
}
}
}

SW_DEBUG("Successfully loaded shapes");

return true;
Expand Down
20 changes: 20 additions & 0 deletions Libs/Analyze/Particles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ void Particles::set_procrustes_transforms(const std::vector<vtkSmartPointer<vtkT
procrustes_transforms_ = transforms;
}

//---------------------------------------------------------------------------
void Particles::set_groomed_centroids(const std::vector<Eigen::Vector3d>& centroids) {
groomed_centroids_ = centroids;
}

//---------------------------------------------------------------------------
Eigen::VectorXd Particles::get_difference_vectors(const Particles& other) const {
auto combined = get_combined_global_particles();
Expand All @@ -178,6 +183,20 @@ void Particles::transform_global_particles() {
transformed_global_particles_.clear();
if (!transform_) {
transformed_global_particles_ = global_particles_;

// Apply groomed mesh centroid offsets to restore world-space positioning.
// This only applies when transform_ is null (local alignment), where global_particles_
// are Procrustes-centered at the origin. When grooming alignment was performed,
// centroids are near zero (no effect). When grooming was skipped, this restores the
// original spatial positions, preventing multi-domain shapes from overlapping at the origin.
for (int d = 0; d < transformed_global_particles_.size() && d < groomed_centroids_.size(); d++) {
Eigen::VectorXd& eigen = transformed_global_particles_[d];
for (size_t i = 0; i < eigen.size(); i += 3) {
eigen[i] += groomed_centroids_[d][0];
eigen[i + 1] += groomed_centroids_[d][1];
eigen[i + 2] += groomed_centroids_[d][2];
}
}
} else {
for (int d = 0; d < local_particles_.size(); d++) {
Eigen::VectorXd eigen = local_particles_[d];
Expand Down Expand Up @@ -235,6 +254,7 @@ void Particles::transform_global_particles() {
transformed_global_particles_.push_back(eigen);
}
}

}

//---------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions Libs/Analyze/Particles.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Particles {
void set_transform(vtkSmartPointer<vtkTransform> transform);
void set_procrustes_transforms(const std::vector<vtkSmartPointer<vtkTransform>>& transforms);
void set_alignment_type(int alignment);
void set_groomed_centroids(const std::vector<Eigen::Vector3d>& centroids);

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

Expand All @@ -72,6 +73,7 @@ class Particles {

vtkSmartPointer<vtkTransform> transform_;
std::vector<vtkSmartPointer<vtkTransform>> procrustes_transforms_;
std::vector<Eigen::Vector3d> groomed_centroids_;
int alignment_type_ = -3; // not a valid value

};
Expand Down
25 changes: 25 additions & 0 deletions Libs/Analyze/Shape.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,26 @@ vtkSmartPointer<vtkTransform> Shape::get_groomed_transform(int domain) {
return nullptr;
}

//---------------------------------------------------------------------------
std::vector<Eigen::Vector3d> Shape::get_groomed_centroids() {
std::vector<Eigen::Vector3d> centroids;
auto meshes = get_groomed_meshes(true);
for (int i = 0; i < meshes.meshes().size(); i++) {
auto mesh = meshes.meshes()[i];
if (mesh && mesh->get_poly_data() && mesh->get_poly_data()->GetNumberOfPoints() > 0) {
auto com = vtkSmartPointer<vtkCenterOfMass>::New();
com->SetInputData(mesh->get_poly_data());
com->Update();
double center[3];
com->GetCenter(center);
centroids.push_back(Eigen::Vector3d(center[0], center[1], center[2]));
} else {
centroids.push_back(Eigen::Vector3d::Zero());
}
}
return centroids;
}

//---------------------------------------------------------------------------
vtkSmartPointer<vtkTransform> Shape::get_procrustes_transform(int domain) {
auto transforms = subject_->get_procrustes_transforms();
Expand Down Expand Up @@ -817,6 +837,11 @@ void Shape::set_particle_transform(vtkSmartPointer<vtkTransform> transform) {
particles_.set_transform(transform);
}

//---------------------------------------------------------------------------
void Shape::set_groomed_centroids(const std::vector<Eigen::Vector3d>& centroids) {
particles_.set_groomed_centroids(centroids);
}

//---------------------------------------------------------------------------
void Shape::set_alignment_type(int alignment) { particles_.set_alignment_type(alignment); }

Expand Down
6 changes: 6 additions & 0 deletions Libs/Analyze/Shape.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ class Shape {
//! Set the particle transform (alignment)
void set_particle_transform(vtkSmartPointer<vtkTransform> transform);

//! Set per-domain centroid offsets for world-space positioning
void set_groomed_centroids(const std::vector<Eigen::Vector3d>& centroids);

//! Set the alignment type
void set_alignment_type(int alignment);

Expand Down Expand Up @@ -158,6 +161,9 @@ class Shape {

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

//! Get the centroid of each groomed mesh domain
std::vector<Eigen::Vector3d> get_groomed_centroids();

vtkSmartPointer<vtkTransform> get_procrustes_transform(int domain = 0);
std::vector<vtkSmartPointer<vtkTransform>> get_procrustes_transforms();

Expand Down
33 changes: 33 additions & 0 deletions Studio/Analysis/AnalysisTool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <Job/ParticleNormalEvaluationJob.h>
#include <Job/StatsGroupLDAJob.h>
#include <Libs/Application/Job/PythonWorker.h>
#include <Groom/GroomParameters.h>
#include <Logging.h>
#include <QMeshWarper.h>
#include <Shape.h>
Expand Down Expand Up @@ -1205,6 +1206,38 @@ void AnalysisTool::reset_stats() {
evals_ready_ = false;
stats_ready_ = false;

// Compute mean groomed centroid per domain to restore world-space positioning.
// Only applies when grooming alignment was NOT performed (i.e., pre-aligned data).
// When grooming alignment was performed, Procrustes centering to origin is correct
// and we don't need to restore original positions.
if (session_ && session_->particles_present()) {
auto shapes = session_->get_non_excluded_shapes();
GroomParameters groom_params(session_->get_project());
bool has_grooming_alignment = groom_params.get_alignment_enabled() && !groom_params.get_skip_grooming();
if (!has_grooming_alignment) {
auto domain_names = session_->get_project()->get_domain_names();
unsigned int num_domains = domain_names.size();
std::vector<Eigen::Vector3d> centroid_sum(num_domains, Eigen::Vector3d::Zero());
int centroid_count = 0;
for (auto& shape : shapes) {
auto centroids = shape->get_groomed_centroids();
for (unsigned int d = 0; d < num_domains && d < centroids.size(); d++) {
centroid_sum[d] += centroids[d];
}
centroid_count++;
}
if (centroid_count > 0) {
std::vector<Eigen::Vector3d> mean_centroids(num_domains);
for (unsigned int d = 0; d < num_domains; d++) {
mean_centroids[d] = centroid_sum[d] / centroid_count;
}
for (auto& shape : shapes) {
shape->set_groomed_centroids(mean_centroids);
}
}
}
}

ui_->pca_scalar_combo->clear();
if (session_) {
for (const auto& feature : session_->get_project()->get_feature_names()) {
Expand Down
2 changes: 2 additions & 0 deletions docs/studio/multiple-domains.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ In the presence of multiple anatomies, there are multiple alignment strategies t
Below is an example of these four options with a pelvis and femur model.

<p><video src="https://sci.utah.edu/~shapeworks/doc-resources/mp4s/multiple-domains-mixed-types.mp4" autoplay muted loop controls style="width:100%"></p>

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).
8 changes: 5 additions & 3 deletions docs/studio/studio-analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ The PCA tab of the View panel shows reconstructed shapes (surface meshes) along
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:
![ShapeWorks Studio Analysis View Panel PCA Display for Multiple-Domain Shape Model](../img/studio/studio_analyze_view_pca_multiple_domain.png)

`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.
`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.

`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.
`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.

`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.
`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.

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).

### Show Difference to Mean

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ nav:
- 'Surface Reconstruction': 'studio/surface-reconstruction.md'
- 'DeepSSM Module': 'studio/deepssm-in-studio.md'
- 'Multiple Domains SSM': 'studio/multiple-domains.md'
- 'Multi-Domain Reference Frames': 'studio/multi-domain-analysis-reference-frames.md'
- 'Shared Boundaries': 'studio/studio-shared-boundary.md'
- 'Segmentation Tool': 'studio/segmentation-tool.md'
- 'AI Assisted Segmentation': 'studio/ai-assisted-segmentation.md'
Expand Down
Loading