欢迎关注更多精彩
关注我,学习常用算法与数据结构,一题多解,降维打击。

qem 全称Quadic Error Metrics 网格简化。
它是一种基于二次度量误差的优化算法。

边塌缩算法

qem算法的基本思想是对某一边进行塌缩,将边的2点合成一个点,如下图。

那么要选择哪条边进行塌缩呢。
就是基于二次度量误差。


上图表示。
假 设 p i , p j 点 边 塌 缩 前 的 端 点 , p x 为 塌 缩 后 的 点 坐 标 假设p_i,p_j点边塌缩前的端点,p_x为塌缩后的点坐标 pi,pjpx

那 么 p x 需 要 到 p i , p i 各 邻 面 的 距 离 平 方 和 最 小 那么p_x需要到p_i,p_i 各邻面的距离平方和最小 pxpi,pi
能量如下:
E x = ∑ P s ∈ Ω ( i ) d ( p x , P s ) 2 + ∑ P s ∈ Ω ( j ) d ( p x , P s ) 2 E_x = \displaystyle \sum_{P_s \in \Omega(i)}d(p_x,P_s)^2 + \displaystyle \sum_{P_s \in \Omega(j)}d(p_x,P_s)^2 Ex=PsΩ(i)d(px,Ps)2+PsΩ(j)d(px,Ps)2

d ( p x , P s ) 代 表 p x 到 平 面 P s 的 垂 线 距 离 d(p_x, P_s) 代表p_x到平面P_s的垂线距离 d(px,Ps)pxPs线

只 要 向 量 p x 在 P s 法 向 上 的 投 影 减 去 平 面 上 一 点 在 法 向 上 的 投 影 即 可 只要向量p_x在P_s法向上的投影减去平面上一点在法向上的投影即可 pxPs

由 于 p i 是 P s 上 一 点 由于p_i是P_s上一点 piPs
d ( p x , P s ) = p x ⋅ n s − p i ⋅ n s d(p_x, P_s)=p_x\cdot n_s-p_i\cdot n_s d(px,Ps)=pxnspins

E x = ∑ P s ∈ Ω ( i ) ( p x ⋅ n s − p i ⋅ n s ) 2 + ∑ P s ∈ Ω ( j ) ( p x ⋅ n s − p j ⋅ n s ) 2 E_x = \displaystyle \sum_{P_s \in \Omega(i)}(p_x\cdot n_s-p_i\cdot n_s)^2 + \displaystyle \sum_{P_s \in \Omega(j)}(p_x\cdot n_s-p_j\cdot n_s)^2 Ex=PsΩ(i)(pxnspins)2+PsΩ(j)(pxnspjns)2

优化上述方程使得E最小就可以得到px坐标。

从矩阵角度来求距离平方和


上 式 中 n i , − d i 都 是 常 量 , 所 以 Q i 也 是 常 上式中n_i, -d_i都是常量,所以Q_i也是常 ni,diQi

再看上式

令 p x ‾ = ( p x , 1 ) 令\overline{p_x} = (p_x, 1) px=(px,1)

∑ P s ∈ Ω ( i ) d ( p x , P s ) 2 \displaystyle \sum_{P_s \in \Omega(i)}d(p_x,P_s)^2 PsΩ(i)d(px,Ps)2

= ∑ P s ∈ Ω ( i ) p x ‾ T Q s p x ‾ =\displaystyle \sum_{P_s \in \Omega(i)}\overline{p_x}^TQ_s \overline{p_x} =PsΩ(i)pxTQspx

= p x ‾ T [ ∑ P s ∈ Ω ( i ) Q s ] p x ‾ =\overline{p_x}^T\left[\displaystyle \sum_{P_s \in \Omega(i)}Q_s \right]\overline{p_x} =pxTPsΩ(i)Qspx

从上述可以看出可以先对某点的总和计算。

Q t i = [ ∑ P s ∈ Ω ( i ) Q s ] Qt_i = \left[\displaystyle \sum_{P_s \in \Omega(i)}Q_s \right] Qti=PsΩ(i)Qs

E x = p x ‾ T Q t i p x ‾ + p x ‾ T Q t j p x ‾ E_x=\overline{p_x}^TQt_i\overline{p_x}+\overline{p_x}^TQt_j\overline{p_x} Ex=pxTQtipx+pxTQtjpx

= p x ‾ T ( Q t i + Q t j ) p x ‾ =\overline{p_x}^T(Qt_i+Qt_j)\overline{p_x} =pxT(Qti+Qtj)px

可 以 看 出 上 只 有 p x 是 变 量 , 是 一 个 二 次 能 量 , 可 以 通 过 优 化 得 到 p x 最 优 解 , 即 为 塌 缩 后 的 点 坐 标 位 置 可以看出上只有p_x是变量,是一个二次能量,可以通过优化得到p_x最优解,即为塌缩后的点坐标位置 pxpx

将 v 看 成 是 一 个 向 量 v = [ x , y , z , 1 ] , 并 展 开 得 到 下 式 , 然 后 进 行 求 导 , 并 可 以 得 到 A v = b 形 式 , 角 得 v = A − 1 b 将v看成是一个向量v=[x,y,z,1],并展开得到下式,然后进行求导,并可以得到Av=b形式, 角得v=A^{-1}b vv=[x,y,z,1]Av=b,v=A1b

算法步骤


for 所有点 i {
	计算点能量总和Qti
}

for 所有边 x{
	得到Ex, 并解出最优点px
}

while(点总数>目标值) {
	先出Ex最小的边
	将该边的两点从网格中删除。
	将两点从网格中删除。
	添加新的点。
	连接受影响的点。
	重新计算新边的能量。
}

代码实现

代码库

//
// Created by chenbinbin on 2022/5/9.
//

#include "hw9.h"
#include "../../PolyMesh/IOManager.h"
#include <Eigen/Dense>
#include <queue>
#include <map>
#include <time.h>
#include <unistd.h>
#include <string>

using namespace std;

#define lambda 1e3

using namespace acamcad;
using namespace polymesh;

struct Edge_priority
{
    double cost;
    MEdge* eh;
    int state;
    MPoint3 NewPoint;
};

struct cmp
{
    bool operator()(const Edge_priority &a, const Edge_priority &b)
    {
        return a.cost > b.cost;
    }
};

std::priority_queue<Edge_priority, std::vector<Edge_priority>, cmp> Cost;
std::map<MVert*, Eigen::Matrix4d> Qv;
std::map<MEdge*, int> State;


void cal_Q(MVert* vh, PolyMesh* mesh)
{
    MVector3 p_vh = vh->position();
    Eigen::Matrix4d Q_temp;
    Q_temp.setZero();
    MVector3 face_nor;
    double a, b, c, d;
    Eigen::Matrix<double, 4, 1> p;
    /*for (auto vf_it = mesh->vf_iter(vh); vf_it.isValid(); ++vf_it)
    {
        face_nor = (*vf_it)->normal();
        a = face_nor[0], b = face_nor[1], c = face_nor[2], d = -dot(face_nor, p_vh);
        p = { a,b,c,d };
        Q_temp += p * p.transpose();
    }
    Qv[vh] = Q_temp;*/
    if (!mesh->isBoundary(vh))
    {
        for (auto vf_it = mesh->vf_iter(vh); vf_it.isValid(); ++vf_it)
        {
            face_nor = (*vf_it)->normal();
            a = face_nor[0], b = face_nor[1], c = face_nor[2], d = -dot(face_nor, p_vh);
            p = { a,b,c,d };
            Q_temp += p * p.transpose();
        }
        Qv[vh] = Q_temp;
    }
    else
    {
        /*for (auto vf_it = mesh->vf_iter(vh); vf_it.isValid(); ++vf_it)
        {
            face_nor = (*vf_it)->normal();
            a = face_nor[0], b = face_nor[1], c = face_nor[2], d = -dot(face_nor, p_vh);
            p = { a,b,c,d };
            Q_temp += p * p.transpose();
            for (auto fhh_it = mesh->fhe_iter(*vf_it); fhh_it.isValid(); ++fhh_it)
            {
                if ((*fhh_it)->pair()->isBoundary())
                {
                    MVector3 vir_face_nor = (cross((*fhh_it)->pair()->tangent(), face_nor)).normalized();
                    a = vir_face_nor[0], b = vir_face_nor[1], c = vir_face_nor[2], d = -dot(vir_face_nor, p_vh);
                    p = { a,b,c,d };
                    Q_temp += lambda * (p * p.transpose());
                    break;
                }
            }
        }*/

        for (auto vhh_it = mesh->voh_iter(vh); vhh_it.isValid(); ++vhh_it)
        {
            if (!(*vhh_it)->isBoundary())
            {
                MPolyFace* vf = (*vhh_it)->polygon();
                MVector3 face_nor = vf->normal();
                double a = face_nor[0], b = face_nor[1], c = face_nor[2], d = -dot(face_nor, p_vh);
                Eigen::Matrix<double, 4, 1> p = { a,b,c,d };
                Q_temp += p * p.transpose();
            }
            else
            {
                MHalfedge* hh_boundary = *vhh_it;
                MHalfedge* prev_hh_boundary = hh_boundary->prev();
                MPolyFace* face_hh = hh_boundary->pair()->polygon(), *face_prev_hh = prev_hh_boundary->pair()->polygon();
                MVector3 face_hh_nor = face_hh->normal(), face_prev_hh_nor = face_prev_hh->normal();
                MVector3 vir_face_hh_nor = cross(hh_boundary->tangent(), face_hh_nor).normalized(), vir_face_prev_hh_nor = cross(prev_hh_boundary->tangent(), face_prev_hh_nor).normalized();
                double a = vir_face_hh_nor[0], b = vir_face_hh_nor[1], c = vir_face_hh_nor[2], d = -dot(vir_face_hh_nor, p_vh);
                Eigen::Matrix<double, 4, 1> p = { a,b,c,d };
                Q_temp += lambda * (p * p.transpose());
                a = vir_face_prev_hh_nor[0], b = vir_face_prev_hh_nor[1], c = vir_face_prev_hh_nor[2], d = -dot(vir_face_prev_hh_nor, p_vh);
                p = { a,b,c,d };
                Q_temp += lambda * (p * p.transpose());
            }
        }
        Qv[vh] = Q_temp;
    }
}

void cal_Cost(MEdge* eh)
{
    Edge_priority temp;
    MHalfedge* hh = eh->halfEdge();
    MVert* v_from = hh->fromVertex(), *v_to = hh->toVertex();
    Eigen::Matrix4d Q_plus = Qv[v_from] + Qv[v_to], Q_solve = Q_plus;
    Q_solve(3, 0) = 0.0, Q_solve(3, 1) = 0.0, Q_solve(3, 2) = 0.0, Q_solve(3, 3) = 1.0;
    MPoint3 new_point;
    Eigen::Vector4d new_vec;
    if (Q_solve.determinant() == 0)
    {
        MVector3 temp = 0.5*(v_from->position() + v_to->position());
        new_point = { temp[0], temp[1], temp[2] };
        new_vec = { new_point[0], new_point[1], new_point[2], 1.0 };
    }
    else
    {
        Eigen::Vector4d temp = { 0.0,0.0,0.0,1.0 };
        new_vec = Q_solve.inverse()*temp;
        new_point = { new_vec[0], new_vec[1], new_vec[2] };
    }
    temp.cost = new_vec.transpose()*Q_plus*new_vec;
    temp.eh = eh;
    temp.state = State[eh];
    temp.NewPoint = new_point;
    Cost.push(temp);
}

void update_Q_and_Cost(MVert* vh, PolyMesh* mesh)
{
    cal_Q(vh, mesh);
    for (auto vv_it = mesh->vv_iter(vh); vv_it.isValid(); ++vv_it)
    {
        cal_Q(*vv_it, mesh);
    }
    for (auto ve_it = mesh->ve_iter(vh); ve_it.isValid(); ++ve_it)
    {
        State[*ve_it]++;
        cal_Cost(*ve_it);
    }
}

bool QEM_collapse(const Edge_priority &temp_edge, PolyMesh* mesh)
{
    bool is_collapse = false;
    MEdge* eh = temp_edge.eh;
    MHalfedge* hh = eh->halfEdge(), *hh_oppo = hh->pair();
    MVert *v_from = hh->fromVertex(), *v_to = hh->toVertex();
    MVert* vh;
//    if (mesh->is_collapse_ok(hh))
	if (mesh->is_collapse_ok_Triangle(hh))
    {
        v_to->setPosition(temp_edge.NewPoint);
        //mesh->collapse(hh);
        mesh->collapseTriangle(hh);
        is_collapse = true;
        vh = v_to;
    }
//    else if (mesh->is_collapse_ok(hh_oppo))
	else if (mesh->is_collapse_ok_Triangle(hh_oppo))
    {
        v_from->setPosition(temp_edge.NewPoint);
        //mesh->collapse(hh_oppo);
        mesh->collapseTriangle(hh_oppo);
        is_collapse = true;
        vh = v_from;
    }
    if (is_collapse)
    {
        update_Q_and_Cost(vh, mesh);
    }
    return is_collapse;
}

void QEM(PolyMesh* mesh)
{
    if (mesh->numVertices() == 3)
        return;
    /// Initial Qv
    for (MVert* vh : mesh->vertices())
    {
        MVector3 p_vh = vh->position();
        Eigen::Matrix4d Q_temp;
        Q_temp.setZero();
        MVector3 face_nor;
        double a, b, c, d;
        Eigen::Matrix<double, 4, 1> p;
        if (!mesh->isBoundary(vh))
        {
            for (auto vf_it = mesh->vf_iter(vh); vf_it.isValid(); ++vf_it)
            {
                face_nor = (*vf_it)->normal();
                a = face_nor[0], b = face_nor[1], c = face_nor[2], d = -dot(face_nor, p_vh);
                p = { a,b,c,d };
                Q_temp += p * p.transpose();
            }
            Qv.insert(std::make_pair(vh, Q_temp));
        }
        else
        {
            for (auto vhh_it = mesh->voh_iter(vh); vhh_it.isValid(); ++vhh_it)
            {
                if (!(*vhh_it)->isBoundary())
                {
                    MPolyFace* vf = (*vhh_it)->polygon();
                    MVector3 face_nor = vf->normal();
                    double a = face_nor[0], b = face_nor[1], c = face_nor[2], d = -dot(face_nor, p_vh);
                    Eigen::Matrix<double, 4, 1> p = { a,b,c,d };
                    Q_temp += p * p.transpose();
                }
                else
                {
                    MHalfedge* hh_boundary = *vhh_it;
                    MHalfedge* prev_hh_boundary = hh_boundary->prev();
                    MPolyFace* face_hh = hh_boundary->pair()->polygon(), *face_prev_hh = prev_hh_boundary->pair()->polygon();
                    MVector3 face_hh_nor = face_hh->normal(), face_prev_hh_nor = face_prev_hh->normal();
                    MVector3 vir_face_hh_nor = cross(hh_boundary->tangent(), face_hh_nor).normalized(), vir_face_prev_hh_nor = cross(prev_hh_boundary->tangent(), face_prev_hh_nor).normalized();
                    double a = vir_face_hh_nor[0], b = vir_face_hh_nor[1], c = vir_face_hh_nor[2], d = -dot(vir_face_hh_nor, p_vh);
                    Eigen::Matrix<double, 4, 1> p = { a,b,c,d };
                    Q_temp += lambda * (p * p.transpose());
                    a = vir_face_prev_hh_nor[0], b = vir_face_prev_hh_nor[1], c = vir_face_prev_hh_nor[2], d = -dot(vir_face_prev_hh_nor, p_vh);
                    p = { a,b,c,d };
                    Q_temp += lambda * (p * p.transpose());
                }
            }
            Qv.insert(std::make_pair(vh, Q_temp));
        }

        /*for (auto vf_it = mesh->vf_iter(vh); vf_it.isValid(); ++vf_it)
        {
            face_nor = (*vf_it)->normal();
            double a = face_nor[0], b = face_nor[1], c = face_nor[2], d = -dot(face_nor, p_vh);
            Eigen::Matrix<double, 4, 1> p = { a,b,c,d };
            Q_temp += p * p.transpose();
        }
        Qv.insert(std::make_pair(vh, Q_temp));*/
    }

    ///Initial Cost
    for (MEdge* eh : mesh->edges())
    {
        State.insert(std::make_pair(eh, 0));
        cal_Cost(eh);
    }

    /// simplification
    int N_V = mesh->numVertices();
    int target_num = std::min((int)(0.5*N_V), 1000);
    while(N_V > target_num)
    {
        Edge_priority temp_edge = Cost.top();
        Cost.pop();
        MEdge* eh = temp_edge.eh;
        if (temp_edge.state == State[eh])
        {
            if (eh->index() != -1)
            {
                if (QEM_collapse(temp_edge, mesh))
                {
                    N_V--;
                }
            }
        }
    }
}

void qem_simplification()
{
    char buffer[500];
    getcwd(buffer, 500);
    printf("The current directory is: %s/../\n", buffer);
    string mesh_path = buffer;
    mesh_path += "/../src/hw9/dragon.obj";
    PolyMesh* mesh = new PolyMesh();
    loadMesh(mesh_path, mesh);

    // read input mesh
    //PolyMesh* mesh = new PolyMesh();
    //loadMesh("cat_open.obj", mesh);

    clock_t start, end;
    std::cout << "Begin QEM" << std::endl;
    start = clock();
    QEM(mesh);
    end = clock();
    std::cout << "time: " << (double)(end - start) / CLOCKS_PER_SEC << "s" << std::endl;
    writeMesh("dragon-simplification-tri.obj", mesh);
}

效果展示

简化前

简化到20000点

简化10000点

简化到5000点

简化到2000点

简化到1000点

可以看到简化后网格数少了,基本形状还是维持得不错。

三角网格Loop细分

在这里插入图片描述
在这里插入图片描述


本人码农,希望通过自己的分享,让大家更容易学懂计算机知识。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐