本博客内容来源于网络以及其他书籍,结合自己学习的心得进行重编辑,因为看了很多文章不便一一标注引用,如图片文字等侵权,请告知删除。
前言
最近在处理网格简化的相关工作,上篇文章sdasdasd先把常见的网格简化算法进行了梳理,本篇主要详细的描述一下QEM算法。
QEM, 全名:Quadric Error Metrics,基于二次误差度量的边坍塌算法,发表于1997年《Surface Simplification Using Quadric Error Metrics》一文中,作为一个即兼顾速度又兼顾效果的网格简化算法,在3D重建等应用上被大量的使用。下面我们来看一下其具体的思路,以及分析一下优缺点。
QEM算法思路
QEM 的算法的思路非常的简单,就是通过大量的迭代进行边坍塌进行网格简化。那么是怎么迭代的呢?
QEM通过对网格上每一条边计算一个度量值(cost)。将这些边放到一个最小堆中,每次迭代都将堆中度量值最小的值将其移除,然后重新计算这条边的顶点直接相连的其他边的度量值,并更新最小堆。直至达到一定的简化条件阈值为止,比如顶点数达到一定数目。
在了解迭代的过程的情况下,其实就已经对QEM的算法流程有了完全的了解,就是这么简单。现在还有一个问题没有解释,就是这个度量值该怎么算,到底选择那条边进行坍塌,这是QEM算法关键部分,下一节我们详细描述一下度量值的计算方式。
QEM 二次误差度量值计算方法
我们先想一想什么样的边适合被收缩,很容易想到,这个边被收缩到某一个点后,产生新的面与原来的面的差异越小越好。
那这个差异我们可以定义为:顶点到原来相邻面的距离的平方和。假如我们不收缩,两个顶点分别到两个顶点相邻面的距离都是0,差异也是最小的。收缩后,两个点变成一个点,我们用这个新点到旧的面的距离来代替计算新的面与旧的面的差异,差异这个新点到原来两个点相邻面的距离的平方和了。这个理解应该没有问题。
现在我们可以用文字来描述这个度量值了,即一条边边收缩后某个新点到原来相邻的面的距离的平方和的最小值。一条边可以收缩到这条边上的很多点,我们用最小值作为这条边的度量值。物理意义我们解释清楚了,下面看一下其计算过程。
举例子我有一条边e,其两个顶点分别为v1,v2。收缩后的顶点为v0,度量误差为Δ。p=[a b c d]T代表平面方程ax+by+cz+d=0(a2+b2+c2=1)的系数。
那么则有:从上式子中,我们可以看出,这个度量误差主要是两部分决定,新点v0的位置,以及边的两个顶点的相邻平面的二次基本误差矩阵Q1与Q2之和,定义为Q。即Δ(v0) = (v0T)Q(v0)
每个点顶点的Q的可以先计算出来。那么新点v0的位置该怎么选择呢?
有两种策略:
- 一种简单的策略就是在v1,v2和(v1+v2)/2中选择一个使得收缩代价Δ(v0)最小的位置。
- 另一种策略就是数值计算顶点v0位置使得Δ(v0)最小,由于Δ的表达式是一个二次项形式,因此令一阶导数为0,即,该式等价于求解:
其中qij为误差矩阵Q中对应的元素。如果Q可逆,那么通过求解上述方程就可以得到新顶点v0的位置,如果系数矩阵不可逆,就通过第一种简单策略来得到新顶点v0的位置。
通过上面我们就可以求出每条边的度量误差最小值以及对应的位置了。也就可以构建我们的最小堆了。刚才是我们对算法的分析,现在我们梳理一下算法计算顺序。
- 对所有的初始顶点计算Q矩阵.
- 选择所有有效的边,即排除不想要收缩的边
- 对每一条有效边(v1,v2),计算度量值以及坍塌后顶点
- 将所有的边按照cost的权值放到一个堆里
- 每次移除度量(cost)最小的边,并且更新包含着v0的所有有效边的代价
- 如果没有达到阈值则重复5,否则退出
现在我们理解了QEM算法的原理了,下面来看看QEM的算法效果
VCGLib QEM 效果展示【代码】
#include <vcg/complex/complex.h>
#include <vcg/complex/algorithms/clean.h>
#include <vcg/complex/algorithms/create/platonic.h>
#include <vcg/complex/algorithms/stat.h>
#include <vcg/complex/algorithms/local_optimization.h>
#include <vcg/complex/algorithms/local_optimization/tri_edge_collapse_quadric.h>
#include <wrap/io_trimesh/import.h>
#include <wrap/io_trimesh/export_ply.h>
using namespace vcg;
using namespace tri;
class MyVertex;
class MyEdge;
class MyFace;
struct MyUsedTypes: public UsedTypes<Use<MyVertex>::AsVertexType,Use<MyEdge>::AsEdgeType,Use<MyFace>::AsFaceType>{};
class MyVertex : public Vertex< MyUsedTypes,
vertex::VFAdj,
vertex::Coord3f,
vertex::Color4b,
vertex::Normal3f,
vertex::Mark,
vertex::BitFlags >{
public:
vcg::math::Quadric<double> &Qd() {return q;}
private:
math::Quadric<double> q;
};
class MyEdge : public Edge< MyUsedTypes> {};
class MyFace : public Face< MyUsedTypes,
face::VFAdj,
face::FFAdj,
face::Mark,
face::VertexRef,
face::BitFlags > {};
class MyMesh : public vcg::tri::TriMesh<std::vector<MyVertex>, std::vector<MyFace> > {};
typedef BasicVertexPair<MyVertex> VertexPair;
class MyTriEdgeCollapse: public vcg::tri::TriEdgeCollapseQuadric< MyMesh, VertexPair, MyTriEdgeCollapse, QInfoStandard<MyVertex> > {
public:
typedef vcg::tri::TriEdgeCollapseQuadric< MyMesh, VertexPair, MyTriEdgeCollapse, QInfoStandard<MyVertex> > TECQ;
typedef MyMesh::VertexType::EdgeType EdgeType;
inline MyTriEdgeCollapse( const VertexPair &p, int i, BaseParameterClass *pp) :TECQ(p,i,pp){}
};
typedef typename MyMesh::FaceType FaceType;
typedef typename MyMesh::FacePointer FacePointer;
typedef typename MyMesh::FaceIterator FaceIterator;
typedef typename MyMesh::VertexType VertexType;
typedef typename MyMesh::VertexPointer VertexPointer;
int main(int argc ,char**argv)
{
int face_count = std::stoi(argv[2]
MyMesh mesh_in;
vcg::tri::io::Importer<MyMesh>::Open(mesh_in,argv[1]);
printf("mesh loaded %d %d \n",mesh_in.vn,mesh_in.fn);
tri::Clean<MyMesh>::RemoveDuplicateVertex(mesh_in);
tri::Clean<MyMesh>::RemoveUnreferencedVertex(mesh_in);
tri::UpdateTopology<MyMesh>::FaceFace(mesh_in);
tri::UpdateFlags<MyMesh>::FaceBorderFromFF(mesh_in);
vcg::tri::UpdateBounding<MyMesh>::Box(mesh_in);
tri::TriEdgeCollapseQuadricParameter pp;
pp.QualityThr=1.0f;
pp.PreserveBoundary=false;
pp.BoundaryWeight = 1.0f;
pp.PreserveTopology=false;
pp.QualityWeight=false;
pp.NormalCheck=false;
pp.OptimalPlacement=true;
pp.QualityQuadric=true;
if(pp.PreserveBoundary ){
pp.FastPreserveBoundary=true;
pp.PreserveBoundary = false;
}
if(pp.NormalCheck) pp.NormalThrRad = M_PI/4.0;
vcg::LocalOptimization<MyMesh> DeciSession(mesh_in,&pp);
DeciSession.Init<MyTriEdgeCollapse >();
DeciSession.SetTargetSimplices(face_count);
DeciSession.SetTimeBudget(2.0f);
int faceToDel=mesh_in.fn-face_count;
while( DeciSession.DoOptimization() && mesh_in.fn>face_count ) {
printf("Simplifying... %d%%\n",(int)(100-100*(mesh_in.fn-face_count)/(faceToDel)));
};
DeciSession.Finalize<MyTriEdgeCollapse >();
tri::Clean<MyMesh>::RemoveFaceOutOfRangeArea(mesh_in,0);
tri::Clean<MyMesh>::RemoveDuplicateVertex(mesh_in);
tri::Clean<MyMesh>::RemoveUnreferencedVertex(mesh_in);
tri::Allocator<MyMesh>::CompactVertexVector(mesh_in);
tri::Allocator<MyMesh>::CompactFaceVector(mesh_in);
vcg::tri::io::ExporterPLY<MyMesh>::Save(mesh_in,parser.get<std::string>(1).c_str(),mask | vcg::tri::io::Mask::IOM_VERTCOLOR);
return 0;
}
有关VCG的具体用法,代码解释,我就不说了,直接看结果。
原始 | 简化50% |
---|---|
简化2.5% | 简化 0.25% |
总结
我们可以看出QEM的简化效果还是很好的,但是VCG实现的版本不容易控制其简化的程度,难以程序自动化去做简化,当简化程度较大的时候,变形还是很明显的。我们可以以QEM作为简化的思路,添加其他的限制来控制其简化的程度。比如我们控制简化的边长限制等等,当边长大于某个值时,其度量值也非常大,来只简化较短边等方式。
重要的事情说三遍:
如果您看到我的文章对您有所帮助,那就点个赞呗 ( * ^ __ ^ * )
如果您看到我的文章对您有所帮助,那就点个赞呗( * ^ __ ^ * )
如果您看到我的文章对您有所帮助,那就点个赞呗( * ^ __ ^ * )
任何人或团体、机构全部转载或者部分转载、摘录,请保留本博客链接或标注来源。博客地址:开飞机的乔巴
作者简介:开飞机的乔巴(WeChat:zhangzheng-thu),现主要从事机器人抓取视觉系统以及三维重建等3D视觉相关方面,另外对slam以及深度学习技术也颇感兴趣,欢迎加我微信或留言交流相关工作。
网友评论