02. 基于MFC读取并显示一幅BMP图像

本博文内容是博文基于MFC框架的图像缩放算法示例的一部分(返回目录)。

上一篇博文01. 基于MFC绘制一个彩色正方形介绍了如何基于Visual Studio的MFC框架搭建一个单文档的GUI程序,并在消息响应函数OnDraw()利用系统提供的绘图工具CDC* pDC绘制一个彩色正方形。

本篇在此基础上介绍有关BMP格式图像的相关内容,编写C++函数读取并在屏幕上显示一个BMP图像,为后续介绍图像缩放算法打下基础。

step 01: BMP文件格式分析

BMP是英文(Bitmap,位图)的简写,是Windows操作系统中的一种标准图像文件格式,能够被多种Windows应用程序所支持。随着Windows操作系统的流行与丰富的Windows应用程序的开发,BMP位图格式理所当然地被广泛应用。这种格式的特点是包含的图像信息较丰富,几乎不进行压缩,但由此导致了它与生俱来的缺点–占用磁盘空间过大。所以,目前BMP在单机上比较流行。

02. 基于MFC读取并显示一幅BMP图像_第1张图片

典型的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字节表示一个像素。

step 02: BMP读取函数readBmp()

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;的含义。

step 03: BMP图像绘制函数drawBmp()

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;
	}
}

step 04: 读取并显示图像ReadAndDrawBMP()

下面的代码在屏幕的指定(左上角)位置绘制文件名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;
} 

step 05: 在OnDraw()函数内绘制BMP图像

将以上代码复制到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);
}

step 06: 另一种思路绘制图像

上面的方法可以读取并绘制了的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界面代码和底层算法代码的耦合度比较低,便于方便代码管理和更新。

你可能感兴趣的:(MFC图像缩放算法示例,MFC数字图像处理,mfc,windows,c++)