一、概述
遙感影像和地理座標進行關聯的方式一般有好幾種,一種是直接給出了仿射變換系數,即6個參數,左上角地理座標,縱橫方向上的分辨率,以及旋轉系數。在這種情況下,求出某一像素點的地理座標非常容易,直接用公式可以求出,具體代碼如下:
void CPL_STDCALL GDALApplyGeoTransform(double *padfGeoTransform,
double dfPixel, double dfLine,
double *pdfGeoX, double *pdfGeoY )
{
*pdfGeoX = padfGeoTransform[0]+ dfPixel * padfGeoTransform[1]
+ dfLine * padfGeoTransform[2];
*pdfGeoY = padfGeoTransform[3]+ dfPixel * padfGeoTransform[4]
+ dfLine * padfGeoTransform[5];
}
可能有的讀者額看到這個覺得非常熟悉,這個是GDAL裏面實現的由像素點轉換到地理座標點的轉換函數,這個函數的聲明在gdal.h中,定義在gdaltransformer.cpp文件中,注意,這個函數計算的是影像像元的左上角的地理座標,也可以計算中心點座標,具體看需求。
另外一種方式是記錄影像控制點座標的數據,一般控制點都記錄着控制點自身的地理座標,還有控制點在影像中的像素座標點,還有控制點必須是在某一特定的地理座標系下,在GDAL中控制點的結構體爲:
/** Ground Control Point */
typedef struct
{
/** Unique identifier, often numeric */
char *pszId;
/** Informational message or "" */
char *pszInfo;
/** Pixel (x) location of GCP on raster */
double dfGCPPixel;
/** Line (y) location of GCP on raster */
double dfGCPLine;
/** X position of GCP in georeferenced space*/
double dfGCPX;
/** Y position of GCP in georeferenced space*/
double dfGCPY;
/** Elevation of GCP, or zero if not known */
double dfGCPZ;
} GDAL_GCP;
那讀者會有疑問了,這個控制點的數據怎麼獲得呢,GDAL裏面有函數接口可以獲得,具體如下;
char* pszWkt1 = (char*)poDatasetchangsha->GetProjectionRef();
if (0 == strlen(pszWkt1))
{
pszWkt1= (char*)poDatasetchangsha->GetGCPProjection();
}
int nGCPCount = poDatasetchangsha->GetGCPCount();
const GDAL_GCP *pGCPList= poDatasetchangsha->GetGCPs();
我首先給出一個例子先有一個比較直觀的感受,我手上有一個spot傳感器的數據,它是帶控制點信息的,控制點剛好是圖像的四個角點。比如,用arcgis打開就已經是糾正顯示了。沒有實施糾正顯示的情況和ARCGIS實現了糾正顯示的情況如下圖:
Arcgis顯示的時候,邊緣的一些像素由於在原始影像上找不到對應的採樣點,所以就用白色填充。
二、幾何校正方法
關於影像幾何校正,一般可以採用下面的幾種方法:
1、幾何多項式校正模型
這個模型比較簡單,迴避了遙感影像的成像過程,適用於覆蓋面積不大喝地形比較平坦的區域。
2、有理函數數學模型
建立像素和地面位置對應關係的簡單數學模型。理論上可以達到跟嚴格衛星傳感器模型相當的定位精度。其優點在於多項式包含高程信息,可以提高校正精度。這個模型自帶了高程信息,一般需要影像提供RPC文件,或者用戶自己選擇地面控制點。
3、局部區域校正模型
這個模型的基本思想是利用控制點建立不規則三角網,然後分區域利用幾何多項式校正,但是這種模型需要很多的控制點,對於地形起伏很大的區域需要的控制點更多,往往實施難度比較大。
4、衛星傳感器模型
這種模型是依據衛星遙感成像的幾何關係,利用攝影測量學中成像瞬間的地面點、透視中心以及相應的像素點三點共線建立起來的一種模型,但是此模型需要DEM數據,優點是精度比較高。
由於本文主要關注幾何多項式模型,所以只利用GDAL中幾何多項式的模型來校正。
可以根據地面控制點求算出仿射變換系數,可以調用GDAL的函數,其函數是:
int CPL_DLL CPL_STDCALL
GDALGCPsToGeoTransform(int nGCPCount,const GDAL_GCP*pasGCPs,
double *padfGeoTransform, int bApproxOK );
這個函數的定義在gdal_misc.cpp文件中,改天可以去了解下。求出仿射變換系數後可以求出圖像的大致範圍和分辨率,這樣就可以目標圖像所佔的行數和列數。
下面的代碼是求出影像的envelope,大概的範圍
double dbX[4];
double dbY[4];
dbX[0] = dblGeoTransform[0]+ 0*dblGeoTransform[1] + 0*dblGeoTransform[2];
dbY[0] = dblGeoTransform[3]+ 0*dblGeoTransform[4] + 0*dblGeoTransform[5];
//右上角座標
dbX[1] = dblGeoTransform[0]+ (lWidth/*-0.5*/)*dblGeoTransform[1] + 0*dblGeoTransform[2];
dbY[1] = dblGeoTransform[3]+ (lWidth/*-0.5*/)*dblGeoTransform[4] + 0*dblGeoTransform[5];
//右下角點座標
dbX[2] = dblGeoTransform[0]+ (lWidth/*-0.5*/)*dblGeoTransform[1] + (lHeight/*-0.5*/)*dblGeoTransform[2];
dbY[2] = dblGeoTransform[3]+ (lWidth/*-0.5*/)*dblGeoTransform[4] + (lHeight/*-0.5*/)*dblGeoTransform[5];
//左下角座標
dbX[3] = dblGeoTransform[0]+ 0*dblGeoTransform[1] + (lHeight/*-0.5*/)*dblGeoTransform[2];
dbY[3] = dblGeoTransform[3]+ 0*dblGeoTransform[4] + (lHeight/*-0.5*/)*dblGeoTransform[5];
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]);
但是上面的方法計算出來的範圍有可能不是很準確,所以這裏要尋找一種比較精確的方法,那就是求出每個邊緣像素的邊緣對應的地理座標,然後逐個比較大小求出比較精確的範圍。這種方法可以採用GDAL算法模塊裏面GDALSuggestedWarpOutput2函數。
CPLErr CPL_STDCALL
GDALSuggestedWarpOutput2(GDALDatasetH hSrcDS,
GDALTransformerFunc pfnTransformer,
void *pTransformArg,
double *padfGeoTransformOut,
int *pnPixels, int *pnLines,
double *padfExtent,int nOptions);
這個函數裏面的參數的意義解釋可以參考GDAL官方文檔,也可以直接看GDAL源代碼,下面我就給出各個參數的意思。
第一個參數是數據源,第二個參數是轉換的回調函數,比如是GCP控制點轉換的函數GDALGCPTransform,或者是RPC校正的函數等等。第三個參數是代表轉換的關係,這裏的轉換關係是控制點幾何校正的轉換關係,此轉換關係可以由void CPL_DLL *
GDALCreateGCPTransformer(int nGCPCount,const GDAL_GCP*pasGCPList,
int nReqOrder, int bReversed );函數生成。第四個參數是圖像糾正後的仿射變換的6個參數,pnPixels是糾正後圖像的列數,pnLines是糾正後圖像的行數,padfExtent就是圖像糾正後的地理範圍,nOptions是相關選項的設置,默認情況下可以設爲空。
三、重採樣及其校正實現
到此,我們已經完成了原始圖像的像素座標到校正後的地理空間座標建立了關聯關係,並且輸出影像的大小、地理範圍、分辨率、像素位深度都已經知道,這樣我們就可以創建一個空的圖像了,但是校正後的圖像的各個像素的值還沒確定,接下來就是如何確定校正後圖像中各個像素的值,也就是圖像處理中的重採樣技術,一般大家聽的比較多的重採樣技術可能是最鄰近方法、雙線性內插重採樣以及三次卷積內插重採樣等方法。還有一些其他的重採樣方法,比如平均值法,中值法等。
重採樣技術我們可以直接調用GDAL的函數去實現,但是我這裏決定自己寫重採樣的過程,以便今後做一些改進,這裏我先用最鄰近採樣方法實現重採樣。
整個過程的代碼如下:
/**
* @brief 幾何多項式校正
* @param pszSrcFile 輸入文件路徑
* @param pszDstFile 輸出文件路徑
* @param nGCPCount GCP點對個數
* @param pasGCPList GCP點對列表
* @param pszDstWKT 輸出文件投影
* @param iOrder 多項式次數
* @param dResX 輸出圖像X方向分辨率
* @param dResY 輸出圖像Y方向分辨率
* @param eResampleMethod 重採樣方式
* @param pszFormat 輸出文件格式
* @return 返回值,目前只返回true或者false
*/
int ImageWarpByGCP(const char * pszSrcFile,
const char * pszDstFile,
int nGCPCount,
const GDAL_GCP *pasGCPList,
const char * pszDstWKT,
int iOrder,
double dResX,
double dResY,
GDALResampleAlg eResampleMethod,
const char * pszFormat)
{
GDALAllRegister();
CPLSetConfigOption("GDAL_FILENAME_IS_UTF8", "NO");
// 打開原始圖像
GDALDatasetH hSrcDS = GDALOpen(pszSrcFile, GA_ReadOnly);
if (NULL == hSrcDS)
{
return 0;
}
GDALDataType eDataType = GDALGetRasterDataType(GDALGetRasterBand(hSrcDS, 1));
int nBandCount = GDALGetRasterCount(hSrcDS);
// 創建幾何多項式座標轉換關係
void *hTransform = GDALCreateGCPTransformer(nGCPCount, pasGCPList, iOrder, FALSE );
if( NULL == hTransform )
{
GDALClose( hSrcDS );
return 0;
}
// 計算輸出圖像四至範圍、大小、仿射變換六參數等信息
double adfGeoTransform[6] = {0};
double adfExtent[4] = {0};
int nPixels = 0, nLines = 0;
if( GDALSuggestedWarpOutput2( hSrcDS, GDALGCPTransform, hTransform,
adfGeoTransform, &nPixels, &nLines, adfExtent, 0 ) != CE_None )
{
GDALClose( hSrcDS );
return 0;
}
// 下面開始根據用戶指定的分辨率來反算輸出圖像的大小和六參數等信息
double dResXSize = dResX;
double dResYSize = dResY;
//如果爲0,則默認爲原始影像的分辨率
if(dResXSize == 0.0 && dResYSize == 0.0 )
{
double dbGeoTran[6] = {0};
GDALGCPsToGeoTransform(nGCPCount,pasGCPList,dbGeoTran,0);
dResXSize = fabs(dbGeoTran[1]);
dResYSize = fabs(dbGeoTran[5]);
}
// 如果用戶指定了輸出圖像的分辨率
else if ( dResXSize != 0.0 || dResYSize != 0.0 )
{
if ( dResXSize == 0.0 ) dResXSize = adfGeoTransform[1];
if ( dResYSize == 0.0 ) dResYSize = adfGeoTransform[5];
}
if ( dResXSize < 0.0 ) dResXSize = -dResXSize;
if ( dResYSize > 0.0 ) dResYSize = -dResYSize;
// 計算輸出圖像的範圍
double minX = adfGeoTransform[0];
double maxX = adfGeoTransform[0] + adfGeoTransform[1] * nPixels;
double maxY = adfGeoTransform[3];
double minY = adfGeoTransform[3] + adfGeoTransform[5] * nLines;
nPixels = ceil(( maxX - minX ) / dResXSize );
nLines = ceil(( minY - maxY ) / dResYSize ) ;
adfGeoTransform[0] = minX;
adfGeoTransform[3] = maxY;
adfGeoTransform[1] = dResXSize;
adfGeoTransform[5] = dResYSize;
// 創建輸出圖像
GDALDriverH hDriver = GDALGetDriverByName( pszFormat );
if (NULL == hDriver)
{
return 0;
}
GDALDatasetH hDstDS = GDALCreate( hDriver, pszDstFile, nPixels, nLines, nBandCount, eDataType, NULL );
if (NULL == hDstDS)
{
return 0;
}
GDALSetProjection( hDstDS, pszDstWKT);
GDALSetGeoTransform( hDstDS, adfGeoTransform );
//獲得原始圖像的行數和列數
int nXsize = GDALGetRasterXSize(hSrcDS);
int nYsize = GDALGetRasterYSize(hSrcDS);
//然後是圖像重採樣
int nFlag = 0;
float dfValue = 0;
CPLErr err = CE_Failure;
//最鄰近採樣
for (int nBandIndex = 0; nBandIndex < nBandCount; nBandIndex ++)
{
GDALRasterBandH hSrcBand = GDALGetRasterBand(hSrcDS,nBandIndex+1);
GDALRasterBandH hDstBand = GDALGetRasterBand(hDstDS,nBandIndex+1);
for (int nRow = 0; nRow < nLines; nRow ++)
{
for (int nCol = 0; nCol < nPixels; nCol ++)
{
double dbX = adfGeoTransform[0] + nCol * adfGeoTransform[1]
+ nRow * adfGeoTransform[2];
double dbY = adfGeoTransform[3] + nCol * adfGeoTransform[4]
+ nRow * adfGeoTransform[5];
//由輸出的圖像地理座標系變換到原始的像素座標系
GDALGCPTransform(hTransform,TRUE,1,&dbX,&dbY,NULL,&nFlag);
int nXCol = (int)(dbX + 0.5);
int nYRow = (int)(dbY + 0.5);
//超出範圍的用0填充
if (nXCol < 0 || nXCol >= nXsize || nYRow < 0 || nYRow >= nYsize)
{
dfValue = 0;
}
else
{
err = GDALRasterIO(hSrcBand,GF_Read,nXCol,nYRow,1,1,&dfValue,1,1,eDataType,0,0);
}
err = GDALRasterIO(hDstBand,GF_Write,nCol,nRow,1,1,&dfValue,1,1,eDataType,0,0);
}
}
}
if (hTransform != NULL)
{
GDALDestroyGCPTransformer(hTransform);
hTransform = NULL;
}
GDALClose(hSrcDS );
GDALClose(hDstDS );
return 1;
}
外部調用的實例
GDALDataset* poDataset = (GDALDataset*)GDALOpen("E\\248-279_5\\SCENE01\\IMAGERY.TIF",GA_ReadOnly);
char* pszWkt1 = (char*)poDataset->GetProjectionRef();
if (0 == strlen(pszWkt1))
{
pszWkt1 = (char*)poDataset->GetGCPProjection();
}
int nGCPCount = poDataset->GetGCPCount();
const GDAL_GCP *pGCPList = poDataset->GetGCPs();
GDAL_GCP *pGCPs = new GDAL_GCP[nGCPCount];
memcpy(pGCPs,pGCPList,sizeof(GDAL_GCP)*nGCPCount);
GDALClose(poDataset);
const char* pszInFile = "E:\\248-279_5\\SCENE01\\IMAGERY.TIF";
const char* pszOutFile = "E:\\248-279_5\\SCENE01\\IMAGERY-校正.TIF";
ImageWarpByGCP(pszInFile,pszOutFile,nGCPCount,pGCPs,pszWkt1,0,0,0,GRA_NearestNeighbour,"GTiff");
對於上述算法的函數,有些讀者可能看出來了,算法的瓶頸就是在那個多重循環那,逐點計算是不是效率太低了,的確,這裏可以用分塊技術來優化一下,甚至有可能的話,用GPU加速也是不錯的選擇,我這邊只是向大家分享一下怎麼去校正帶控制點的影像,算是拋磚引玉吧。如果有什麼不妥的地方也可以指出來,大家一起討論。
四、後續研究
1. 如何進行校正顯示,不生成新到的校正影像。
2. 如何分塊處理
3. 如何用硬件加速,GPU加速
參考文獻
張永生,劉軍. 高分辨率遙感衛星立體影像RPC模型定位的算法及其優化測繪工程, 2004,
劉軍,王冬紅,毛國苗. 基於RPC模型的IKONOS衛星影像高精度立體定位. 測繪通報, 2004
劉軍,張永生,範永弘. 基於通用成像模型———有理函數模型的攝影測量定位方法. 測繪通報, 2003
鞏丹超,鄧雪清,張雲彬. 新型遙感衛星傳感器幾何模型—有理函數模型. 海洋測繪, 2003
GDAL官方文檔以及源碼
阮秋琦、數字圖像處理(第三版)