半边数据结构学习
半边数据结构学习
一、网格数据结构
对于表面网络来说,其关键在于拓扑,也就是曲面是如何表达的。拓扑的不同造就了不同的数据结构和标准,其进行网格查询和编辑的性能也不同。
常见的网格数据结构有基于面的数据结构、基于边的数据结构、半边数据结构、有向边数据结构等。这里主要结合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 iterator
typedef 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 iterator
typedef 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 positions
mesh_->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);
} else
ch = 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 boundary
for ( 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为空,则创建一个新filler
if (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);
} else
ch = 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 ) );
// 初始化边界上相邻顶点的权重为0
for ( 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";
} else
std::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
原文地址:https://blog.csdn.net/m0_50910915/article/details/140198377
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!