目录索引
前言
这一次数字图像处理实验的内容虽然有OpenCV环境的配置,但是重头戏在于BMP文件的读取,对于编程基础欠佳的同学有一定的挑战性,一开始我也觉得这个任务很难,但是一步步自己查找资料下来,不断地debug也硬是做出来了(笑)。
本次实验有很多种实现方式,在OpenCV库里有直接能够调用的函数,但是为了能深度理解BMP位图在计算机中的存储方式,这里介绍的思路是用C++的基本函数读取BMP文件。
理论准备
图像的文件格式
- 图像文件格式即图像文件存放的格式
- 软硬件商提出并广泛采用的格式: BMP、 GIF、 PSD 等
- 国际标准组织提出的格式: JPEG、 TIFF、 DICOM、 MPEG等
- BMP文件格式:(包括DIB、 DDB格式) Windows操作系统支持的文件格式。不经过压缩、按位存储。
- DIB格式(Device Independent Bitmap):与设备无关,存储未压缩图像,是很多图像处理程序的内 存图像数据格式。
- DDB格式(Device Dependent Bitmap) :与设备有关,存储显示和打印设备兼容的未压缩图像。
位图和矢量图的区别
- 位图图像:由称作像素(图片元素)的单个点组成的,对图像进行缩放会改变图像辨识度。
- 矢量图形:根据几何特性来绘制图形,矢量可以是一个点或一条线,对图像进行缩放操作不会改变图像辩认度。
BMP文件格式
BMP图像文件包括四个部分:
- 位图文件头(BITMAPFILEHEADER):长度14字节,记录了文件类型、大小、数据起始位置。
- 位图信息头(BITMAPINFOHEADER):长度40字节,位图宽度、高度,每个像素需要位数,压缩类型(最常用不压缩), 位平面数、颜色索引等信息。
- 位图调色板(Palette):可选,使用索引来表示图像颜色,调色板可索引与其对应的颜色的映射表。有些位 图需要调色板(如16色, 256色),真彩色(24或32位RGB)不需要调色板。
- 位图数据(Pixel data):每个像素占一个字节,取得字节后,以该字节为索引查询相应的颜色。真彩色不需要 索引,位图数据就是像素颜色值。
接下来将具体介绍BMP文件四个部分的内容。
位图文件头(BITMAPFILEHEADER)
Offset | Member | Introduction |
---|---|---|
0 | bfType | 位图文件类型,必须为“BM” |
2 | bfSize | 文件大小,单位Byte |
6 | bfReserved1 | 保留字节,必须为0 |
8 | bfReserved2 | 保留字节,必须为0 |
10 | bfOffBits | 位图实际数据起始位置 |
文件头部分所包含的图像信息:
- bfType:指定文件类型,必须是0x424D,即字符串“BM”,也就是说所有.bmp文件的头两个字节都是“BM”。
- bfSize:指定文件大小,包括这14个字节。
- bfReserved1,bfReserved2 :为保留字,不用考虑。
- bfOffBits:为从文件头到实际的位图数据的偏移字节数,即前三个部分的长度之和。
位图信息头(BITMAPINFOHEADER)
Offset | Member | Introduction |
---|---|---|
14 | biSize | 该结构长度,必须为40 |
18 | biWidth | 图像实际宽度,单位是像素 |
22 | biHeight | 图像实际高度`,单位是像素 |
26 | biPlanes | 必须为1 |
28 | biBiCount | 每像素所需bit位数,常用的值是1(黑白图),4(16色图),8(256色图),24(真彩色图) |
30 | biCompression | 非压缩为0 |
34 | biSizeImage | 指实际的位图数据所占用的字节,非压缩时可为0 |
40 | biXPelsPerMeter | 水平分辨率,pixel/m |
44 | biYPelsPerMeter | 垂直分辨率,pixel/m |
48 | biClrUsed | 实际用到的颜色数,若为0,则用到的颜 色数为2的biBitCount次方种 |
52 | biClrImportant | 重要颜色数,0指代所有元素都重要 |
位图调色板(Palette)
针对非真彩色图,其颜色显示方式并不是直接显示像素块的RGB值,需要一个数组来保存需要使用的颜色,以便于在位图文件中调用,这个保存颜色的数组就是调色板,而真彩色图的位图数据直接显示的是像素块的RGB值因此不需要调色板。
调色板数据结构定义:
typedef struct tagRGBQUAD{
BYTE rgbBlue;
Byte rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
}RGBQUAD;
调色板数组中的每一个元素占四个字节,分别保存颜色的RGB变量和一个字节的保留值。
位图数据(Pixel data)
对于用到调色板的位图,图像数据就是该像素在调色板中的索引值。对于真彩色图,图像数据就是实际的R、G、B值。
对于2色位图,用1位就可以表示该像素的颜色(一般0表示黑,1表示白),所以一个 字节可以表示8个像素。
对于16色位图,用4位可以表示一个像素的颜色,所以一个字节可以表示2个像素。
对于256色位图,一个字节刚好可以表示1个像素。
对于真彩色图,三个字节才能表示1个像素。
一般来说,BMP文件的数据从下到上,从左到右的。也就是说,从文件中最先读到的是图象最下面一行的左边第一个像素, 然后是左边第二个像素……接下来是倒数第二行左边第一个像素,左边第二个像素……依次类推,最后得到的是最上面一行的最右一个像素。
fstream头文件
头文件fstream包含了ifstream、ofstream、fstream三个类,可以通过定义这三个类的对象来实现相对应的文件操作。
#include <fstream>
ofstream //文件写操作,内存写入存储设备
ifstream //文件读操作,存储设备读取到内存中
fstream //读写操作,对打开的文件可进行读写操作
计算机中的文件是按二进制的形式存储的,它一律把数据看成是字节构成的序列,即字节流,对文件的存取也是以字节为单位的,输入/输出的的数据流仅受程序控制而不受物理符号(如回车换行符)的控制,所以说C语言文件为流式文件。在该实验中,由于需要读取并解析文件数据,因此需要用到FILE,fopen_s,fseek,fread,fclose。
文件类型指针
可以用该结构体类型来定义文件类型的指针变量:
FILE *fp;
FILE是在stdio.h中定义的结构体类型,封装了与文件有关的信息,如文件句柄、位置指针及缓冲区等,缓冲文件系统为每个被使用的文件在内存中开辟一个缓冲区,
用来存放文件的有关信息,这些信息被保存在一个FILE结构类型的变量中,fp是一个指向FILE结构体类型的指针变量。
fopen_s函数
在高版本的visual sdudio中使用fopen会报错,因此这里只介绍fopen_s函数,该函数的功能是将文件打开,并指定使用文件的方式。
fopen_s函数声明如下:
errno_t fopen_s(
FILE** fp,
char const* _FileName,
char const* _Mode
);
该函数有三个参数:指针、文件名、使用文件的方式。
前文说到fp是一个指向FILE结构体类型的指针变量,现在定义的新指针该指针是用来接收fp的指针,即fp的地址。
文件名就是待打开文件的文件名。
如果文件打开成功,则函数返回值为0,若返回了正整数,可以根据errno_t类的定义查询具体的错误。
文件使用方式有十二种,如下表格:
符号 | 功能 |
---|---|
“r” = “rt” | 打开一个文本文件,文件必须存在,只允许读 |
“r+” = “rt+” | 打开一个文本文件,文件必须存在,允许读写 |
“rb” | 打开一个二进制文件,文件必须存在,只允许读 |
“rb+” | 打开一个二进制文件,文件必须存在,允许读写 |
“w” = “wt” | 新建一个文本文件,已存在的文件将内容清空,只允许写 |
“w+” =“wt+” | 新建一个文本文件,已存在的文件将内容清空,允许读写 |
“wb” | 新建一个二进制文件,已存在的文件将内容清空,只允许写 |
“wb+” | 新建一个二进制文件,已存在的文件将内容清空,允许读写 |
“a” = “at” | 打开或新建一个文本文件,只允许在文件末尾追写 |
“a+” = “at+” | 打开或新建一个文本文件,可以读,但只允许在文件末尾追写 |
“ab” | 打开或新建一个二进制文件,只允许在文件末尾追写 |
“ab+” | 打开或新建一个二进制文件,可以读,但只允许在文件末尾追写 |
fseek
fseek函数的声明如下:
int fseek(FILE *stream, long offset, int fromwhere);
其功能是重新设置文件指针stream的位置。其中stream也就是上文提到的fp,offset偏移量,值为正数表示指针在fromwhere后多少位,值为负数表示指针在fromwhere前多少位。fromwhere是指针位置的基准,可以使用以下三个常量:
变量 | 意义 |
---|---|
SEEK_SET | 文件起始位置 |
SEEK_CUR | 指针当前指向位置 |
SEEK_END | 文件结束位置 |
由于fstream中对文件的具体操作都要基于stream的位置,因此fseek的使用显得尤其重要。
例:如果我们要把fp的指针位置移到文件开头后的四个字节的位置,则需输入如下代码:
fseek(fp,4,SEEK_SET);
fread函数
fread函数的作用是从文件流中读取数据到提前定义好的数组中,其声明如下:
size_t fread( void *restrict buffer, size_t size, size_t count, FILE *restrict stream );
其四个参数的定义如下表所示:
参数 | 定义 |
---|---|
buffer | 指向要读取的数组中首个对象的指针 |
size | 每个对象的大小(单位是字节) |
count | 要读取的对象个数 |
stream | 输入流 |
需要特别注意的是,在使用fread后若还需对文件流进行操作,需再次使用fseek函数设置文件流指针的新位置。
fclose函数
fclose的功能是关闭一个文件流,函数声明如下:
int fclose( FILE *fp );
如果流成功关闭,fclose 返回 0,否则返回EOF(-1)。(如果流为NULL,而且程序可以继续执行,fclose设定error number给EINVAL,并返回EOF。)
typedef
在该实验中需要使用typedef来声明位图文件头、信息头、调色板,typedef是在计算机编程语言中用来为复杂的声明定义简单的别名。在该实验中需要频繁地用到一个字节、两个字节、四个字节的定义,因此可声明如下变量:
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef char BYTE;
同时也给出typedef声明结构体的思路,将文件头,信息头,调色板的类型用typedef作声明,然后在main函数里直接使用,不仅能加深对BMP位图文件的理解,同时也能更加方便地解析调用位图相关的参数。
如,位图文件头可声明如下:
typedef struct BITMAP_FILE_HEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
}BITMPFILEHEADER;
实战—读取位图文件头
接下来我将以读取位图文件的文件头并输出前两个字符为例(假设输入的文件确实是bmp文件)利用理论准备中的方法,首先我们得定义文件头的结构体:
typedef struct BITMAP_FILE_HEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
}BITMPFILEHEADER;
接下来读入文件:
int main(){
FILE* fp;
errno_t err;
err = fopen_s(&fp, "test1.bmp", "rb");
unsigned char a[2];//定义数组来保存文件的头两位
memset(a, 0, sizeof(a));