Skip to content

Commit fc9d407

Browse files
committed
Replace VNL eigensystem with Eigen SVD for PCA computation
Switch ParticleShapeStatistics::compute_modes() from VNL's vnl_symmetric_eigensystem to Eigen's BDCSVD algorithm. This provides: - Better numerical stability by computing SVD directly on the centered data matrix rather than forming the covariance matrix A^T*A first - Reduced dependency on legacy VNL library in favor of Eigen - Cleaner code using Eigen's built-in vector normalization The SVD singular values squared equal the eigenvalues, and the left singular vectors (U) correspond to the eigenvectors in particle space. Results are reversed to maintain ascending eigenvalue order for backward compatibility. Also add profiling instrumentation to compute_modes() and Studio main.
1 parent b213366 commit fc9d407

File tree

3 files changed

+38
-32
lines changed

3 files changed

+38
-32
lines changed

Libs/Common/Profiling.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ void Profiler::finalize() {
157157
write_trace_file();
158158
}
159159
}
160+
161+
//---------------------------------------------------------------------------
160162
void Profiler::write_profile_report() {
161163
// Calculate total time
162164
double total_time_ms = elapsed_timer_.elapsed();

Libs/Particles/ParticleShapeStatistics.cpp

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <vnl/algo/vnl_symmetric_eigensystem.h>
88

99
#include "ExternalLibs/tinyxml/tinyxml.h"
10+
#include "Profiling.h"
1011
#include "ShapeEvaluation.h"
1112

1213
namespace shapeworks {
@@ -577,49 +578,48 @@ int ParticleShapeStatistics::do_pca(std::shared_ptr<Project> project) {
577578
}
578579

579580
//---------------------------------------------------------------------------
581+
580582
int ParticleShapeStatistics::compute_modes() {
583+
TIME_SCOPE("ParticleShapeStatistics::compute_modes");
581584
SW_DEBUG("computing PCA modes");
582-
Eigen::MatrixXd A = points_minus_mean_.transpose() * points_minus_mean_ * (1.0 / ((double)(num_samples_ - 1)));
583585

584-
auto vnlA = vnl_matrix<double>(A.data(), A.rows(), A.cols());
585-
vnl_symmetric_eigensystem<double> symEigen(vnlA);
586+
double scale = std::sqrt(num_samples_ - 1);
587+
Eigen::BDCSVD<Eigen::MatrixXd> svd(points_minus_mean_ / scale, Eigen::ComputeThinU);
586588

587-
Eigen::MatrixXd eigenSymEigenV =
588-
Eigen::Map<Eigen::MatrixXd>(symEigen.V.transpose().data_block(), symEigen.V.rows(), symEigen.V.cols());
589-
Eigen::VectorXd eigenSymEigenD = Eigen::Map<Eigen::VectorXd>(symEigen.D.data_block(), symEigen.D.rows(), 1);
589+
Eigen::VectorXd eigenvalues_eigen = svd.singularValues().array().square();
590590

591-
eigenvectors_ = points_minus_mean_ * eigenSymEigenV;
592-
eigenvalues_.resize(num_samples_);
591+
// U * S gives us the eigenvectors in particle space (like points_minus_mean_ * V did before)
592+
// But we want them normalized, so just use U (which is already orthonormal)
593+
eigenvectors_ = svd.matrixU() * scale; // Scale back to match original magnitudes
593594

594-
// normalize the eigenvectors
595-
for (unsigned int i = 0; i < num_samples_; i++) {
596-
double total = 0.0f;
597-
for (unsigned int j = 0; j < num_dimensions_; j++) {
598-
total += eigenvectors_(j, i) * eigenvectors_(j, i);
599-
}
600-
total = sqrt(total);
595+
int num_modes = eigenvalues_eigen.size();
601596

602-
for (unsigned int j = 0; j < num_dimensions_; j++) {
603-
eigenvectors_(j, i) = eigenvectors_(j, i) / (total + 1.0e-15);
604-
}
597+
// normalize the eigenvectors
598+
for (int i = 0; i < eigenvectors_.cols(); i++) {
599+
eigenvectors_.col(i).normalize();
600+
}
605601

606-
eigenvalues_[i] = eigenSymEigenD(i);
602+
// SVD returns values in descending order, but we need ascending order for backward compatibility
603+
// Reverse both eigenvalues and eigenvector columns
604+
eigenvalues_.resize(num_modes);
605+
for (int i = 0; i < num_modes; i++) {
606+
eigenvalues_[i] = eigenvalues_eigen[num_modes - 1 - i];
607607
}
608+
eigenvectors_ = eigenvectors_.rowwise().reverse().eval();
608609

609-
float sum = 0.0;
610-
for (unsigned int n = 0; n < num_samples_; n++) {
611-
sum += eigenvalues_[(num_samples_ - 1) - n];
610+
// eigenvalues are now in ascending order (smallest to largest)
611+
double sum = 0.0;
612+
for (int n = 0; n < num_modes; n++) {
613+
sum += eigenvalues_[n];
612614
}
613615

614-
float sum2 = 0.0;
615-
bool found = false;
616-
for (unsigned int n = 0; n < num_samples_; n++) {
617-
sum2 += eigenvalues_[(num_samples_ - 1) - n];
616+
// percent_variance_by_mode_ accumulates from largest eigenvalue (at end) to smallest
617+
// to match the old behavior
618+
percent_variance_by_mode_.clear();
619+
double sum2 = 0.0;
620+
for (int n = num_modes - 1; n >= 0; n--) {
621+
sum2 += eigenvalues_[n];
618622
percent_variance_by_mode_.push_back(sum2 / sum);
619-
620-
if ((sum2 / sum) >= 0.95 && found == false) {
621-
found = true;
622-
}
623623
}
624624

625625
return 0;

Studio/main.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
#include <QSurfaceFormat>
1111
#include <iostream>
1212

13+
#include "Profiling.h"
14+
1315
// itk
1416
#include <itkMacro.h>
1517

@@ -77,8 +79,8 @@ static void new_log() {
7779
int main(int argc, char** argv) {
7880
// tbb::task_scheduler_init init(1);
7981

82+
TIME_SCOPE("ShapeWorksStudio");
8083
try {
81-
8284
#ifdef Q_OS_MACOS
8385
// Prevent cursor crashes on Apple Silicon
8486
qputenv("QT_MAC_DISABLE_NATIVE_CURSORS", "1");
@@ -148,7 +150,9 @@ int main(int argc, char** argv) {
148150
app.file_open_callback_(app.stored_filename_);
149151
}
150152

151-
return app.exec();
153+
auto rc = app.exec();
154+
TIME_FINALIZE();
155+
return rc;
152156
} catch (itk::ExceptionObject& excep) {
153157
std::cerr << excep << std::endl;
154158
} catch (std::exception& e) {

0 commit comments

Comments
 (0)