等值线算法

等值线算法

最近导师提了一个实现等值线算法的要求。在思考要怎么写的时候,突然想到表面重建就是重建的等值面,那来个降维打击,不就是等值线了嘛!

等值线算法思路

网上以及许多文章里的等值线算法都是基于三角形网格的算法,由于我项目的数据性质,我就使用正方形的网格算法,主要是方便编写哈哈。

等值线算法主要步骤分为以下几步:

  1. 构建网格,并为每个网格分配索引。
  2. 根据每个网格索引去索引边表,获取其所在边位置(并赋予边编号),并记录点坐标和边编号。
  3. 通过每个网格索引去索引线表,索引出点的连接顺序(连接顺序形式为边编号),用边编号从记录的信息中索引出点的坐标位置。
  4. 最后根据所有点的坐标绘制连线。

构建网格

如该图所示,我们构建一个N*M的网格,网格上每个黑点都有一个数据值(降水量)。我们令黑点上的值大于阈值(等值线值)时为1,小于阈值时为0,我们可以保存在一个4位2进制内。我们设定一个顶点顺序(白色数字)和边顺序(绿色数字)我们:

根据该顺序,我们保存4位2进制根据如下图:

顶点和边的4位二进组都根据上图保存,得到的数最后转化为10进制即可。我们根据此便可以画出所有的情况:

如图所示,我们会发现所有的情况为以上16种。比如我们的1,2,4号顶点标记为0。3号顶点标记为1。我们能获得4位2进制数0010(10进制为2),我们发现该情况边会连接在第三号边和第四号边上,即:

可以得到新的4位2进制0011(10进制为3),因为连接线处于三号边和4号边上。所以我们会得到edgeTable[2]=3。我们列出所有的16种情况就可以建立边表:

//矩形4位2进制对应边索引。如0010——>0011,然后转为10进制
const unsigned char edgeTable[16]={
    0,    9,    3,    10,
    6,    15,    5,    12,
    12,    5,    15,    6,
    10,    3,    9,    0
};

我们还可以同样的建立一个线表,表示该网格连接的线为哪两条边(由于有可能出现2条线的情况,所以我们第二维用大小为5的数组,便于之后的遍历)。

//矩形4位2进制对应连接线,连接线对应两个边(索引)上的点。255指边上无点
const unsigned char lineTable[16][5]={
    {255,255,255,255,255},
    {4,1,255,255,255},
    {2,1,255,255,255},
    {4,2,255,255,255},
    {3,2,255,255,255},
    {4,3,2,1,255},
    {3,1,255,255,255},
    {4,3,255,255,255},
    {4,3,255,255,255},
    {3,1,255,255,255},
    {3,2,1,4,255},
    {3,2,255,255,255},
    {4,2,255,255,255},
    {2,1,255,255,255},
    {4,1,255,255,255},
    {255,255,255,255,255}
};

为每个网格分配索引

网格构建完毕之后,分配索引就很简单了,我们只要遍历网格,判断其是否大于等值线阈值然后赋值即可。代码如下:

//等值线生成
    for (int iso=0; iso<m_tIsoLevel.size(); iso++) {//多条等值线情况
        for (unsigned int y=0; y<m_Grid[1]; y++) {
            for (unsigned int x=0; x<m_Grid[0]; x++) {
                //计算网格内的顶点放置信息索引
                unsigned int tableIndex=0;
                int stx=m_stride*x,sty=m_stride*y;//stride为绘制网格的缩放值,为1就是1:1

                if (m_ptScalarField[6*(sty*m_sliceX+stx)+2]<m_tIsoLevel[iso])
                    tableIndex|=1;
                if (m_ptScalarField[6*(sty*m_sliceX+stx+m_stride)+2]<m_tIsoLevel[iso])
                    tableIndex|=2;
                if (m_ptScalarField[6*((sty+m_stride)*m_sliceX+stx+m_stride)+2]<m_tIsoLevel[iso])
                    tableIndex|=4;
                if (m_ptScalarField[6*((sty+m_stride)*m_sliceX+stx)+2]<m_tIsoLevel[iso])
                    tableIndex|=8;
                 
                 ...

                }
           }
}

索引边表

我们已经获得了所有的网格索引,可以通过网格索引判断顶点落在哪个边上了。我们为所有边编号:

通过网格索引获取边信息,然后转化为所有边的编号,然后获取边的两个顶点,进行插值计算点的位置,我们就保存该点的位置信息和边编号。

索引线表

通过网格索引,获取连线的两个顶点信息,通过在边索引表阶段保存的点信息和编号,保存连线的最终点信息。绘制效果如下图:

若我们选择等值线阈值为4,则可生成图示效果。

下面贴出源码,.h文件:

#include <stdio.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <map>
#include <string>
#include <vector>
#include <iterator>
#include "Ct_table.hpp"

using namespace std;

struct CtVertexID{ //点的信息
    unsigned int newID;//索引
    double x,y;//位置
};

struct CtLine{ //线的信息
    unsigned int pointID[2];//存有每个点的索引
};


typedef std::map<unsigned int,CtVertexID> ID2VertexID;//边序号to点信息

typedef std::vector<CtLine> CtLineV;//保存所有的线的vector

class Contour_line{
public:
    Contour_line();
    ~Contour_line();
    
    //从标量场生成等值线(vrts保存所有点,lns保存所有的线上点点索引)
    bool CreatLineV(float* field,int n[2],vector<float> threshold,vector<float>& vrts,
                    vector<int> &lns);
    
    //从标量场生成直线段
    void GenerateLineV(vector<float>& vrts,
                           vector<int> &lns);
    
    //等值线创建成功则返回true
    bool IsLineValid() const { return m_bValidLine; }
    
    //删除等值线
    void DeleteLine();
    
    //获取等值线有关的信息
    unsigned int GetNumVertices() const { return m_nVertices; }
    unsigned int GetNumLines() const { return m_nLines; }
    
    void SetStride(int s){m_stride=s;}

    
private:
    unsigned int m_nVertices,m_nLines;//顶点个数与线段个数
    ID2VertexID m_i2v;//形成等值线的顶点列表(以边索引为key,等值面的点为value)
    CtLineV m_lineVertex;//形成等值线的顶点列表
    int m_Grid[2];//网格大小x*y,x:[0],y:[1]
    
    const float * m_ptScalarField;     //保存标量场的样本值
    
    vector<float> m_tIsoLevel;   //阈值
    bool m_bValidLine;   //表面是否生成成功
    
    int m_stride;//网格偏移量
    
    int m_sliceX;//数据的列数
    
    // 边id
    unsigned int GetEdgeID(unsigned int nX, unsigned int nY, unsigned int nEdgeNo);
    // 顶点ID
    unsigned int GetVertexID(unsigned int nX, unsigned int nY);

    // 计算边上的等值点
    CtVertexID CalculateIntersection(unsigned int nX, unsigned int nY,
                                     unsigned int nEdgeNo,int isoI);
    // 通过边两端的隐式函数值的线性插值计算等值点
    CtVertexID Interpolate(double fX1, double fY1, double fX2, double fY2,
                           float tVal1, float tVal2,int isoI);
    // 以输出形式存储顶点和等值线几何信息
    void RenameVerticesAndLines(vector<float> &vrts, unsigned int &nvrts,
                                    vector<int> &lns, unsigned int &nlns);
    
};

.cpp文件:

#include "contour.hpp"

Contour_line::Contour_line(){
    m_Grid[0]=0;
    m_Grid[1]=0;
    m_stride=1;
    
    m_nVertices=0;
    m_nLines=0;
    
    m_ptScalarField=nullptr;
    m_bValidLine=false;
}

Contour_line::~Contour_line(){
    DeleteLine();
}

void Contour_line::DeleteLine(){
    m_Grid[0]=0;
    m_Grid[1]=0;
    
    m_nVertices=0;
    m_nLines=0;
    
    m_ptScalarField=nullptr;
    m_tIsoLevel.clear();
    m_bValidLine=false;
}

/* 等值线生成(标量场创建)
* @param [in] field标量场信息
* @param [in] threshold等值线阈值
* @param [out] vrts等值线顶点
* @param [out] lns等值线信息(顶点连接信息)
*/
bool Contour_line::CreatLineV(float* field,int n[2],vector<float> threshold,vector<float>& vrts,
                              vector<int>&lns){
    if (field==nullptr) return false;
    
    m_sliceX=n[0];
    m_Grid[0]=(n[0]-1)/m_stride;
    m_Grid[1]=(n[1]-1)/m_stride;
    
    m_tIsoLevel=threshold;
    m_ptScalarField=field;
    
    //生成等值线上的点和线
    GenerateLineV(vrts, lns);
    
    return true;
}

/* 等值线信息生成(标量场创建)
 * @param [in] field标量场信息
 * @param [in] threshold等值线阈值
 * @param [out] vrts等值线顶点
 * @param [out] lns等值线信息(顶点连接信息)
 */
void Contour_line::GenerateLineV(vector<float> &vrts, vector<int> &lns){
    if (m_bValidLine) {
        DeleteLine();
    }
    
    
    //等值线生成
    for (int iso=0; iso<m_tIsoLevel.size(); iso++) {
        for (unsigned int y=0; y<m_Grid[1]; y++) {
            for (unsigned int x=0; x<m_Grid[0]; x++) {
                //计算网格内的顶点放置信息索引
                unsigned int tableIndex=0;
                int stx=m_stride*x,sty=m_stride*y;

                int nanCount=0;
                if (m_ptScalarField[6*(sty*m_sliceX+stx)+2]==-1.0){
                    nanCount++;
                }
                else if (m_ptScalarField[6*(sty*m_sliceX+stx)+2]<m_tIsoLevel[iso])
                    tableIndex|=1;
                if (m_ptScalarField[6*(sty*m_sliceX+stx+m_stride)+2]==-1.0){
                    nanCount++;
                }
                else if (m_ptScalarField[6*(sty*m_sliceX+stx+m_stride)+2]<m_tIsoLevel[iso])
                    tableIndex|=2;
                if (m_ptScalarField[6*((sty+m_stride)*m_sliceX+stx+m_stride)+2]==-1.0){
                    nanCount++;
                }
                else if(m_ptScalarField[6*((sty+m_stride)*m_sliceX+stx+m_stride)+2]<m_tIsoLevel[iso])
                    tableIndex|=4;
                if (m_ptScalarField[6*((sty+m_stride)*m_sliceX+stx)+2]==-1.0){
                    nanCount++;
                }
                else if (m_ptScalarField[6*((sty+m_stride)*m_sliceX+stx)+2]<m_tIsoLevel[iso])
                    tableIndex|=8;
                
                if (edgeTable[tableIndex]!=0) {
                    //计算边上的顶点
                    if (edgeTable[tableIndex]&1) {
                        CtVertexID pt=CalculateIntersection(stx, sty, 1,iso);
                        unsigned int id=GetEdgeID(x, y, 1);
                        m_i2v.insert(ID2VertexID::value_type(id,pt));
                    }
                    if (edgeTable[tableIndex]&8) {
                        CtVertexID pt=CalculateIntersection(stx, sty, 4,iso);
                        unsigned int id=GetEdgeID(x, y, 4);
                        m_i2v.insert(ID2VertexID::value_type(id,pt));
                    }
                    if(x==m_Grid[0]-1){
                        if (edgeTable[tableIndex]&2) {
                            CtVertexID pt=CalculateIntersection(stx, sty, 2,iso);
                            unsigned int id=GetEdgeID(x, y, 2);
                            m_i2v.insert(ID2VertexID::value_type(id,pt));
                        }
                    }
                    if(y==m_Grid[1]-1){
                        if (edgeTable[tableIndex]&4) {
                            CtVertexID pt=CalculateIntersection(stx, sty, 3,iso);
                            unsigned int id=GetEdgeID(x, y, 3);
                            m_i2v.insert(ID2VertexID::value_type(id,pt));
                        }
                    }
                    
                    
                    //生成等值线,获取线上的点在哪条边上
                    for(unsigned int i=0;lineTable[tableIndex][i]!=255;i+=2){
                        CtLine line;
                        unsigned int pointID1,pointID2;
                        pointID1=GetEdgeID(x, y, lineTable[tableIndex][i]);
                        pointID2=GetEdgeID(x, y, lineTable[tableIndex][i+1]);
                        line.pointID[0]=pointID1;
                        line.pointID[1]=pointID2;
                        m_lineVertex.push_back(line);
                    }
                }
            }
        }
        RenameVerticesAndLines(vrts, m_nVertices, lns, m_nLines);
    }
    m_bValidLine=true;
}

/* 获取边索引
 * @param [in] nX当前网格x值
 * @param [in] nY当前网格y值
 * @param [in] nEdgeNo网格索引(1-4)
 */
unsigned int Contour_line::GetEdgeID(unsigned int nX, unsigned int nY, unsigned int nEdgeNo){
    switch (nEdgeNo) {
        case 1:
            return GetVertexID(nX, nY);
            break;
        case 2:
            return GetVertexID(nX+1, nY)+1;
            break;
        case 3:
            return GetVertexID(nX, nY+1);
            break;
        case 4:
            return GetVertexID(nX, nY)+1;
            break;
            
        default:
            //无效值
            return -1;
            break;
    }
}

/*!
 *获取顶点所在边ID
 * @param [in] nX,nY网格位置
 * @return顶点所在边ID
 */
unsigned int Contour_line::GetVertexID(unsigned int nX, unsigned int nY){
    return 2*(nY*(m_Grid[0]+1)+nX);
}

/*!
   * 通过插值计算边缘上的等值点(从样本量)
   * @param [in] nX,nY网格位置
   * @param [in] nEdgeNo边数
   * @return网格顶点信息
   */
CtVertexID Contour_line::CalculateIntersection(unsigned int nX, unsigned int nY, unsigned int nEdgeNo,int isoI){
    float x1, y1, x2, y2;
    unsigned int v1x = nX, v1y = nY;
    unsigned int v2x = nX, v2y = nY;
    switch (nEdgeNo) {
        case 1:
            v2x+=m_stride;
            break;
        case 2:
            v1x+=m_stride;
            v2x+=m_stride;
            v2y+=m_stride;
            break;
        case 3:
            v2x+=m_stride;
            v1y+=m_stride;
            v2y+=m_stride;
            break;
        case 4:
            v2y+=m_stride;
            break;
            
        default:
            break;
    }
    
    //获取边的两点坐标
    x1=m_ptScalarField[6*(v1y*m_sliceX+v1x)];
    y1=m_ptScalarField[6*(v1y*m_sliceX+v1x)+1];
    x2=m_ptScalarField[6*(v2y*m_sliceX+v2x)];
    y2=m_ptScalarField[6*(v2y*m_sliceX+v2x)+1];
    
    float val1=m_ptScalarField[6*(v1y*m_sliceX+v1x)+2];
    float val2=m_ptScalarField[6*(v2y*m_sliceX+v2x)+2];
    CtVertexID  intersection=Interpolate(x1, y1, x2, y2, val1, val2, isoI);
    
    return intersection;
}


/*!
   *通过网格边缘两端隐式函数值的线性插值计算等值点
   * @param [in] fX 1,fY 1终点坐标1
   * @param [in] fX 2,fY 2终点坐标2
   * @param [in] tVal 1在终点坐标1处的标量值
   * @param [in] tVal 2在终点坐标2处的标量值
   * @返回顶点信息
   */
CtVertexID Contour_line::Interpolate(double fX1, double fY1, double fX2, double fY2, float tVal1, float tVal2,int isoI){
    CtVertexID interp;
    float mu=float(m_tIsoLevel[isoI]-tVal1)/(tVal2-tVal1);
    interp.x=fX1+mu*(fX2-fX1);
    interp.y=fY1+mu*(fY2-fY1);
    
    return interp;
}

/*!
   * 复制信息到顶点和线缓存中
   * @param [out] vrts顶点坐标
   * @param [out] nvrts顶点数
   * @param [out] lns等值线的几何信息
   * @param [out] nlns等值线的数量
   */
void Contour_line::RenameVerticesAndLines(vector<float> &vrts, unsigned int &nvrts, vector<int> &lns, unsigned int &nlns){
    static unsigned int nextID=0;
    auto mapIt=m_i2v.begin();
    auto vecIt=m_lineVertex.begin();
    
    //为每个顶点标记在顶点缓存中的编号
    while (mapIt!=m_i2v.end()) {
        (*mapIt).second.newID=nextID;
        nextID++;
        mapIt++;
    }
    
    //将等值线vector内的顶点编号更新为顶点缓存中的编号
    while (vecIt!=m_lineVertex.end()) {
        for (unsigned int i=0; i<2; i++) {
            unsigned int newID=m_i2v[(*vecIt).pointID[i]].newID;
            (*vecIt).pointID[i]=newID;
        }
        vecIt++;
    }
    
    //复制所有的顶点到顶点缓存vrts中
    mapIt=m_i2v.begin();
    nvrts=int(m_i2v.size());
    for (unsigned int i=0; i<nvrts; i++,mapIt++) {
        vrts.push_back((*mapIt).second.x);
        vrts.push_back((*mapIt).second.y);
    }
    
    //复制所有的线索引
    vecIt=m_lineVertex.begin();
    nlns=int(m_lineVertex.size());
    for (unsigned int i=0; i<nlns; i++,vecIt++) {
        lns.push_back((*vecIt).pointID[0]);
        lns.push_back((*vecIt).pointID[1]);
    }
    
    //释放空间
    m_i2v.clear();
    m_lineVertex.clear();
}

最后我根据我的数据绘制了总体效果图为:

由于数据中有许多nan(空)值,所以这里我就直接在空值不去绘制线条。

发布了22 篇原创文章 · 获赞 22 · 访问量 2586

猜你喜欢

转载自blog.csdn.net/qq_39300235/article/details/102860310
今日推荐