半边数据结构学习
- 一、网格数据结构
- 二、半边数据结构
- 顶点(Vertex)
- 半边(HalfEdge)
- 面片(Face)
- 三、OpenMesh 相关代码
- 拓扑关联对象
- 遍历
- 四、OpenFilpper 相关代码
- HoleInfo类
- 孔洞检测
- 孔洞信息
- HoleFiller类
- 孔洞补全
一、网格数据结构
对于表面网络来说,其关键在于拓扑,也就是曲面是如何表达的。拓扑的不同造就了不同的数据结构和标准,其进行网格查询和编辑的性能也不同。
常见的网格数据结构有基于面的数据结构、基于边的数据结构、半边数据结构、有向边数据结构等。这里主要结合OpenMesh以及OpenFlipper学习一下半边数据结构。
二、半边数据结构
每个边分为两个半边,每个半边都是一个有向边,方向相反。如果一个边被两个面片公用,则每个面片都能各自拥有一个半边。如果一个边仅被一个面片占用(边界边),则这个面片仅拥有该边的其中一个半边,另一个半边为闲置状态。每一条半边仅存储它的起点指针。
那么是否可以通过边的闲置状态来定位网格中的孔洞或边界?
半边数据结构仅支持流形网络。
计算机图形学上,通常说的流形是一种几何模型表面(但不是所有的),即二维流形,对应拓扑流形。如果网格的每个边最多被两个面片共用,那么这个网格就是流形网络,否则称为非流形网络。
半边数据结构的三个重要的数据结构——顶点、半边、面片。
顶点(Vertex)
包含出半边(OutgoingHalfedge)的指针或索引。
在半边数据结构中的点储存着其位置和以其为起始点的半边的指针。
当在点上存在有多条半边相连,可以指向任意一半边,可以通过以下查询方式遍历所有半边
struct HE_vert { float x,y,z; HE_edge* edge; // one of the half-edges emantating from the vertex 指向任意一个以它为出发点的半边};
半边(HalfEdge)
包含起点(StartVertex)、邻接面(AdjacentFace)、下一条半边(NextHalfedge)、相反边(opposite)的指针或索引。
注意:有的实现方式是将指向半边的终点。
struct HE_edge { HE_vert* vert; // vertex at the start of the half-edge 指向半边的出发点 HE_edge* pair; // oppositely oriented adjacent half-edge 指向相反的相邻半边(也称twin)HE_edge* next; // next half-edge around the face 指向后一个半边HE_face* face; // face the half-edge borders 指向半边相邻的面片};
面片(Face)
包含一条起始边(FirstHalfedge)的指针或索引对于一个半边数据结构的简单形式,一个面仅仅需要储存一个围绕它的边的指针,在一些特定场合可能要求我们储存比如材质和法向一类的信息。和上面一样,虽然有很多边围绕着面,我们只需要储存其中一条,而无所谓是哪一条。
struct HE_face { HE_edge* edge; // one of the half-edges bordering the face 指向任意一个环绕它的半边};
顶点可能有两条或以上的出半边,而顶点的数据表达只有一条出半边,那这条出半边是哪一条?半边的下一条半边又是哪一条?面片的起始半边又是哪一条?通过某个网格的数据结构图能看得出这些信息吗?
事实上,半边数据结构的网格的构建通常是通过面列表来创建的,也就是说,正常的构建半边数据结构网格是通过一个一个面片的添加来构建的。
所以面的添加顺序就决定了点边面结构的信息,添加面的方法通常是addFace(a,b,c,…)
,a,b,c…参数是该面片按其某条环路顺序排列的顶点的指针或索引。注意,环路可以是顺时针或者逆时针,决定了该面片的方向(法向量的方向)。
三、OpenMesh 相关代码
OpenMesh就是使用的半边数据结构。
拓扑关联对象
// Type declarations for handles: 句柄类型声明:
OpenMesh::VertexHandle verH; // 顶点句柄声明
OpenMesh::FaceHandle facH; // 面句柄声明
OpenMesh::HalfedgeHandle hedH, hedH_n, hedH_p; // 半边句柄声明
OpenMesh::Vec3d point; // 三维向量,用于存储顶点坐标// Half-edge operations: 半边操作:
half-edge → vert: verH = mesh.to_vertex_handle(hedH); // 从半边获取顶点句柄
half-edge→ pair→vert:verH = mesh.from_vertex_handle(hedH); // 从对向半边获取顶点句柄
half-edge→ next:hedH_n = mesh.next_halfedge_handle(hedH); // 获取当前面中下一条半边的句柄
half-edge→pair:hedH_p = mesh.opposite_halfedge_handle(hedH); // 获取当前半边的对向半边的句柄
half-edge → face: facH = mesh.face_handle(hedH); // 获取包含给定半边的面句柄
face → half-edge: hedH = mesh.halfedge_handle(facH); // 获取给定面的一条半边句柄
vert → half-edge: hedH = mesh.halfedge_handle(verH); // 获取从给定顶点出发的一条半边句柄
vert coordinates: point = mesh.point(verH); // 获取与顶点句柄相关联的顶点的三维坐标
遍历
// vertex iterator
typedef OpenMesh::TriMesh_ArrayKernelT<> MyMesh; // 定义网格类型
MyMesh mesh;
OpenMesh::VertexHandle verH; // 定义顶点句柄
MyMesh::VertexIter vit; // 定义顶点迭代器
OpenMesh::Vec3d p;
int valence; // 定义存储顶点度的变量
OpenMesh::IO::read_mesh(mesh, "cube.off")
for (vit = mesh.vertices_begin(); vit != mesh.vertices_end(); vit++) // 遍历所有顶点
{verH = *vit; // 获取当前迭代器指向的顶点句柄p = mesh.point(verH); // 获取顶点坐标valence = mesh.valence(verH); // 获取顶点的度(连接的边数)cout << p[0] << " " << p[1] << " " << p[2] << " " << valence << endl; // 输出顶点坐标和度数
}
// face iteratortypedef OpenMesh::TriMesh_ArrayKernelT<> MyMesh;
MyMesh mesh;
OpenMesh::FaceHandle facH; // 定义面句柄
MyMesh::FaceIter fit; // 定义面迭代器
int nvert; // 定义存储顶点数的变量
OpenMesh::IO::read_mesh(mesh, "cube.off")
for (fit = mesh.faces_begin(); fit != mesh.faces_end(); fit++) // 遍历所有面
{facH = *fit; // 获取当前迭代器指向的面句柄nvert = mesh.valence(facH); // 获取面的顶点数(面的度)cout << "Number of vertices = " << nvert << endl;
}
// edge iteratortypedef OpenMesh::TriMesh_ArrayKernelT<> MyMesh;
MyMesh mesh;
OpenMesh::EdgeHandle edgH; // 定义边句柄
MyMesh::EdgeIter eit; // 定义边迭代器
float len, angle; // 定义存储边长度和二面角的变量
OpenMesh::IO::read_mesh(mesh, "cube.off")
for (eit = mesh.edges_begin(); eit != mesh.edges_end(); eit++) // 遍历所有边
{edgH = *eit;len = mesh.calc_edge_length(edgH); // 计算边的长度angle = mesh.calc_dihedral_angle(edgH); // 输出边的长度和二面角cout << "Edge length = " << len <<" Dihedral angle = " << angle << endl;
}
半面及二面角 :一条直线把平面分成两个部分,其中的每一部分都称为半平面。从一条直线出发的两个半平面所组成的图形称为二面角。这条直线称为二面角的棱,这两个半平面称为二面角的面。
四、OpenFilpper 相关代码
在三维重建中,建模的网格尝尝由于遮挡等原因产生孔洞,导致其完整性降低,因此,常通过网格孔洞修复来得到一个完整的结构(但一般只能用于简单网格)。接下来看一下OpenFilpper中是怎么查找和修复孔洞的。
HoleInfo类
孔洞检测
template< class MeshT >void HoleInfo< MeshT >::getHoles(){// Property for the active mesh to mark already visited edges // 边界搜索的属性句柄,用于标记已访问的边界边OpenMesh::EPropHandleT< bool > boundary_search;// Add a property to the mesh to store original vertex positionsmesh_->add_property( boundary_search, "Boundary search" ); // Initialize Property // 先都初始化为false,表示未被访问typename MeshT::EdgeIter e_it, e_end=mesh_->edges_end();for (e_it=mesh_->edges_begin(); e_it!=e_end; ++e_it) {mesh_->property( boundary_search , *e_it ) = false;}holes_.clear();for (e_it=mesh_->edges_begin(); e_it!=e_end; ++e_it) {// Skip already visited edges // 跳过已访问的边if ( mesh_->property( boundary_search , *e_it ) )continue;// Check only boundary edges // 只处理边界边if ( !mesh_->is_boundary(*e_it))continue; // Get boundary halfedge // 获取边界的半边typename MeshT::HalfedgeHandle hh = mesh_->halfedge_handle( *e_it, 0 );if ( ! mesh_->is_boundary( hh ) )hh = mesh_->opposite_halfedge_handle( hh );typename MeshT::Point center(0,0,0);Hole currentHole; // 初始化孔洞信息// Collect boundary edges // 收集边界边typename MeshT::HalfedgeHandle ch = hh;do {currentHole.push_back( mesh_->edge_handle(ch) );// 计算孔洞中心center += mesh_->point( mesh_->from_vertex_handle(ch) ); mesh_->property( boundary_search , mesh_->edge_handle(ch) ) = true;//check number of outgoing boundary HEH's at Vertex // 检查顶点的出边数目int c = 0;typename MeshT::VertexHandle vh = mesh_->to_vertex_handle(ch);for ( typename MeshT::VertexOHalfedgeIter voh_it(*mesh_,vh); voh_it.is_valid(); ++voh_it)if ( mesh_->is_boundary( *voh_it ) )c++;if ( c >= 2){ // 如果顶点有多于两条出边,选择下一个出边typename MeshT::HalfedgeHandle op = mesh_->opposite_halfedge_handle( ch );typename MeshT::VertexOHalfedgeIter voh_it(*mesh_,op);ch = *(++voh_it);} elsech = mesh_->next_halfedge_handle( ch );} while ( ch != hh );center /= currentHole.size(); // 计算孔洞中心bool isHole = true; // 检查孔洞形状int err = 0;for (unsigned int i=0; i < currentHole.size(); i++){typename MeshT::HalfedgeHandle hh = mesh_->halfedge_handle( currentHole[i], 0 );if ( ! mesh_->is_boundary( hh ) )hh = mesh_->opposite_halfedge_handle( hh );typename MeshT::VertexHandle vh = mesh_->from_vertex_handle(hh);typename MeshT::Normal n = mesh_->normal( vh );typename MeshT::Point p = mesh_->point( vh );// 检查孔洞中每个点到孔洞中心的距离if ( (p - center).norm() < (p + n - center).norm() ){// isHole = false;// break;err++;}}// std::cerr << "Errors " << err << " Size " << hole.count << std::endl; if (isHole) // 如果是孔洞,将其加入孔洞列表holes_.push_back(currentHole);}mesh_->remove_property( boundary_search); // 结束时,移除boundary_search属性}
使用半边数据结构检测孔洞的步骤
- 标记边界边
- 遍历网格的所有边,找出只有半边的边作为边界边。
- 遍历边界边
- 对于每条边界边,获取与该边关联的半边。
- 跟踪半边环
- 从一条边界边开始,可以通过半边数据结构相关函数沿着边界环遍历。
- 遍历半边环的方法:
- 使用
next_halfedge_handle()
函数从当前半边跳转到下一条边界上的半边。 - 如果遇到顶点有多条出边的情况(表示顶点是网格的拐角),可以使用
opposite_halfedge_handle()
函数找到对称的半边,然后从中选择下一个出边。
- 使用
- 收集孔洞边
- 在遍历半边环的过程中,收集所有构成孔洞的边界边。可以使用一个列表或类似的数据结构来存储这些边的句柄或索引。
- 验证孔洞
- 一旦收集完一条半边环上的所有边界边,可以进行一些验证步骤来确认它确实是一个孔洞,比如计算孔洞的几何中心,检查边界边是否按某种顺序排列等。
- 存储孔洞
- 验证通过则将孔洞的边界边等信息存储在孔洞列表中。
孔洞信息
template< class MeshT >void HoleInfo< MeshT >::getHolePostitionInfo(const int _index, typename MeshT::Normal& _holeNormal, typename MeshT::Point& _holeCenter) const{// 计算指定孔洞 _index 的中心位置和平均法线 _holeCenter = typename MeshT::Point(0.0,0.0,0.0);_holeNormal = typename MeshT::Normal(0.0,0.0,0.0);// Center of gravity of hole and an average normal at the hole boundaryfor ( size_t i = 0 ; i < holes_[_index].size() ; ++i ) {const typename MeshT::HalfedgeHandle he = mesh_->halfedge_handle(holes_[_index][i],0);const typename MeshT::VertexHandle vh_to = mesh_->to_vertex_handle(he);_holeCenter += mesh_->point(vh_to);_holeNormal += mesh_->normal(vh_to);}_holeCenter /= typename MeshT::Scalar(holes_[_index].size());_holeNormal /= typename MeshT::Scalar(holes_[_index].size());_holeNormal.normalize();}template< class MeshT >void HoleInfo< MeshT >::getHoleInfo(const unsigned int _index, size_t& _edges, typename MeshT::Scalar& _diagonal, typename MeshT::Scalar& _boundaryLength) const{//获取指定孔洞 _index 的基本信息,包括边数、对角线长度和边界长度 if ( _index >= holes_.size() ) {std::cerr << "Invalid hole index " << _index << std::endl;return;}_boundaryLength = 0.0;typename MeshT::Point minCoord = typename MeshT::Point(std::numeric_limits<typename MeshT::Scalar>::max(),std::numeric_limits<typename MeshT::Scalar>::max(),std::numeric_limits<typename MeshT::Scalar>::max());typename MeshT::Point maxCoord = typename MeshT::Point(-std::numeric_limits<typename MeshT::Scalar>::max(),-std::numeric_limits<typename MeshT::Scalar>::max(),-std::numeric_limits<typename MeshT::Scalar>::max());for (size_t i = 0 ; i < holes_[_index].size() ; ++i) {_boundaryLength += mesh_->calc_edge_length(holes_[_index][i]);typename MeshT::Point pos = mesh_->point(mesh_->from_vertex_handle(mesh_->halfedge_handle(holes_[_index][i],0)));minCoord[0] = std::min(minCoord[0],pos[0]);minCoord[1] = std::min(minCoord[1],pos[1]);minCoord[2] = std::min(minCoord[2],pos[2]);maxCoord[0] = std::max(maxCoord[0],pos[0]);maxCoord[1] = std::max(maxCoord[1],pos[1]);maxCoord[2] = std::max(maxCoord[2],pos[2]);}_edges = holes_[_index].size();_diagonal = (maxCoord - minCoord).length();}template< class MeshT >std::vector< std::vector< typename MeshT::EdgeHandle > >* HoleInfo< MeshT >::holes(){//返回指向孔洞列表 holes_ 的指针return &holes_;}
HoleFiller类
孔洞补全
template< class MeshT >
void HoleInfo< MeshT >::fillHole(int _index, int _stages)
{if ((uint) _index > holes_.size()){ // 检查索引是否有效std::cerr << "Cannot fill hole. Index invalid." << std::endl;return;}// 如果filler为空,则创建一个新fillerif (filler_ == 0)filler_ = new HoleFiller< MeshT >(*mesh_);// 使用filler填补指定孔洞的第一个边界边filler_->fill_hole(holes_[_index][0], _stages);// 垃圾回收,清理无效数据mesh_->garbage_collection();// 清除边选择状态MeshSelection::clearEdgeSelection(mesh_);// 更新网格法线mesh_->update_normals();
}template< class MeshT >
void HoleInfo< MeshT >::fillHole(typename MeshT::EdgeHandle _eh, int _stages)
{if (filler_ == 0)filler_ = new HoleFiller< MeshT >(*mesh_);filler_->fill_hole(_eh, _stages);mesh_->garbage_collection();MeshSelection::clearEdgeSelection(mesh_);mesh_->update_normals();
}template< class MeshT >void HoleInfo< MeshT >::fillAllHoles(int _stages){if (filler_ == 0)filler_ = new HoleFiller< MeshT >(*mesh_);filler_->fill_all_holes( _stages );}
//=============================================================================
//
// Fill a hole which is identified by one of its boundary edges.
// 通过其边界边填补一个被识别的孔洞。
//
//=============================================================================template< class MeshT >
void HoleFiller< MeshT >::fill_hole( EH _eh, int _stages )
{std::cerr << " Stage 1 : Computing a minimal triangulation ... ";// 打印信息:阶段1:计算最小三角剖分// 记录最后一个顶点,用于选择新顶点typename MeshT::VertexHandle old_last_handle = *(--mesh_.vertices_end());// 如果没有边界边,则不是孔洞if ( ! mesh_.is_boundary( _eh ) )return;// 获取边界半边HH hh = mesh_.halfedge_handle( _eh, 0 );if ( ! mesh_.is_boundary( hh ) )hh = mesh_.opposite_halfedge_handle( hh );// 收集边界顶点和相对应的对向顶点boundary_vertex_.clear();opposite_vertex_.clear();HH ch = hh;do {boundary_vertex_.push_back( mesh_.from_vertex_handle( ch ) );opposite_vertex_.push_back( mesh_.to_vertex_handle(mesh_.next_halfedge_handle( mesh_.opposite_halfedge_handle( ch ) ) ) );// 计算顶点处的外向半边数量int c = 0;VH vh = mesh_.to_vertex_handle(ch);for ( typename MeshT::VertexOHalfedgeIter voh_it(mesh_, vh); voh_it.is_valid(); ++voh_it )if ( mesh_.is_boundary( *voh_it ) )c++;if ( c >= 2 ) {HH op = mesh_.opposite_halfedge_handle( ch );typename MeshT::VertexOHalfedgeIter voh_it(mesh_, op);ch = *(++voh_it);} elsech = mesh_.next_halfedge_handle( ch );} while ( ch != hh );int nv = boundary_vertex_.size();// 计算初始三角剖分所需的权重和连接信息w_.clear();w_.resize( nv, std::vector<Weight>( nv, Weight() ) );l_.clear();l_.resize( nv, std::vector<int>( nv, 0 ) );// 初始化边界上相邻顶点的权重为0for ( int i = 0; i < nv - 1; ++i )w_[i][i+1] = Weight( 0, 0 );// 动态规划计算最小权重的三角剖分for ( int j = 2; j < nv; ++j ) {#pragma omp parallel for shared(j, nv)for ( int i = 0; i < nv - j; ++i ) {Weight valmin;int argmin = -1;for ( int m = i + 1; m < i + j; ++m ) {Weight newval = w_[i][m] + w_[m][i+j] + weight( i, m, i+j );if ( newval < valmin ) {valmin = newval;argmin = m;}}w_[i][i+j] = valmin;l_[i][i+j] = argmin;}}// 实际填补孔洞。收集所有新生成的三角形和边界边hole_edge_.clear();hole_triangle_.clear();if ( fill( 0, nv - 1 ) ) {std::cerr << "ok\n";// 如果只需要一阶段,则返回if ( _stages <= 1 )return;std::cerr << " Stage 2 : Fairing the filling ... ";// 打印信息:阶段2:平滑填充区域std::vector< FH > handles = hole_triangle_;// 对填补后的三角形进行平滑处理fairing(handles);// 标记所有新顶点为已选择状态typename MeshT::VertexIter old_end = ++typename MeshT::VertexIter(mesh_, old_last_handle);typename MeshT::VertexIter v_end = mesh_.vertices_end();for ( ; old_end != v_end; ++old_end )if ( !mesh_.status(*old_end).deleted() )mesh_.status(*old_end).set_selected( true );std::cerr << "ok\n";} elsestd::cerr << "Could not create triangulation" << std::endl;
}
使用 OpenFlipper 测试了一下,孔洞都能找到,但是需要进一步筛选。然后如果孔洞较大、不规则,其补全效果其实比较差。
先到这儿。
参考以推荐阅读:
1.半边数据结构&网格细分与简化
2.半边数据结构与OpenMesh中的处理
3.Polygon Mesh Processing 阅读笔记(2) 网格数据结构
4.The Half Edge Data Structure
5.Half-Edge Data Structures