三维重建——孔洞填补算法

本文是借助openMesh库进行三维重建的孔洞填补的。openMesh里面有非常优秀的三维的数据结构。

核心:

找到三维模型当中所有的空洞,对于每一个孔洞,找出其所有的半边然后对其进行排序;对排序过的所有的半边,找到角度最小的两条边,增加第三条边,形成新的三角面片;如此迭代。具体的每一步的算法如下:

  1. 半边排序算法:
    对于一个孔洞,找出其中的一条半边作为起始边,遍历所有其他的半边集合,找到以该条半边的尾点作为起点的半边;如此迭代,直到找到某一条边的尾点是第一条边的起点为止。

  2. 计算角度:
    三维重建——孔洞填补算法_第1张图片
    对于上面这两种孔洞,箭头处可能角度都比较的小,但是图2 当中就是不能填补的。因此,计算的角度是和他们的方向是有关的。
    计算的方法是:首先求出这个角度的大小(利用向量求出来一个0~180度之间的一个值angle,然后再确定由这两条边组成的一个面的法向量是否和第一条边对面的边所在的面的法向量一致,一致则输出角度,不一致则输出: 360-angle;

  3. 计算距离:

    两点的欧拉距离计算公式;

  4. 单个孔洞填补算法:
    如下图所示:
    三维重建——孔洞填补算法_第2张图片
    如果 两点之间的距离过长(图3),则考虑在中间插入一个点(实际上是替换点),增加两个三角面片,用最外侧的两个半边句柄更新整个半边句柄;否则直接增加一个三角面片,更新句柄(图4)。

关键代码:

// 找到一个洞的半边集合, 排序
void DFS(int nNum, int n)// 在这n条半边当中找到并排序到 有序边对列的第 nNum 条处。 但这个只能是对一个孔洞
{
    if (toTemp[nNum - 1] == fromTemp[0])// 首尾相接的意思吧,就是说找到最后一个了。 也就是完成了所有的边的排序
    {
        if (nNum < vNum)
        {
            vNum = nNum;//原来分配的空间大了, 这是这个环里面的边的条数, ******** 而n表示的是总的半边的数目

            for (int i = 0; i < vNum;++i )
            {
                fromV[i] = fromTemp[i];// 直到找到了所有的半边后,才将所有的半边放入有序的半边集合,这样节省空间
                toV[i] = toTemp[i];
            }
        }
        return;
    }
    for (int i = 1; i < n;i ++)// n是给定的值,半边的个数
    {
        if (!vis[i] && toTemp[nNum - 1] == fromVetex[i])// 找到没有存储的且以 上一个尾点 为 起点的边
        {
            fromTemp[nNum] = fromVetex[i];
            toTemp[nNum] = toVetex[i];
            vis[i] = true;// 找到后,在空洞的半边集合的标记为true
            DFS(nNum + 1, n);
            vis[i] = false;// 这个好像有点问题 ?????????????? 不可能出现一对多的情况,不理解为什么要做这个
        }
    }
}
//修复孔洞
void CMeshFillHole::DoRepairHole(ICLTriMesh* mesh)
{
    std::vector<ICLTriMesh::VertexHandle> AddedFace;
    int i, edge_num = 0;
    if (mesh == nullptr) {return;}
    // 获取孔洞半边集合
    for (ICLTriMesh::HalfedgeIter it = mesh->halfedges_begin(); it != mesh->halfedges_end(); ++it)// 读入mesh 当中的所有半边
    {
        ICLTriMesh::HalfedgeHandle he = it.handle();// 半边的句柄
        if(mesh->is_boundary(he))//is_boundary()非常有效,找到所有的孔洞半边集合
        {
            fromVetex[edge_num] = mesh->from_vertex_handle(he);// 找到孔洞的每一条半边
            toVetex[edge_num++] = mesh->to_vertex_handle(he);
        }
    }
    if (edge_num == 0) {return;}

    // 整理孔洞半边集合
    memset (vis, false, sizeof (vis));//在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法

    //srand((unsigned)time(0));//这个也是关键,是放入随机数种子,通常都是放系统当前时间的。
    //int index= rand()%edge_num;//edge_num
    //cout<<"the index is: "<<index<<endl;

    fromTemp[0] = fromVetex[0];// 将第一条半边赋给temp, test: 换为index
    toTemp[0] = toVetex[0];
    //vis[index]=true;
    vis[0] = true;
    vNum = 10001;
    DFS(1, edge_num);// edge_num得到的是最终的外边的个数
    cout<<"the total number of edges: "<<edge_num<<endl;
    cout<<"The number in this hole is : "<<vNum<<endl;

    // 计算孔洞的平均长度??? 这里是在计算半边的平均长度吧。
    double sum = 0;
    for (i = 0; i < vNum;i++)
    {

        auto s = mesh->point(fromV[i]);//
        auto e = mesh->point(toV[i]);
        sum += cal_dis(s, e);
    }
    double avg_len = sum / vNum;


    // 补三角片
    while (vNum > 3)
    {
        // 计算出每个夹角,并找到最小的
        double min_ang = 360;
        int pos = 0, nxt;
        ICLTriMesh::VertexHandle vVetex0, vVetex1, vVetex2;
        for (i = 0; i < vNum;i ++)
        {
            nxt = (i + 1) % vNum;// 这个vNum 是排序后的半边的个数

            auto s1 = mesh->point(fromV[i]);
            auto e1 = mesh->point(toV[i]);

            auto s2 = mesh->point(fromV[nxt]);
            auto e2 = mesh->point(toV[nxt]);

            //求法矢
            ICLTriMesh::Normal v1(s1.data()[0]-e1.data()[0], s1.data()[1]-e1.data()[1], s1.data()[2]-e1.data()[2]);
            ICLTriMesh::Normal v2(e2.data()[0]-s2.data()[0], e2.data()[1]-s2.data()[1], e2.data()[2]-s2.data()[2]);// ****非常好,这里一定要反过来计算

            // 找出最小角所在半边
            ICLTriMesh::HalfedgeHandle minPointHaleAge;
            for(ICLTriMesh::HalfedgeIter itx = mesh->halfedges_begin(); itx != mesh->halfedges_end(); ++itx) // 这里为什么不写auto了
            {
                ICLTriMesh::HalfedgeHandle tmp = itx.handle();// 再一次回到mesh当中去找,这个有点。。。。。
                if(mesh->from_vertex_handle(tmp) == fromV[i] && mesh->to_vertex_handle(tmp) == toV[i]) // 其实可以在第一个找孔洞半边的时候就应该把 CLTriMesh::HalfedgeHandle存下来,这样就不用再到mesh整体当中去遍历了
                {
                    minPointHaleAge = tmp;//可以从isBoundray()的半边里面去找
                    break;
                }
            }
            double angle = cal_ang(v1, v2, mesh, minPointHaleAge);

            if(angle < min_ang)// 在所有的边当中找到角度最小的 角度最小的进行扩充
            {
                min_ang = angle;
                pos = i;
                vVetex0 = fromV[i];
                vVetex1 = toV[i];
                vVetex2 = toV[nxt];
            }
        }

        // 计算第三边长度dis
        ICLTriMesh::Point p0 = mesh->point(vVetex0);
        ICLTriMesh::Point p1 = mesh->point(vVetex1);
        ICLTriMesh::Point p2 = mesh->point(vVetex2);
        double dis = cal_dis(p1, p2);// p0-p1构成的那一条边本来就是有的

        // 当 dis > 2 * avg_len 时加两个三角形, 加的位置就是这p0 和 p2 两个点的中间
        if (dis > 2 * avg_len)
        {
            ICLTriMesh::Point newPoint((p0.data()[0]+p2.data()[0])/2, (p0.data()[1]+p2.data()[1])/2, (p0.data()[2]+p2.data()[2])/2);
            ICLTriMesh::VertexHandle newVertexHandle = mesh->add_vertex(newPoint);
            toV[pos] = newVertexHandle;
            fromV[(pos + 1)%vNum] = newVertexHandle;// 下一次则从新加入的第二条半边入手

            AddedFace.clear();
            AddedFace.push_back(vVetex0);
            AddedFace.push_back(vVetex1);
            AddedFace.push_back(newVertexHandle);
            mesh->add_face(AddedFace);

            AddedFace.clear();
            AddedFace.push_back(vVetex1);
            AddedFace.push_back(vVetex2);
            AddedFace.push_back(newVertexHandle);
            mesh->add_face(AddedFace);

            cout<<"加入了两个: "<<i<<endl;
        }
        else // 否则加一个三角形
        {
            AddedFace.clear();
            AddedFace.push_back(vVetex0);
            AddedFace.push_back(vVetex1);
            AddedFace.push_back(vVetex2);
            mesh->add_face(AddedFace);

            toV[pos] = toV[(pos + 1) % vNum];
            if (pos + 1 == vNum)
            {pos = -1;}
            for (i = pos + 1; i < vNum - 1;i ++)
            {
                fromV[i] = fromV[i + 1];
                toV[i] = toV[i + 1];//意思是从下 两个节点开始
            }
            vNum --;
            cout<<"vNum: "<<vNum<<endl;
        }
    }
    if (vNum <= 3)//如果是这样直接组成一个面(这里是有问题的, 如果这里多个空始终达不到这个条件)
    {
        if (vNum != 0)
        {
            ICLTriMesh::VertexHandle vVetex1 = fromV[0];// 怎么只有 from勒?因为to也是一样的
            ICLTriMesh::VertexHandle vVetex0 = fromV[1];
            ICLTriMesh::VertexHandle vVetex2 = fromV[2];

            AddedFace.clear();
            AddedFace.push_back(vVetex1);
            AddedFace.push_back(vVetex0);
            AddedFace.push_back(vVetex2);
            mesh->add_face(AddedFace);
        }

        return;
    }
}

以上是两个关键的代码,但是只适合单孔的情况。

效果展示

三维重建——孔洞填补算法_第3张图片三维重建——孔洞填补算法_第4张图片

该算法还有很多可以优化的点,以至于提高运行的速度。

你可能感兴趣的:(三维重建——孔洞填补算法)