等值线算法
最近导师提了一个实现等值线算法的要求。在思考要怎么写的时候,突然想到表面重建就是重建的等值面,那来个降维打击,不就是等值线了嘛!
等值线算法思路
网上以及许多文章里的等值线算法都是基于三角形网格的算法,由于我项目的数据性质,我就使用正方形的网格算法,主要是方便编写哈哈。
等值线算法主要步骤分为以下几步:
- 构建网格,并为每个网格分配索引。
- 根据每个网格索引去索引边表,获取其所在边位置(并赋予边编号),并记录点坐标和边编号。
- 通过每个网格索引去索引线表,索引出点的连接顺序(连接顺序形式为边编号),用边编号从记录的信息中索引出点的坐标位置。
- 最后根据所有点的坐标绘制连线。
构建网格
如该图所示,我们构建一个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(空)值,所以这里我就直接在空值不去绘制线条。