这里要跟大家分享的是2013年Siggraph上面的一篇paper,名为《Geodesics in Heat:A New Approach to Computing Distance Based on Heat Flow》,这篇paper没有提供源代码,但是因为算法的思想相当新颖,如果你之前有研究过其它的测地三角网格曲面上的测地距离算法,那么看到这篇paper后,你会非常的激动,觉得这个算法相当神奇,网格曲面上测地距离的计算方法又有了新的突破。因为看到这篇paper非常激动,以至于我兴奋得马上去把代码写了一遍,虽然测地距离的计算方法,对于我来说没什么用,但它的想法,它的思想值得我们学习。
《Geodesics in Heat:A New Approach to Computing Distance Based on Heat Flow》算法主要是通过向量场的方法,通过求解热流,求解泊松方程。这篇paper我觉得应该把它归类为向量场类型的文献,曲面上向量场的应用非常广泛,可以用于网格优化重建、参数化纹理映射、网格变形……等,如今这篇paper又让我明白向量场也可以求解测地距离。通过热量传播的方法,去求解测地距离,真是大牛啊
一、理论知识
在很早之前对于测地距离,大牛们就推导出了测地距离的求解归结为求解eikonal equation(程函方程):
且满足边界约束条件为:
上面符号Φ就是测地距离。
也就是不管是欧式空间还是曲面空间,其测地距离的梯度模长恒等于1,然后边界条件:源点的测地距离值为0。
因此很多大牛,都是针对上面的程函方程的求解进行研究,然而所提出的算法都是非线性的方法,因为上面的方程就是非线性方程,在网格曲面上计算量非常大。而这篇paper作者的真正贡献在于把非线性问题转换为线性问题,到最后只需要求解一个泊松方程就可以了。开始这个算法之前,建议先看一下《Mesh Editing with Poisson-Based Gradient Field Manipulation》这篇利用泊松方程进行三角网格模型编辑,其实这篇paper的思想应该是借用了《Poisson Image Editing》,如果你已经很熟悉《Mesh Editing with Poisson-Based Gradient Field Manipulation》那么看这篇paper将事半功倍。
1、时间离散化,热传播方程时间离散化为:
2、空间离散化,在网格曲面上,离散的拉普拉斯坐标定义为:
Ai表示三角面片的面积的三份之一,j表示顶点i的邻接顶点。对于有V个顶点的网格模型,我们可以列出上面的V个方程,写出矩阵形式为:
其中,Lc为拉普拉矩阵,A为包含每个顶点面积的对角矩阵。
代入公式(3)可得:
在网格曲面上,对于一个给定的三角面片平面上,标量场的梯度计算公式如下:
Af表示三角形的面积,N表示三角面片的法矢,ui就是标量场,我们可以把网格曲面每个顶点的测地距离看成是一个标量场。
顶点i的散度离散形式为:
懒得多说废话了,总之到最后归结为求解如下方程:
其中b我们需要先通过求解标量场u,然后根据散度的计算公式,计算出顶点测地距离的散度。
二、算法实现
算法总流程:
示意图:
其中,Φ就是我们要求的测地距离,t是一个无穷小的数,趋近于0
算法具体实现,先说明一下,我下面自己写的代码很乱,懒得整理,因为我只是为了学习:
1、求解u
根据公式:
求解u,因此我们需要先算出A,t,Lc,还有δ。接着我将逐渐讲解着四个参数的求解:
a、时间t计算:见文献3.2.4,时间理论上来说是一个非常小的数,文中作者给出了其合适的值为:网格平均边长的平方
代码实现如下:
//计算时间步长,文献中时间步长为:网格模型的边长平均,然后平方 void CHeatGeodesics::Set_Time() { m_BaseMesh->need_edge(); int en=m_BaseMesh->m_edges.size(); float sumlength=0; for (int i=0;i<en;i++) { sumlength+=m_BaseMesh->m_edges[i].length(); } sumlength=sumlength/en;//边长的平均长度 sumlength=sumlength*sumlength; m_Time_Step=sumlength; }b、面积对角矩阵A的计算:
//计算Geodesics in Heat 文献中的包含顶点Vi的面积矩阵 void CHeatGeodesics::Get_Matrix_A_areas() { m_BaseMesh->need_adjacentfaces(); gsi::SparseMatrix &A=m_A_areas; A.Resize(m_NumberV,m_NumberV); //计算每个三角面片的面积 int fn=m_BaseMesh->faces.size(); vector<float>&face_area=m_Faces_Area; face_area.resize(fn); for (int i=0;i<fn;i++) { TriMesh::Face &f=m_BaseMesh->faces[i]; vec vij=m_BaseMesh->vertices[f[1]]-m_BaseMesh->vertices[f[0]]; vec vik=m_BaseMesh->vertices[f[2]]-m_BaseMesh->vertices[f[0]]; float areaf=0.5*len(vij CROSS vik); face_area[i]=areaf; } //计算拉普拉斯算子中每个顶点所占据的面积,即邻接三角面片面积总和的三分之一 for (int i=0;i<m_NumberV;i++) { vector<int>&af=m_BaseMesh->adjacentfaces[i]; int n_af=af.size(); float sumarea=0.0; for (int j=0;j<n_af;j++) { sumarea+=face_area[af[j]]; } //包含顶点的面积为:邻接三角面片面积总和的三分之一 sumarea=sumarea/3.0; A(i,i) =sumarea; } }C、拉普拉斯矩阵Lc计算:
//计算Geodesics in Heat 文献中的Lc矩阵,即拉普拉斯算子 void CHeatGeodesics::Get_Matrix_Lc() { Ls.resize(m_NumberV,m_NumberV); int m_nEdges=10000 ; Ls.reserve(m_nEdges+m_NumberV); for (int i = 0;i<m_NumberV;++i) { VProperty & vi = m_vertices[i]; int nNbrs = vi.VNeighbors.size(); for (int k = 0;k<nNbrs;++k) { Ls.insert(i, vi.VNeighbors[k]) = vi.VNeiWeight[k]; } Ls.insert(i, i) = -vi.VSumWeight; } Ls.finalize(); gsi::SparseMatrix &A=m_Lc; A.Resize(m_NumberV,m_NumberV); for(int k=0;k<m_NumberV;++k) { for ( SparseMatrixType::InnerIterator it(Ls,k); it; ++it) { A.Set( it.row(), it.col(), it.value() ); } } }
邻接顶点的余切cot权重计算:
//邻接顶点的余切权重计算 void CHeatGeodesics::CotangentWeights(TriMesh*TMesh,int vIndex,vector<float>&vweight,float &WeightSum,bool bNormalize)//计算一阶邻近点的各自cottan权重 { int NeighborNumber=TMesh->neighbors[vIndex].size(); vweight.resize(NeighborNumber); WeightSum=0; vector<int>&NeiV=TMesh->neighbors[vIndex]; for (int i=0;i<NeighborNumber;i++) { int j_nei=NeiV[i]; vector<int>tempnei; Co_neighbor(TMesh,vIndex,j_nei,tempnei); float cotsum=0.0; for (int j=0;j<tempnei.size();j++) { vec vivo=TMesh->vertices[vIndex]-TMesh->vertices[tempnei[j]]; vec vjvo=TMesh->vertices[j_nei]-TMesh->vertices[tempnei[j]]; float dotvector=vivo DOT vjvo; dotvector=dotvector/sqrt(len2(vivo)*len2(vjvo)-dotvector*dotvector); cotsum+=dotvector; } vweight[i]=cotsum/2.0; WeightSum+=vweight[i]; } if ( bNormalize ) { for (int k=0;k<NeighborNumber;++k) { vweight[k]/=WeightSum; } WeightSum=1.0; } } void CHeatGeodesics:: UniformWeights(TriMesh*TMesh,int vIndex,vector<float>&vweight,float &WeightSum,bool bNormalize) { int NeighborNumber=TMesh->neighbors[vIndex].size(); vweight.resize(NeighborNumber); WeightSum = 0; for (int j = 0; j <NeighborNumber; ++j ) { vweight[j] = 1; WeightSum += vweight[j]; } if ( bNormalize ) { for ( int k = 0; k < NeighborNumber; ++k ) vweight[k] /= WeightSum; WeightSum=1.0; } } //获取两顶点的共同邻接顶点 void CHeatGeodesics::Co_neighbor(TriMesh *Tmesh,int u_id,int v_id,vector<int>&co_neiv) { Tmesh->need_adjacentedges(); vector<int>&u_id_ae=Tmesh->adjancetedge[u_id]; int en=u_id_ae.size(); Tedge Co_Edge; for (int i=0;i<en;i++) { Tedge &ae=Tmesh->m_edges[u_id_ae[i]]; int opsi=ae.opposite_vertex(u_id); if (opsi==v_id) { Co_Edge=ae; break; } } for (int i=0;i<Co_Edge.m_adjacent_faces.size();i++) { TriMesh::Face af=Co_Edge.m_adjacent_faces[i]; for (int j=0;j<3;j++) { if((af[j]!=u_id)&&(af[j]!=v_id)) { co_neiv.push_back(af[j]); } } } }
最后求解方程组,就可以把u求出来了。
2、求解三角网格曲面的热量场▽u (Heat flow):
这一步直接根据公式:
求解就可以了。然后对▽u进行归一化,并取热量场的反方向,即求测地距离的梯度场:
for (int i=0;i<fn;i++) { TriMesh::Face &f=m_BaseMesh->faces[i]; for (int j=0;j<3;j++) { vec ei=m_BaseMesh->vertices[f[(j+2)%3]]-m_BaseMesh->vertices[f[(j+1)%3]]; vec FNormal=normalize(m_BaseMesh->FaceNormal[i]); vec gradient=float(m_vertices[f[j]].VU*0.5/m_Faces_Area[i])*(FNormal CROSS ei); FGradient[i]=FGradient[i]+gradient; } normalize(FGradient[i]);//文献的重要两步 即梯度单位化和梯度反向 FGradient[i]=float(-1.0)*FGradient[i]; //length0[i]=len(FGradient[i]); }
根据公式:
求解测地距离标量场梯度场的散度。
//计算顶点的散度 for (int i=0;i<vn;i++) { vector<int>&adjacentface=m_BaseMesh->adjacentfaces[i]; for (int j=0;j<adjacentface.size();j++) { TriMesh::Face &f=m_BaseMesh->faces[adjacentface[j]]; for (int k=0;k<3;k++) { if (f[k]==i) { vec ei=m_BaseMesh->vertices[f[(k+2)%3]]-m_BaseMesh->vertices[f[(k+1)%3]]; /*vec gradient=float(0.5/m_Faces_Area[adjacentface[j]])*(m_BaseMesh->FaceNormal[adjacentface[j]] CROSS ei); //计算散度 m_vertices[i].VDivergence+=(FGradient[adjacentface[j]] DOT gradient)*m_Faces_Area[adjacentface[j]];*/ vec e1=m_BaseMesh->vertices[f[(k+1)%3]]-m_BaseMesh->vertices[f[k]]; vec e2=m_BaseMesh->vertices[f[(k+2)%3]]-m_BaseMesh->vertices[f[k]]; float cot_angle1=Cot_angle(e2,ei); float cot_angle2=Cot_angle(float(-1.0)*e1,ei); m_vertices[i].VDivergence+=0.5*(cot_angle1*(e1 DOT FGradient[adjacentface[j]])+cot_angle2*(e2 DOT FGradient[adjacentface[j]])); break; } } } }4、最后求解通过求解泊松方程,求取网格曲面上每个顶点的测地距离。
gsi::SparseLinearSystem *pSystemPos2 = new gsi::SparseLinearSystem(); gsi::Solver_TAUCS * pSolverPos2 = new gsi::Solver_TAUCS(pSystemPos2); pSystemPos2->Resize(m_NumberV, m_NumberV); pSystemPos2->ResizeRHS(1); //由于以下解方程组使用Solver_TAUCS::TAUCS_LLT ,即半正定矩阵模式 m_Lc.Multiply(-1.0); m_Lc(0,0) =m_Lc(0,0) +10; pSystemPos2->SetMatrix(m_Lc); if ( ! pSystemPos2->Matrix().IsSymmetric() )assert(0); pSolverPos2->OnMatrixChanged(); pSolverPos2->SetStoreFactorization(true); pSolverPos2->SetSolverMode( gsi::Solver_TAUCS::TAUCS_LLT ); pSolverPos2->SetOrderingMode( gsi::Solver_TAUCS::TAUCS_METIS ); gsi::Vector pRHSPos2 ; pRHSPos2.Resize(m_NumberV); // 右边项约束添加 for (int i=0;i<m_NumberV;i++) { pRHSPos2[i]=float(-1.0)*m_vertices[i].VDivergence; } //初始条件为方程热源的测地距离为0 pRHSPos2[0]=pRHSPos2[0]+10; pSystemPos2->SetRHS(0, pRHSPos2); bool boksoveLs2=pSolverPos2->Solve(); if (!boksoveLs2)assert(0); m_GeodesicsDistance.resize(m_NumberV); for ( int i=0;i<m_NumberV;++i) { m_GeodesicsDistance[i]=(float)pSystemPos2->GetSolution(i,0); }
可以说到了这里算法已经结束了,当然paper后面还有后续的处理,比如距离光顺,还有边界条件的等问题。但都属于后处理,你如果要实现跟paper一模一样的效果,还是得好好看一看后面的边界条件问题。
边界条件对于结果的影响还是蛮大的。
OK,做一下简单的总结:算法整个过程就说白了就是求解一个泊松方程,说的更白一点,就是求解一个大型的方程组AX=b,因此我们的目标就是先算出A、b,其中对于一个给定的网格曲面模型来说,A就是拉普拉斯矩阵,是固定的。b的求解就是整个算法成功的关键。本文地址:http://blog.csdn.net/hjimce/article/details/46415499 作者:hjimce 联系qq:1393852684 更多资源请关注我的博客:http://blog.csdn.net/hjimce 原创文章,转载请保留本行信息。
参考文献:
1、《Geodesics in Heat:A New Approach to Computing Distance Based on Heat Flow》
2、《Mesh Editing with Poisson-Based Gradient Field Manipulation》
3、《Poisson Image Editing》