GDAL关于读写图像的简明总结

154 阅读7分钟

读写影像可以说是图像处理最基础的一步。关于使用GDAL读写影像,平时也在网上查了很多资料,就想结合自己的使用心得,做做简单的总结。

在这里写一个例子:裁剪lena图像的某部分内容,将其放入到新创建的.tif中。以此来说明GDAL读写影像的具体实现。

图1 经典的数字图像处理样例lena

1.打开图像

用GDAL打开lena.bmp,实现如下。注意这里打开图像,指的是获取图像的头文件,以此得到图像的一些信息,没有涉及到读取像素操作。


GDALAllRegister();          //GDAL所有操作都需要先注册格式

CPLSetConfigOption("GDAL_FILENAME_IS_UTF8", "NO");  //支持中文路径

const char* imgPath = "E:\\Data\\lena.bmp";

GDALDataset* img = (GDALDataset *)GDALOpen(imgPath, GA_ReadOnly);

if (!img)

{

    cout << "Can't Open Image!" << endl;

    return 1;

}

图像需要关注的信息很多,可以重点关注以下四个值。图像的宽度、高度众所周知了,而波段数就是通道,如RGB图像的波段数为3。深度标识的就是图像的存储单位,比如一般图像就是8位,用无字节字符型unsigned char来表达0~255的像素值;而除以8标识1个字节,方便读取像素buf。


int imgWidth = img->GetRasterXSize();   //图像宽度

int imgHeight = img->GetRasterYSize();  //图像高度

int bandNum = img->GetRasterCount();    //波段数

int depth = GDALGetDataTypeSize(img->GetRasterBand(1)->GetRasterDataType()) / 8;    //图像深度

如果已经读取完毕或者不需要这张图像的相关操作了,最后要关闭打开的文件,否则会内存泄漏。


GDALClose(img);

2.创建图像

用GDAL创建一个新的图像,例如这里创建了一个256X256大小,被读取图像波段,深度8位的tif。


GDALDriver *pDriver = GetGDALDriverManager()->GetDriverByName("GTIFF"); //图像驱动

char** ppszOptions = NULL;

ppszOptions = CSLSetNameValue(ppszOptions, "BIGTIFF", "IF_NEEDED"); //配置图像信息

const char* dstPath = "E:\\Data\\dst.tif";

int bufWidth = 256;

int bufHeight = 256;

GDALDataset* dst = pDriver->Create(dstPath, bufWidth, bufHeight, bandNum, GDT_Byte, ppszOptions);

if (!dst)

{

    printf("Can't Write Image!");

    return false;

}

需要注意的是创建图像可能需要一些特别的设置信息,是需要到GDAL对应格式的文档中去查看的,也可以什么都不设置用默认值。我这里设置的是如果需要的话,就创建支持大小超过4G的bigtiff。

如果已经写入完毕或者不需要这张图像的相关操作了,最后一定要注意关闭关闭打开的文件,之前只会内存泄漏,而这里还会可能创建失败。


GDALClose(dst);

如果创建后什么都不做,关闭后GDAL会自动写入0像素值,打开后就是纯黑色图像。

图2 创建后直接关闭会生成纯黑色图像

3.图像读写

GDAL读写图像是通过RasterIO()这个函数实现的,这个函数提供了非常强大的功能,目前笔者也只总结了这以下方面的内容。

3.1.一般情况下读写

GDAL读取图像是以左上角为起点的,读取起点位置开始的256X256的内容,写入dst.tif中的实现如下:


//申请buf

size_t imgBufNum = (size_t) bufWidth * bufHeight * bandNum * depth;

GByte *imgBuf = new GByte[imgBufNum];  

//读取

img->RasterIO(GF_Read, 0, 0, bufWidth, bufHeight, imgBuf, bufWidth, bufHeight,

    GDT_Byte, bandNum, nullptr, bandNum*depth, bufWidth*bandNum*depth, depth);

//写入

dst->RasterIO(GF_Write, 0, 0, bufWidth, bufHeight, imgBuf, bufWidth, bufHeight,

    GDT_Byte, bandNum, nullptr, bandNum*depth, bufWidth*bandNum*depth, depth);

//释放

delete[] imgBuf;

imgBuf = nullptr;

逐个说明RasterIO()参数的含义:

  • 参数1:读写标记。如果为GF_Read,则是将影像内容写入内存,如果为GF_Write,则是将内存中内容写入文件。

  • 参数2、3:读写开始位置。相对于图像左上角顶点(从零开始)的行列偏移量。

  • 参数4、5:要读写的块在x方向的象素个数和y方向的象素列数。

  • 参数6:指向目标缓冲区的指针,由用户分配。

  • 参数7、8:目标块在x方向上和y方向上的大小。

  • 参数9:目标缓冲区的数据类型,原类型会自动转换为目标类型。

  • 参数10:要处理的波段数。

  • 参数11:记录要操作的波段的索引(波段索引从1开始)的数组,若为空则数组中存放的是前nBandCount个波段的索引。

  • 参数12:X方向上两个相邻象素之间的字节偏移,默认为0,则列间的实际字节偏移由目标数据类型eBufType确定。

  • 参数13:y方向上相邻两行之间的字节偏移, 默认为0,则行间的实际字节偏移为eBufType * nBufXSize。

  • 参数14:相邻两波段之间的字节偏移,默认为0,则意味着波段是顺序结构的,其间字节偏移为nLineSpace * nBufYSize。

有的参数推荐使用上面的标准写法而不是采用默认值0,可以更好地理解图像buf的存放排布。最后得到的dst.tif如下:

图3 一般情况下读写的结果

3.2.16位影像读写

上述RasterIO()的写法可以兼容16为图像的读写,只不过要注意的是buf中是用2个Gbyte来表达1个16像素值的。当然为了更方便图像处理,也可以采用16位整型来读取buf:


//申请buf

size_t imgBufNum = (size_t)bufWidth * bufHeight * bandNum;

GUInt16 *imgBuf = new GUInt16[imgBufNum];

//读取

img->RasterIO(GF_Read, 0, 0, bufWidth, bufHeight, imgBuf, bufWidth, bufHeight,

    GDT_UInt16, bandNum, nullptr, bandNum*depth, bufWidth*bandNum*depth, depth);

//写入

dst->RasterIO(GF_Write, 0, 0, bufWidth, bufHeight, imgBuf, bufWidth, bufHeight,

    GDT_UInt16, bandNum, nullptr, bandNum*depth, bufWidth*bandNum*depth, depth);

//释放

delete[] imgBuf;

imgBuf = nullptr;

可以发现,除了要更改buf的容量和RasterIO()的第九个参数GDT_UInt16,其余什么都不需要更改。注意创建16位图像时参数也需要更改成16位:


GDALDataset* dst = pDriver->Create(dstPath, bufWidth, bufHeight, bandNum, GDT_UInt16, ppszOptions);

3.3.读取特定波段

某些情况下需要读取特定波段,或者需要重组波段顺序。例如VC中显示图像往往需要将buf按照BGR传递给BITMAP,再显示BITMAP。这时只需要修改第11个参数就行了:


//波段索引

int panBandMap[3] = { 3,2,1 };

//申请buf

size_t imgBufNum = (size_t) bufWidth * bufHeight * bandNum * depth;

GByte *imgBuf = new GByte[imgBufNum];  

//读取

img->RasterIO(GF_Read, 0, 0, bufWidth, bufHeight, imgBuf, bufWidth, bufHeight,

    GDT_Byte, bandNum, panBandMap, bandNum*depth, bufWidth*bandNum*depth, depth);

//写入

dst->RasterIO(GF_Write, 0, 0, bufWidth, bufHeight, imgBuf, bufWidth, bufHeight,

    GDT_Byte, bandNum, nullptr, bandNum*depth, bufWidth*bandNum*depth, depth);

//释放

delete[] imgBuf;

imgBuf = nullptr;

这时得到的dst.tif为:

图4 读取特定波段的结果

3.4.左下角起点读写

默认情况RasterIO()是以左上角起点读写的,不过也是可以以左下角为起点读写,只需要重新设置排布buf的位置。这里读写lena图像上同一块位置:


//申请buf

size_t imgBufNum = (size_t) bufWidth * bufHeight * bandNum * depth;

size_t imgBufOffset = (size_t) bufWidth * (bufHeight-1) * bandNum * depth;

GByte *imgBuf = new GByte[imgBufNum];

//读取

img->RasterIO(GF_Read, 0, 0, bufWidth, bufHeight, imgBuf + imgBufOffset, bufWidth, bufHeight,

    GDT_Byte, bandNum, nullptr, bandNum*depth, -bufWidth*bandNum*depth, depth);

//写入

dst->RasterIO(GF_Write, 0, 0, bufWidth, bufHeight, imgBuf + imgBufOffset, bufWidth, bufHeight,

    GDT_Byte, bandNum, nullptr, bandNum*depth, -bufWidth*bandNum*depth, depth);

//释放

delete[] imgBuf;

imgBuf = nullptr;

注意这里Y方向起点位置,也就是第三个参数仍然要用左上角起算,但是buf已经是左下角起点了。

3.5.重采样读写

RasterIO()另外一个用法是可以自动缩放,重采样读写影像,例如这里将512X512大小的lena图像重采样成256X256大小:


//申请buf

size_t imgBufNum = (size_t) bufWidth * bufHeight * bandNum * depth;

size_t imgBufOffset = (size_t) bufWidth * (bufHeight-1) * bandNum * depth;

GByte *imgBuf = new GByte[imgBufNum];  

//读取

img->RasterIO(GF_Read, 0, 0, imgWidth, imgHeight, imgBuf + imgBufOffset, bufWidth, bufHeight,

    GDT_Byte, bandNum, nullptr, bandNum*depth, -bufWidth*bandNum*depth, depth);

//写入

dst->RasterIO(GF_Write, 0, 0, bufWidth, bufHeight, imgBuf + imgBufOffset, bufWidth, bufHeight,

    GDT_Byte, bandNum, nullptr, bandNum*depth, -bufWidth*bandNum*depth, depth);

//释放

delete[] imgBuf;

imgBuf = nullptr;

可以看到重采样读写只需要修改参数4,参数5就行了。查阅网上资料得知,RasterIO()重采样方式默认是最临近的方法。最后得到的dst.tif结果:

图5 重采样读写的结果

GDAL功能非常丰富,本文仅仅做了一点关于图像读写的总结,自认为算的上“简明”了。当然也希望大家批评指正。