引言
生产或生活中,我们往往只能得到有限的关于某个物理量的离散点(比如一张图片,我们只能得到其原大小内每个坐标处的颜色信息),但是很多时候,我们还需要不在已有栅格点的物理量信息(比如图像缩放时)。此时,就需要某种估计方法,用于得到该坐标处的物理量值,此时就需要通过插值得到。常用的插值算法有很多,从简单的线性插值,Lagrange插值,Newton插值,到较复杂的Herminate插值,二维三次卷积插值,分段多项式插值以及样条插值。
那么在使用时,该如何选择呢?
一般来说,有两个准则:第一个是要满足精度要求、连续性要求(比如曲面过渡往往要求一阶或者二阶导数连续);第二个是要满足具体使用时的算法复杂度要求。算法本无优劣,须要结合具体情况灵活选择。
这里介绍一种在图像处理和DEM(Digital Elevation Model)中应用很广的一种插值算法—-二维三次卷积插值算法。
DEM数据是二维数据,对于二维数据,经常使用的插值方法有双线性插值和双立方插值,它们都是在最邻近的四个网点之间进行插值。双线性插值算法简单,但是却无法得到网点处的导数.双立方Herminate插值是四个邻近网点高度的加权和,且方程连接可微。但是该方法的缺点是网点处的微分被强迫为零,虽保证了插值方程的连续性,但是地形在网点处为平面时和直觉矛盾。
综合考虑算法效率和插值精度,二维三次卷积插值是比较理想的插值方法。
在处理地形时, 二维三次插值算法是以待插值点周围16个点的高程值来得到它自己的高程值。如下图所示。
由于该插值方法需要使用未知点周围16个点的数据,当数字地形风格为有限大小时,如果未知点处于风格国家级,就无法得到风格以下的网点数据,因此,需要研究该算法的边界条件。
Robert G K在《Cubic convolution interpolation for digital image processing》一文中给出了一种较好的边界条件。设网点在x方向由0到M,在y方向由0到N,则边界条件为
下面是用C++写的二维三次卷积插值的源代码。
首先是point.h文件
#ifndef POINT_H
#define POINT_H
typedef struct
{
float x;
float y;
float z;
}Point;
#endif
然后是interpolation.h文件
#ifndef INTERPOLATION_h
#define INTERPOLATION_H
#include "Point.h"
#include <string>
#include <Math.h>
#include <iostream>
#include <fstream>
using namespace std;
float getInterpolationHeight(float**,int,int,float,float);
float funA(float);
float funB(float);
float funC(float);
float funD(float);
float getDEMInterpolationHeight(Point**p,float**expandedH,int rowNum,int colNum,float delta,float x,float y);
float**getExpandedH(Point**p,int rowNum,int colNum);
Point**interpolation( Point**p,const int rowNum,const int colNum) //p为传入的二维矩阵。返回的也是二维矩阵,不过是在函数中用new新建的。加上const是为了让p对应的值不能改变。interpolateTimes是插值次数。
{
Point**dataPoint;
dataPoint=new Point*[2*rowNum-1]; //新的点阵是(2*rowNum-1)*(2*colNum-1)
for(int i=0;i<2*rowNum-1;++i)
{
dataPoint[i]=new Point[2*colNum-1];
}
/////////////////////////////对于行号和列号都为偶数的点,它就等于原数组元素。
for(int i=0;i<rowNum*2-1;i=i+2)
{
for(int j=0;j<colNum*2-1;j=j+2)
{
//dataPoint[i][j].x=i/2.0f;
//dataPoint[i][j].y=j/2.0f; //横纵坐标不能那样求,在下面给出了求解。
dataPoint[i][j].z=p[i/2][j/2].z;
}
}
//////////////////////////////////////////下面要插值求的其实就是行号或者列号为奇数的点。关键就是要得到它周围的16个点(4*4的矩阵),只要得到了这16个点,直接带入公式就行。
//////////////////////
float*Hrowminus=new float[colNum],*Hrowplus=new float[colNum];
float*Hcolminus=new float[rowNum],*Hcolplus=new float[rowNum];
float Hmm,Hpm,Hmp,Hpp; //Hmm代表H-1,-1 Hp,m代表H(rowNum-1)+1,-1 Hm,p代表H-1,(colNum-1)+1 Hpp代表H(rowNum-1)+1,(rowNum-1)+1.这四个值要等到那4个数组的值都计算出来之知道。
for(int j=0;j<colNum;++j)
{
Hrowminus[j]=3*p[0][j].z-3*p[1][j].z+p[2][j].z;
Hrowplus[j]=3*p[rowNum-1][j].z-3*p[rowNum-2][j].z+p[rowNum-3][j].z;
}
///////////////
for(int i=0;i<rowNum;++i)
{
Hcolminus[i]=3*p[i][0].z-3*p[i][1].z+p[i][2].z;
Hcolplus[i]=3*p[i][colNum-1].z-3*p[i][colNum-2].z+p[i][colNum-3].z;
}
/////////下面计算4个角点的值。
Hmm=3*Hcolminus[0]-3*Hcolminus[1]+Hcolminus[2];//H-1,-1
Hpm=3*Hcolminus[rowNum-1]-3*Hcolminus[rowNum-2]+Hcolminus[rowNum-3];
Hmp=3*Hrowminus[colNum-1]-3*Hrowminus[colNum-2]+Hrowminus[colNum-3];
Hpp=3*Hcolplus[rowNum-1]-3*Hcolplus[rowNum-2]+Hcolplus[rowNum-3];
////////////////////////////////如果分类讨论的话,算法就过于复杂了,出错的概率就很大,与其这样,还不如以空间换取时间,新建一个扩大数组,包含了所有高程值(包括扩充的四条边).
float**expandH=getExpandedH(p,rowNum,colNum);
int startRow=0,startCol=0; //实际上要知道它周围的16个点不用把每个点都求出来,只需求出这16个点的起始点,或者说起始点的序号。
float deltaX=0.0f,deltaY=0.0f;
for(int i=0;i<2*rowNum-2;++i) //最后一行(序号为2*rowNum-2)和最后一列(序号为2*colNum-2)单独考虑。
{
for(int j=0;j<2*colNum-2;++j)
{
if(i%2==0)
{
if(j%2!=0) //偶数行只需要计算奇数列。因为偶数列就是原来的数据点。在前面已经赋过值了。
{
startRow=i/2;
startCol=(j-1)/2; //其实由于j为奇数,所以(j-1)/2==j/2,所以写成j/2也可以。
deltaX=j-startCol*2;//注意:deltaX和deltaY的值要么等于0,要么等于1,不会是负值。
deltaY=i-startRow*2;
deltaX=deltaX/2.0f;
deltaY=deltaY/2.0f;
//然后代入插值方程计算得到高程值。
dataPoint[i][j].z=getInterpolationHeight(expandedH,startRow,startCol,deltaX,deltaY);
}
}
else
{
/////////////////////////奇数行的每列都要计算。
startRow=(i-1)/2; //其实由于i为奇数,所以(i-1)/2==i/2,所以这里写成i/2也行。所以其实j%2!=0和j%2==0的代码可以合二为一,等下弄好了再测试一下,如果确实可以的话就很好了。
startCol=j/2;
deltaX=j-startCol*2;
deltaY=i-startRow*2;
deltaX=deltaX/2.0f;
deltaY=deltaY/2.0f;
dataPoint[i][j].z=getInterpolationHeight(expandedH,startRow,startCol,deltaX,deltaY);
}
}
}
////////////////////////////////////下面考虑最后一行。分析发现最后一行的周围16个点要与它上面的一样,而最后一列的16个点要与它左边的一样。
for(int j=1;j<2*colNum-2;j=j+2)
{
startRow=rowNum-2;
startCol=j/2;
deltaX=j-startCol*2;
// deltaY=(2*rowNum-2)-startRow*2; //把startRow代入计算发现它就等于2,所以直接写好了。
deltaY=2.0f;
deltaX=deltaX/2.0f;
deltaY=deltaY/2.0f;
dataPoint[2*rowNum-2][j].z=getInterpolationHeight(expandedH,startRow,startCol,deltaX,deltaY);
}
/////////////////////////////下面考虑最后一列。
for(int i=1;i<2*rowNum-1;i=i+2)
{
startRow=i/2;
startCol=colNum-2;
deltaX=2.0f;
deltaY=1.0f;
deltaX=deltaX/2.0f;//因为要除以网点间距。
deltaY=deltaY/2.0f;
dataPoint[i][2*colNum-2].z=getInterpolationHeight(expandedH,startRow,startCol,deltaX,deltaY);
}
///////////////////////////////////////////////////////////////////////////////////至此,所有插值点的高程都已经计算出来了,但是横纵坐标还没有求出来。
float d =p[1][1].y-p[0][0].y; //这是网格间距,由于栅格是正方形,所以只要求出水平或者竖直方向的其中一个方向的间距就可以。其实这个最好是通过传入网格间距(delta)来获取。
d=d/2.0f;//新的网点间距是原来的一半。
for(int i=0;i<2*rowNum-1;++i)
{
for(int j=0;j<2*colNum-1;++j)
{
dataPoint[i][j].x=p[0][0].x+d*i;
dataPoint[i][j].y=p[0][0].y+d*j;
}
}
////////////////////////////////////在值返回之前,要将在这个函数中新建的而后面又不再需要的数组delete掉,否则它会在程序运行时一直存在。
delete []Hrowminus;
Hrowminus=NULL;
delete []Hrowplus;
Hrowplus=NULL;
delete []Hcolminus;
Hcolminus=NULL;
delete []Hcolplus;
Hcolplus=NULL;
for(int i=0;i<rowNum+2;++i)
{
delete []expandedH[i];
expandedH[i]=NULL;
}
return dataPoint;
}
float getInterpolationHeight(float**expandH,int startRow,int startCol,float deltaX,float deltaY) //expandH是扩大之后的那个矩阵,而startRow和startCol是16个点中起始点的序号。
{
float**surroundingH=new float*[4];
for(int i=0;i<4;++i)
{
surroundingH[i]=new float[4];
for(int j=0;j<4;++j)
{
surroundingH[i][j]=expandH[startRow+i][startCol+j];
}
}
/////////////////////////////////
float aX,aY,bX,bY,cX,cY,dX,dY;
aX=funA(deltaX);
aY=funA(deltaY);
bX=funB(deltaX);
bY=funB(deltaY);
cX=funC(deltaX);
cY=funC(deltaY);
dX=funD(deltaX);
dY=funD(deltaY);
////////////////////////////////////////////////////////////////////////////////////////
float*leftArray=new float[4];
leftArray[0]=aY;
leftArray[1]=bY;
leftArray[2]=cY;
leftArray[3]=dY;
float*rightArray=new float[4];
rightArray[0]=aX;
rightArray[1]=bX;
rightArray[2]=cX;
rightArray[3]=dX;
//////////////////////////////////
float*array02=new float[4];
for(int i=0;i<4;++i)
array02[i]=0.0f;
for(int i=0;i<4;++i)
{
for(int j=0;j<4;++j)
array02[i]+=leftArray[j]*surroundingH[j][i];
}
float height=0.0f;
for(int i=0;i<4;++i)
height+=array02[i]*rightArray[i];
height=height/4.0f; //千万别忘了这个。
//////////////////////////////////注意:为了防止内存泄漏,要将后面不会用到的而不新建了的数组delete掉,否则它在程序运行时会一直存在,增加内存负担。
for(int i=0;i<4;++i)
{
delete []surroundingH[i];
surroundingH[i]=NULL;
}
delete[]leftArray;
leftArray=NULL;
delete[]rightArray;
rightArray=NULL;
////////////////////////////////
return height;
}
//////////////////////////////////////////////////下面这个是得到扩展矩阵的函数。
float**getExpandedH(Point**p,int rowNum,int colNum)
{
float*Hrowminus=new float[colNum],*Hrowplus=new float[colNum];
float*Hcolminus=new float[rowNum],*Hcolplus=new float[rowNum];
float Hmm,Hpm,Hmp,Hpp; //Hmm代表H-1,-1 Hp,m代表H(rowNum-1)+1,-1 Hm,p代表H-1,(colNum-1)+1 Hpp代表H(rowNum-1)+1,(rowNum-1)+1.这四个值要等到那4个数组的值都计算出来之知道。
for(int j=0;j<colNum;++j)
{
Hrowminus[j]=3*p[0][j].z-3*p[1][j].z+p[2][j].z;
Hrowplus[j]=3*p[rowNum-1][j].z-3*p[rowNum-2][j].z+p[rowNum-3][j].z;
}
///////////////
for(int i=0;i<rowNum;++i)
{
Hcolminus[i]=3*p[i][0].z-3*p[i][1].z+p[i][2].z;
Hcolplus[i]=3*p[i][colNum-1].z-3*p[i][colNum-2].z+p[i][colNum-3].z;
}
/////////下面计算4个角点的值。
Hmm=3*Hcolminus[0]-3*Hcolminus[1]+Hcolminus[2];//H-1,-1
Hpm=3*Hcolminus[rowNum-1]-3*Hcolminus[rowNum-2]+Hcolminus[rowNum-3];
Hmp=3*Hrowminus[colNum-1]-3*Hrowminus[colNum-2]+Hrowminus[colNum-3];
Hpp=3*Hcolplus[rowNum-1]-3*Hcolplus[rowNum-2]+Hcolplus[rowNum-3];
////////////////////////////////像上面那样分类讨论的话,算法就过于复杂了,出错的概率就很大,与其这样,还不如以空间换取时间,新建一个扩大数组,包含了所有高程值(包括扩充的四条边).
float**expandedH=new float*[rowNum+2];
for(int i=0;i<rowNum+2;++i)
{
expandedH[i]=new float[colNum+2];
for(int j=0;j<colNum+2;++j)
{
if(i==0)
{
if(j==0)
expandedH[i][j]=Hmm; //其实就是expandH[0][0]=Hmm;
else if(j==colNum+1)
expandedH[i][j]=Hmp;
else //即j>0&&j<colNum+1
expandedH[i][j]=Hrowminus[j-1];
}
else if(i==rowNum+1)
{
if(j==0)
expandedH[i][j]=Hpm;
else if(j==colNum+1) //注意==不要写成=
expandedH[i][j]=Hpp;
else
expandedH[i][j]=Hrowplus[j-1];
}
else //剩下的就是中间各行了。即从i==1到i=rowNum的那些行。
{
if(j==0)
expandedH[i][j]=Hcolminus[i-1];
else if(j==colNum+1)
expandedH[i][j]=Hcolplus[i-1];
else //即从j==1到j==colNum那些列。
expandedH[i][j]=p[i-1][j-1].z;
}
}
}
/////////////////////////////////////////至此,扩充数组就新建并初始化完毕。在返回之前,要先把不需要的数组删除以释放内存。
delete []Hrowminus;
Hrowminus=NULL;
delete []Hrowplus;
Hrowplus=NULL;
delete []Hcolminus;
Hcolminus=NULL;
delete []Hcolplus;
Hcolplus=NULL;
return expandedH;
}
//////////////////////////////////////////////////////////////////////下面这个函数是任给一坐标,通过已有的DEM高程值求得该坐标的高程值。而关键是扩展矩阵和起始序号的获取。
float getDEMInterpolationHeight(Point**p,float**expandedH,int rowNum,int colNum,float delta,float x,float y)//这个函数已经通过验证了。但是这个函数名没有取好,实际上它不只是对于DEM高程有效,只要是有一个包含高程值的点的数组就行。所以要考虑改名或者重载函数getInterpolationHeight(...);
{
/*
//////////////////////
*/
int startRow=100000,startCol=100000; //先赋给它们一个很大的值,是不可能为行号或者序号的值,如果没有改变则说明到了最后一行。应该是
float deltaX,deltaY;
for(int i=0;i<rowNum-1;++i)
{
if((x>=i*delta)&&(x<(i+1)*delta))
{
startRow=i;
break;
}
}
for(int j=0;j<colNum-1;++j)
{
if((y>=j*delta)&&(y<(j+1)*delta))
{
startCol=j;
break;
}
}
///////////////////////////////////////////////最后一行和最后一列要单独考虑。
if(startRow==100000)//说明是最后一行或者是最后一列。
startRow=rowNum-2;
if(startCol==100000)
startCol=colNum-2;
//////////////////////////特别注意下面这个!deltaX要由y和startCol得到,而deltaY要由x和startRow得到。
deltaX=(y-startCol*delta)/delta;
deltaY=(x-startRow*delta)/delta;
if((deltaX==0)&&(deltaY==0))
return p[startRow][startCol].z;
////////////////////////////////////////////////////////
float height;
height=getInterpolationHeight(expandedH,startRow,startCol,deltaX,deltaY);
//////////////////////////////////在返回之前,记得将不需要的数组删除。
return height;
}
/////////////////////////////////////////////////
///////////////////////////////////////
float funA(float delta)
{
float a;
a=0-pow(delta,3)+2*pow(delta,2)-delta;
return a;
}
float funB(float delta)
{
float b;
b=3*pow(delta,3)-5*pow(delta,2)+2;
return b;
}
float funC(float delta)
{
float c;
c=0-3*pow(delta,3)+4*pow(delta,2)+delta;
return c;
}
float funD(float delta)
{
float d;
d=pow(delta,3)-pow(delta,2);
return d;
}
#endif
下面两张图是插值前后的对比结果(利用图形接口OpenGL进行显示)。