网格平滑
网格平滑属于数字几何处理领域的问题,计算机图形学和计算机辅助设计中,用多边形网格可以表示复杂的三维实体。随着三维扫描和曲面重建技术的发展,得到这些实体表面的多边形网格表示已经不是难事,但所得到的表面往往包含含噪声。在形状设计领域,在散乱点拟合和光滑形伏、纹理映射等应用领域,都有对平滑曲面的极大需求。故产生了网格平滑这一个研究点。
平滑前 | 平滑后 |
有很多相关的论文都在探讨各式各样的网格平滑算法,并从一些几何理论上去评估这些算法的好坏,这方面的论文资料是相当多的,所以本文不主要专注于介绍平滑算法的原理,而是更多的关注算法的实现。
网格数据结构基础
在前面的2篇文章中介绍了关于三角网格的数据结构表示,根据前面的论述,一种最为普遍的三角网格表示法是点集+三角片集:
struct Point3d { public: float X; float Y; float Z; Point3d():X(0),Y(0),Z(0){} Point3d(float x, float y, float z):X(x),Y(y),Z(z){} }; struct Triangle { public : long P0Index; long P1Index; long P2Index; Triangle(long p0index, long p1index, long p2index):P0Index(p0index),P1Index(p1index),P2Index(p2index){} Triangle():P0Index(-1),P1Index(-1),P2Index(-1){}
public: bool HasVertex(long index) { return P0Index==index||P1Index==index||P2Index==index; } long GetOtherIndex(long i1,long i2) { return P0Index+P1Index+P2Index-i1-i2; } };
class Mesh { public: std::vector<Point3d> Vertices; std::vector<Triangle> Faces; Mesh(); ~Mesh(); long AddVertex(Point3d& toAdd); long AddFace(Triangle& tri); }; long Mesh::AddVertex(Point3d& toAdd) { long index = (long)Vertices.size(); Vertices.push_back(toAdd); return index; } long Mesh::AddFace(Triangle& tri) { long index = (long)Faces.size(); Faces.push_back(tri); return index; }
平滑的算法多数是基于伞状结构的操作,所谓伞状结构,是以三角网格中某一个顶点P为中心,取所有与其相邻的顶点P1...Pn-1和边组成的一阶邻域结构。如下图所示:
为了方便于在O(1)的时间内获取点P的相邻点与相邻面,故需要提供辅助结构来存储这些邻接点面的信息,故用上面那个简单的Mesh定义就不行了,应该将Mesh数据结构按如下方式定义,增加点邻接信息的存储。为实现这些,定义了PointAttachmentInfo结构和新的Mesh类,相应的代码如下:
//点邻接信息结构,每一个顶点对应一个这个结构 struct PointAttachmentInfo { public: PointAttachmentInfo(); ~PointAttachmentInfo(); void ClearVertexAdj(); void ClearTriangleAdj(); std::vector<int>* VertexAdjacencyList;//该顶点的相邻点索引 std::vector<int>* TriangleAdjacencyList;//该顶点的相邻面索引 }; PointAttachmentInfo::PointAttachmentInfo() { VertexAdjacencyList=NULL; TriangleAdjacencyList=NULL; } PointAttachmentInfo::~PointAttachmentInfo() { ClearVertexAdj(); ClearTriangleAdj(); } void PointAttachmentInfo::ClearVertexAdj() { if(VertexAdjacencyList!=NULL) { delete VertexAdjacencyList; VertexAdjacencyList=NULL; } } void PointAttachmentInfo::ClearTriangleAdj() { if(TriangleAdjacencyList!=NULL) { delete TriangleAdjacencyList; VertexAdjacencyList=NULL; } } //带各个顶点邻接信息的Mesh结构 class Mesh { public: std::vector<Point3d> Vertices;//顶点列表 std::vector<Triangle> Faces;//面列表 std::vector<PointAttachmentInfo> AdjInfos;//顶点邻接信息表,长度等于顶点列表 bool GetIsPerVertexVertexInfoEnabled(); bool GetIsPerVertexTriangleInfoEnabled(); Mesh(); ~Mesh(); long AddVertex(Point3d& toAdd); long AddFace(Triangle& tri); void InitPerVertexVertexAdj();//初始化点点邻接关系 void InitPerVertexTriangleAdj();//初始化点面邻接关系 void ClearPerVertexVertexAdj(); void ClearPerVertexTriangleAdj(); private: bool IsPerVertexVertexInfoEnabled; bool IsPerVertexTriangleInfoEnabled; }; Mesh::Mesh() { IsPerVertexTriangleInfoEnabled=false; IsPerVertexVertexInfoEnabled=false; } Mesh::~Mesh() { if(IsPerVertexTriangleInfoEnabled) ClearPerVertexTriangleAdj(); if(IsPerVertexVertexInfoEnabled) ClearPerVertexVertexAdj(); } long Mesh::AddVertex(Point3d& toAdd) { long index = (long)Vertices.size(); Vertices.push_back(toAdd); return index; } long Mesh::AddFace(Triangle& tri) { long index = (long)Faces.size(); Faces.push_back(tri); return index; } void Mesh::InitPerVertexVertexAdj() { if(IsPerVertexVertexInfoEnabled) ClearPerVertexVertexAdj(); IsPerVertexVertexInfoEnabled = true; if(AdjInfos.size()!=Vertices.size()) AdjInfos.resize(Vertices.size()); size_t vcount=Vertices.size(); size_t fcount= Faces.size(); for (size_t i = 0; i < vcount; i++) { std::vector<long>* vertexAdjacencyList= new (std::nothrow)std::vector<long>(); if(vertexAdjacencyList==NULL){return;} vertexAdjacencyList->reserve(6); AdjInfos[i].VertexAdjacencyList=vertexAdjacencyList; } for (size_t i = 0; i < fcount; i++) { Triangle &t = Faces[i]; std::vector<long> *p0list= AdjInfos[t.P0Index].VertexAdjacencyList; std::vector<long> *p1list= AdjInfos[t.P1Index].VertexAdjacencyList; std::vector<long> *p2list= AdjInfos[t.P2Index].VertexAdjacencyList; if (std::find(p0list->begin(), p0list->end(), t.P1Index)==p0list->end()) p0list->push_back(t.P1Index); if (std::find(p0list->begin(), p0list->end(), t.P2Index)==p0list->end()) p0list->push_back(t.P2Index); if (std::find(p1list->begin(), p1list->end(), t.P0Index)==p1list->end()) p1list->push_back(t.P0Index); if (std::find(p1list->begin(), p1list->end(), t.P2Index)==p1list->end()) p1list->push_back(t.P2Index); if (std::find(p2list->begin(), p2list->end(), t.P0Index)==p2list->end()) p2list->push_back(t.P0Index); if (std::find(p2list->begin(), p2list->end(), t.P1Index)==p2list->end()) p2list->push_back(t.P1Index); } } void Mesh::InitPerVertexTriangleAdj() { if(IsPerVertexTriangleInfoEnabled) ClearPerVertexTriangleAdj(); IsPerVertexTriangleInfoEnabled = true; if(AdjInfos.size()!=Vertices.size()) AdjInfos.resize(Vertices.size()); for (size_t i = 0; i < Vertices.size(); i++) { std::vector<long>* triangleAdjacencyList = new (std::nothrow)std::vector<long>(); if(triangleAdjacencyList==NULL){return;} triangleAdjacencyList->reserve(6); AdjInfos[i].TriangleAdjacencyList=triangleAdjacencyList; } for (size_t i = 0; i < Faces.size(); i++) { Triangle& t = Faces[i]; std::vector<long> *t0list= AdjInfos[t.P0Index].TriangleAdjacencyList; std::vector<long> *t1list= AdjInfos[t.P1Index].TriangleAdjacencyList; std::vector<long> *t2list= AdjInfos[t.P2Index].TriangleAdjacencyList; t0list->push_back(i); t1list->push_back(i); t2list->push_back(i); } } void Mesh::ClearPerVertexVertexAdj() { if(!IsPerVertexVertexInfoEnabled) return; for (size_t i = 0; i < Vertices.size(); i++) { if (AdjInfos[i].VertexAdjacencyList != NULL) { delete AdjInfos[i].VertexAdjacencyList; AdjInfos[i].VertexAdjacencyList=NULL; } } IsPerVertexVertexInfoEnabled = false; } void Mesh::ClearPerVertexTriangleAdj() { if(!IsPerVertexTriangleInfoEnabled) return; for (size_t i = 0; i < Vertices.size(); i++) { if (AdjInfos[i].TriangleAdjacencyList != NULL) { delete AdjInfos[i].TriangleAdjacencyList; AdjInfos[i].TriangleAdjacencyList=NULL; } } IsPerVertexTriangleInfoEnabled = false; } bool Mesh::GetIsPerVertexVertexInfoEnabled() { return IsPerVertexVertexInfoEnabled; } bool Mesh::GetIsPerVertexTriangleInfoEnabled() { return IsPerVertexTriangleInfoEnabled; }
拉普拉斯平滑
在前面的博客中,介绍了最为基础的拉普拉斯平滑算法的实现,简单的拉普拉斯平滑算法的原理是将每个顶点都移动到相邻顶点的平均位置,即采用所谓伞状算子:
在伞状结构中表示这样的过程如下图:
其具体的实现逻辑表述如下:
- 初始化Mesh的邻接点结构集
- 新建临时点集,用来存储点平滑后的位置
- 对所有Mesh中的顶点P
- 初始化临时向量为零向量
- 获取P的邻域点集Adj(P)
- 对所有领域点T,将其位置加到临时向量里
- 临时向量/=领域点集数
- 将临时向量的位置存入临时点集
- 对所有Mesh中的顶点P
- 将P的位置修改为临时点集中对应点的位置
用代码实现如下:
class Smoothing { private: Mesh* mesh; public: Smoothing(Mesh* m) { this->mesh=m; m->InitPerVertexVertexAdj(); } ~Smoothing() { this->mesh=NULL; } void Laplacian() { Point3d* tempList=new Point3d[mesh->Vertices.size()]; for(size_t i=0;i<mesh->Vertices.size();i++) { tempList[i]=GetSmoothedVertex_Laplacian(i); } for(size_t i=0;i<mesh->Vertices.size();i++) { mesh->Vertices[i]=tempList[i]; } delete[] tempList; } Point3d GetSmoothedVertex_Laplacian(size_t index) { float nx=0,ny=0,nz=0; std::vector<long>& adjVertices=*(this->mesh->AdjInfos[index].VertexAdjacencyList); if(adjVertices.size()==0) return mesh->Vertices[index]; Point3d& P=mesh->Vertices[index]; for(size_t i=0;i<adjVertices.size();i++) { nx+=mesh->Vertices[adjVertices[i]].X; ny+=mesh->Vertices[adjVertices[i]].Y; nz+=mesh->Vertices[adjVertices[i]].Z; } nx/=adjVertices.size(); ny/=adjVertices.size(); nz/=adjVertices.size();return Point3d(nx,ny,nz); } }
拉普拉斯平滑算法有很多进一步的变形,首先在求取平均位置时,可以采用不同的加权策略,例如对不同的邻接点采用不同的权值。一般来说,距离中心点P较远的邻接点,我们可以让他对P平滑后的位置影响小一点。这样就可以采用一种距离的倒数为权值的拉普拉斯平滑。
有时为了控制平滑的速率,也会引入参数lambda来控制平滑的速率,即从原来所执行的:
转变成同时,平滑算法往往可以反复对Mesh执行,使得Mesh越来越光顺,迭代次数T也是平滑算法中重要的参数。这样,考虑以上参数来实现的拉普拉斯平滑算法的代码如下:
class Smoothing { private: Mesh* mesh; public: Smoothing(Mesh* m) { this->mesh=m; m->InitPerVertexVertexAdj(); } ~Smoothing() { this->mesh=NULL; } public: void ScaleDependentLaplacian(int iterationTime,float lambda=1.0) { Point3d* tempList=new Point3d[mesh->Vertices.size()]; for(int c=0;c<iterationTime;c++) { for(size_t i=0;i<mesh->Vertices.size();i++) { tempList[i]=GetSmoothedVertex_ScaleDependentLaplacian(i,lambda); } for(size_t i=0;i<mesh->Vertices.size();i++) { mesh->Vertices[i]=tempList[i]; } } delete[] tempList; } private: Point3d GetSmoothedVertex_ScaleDependentLaplacian(size_t index,float lambda=1.0f) { float dx=0,dy=0,dz=0; std::vector<long>& adjVertices=*(this->mesh->AdjInfos[index].VertexAdjacencyList); Point3d& p=mesh->Vertices[index]; if(adjVertices.size()==0) return mesh->Vertices[index]; float sumweight=0; for(size_t i=0;i<adjVertices.size();i++) { Point3d& t=mesh->Vertices[adjVertices[i]]; float weight=GetDistence(p,t); dx+=weight*(t.X-p.X); dy+=weight*(t.Y-p.Y); dz+=weight*(t.Z-p.Z); sumweight+=weight; } dx/=sumweight; dy/=sumweight; dz/=sumweight; float newx=lambda*dx+p.X; float newy=lambda*dy+p.Y; float newz=lambda*dz+p.Z; return Point3d(newx,newy,newz); } float GetWeight(size_t index,size_t adjIndex,std::vector<long>& adjVertices) { Point3d& p=mesh->Vertices[index]; Point3d& t=mesh->Vertices[adjVertices[adjIndex]]; return 1.0f/(GetDistence(p,t)); } float GetDistence(Point3d& p1,Point3d& p2) { return (float)sqrt((p1.X-p2.X)*(p1.X-p2.X)+(p1.Y-p2.Y)*(p1.Y-p2.Y)+(p1.Z-p2.Z)*(p1.Z-p2.Z)); } };
上述代码中将计算权值的函数独立出来,这样通过修改函数可以实现自定义的计算权值的方案。
基于曲率的平滑
基于曲率的平滑与上面介绍的拉普拉斯平滑的区别在于,基于曲率的平滑是沿着顶点P的法向量所在直线去移动P的,从下图可以看出,一般的拉普拉斯平滑会讲点P移动到类似于重心的位置,而基于曲率的平滑的移动位置是与法向量方向相反的,这样做的目的是要更好的保持模型原来的大致形状。
通过适当的选择拉普拉斯平滑中的权值ω,我们可以让基于曲率的平滑与拉普拉斯平滑达到算法上的统一。经过推导可知ω选择如下值的时候,即等价于将P如上图2中进行移动。
其中α和β的是P与其第i个邻接点Pi所构成边所对的两个角,(这里假设网格为流形,即每条边都只有至多两个三角形共有),下图显示了这两个角的位置:
这样点P的位置仍然由如下公式来决定,只是修改了权值计算方式:
依据这个原理所实现的平滑算法,其代码只需修改GetWeight函数的实现方式:
class Smoothing { private: Mesh* mesh; public: Smoothing(Mesh* m) { this->mesh=m; m->InitPerVertexVertexAdj(); } ~Smoothing() { this->mesh=NULL; } public: void CurvatureFlow(int iterationTime) { if(!mesh->GetIsPerVertexTriangleInfoEnabled()) mesh->InitPerVertexTriangleAdj(); Point3d* tempList=new Point3d[mesh->Vertices.size()]; for(int c=0;c<iterationTime;c++) { for(size_t i=0;i<mesh->Vertices.size();i++) { tempList[i]=GetSmoothedVertex_CurvatureFlow(i); } for(size_t i=0;i<mesh->Vertices.size();i++) { mesh->Vertices[i]=tempList[i]; } } delete[] tempList; } private: Point3d GetSmoothedVertex_CurvatureFlow(size_t index) { float dx=0,dy=0,dz=0; std::vector<long>& adjVertices=*(this->mesh->AdjInfos[index].VertexAdjacencyList); std::vector<long>& adjFaces=*(this->mesh->AdjInfos[index].TriangleAdjacencyList); Point3d& p=mesh->Vertices[index]; if(adjVertices.size()==0||adjVertices.size()!=adjFaces.size()) return mesh->Vertices[index]; float sumweight=0; for(size_t i=0;i<adjVertices.size();i++) { Point3d& t=mesh->Vertices[adjVertices[i]]; float cotWeight=GetWeight(index,adjVertices[i],adjVertices,adjFaces); dx+=cotWeight*(t.X-p.X); dy+=cotWeight*(t.Y-p.Y); dz+=cotWeight*(t.Z-p.Z); sumweight+=cotWeight; } dx/=sumweight; dy/=sumweight; dz/=sumweight; float newx=dx+p.X; float newy=dy+p.Y; float newz=dz+p.Z; return Point3d(newx,newy,newz); } float GetWeight(size_t index,int adjindex,std::vector<long>& adjVertices,std::vector<long>& adjFaces) { float w=0; int count=0; for(size_t i=0;i<adjFaces.size();i++) { Triangle& t=mesh->Faces[adjFaces[i]]; if(t.HasVertex(adjindex)) { long otherIndex=t.GetOtherIndex(index,adjindex); float cot=GetCot(t,otherIndex); w+=cot; count++; } } if(count==0) return 0; w=w/count; return w; } float GetCot(Triangle& t,long index) { std::vector<Point3d>& v=mesh->Vertices; float cos; if(t.P0Index==index) { cos=GetCos(v[t.P0Index],v[t.P1Index],v[t.P2Index]); }else if(t.P1Index==index) { cos=GetCos(v[t.P1Index],v[t.P0Index],v[t.P2Index]); }else if(t.P2Index==index) { cos=GetCos(v[t.P2Index],v[t.P1Index],v[t.P0Index]); } return cos/sqrt(1-cos*cos); } float GetCos(Point3d& ps,Point3d& pe1,Point3d& pe2) { Vector pse1(pe1.X-ps.X,pe1.Y-ps.Y,pe1.Z-ps.Z); Vector pse2(pe2.X-ps.X,pe2.Y-ps.Y,pe2.Z-ps.Z); float mo1=sqrt(pse1.X*pse1.X+pse1.Y*pse1.Y+pse1.Z*pse1.Z); float mo2=sqrt(pse2.X*pse2.X+pse2.Y*pse2.Y+pse2.Z*pse2.Z); float mul=pse1.X*pse2.X+pse1.Y*pse2.Y+pse1.Z*pse2.Z; return mul/(mo1*mo2); } };
Taubin平滑
拉普拉斯平滑虽然能够让Mesh的表面光顺,但迭代次数一旦多了,就会使得模型整体发生收缩现象。
Taubin平滑的原理基于了一部分数字信号处理方面的知识,用简单的话来表述,就是使用一个负收缩因子μ将拉普拉斯平滑照成的收缩再放大回去,其算法的主体采用了2个与拉普拉斯平滑算法相似的过程,一个过程采用正因子λ(0~1),另一个过程采用负因子μ(-1~0),每一次迭代都必须重复这两个过程。即有:
使用代码实现之如下:
class Smoothing { private: Mesh* mesh; public: Smoothing(Mesh* m) { this->mesh=m; m->InitPerVertexVertexAdj(); } ~Smoothing() { this->mesh=NULL; } public: void Taubin(int iterationTime,float lambda,float mu) { Point3d* tempList=new Point3d[mesh->Vertices.size()]; for(int c=0;c<iterationTime;c++) { for(size_t i=0;i<mesh->Vertices.size();i++) { tempList[i]=GetSmoothedVertex_Taubin_Step(i,lambda); } for(size_t i=0;i<mesh->Vertices.size();i++) { mesh->Vertices[i]=tempList[i]; } for(size_t i=0;i<mesh->Vertices.size();i++) { tempList[i]=GetSmoothedVertex_Taubin_Step(i,mu); } for(size_t i=0;i<mesh->Vertices.size();i++) { mesh->Vertices[i]=tempList[i]; } } delete[] tempList; } private: Point3d GetSmoothedVertex_Taubin_Step(size_t index,float lambda) { float dx=0,dy=0,dz=0; std::vector<long>& adjVertices=*(this->mesh->AdjInfos[index].VertexAdjacencyList); Point3d& p=mesh->Vertices[index]; if(adjVertices.size()==0) return mesh->Vertices[index]; for(size_t i=0;i<adjVertices.size();i++) { Point3d& t=mesh->Vertices[adjVertices[i]]; dx+=(t.X-p.X); dy+=(t.Y-p.Y); dz+=(t.Z-p.Z); } dx/=adjVertices.size(); dy/=adjVertices.size(); dz/=adjVertices.size(); float newx=lambda*dx+p.X; float newy=lambda*dy+p.Y; float newz=lambda*dz+p.Z; return Point3d(newx,newy,newz); } };
HCLaplacian平滑
HCLaplacian平滑是一种对拉普拉斯平滑的改进方法,其针对的问题也是拉普拉斯算法产生的模型整体收缩问题,与Taubin算法不同,其不再是从信号处理的角度去看待拉普拉斯算法所引入的系统误差,HCLaplacian算法试图将拉普拉斯算法对顶点的移动以某种程度再移动回去,移动的具体原则需要参考顶点原先的位置。详细的原理在最后给出的参考文章中有,这里不再详述,仅仅贴出用于实现本算法的代码:
void HCLaplacian(int iterationTime,float factor1,float factor2) { std::vector<Point3d> point_vector; std::vector<Point3d> startPoint; point_vector.resize(mesh->Vertices.size()); startPoint.resize(mesh->Vertices.size()); for(size_t i=0;i<mesh->Vertices.size();i++) { startPoint[i].X=mesh->Vertices[i].X; startPoint[i].Y=mesh->Vertices[i].Y; startPoint[i].Z=mesh->Vertices[i].Z; } for(int c=0;c<iterationTime;c++) { for(size_t i=0;i<mesh->Vertices.size();i++) { float dx = 0, dy = 0, dz = 0; std::vector<long>& adV=*(mesh->AdjInfos[i].VertexAdjacencyList); for (size_t j=0;j<adV.size();j++) { Point3d& t=mesh->Vertices[adV[j]]; dx += t.X; dy += t.Y; dz += t.Z; } dx = dx / adV.size(); dy = dy / adV.size(); dz = dz / adV.size(); point_vector[i].X=dx - (factor1 * startPoint[i].X + (1 - factor1) *mesh->Vertices[i].X) ; point_vector[i].Y=dy - (factor1 * startPoint[i].Y + (1 - factor1) *mesh->Vertices[i].Y) ; point_vector[i].Z=dz - (factor1 * startPoint[i].Z + (1 - factor1) *mesh->Vertices[i].Z) ; startPoint[i].X=dx; startPoint[i].Y=dy; startPoint[i].Z=dz; } for(size_t i=0;i<mesh->Vertices.size();i++) { mesh->Vertices[i].X=point_vector[i].X; mesh->Vertices[i].Y=point_vector[i].Y; mesh->Vertices[i].Z=point_vector[i].Z; } for(size_t i=0;i<mesh->Vertices.size();i++) { float dx = 0, dy = 0, dz = 0; std::vector<long>& adV=*(mesh->AdjInfos[i].VertexAdjacencyList); for (size_t j=0;j<adV.size();j++) { Point3d& t=mesh->Vertices[adV[j]]; dx += t.X; dy += t.Y; dz += t.Z; } dx = (1.0f - factor2) * dx / adV.size(); dy = (1.0f - factor2) * dy / adV.size(); dz = (1.0f - factor2) * dz / adV.size(); point_vector[i].X=startPoint[i].X - (factor2*mesh->Vertices[i].X +dx); point_vector[i].Y=startPoint[i].Y - (factor2*mesh->Vertices[i].Y +dy); point_vector[i].Z=startPoint[i].Z - (factor2*mesh->Vertices[i].Z +dz); } for(size_t i=0;i<mesh->Vertices.size();i++) { mesh->Vertices[i].X=point_vector[i].X; mesh->Vertices[i].Y=point_vector[i].Y; mesh->Vertices[i].Z=point_vector[i].Z; } } }
效果展示
本文没有刻意的去针对不同算法的特点去筛选数据,只是简单的选择了一个Lobster网格数据做的平滑,其原网格和平滑网格如下图所示:
原始Mesh | 拉普拉斯平滑 | 基于曲率的平滑 | Taubin平滑 | HCLaplacian平滑 |
参考文献与代码
拉普拉斯平滑 |
Field D A. Laplacian smoothing and Delaunay triangulations[J]. Communications in applied numerical methods, 1988, 4(6): 709-712. |
基于曲率的平滑 |
Desbrun M, Meyer M, Schröder P, et al. Implicit fairing of irregular meshes using diffusion and curvature flow[C]//Proceedings of the 26th annual conference on Computer graphics and interactive techniques. ACM Press/Addison-Wesley Publishing Co., 1999: 317-324. |
Taubin平滑 |
Taubin G. A signal processing approach to fair surface design[C]//Proceedings of the 22nd annual conference on Computer graphics and interactive techniques. ACM, 1995: 351-358. |
HCLaplacian平滑 |
Vollmer J, Mencl R, Mueller H. Improved Laplacian smoothing of noisy surface meshes[C]//Computer Graphics Forum. Blackwell Publishers Ltd, 1999, 18(3): 131-138. |
代码工程下载:https://github.com/chnhideyoshi/SeededGrow2d/tree/master/Smoothing
爬网的太疯狂了,转载本文要注明出处啊:http://www.cnblogs.com/chnhideyoshi/