You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
18 KiB
437 lines
18 KiB
/**************************************************************************** |
|
* VCGLib o o * |
|
* Visual and Computer Graphics Library o o * |
|
* _ O _ * |
|
* Copyright(C) 2004-2023 \/)\/ * |
|
* Visual Computing Lab /\/| * |
|
* ISTI - Italian National Research Council | * |
|
* \ * |
|
* All rights reserved. * |
|
* * |
|
* This program is free software; you can redistribute it and/or modify * |
|
* it under the terms of the GNU General Public License as published by * |
|
* the Free Software Foundation; either version 2 of the License, or * |
|
* (at your option) any later version. * |
|
* * |
|
* This program is distributed in the hope that it will be useful, * |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of * |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
|
* GNU General Public License (http://www.gnu.org/licenses/gpl.txt) * |
|
* for more details. * |
|
* * |
|
****************************************************************************/ |
|
#ifndef __VCG_GEODESIC_HEAT |
|
#define __VCG_GEODESIC_HEAT |
|
#include <Eigen/Sparse> |
|
#include <Eigen/SparseCholesky> |
|
|
|
#include <vcg/complex/algorithms/update/quality.h> |
|
#include <vcg/complex/algorithms/stat.h> |
|
|
|
#include <vector> |
|
#include <memory> |
|
|
|
namespace vcg{ |
|
namespace tri{ |
|
|
|
template <class MeshType> |
|
class GeodesicHeat{ |
|
typedef typename MeshType::VertexType VertexType; |
|
typedef typename MeshType::VertexIterator VertexIterator; |
|
typedef typename MeshType::VertexPointer VertexPointer; |
|
typedef typename MeshType::FacePointer FacePointer; |
|
typedef typename MeshType::FaceIterator FaceIterator; |
|
typedef typename MeshType::FaceType FaceType; |
|
typedef typename MeshType::ScalarType ScalarType; |
|
|
|
public: |
|
/** @brief Fills the matrix with dual cell area of each vertex. |
|
* Additionally saves the area of faces in the face quality field |
|
* |
|
* @param mesh the mesh |
|
* @param mass a sparse matrix to store values into |
|
*/ |
|
static void buildMassMatrix(MeshType &mesh, Eigen::SparseMatrix<double> &mass){ |
|
mass.resize(mesh.VN(), mesh.VN()); |
|
mass.reserve(Eigen::VectorXi::Constant(mesh.VN(),1)); |
|
vcg::tri::UpdateQuality<MeshType>::FaceArea(mesh); |
|
|
|
// compute area of the dual cell for each vertex |
|
for (int i = 0; i < mesh.VN(); ++i){ |
|
VertexType *vp = &mesh.vert[i]; |
|
|
|
std::vector<FacePointer> faces; |
|
std::vector<int> indices; |
|
vcg::face::VFStarVF<FaceType>(vp, faces, indices); |
|
|
|
double area = 0; |
|
for (int j = 0; j < faces.size(); ++j) |
|
{ |
|
area += faces[j]->Q(); |
|
} |
|
area /= 3; |
|
mass.insert(i, i) = area; |
|
} |
|
|
|
mass.makeCompressed(); |
|
} |
|
|
|
/** @brief Fills the matrix with cotan weights. Only lower triangular values are saved. |
|
* |
|
* @param mesh the mesh |
|
* @param mass a sparse matrix to store values into |
|
*/ |
|
static void buildCotanLowerTriMatrix(MeshType &mesh, Eigen::SparseMatrix<double> &cotanOperator){ |
|
cotanOperator.resize(mesh.VN(), mesh.VN()); |
|
|
|
typedef Eigen::Triplet<double> T; |
|
std::vector<T> tripletList; |
|
tripletList.reserve(3*mesh.VN() + 3*mesh.FN()); |
|
|
|
// compute cotan weights |
|
for (int i = 0; i < mesh.FN(); ++i){ |
|
FacePointer fi = &mesh.face[i]; |
|
|
|
VertexPointer v0 = fi->V(0); |
|
VertexPointer v1 = fi->V(1); |
|
VertexPointer v2 = fi->V(2); |
|
|
|
vcg::Point3f p0 = v0->P(); |
|
vcg::Point3f p1 = v1->P(); |
|
vcg::Point3f p2 = v2->P(); |
|
|
|
Eigen::Vector3d e0 = toEigen(p2 - p1); |
|
Eigen::Vector3d e1 = toEigen(p0 - p2); |
|
Eigen::Vector3d e2 = toEigen(p1 - p0); |
|
|
|
// first edge is inverted to get correct orientation |
|
double alpha0 = cotan(-e1, e2) / 2; |
|
double alpha1 = cotan(-e2, e0) / 2; |
|
double alpha2 = cotan(-e0, e1) / 2; |
|
|
|
int i0 = vcg::tri::Index(mesh,v0); |
|
int i1 = vcg::tri::Index(mesh,v1); |
|
int i2 = vcg::tri::Index(mesh,v2); |
|
|
|
// save only lower triangular part |
|
if (i0 > i1) |
|
tripletList.push_back(T(i0,i1,alpha2)); |
|
else |
|
tripletList.push_back(T(i1,i0,alpha2)); |
|
if (i0 > i2) |
|
tripletList.push_back(T(i0,i2,alpha1)); |
|
else |
|
tripletList.push_back(T(i2,i0,alpha1)); |
|
if (i1 > i2) |
|
tripletList.push_back(T(i1,i2,alpha0)); |
|
else |
|
tripletList.push_back(T(i2,i1,alpha0)); |
|
|
|
tripletList.push_back(T(i0,i0,-(alpha1 + alpha2))); |
|
tripletList.push_back(T(i1,i1,-(alpha0 + alpha2))); |
|
tripletList.push_back(T(i2,i2,-(alpha0 + alpha1))); |
|
} |
|
|
|
cotanOperator.setFromTriplets(tripletList.begin(), tripletList.end()); |
|
cotanOperator.makeCompressed(); |
|
} |
|
|
|
/** @brief given a mesh returns the average weight length |
|
* @param mesh the mesh |
|
* @return double the average edge length |
|
*/ |
|
static double computeAverageEdgeLength(MeshType &mesh){ |
|
return vcg::tri::Stat<MeshType>::ComputeFaceEdgeLengthAverage(mesh); |
|
} |
|
|
|
/** @brief Computes the gradient of a (vertex sampled) scalar field with respect to faces |
|
* @param mesh the mesh |
|
* @param scalarField a scalar field of size (VN) |
|
* @return Eigen::MatrixX3d a vector field of with size (FN, 3) |
|
* |
|
* Requires both face normals and quality. The latter containing face areas. |
|
*/ |
|
static Eigen::MatrixX3d computeFaceGradient(MeshType &mesh, const Eigen::VectorXd &scalarField){ |
|
Eigen::MatrixX3d faceGradientField(mesh.FN(), 3); |
|
// compute gradient of the scalar function at each face |
|
for (int i = 0; i < mesh.FN(); ++i){ |
|
FacePointer fp = &mesh.face[i]; |
|
|
|
VertexPointer v0 = fp->V(0); |
|
VertexPointer v1 = fp->V(1); |
|
VertexPointer v2 = fp->V(2); |
|
|
|
vcg::Point3f p0 = v0->P(); |
|
vcg::Point3f p1 = v1->P(); |
|
vcg::Point3f p2 = v2->P(); |
|
|
|
int i0 = vcg::tri::Index(mesh,v0); |
|
int i1 = vcg::tri::Index(mesh,v1); |
|
int i2 = vcg::tri::Index(mesh,v2); |
|
|
|
// normal unit vector |
|
Eigen::Vector3d n = toEigen(fp->N()); |
|
n /= n.norm(); |
|
// face area |
|
double faceArea = fp->Q(); |
|
// edge unit vectors (counter-clockwise) |
|
Eigen::Vector3d e0 = toEigen(p2 - p1); |
|
Eigen::Vector3d e1 = toEigen(p0 - p2); |
|
Eigen::Vector3d e2 = toEigen(p1 - p0); |
|
// gradient unit vectors |
|
Eigen::Vector3d g0 = n.cross(e0); //v0 grad |
|
Eigen::Vector3d g1 = n.cross(e1); //v1 grad |
|
Eigen::Vector3d g2 = n.cross(e2); //v2 grad |
|
|
|
// add vertex gradient contributions |
|
Eigen::Vector3d tri_grad = (g0 * scalarField(i0) + g1 * scalarField(i1) + g2 * scalarField(i2)) / (2 * faceArea); |
|
|
|
faceGradientField.row(i) = tri_grad; |
|
} |
|
return faceGradientField; |
|
} |
|
|
|
/** @brief Computes the per-row normalization of a given vector field |
|
* @param field a vector field |
|
* @return Eigen::MatrixX3d the per-row normalized vector field |
|
*/ |
|
static Eigen::MatrixX3d normalizeVectorField(const Eigen::MatrixX3d &field){ |
|
Eigen::MatrixX3d normalizedField(field.rows(), 3); |
|
// normalize vector field at each vertex |
|
for (int i = 0; i < field.rows(); ++i){ |
|
Eigen::Vector3d v = field.row(i); |
|
normalizedField.row(i) = v / v.norm(); |
|
} |
|
return normalizedField; |
|
} |
|
|
|
/** @brief Computes the per-vertex divergence of a (normalized) per-face gradient (vector field) |
|
* @param mesh the mesh |
|
* @param field a vector field of size (FN, 3) |
|
* @return Eigen::VectorXd the per-vertex divergence |
|
*/ |
|
static Eigen::VectorXd computeVertexDivergence(MeshType &mesh, const Eigen::MatrixX3d &field){ |
|
Eigen::VectorXd divergence(mesh.VN()); |
|
divergence.setZero(); |
|
|
|
// compute divergence of vector field at each vertex |
|
for (int i = 0; i < mesh.VN(); ++i){ |
|
VertexPointer vp = &mesh.vert[i]; |
|
|
|
std::vector<FacePointer> faces; |
|
std::vector<int> indices; |
|
vcg::face::VFStarVF<FaceType>(vp, faces, indices); |
|
for (int j = 0; j < faces.size(); ++j) |
|
{ |
|
FacePointer fp = faces[j]; |
|
int index = indices[j]; |
|
|
|
vcg::Point3f p0 = fp->V(0)->P(); |
|
vcg::Point3f p1 = fp->V(1)->P(); |
|
vcg::Point3f p2 = fp->V(2)->P(); |
|
|
|
// edge vectors |
|
Eigen::Vector3d el, er, eo; //left, right, opposite to vp |
|
if (index == 0){ |
|
el = toEigen(p2 - p0); //-e1 |
|
er = toEigen(p1 - p0); //e2 |
|
eo = toEigen(p2 - p1); //e0 |
|
} else if (index == 1){ |
|
el = toEigen(p0 - p1); //-e2 |
|
er = toEigen(p2 - p1); //e0 |
|
eo = toEigen(p0 - p2); //e1 |
|
} else if (index == 2){ |
|
el = toEigen(p1 - p2); //-e0 |
|
er = toEigen(p0 - p2); //e1 |
|
eo = toEigen(p1 - p0); //e2 |
|
} |
|
// compute left and right cotangents |
|
double cotl = cotan(-el, -eo); |
|
double cotr = cotan(-er, eo); |
|
// add divergence contribution of given face |
|
Eigen::Vector3d x = field.row(vcg::tri::Index(mesh,fp)); |
|
divergence(i) += (cotl * er.dot(x) + cotr * el.dot(x)) / 2; |
|
} |
|
} |
|
return divergence; |
|
} |
|
|
|
/** |
|
* @brief Computes an approximated geodesic distance using the heat method. |
|
* |
|
* @param mesh the mesh |
|
* @param sources a vector of source points |
|
* @param m a parameter regulating the timestep taken in the backward Euler method (default = 1) |
|
* @return true in case the computation was successful |
|
* |
|
* Approximated geodesic distance is stored in the vector quality field. |
|
* If the underlying factorization fails or the linear system cannot be solved false is returned. |
|
* |
|
* THIS METHOD IS NOT APPROPRIATE FOR MULTIPLE CALLS ON DIFFERENT SOURCE POINTS. |
|
* If multiple calls are required its best to first build the factorizations with |
|
* GeodesicHeat::BuildCache(mesh, m) then call ComputeFromCache(mesh, source, cache) |
|
* this will avoid recomputing the underlying factorizations for each set of source points. |
|
* |
|
*/ |
|
static bool Compute(MeshType &mesh, const std::vector<VertexPointer> sources, float m = 1){ |
|
vcg::tri::RequireVFAdjacency<MeshType>(mesh); |
|
vcg::tri::RequireFFAdjacency<MeshType>(mesh); |
|
vcg::tri::RequirePerFaceNormal<MeshType>(mesh); |
|
vcg::tri::RequirePerFaceQuality<MeshType>(mesh); |
|
vcg::tri::RequirePerVertexQuality<MeshType>(mesh); |
|
|
|
// initialization of required components |
|
vcg::tri::Allocator<MeshType>::CompactEveryVector(mesh); |
|
vcg::tri::UpdateTopology<MeshType>::VertexFace(mesh); |
|
vcg::tri::UpdateTopology<MeshType>::FaceFace(mesh); |
|
vcg::tri::UpdateNormal<MeshType>::PerFaceNormalized(mesh); |
|
|
|
if(sources.empty()) return false; |
|
|
|
Eigen::VectorXd sourcePoints(mesh.VN()); |
|
sourcePoints.setZero(); |
|
for(VertexPointer vp : sources){ |
|
sourcePoints(vcg::tri::Index(mesh, vp)) = 1; |
|
} |
|
|
|
Eigen::SparseMatrix<double> massMatrix; |
|
Eigen::SparseMatrix<double> cotanMatrix; |
|
double averageEdgeLength; |
|
|
|
buildMassMatrix(mesh, massMatrix); |
|
buildCotanLowerTriMatrix(mesh, cotanMatrix); |
|
averageEdgeLength = computeAverageEdgeLength(mesh); |
|
|
|
Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> solver1; |
|
Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> solver2; |
|
|
|
// core of the heat method |
|
double timestep = m * averageEdgeLength * averageEdgeLength; |
|
solver1.compute(massMatrix - timestep * cotanMatrix); |
|
if (solver1.info() != Eigen::Success) return false; |
|
solver2.compute(cotanMatrix); |
|
if (solver2.info() != Eigen::Success) return false; |
|
|
|
Eigen::VectorXd heatflow = solver1.solve(sourcePoints); // (VN) |
|
if (solver1.info() != Eigen::Success) return false; |
|
Eigen::MatrixX3d heatGradient = computeFaceGradient(mesh, heatflow); // (FN, 3) |
|
Eigen::MatrixX3d unitVectorField = normalizeVectorField(-heatGradient); // (FN, 3) |
|
Eigen::VectorXd divergence = computeVertexDivergence(mesh, unitVectorField); // (VN) |
|
Eigen::VectorXd geodesicDistance = solver2.solve(divergence); // (VN) |
|
if (solver2.info() != Eigen::Success) return false; |
|
|
|
// shift to impose dist(source) = 0 |
|
geodesicDistance.array() -= geodesicDistance.minCoeff(); |
|
|
|
// save geodesic in vertex quaity field |
|
for(int i=0; i < mesh.VN(); i++){ |
|
mesh.vert[i].Q() = geodesicDistance(i); |
|
} |
|
return true; |
|
} |
|
|
|
// used to cache factorizations |
|
typedef typename std::pair< |
|
std::shared_ptr<Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>>>, |
|
std::shared_ptr<Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>>> |
|
> GeodesicHeatCache; |
|
|
|
/** |
|
* @brief Precomputes matrix factorizations required by the heat method |
|
* |
|
* @param mesh the mesh |
|
* @param m a parameter regulating the timestep taken in the backward Euler method (default = 1) |
|
* @return GeodesicHeatCache object containing the matrix factorizations |
|
* |
|
* This method returns the cache to use in ComputeFromCache. |
|
* Note that when the mesh changes this cache should be rebuilt. |
|
* |
|
* If the factorization fails no error is thrown (errors can be caught during ComputeFromCache). |
|
*/ |
|
static GeodesicHeatCache BuildCache(MeshType &mesh, float m = 1){ |
|
vcg::tri::RequireVFAdjacency<MeshType>(mesh); |
|
vcg::tri::RequireFFAdjacency<MeshType>(mesh); |
|
vcg::tri::RequirePerFaceNormal<MeshType>(mesh); |
|
vcg::tri::RequirePerFaceQuality<MeshType>(mesh); |
|
vcg::tri::RequirePerVertexQuality<MeshType>(mesh); |
|
|
|
// initialization of required components |
|
vcg::tri::Allocator<MeshType>::CompactEveryVector(mesh); |
|
vcg::tri::UpdateTopology<MeshType>::VertexFace(mesh); |
|
vcg::tri::UpdateTopology<MeshType>::FaceFace(mesh); |
|
vcg::tri::UpdateNormal<MeshType>::PerFaceNormalized(mesh); |
|
|
|
GeodesicHeatCache cache = std::make_pair( |
|
std::make_shared<Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>>>(), |
|
std::make_shared<Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>>>() |
|
); |
|
|
|
Eigen::SparseMatrix<double> massMatrix; |
|
Eigen::SparseMatrix<double> cotanMatrix; |
|
double averageEdgeLength; |
|
|
|
buildMassMatrix(mesh, massMatrix); |
|
buildCotanLowerTriMatrix(mesh, cotanMatrix); |
|
averageEdgeLength = computeAverageEdgeLength(mesh); |
|
|
|
// compute factorizations |
|
double timestep = m * averageEdgeLength * averageEdgeLength; |
|
std::get<0>(cache)->compute(massMatrix - timestep * cotanMatrix); |
|
std::get<1>(cache)->compute(cotanMatrix); |
|
|
|
return cache; |
|
} |
|
|
|
/** |
|
* @brief Computes an approximated geodesic distance using the heat method. |
|
* |
|
* @param mesh the mesh |
|
* @param sources a vector of source points |
|
* @param cache an (up to date) GeodesicHeatCache of the mesh |
|
* @return true if computation was successful |
|
* |
|
* We assume that the face quality field has not changed after BuildCache was called. |
|
* |
|
* Approximated geodesic distance is stored in the vector quality field. |
|
*/ |
|
static bool ComputeFromCache(MeshType &mesh, const std::vector<VertexPointer> sources, GeodesicHeatCache &cache){ |
|
if(sources.empty()) return false; |
|
|
|
Eigen::VectorXd sourcePoints(mesh.VN()); |
|
sourcePoints.setZero(); |
|
for(VertexPointer vp : sources){ |
|
sourcePoints(vcg::tri::Index(mesh, vp)) = 1; |
|
} |
|
|
|
Eigen::VectorXd heatflow = std::get<0>(cache)->solve(sourcePoints); // (VN) |
|
if (std::get<0>(cache)->info() != Eigen::Success) return false; |
|
Eigen::MatrixX3d heatGradient = computeFaceGradient(mesh, heatflow); // (FN, 3) |
|
Eigen::MatrixX3d unitVectorField = normalizeVectorField(-heatGradient); // (FN, 3) |
|
Eigen::VectorXd divergence = computeVertexDivergence(mesh, unitVectorField); // (VN) |
|
Eigen::VectorXd geodesicDistance = std::get<1>(cache)->solve(divergence); // (VN) |
|
if (std::get<1>(cache)->info() != Eigen::Success) return false; |
|
|
|
// shift to impose dist(source) = 0 |
|
geodesicDistance.array() -= geodesicDistance.minCoeff(); |
|
|
|
// save geodesic in vertex quality field |
|
for(int i=0; i < mesh.VN(); i++){ |
|
mesh.vert[i].Q() = geodesicDistance(i); |
|
} |
|
return true; |
|
} |
|
|
|
private: |
|
static inline Eigen::Vector3d toEigen(const vcg::Point3f& p){ |
|
return Eigen::Vector3d(p.X(), p.Y(), p.Z()); |
|
} |
|
static inline double cotan(const Eigen::Vector3d& v0, const Eigen::Vector3d& v1){ |
|
// cos(theta) / sin(theta) |
|
return v0.dot(v1) / v0.cross(v1).norm(); |
|
} |
|
}; |
|
|
|
} |
|
} |
|
|
|
#endif
|
|
|