数字图像处理的代码在网上已经非常普遍了,但是大部分文章中的代码都是针对某一个功能的,而且实现功能的编程语言种类非常多,这在笔者学习的过程中造成了很多的麻烦。所以笔者打算写几篇的文章来总结笔者在入门时候遇到的“坑”。如果您只想参考图像处理过程中的代码,您可以直接跳到文章的代码部分。笔者在代码中也会有代码的详细注释。当然,如果您刚刚入坑,笔者也会在文章中附加一些段落来帮你理解图像处理。
本专栏的内容参考课程:由武汉大学组织开设,授课教师为贾永红、崔卫红、付仲良等5位老师讲授的《图像处理与分析》。
本专栏的参考书籍:贾永红,《数字图像处理(第三版)》。
文章目录
1. 写在前面
1.1. 先放一张图
熟悉图像处理的人一定认识这张图像。因为他们在实验或者项目任务中经常使用Lena图像。Lena图像已经成为被广泛使用的测试图像。今天,Lena图像的使用被认为是数字图像历史上最重要的事件之一1。如果你刚开始接触图像处理,那么恭喜你以后要经常见到这个图像了。
1.2. 再说一种图像格式
说完了上面的图片,那么接下来再了解一种图像格式:BMP。
BMP是英文Bitmap(位图)的缩写,它是Windows操作系统中的标准图像文件格式2,其能够被多种Windows应用程序所支持。这种格式的特点是包含的图像信息比较丰富,几乎不进行压缩,但由此导致了它与生俱来的缺点——占用空间过大。
1.3. 接着介绍一个头文件
随着Windows的逐渐普及,支持BMP图像格式的应用软件越来越多。这主要是因为Windows把BMP作为图像的标准格式,并且内含了一套支持BMP图像处理的API函数。在C(C++)语言编程中,这套API函数的存在是以Windows.h头文件存在于系统中。在用C(C++)语言3编程实现图像处理的时候我们我们需要在代码最前端引用Windows.h头文件。
Windows.h头文件中包含了windef.h、winnt.h、winbase.h、winuser.h、wingdi.h等头文件,涉及到了Windows内核API,图形界面接口,图形设备函数等重要的功能。
2. BMP文件格式
BMP文件格式4可分为文件信息头、位图信息和位图数据三部分。
2.1. 文件信息头
BMP文件头含有BMP文件的类型、大小和存放位置等信息。Windows.h中对其定义为:
typedef struct tagBITMAPFILEHEADER{
WORD bftype; // 位图文件的类型,必须设置为BM
DWORD bfSize; // 位图文件的大小,以字节为单位
WORD bfReserved1; // 位图文件保留字,必须设置为0
WORD bfReserved2; // 位图文件保留字,必须设置为0
DWORD bfoffBits; // 位图数据相对于位图文件头的偏移量表示
} BITMAPFILEHEADER;
2.2. 位图信息
位图信息用BITMAPINFO结构定义,它是由位图信息头(bitmap-information header)和彩色表(color table)组成,位图信息头用BITMAPINFOHEADER结构定义,彩色表用RGBQUAD结构定义。BITMAPINFO结构具有如下形式:
typedef struct tagBITMAPINFO{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColor[];
} BITMAPINFO;
(1)bmiHeader是一个位图信息头(BITMAPINFOHEADER)类型的数据类型,用于说明位图的尺寸。BITMAPINFOHEADER的定义如下:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // bmiHeader结构的高度
DWORD biWidth; // 位图的宽度,以像素为单位
DWORD biHeight; // 位图的高度,以像素为单位
WORD biPlanes; // 目标设备的位平面数,必须为1
WORD biBitCount; // 每个像素的位数,必须是1(单色)、4(16色)、8(256色)或24(真彩色)
DWORD biCompression; // 位图的压缩类型,必须是0(不压缩)、1(BI-RLE8压缩类型)或2(BI-RLE4压缩类型)
DWORD biSizeImage; // 位图的大小,以字节为单位
DWORD biXPeIsPerMeter; // 位图的目标设备水平分辨率,以每米像素数为单位
DWORD biYPeIsPerMeter; // 位图的目标设备垂直分辨率,以每米像素数为单位
DWORD biClrUsed; // 位图实际使用的颜色表中的颜色变址数
DWORD biClrImpotant; // 位图显示过程中被认为重要颜色的变址数
} BITMAPINFOHEADER;
(2)bmiColor[ ]是一个颜色表,用于说明位图中的颜色。它有若干个表项,每一表项是一个RGBQUAD类型的结构,定义一种颜色。RGBQUAD的定义如下:
typedef tagRGBQUAD{
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
在RGBQUAD定义的颜色中,蓝色的亮度由rgbBlue确定,绿色的亮度由rgbGreen确定,红色的亮度由rgbRed确定。rgbReserved必须为0。
例如:若某表项为00,00,FF,00,那么它定义的颜色为存红色。
bmiColor[ ]表项的个数由biBiCount来定:
当 biBitCount = 1、4、8 时,bmiColor[ ]分别有 2,16,256 个表项。若某点的像素值为 n,则该像素的颜色为bmiColor[n]所定义的颜色。
当 biBitCount = 24 时,bmiColor[ ]的表项为空。位图阵列的每 3 个字节代表一个像素,3 个字节直接定义了像素颜色中蓝、绿、红的相对亮度,因此省去了bmiColor[ ]颜色。
2.3. 位图数据
位图阵列记录了位图的每一个像素值。在生成位图文件时,Windows从位图的左下角开始(即从左到右从下到上)逐行扫描位图,将位图的像素值一一记录下来。这些记录像素值的字节组成了位图阵列。位图阵列有压缩和非压缩两种存储格式。
(1)非压缩格式。在非压缩格式中,位图的每个像素值对应位图阵列的若干位(bits),位图阵列的大小由位图的亮度、高度及位图的颜色数决定。
a.位图扫描行与位图阵列的关系
设记录一个扫描行的像素值需 n 个字节,则位图阵列的 0 至 n-1 个字节记录了位图的第一个扫描行的像素值;位图阵列的 n 至 2n-1 个字节记录了位图的第二个扫描行的像素值;依此类推,位图排列的 (m-1) x n 至 m x n - 1 个字节记录了位图的第 m 个扫描行的像素值。位图阵列的大小为 n * biHeight。
当 (biWidth * biBitCount) mod32 = 0 时,n = (biWidth * biBitCount) / 8
当 (biWidth * biBitCount) mod32 ≠ 0 时,n = (biWidth * biBitCount) / 8 + 4
上式中 +4 而不是 +1 的原因是为了使一个扫描行的像素值占用位图阵列的字节数为 4 的倍数(Windows规定其必须在long边界结束),不足的位用 0 补充。
b.位图像素值与位图阵列的关系(以第 m 扫描行为例)
设记录第 m 个扫描行的像素值的 n 个字节分别为a0,a1,a2,···,则
当 biBitCount = 1 时:a0 的 D7 记录了位图的第 m 个扫描的第 1 个像素值,D6位记录了位图的第 m 个扫描行的第 2 个像素值,···,D0 记录了位图的第 m 个扫描行的第 8 个像素值,a1 的 D7 位记录了位图的第 m 个扫描行的第 9 个像素值,D6 位记录了位图的第 m 个扫描行的第 10 个像素值··· ···
当 biBitCount = 4 时:a0 的 D7 ~ D4 位记录了位图的第 m 个扫描行的第 1 个像素值,D3 ~ D0 位记录了位图的第 m 个扫描行的第 2 个像素值,a1 的 D7 ~ D4 位记录了位图的第 m 个扫描行的第 3 个像素值··· ···
当 biBitCount = 8 时:a0 记录了位图的第 m 个扫描行的第 1 个像素值,a1 记录了位图的第 m 个扫描行的第 2 个像素值··· ···
当 biBitCount = 24 时:a0、a1、a2 记录了位图的第 m 个扫描行的第 1 个像素值,a3、a4、a5 记录了位图的第 m 个扫描行的第 2 个像素值··· ···
位图其他扫描行的像素值与位图阵列的对应关系与此相似。
(2)压缩格式。Windows支持 BI_RLE8 及 BI_RLE4 压缩位图存储格式,压缩减少了位图阵列所占用的磁盘空间。
a. BI_RLE8 压缩格式
当biCompression = 1 时,位图文件采用此压缩编码格式。压缩编码以两个字节为基本单位。其中第一个字节规定了用两个字节所指定的颜色出现的连续像素的个数。
例如,压缩编码 05 04 表示从当前位置开始连续显示 5 个像素,着 5 个像素的像素值均为 04。
在第一个字节为零时,第二个字节有特殊的含义:0——行末;1——图末;2——转义后面的两个字节,这两个字节分别表示一个像素从当前位置开始的水平位移和垂直位移;n (0x003 < n < 0xFF)——转义后面的 n 个字节,其后的 n 像素分别用这 n 个字节所指定的颜色画出。注意:实际编码时必须保证后面的字节数是 4 的倍数,不足的位用 0 补充。
b. BI_RLE4 压缩格式
当 biCompression = 2时,位图文件采用此种压缩编码格式。它与 BI_RLE8 的编码方式类似,唯一的不同是:BI_RLE4 的一个字节包含了两个像素的颜色。当连续显示时,第一个像素按字节高四位规定的颜色画出··· ···直到所有像素都画出为止。
归纳起来,BMP图像文件有下列四个特点:
I. 改格式只能存放一幅图像。
II. 只能存储单色、16色、256色或彩色四种图像数据之一。
III. 图像数据有压缩或不压缩两种处理方式,压缩方式为:RLE_4 和 RLE_8。RLE_4只能处理 16 色图像数据;而 RLE_8 则只能压缩 256 色图像数据。
IV. 调色板的数据存储结构较为特殊。
看到这里BMP的文件格式就介绍完了,本次进行编码实现的主要是不压缩的数据格式,压缩的数据格式等到以后有机会再详细介绍。
2.4. 再说一种坐标系
说完了BMP文件格式,我们再说一说图像坐标系5,下面这张图片是笔者画的图像坐标系和图像的存储顺序。这张图可能有助于你理解下面代码中对于像素的提取。
3. 编码实现
3.1. 创建一个C(C++)项目
首先我们打开任何一个能进行C语言编程的编译,建立一个空项目的控制台程序,新建一个CPP文件,并将Lena.bmp拷到项目文件夹中(这里我以Visual Studio 2015 为例,点击图片可以看大图)。
3.2. 编码
在编译器中写上下面的代码(下面的代码是灰度图的读取,关于后面多波段的读取,我们以后在再介绍)。
#include <stdio.h> // C语言代码中必须要引用的头文件
#include <windows.h> // 图像读取的头文件
// 使代码在新版本的VS中正常运行不报错
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)
// 定义一个结构体来读取BMP的信息
typedef struct {
// 定义文件头信息
BITMAPFILEHEADER header;
// 定义位图信息头
BITMAPINFOHEADER info;
// 定义彩色表
RGBQUAD rgb[256];
// 定义位图数据指针
unsigned char *data;
} BMP;
// 读图像函数
void bmpRead(const char *bmpPath, BMP &bmp) {
// 以二进制的方式打开图片
FILE *file = fopen(bmpPath, "rb");
// 读取文件信息头
fread(&bmp.header, sizeof(BITMAPFILEHEADER), 1, file);
// 读取位图信息头
fread(&bmp.info, sizeof(BITMAPINFOHEADER), 1, file);
// 读取彩色表
fread(&bmp.rgb, sizeof(RGBQUAD), 256, file);
// 定义位图数据内存大小
bmp.data = new unsigned char[bmp.info.biWidth * bmp.info.biHeight];
// 读取元素的位图数据
fread(bmp.data, sizeof(unsigned char), bmp.info.biWidth*bmp.info.biHeight, file);
// 关闭图片
fclose(file);
}
// 定一个函数将读取到bmp.data的像素灰度值存储到一个txt文档中
void bmpCells(const char *pixelValuePath, BMP &bmp){
// 新建一个txt文件用于存储像素值
FILE *file = fopen(pixelValuePath, "w");
// 循环读取像素值
for (int i = 0; i < bmp.info.biHeight; i++)
{
for (int j = 0; j < bmp.info.biWidth; j++)
{
// 获取循环到像元的像素值
unsigned char pixel = bmp.data[j + i*bmp.info.biWidth];
// 这里我们打印一下读出的像元
printf("%d ", pixel);
// 保存到文件中
fprintf(file, "%d ", pixel);
}
// 打印一行后换行
printf("\n");
// 完成一行像素的保存后换行继续进行保存
fprintf(file, "\n");
}
// 关闭文件
fclose(file);
}
int main() {
// 定义图像结构体
BMP mbmp;
// 读取图像(如果不是按照本文的新建项目方法,建议使用绝对路径,否则可能会因为文件路径错误而报错。)
bmpRead("../lena.bmp", mbmp);
// 将像素值存储到一个txt文本中
bmpCells("../pixelValue.txt", mbmp);
// 删除指针,释放内存
delete[] mbmp.data;
// 使程序暂停,便于查看
scanf("……");
return 0;
}
4. 验证一下结果
4.1. 结果图
接下来让我们看一下运行的结果。
4.2. 验证正确性
如果你了解遥感图像处理,那么你一定会知道 ERDAS IMAGE 这个软件,现在我们就用这个软件来验证我们编码的结果是否正确。下图中是我们在ERDAS IMAGE中打开的图像像素值。
通过上面的初步对比,我们我们可以看出我们读取的像素值是正确的。
这节的介绍我们到这里就结束了,有什么错误欢迎大家指出。下一节我继续介绍《数字图像处理(二)——BMP图像的统计》,下节将介绍如何获取图像像素的最小值、最大值、均值、标准差、计算图像的熵、以及统计图像灰度直方图。
本博客所有内容为笔者自我理解,如有错误,欢迎批评指正。
Lena图像:了解更多关于Lena图像的信息,请转到Lena图像的百度百科介绍。 ↩︎
图像格式:BMP仅是众多图像格式中的一种,了解更多的格式请转到图像格式的百度百科介绍。 ↩︎
C语言编程入门教程:网上关于C语言的编程的内容很多。例如,菜鸟教程、MOOC慕课平台。笔者这里给列出两种,仅供参考。 ↩︎
BMP文件格式:关于BMP文件格式的介绍来源于《数字图像处理(第三版)》(贾永红版)。 ↩︎