bmp是一种常见的未压缩图像格式,也是大多数图像处理入门课会用到的一种引路格式。具体的BMP图像格式解析请见:BMP图像格式详解
老师已经给出了用于处理BMP格式的类的头文件以及部分函数实现代码(C/C++)。由于函数实现为老师的成果,所以我就不贴出代码了,不过我们可以一起分析一下头文件,看一看这个类有什么数据成员,能完成什么功能。
class BMPFILE {
BYTE *Imagedata; //位图数据域
public:
int imagew, imageh; //图片的宽度和高度
int iYRGBnum; //1:灰度,3:彩色
RGBQUAD palette[256]; //调色板
BYTE *pDataAt(int h, int Y0R0G1B2 = 0);
//指向图像第h行的位置,Y0R0G1B2表示:灰度(Y)=0,R=0,G=1,B=2
BOOL AllocateMem(); //为图像分配内存
BOOL LoadBMPFILE(const char *fname);//从硬盘加载图像
BOOL SaveBMPFILE(const char *fname);//将图像保存至硬盘
BMPFILE(); //构造函数,初始化
~BMPFILE(); //析构函数
};
这里唯一需要解释的可能是pDataAt这个函数,尤其是形参中Y0R0G1B2这个让人摸不着头脑的命名。
首先说功能,这个函数的功能是获取第w列列首像素的位置,也就是求偏移量h=imageh*w,然后再用Imagedata+h去定位。
但是这里要补充一个点,那就是BMP数据在内存中的存储方式。我们知道,对24bit的彩图而言,在硬盘中每位像素会以BGR的格式排列,但这不意味着在内存中也是同样连续存储BGR的。相反,在内存中的真实状态是R、G、B被分别存储于3个矩阵中,因此我们要找到第h行的行首像素时,要分别找到其R、G、B的位置。因此实际偏移量应为:
h=imageh*w+Y0R0G1B2*imagew*imageh
// 当然也可以写作 int w = imageh * h + Y0R0G1B2 * imagew * imageh,则定位到h行,行首像素
我们把实验分为两部分,因为bmp2txt并用excel作图那部分卡了我很久,而前面那三个却很简单,我们就先说这简单的部分吧。
这里所谓的反白其实就是反色,只是对于灰度图而言我们看到的效果是黑白颠倒。我们选用的颜色量级为0-255,因此反色就是用255减去当前的颜色。下面贴出部分代码(只给了大家灰度图的反白,如何做彩图的反色呢?留给读者自行解决)
void Reverse(BMPFILE &src,BMPFILE &des) //反白函数
{
for (int i = 0; i < src.imageh; i++)
for (int j = 0; j < src.imagew; j++)
{
//反白图
des.pDataAt(i)[j] = 255 - src.pDataAt(i)[j];
}
}
效果如下
在没有要求的情况下,随意修改即可,内容没什么可讲的那我们就来说说调色板吧。
typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
上面是RGBQUAD,也就是调色板所用的结构体的定义。可以看到每一个RGBQUAD中定义了蓝、绿、红和一个保留位。如果调色板中R=G=B,则显示的是灰度图;相反,如果RGB值各不相同,那么一共可以有256x256x256种颜色,不过注意,由于调色板是一个有256个RGBQUAD的数组,所以最终的图像最多也就256种颜色。对于rgbReserved这一项,stackoverflow上的这份问答大家可以读一下,相信你会有比较深刻的理解。What is rgbReserved?
彩图变灰图其实也没什么可说的,因为经过历代程序员的尝试我们已经有了非常完美的转换公式,Y=0.299R + 0.587G + 0.114B。我们的要求只是显示效果的彩转灰,如果真的是要变成灰度图,那么除了让像素颜色按照公式转换,我们还应该对信息头、文件头以及调色板作出修改,这里就不做了。
说实话这个把bmp数据存入txt的题目要求让我疑惑了很久,很长一段时间大概一天我都在修改类似下面的代码:
bool bmp2txt(const char *cFilename)
{
FILE *fin,*fout;
fin=fopen(cFilename, "r+b");
fout = fopen("bmp.txt", "w+");
int rc;
unsigned char buf[1024];
fseek(fin, sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER), SEEK_SET);
while ((rc = fread(buf, sizeof(unsigned char), 1, fin)) != 0)
{
fwrite(buf, sizeof(unsigned char), rc, fout);
}
fclose(fin);
fclose(fout);
}
为什么卡那么久,主要是这个代码如果把w+改成w+b,再把fseek删掉,写出的二进制文件改成bmp图像是可以正常显示的。这样就让我一度认为方法是没问题的,细节上出错了。包括改为fprintf(fout,"%4d",buf)试图转换为10进制输出也没有解决问题(输出的是重复值,原因未明)。
终于,我决定重新读题,然后我发现了这句话“txt 文件的一行对应图像的一行,按图像显示的顺序存储。”
突然间我就悟了,下面这段正确代码很快就写出来了:
bool bmp2txt(const char *cFilename) //将bmp数据转存入txt,路径为形参
{
//将像素数据存入TXT文件。
BMPFILE bmpfile;
if (!bmpfile.LoadBMPFILE(cFilename))
return false;
FILE *outfile;
if ((outfile = fopen("bmp.txt", "w+")) == NULL)
{
printf("ERROR\n");
return false;
}
for (int i = 0; i < bmpfile.imageh; i++) //按行写入
{
for (int j = 0; j < bmpfile.imagew; j++)
{
fprintf(outfile, "%4d", bmpfile.pDataAt(i)[j]);
}
fprintf(outfile, "\n");
}
fclose(outfile);
return true;
}
//下面贴一下loadbmpfile的函数实现,毕竟挺重要的
//其实就是按照文件头、信息头、数据域一一读入
BOOL BMPFILE::LoadBMPFILE(const char *cFilename)
{
FILE *f;
if (strlen(cFilename) < 1) //文件不存在
return FALSE;
f = fopen(cFilename, "r+b"); //以二进制只读打开
if (f == NULL)
return FALSE;
BITMAPFILEHEADER fh; //文件头
BITMAPINFOHEADER ih; //信息头
fread(&fh, sizeof(BITMAPFILEHEADER), 1, f); //从文件中读取文件头
if (fh.bfType != 0x4d42) //如果不是"BM"文件,直接退出
{
fclose(f);
return FALSE;
}
fread(&ih, sizeof(BITMAPINFOHEADER), 1, f); //从文件中读取信息头
if ((ih.biBitCount != 8) && (ih.biBitCount != 24)) //如果不是8位灰度图,或24位彩图,则退出
{
fclose(f);
return FALSE;
}
iYRGBnum = ih.biBitCount / 8; //1为灰度图,3为彩图
imagew = ih.biWidth;
imageh = ih.biHeight;
if (!AllocateMem()) //如果分配内存失败,退出
{
fclose(f);
return FALSE;
}
if (iYRGBnum == 1) //灰度图需要调色板
fread(palette, sizeof(RGBQUAD), 256, f);
fseek(f, fh.bfOffBits, SEEK_SET); //指向像素数据包
int w4b = (imagew * iYRGBnum + 3) / 4 * 4, i, j; //w4b为像素数据包大小
BYTE *ptr;
ptr = new BYTE[w4b];
if (ptr == NULL)
{
fclose(f);
return FALSE;
}
if (iYRGBnum == 1)
{
for (i = imageh - 1; i >= 0; i--) //从最下面开始往上拷贝字节
{
fread(ptr, w4b, 1, f);
memmove(pDataAt(i), ptr, imagew); //memmove(des,src,count)
}
}
if (iYRGBnum == 3)
{
for (i = imageh - 1; i >= 0; i--)
{
fread(ptr, w4b, 1, f);
for (j = 0; j < imagew; j++) //分别读取R、G、B(注意磁盘里反储)
{
*(pDataAt(i, 0) + j) = *(ptr + j * 3 + 2);
*(pDataAt(i, 1) + j) = *(ptr + j * 3 + 1);
*(pDataAt(i, 2) + j) = *(ptr + j * 3 + 0);
}
}
}
delete[] ptr;
fclose(f);
return TRUE;
}
这告诉我们还是要认真读题啊。。。
这里只给出我认为有意义的一种可视化,因为老师说他只是让我们看看数据在不同表现形式下的效果,理解“有数就有图”。但实际上直方图、折线图都完全没有意义,下面介绍下色阶表示。
我们首先将bmp.txt导入到excel中穷人只能用wps了呜呜呜,如图:
之后全选(ctrl+a),然后在开始——条件格式——色阶,随意选一个之后缩放到最小,就能看到神奇的画面:
当然这个灰度图的显示效果是自定义的,你只需要在其他规则中把最低值改为黑色,最高值改为白色即可。这个原理其实还是个调色板,按照数值的大小去显示介于最小值和最大值之间的一种颜色罢了。不过做出来的那一刻还是很有成就感的。
这学期的第一次图像处理实验,说是三周内上交实际上已经给了快五周时间了,如果对已经给出的代码掌握熟练的话,其实一两天完成不是问题。完整项目工程可见我的gitee。