基于RBF函数的点云孔洞修补

一、简介

之前无意中看到这样一种点云孔洞修补方法,感觉很有意思,就下载了作者的源码看看效果,具体流程如下所述:
1、首先,我们需要先定位网格上的孔洞。
2、然后计算孔洞边界顶点的中心位置(孔洞边界顶点位置的平均值)。边界上的顶点与中心位置上新创建的顶点进行连接,从而创建一个填充孔洞的表面补丁。在这个补丁中的三角形会被上采样(细分),这样当我们进行补丁平滑时,我们可以有更多的顶点来操作。

注意,这里对原始网格的三角形进行了上采样,因为这使它非常容易将原始补丁和原始几何图形的顶点融合在一起。所谓“融合”,就是指确保原始网格的边界,和补丁的边界共享相同的顶点。

3、接下来,通过在补丁上求解方程Δ^{2}f=0来创建一个平滑的补丁。这需要离散拉普拉斯算子来实现这一点,而拉普拉斯算子在网格表面上的离散被称为拉普拉斯-贝尔特拉米算子。这里使用libigl库创建了离散拉普拉斯- beltrami运算子,然后使用该运算子求解方程Δ^{2}f=0。

4、最后,我们使用网格抽取算法对网格进行下采样,得到最终修补之后的结果。

二、实现代码

CMakeLists.txt

cmake_minimum_required(VERSION 3.1)
project(hole_fixer)

set(CMAKE_BUILD_TYPE "Release")
set(CMAKE_CXX_FLAGS_RELEASE "-Zi -Od")
set(CMAKE_EXE_LINKER_FLAGS "-Debug")
message("Build Type:" ${CMAKE_BUILD_TYPE})
message("Release mode:" ${CMAKE_CXX_FLAGS_RELEASE})
message("Linker mode:" ${CMAKE_EXE_LINKER_FLAGS})

include_directories(libigl/ libigl/eigen/)
add_executable(hole_fixer main.cpp)

这里对原始代码做了一下改动,让它更符合我的习惯。

main.cpp

#include <igl/boundary_loop.h>
#include <igl/cat.h>
#include <igl/colon.h>
#include <igl/slice.h>
#include <igl/upsample.h>
#include <igl/decimate.h>
#include <igl/shortest_edge_and_midpoint.h>
#include <igl/harmonic.h>
#include <igl/readOFF.h>
#include <igl/writeOFF.h>

using Eigen::VectorXi;
using Eigen::MatrixXd;
using Eigen::MatrixXi;
using Eigen::VectorXd;
typedef Eigen::SparseMatrix<double> SpMat;
typedef Eigen::Triplet<double> Tri;

void printHelpExit() {
    printf("Invalid command line arguments specified!\n\n");

    printf("USAGE: hole_fixer [options]\n\n");

    printf("OPTIONS: \n");

    printf("  -in\t\t\ttarget mesh file in .off-format, with a hole\n");
    printf("  -out\t\t\toutput mesh file in .off-format\n");
    printf("  -outfaces\t\tHow many faces to decimate the mesh to\n");
    printf("  -upsample\t\tHow much upsampling to use when creating the patch\n");

    exit(1);
}

const char* findToken(const char* param, int argc, char* argv[]) {
    const char* token = nullptr;
    for (int i = 0; i < argc; ++i) {
        if (strcmp(argv[i], param) == 0) {
            if (i + 1 < argc) {
                token = argv[i + 1];
                break;
            }
        }
    }

    if (token == nullptr) {
        printf("Could not find command-line parameter %s\n", param);
        return nullptr;
    }

    return token;
}

const char* parseStringParam(const char* param, int argc, char* argv[]) {
    const char* token = findToken(param, argc, argv);
    return token;
}

bool parseIntParam(const char* param, int argc, char* argv[], unsigned int& out) {
    const char* token = findToken(param, argc, argv);
    if (token == nullptr)
        return false;

    int r = sscanf(token, "%u,", &out);
    if (r != 1 || r == EOF) {
        return false;
    }
    else {
        return true;
    }
}

int main(int argc, char *argv[])
{
    
    //
    // command line parsing.
    //
    const char* inFile = parseStringParam("-in", argc, argv);
    if (inFile == nullptr) printHelpExit();

    const char* outFile = parseStringParam("-out", argc, argv);
    if (outFile == nullptr) printHelpExit();

    unsigned int outFacesN;
    if (!parseIntParam("-outfaces", argc, argv, outFacesN)) printHelpExit();

    unsigned int upsampleN;
    if (!parseIntParam("-upsample", argc, argv, upsampleN)) printHelpExit();
    
    // original mesh vertices and indices. This is the original mesh, which has a hole.
    MatrixXd originalV;
    MatrixXi originalF;

    if (!igl::readOFF(inFile, originalV, originalF)) {
        printHelpExit();
    }

    VectorXi originalLoop; // indices of the boundary of the hole. 
    igl::boundary_loop(originalF, originalLoop);

    if (originalLoop.size() == 0) {
        printf("Mesh has no hole!");
        printHelpExit();
    }

    // upsample the original mesh. this makes fusing the original mesh with the patch much easier.
    igl::upsample(Eigen::MatrixXd(originalV), Eigen::MatrixXi(originalF), originalV, originalF, upsampleN);

    // compute boundary center.
    VectorXd bcenter(3);
    {
        VectorXi R = originalLoop;
        VectorXi C(3); C << 0, 1, 2;
        MatrixXd B;
        MatrixXd A = originalV;
        igl::slice(A, R, C, B);
        bcenter = (1.0f / originalLoop.size()) * B.colwise().sum();
    }

    // a flat patch that fills the hole.
    MatrixXd patchV = MatrixXd(originalLoop.size() + 1, 3); // patch will have an extra vertex for the center vertex.
    MatrixXi patchF = MatrixXi(originalLoop.size(), 3);

    {
        VectorXi R = originalLoop;
        VectorXi C(3); C << 0, 1, 2;
        MatrixXd A = originalV;
        MatrixXd temp1;
        igl::slice(A, R, C, temp1);

        MatrixXd temp2(1, 3);
        temp2 << bcenter(0), bcenter(1), bcenter(2);

        // patch vertices will be the boundary vertices, plus a center vertex. concat these together.
        igl::cat(1, temp1, temp2, patchV);

        // create triangles that connect boundary vertices and the center vertex.
        for (int i = 0; i < originalLoop.size(); ++i) {
            patchF(i, 2) = (int)originalLoop.size();
            patchF(i, 1) = i;
            patchF(i, 0) = (1 + i) % originalLoop.size();
        }

        // also upsample patch. patch and original mesh will have the same upsampling level now
        // making it trivial to fuse them together.
        igl::upsample(Eigen::MatrixXd(patchV), Eigen::MatrixXi(patchF), patchV, patchF, upsampleN);
    }

    // the final mesh, where the patch and original mesh has been fused together.
    std::vector<std::vector<double>> fusedV;
    std::vector<std::vector<int>> fusedF;

    int index = 0; // vertex index counter for the fused mesh.

                   // first, we add the upsampled patch to the fused mesh.
    {
        for (; index < patchV.rows(); ++index) {
            fusedV.push_back({ patchV(index, 0), patchV(index, 1), patchV(index, 2) });
        }

        int findex = 0;
        for (; findex < patchF.rows(); ++findex) {
            fusedF.push_back({ patchF(findex, 0), patchF(findex, 1), patchF(findex, 2) });
        }
    }

    // now, we fuse the patch and the original mesh together.
    {
        // remaps indices from the original mesh to the fused mesh.
        std::map<int, int> originalToFusedMap;

        for (int itri = 0; itri < originalF.rows(); ++itri) {

            int triIndices[3];
            for (int iv = 0; iv < 3; ++iv) {

                int triIndex = originalF(itri, iv);

                int ret;

                if (originalToFusedMap.count(triIndex) == 0) {
                    int foundMatch = -1;

                    // the vertices at the boundary are the same, for both the two meshes(patch and original mesh).
                    // we ensure that these vertices are not duplicated.
                    // this is also how we ensure that the two meshes are fused together.
                    for (int jj = 0; jj < patchV.rows(); ++jj) {
                        VectorXd u(3); u << fusedV[jj][0], fusedV[jj][1], fusedV[jj][2];
                        VectorXd v(3); v << originalV(triIndex, 0), originalV(triIndex, 1), originalV(triIndex, 2);

                        if ((u - v).norm() < 0.00001) {
                            foundMatch = jj;
                            break;
                        }
                    }

                    if (foundMatch != -1) {
                        originalToFusedMap[triIndex] = foundMatch;
                        ret = foundMatch;
                    }
                    else {
                        fusedV.push_back({ originalV(triIndex, 0), originalV(triIndex, 1), originalV(triIndex, 2) });
                        originalToFusedMap[triIndex] = index;
                        ret = index;
                        index++;
                    }
                }
                else {
                    ret = originalToFusedMap[triIndex];
                }

                triIndices[iv] = ret;
            }

            fusedF.push_back({
                triIndices[0],
                triIndices[1],
                triIndices[2] });


        }

    }

    MatrixXd fairedV(fusedV.size(), 3);
    MatrixXi fairedF(fusedF.size(), 3);
    // now we shall do surface fairing on the mesh, to ensure
    // that the patch conforms to the surrounding curvature.
    {

        for (int vindex = 0; vindex < fusedV.size(); ++vindex) {
            auto r = fusedV[vindex];

            fairedV(vindex, 0) = r[0];
            fairedV(vindex, 1) = r[1];
            fairedV(vindex, 2) = r[2];
        }

        for (int findex = 0; findex < fusedF.size(); ++findex) {
            auto r = fusedF[findex];

            fairedF(findex, 0) = r[0];
            fairedF(findex, 1) = r[1];
            fairedF(findex, 2) = r[2];
        }

        VectorXi b(fairedV.rows() - patchV.rows());
        MatrixXd bc(fairedV.rows() - patchV.rows(), 3);
        // setup the boundary conditions. This is simply the vertex positions of the vertices not part of the patch.
        for (int i = (int)patchV.rows(); i < (int)fairedV.rows(); ++i) {
            int jj = i - (int)patchV.rows();

            b(jj) = i;

            bc(jj, 0) = fairedV(i, 0);
            bc(jj, 1) = fairedV(i, 1);
            bc(jj, 2) = fairedV(i, 2);
        }

        MatrixXd Z;
        int k = 2;
        // surface fairing simply means that we solve the equation
        // Delta^2 f = 0
        // with appropriate boundary conditions.
        // this function igl::harmonic from libigl takes care of that.

        // note that this is pretty inefficient thought.
        // the only boundary conditions necessary are the 2-ring of vertices around the patch.
        // the rest of the mesh vertices need not be specified.
        // we specify the rest of the mesh for simplicity of code, but it is not strictly necessary,
        // and pretty inefficient, since we have to solve a LARGE matrix equation as a result of this.
        igl::harmonic(fairedV, fairedF, b, bc, k, Z);
        fairedV = Z;
    }

    // finally, we do a decimation step.
    MatrixXd finalV(fusedV.size(), 3);
    MatrixXi finalF(fusedF.size(), 3);
    VectorXi temp0; VectorXi temp1;
    igl::decimate(fairedV, fairedF, outFacesN, finalV, finalF, temp0, temp1);
    
    igl::writeOFF(outFile, finalV, finalF);
    std::cout << "执行成功!" << std::endl;
    system("pause");

    return 0;
}

全部的代码可以点击网址:https://github.com/Erkaman/hole_fixer

三、配置工作

1、代码获取到之后,首先使用cmake工具生成VS项目。

2、右击"ALL BUILD"生成所有项目,待生成所有项目之后,设置相应的命令行参数。

3、最后设置hole_fixer项目为启动项目,运行程序就可以了。

四、执行效果

不过可以明显的看到,修补的效果并不是很理想,补丁部分明显与周围的网格密度有很大差别,这还需后续的整形处理。

四、参考资料

[1]https://github.com/Erkaman/hole_fixer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容