diff --git a/Libs/Analyze/Analyze.cpp b/Libs/Analyze/Analyze.cpp index 728d8399f7..03f51f7126 100644 --- a/Libs/Analyze/Analyze.cpp +++ b/Libs/Analyze/Analyze.cpp @@ -1,5 +1,6 @@ #include "Analyze.h" +#include #include #include #include @@ -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 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 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; diff --git a/Libs/Analyze/Particles.cpp b/Libs/Analyze/Particles.cpp index 4bd10a20de..58924883c7 100644 --- a/Libs/Analyze/Particles.cpp +++ b/Libs/Analyze/Particles.cpp @@ -163,6 +163,11 @@ void Particles::set_procrustes_transforms(const std::vector& centroids) { + groomed_centroids_ = centroids; +} + //--------------------------------------------------------------------------- Eigen::VectorXd Particles::get_difference_vectors(const Particles& other) const { auto combined = get_combined_global_particles(); @@ -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]; @@ -235,6 +254,7 @@ void Particles::transform_global_particles() { transformed_global_particles_.push_back(eigen); } } + } //--------------------------------------------------------------------------- diff --git a/Libs/Analyze/Particles.h b/Libs/Analyze/Particles.h index 3fef7c362d..17868f8ee4 100644 --- a/Libs/Analyze/Particles.h +++ b/Libs/Analyze/Particles.h @@ -49,6 +49,7 @@ class Particles { void set_transform(vtkSmartPointer transform); void set_procrustes_transforms(const std::vector>& transforms); void set_alignment_type(int alignment); + void set_groomed_centroids(const std::vector& centroids); Eigen::VectorXd get_difference_vectors(const Particles& other) const; @@ -72,6 +73,7 @@ class Particles { vtkSmartPointer transform_; std::vector> procrustes_transforms_; + std::vector groomed_centroids_; int alignment_type_ = -3; // not a valid value }; diff --git a/Libs/Analyze/Shape.cpp b/Libs/Analyze/Shape.cpp index 379ebd9e43..fa87bc0ce8 100644 --- a/Libs/Analyze/Shape.cpp +++ b/Libs/Analyze/Shape.cpp @@ -773,6 +773,26 @@ vtkSmartPointer Shape::get_groomed_transform(int domain) { return nullptr; } +//--------------------------------------------------------------------------- +std::vector Shape::get_groomed_centroids() { + std::vector 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::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 Shape::get_procrustes_transform(int domain) { auto transforms = subject_->get_procrustes_transforms(); @@ -817,6 +837,11 @@ void Shape::set_particle_transform(vtkSmartPointer transform) { particles_.set_transform(transform); } +//--------------------------------------------------------------------------- +void Shape::set_groomed_centroids(const std::vector& centroids) { + particles_.set_groomed_centroids(centroids); +} + //--------------------------------------------------------------------------- void Shape::set_alignment_type(int alignment) { particles_.set_alignment_type(alignment); } diff --git a/Libs/Analyze/Shape.h b/Libs/Analyze/Shape.h index cf31afdb49..971d6b86e0 100644 --- a/Libs/Analyze/Shape.h +++ b/Libs/Analyze/Shape.h @@ -107,6 +107,9 @@ class Shape { //! Set the particle transform (alignment) void set_particle_transform(vtkSmartPointer transform); + //! Set per-domain centroid offsets for world-space positioning + void set_groomed_centroids(const std::vector& centroids); + //! Set the alignment type void set_alignment_type(int alignment); @@ -158,6 +161,9 @@ class Shape { vtkSmartPointer get_groomed_transform(int domain = 0); + //! Get the centroid of each groomed mesh domain + std::vector get_groomed_centroids(); + vtkSmartPointer get_procrustes_transform(int domain = 0); std::vector> get_procrustes_transforms(); diff --git a/Studio/Analysis/AnalysisTool.cpp b/Studio/Analysis/AnalysisTool.cpp index 98c7e7690a..da70b3ed7b 100644 --- a/Studio/Analysis/AnalysisTool.cpp +++ b/Studio/Analysis/AnalysisTool.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -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 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 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()) { diff --git a/docs/studio/multiple-domains.md b/docs/studio/multiple-domains.md index d4ad5ddc93..6a8f28aad9 100644 --- a/docs/studio/multiple-domains.md +++ b/docs/studio/multiple-domains.md @@ -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.

+ +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). diff --git a/docs/studio/studio-analyze.md b/docs/studio/studio-analyze.md index 66727a7d22..2500d8aa89 100644 --- a/docs/studio/studio-analyze.md +++ b/docs/studio/studio-analyze.md @@ -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 diff --git a/mkdocs.yml b/mkdocs.yml index e6bd100b20..31024b210d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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'