一 RPC正射校正的原理
影像正射校正的方法有很多,主要包含兩大類:一類是嚴格的幾何糾正模型,另一類是近似幾何糾正模型。當遙感影像的成像模型和有關參數已知時,可以根據嚴格的成像模型來校正圖像,這種方法屬於嚴格幾何糾正,最具代表的是共線方程法。當傳感器成像模型未知或者無法獲取相關的輔助參數時,可以用假定的數學模型模擬成像模型,對影像實現校正,這種方法屬於近似幾何糾正,主要有:幾何多項式糾正、有理函數法、局部區域校正等模型。本文將主要對RPC正射校正模型進行展開討論。
RPC模型將像點坐標d(line,sample)表示為以地面點大地坐標D(Latitude,Longitude,Height)為自變量的比值。為了減小計算過程中舍入誤差,增強參數求解的穩定性,需要把地面坐標和影像坐標正則化到(-1,1)之間。對於一個影像,定義如下比值多項式:

式中,
其中,b1和d1通常為1,(P,L,H)為正則化的地面坐標,(X,Y)為正則化的影像坐標,a1,a2,a3,...a20,b1,b2,b3,...b20,c1,c2,c3,...c20,d1,d2,d3,..d20為RPC參數。
正則化規則如下:

這里,LAT_OFF,LAT_SCALE,LONG_OFF,LONG_SCALE,HEIGHT_OFF,HEIGHT_SCALE為物方坐標的正則化參數。SAMP_OFF,SAMP_SCALE,LINE_OFF,LINE_SCALE為像方坐標的正則化參數。它們與RPC模型中4個多項式的80個系數共同保存於衛星廠家提供給用戶的RPC文件中。
在RPC模型中,可用一階多項式來表示由光學投影引起的畸變誤差模型,由二階多項式趨近由地球曲率、投影折射、鏡頭傾斜等因素引起的畸變,可用三階多項式來模擬高階部分的其他未知畸變。
RPC模型的優化方法有兩種:分為直接優化法和間接優化法。直接優化法是利用足夠的地面控制點信息重新計算RPC系數。因此優化后的RPC系數可以直接代入用於影像的定位和計算,而不需要對已經建立起來的模型進行改變。間接優化法是在像方或物方空間進行一些補償換算,以減小一些系統誤差。在間接優化法中,仿射變換法是最常用的方法。
仿射變換公式如下:

式中,(x,y )是在影像上量測得到的GCP的影像坐標。
(sample,line)是利用RPC模型計算得到的GCP的影像坐標。
(e0,e1,e2,f0,f1,f2)是兩組坐標之間的仿射變換參數。
利用少量的地面控制點即可解算出仿射變換的參數。這樣就能利用仿射變換對由RPC模型計算得到的影像坐標解求出更為精確的行列坐標,從而達到優化RPC模型的目的。
本博客暫時先不考慮控制點和DEM,待以后完善。根據RPC系數,RPC正解變換就是從影像的行列號坐標到地面坐標點的變換,在RPC模型中,正解變換需要用到仿射變換六個參數以及RPC系數。RPC反變換是地面到影像行列號坐標的變換,只需要RPC系數就可以。
二、RPC正射校正的CPU上實現
RPC正射校正需要解決幾個基本的問題,一個問題是第一部分提到的RPC正解和反解變換,另外還需要根據RPC系數填充多項式的系數。
1、計算各個多項式的項,即計算公式中(1-1)中的乘積項,也就是L,P,H相乘的那部分,其代碼如下:
void RPCComputeCoeffTerms( double dfLong, double dfLat, double dfHeight, double *padfTerms ) { padfTerms[0] = 1.0; padfTerms[1] = dfLong; padfTerms[2] = dfLat; padfTerms[3] = dfHeight; padfTerms[4] = dfLong * dfLat; padfTerms[5] = dfLong * dfHeight; padfTerms[6] = dfLat * dfHeight; padfTerms[7] = dfLong * dfLong; padfTerms[8] = dfLat * dfLat; padfTerms[9] = dfHeight * dfHeight; padfTerms[10] = dfLong * dfLat * dfHeight; padfTerms[11] = dfLong * dfLong * dfLong; padfTerms[12] = dfLong * dfLat * dfLat; padfTerms[13] = dfLong * dfHeight * dfHeight; padfTerms[14] = dfLong * dfLong * dfLat; padfTerms[15] = dfLat * dfLat * dfLat; padfTerms[16] = dfLat * dfHeight * dfHeight; padfTerms[17] = dfLong * dfLong * dfHeight; padfTerms[18] = dfLat * dfLat * dfHeight; padfTerms[19] = dfHeight * dfHeight * dfHeight; }
2、計算各個RPC項和L,P,H等的乘積和,代碼如下:
double RPCEvaluateSum( double *padfTerms, double *padfCoefs ) { double dfSum = 0.0; int i; for( i = 0; i < 20; i++ ) dfSum += padfTerms[i] * padfCoefs[i]; return dfSum; }
3、正解變換,即影像到地面坐標的變換,這個函數需要用到迭代,具體代碼如下:
void RPCInverseTransform( stRPCInfo *pRPC,double *dbGeoTran, double dfPixel, double dfLine, double dfHeight, double *pdfLong, double *pdfLat ) { double dfResultX, dfResultY; //初始值使用放射變換系數求解 dfResultX = dbGeoTran[0] + dbGeoTran[1] * dfPixel + dbGeoTran[2] * dfLine; dfResultY = dbGeoTran[3] + dbGeoTran[4] * dfPixel + dbGeoTran[5] * dfLine; //開始用正變換的函數進行迭代 double dfPixelDeltaX=0.0, dfPixelDeltaY=0.0; int nIter; for( nIter = 0; nIter < 20; nIter++ ) { double dfBackPixel, dfBackLine; //反算像點坐標,計算閾值 RPCTransform( pRPC, dfResultX, dfResultY, dfHeight, &dfBackPixel, &dfBackLine ); dfPixelDeltaX = dfBackPixel - dfPixel; dfPixelDeltaY = dfBackLine - dfLine; dfResultX = dfResultX - dfPixelDeltaX * dbGeoTran[1] - dfPixelDeltaY * dbGeoTran[2]; dfResultY = dfResultY - dfPixelDeltaX * dbGeoTran[4] - dfPixelDeltaY * dbGeoTran[5]; if( fabs(dfPixelDeltaX) < 0.001 && fabs(dfPixelDeltaY) < 0.001 ) { break; } } //printf("迭代%d次\n",nIter); *pdfLong = dfResultX; *pdfLat = dfResultY; }
4、反解變換,即地面坐標點到影像行列號坐標的變換,可由RPC系數直接求取,具體代碼如下:
void RPCTransform( stRPCInfo *pRPC, double dfLong, double dfLat, double dfHeight, double *pdfPixel, double *pdfLine ) { double dfResultX, dfResultY; double adfTerms[20]; RPCComputeCoeffTerms( (dfLong - pRPC->dfLONG_OFF) / pRPC->dfLONG_SCALE, (dfLat - pRPC->dfLAT_OFF) / pRPC->dfLAT_SCALE, (dfHeight - pRPC->dfHEIGHT_OFF) / pRPC->dfHEIGHT_SCALE, adfTerms ); dfResultX = RPCEvaluateSum( adfTerms, pRPC->adfSAMP_NUM_COEFF ) / RPCEvaluateSum( adfTerms, pRPC->adfSAMP_DEN_COEFF ); dfResultY = RPCEvaluateSum( adfTerms, pRPC->adfLINE_NUM_COEFF ) / RPCEvaluateSum( adfTerms, pRPC->adfLINE_DEN_COEFF ); *pdfPixel = dfResultX * pRPC->dfSAMP_SCALE + pRPC->dfSAMP_OFF; *pdfLine = dfResultY * pRPC->dfLINE_SCALE + pRPC->dfLINE_OFF; }
至此,一些基本的准備做好了,那么下面就開始真正的校正了,RPC校正的大概的流程如下:
(1)根據輸入未校正的影像得到輸出影像的地理范圍、地面分辨率大小以及仿射變換的留個參數。
(2)在輸出的校正影像上,求得每一個像素對應的地理坐標,然后變換到原始影像上的行列號,根據一定的插值算法求得輸出新影像上的對應的像素值。
(3)將對應的像素值寫入到輸出文件中。
基本流程已經確定后,那么下面就開始講如何對大數據進行分塊,是按照輸入影像分塊還是輸出影像分塊,為了方便編程,一般采用輸出影像進行分塊,具體過程如下:首先將輸出影像按照行進行分塊,然后計算每一個輸出分塊所對應的原始影像的行列號范圍,再進行重采樣操作,最后得到輸出影像的像素值。在本博客中,每多少行為一塊分塊只是本人自己的實驗,實際產品和生產中需要動態檢測內存等資源信息。
還有一個需要注意的是,假設影像是多波段的影像,數據怎么排列,為了效率,我創建的輸出影像是按照BIP規格存儲,就一個像素的所有波段的數據存儲在一起,每一個分塊的數據也按照BIP方式讀取,寫入也按照BIP格式寫入。
綜上所述,CPU上的正射校正可以比容易寫出來,影像的IO還是一如既往的用GDAL,具體代碼如下:
bool ImageWarpRPC(const char * pszSrcFile, const char * pszDstFile, const char * pszDstWKT, double dResX, double dResY, 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); double dbGeonTran[6]; GDALGetGeoTransform(hSrcDS,dbGeonTran); const char* pszItem = GDALGetMetadataItem(hSrcDS,"INTERLEAVE","IMAGE_STRUCTURE"); //獲得原始圖像的行數和列數 int nXsize = GDALGetRasterXSize(hSrcDS); int nYsize = GDALGetRasterYSize(hSrcDS); // 提取RPC系數 char** pszMetadata = GDALGetMetadata(hSrcDS,"RPC"); GDALRPCInfo sRPCInfo; if (pszMetadata != NULL) { GDALExtractRPCInfo( pszMetadata, &sRPCInfo ); } stRPCInfo stInfo; memcpy(&stInfo,&sRPCInfo,sizeof(stInfo)); // 計算輸出圖像四至范圍、大小、仿射變換六參數等信息 double adfGeoTransform[6] = {0}; double adfExtent[4] = {0}; //minx,maxx,miny,maxy int nPixels = 0, nLines = 0; double dbX[4]; double dbY[4]; //計算四個角點的地理坐標 RPCInverseTransform(&stInfo,dbGeonTran,0,0,100,dbX,dbY); RPCInverseTransform(&stInfo,dbGeonTran,nXsize,0,100,dbX+1,dbY+1); RPCInverseTransform(&stInfo,dbGeonTran,nXsize,nYsize,100,dbX+2,dbY+2); RPCInverseTransform(&stInfo,dbGeonTran,0,nYsize,100,dbX+3,dbY+3); adfExtent[0] = min(min(dbX[0],dbX[1]) , min(dbX[2],dbX[3])); adfExtent[1] = max(max(dbX[0],dbX[1]) , max(dbX[2],dbX[3])); adfExtent[2] = min(min(dbY[0],dbY[1]) , min(dbY[2],dbY[3])); adfExtent[3] = max(max(dbY[0],dbY[1]) , max(dbY[2],dbY[3])); int nMinCellSize = min(dbGeonTran[1],fabs(dbGeonTran[5])); nPixels = ceil( fabs(adfExtent[1] - adfExtent[0]) / dbGeonTran[1] ); nLines = ceil ( fabs(adfExtent[3] - adfExtent[2]) / fabs(dbGeonTran[5]) ); adfGeoTransform[0] = adfExtent[0]; adfGeoTransform[3] = adfExtent[3]; adfGeoTransform[1] = dbGeonTran[1]; adfGeoTransform[5] = dbGeonTran[5]; // 創建輸出圖像 GDALDriverH hDriver = GDALGetDriverByName( pszFormat ); if (NULL == hDriver) { return 0; } char **papszOptions = NULL; papszOptions = CSLSetNameValue( papszOptions, "INTERLEAVE", "PIXEL" ); GDALDatasetH hDstDS = GDALCreate( hDriver, pszDstFile, nPixels, nLines, nBandCount, eDataType, papszOptions ); if (NULL == hDstDS) { return 0; } GDALSetProjection( hDstDS, pszDstWKT); GDALSetGeoTransform( hDstDS, adfGeoTransform ); //然后是圖像重采樣 double dfPixel = 0; double dfLine = 0; int nFlag = 0; float dfValue = 0; CPLErr err = CE_Failure; int nOffset = 0; //內存偏移量 int nBandList[4] = {1,2,3,4}; //分塊的做法 int nSubHeight = 2000; int nBlockNum = (nLines+nSubHeight-1)/nSubHeight; //塊的數量 /*double *pLineXs = new double[nPixels*nSubHeight]; double *pLineYs = new double[nPixels*nSubHeight];*/ unsigned short *pValues = new unsigned short[nPixels*nBandCount*nSubHeight]; unsigned short *pSrcValues = NULL; for (int nIndex = 0; nIndex < nBlockNum; nIndex ++) { //最后一塊特殊處理 int nBottomIndex = (nIndex + 1) *nSubHeight; if (nBlockNum - 1 == nIndex) { nBottomIndex = nLines; } //分塊的左上角地理坐標 double dbX1 = adfGeoTransform[0] + 0* adfGeoTransform[1] + nIndex*nSubHeight * adfGeoTransform[2]; double dbY1 = adfGeoTransform[3] + 0 * adfGeoTransform[4] + nIndex*nSubHeight * adfGeoTransform[5]; //分塊的右下角地理坐標 double dbX2 = adfGeoTransform[0] + nPixels * adfGeoTransform[1] + nBottomIndex * adfGeoTransform[2]; double dbY2 = adfGeoTransform[3] + nPixels * adfGeoTransform[4] + nBottomIndex * adfGeoTransform[5]; //分塊的右上角地理坐標 double dbX3 = adfGeoTransform[0] + nPixels * adfGeoTransform[1] + nIndex*nSubHeight * adfGeoTransform[2]; double dbY3 = adfGeoTransform[3] + nPixels * adfGeoTransform[4] + nIndex*nSubHeight * adfGeoTransform[5]; //分塊的左下角地理坐標 double dbX4 = adfGeoTransform[0] + 0 * adfGeoTransform[1] + nBottomIndex * adfGeoTransform[2]; double dbY4 = adfGeoTransform[3] + 0 * adfGeoTransform[4] + nBottomIndex * adfGeoTransform[5]; //由輸出的圖像地理坐標系變換到原始的像素坐標系 RPCTransform(&stInfo,dbX1,dbY1,100,&dfPixel,&dfLine); int nCol1 = (int)(dfPixel + 0.5); int nRow1 = (int)(dfLine + 0.5); RPCTransform(&stInfo,dbX2,dbY2,100,&dfPixel,&dfLine); int nCol2 = (int)(dfPixel + 0.5); int nRow2 = (int)(dfLine + 0.5); RPCTransform(&stInfo,dbX3,dbY3,100,&dfPixel,&dfLine); int nCol3 = (int)(dfPixel + 0.5); int nRow3 = (int)(dfLine + 0.5); RPCTransform(&stInfo,dbX4,dbY4,100,&dfPixel,&dfLine); int nCol4 = (int)(dfPixel + 0.5); int nRow4 = (int)(dfLine + 0.5); int nMinRow = min( min( nRow1,nRow2 ), min( nRow3 , nRow4)); if (nMinRow < 0) { nMinRow = 0; } int nMaxRow = max( max( nRow1,nRow2 ), max( nRow3 , nRow4)); if (nMaxRow >= nYsize) { nMaxRow = nYsize-1; } int nHeight = nMaxRow-nMinRow+1; //讀取原始數據 pSrcValues = new unsigned short[nXsize*nHeight*nBandCount]; int nDataSize = GDALGetDataTypeSize(eDataType)/8; ((GDALDataset*)hSrcDS)->RasterIO(GF_Read,0,nMinRow,nXsize,nHeight, pSrcValues,nXsize,nHeight,eDataType,nBandCount,nBandList,nBandCount*nDataSize, nXsize*nBandCount*nDataSize,1*nDataSize); //這一塊適合做並行 if (HasCUDA()) { DWORD t1 = GetTickCount(); /*RPCWarpCL(stInfo,adfGeoTransform,pSrcValues,nXsize,nHeight,nMinRow, nYsize,pValues,nPixels,nSubHeight,nIndex*nSubHeight,nBandCount,nIndex,nBlockNum);*/ RPCWarpCuda(stInfo,adfGeoTransform,pSrcValues,nXsize,nHeight,nMinRow, nYsize,pValues,nPixels,nSubHeight,nIndex*nSubHeight,nBandCount,nIndex,nBlockNum); DWORD t2 = GetTickCount(); double tt = (t2-t1); printf("%f毫秒\n",tt); } else { for (int nRow = nIndex*nSubHeight; nRow < nBottomIndex; nRow ++) { for (int nCol = 0; nCol < nPixels; nCol ++) { nOffset = ( (nRow - nIndex*nSubHeight)*nPixels + nCol ) * nBandCount; double dbX = adfGeoTransform[0] + nCol * adfGeoTransform[1] + nRow * adfGeoTransform[2]; double dbY = adfGeoTransform[3] + nCol * adfGeoTransform[4] + nRow * adfGeoTransform[5]; RPCTransform(&stInfo,dbX,dbY,100,&dfPixel,&dfLine); int nColIndex = (int)(dfPixel + 0.5); int nRowIndex = (int)(dfLine + 0.5); int nOffsetSrc = ( (nRowIndex - nMinRow)*nXsize + nColIndex ) * nBandCount; //超出范圍的用0填充 if (nColIndex < 0 || nColIndex >= nXsize || nRowIndex < 0 || nRowIndex >= nYsize) { for (int nBandIndex = 0; nBandIndex < nBandCount; nBandIndex ++) { pValues[nOffset++] = 0; } } else { for (int nBandIndex = 0; nBandIndex < nBandCount; nBandIndex ++) { pValues[nOffset++] = pSrcValues[nOffsetSrc++]; } } }//for }//for }//else delete []pSrcValues; //寫入數據 if (nIndex < nBlockNum - 1) { ( (GDALDataset*)hDstDS )->RasterIO(GF_Write,0,nIndex*nSubHeight,nPixels,nSubHeight, pValues,nPixels,nSubHeight,(GDALDataType)eDataType,nBandCount,nBandList,nBandCount*nDataSize, nPixels*nBandCount*nDataSize,1*nDataSize); } else if (nIndex == nBlockNum - 1) { int nRealIndex = nIndex*nSubHeight; nSubHeight = nLines - nIndex*nSubHeight; ( (GDALDataset*)hDstDS )->RasterIO(GF_Write,0,nRealIndex,nPixels,nSubHeight, pValues,nPixels,nSubHeight,(GDALDataType)eDataType,nBandCount,nBandList,nBandCount*nDataSize, nPixels*nBandCount*nDataSize,1*nDataSize); } } /*delete []pLineXs; delete []pLineYs;*/ delete []pValues; GDALClose(hSrcDS ); GDALClose(hDstDS ); return 1; }
上面的代碼還是有很多瑕疵的,比如計算輸出影像的范圍,比較保險的做法是根據原始影像的四條邊來計算,還有影像的數據類型還不支持泛型,這些都將在以后不斷完善。細心的讀者可能已經發現有RPCWarpCuda和RPCWrapCL的函數,不錯,這正是CUDA實現和opencl上的實現,其實大家都知道影像幾何校正的時間大部分都花費在影像的重采樣上,所以本博客對於正射校正的GPU實現也主要針對坐標的正反算和重采樣上。下面開始GPU之旅吧!
三、RPC正射校正的GPU實現
自從NVIDIA推出CUDA,GPGPU的發展速度非常迅速,以前要編寫GPU上處理非圖形的編程,需要將問題域轉化為圖形問題,而GPGPU的出現,大大簡化了GPU編程,隨着技術的進一步發展,之后又出現opencl,這是跨平台的一個標准,最先由蘋果公司聯合幾家公司推出的標准,opencl的具體實現交給各個硬件廠商。
下面就着重介紹CUDA上的實現。CUDA上實現的函數聲明如下:
extern "C" void RPCWarpCuda(stRPCInfo& stInfo, double*pfGeoTransform, unsignedshort* poDataIn, intnWidthIn, intnHeightIn, intnMinRowIn, intnSrcHeight, unsignedshort* poDataOut, intnWidthOut, intnHeightOut, intnMinRowOut, intnBandCount, intnBlockIndex, intnBlockNum);
CUDA的運行模型是由多個線程運行同一個計算過程,在一個計算設備上,划分為不同的線程塊,每一個線程塊由不同的線程組成,這樣組成了一個兩層的線程組織體系,對於圖像處理來說,最適合划分為二維網格,每一個線程計算一個輸出像素,在RPC正射校正模型中,波段數、RPC的參數以及歸一化的參數以及輸出影像的仿射變換系數在每一個線程都是一樣的,所以只需拷貝這些參數到GPU上一次,這些參數在CUDA設備上需要聲明為__constant__的,如下:
__constant__ double dbGeoTrans[6]; __constant__ double dfLINE_OFF; __constant__ double dfSAMP_OFF; __constant__ double dfLAT_OFF; __constant__ double dfLONG_OFF; __constant__ double dfHEIGHT_OFF; //縮放比例 __constant__ double dfLINE_SCALE; __constant__ double dfSAMP_SCALE; __constant__ double dfLAT_SCALE; __constant__ double dfLONG_SCALE; __constant__ double dfHEIGHT_SCALE; //系數 __constant__ double adfLINE_NUM_COEFF[20]; __constant__ double adfLINE_DEN_COEFF[20]; __constant__ double adfSAMP_NUM_COEFF[20]; __constant__ double adfSAMP_DEN_COEFF[20]; //波段數 __constant__ int nBandCount;
在RPCWarpCuda中,只在第一個塊的數據傳遞過來的時候將這些數據拷貝的CUDA設備的常量內存上。代碼如下,
if(0 == nBlockIndex) { cudaMalloc(&poDataOut_d,nDataSizeOut); //給參數常量分配空間 cudaMemcpyToSymbol(adfLINE_DEN_COEFF,stInfo.adfLINE_DEN_COEFF,sizeof(double)*20); cudaMemcpyToSymbol(adfLINE_NUM_COEFF,stInfo.adfLINE_NUM_COEFF,sizeof(double)*20); cudaMemcpyToSymbol(adfSAMP_DEN_COEFF,stInfo.adfSAMP_DEN_COEFF,sizeof(double)*20); cudaMemcpyToSymbol(adfSAMP_NUM_COEFF,stInfo.adfSAMP_NUM_COEFF,sizeof(double)*20); cudaMemcpyToSymbol(dfHEIGHT_OFF,&stInfo.dfHEIGHT_OFF,sizeof(double)); cudaMemcpyToSymbol(dfHEIGHT_SCALE,&stInfo.dfHEIGHT_SCALE,sizeof(double)); cudaMemcpyToSymbol(dfLAT_OFF,&stInfo.dfLAT_OFF,sizeof(double)); cudaMemcpyToSymbol(dfLAT_SCALE,&stInfo.dfLAT_SCALE,sizeof(double)); cudaMemcpyToSymbol(dfLINE_OFF,&stInfo.dfLINE_OFF,sizeof(double)); cudaMemcpyToSymbol(dfLINE_SCALE,&stInfo.dfLINE_SCALE,sizeof(double)); cudaMemcpyToSymbol(dfLONG_OFF,&stInfo.dfLONG_OFF,sizeof(double)); cudaMemcpyToSymbol(dfLONG_SCALE,&stInfo.dfLONG_SCALE,sizeof(double)); cudaMemcpyToSymbol(dfSAMP_OFF,&stInfo.dfSAMP_OFF,sizeof(double)); cudaMemcpyToSymbol(dfSAMP_SCALE,&stInfo.dfSAMP_SCALE,sizeof(double)); //傳遞放射變換系數 cudaMemcpyToSymbol(dbGeoTrans,pfGeoTransform,sizeof(double)*6); cudaMemcpyToSymbol(nBandCount,&nBands,sizeof(int)); }
由於輸出影像是均勻分塊,所以只需要第一次給輸出的數據申請設備存在,這樣可以減少設備內存分配的時間,在最后一次的時候將輸出數據設備內存釋放掉。
在RPCWarpCuda中,只在第一個塊的數據傳遞過來的時候將這些數據拷貝的CUDA設備的常量內存上。還有調用內很函數的時候將線程塊設為32*32的,這剛好是1024個線程,現在很多設備都至少支持每個線程塊都支持1024個線程,但是真正產品中需要檢測設備的參數。一切准備工作就緒后,那么下面就只需編寫內核函數了,其實內核函數要做的工作主要是影像坐標和地面點的坐標之間的換算以及影像重采樣,其核函數如下:
__global__ void RPCWarpKernel( unsigned short* poDataIn, int nWidthIn, int nHeightIn, int nMinRowIn, int nSrcHeight, unsigned short* poDataOut, int nWidthOut, int nHeightOut, int nMinRowOut) { int idy = blockIdx.y*blockDim.y + threadIdx.y; //行 int idx = blockIdx.x*blockDim.x + threadIdx.x; //列 if (idx < nWidthOut && idy < nHeightOut) { //求出輸出影像的實際行列號 int nRow = idy + nMinRowOut; int nCol = idx; double dbX = dbGeoTrans[0] + nCol * dbGeoTrans[1] + nRow * dbGeoTrans[2]; double dbY = dbGeoTrans[3] + nCol * dbGeoTrans[4] + nRow * dbGeoTrans[5]; double dfPixel = 0.0; double dfLine = 0.0; RPCTransformCUDA(dbX,dbY,100,&dfPixel,&dfLine); //求出原始影像所在坐標,然后最鄰近采樣 int nColIndex = (int)(dfPixel + 0.5); int nRowIndex = (int)(dfLine + 0.5); int nOffsetSrc = ( (nRowIndex - nMinRowIn)*nWidthIn + nColIndex ) * nBandCount; int nOffset = ( (nRow - nMinRowOut)*nWidthOut + nCol ) * nBandCount; //超出范圍的用0填充 if (nColIndex < 0 || nColIndex >= nWidthIn || nRowIndex < 0 || nRowIndex >= nSrcHeight) { for (int nBandIndex = 0; nBandIndex < nBandCount; nBandIndex ++) { poDataOut[nOffset++] = 0; } } else { for (int nBandIndex = 0; nBandIndex < nBandCount; nBandIndex ++) { poDataOut[nOffset++] = poDataIn[nOffsetSrc++]; } } } }
至此,CUDA上的實現就介紹到這里,關於OPENCL上的實現也和CUDA實現差不多,具體就不闡述了,我會上傳代碼,有興趣的讀者可以下載自己研究並改進。
四、性能測試
我測試環境是硬件環境是GT750m,CUDA是5.5版本,opencl是CUDA上的實現。
測試數據時高分一號寬幅的數據,行數是13400,列數是12000,4個波段,數據類型是unsigned short,原始影像文件的大小是1.2GB。經過大量的測試,測試的結果如下:
|
|
CPU |
CUDA |
加速比 |
OPENCL |
加速比 |
| 包括IO時間 |
230.031s |
85.773s |
2.68 |
85.782s |
2.68 |
| 不包括IO時間 |
130.576s |
6.657s |
19.61 |
6.781s |
19.26 |
從上面的結論可以看出,如果不統計影像IO的時間,CUDA和OPENCL都可以獲得很大的加速比,加速比能夠達到20左右,並且CUDA和OPENCL的加速效果相當接近。影像IO的時間大約是80-100秒左右。如果計算影像IO的時間,加速效果大概是2.68倍左右,所以下一步該研究如何優化影像的IO操作。
校正前后的圖像比較如下:

校正前:

五、結論與展望
通過對RPC正射校正的的GPU實現,可以看出,GPU在遙感影像處理中可以發揮它巨大的優勢,有GPU的用武之地。但是本文沒對GPU優化進行進一步研究。下一步研究加入DEM以及加入控制點提高精度;對於影像IO部分,也需要深入的研究。代碼下載地址是:http://download.csdn.net/detail/zhouxuguang236/7910583
對於相關的環境,可能需要讀者自己配置了。也歡迎大家提出意見,共同進步。
