通常,可以重新继承GDALDataset和GDALRasterBand来实现对特定格式数据的支持。 同样,还需要为这种格式创建一个GDALDriver的实例,让后通过GDALDriverManager将 驱动注册给GDAL系统。
该教程将为JDEM格式数据实现一个简单的只读驱动开始,进而使用RawRasterBand帮助类, 实现一个可创建和更改的格式以及比较高级的一些问题。
强烈建议在实现GDAL驱动之前前面所描述的GDAL数据模型的内容。
class JDEMDataset : public GDALDataset { FILE *fp; GByte abyHeader[1012]; public: ~JDEMDataset(); static GDALDataset *Open( GDALOpenInfo * ); };
我们可以通过重载基类GDALDataset中的一些虚函数来为驱动重新实现某些特殊的 功能。然而,Open()相对比较特殊,它不是基类 GDALDataset的虚函数。我们 需要一个独立的函数来实现这个功能,因此我们把他声明为static的。将Open()声明 为 JDEMDataset的static函数比较方便,因为这样可以用JDEMDataset的私有方法去 修改内容。
Open()函数具体实现如下:
GDALDataset *JDEMDataset::Open( GDALOpenInfo * poOpenInfo )
{
// --------------------------------------------------------------------
// Before trying JDEMOpen() we first verify that there is at
// least one "\n#keyword" type signature in the first chunk of
// the file.
// --------------------------------------------------------------------
if( poOpenInfo->fp == NULL || poOpenInfo->nHeaderBytes < 50 )
return NULL;
// check if century values seem reasonable
if( (!EQUALN((char *)poOpenInfo->pabyHeader+11,"19",2)
&& !EQUALN((char *)poOpenInfo->pabyHeader+11,"20",2))
|| (!EQUALN((char *)poOpenInfo->pabyHeader+15,"19",2)
&& !EQUALN((char *)poOpenInfo->pabyHeader+15,"20",2))
|| (!EQUALN((char *)poOpenInfo->pabyHeader+19,"19",2)
&& !EQUALN((char *)poOpenInfo->pabyHeader+19,"20",2)) )
{
return NULL;
}
// --------------------------------------------------------------------
// Create a corresponding GDALDataset.
// --------------------------------------------------------------------
JDEMDataset *poDS;
poDS = new JDEMDataset();
poDS->fp = poOpenInfo->fp;
poOpenInfo->fp = NULL;
// --------------------------------------------------------------------
// Read the header.
// --------------------------------------------------------------------
VSIFSeek( poDS->fp, 0, SEEK_SET );
VSIFRead( poDS->abyHeader, 1, 1012, poDS->fp );
poDS->nRasterXSize = JDEMGetField( (char *) poDS->abyHeader + 23, 3 );
poDS->nRasterYSize = JDEMGetField( (char *) poDS->abyHeader + 26, 3 );
// --------------------------------------------------------------------
// Create band information objects.
// --------------------------------------------------------------------
poDS->nBands = 1;
poDS->SetBand( 1, new JDEMRasterBand( poDS, 1 ));
return( poDS );
}
任何数据集打开文件之前,都要判断驱动是否支持该类型。我们应该知道, 在打开文件的时候每个驱动的open函数将被依次调用,直到有一个成功为止。 如果传递的文件不是驱动所支持的类型,则它们必须返回NULL。如果格式是它们所 支持的类型,但是数据已经被破坏,那么它们应该产生一个错误。
文件的信息在文件打开之后被传递给GDALOpenInfo对象。GDALOpenInfo对象的公有成员如下:
char *pszFilename; GDALAccess eAccess; // GA_ReadOnly or GA_Update GBool bStatOK; VSIStatBuf sStat; FILE *fp; int nHeaderBytes; GByte *pabyHeader;
驱动可以检查这些值来判断是否支持这个格式的文件。如果pszFilename指向 一个文件系统对象,那么bStatOK将被设置,并且sStat结构将包含关于对象的 通用信息。如果对象是一个规则的可读文件,那么fp指针将非空,并且可以使 用fp来读取文件(请使用cpl_vsi.h中标准输入输出文件)。最后,如果文件可以 被打开,那么开头的近1K字节的数据将被读到pabyHeader中,nHeaderBytes保存 实际读取的字节数。
在这个例子中,假设文件已经被打开并且可以测试文件头部的一些信息。在这里, JDEM并没有魔术数字,因此我们只是检测不同的数据域。如果文件不是当前驱动所 支持的格式,那么将返回NULL。
if( poOpenInfo->fp == NULL || poOpenInfo->nHeaderBytes < 50 ) return NULL; // check if century values seem reasonable if( (!EQUALN((char *)poOpenInfo->pabyHeader+11,"19",2) && !EQUALN((char *)poOpenInfo->pabyHeader+11,"20",2)) || (!EQUALN((char *)poOpenInfo->pabyHeader+15,"19",2) && !EQUALN((char *)poOpenInfo->pabyHeader+15,"20",2)) || (!EQUALN((char *)poOpenInfo->pabyHeader+19,"19",2) && !EQUALN((char *)poOpenInfo->pabyHeader+19,"20",2)) ) { return NULL; }
在真正的测试中,测试代码越严格越好。这里的测试相对比较弱。如果一个文件的 在相应的位置具有相同的内容,那么它们很可能被错误地当作JDEM格式处理,最终 导致不可预期的结果。
如果文件是我们支持的类型,我们需要创建一个database的实例,并在database中 记录必要的信息。
JDEMDataset *poDS;
poDS = new JDEMDataset();
poDS->fp = poOpenInfo->fp;
poOpenInfo->fp = NULL;
通常在这个时刻我们将打开文件(这里已经打开了),并且保存文件的指针。 然而,如果只是只读的话,仅仅从GDALOpenInfo中获取文件的指针也是可以的。 在这里我们将GDALOpenInfo的文件指针设置为NULL,以防止文件可能被关闭两次 (因为JDEMDataset的析构函数中已经关闭了文件)。同样,我们假设文件的当前读写 状态是不确定的,因此我们需要用VSIFSeek()重新定位文件的当前地址。下面的两行 代码完成了重新定位和读文件头的工作。
VSIFSeek( poDS->fp, 0, SEEK_SET );
VSIFRead( poDS->abyHeader, 1, 1012, poDS->fp );
接着,从abyHeader中获取x和y的大小。变量nRasterXSize和nRasterYSize是 从基类GDALDataset中继承的,并且必须在Open()中被设置。
poDS->nRasterXSize = JDEMGetField( (char *) poDS->abyHeader + 23, 3 ); poDS->nRasterYSize = JDEMGetField( (char *) poDS->abyHeader + 26, 3 );
最后,通过SetBand()将所有的波段与当前的GDALDataset对象绑定。在下一节, 我们将讨论JDEMRasterBand类的具体细节。
poDS->SetBand( 1, new JDEMRasterBand( poDS, 1 )); return( poDS );
class JDEMRasterBand : public GDALRasterBand { public: JDEMRasterBand( JDEMDataset *, int ); virtual CPLErr IReadBlock( int, int, void * ); };
构造函数可以任意定义,但是只能从Open()函数中调用。其他的虚函数必须 和gdal_priv.h中定义的一致,例如IReadBlock()。构造函数实现代码如下:
JDEMRasterBand::JDEMRasterBand( JDEMDataset *poDS, int nBand )
{
this->poDS = poDS;
this->nBand = nBand;
eDataType = GDT_Float32;
nBlockXSize = poDS->GetRasterXSize();
nBlockYSize = 1;
}
下面成员变量从GDALRasterBand继承,并且通常在构造函数中设置。
所有的GDALDataType类型在gdal.h文件中定义,包括GDT_Byte、GDT_UInt16、 GDT_Int16和 GDT_Float32。块的尺寸(block size)记录数据的实际或有效的大小。 对于约束数据集这将是一个约束尺寸,而对于其他大多数数据而言这将是一个扫描线。
接下来,我们将实现真正读取影象数据的代码——IReadBlock()。
CPLErr JDEMRasterBand::IReadBlock( int nBlockXOff, int nBlockYOff, void * pImage ) { JDEMDataset *poGDS = (JDEMDataset *) poDS; char *pszRecord; int nRecordSize = nBlockXSize*5 + 9 + 2; int i; VSIFSeek( poGDS->fp, 1011 + nRecordSize*nBlockYOff, SEEK_SET ); pszRecord = (char *) CPLMalloc(nRecordSize); VSIFRead( pszRecord, 1, nRecordSize, poGDS->fp ); if( !EQUALN((char *) poGDS->abyHeader,pszRecord,6) ) { CPLFree( pszRecord ); CPLError( CE_Failure, CPLE_AppDefined, "JDEM Scanline corrupt. Perhaps file was not transferred\n" "in binary mode?" ); return CE_Failure; } if( JDEMGetField( pszRecord + 6, 3 ) != nBlockYOff + 1 ) { CPLFree( pszRecord ); CPLError( CE_Failure, CPLE_AppDefined, "JDEM scanline out of order, JDEM driver does not\n" "currently support partial datasets." ); return CE_Failure; } for( i = 0; i < nBlockXSize; i++ ) ((float *) pImage)[i] = JDEMGetField( pszRecord + 9 + 5 * i, 5) * 0.1; return CE_None; }
需要注意的地方:
GDALRasterBand::poDS成员传递给子类是常用的做法。如果你的RasterBand需要 操作dataset的私有成员,确保将它声明为JDEMRasterBand类的友元。
CPL_C_START void GDALRegister_JDEM(void); CPL_C_END ... void GDALRegister_JDEM() { GDALDriver *poDriver; if( GDALGetDriverByName( "JDEM" ) == NULL ) { poDriver = new GDALDriver(); poDriver->SetDescription( "JDEM" ); poDriver->SetMetadataItem( GDAL_DMD_LONGNAME, "Japanese DEM (.mem)" ); poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "frmt_various.html#JDEM" ); poDriver->SetMetadataItem( GDAL_DMD_EXTENSION, "mem" ); poDriver->pfnOpen = JDEMDataset::Open; GetGDALDriverManager()->RegisterDriver( poDriver ); } }
当第一次被调用的时候,注册函数将new一个GDALDriver对象,并且通过GDALDriverManager来注册。在用GDALDriverManager注册驱动之前,需要设置以下的成员变量:
一旦所有的这些操作都完成,我们将重新构件GDAL,并且所有的应用程序都将识别 新的格式。gdalinfo程序可以用来测打开数据和显示信息。gdal_translate可以用 来测试影象的读操作。
CPLErr GetGeoTransform( double * padfTransform ); const char *GetProjectionRef();
重新实现的GetGeoTransform()函数只是复制地理转换矩阵到缓冲。GetGeoTransform() 函数可能经常使用,因此一般最好保持该函数短小。在许多时候,在Open()函数 将收集地理转换,并且用这个函数复制。需要注意的是,转换矩阵对应像素左上角 为原点,而不是中心。
CPLErr JDEMDataset::GetGeoTransform( double * padfTransform ) { double dfLLLat, dfLLLong, dfURLat, dfURLong; dfLLLat = JDEMGetAngle( (char *) abyHeader + 29 ); dfLLLong = JDEMGetAngle( (char *) abyHeader + 36 ); dfURLat = JDEMGetAngle( (char *) abyHeader + 43 ); dfURLong = JDEMGetAngle( (char *) abyHeader + 50 ); padfTransform[0] = dfLLLong; padfTransform[3] = dfURLat; padfTransform[1] = (dfURLong - dfLLLong) / GetRasterXSize(); padfTransform[2] = 0.0; padfTransform[4] = 0.0; padfTransform[5] = -1 * (dfURLat - dfLLLat) / GetRasterYSize(); return CE_None; }
GetProjectionRef() 方法将返回一个字符串,其中包括OGC WKT格式的坐标系统 的定义。在这个例子中,坐标系统适合于这种格式的所有数据。但是在比较复杂的 数据格式中,我们可以需要使用 OGRSpatialReference得到针对与特定情形的更具体 的坐标系统。
const char *JDEMDataset::GetProjectionRef() { return( "GEOGCS[\"Tokyo\",DATUM[\"Tokyo\",SPHEROID[\"Bessel 1841\"," "6377397.155,299.1528128,AUTHORITY[\"EPSG\",7004]],TOWGS84[-148," "507,685,0,0,0,0],AUTHORITY[\"EPSG\",6301]],PRIMEM[\"Greenwich\"," "0,AUTHORITY[\"EPSG\",8901]],UNIT[\"DMSH\",0.0174532925199433," "AUTHORITY[\"EPSG\",9108]],AXIS[\"Lat\",NORTH],AXIS[\"Long\",EAST]," "AUTHORITY[\"EPSG\",4301]]" ); }
到这里已经能够完成了JDEM驱动的部分特征代码,我们重新回顾一下前面的代码。
Formats can also report that they have arbitrary overviews, by overriding the HasArbitraryOverviews() method on the GDALRasterBand, returning TRUE. In this case the raster band object is expected to override the RasterIO() method itself, to implement efficient access to imagery with resampling. This is also involved, and there are a lot of requirements for correct implementation of the RasterIO() method. An example of this can be found in the OGDI and ECW formats.
However, by far the most common approach to implementing overviews is to use the default support in GDAL for external overviews stored in TIFF files with the same name as the dataset, but the extension .ovr appended. In order to enable reading and creation of this style of overviews it is necessary for the GDALDataset to initialize the oOvManager object within itself. This is typically accomplished with a call like the following near the end of the Open() method.
poDS->oOvManager.Initialize( poDS, poOpenInfo->pszFilename );
This will enable default implementations for reading and creating overviews for the format. It is advised that this be enabled for all simple file system based formats unless there is a custom overview mechanism to be tied into.
第一种方法的优点是在创建文件的所有的信息都被输出。对于通过在文件创建时需要例如颜 色图,参照系外在库实现文件格式是特别重要的。关于这种方法其他的优点是CreateCopy方法 读取各种没有相应的设置方法的信息,例如min/max、scaling、description和GCPs。
第二种方法的优点在于可以创建一个空的新文件,并且在需要的时候把结果写入其中。 对于一个影象来说,动态创建不需要提前获取所有信息。
对于比较重要的格式而言,两种方法最好都支持。
GDALDriver::CreateCopy()方法可以直接调用,因此只需要知道调用的参数既可。不过以下的细节需要注意:
JPEG格式对应的CreateCopy的代码如下:
static GDALDataset *
JPEGCreateCopy( const char * pszFilename, GDALDataset *poSrcDS,
int bStrict, char ** papszOptions,
GDALProgressFunc pfnProgress, void * pProgressData )
{
int nBands = poSrcDS->GetRasterCount();
int nXSize = poSrcDS->GetRasterXSize();
int nYSize = poSrcDS->GetRasterYSize();
int nQuality = 75;
int bProgressive = FALSE;
// --------------------------------------------------------------------
// Some some rudimentary checks
// --------------------------------------------------------------------
if( nBands != 1 && nBands != 3 )
{
CPLError( CE_Failure, CPLE_NotSupported,
"JPEG driver doesn't support %d bands. Must be 1 (grey) "
"or 3 (RGB) bands.\n", nBands );
return NULL;
}
if( poSrcDS->GetRasterBand(1)->GetRasterDataType() != GDT_Byte && bStrict )
{
CPLError( CE_Failure, CPLE_NotSupported,
"JPEG driver doesn't support data type %s. "
"Only eight bit byte bands supported.\n",
GDALGetDataTypeName(
poSrcDS->GetRasterBand(1)->GetRasterDataType()) );
return NULL;
}
// --------------------------------------------------------------------
// What options has the user selected?
// --------------------------------------------------------------------
if( CSLFetchNameValue(papszOptions,"QUALITY") != NULL )
{
nQuality = atoi(CSLFetchNameValue(papszOptions,"QUALITY"));
if( nQuality < 10 || nQuality > 100 )
{
CPLError( CE_Failure, CPLE_IllegalArg,
"QUALITY=%s is not a legal value in the range 10-100.",
CSLFetchNameValue(papszOptions,"QUALITY") );
return NULL;
}
}
if( CSLFetchNameValue(papszOptions,"PROGRESSIVE") != NULL )
{
bProgressive = TRUE;
}
// --------------------------------------------------------------------
// Create the dataset.
// --------------------------------------------------------------------
FILE *fpImage;
fpImage = VSIFOpen( pszFilename, "wb" );
if( fpImage == NULL )
{
CPLError( CE_Failure, CPLE_OpenFailed,
"Unable to create jpeg file %s.\n",
pszFilename );
return NULL;
}
// --------------------------------------------------------------------
// Initialize JPG access to the file.
// --------------------------------------------------------------------
struct jpeg_compress_struct sCInfo;
struct jpeg_error_mgr sJErr;
sCInfo.err = jpeg_std_error( &sJErr );
jpeg_create_compress( &sCInfo );
jpeg_stdio_dest( &sCInfo, fpImage );
sCInfo.image_width = nXSize;
sCInfo.image_height = nYSize;
sCInfo.input_components = nBands;
if( nBands == 1 )
{
sCInfo.in_color_space = JCS_GRAYSCALE;
}
else
{
sCInfo.in_color_space = JCS_RGB;
}
jpeg_set_defaults( &sCInfo );
jpeg_set_quality( &sCInfo, nQuality, TRUE );
if( bProgressive )
jpeg_simple_progression( &sCInfo );
jpeg_start_compress( &sCInfo, TRUE );
// --------------------------------------------------------------------
// Loop over image, copying image data.
// --------------------------------------------------------------------
GByte *pabyScanline;
CPLErr eErr;
pabyScanline = (GByte *) CPLMalloc( nBands * nXSize );
for( int iLine = 0; iLine < nYSize; iLine++ )
{
JSAMPLE *ppSamples;
for( int iBand = 0; iBand < nBands; iBand++ )
{
GDALRasterBand * poBand = poSrcDS->GetRasterBand( iBand+1 );
eErr = poBand->RasterIO( GF_Read, 0, iLine, nXSize, 1,
pabyScanline + iBand, nXSize, 1, GDT_Byte,
nBands, nBands * nXSize );
}
ppSamples = pabyScanline;
jpeg_write_scanlines( &sCInfo, &ppSamples, 1 );
}
CPLFree( pabyScanline );
jpeg_finish_compress( &sCInfo );
jpeg_destroy_compress( &sCInfo );
VSIFClose( fpImage );
return (GDALDataset *) GDALOpen( pszFilename, GA_ReadOnly );
}
接下来的自立简单实现了PCI.aux标记的原始矢量创建。它采用的自己的方法创建了一个 空白,并且在最后调用GDALOpen。这避免在Open函数中出现两套不同的启动过程。
GDALDataset *PAuxDataset::Create( const char * pszFilename,
int nXSize, int nYSize, int nBands,
GDALDataType eType,
char ** // papszParmList )
{
char *pszAuxFilename;
// --------------------------------------------------------------------
// Verify input options.
// --------------------------------------------------------------------
if( eType != GDT_Byte && eType != GDT_Float32 && eType != GDT_UInt16
&& eType != GDT_Int16 )
{
CPLError( CE_Failure, CPLE_AppDefined,
"Attempt to create PCI .Aux labelled dataset with an illegal\n"
"data type (%s).\n",
GDALGetDataTypeName(eType) );
return NULL;
}
// --------------------------------------------------------------------
// Try to create the file.
// --------------------------------------------------------------------
FILE *fp;
fp = VSIFOpen( pszFilename, "w" );
if( fp == NULL )
{
CPLError( CE_Failure, CPLE_OpenFailed,
"Attempt to create file `%s' failed.\n",
pszFilename );
return NULL;
}
// --------------------------------------------------------------------
// Just write out a couple of bytes to establish the binary
// file, and then close it.
// --------------------------------------------------------------------
VSIFWrite( (void *) "\0\0", 2, 1, fp );
VSIFClose( fp );
// --------------------------------------------------------------------
// Create the aux filename.
// --------------------------------------------------------------------
pszAuxFilename = (char *) CPLMalloc(strlen(pszFilename)+5);
strcpy( pszAuxFilename, pszFilename );;
for( int i = strlen(pszAuxFilename)-1; i > 0; i-- )
{
if( pszAuxFilename[i] == '.' )
{
pszAuxFilename[i] = '\0';
break;
}
}
strcat( pszAuxFilename, ".aux" );
// --------------------------------------------------------------------
// Open the file.
// --------------------------------------------------------------------
fp = VSIFOpen( pszAuxFilename, "wt" );
if( fp == NULL )
{
CPLError( CE_Failure, CPLE_OpenFailed,
"Attempt to create file `%s' failed.\n",
pszAuxFilename );
return NULL;
}
// --------------------------------------------------------------------
// We need to write out the original filename but without any
// path components in the AuxilaryTarget line. Do so now.
// --------------------------------------------------------------------
int iStart;
iStart = strlen(pszFilename)-1;
while( iStart > 0 && pszFilename[iStart-1] != '/'
&& pszFilename[iStart-1] != '\\' )
iStart--;
VSIFPrintf( fp, "AuxilaryTarget: %s\n", pszFilename + iStart );
// --------------------------------------------------------------------
// Write out the raw definition for the dataset as a whole.
// --------------------------------------------------------------------
VSIFPrintf( fp, "RawDefinition: %d %d %d\n",
nXSize, nYSize, nBands );
// --------------------------------------------------------------------
// Write out a definition for each band. We always write band
// sequential files for now as these are pretty efficiently
// handled by GDAL.
// --------------------------------------------------------------------
int nImgOffset = 0;
for( int iBand = 0; iBand < nBands; iBand++ )
{
const char * pszTypeName;
int nPixelOffset;
int nLineOffset;
nPixelOffset = GDALGetDataTypeSize(eType)/8;
nLineOffset = nXSize * nPixelOffset;
if( eType == GDT_Float32 )
pszTypeName = "32R";
else if( eType == GDT_Int16 )
pszTypeName = "16S";
else if( eType == GDT_UInt16 )
pszTypeName = "16U";
else
pszTypeName = "8U";
VSIFPrintf( fp, "ChanDefinition-%d: %s %d %d %d %s\n",
iBand+1, pszTypeName,
nImgOffset, nPixelOffset, nLineOffset,
#ifdef CPL_LSB
"Swapped"
#else
"Unswapped"
#endif
);
nImgOffset += nYSize * nLineOffset;
}
// --------------------------------------------------------------------
// Cleanup
// --------------------------------------------------------------------
VSIFClose( fp );
return (GDALDataset *) GDALOpen( pszFilename, GA_Update );
}
文件格式支持动态创建或支持更新都需要实现GDALRasterBand中的IWriteBlock()方法。 它类似于IReadBlock()。并且,因为许多理由,在GDALRasterBand的析构中实现 FlushCache()方法是比较危险的。因此,必须确保在析构方法调用之前,带的任何 写缓冲区块都被清除。
In these cases the format specific band class may not be required, or if required it can be derived from RawRasterBand. The dataset class should be derived from RawDataset.
The Open() method for the dataset then instantiates raster bands passing all the layout information to the constructor. For instance, the PNM driver uses the following calls to create it's raster bands.
if( poOpenInfo->pabyHeader[1] == '5' ) { poDS->SetBand( 1, new RawRasterBand( poDS, 1, poDS->fpImage, iIn, 1, nWidth, GDT_Byte, TRUE )); } else { poDS->SetBand( 1, new RawRasterBand( poDS, 1, poDS->fpImage, iIn, 3, nWidth*3, GDT_Byte, TRUE )); poDS->SetBand( 2, new RawRasterBand( poDS, 2, poDS->fpImage, iIn+1, 3, nWidth*3, GDT_Byte, TRUE )); poDS->SetBand( 3, new RawRasterBand( poDS, 3, poDS->fpImage, iIn+2, 3, nWidth*3, GDT_Byte, TRUE )); }
The RawRasterBand takes the following arguments.
Simple file formats utilizing the Raw services are normally placed all within one file in the gdal/frmts/raw directory. There are numerous examples there of format implementation.
1.5.4