又有一段时间没有写博客了,今天就把影像(DEM/遥感影像等)投影转换的经验和大家分享下。那么关于GDAL影像投影转换的文章在网上可以说是有很多,如果你还不清楚地图投影那么我建议你去看一看地图投影和坐标系统的基础知识,在这里就不详细讲解了。
影像投影转换就是将一个地理坐标系统转换到另一个坐标系统,如果在同一个椭球基准面下的转换就是严密的转换,如果在同一个椭球体不同基准面的转换是不严密的,不同椭球体之间的转换是不严密的,这就需要用到七参数、三参数等方法。需要两个不同坐标系统下公共点坐标求得系数。例如北京54和WG4-84坐标下的同一点的经纬度或者是经过投影后的平面坐标也是不同的。那么影像投影主要分为哪些步骤呢?说白了,就三个步骤,第一,坐标转换;第二,影像的重采样,最后就是写入到新文件中。
首先来说第一步,坐标转换需要转换四个坐标,也就是四个角点。也许有人说两个点就够了,左下点和右上点。在此,我告诉你,这是错误的。投影转换后这个四个角点组成的矩形那么很有可能就不是矩形了,如果你取两个点做转换那么后面的影像投影转换后的范围就不正确了。
或者再有人问,我怎么知道影像的四个角点的坐标啊?这个很简单,通过仿射变换系数,它就是影像的像素坐标(行列号)和地理坐标之间进行关联的系数。一般是六个参数。在GDAL中可以通过以下这个函数来获得,如果影像有仿射变换系数的话。如果没有仿射变换系数但是有控制点的话也能解算出系数;如果都没有,说明这幅影像是没有地理参考的,那么狠遗憾的告诉你,这个影像就不能做投影转换。还有就是,如果你这幅影像没有投影的话也就不能做投影转换了,因为你根本不知道这幅影像的投影是啥。
CPLErr GDALDataset::GetGeoTransform |
( |
double * |
padfTransform |
) |
其中padfTransform就存储了这六个参数。这是个六个double型的数的数组。
在向北向的图像中,padfTransform[1]代表像素宽度,padfTransform[5]代表像素高度。图像左上角的坐标是(padfTransform[0],padfTransform[3]),adfGeoTransform[1] X方向也就是横向的分辨率大小,padfTransform [2] 旋转系数,如果为0,就是标准的正北向图像,padfTransform [4] 旋转系数,如果为0,就是标准的正北向图像,知道了这两个参数的意义,那么我们就可以得到四个角点的地理坐标了。
在正北向的图像中,四个角点的坐标计算如下:
//计算源图像的MBR
double dbX[4];
double dbY[4];
double dbZ[4] = {0,0,0,0};
dbX[0] = adfDstGeoTransform[0]; //左上角点坐标
dbY[0] = adfDstGeoTransform[3];
//右上角坐标
dbX[1] = adfDstGeoTransform[0] + nXsize*adfDstGeoTransform[1];
dbY[1] = adfDstGeoTransform[3];
//右下角点坐标
dbX[2] = adfDstGeoTransform[0] + nXsize*adfDstGeoTransform[1] + nYsize*adfDstGeoTransform[2];
dbY[2] = adfDstGeoTransform[3] + nXsize*adfDstGeoTransform[4] + nYsize*adfDstGeoTransform[5];
//左下角坐标
dbX[3] = adfDstGeoTransform[0];
dbY[3] = adfDstGeoTransform[3] + nYsize*adfDstGeoTransform[5];
这样我们就找到了需要参与投影转换的坐标点了,下一步就是坐标转换,坐标转换的过程通过GDAL的接口实现,其底层依赖了PROJ4地图投影开源类库。对于不同的椭球体之间变换需要用到三参数布尔莎或者七参数布尔莎模型,具体过程就是首先将经纬度大地坐标转换为地心坐标系下的空间直角坐标,然后用布尔莎模型计算,最后将计算后的结果重新转换到目标地理坐标系统下的经纬度大地坐标。有了需要转换的坐标后,我们将对上述四个点的坐标进行变换,其函数如下:
bool TranformCoordsOGR(char* pszSrcWkt,char* pszDestWkt, int nCount,double* x,double* y,double* z
,double *dfParaSrc,double* dfParaDst,int nParaCount)
{
//创建OGR的空间参考系
OGRSpatialReference oSrcSrs; //源坐标系统
OGRSpatialReference oDestSrs; //目的坐标系统
oSrcSrs.importFromWkt(&pszSrcWkt);
oDestSrs.importFromWkt(&pszDestWkt);
int nSameGeoCS = oSrcSrs.IsSameGeogCS(&oDestSrs);
//相同的椭球基准面,则进行转换
if (nSameGeoCS)
{
OGRCoordinateTransformation *poCT = NULL;
poCT = OGRCreateCoordinateTransformation( &oSrcSrs,&oDestSrs );
if (NULL == poCT)
{
return false;
}
int nFlag = poCT->Transform(nCount,x,y,z);
if (nFlag)
{
OGRCoordinateTransformation::DestroyCT(poCT);
return true;
}
return false;
}
else //不同的椭球体基准面,要设置七参数或者三参数
{
int nFlag = 0;
//如果是地理坐标系,直接转换为空间直角坐标
OGRErr err = 0;
double dbAsrc = 0;
double dbBsrc = 0;
double dbEsrc = 0;
dbAsrc = oSrcSrs.GetSemiMajor(&err);
dbBsrc = oSrcSrs.GetSemiMinor(&err);
dbEsrc = 1-pow((dbBsrc/dbAsrc),2.0);
if (oSrcSrs.IsProjected())
{
OGRSpatialReference* poTmpSRS = oSrcSrs.CloneGeogCS();
OGRCoordinateTransformation *poCTTmp = NULL;
poCTTmp = OGRCreateCoordinateTransformation( &oSrcSrs,poTmpSRS );
nFlag = poCTTmp->Transform(nCount,x,y,z);
if (!nFlag)
{
OGRCoordinateTransformation::DestroyCT(poCTTmp);
return false;
}
OGRCoordinateTransformation::DestroyCT(poCTTmp);
//pj_geodetic_to_geocentric(dbAsrc,dbEsrc,nCount,0,x,y,z);
}
//将经纬度坐标转换为空间直角坐标
CGeoEllipse geoEllipse(dbAsrc,dbBsrc);
double dbX = 0;
double dbY = 0;
double dbZ = 0;
for (int i = 0; i < nCount; i ++)
{
geoEllipse.BLH_XYZ(x[i],y[i],z[i],dbX,dbY,dbZ);
x[i] = dbX;
y[i] = dbY;
z[i] = dbZ;
}
//七参数模型
vector vecX;
vector vecY;
vector vecZ;
for (int i = 0; i < nCount; i ++)
{
double dbX = dfParaSrc[0] + (1+dfParaSrc[6])*(x[i]+dfParaSrc[5]*y[i]-dfParaSrc[4]*z[i]);
vecX.push_back(dbX);
double dbY = dfParaSrc[1] + (1+dfParaSrc[6])*(-dfParaSrc[5]*x[i]+y[i]+dfParaSrc[3]*z[i]);
vecY.push_back(dbY);
double dbZ = dfParaSrc[2] + (1+dfParaSrc[6])*(dfParaSrc[4]*x[i]-dfParaSrc[3]*y[i]+z[i]);
vecZ.push_back(dbZ);
}
memcpy(x,&vecX[0],sizeof(double)*nCount);
memcpy(y,&vecY[0],sizeof(double)*nCount);
memcpy(z,&vecZ[0],sizeof(double)*nCount);
double dbAdst = 0;
double dbBdst = 0;
double dbEdst = 0;
dbAdst = oDestSrs.GetSemiMajor(&err);
dbBdst = oDestSrs.GetSemiMinor(&err);
//再将空间直角坐标转换为地理坐标,即经纬度坐标
CGeoEllipse geoEllipse1(dbAdst,dbBdst);
for (int i = 0; i < nCount; i ++)
{
geoEllipse1.XYZ_BLH(x[i],y[i],z[i],dbX,dbY,dbZ);
x[i] = dbX;
y[i] = dbY;
z[i] = dbZ;
}
if (oDestSrs.IsProjected())
{
const char* pszProjName = oDestSrs.GetAttrValue("PROJECTION");
OGRSpatialReference* poTmpSRS = oDestSrs.CloneGeogCS();
int nZone = oDestSrs.GetUTMZone();
char* pszTmp;
poTmpSRS->exportToWkt(&pszTmp);
OGRCoordinateTransformation *poCTTmp = NULL;
poCTTmp = OGRCreateCoordinateTransformation( poTmpSRS,&oDestSrs );
if (NULL == poCTTmp)
{
//MessageBox(NULL,_T("失败"),_T("提示"),MB_OK);
return false;
}
nFlag = poCTTmp->Transform(nCount,x,y,z);
if (!nFlag)
{
OGRCoordinateTransformation::DestroyCT(poCTTmp);
return false;
}
OGRCoordinateTransformation::DestroyCT(poCTTmp);
return true;
}
return true;
}
return false;
}
上述代码中有空间直角坐标和大地坐标之间的变换,这个是我自己写的,读者也可以使用PROJ中的接口进行变换。
第二步就是影像重采样了,重采样就是通过原始影像的像素值内插得到新到采样点上的像素值。这个可以直接用GDAL中重采样接口来完成。
第三步就不用详细说了,一般投影转换后需要将投影后的影像写入的新文件中,直接用GDAL的读写接口来完成。
上一节已经完成了点的投影转换,那么我们现在就要估算投影后的像素分辨率大小和仿射变换系数了。
如果投影变换前是投影坐标系统,投影转换后也是投影坐标系统,或者说另外一种情况:投影变换前是地理坐标系统,投影变换后也是地理坐标系统,并且坐标的单位都一致的,那么分辨率大小基本上没变换,可以用变换前的分辨率大小。如果变换前是地理坐标系统,投影变换后是投影坐标系统,假设地理坐标系统以度为单位,投影坐标系统以米为单位,那么投影后的像素大小可以这样估计,因为经线上一个纬度的距离大约是111km,那么变换后的分辨率可以由原始分辨率乘以111000;相反的话,如果变换前是投影坐标系统,投影变换后是地理坐标系统,假设地理坐标系统以度为单位,投影坐标系统以米为单位,同理投影后的像素大小可以这样估计,变换后的分辨率可以由原始分辨率除以111000。
仿射变换系数这样也就可以确定了,左上角的坐标就是最小x值,最大y值,在变换后的四个角点坐标中比较获得,分辨率上一段也讲了如何获得。对于正北向的图像这就够了。 然后行列数就用变换后的四个角点组成的区域的MBR的宽度除以横向分辨率得到列数,高度除以纵向分辨率得到行数。
具体的代码片段如下:
//转换为PROJ4结构
projPJ pj_SourceProjection = NULL, pj_DestinationProjection = NULL;
pj_SourceProjection = pj_init_plus(pszSrcProj);
pj_DestinationProjection = pj_init_plus(pszDestProj);
if (pj_is_latlong( pj_SourceProjection ) && !pj_is_latlong(pj_DestinationProjection))
{
dbRes = dbRes * 111000;
}
else if (!pj_is_latlong( pj_SourceProjection ) && pj_is_latlong(pj_DestinationProjection))
{
dbRes = dbRes / 111000;
}
double dbMinx = 0;
double dbMaxx = 0;
double dbMiny = 0;
double dbMaxy = 0;
dbMinx = min(min(min(dbX[0],dbX[1]),dbX[2]),dbX[3]);
dbMaxx = max(max(max(dbX[0],dbX[1]),dbX[2]),dbX[3]);
dbMiny = min(min(min(dbY[0],dbY[1]),dbY[2]),dbY[3]);
dbMaxy = max(max(max(dbY[0],dbY[1]),dbY[2]),dbY[3]);
//估算行列号
adfDstGeoTransform[0] = dbMinx; //左上角点坐标
adfDstGeoTransform[3] = dbMaxy;
//padfTransform[1] 像素宽度, padfTransform[5]像素高度
adfDstGeoTransform[1] = dbRes;
adfDstGeoTransform[5] = -dbRes;
//估算行列数
nPixels = ceil(fabs(dbMaxx-dbMinx)/dbRes);
nLines = ceil(fabs(dbMaxy-dbMiny)/dbRes);
投影后处理主要就是重采样和写入数据到新文件中了。
重采样不多说,主要有最邻近、双线性插值、立方卷积法等。可以直接调用GDAL接口,写入也就不多说了。
这个在网上有很多这样的代码,包括李民录大哥的博客等。不过还是将代码共享出来
// 创建输出文件
hDstDS = GDALCreate(hDriver,info.m_strOutputFile.c_str(), nPixels, nLines,GDALGetRasterCount(hSrcDS),eDT,NULL);
CPLAssert( hDstDS != NULL );
// 写入投影
GDALSetProjection( hDstDS,info.m_strOutWkt.c_str());
GDALSetGeoTransform( hDstDS,adfDstGeoTransform );
// 复制颜色表,如果有的话
GDALColorTableH hCT;
hCT = GDALGetRasterColorTable( GDALGetRasterBand(hSrcDS,1));
if( hCT != NULL )
GDALSetRasterColorTable( GDALGetRasterBand(hDstDS,1),hCT );
ProgressCreateEx( &hPolygonizeProgress,_T(""),TRUE,FALSE );
ProgressBeginEx( hPolygonizeProgress,PROGRESS_MODE_PERCENT, 100);
ProgressSetStepTitleEx( hPolygonizeProgress, _T("图像重投影"));
// 建立变换选项
GDALWarpOptions* psWarpOptions = GDALCreateWarpOptions();
psWarpOptions->hSrcDS =hSrcDS;
psWarpOptions->hDstDS =hDstDS;
int nBandCount = GDALGetRasterCount(hSrcDS);
psWarpOptions->nBandCount = nBandCount;
psWarpOptions->panSrcBands =
(int *) CPLMalloc(sizeof(int) * psWarpOptions->nBandCount );
for (int i = 0; i < nBandCount; i ++)
{
psWarpOptions->panSrcBands[i] = i+1;
}
psWarpOptions->panDstBands =
(int *) CPLMalloc(sizeof(int) * psWarpOptions->nBandCount );
for (int i = 0; i < nBandCount; i ++)
{
psWarpOptions->panDstBands[i] = i+1;
}
psWarpOptions->pfnProgress = ImageReProjectProgress;
psWarpOptions->eResampleAlg = info.m_enResampleAlg;
// 创建重投影变换函数
psWarpOptions->pTransformerArg =
GDALCreateGenImgProjTransformer( hSrcDS,
GDALGetProjectionRef(hSrcDS),
hDstDS,
GDALGetProjectionRef(hDstDS),
FALSE,0.0, 1 );
psWarpOptions->pfnTransformer = GDALGenImgProjTransform;
// 初始化并且执行变换操作
GDALWarpOperation oOperation;
oOperation.Initialize(psWarpOptions );
oOperation.ChunkAndWarpImage(0,0,GDALGetRasterXSize(hDstDS),GDALGetRasterYSize(hDstDS));
ProgressEndEx( hPolygonizeProgress );
ProgressCloseEx( &hPolygonizeProgress );
hPolygonizeProgress=NULL;
GDALDestroyGenImgProjTransformer(psWarpOptions->pTransformerArg );
GDALDestroyWarpOptions( psWarpOptions );
GDALClose( hDstDS );
GDALClose( hSrcDS );
上述过程就是一个比较完整的对影像进行投影转换的过程,如果说的有错误页请指出来,大家一起讨论。