自学内容网 自学内容网

半边数据结构学习


一、网格数据结构

对于表面网络来说,其关键在于拓扑,也就是曲面是如何表达的。拓扑的不同造就了不同的数据结构和标准,其进行网格查询和编辑的性能也不同。

常见的网格数据结构有基于面的数据结构、基于边的数据结构、半边数据结构、有向边数据结构等。这里主要结合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属性
 
 }

使用半边数据结构检测孔洞的步骤

  1. 标记边界边
    • 遍历网格的所有边,找出只有半边的边作为边界边。
  2. 遍历边界边
    • 对于每条边界边,获取与该边关联的半边。
  3. 跟踪半边环
    • 从一条边界边开始,可以通过半边数据结构相关函数沿着边界环遍历。
    • 遍历半边环的方法:
      • 使用 next_halfedge_handle() 函数从当前半边跳转到下一条边界上的半边。
      • 如果遇到顶点有多条出边的情况(表示顶点是网格的拐角),可以使用 opposite_halfedge_handle() 函数找到对称的半边,然后从中选择下一个出边。
  4. 收集孔洞边
    • 在遍历半边环的过程中,收集所有构成孔洞的边界边。可以使用一个列表或类似的数据结构来存储这些边的句柄或索引。
  5. 验证孔洞
    • 一旦收集完一条半边环上的所有边界边,可以进行一些验证步骤来确认它确实是一个孔洞,比如计算孔洞的几何中心,检查边界边是否按某种顺序排列等。
  6. 存储孔洞
    • 验证通过则将孔洞的边界边等信息存储在孔洞列表中。

孔洞信息

 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)!