本博文内容是博文基于MFC框架的图像缩放算法示例的一部分(返回目录)。
上一篇博文01. 基于MFC绘制一个彩色正方形介绍了如何基于Visual Studio的MFC框架搭建一个单文档的GUI程序,并在消息响应函数OnDraw()利用系统提供的绘图工具CDC* pDC
绘制一个彩色正方形。
本篇在此基础上介绍有关BMP格式图像的相关内容,编写C++函数读取并在屏幕上显示一个BMP图像,为后续介绍图像缩放算法打下基础。
BMP是英文(Bitmap,位图)的简写,是Windows操作系统中的一种标准图像文件格式,能够被多种Windows应用程序所支持。随着Windows操作系统的流行与丰富的Windows应用程序的开发,BMP位图格式理所当然地被广泛应用。这种格式的特点是包含的图像信息较丰富,几乎不进行压缩,但由此导致了它与生俱来的缺点–占用磁盘空间过大。所以,目前BMP在单机上比较流行。
典型的BMP文件由文件信息头、位图信息头、颜色表和位图数据四部分组成。
(1)文件信息头:共14字节,如下面结构体BITMAPFILEHEADER所示,包含BMP文件的类型、文件大小和位图起始位置等信息。
typedef struct tagBITMAPFILEHEADER {
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;
上图是一个用HexViewer之类的十六进制显示工具读取的一个BMP图像数据的前面部分内容,可以看到文件最开始有42 4d 36 c0
四个字节,对应的ASCII码是BM6?
,这是BMP图像的标志bfType。
(2)位图信息头:共40字节,包含BMP位图信息头数据,用于说明位图的宽度、高度和每个像素所用比特数等信息。
typedef struct tagBITMAPINFOHEADER {
DWORD biSize;
LONG biWidth; //图像宽度
LONG biHeight; //图像高度
WORD biPlanes;
WORD biBitCount; //每个像素所占比特数
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER;
(3)颜色表:用于说明位图中的颜色,它有若干个表项,每一个表项是一个RGBQUAD类型的结构,定义一种颜色。颜色表又称为调色板,这个部分是可选的,有些位图需要调色板,有些位图比如真彩色图(24位的BMP)就不需要颜色表,小于24位的BMP图像使用调色板中的颜色索引值。
(4)位图数据:记录位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描行之间是从下到上。位图的一个像素值所占的比特数记录在位图信息头中,比如真彩色图使用24比特,也就是3字节表示一个像素。
bool readBmp(char* bmpName,
unsigned char* img_data,
int* bmpWidth, int* bmpHeight,
int* biBitCount, int* lineByte) {
//二进制读方式打开指定的图像文件
FILE* fp;
fopen_s(&fp, bmpName, "rb");
if (fp == NULL)
return false;
//读取位图文件头结构BITMAPFILEHEADER
BITMAPFILEHEADER file_header;
fread(&file_header, sizeof(BITMAPFILEHEADER), 1, fp);
//定义位图信息头结构变量,读取位图信息头进内存,存放在变量head中
BITMAPINFOHEADER info_header;
fread(&info_header, sizeof(BITMAPINFOHEADER), 1, fp);
//获取图像的宽、高、每像素所占比特数等信息
*bmpWidth = info_header.biWidth;
*bmpHeight = info_header.biHeight;
*biBitCount = info_header.biBitCount;
//计算图像每行像素所占的字节数(必须是4的倍数)
*lineByte = (*bmpWidth * *biBitCount / 8 + 3) / 4 * 4;
//读位图数据进内存
fread(img_data, 1, (*lineByte) * (*bmpHeight), fp);
fclose(fp);//关闭文件
return true;//读取文件成功
}
此处需要提醒的是BMP图像的数据是按行存储的,有一个要求是每行图像数据的字节数必须是4的倍数,如果不足就在后面补0以满足要求。比如,一个宽度为101的真彩图像每行像素所用字节数是101*3 = 303,不是4的倍数,就在每行像素数据后补一个字节0,达到304个字节,满足4的倍数的要求。这就是代码内*lineByte = (*bmpWidth * *biBitCount / 8 + 3) / 4 * 4;
的含义。
void drawBmp(CDC* pDC,
unsigned char* img_data,
int bmpWidth, int bmpHeight, int lineByte,
int offset_left, int offset_top)
{
unsigned char red, green, blue = 0;
unsigned char* img_line = img_data;
for (int i = 0; i < bmpHeight; i++) {
for (int k = 0; k < bmpWidth; k++)
{
blue = img_line[3 * k + 0];
green = img_line[3 * k + 1];
red = img_line[3 * k + 2];
pDC->SetPixel( k + offset_left,
bmpHeight - 1 - i + offset_top,
RGB(red,green,blue));
}
img_line += lineByte;
}
}
下面的代码在屏幕的指定(左上角)位置绘制文件名bmpName指定的BMP图像。注意,为简单起见,此段代码假定读取的是24位真彩色BMP图像,并没有处理各种异常情况。
void readAndDrawBMP(CDC* pDC,
char* bmpName,
int offset_left,int offset_top)
{
//申请保存图像数据的内存区,最大图像可以有1920*1080像素
unsigned char* img_data = new unsigned char[1920 * 1080 * 3];
//图像相关信息变量
int bmpWidth, bmpHeight, biBitCount, lineByte;
//读取图像数据到内存
bool result = readBmp(bmpName, img_data,
&bmpWidth, &bmpHeight,
&biBitCount, &lineByte);
if (!result) {
delete[] img_data;
return;
}
//绘制读取的BMP图像
drawBmp(pDC, img_data,
bmpWidth, bmpHeight, lineByte,
offset_left, offset_top);
delete[] img_data;
}
将以上代码复制到CMFCSD02View.cpp
文件内OnDraw()的上方,再在OnDraw()内部添加readAndDrawBMP()函数即可绘制指定文件名的BMP图像。
void CMFCSD02View::OnDraw(CDC* pDC)
{
CMFCSD02Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;
// TODO: 在此处为本机数据添加绘制代码
//1. 要绘制的图像文件名
char bmpName[] = "d://1.bmp";
//2. 设定左上角坐标
int offset_left = 100;
int offset_top = 200;
//3. 读取并绘制图像
readAndDrawBMP(pDC, bmpName, offset_left, offset_top);
}
上面的方法可以读取并绘制了的BMP图像,对于Red,Green和Blue三个通道合并处理,这在我们后续课程中处理单色图像并不方便。因此,我们还可以还一种思路来绘制BMP图像。即在读取BMP图像后将其分离为Red,Green和Blue三个独立的通道,对三个通道分别进行运行相关算法,在合并绘制三个通道数据。为此我们添加两个新的函数
将彩色BMP图像分离为Red,Green和Blue三个独立的通道 separateRGB()
bool separateRGB(unsigned char* img_data,
unsigned char* R,
unsigned char* G,
unsigned char* B,
int bmpWidth, int bmpHeight, int lineByte)
{
for (int i = 0; i < bmpHeight; i++) {
for (int k = 0; k < bmpWidth; k++)
{
*R++ = img_data[3 * k + 0];
*G++ = img_data[3 * k + 1];
*B++ = img_data[3 * k + 2];
}
img_data += lineByte;
}
return true;
}
合并绘制Red,Green和Blue三个独立的通道图像 print_matrix()
void print_matrix( CDC* pDC,
unsigned char* img_R,
unsigned char* img_G,
unsigned char* img_B,
int width, int height,
int offset_left, int offset_top)
{
unsigned char r, g, b = 0;
for (int i = 0; i < height; i++)
for (int k = 0; k < width; k++) {
r = (unsigned char)img_R[i * width + k];
g = (unsigned char)img_G[i * width + k];
b = (unsigned char)img_B[i * width + k];
pDC->SetPixel(k + offset_left,
height - 1 - i + offset_top,
RGB(b, g, r));
}
}
为新的方法编写新的readAndDrawBMP函数,为与之前的函数区分,新函数命名为readAndDrawBMP_seperate()
void readAndDrawBMP_seperate(CDC* pDC,
char* bmpName,
int offset_left, int offset_top)
{
//申请保存图像数据的内存区,最大图像可以有1920*1080像素
unsigned char* img_data = new unsigned char[1920 * 1080 * 3];
//图像相关信息变量
int bmpWidth, bmpHeight, biBitCount, lineByte;
//读取图像数据到内存
bool result = readBmp(bmpName, img_data,
&bmpWidth, &bmpHeight,
&biBitCount, &lineByte);
if (!result) {
delete[] img_data;
return;
}
unsigned char* R = new unsigned char[1920 * 1080];
unsigned char* G = new unsigned char[1920 * 1080];
unsigned char* B = new unsigned char[1920 * 1080];
separateRGB(img_data, R, G, B, bmpWidth, bmpHeight, lineByte);
//绘制读取的BMP图像
print_matrix(pDC, R, G, B, bmpWidth, bmpHeight, offset_left, offset_top);
//
delete[] img_data;
delete[] R;
delete[] G;
delete[] B;
}
将上面三个新的函数添加到OnDraw()函数,然后在OnDraw()函数内部将readAndDrawBMP更新为readAndDrawBMP_seperate即可,可以看出效果是完全一样的。
void CMFCSD02View::OnDraw(CDC* pDC)
{
CMFCSD02Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;
// TODO: 在此处为本机数据添加绘制代码
//要绘制的图像文件名
char bmpName[] = "d://1.bmp";
//左上角坐标
int offset_left = 100;
int offset_top = 200;
//读取并绘制图像
readAndDrawBMP_seperate(pDC, bmpName, offset_left, offset_top);
}
用C++的类和对象概念将上面处理BMP图像相关的函数封装为一个类,并在OnDraw()函数内调用bmp对象的方法完成同样的功能。这样处理的最大好处是GUI界面代码和底层算法代码的耦合度比较低,便于方便代码管理和更新。