Skip to content

Commit 2cbd705

Browse files
authored
Merge pull request #2513 from SCIInstitute/amorris/laplacian_warp
Add Laplacian warp method for MeshWarper
2 parents 01fdde2 + 3b69f57 commit 2cbd705

18 files changed

Lines changed: 573 additions & 90 deletions

Applications/shapeworks/MeshCommands.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1505,7 +1505,7 @@ bool WarpMesh::execute(const optparse::Values &options, SharedCommandData &share
15051505
Eigen::MatrixXd vertices_updated = output.points();
15061506
std::map<int, int> landmarks_map = warper.get_landmarks_map();
15071507

1508-
for (vtkIdType i = 0; i < warper.get_warp_matrix().rows(); i++) {
1508+
for (vtkIdType i = 0; i < vertices_updated.rows(); i++) {
15091509
if (landmarks_map.count(i)) {
15101510
// This vertex i corresponds to a landmark
15111511
int landmark_index = landmarks_map.lower_bound(i)->second;

Libs/Mesh/MeshWarper.cpp

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
#include <Mesh/Mesh.h>
33
#include <Mesh/MeshUtils.h>
44
#include <Mesh/MeshWarper.h>
5+
#include <Profiling.h>
56
#include <igl/biharmonic_coordinates.h>
67
#include <igl/boundary_loop.h>
8+
#include <igl/cotmatrix.h>
79
#include <igl/edges.h>
810
#include <igl/facet_components.h>
11+
#include <igl/min_quad_with_fixed.h>
912
#include <igl/point_mesh_squared_distance.h>
1013
#include <igl/remove_unreferenced.h>
1114
#include <vtkCellLocator.h>
@@ -19,6 +22,7 @@
1922
#include <vtkTriangleFilter.h>
2023

2124
#include <set>
25+
#include <unordered_map>
2226

2327
namespace shapeworks {
2428

@@ -27,6 +31,7 @@ static std::mutex mutex;
2731

2832
//---------------------------------------------------------------------------
2933
vtkSmartPointer<vtkPolyData> MeshWarper::build_mesh(const Eigen::MatrixXd& particles) {
34+
TIME_SCOPE("MeshWarper::build_mesh");
3035
if (!warp_available_) {
3136
return nullptr;
3237
}
@@ -44,7 +49,8 @@ vtkSmartPointer<vtkPolyData> MeshWarper::build_mesh(const Eigen::MatrixXd& parti
4449

4550
auto points = remove_bad_particles(particles);
4651

47-
vtkSmartPointer<vtkPolyData> poly_data = MeshWarper::warp_mesh(points);
52+
vtkSmartPointer<vtkPolyData> poly_data =
53+
laplacian_ready_ ? MeshWarper::warp_mesh_laplacian(points) : MeshWarper::warp_mesh(points);
4854

4955
for (int i = 0; i < poly_data->GetNumberOfPoints(); i++) {
5056
double* p = poly_data->GetPoint(i);
@@ -81,6 +87,7 @@ void MeshWarper::set_reference_mesh(vtkSmartPointer<vtkPolyData> reference_mesh,
8187

8288
// mark that the warp needs to be generated
8389
needs_warp_ = true;
90+
laplacian_ready_ = false;
8491

8592
warp_available_ = true;
8693

@@ -372,6 +379,7 @@ void MeshWarper::split_cell_on_edge(int cell_id, int new_vertex, int v0, int v1,
372379

373380
//---------------------------------------------------------------------------
374381
bool MeshWarper::generate_warp() {
382+
TIME_SCOPE("MeshWarper::generate_warp");
375383
if (is_contour_) {
376384
update_progress(1.0);
377385
needs_warp_ = false;
@@ -414,6 +422,15 @@ bool MeshWarper::generate_warp() {
414422
faces_ = reference_mesh.faces();
415423

416424
// Perform warp
425+
if (warp_method_ == WarpMethod::Laplacian) {
426+
if (generate_laplacian_warp(vertices, faces_, vertices_)) {
427+
update_progress(1.0);
428+
needs_warp_ = false;
429+
return true;
430+
}
431+
SW_DEBUG("Laplacian warp failed, falling back to biharmonic");
432+
}
433+
417434
if (MeshWarper::generate_warp_matrix(vertices, faces_, vertices_, warp_)) {
418435
// Success!
419436
update_progress(1.0);
@@ -422,7 +439,7 @@ bool MeshWarper::generate_warp() {
422439
}
423440
}
424441

425-
SW_ERROR("Mesh Warp Error: igl:biharmonic_coordinates failed");
442+
SW_ERROR("Mesh Warp Error: warp generation failed");
426443

427444
// All attempts failed
428445
update_progress(1.0);
@@ -433,6 +450,7 @@ bool MeshWarper::generate_warp() {
433450
//---------------------------------------------------------------------------
434451
bool MeshWarper::generate_warp_matrix(Eigen::MatrixXd target_vertices, Eigen::MatrixXi target_faces,
435452
const Eigen::MatrixXd& references_vertices, Eigen::MatrixXd& warp) {
453+
TIME_SCOPE("MeshWarper::generate_warp_matrix (biharmonic)");
436454
Eigen::VectorXi closest_vertex_indices;
437455

438456
Eigen::VectorXi vertex_indices = Eigen::VectorXi::LinSpaced(target_vertices.rows(), 0, target_vertices.rows() - 1);
@@ -480,6 +498,7 @@ bool MeshWarper::find_landmarks_vertices_on_ref_mesh() {
480498

481499
//---------------------------------------------------------------------------
482500
vtkSmartPointer<vtkPolyData> MeshWarper::warp_mesh(const Eigen::MatrixXd& points) {
501+
TIME_SCOPE("MeshWarper::warp_mesh (biharmonic)");
483502
int num_vertices = warp_.rows();
484503
int num_faces = faces_.rows();
485504
Eigen::MatrixXd v_out = warp_ * (points.rowwise() + Eigen::RowVector3d(0, 0, 0));
@@ -502,6 +521,135 @@ vtkSmartPointer<vtkPolyData> MeshWarper::warp_mesh(const Eigen::MatrixXd& points
502521
return poly_data;
503522
}
504523

524+
//---------------------------------------------------------------------------
525+
int MeshWarper::get_num_warp_vertices() const {
526+
if (laplacian_ready_) {
527+
return laplacian_vertices_.rows();
528+
}
529+
return warp_.rows();
530+
}
531+
532+
//---------------------------------------------------------------------------
533+
bool MeshWarper::generate_laplacian_warp(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F,
534+
const Eigen::MatrixXd& control_points) {
535+
TIME_SCOPE("MeshWarper::generate_laplacian_warp");
536+
// Map control points to closest mesh vertices
537+
Eigen::VectorXi vertex_indices = Eigen::VectorXi::LinSpaced(V.rows(), 0, V.rows() - 1);
538+
Eigen::VectorXd squared_distances;
539+
Eigen::MatrixXd unused_closest_points;
540+
Eigen::VectorXi closest_vertex_indices;
541+
igl::point_mesh_squared_distance(control_points, V, vertex_indices, squared_distances, closest_vertex_indices,
542+
unused_closest_points);
543+
544+
// Deduplicate handle indices
545+
std::set<int> handle_set(closest_vertex_indices.data(),
546+
closest_vertex_indices.data() + closest_vertex_indices.size());
547+
laplacian_handles_.resize(handle_set.size());
548+
int idx = 0;
549+
for (int h : handle_set) {
550+
laplacian_handles_(idx++) = h;
551+
}
552+
553+
// Build cotangent Laplacian
554+
Eigen::SparseMatrix<double> L;
555+
igl::cotmatrix(V, F, L);
556+
557+
// Q = L^T * L (bilaplacian energy)
558+
Eigen::SparseMatrix<double> Q = L.transpose() * L;
559+
560+
// Precompute factorization with fixed handle constraints
561+
Eigen::SparseMatrix<double> Aeq; // empty equality constraints
562+
if (!igl::min_quad_with_fixed_precompute(Q, laplacian_handles_, Aeq, true, mqwf_data_)) {
563+
SW_DEBUG("Laplacian precompute failed");
564+
return false;
565+
}
566+
567+
// Store constant RHS: B = -Q * V_ref (the linear term for the unconstrained part)
568+
laplacian_rhs_ = -Q * V;
569+
570+
// Store reference vertices
571+
laplacian_vertices_ = V;
572+
573+
// Precompute handle_map_: a sparse matrix (num_handles x num_controls) that maps
574+
// control point positions to handle Y rows, averaging when multiple controls map to the same handle.
575+
// Also precompute handle_default_: reference positions for handles with no control point.
576+
int num_handles = laplacian_handles_.size();
577+
int num_controls = closest_vertex_indices.size();
578+
579+
// Build a map from vertex id to handle index
580+
std::unordered_map<int, int> vertex_to_handle;
581+
for (int i = 0; i < num_handles; i++) {
582+
vertex_to_handle[laplacian_handles_(i)] = i;
583+
}
584+
585+
// Count how many controls map to each handle
586+
Eigen::VectorXi handle_counts = Eigen::VectorXi::Zero(num_handles);
587+
for (int i = 0; i < num_controls; i++) {
588+
auto it = vertex_to_handle.find(closest_vertex_indices(i));
589+
if (it != vertex_to_handle.end()) {
590+
handle_counts(it->second)++;
591+
}
592+
}
593+
594+
// Build sparse matrix with weights = 1/count for averaging
595+
std::vector<Eigen::Triplet<double>> triplets;
596+
triplets.reserve(num_controls);
597+
for (int i = 0; i < num_controls; i++) {
598+
auto it = vertex_to_handle.find(closest_vertex_indices(i));
599+
if (it != vertex_to_handle.end()) {
600+
int h = it->second;
601+
triplets.emplace_back(h, i, 1.0 / handle_counts(h));
602+
}
603+
}
604+
handle_map_.resize(num_handles, num_controls);
605+
handle_map_.setFromTriplets(triplets.begin(), triplets.end());
606+
607+
// Default Y: reference positions for handles with no control point
608+
handle_default_ = Eigen::MatrixXd::Zero(num_handles, 3);
609+
for (int i = 0; i < num_handles; i++) {
610+
if (handle_counts(i) == 0) {
611+
handle_default_.row(i) = V.row(laplacian_handles_(i));
612+
}
613+
}
614+
615+
laplacian_ready_ = true;
616+
return true;
617+
}
618+
619+
//---------------------------------------------------------------------------
620+
vtkSmartPointer<vtkPolyData> MeshWarper::warp_mesh_laplacian(const Eigen::MatrixXd& points) {
621+
TIME_SCOPE("MeshWarper::warp_mesh_laplacian");
622+
// Build Y from precomputed sparse map: Y = handle_map_ * points + handle_default_
623+
Eigen::MatrixXd Y = handle_map_ * points + handle_default_;
624+
625+
Eigen::MatrixXd Beq; // empty
626+
Eigen::MatrixXd Z;
627+
if (!igl::min_quad_with_fixed_solve(mqwf_data_, laplacian_rhs_, Y, Beq, Z)) {
628+
SW_ERROR("Laplacian solve failed");
629+
return warp_mesh(points); // fallback to biharmonic
630+
}
631+
632+
// Build VTK polydata from solved vertices
633+
int num_vertices = Z.rows();
634+
int num_faces = faces_.rows();
635+
vtkSmartPointer<vtkPolyData> poly_data = vtkSmartPointer<vtkPolyData>::New();
636+
vtkSmartPointer<vtkPoints> out_points = vtkSmartPointer<vtkPoints>::New();
637+
out_points->SetNumberOfPoints(num_vertices);
638+
for (vtkIdType i = 0; i < num_vertices; i++) {
639+
out_points->SetPoint(i, Z(i, 0), Z(i, 1), Z(i, 2));
640+
}
641+
vtkSmartPointer<vtkCellArray> polys = vtkSmartPointer<vtkCellArray>::New();
642+
for (vtkIdType i = 0; i < num_faces; i++) {
643+
polys->InsertNextCell(3);
644+
polys->InsertCellPoint(faces_(i, 0));
645+
polys->InsertCellPoint(faces_(i, 1));
646+
polys->InsertCellPoint(faces_(i, 2));
647+
}
648+
poly_data->SetPoints(out_points);
649+
poly_data->SetPolys(polys);
650+
return poly_data;
651+
}
652+
505653
//---------------------------------------------------------------------------
506654
//---------------------------------------------------------------------------
507655
void MeshWarper::diagnose_biharmonic_failure(const Eigen::MatrixXd& TV, const Eigen::MatrixXi& TF,

Libs/Mesh/MeshWarper.h

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,24 @@
77
* The MeshWarper provides an object to warp meshes for surface reconstruction
88
*/
99

10+
#include <igl/min_quad_with_fixed.h>
1011
#include <vtkPolyData.h>
1112

1213
#include <Eigen/Eigen>
1314
#include <vector>
1415

1516
namespace shapeworks {
1617

18+
//! Warp method selection for MeshWarper
19+
enum class WarpMethod { Laplacian, Biharmonic };
20+
1721
/**
1822
* \class MeshWarper
1923
* \ingroup Group-Mesh
2024
*
2125
* This class implements mesh warping based on correspondence particles.
22-
* Correspondence points are embedded into the mesh as new vertices (traingles split). Then a biharmonic deformation
23-
* is used to warp the mesh to new sets of correspondence particles.
26+
* Correspondence points are embedded into the mesh as new vertices (triangles split). Then a deformation
27+
* (Laplacian or biharmonic) is used to warp the mesh to new sets of correspondence particles.
2428
*
2529
* It can optionally be used to warp landmarks along with the mesh by embedding them as vertices
2630
*
@@ -66,6 +70,15 @@ class MeshWarper {
6670
//! Return the reference particles
6771
const Eigen::MatrixXd& get_reference_particles() const { return this->reference_particles_; }
6872

73+
//! Set the warp method (Laplacian or Biharmonic)
74+
void set_warp_method(WarpMethod method) { warp_method_ = method; }
75+
76+
//! Get the current warp method
77+
WarpMethod get_warp_method() const { return warp_method_; }
78+
79+
//! Return the number of vertices in the warped mesh
80+
int get_num_warp_vertices() const;
81+
6982
protected:
7083
//! For overriding to handle progress updates
7184
virtual void update_progress(float p) {}
@@ -103,6 +116,12 @@ class MeshWarper {
103116
void diagnose_biharmonic_failure(const Eigen::MatrixXd& TV, const Eigen::MatrixXi& TF,
104117
const std::vector<std::vector<int>>& S, int k);
105118

119+
//! Generate the Laplacian warp data
120+
bool generate_laplacian_warp(const Eigen::MatrixXd& V, const Eigen::MatrixXi& F,
121+
const Eigen::MatrixXd& control_points);
122+
123+
//! Warp mesh using Laplacian deformation
124+
vtkSmartPointer<vtkPolyData> warp_mesh_laplacian(const Eigen::MatrixXd& points);
106125

107126
// Members
108127
Eigen::MatrixXi faces_;
@@ -126,5 +145,17 @@ class MeshWarper {
126145
Eigen::MatrixXd reference_particles_;
127146
//! Whether the reference is a contour
128147
bool is_contour_ = false;
148+
149+
//! Selected warp method
150+
WarpMethod warp_method_ = WarpMethod::Biharmonic;
151+
152+
//! Laplacian solve data
153+
igl::min_quad_with_fixed_data<double> mqwf_data_;
154+
Eigen::MatrixXd laplacian_rhs_; //!< Constant RHS term: -Q * V_ref (n x 3)
155+
Eigen::MatrixXd laplacian_vertices_; //!< Reference surface vertices
156+
Eigen::VectorXi laplacian_handles_; //!< Handle vertex indices
157+
Eigen::SparseMatrix<double> handle_map_; //!< Maps control points to handle Y rows (num_handles x num_controls)
158+
Eigen::MatrixXd handle_default_; //!< Default Y for handles with no control point (num_handles x 3)
159+
bool laplacian_ready_ = false;
129160
};
130161
} // namespace shapeworks

Libs/Python/ShapeworksPython.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,12 @@ PYBIND11_MODULE(shapeworks_py, m) {
11121112

11131113
;
11141114

1115+
// WarpMethod enum
1116+
py::enum_<WarpMethod>(m, "WarpMethod")
1117+
.value("Laplacian", WarpMethod::Laplacian)
1118+
.value("Biharmonic", WarpMethod::Biharmonic)
1119+
.export_values();
1120+
11151121
// MeshWarping
11161122
py::class_<MeshWarper>(m, "MeshWarper")
11171123

@@ -1177,7 +1183,19 @@ PYBIND11_MODULE(shapeworks_py, m) {
11771183
[](MeshWarper& w, const Mesh& warped_mesh) -> decltype(auto) {
11781184
return w.extract_landmarks(warped_mesh.getVTKMesh());
11791185
},
1180-
"Extract the landmarks from the warped mesh and return the landmarks (matrix [Nx3])", "warped_mesh"_a);
1186+
"Extract the landmarks from the warped mesh and return the landmarks (matrix [Nx3])", "warped_mesh"_a)
1187+
1188+
.def(
1189+
"setWarpMethod", [](MeshWarper& w, WarpMethod method) { w.set_warp_method(method); },
1190+
"Set the warp method (WarpMethod.Laplacian or WarpMethod.Biharmonic)", "method"_a)
1191+
1192+
.def(
1193+
"getWarpMethod", [](MeshWarper& w) -> decltype(auto) { return w.get_warp_method(); },
1194+
"Return the current warp method.")
1195+
1196+
.def(
1197+
"getNumWarpVertices", [](MeshWarper& w) -> decltype(auto) { return w.get_num_warp_vertices(); },
1198+
"Return the number of vertices in the warped mesh.");
11811199

11821200
// MeshUtils
11831201
py::class_<MeshUtils>(m, "MeshUtils")

0 commit comments

Comments
 (0)