BMP文件及直方图均衡化处理
BMP文件格式
一、BMP简介
BMP(Bitmap-File)是一种与硬件设备无关的图像文件格式,由于它不采用压缩的方式,所以BMP文件所占用的空间很大。BMP文件的图像位深可选1bit、4bit、8bit、24bit,文件存储的时候图像的扫描方式是从左到右,从下到上的顺序。接下来主要看看BMP文件的结构:
二、BMP文件的结构
BMP文件由文件头(BITMAPFILEHEADER)、位图信息头(BITMAPINFOHEADER)、调色板(Rgbquad)以及图像数据(BYTEDATA)。在C语言中,内置了结构体以说明BMP信息,具体如下面所示,但在说明具体信息之前我们必须得知道一些关键词的信息,有助于我们对之后
的读取BMP文件信息的操作:
关键词 | 实际表示 | 比特数 | 字节数 |
---|---|---|---|
WORD | unsigned short | 16 | 2 |
DWORD | unsigned long | 32 | 4 |
LONG | long | 32 | 4 |
BYTE | unsigned char | 8 | 1 |
- 文件头(BITMAPFILEHEADER)
图像的文件头包括文件类型、文件大小、两个保留字以及关键的偏移量,具体如下结构体所示:
typedef struct BITMAPFILEHEADER
{
WORD bfType; //文件类型
DWORD bfSize; //文件大小
WORD bfReserved1; //保留字,必须为0
WORD bfReserved2; //保留字,必须为0
DWORD bfOffBits; //图像数据的起始位置以相对于文件头的偏移量表示,字节为单位
}BITMAPFILEHEADER;
如果你想通过C语言读取文件头的信息并输出,你可以在你打开的文件中这样操作:
BITMAPFILEHEADER bmpFileHeader; //定义文件头
fseek(fpbmp, 0L, SEEK_SET);//将文件指针移至开头
fread(&bmpFileHeader, sizeof(BITMAPFILEHEADER), 1, fpbmp);
//输出文件头信息
printf("************************************************\n");
printf("*************tagBITMAPFILEHEADER info***********\n");
printf("************************************************\n");
printf("bfType is %d.\n", bmpFileHeader.bfType);
printf("bfSize is %d.\n", bmpFileHeader.bfSize);
printf("bfReserved1 is %d.\n", bmpFileHeader.bfReserved1);
printf("bfReserved2 is %d.\n", bmpFileHeader.bfReserved2);
printf("bfOffBits is %d.\n", bmpFileHeader.bfOffBits);
运行结果是这样的(一定要先打开文件并且写指针之类的,下面都是以256×256像素为例展开说明):
在这里我想说的是这个 偏移量 真的是很重要的东西,在读取文件具体数据的时候,因为图像信息头以及调色板的长度会根据不同情况而变化,可根据偏移量迅速读取图像数据。关于图像数据将会在接下来的文章中进行说明。
- 位图信息头(BITMAPINFOHEADER)
位图信息头包括BMP图像的占用字节数、位宽、位高、目标设备级别、位深、压缩数据、大小、水平分辨率、垂直分辨率、实际使用调色板的颜色数、重要颜色数:
typedef struct BITMAPINFOHEADER
{
DWORD biSize; //本结构占用的字节数
LONG biWidth; //位图的宽度,以像素为单位
LONG biHeight; //位图的高度,以像素为单位
WORD biPlanes; //目标设备的级别,必须为
WORD biBitCount; //每个像素所需的位数
DWORD biCompression; //位图压缩类型必须是0
DWORD biSizeImage; //位图的大小,以字节为单位
LONG biXPelsPerMeter; //位图水平分辨率,每米像素数
LONG biYPelsPerMeter; //位图垂直分辨率,每米像素数
DWORD biClrUsed; //位图实际使用调色板的颜色数
DWORD biClrImportant; //位图显示过程中重要的颜色数/
}BITMAPINFOHEADER;
其中我想说明的是以下几点:
- 一张图的像素=biWidth×biHeight,这点在接下来的直方图处理中很关键,因为要定义一下内存的大小以及位图数据读取时的循环次数。
- 每个像素的位数,也就是我们常说的位深biBitCount,下面介绍一下1位、4位以及8位的情况:
- 如果位深为1,则说明位图最多只有两种颜色;
- 如果位深为4,则说明位图最多有16种颜色,每个像素用4位来表示,并用这4位作为彩色表的表项来查找该像素的颜色,比如说位图第一个字节是0x1F,表示有两个表项,第一像素的颜色就在调色板的第2表项就找,第二像素的颜色就在彩色表的第16表项中查找。此时调色板在缺省的情况下会有16个RGB,对应于索引0~15;
- 如果位深是8,表示位图最多有256种颜色,每个像素用8位表示,我们在接下来的均衡中用的也是这种8位的图;
BY THE WAY,我们在接下来的均衡中,像我老师说的24位的图像不采用索引图像格式,没有调色板部分,像素数据直接用BGR值,导致我在读取其他位数(除了8位)时会出现数据错误,或者读到的是一些错误的数据。当然这些也都是图像数据的内容,我在这里也就先提前声明了
接下来我们看一下如何在C语言程序中调用信息头信息
BITMAPINFOHEADER bmpInfoHeader;
fseek(fpbmp, 14L, SEEK_SET); //将指针移至离开头14L处
fread(&bmpInfoHeader, sizeof(BITMAPINFOHEADER), 1, fpbmp);
//输出BMP文件头的详细信息
printf("************************************************\n");
printf("*************tagBITMAPINFOHEADER info***********\n");
printf("************************************************\n");
printf("biSize is %d.\n", bmpInfoHeader.biSize);
printf("biWidth is %d.\n", bmpInfoHeader.biWidth);
printf("biHeight is %d.\n", bmpInfoHeader.biHeight);
printf("biPlanes is %d.\n", bmpInfoHeader.biPlanes);
printf("biBitCount is %d.\n", bmpInfoHeader.biBitCount);
printf("biCompression is %d.\n", bmpInfoHeader.biCompression);
printf("biSizeImage is %d.\n", bmpInfoHeader.biSizeImage);
printf("biXPelsPerMeter is %d.\n", bmpInfoHeader.biXPelsPerMeter);
printf("biYPelsPerMeter is %d.\n", bmpInfoHeader.biYPelsPerMeter);
printf("biClrUsed is %d.\n", bmpInfoHeader.biClrUsed);
printf("biClrImportant is %d.\n", bmpInfoHeader.biClrImportant);
运行结果如下图所示:
- 调色板(Rgbquad)
调色板应予说明位图中的颜色,它有若干个表项,每一个表项是一个RGBQUAD类型的结构定义一种颜色,RGBQUAD结构数据的个数由位图信息头的biBitCount来确定,当biBitCount=1,4,8时,分别有2,16,256个表项,而biBitCount=24时没有彩色表项。
typedef struct RGBQUAD
{
BYTE rgbBlue; //蓝色的亮度(0~255)
BYTE rgbGreen; //绿色的亮度(0~255)
BYTE rgbRed; //红色的亮度(0~255)
BYTE rgbReserved; //保留,必须为0
}RGBQUAD;
我们可以通过下面的操作读取调色板的数据:
RGBQUAD bmpColorTable[256];
fread(bmpColorTable, sizeof(RGBQUAD), 256, fpbmp);
- 图像数据构成(BYTEDATA)
位图数据按我的理解是一种索引号,为每个像素点在调色板中寻找出对应的颜色。在编程中喜欢把位图信息头和调色板合为位图信息:
typedef struct BITMAPINFO
{
BITMAPINFOHEADER bmiHeader; //位图信息头
RGBQUAD bmiColors[256]; //彩色表
}BITMAPINFO;
在程序中我是这样调用的:
fseek(fpbmp, bmpFileHeader.bfOffBits, SEEK_SET);//偏移量
fread(bmpValue, total_xiangsu, 1, fpbmp);//total_xiangsu是总的像素数=位宽*位高
位图数据极为重要,在接下来的直方图中我们将主要对位图数据进行处理。
三、读取数据的顺序
按照下面的流程图才能一步一步读取成功BMP文件的数据,这样BMP文件的所有信息才能读出来:
直方图均衡化处理
一、直方图的概念
- 定义:指灰度统计直方图,即数字图像中像素灰度值的分布情况。具体地说,即数字图像中的每一灰度级与其出现的频数间的统计关系。
- 灰度值与直方图:设一数字图像有L个灰度级,灰度级范围为[0, L-1],则其直方图可用一个离散函数来定义:
h ( r k ) = n k h(r^k)=n_{k} h(rk)=nk
其中,rk是[0, L-1]内的第k级灰度,nk是图像中灰度级为k的像素个数(对应出现的频率),其中k=0,1,2,…,L-1,把离散函数h(rk)—rk用图形表示出来即得到直方图。横坐标为图像的灰度, 纵坐标为具有某灰度的像素个数。 - 归一化直方图:用h(rk)除以图像的像素总数MN,记为:
p ( r k ) = h ( r k ) M N = n k M N p(r_k)=\frac {h(r^k)}{MN}=\frac {n_k}{MN} p(rk)=MNh(rk)=MNnk
也就是: p ( r k ) = 灰 度 值 为 r k 的 像 素 个 数 图 像 的 像 素 总 数 p(r_k)=\frac {灰度值为r_k的像素个数}{图像的像素总数} p(rk)=图像的像素总数灰度值为rk的像素个数且有 ∑ k = 0 L − 1 p ( r k ) = 1 \sum_{k=0}^{L-1} p(r_k)=1 k=0∑L−1p(rk)=1
故p(rk)近似为灰度rk的出现概率,整个归一化直方图则表示了图像的灰度级概率分布。
二、直方图均衡化
- 基本思想:将原始图的直方图变换为近似均匀分布的形式。
- 直方图均衡实际上就是 灰度变换,关键在于求这个变换的函数,且这个函数s要满足以下条件:
- 在0≤r≤L-1范围内s=T( r)为单调递增函数各灰度级在变换后仍保持从黑到白的单一变化顺序;
- 0≤T( r)≤L-1,变换前后灰度值动态范围一致,避免整体变亮或变暗
- 简单点说就是将一个非均匀分布的直方图转换成均匀分布的直方图:
均衡后:
- 具体到离散灰度级来讲,设一幅图像总像素数为MN,分成L个灰度级,灰度变换函数的形式为: s k = T ( r k ) = ( L − 1 ) ∑ j = 0 k p r ( r j ) = L − 1 M N ∑ j = 0 k n j s_k=T(r^k)=(L-1)\sum_{j=0}^{k}p_r(r_j)=\frac{L-1}{MN}\sum_{j=0}^{k}n_j sk=T(rk)=(L−1)j=0∑kpr(rj)=MNL−1j=0∑knj
证明过程是利用概率论的知识有 p s ( s ) d s = p r d r p_s(s)ds=p_rdr ps(s)ds=prdr既然是均衡化,自然有ps(s)= 1/L-1代进去求得上述式子。
得到的新的灰度级下,将相同灰度级的频数合并就能让整张图的灰度值相对比较平稳。下面将介绍在代码中的实现。
三、直方图均衡化在代码中实现
作为一个C语言不熟的nan人(好吧其实啥也不行哈哈),写这个代码着实是太为难了,好吧,吐槽的话就最后再写写,现在我把代码的流程图大概说一下。
事先说明的是:我用的灰度值是图像数据代替,也就是我们之前文中提到的索引号,考虑到索引号的排序与灰度值的大小排序应该是一致的。
流程图如下图所示:
我觉得有必要概括一下自己的均衡化的思路,当然参考了众多大佬的方案:
我在读取位图数据成功后(注意这里一定要先加上 偏移量 再去读取位图数据,否则读出来的信息是错误的),首先按照像素点的次序统计了灰度值的频数oldhuiduCount,接着按照灰度值的大小次序得到了原灰度值频数、频率以及原灰度值等级,接着我按照公式
s
k
=
T
(
r
k
)
=
(
L
−
1
)
∑
j
=
0
k
p
r
(
r
j
)
=
L
−
1
M
N
∑
j
=
0
k
n
j
s_k=T(r^k)=(L-1)\sum_{j=0}^{k}p_r(r_j)=\frac{L-1}{MN}\sum_{j=0}^{k}n_j
sk=T(rk)=(L−1)j=0∑kpr(rj)=MNL−1j=0∑knj对数据进行了均衡化处理得到的均衡化后的灰度级 new_level,接下来我写的程序里面作用不大,完全是为了按照我老师给予的PPT以及指导写的,就是将相同的灰度级数的频数叠加在一起罢了。之后就是按照像素点开始循环,找出 每个像素点上对应的原灰度值对应的级数,从而找到新的灰度级对应的灰度值赋给新的bmpValue,然后就是把之前得到的bmpFileHeader、bmpInfoHeader、bmpColorTable以及现在得到的newbmpValue全部写进BMP文件里面就可以了,运行结果如下图所示(截取一小部分),GAME OVER.
写到这里我不敢放输出的图片,因为我也不知道什么原因,我输出的图片是黑色的,仅仅能看到一些轮廓,但是用MATLAB去做直方图(接下来要做的验证)却能显示出来,具体的图片我之后再给出。如果有大佬知道为啥出现这种情况,希望您能跟我说一下原因,感激不尽!!!
以下是部分代码:
BYTE oldhuidu[total_xiangsu] = {0}; //每个像素点对应的灰度值,共total_xiangsu个点
BYTE old_level[total_xiangsu] = {0}; //原来图像的灰度值级别
BYTE new_level[total_xiangsu] = {0}; //均衡化后新图像的灰度值级别
BYTE bmpValue[total_xiangsu]={0}; //灰度值对应的位图实际数据
BYTE newbmpValue[total_xiangsu]={0}; //新灰度值对应的位图实际数据
BYTE newgrayValue[total_xiangsu] = {0}; //均衡化后新图像对应bmp像素点的灰度值
int i;
int j;
int k;
int c;
fseek(fpbmp, bmpFileHeader.bfOffBits, SEEK_SET);
fread(bmpValue, total_xiangsu, 1, fpbmp); //读取各像素点
for (i = 0; i < total_xiangsu; i++)
{
oldhuidu[i]=bmpValue[i];
}
//直方图
int oldhuiduCount[256] = {0}; //原灰度级下每个灰度值出现的频数(次序是灰度值排序)
int oldhuiduCount_level[256] = {0}; //记录原灰度级对应的灰度频数(次序是原灰度等级排序)
int oldhuidu_level[256] = {0}; //记录下对应灰度级下的灰度值(从最小的灰度值开始排序)
double oldhuiduFrequency[256] = {0.0}; //记录下每个灰度值出现的频率(次序是灰度值排序)
double oldhuiduFrequency_level[256] = {0.0}; //记录下原灰度级下的灰度概率(次序是灰度等级)
int newhuiduCount_level[256]={0}; //记录新灰度级对应的灰度频数,要根据具体的灰度级改内存
int level=0; //原来的灰度灰度等级
for (i = 0; i < total_xiangsu; i++)
{
oldhuiduCount[oldhuidu[i]]++; //寻找像素点中相同的灰度值,计算每个灰度值下的频数
}
for (i = 0; i < 256; i++)
{
if (oldhuiduCount[i]) //原灰度存在,此刻的i是灰度值
{
oldhuiduCount_level[level] = oldhuiduCount[i];
oldhuidu_level[level] = i;
old_level[level] = level;
oldhuiduFrequency[i] = oldhuiduCount[i] / (total_xiangsudian);
oldhuiduFrequency_level[level] = oldhuiduFrequency[i];
printf("灰度值%d\t频数为%d\t频率为%f\t第%d个灰度等级.\n", oldhuidu_level[level], oldhuiduCount[i], oldhuiduFrequency[i],old_level[level]);
level++;
}
}
//均衡化处理过程
double oldhuiduFrequency_add; //不同原灰度级下灰度概率的累加值
for (i = 0; i < level; i++)
{
oldhuiduFrequency_add+=oldhuiduFrequency_level[i];
new_level[i]=((level-1.0)*oldhuiduFrequency_add)+0.5; //公式:新的灰度等级=(原灰度等级-1)*原灰度等级下根据等级不同得到的不同灰度概率的累加值
printf("新的灰度等级是%d.\t旧的灰度等级是%d.\n", new_level[i], old_level[i]);
}
int num; //记录相同新灰度级个数
for(i = 0; i < level; i = i+1+num)
{
num=0;
for (j = (i+1); j < level; j++)
{
if (new_level[j]==new_level[i]) //存在相同的灰度等级
{
num++;
newhuiduCount_level[i]+=oldhuiduCount_level[j]; //这在代码中没什么作用,只是为了说明新的灰度等级下,原像素点对应的灰度值将会改变
}
}
if(num == 0)
{
newhuiduCount_level[i]=oldhuiduCount_level[i];
}
printf("第%d灰度等级下的灰度值对应的频数为%d.\n",new_level[i],newhuiduCount_level[i]);
}
int x=0;
//求对应新灰度级下的bmp数据
for (i = 0; i < total_xiangsu; i++)
{
for(j = 0; j < level; j++)
{
if(oldhuidu[i] == oldhuidu_level[j]) //i是灰度值i对应的bmp文件的像素点
{
newgrayValue[i] = oldhuidu_level[new_level[j]]; //修改原像素点对应的灰度等级对应的灰度值,并保存在新的像素点-灰度值阵列中
x++;
}
}
}
for (i = 0; i < total_xiangsu; i++)
{
newbmpValue[i]=newgrayValue[i];
}
printf("x=%d.\n",x);
//输出均衡化后的图片
FILE* fop = fopen("C:\\Users\\xinni\\Desktop\\DSP\\four.bmp", "wb");
fwrite(&bmpFileHeader, sizeof(BITMAPFILEHEADER), 1, fop);
fwrite(&bmpInfoHeader, sizeof(BITMAPINFOHEADER), 1, fop);
fwrite(bmpColorTable, sizeof(RGBQUAD), 256, fop);
fwrite(newbmpValue, total_xiangsu, 1, fop);
fclose(fop);
写到这里,靓仔有话要说,之前我也是一直疑惑,介绍完BMP文件的信息之后以及直方图的一些公式,却不知道灰度值具体在BMP文件中的数据是啥,我有朋友给我发了下面提到的一篇CSDN博客上是这样考虑的:他将索引号提取出来之后,再通过寻找索引号找到对应的颜色值(他以蓝色的调色板为例),以这些数据作为他的灰度值,再用公式进行均衡之后,通过循环像素点数,让每一个均衡后的像素点数所对应的灰度值(也即是颜色数据)与调色板进行对应找到新的索引号,并将这些索引号再写进新的BMP文件中。看起来好像是很perfect,他的结果对一些图确实行得通,但是我用的图不知道为啥老是会出现错误,得到的BMP文件也是打不开的。于是我运行看了一下输出的结果,我的图像像素是256×256,但是我最终要写进BMP中的数据却是不止256×256项。之后我打开调色板的数据,发现调色板并不是每个值都是不同的,那我们在进行像素点数循环检测调色板是否匹配进而赋值的时候就有可能出现重复赋值的情况,这样这个数据就会有多的情况。可是令靓仔疑惑的是,他的图竟然可以!!
PS:我吐了,感觉不好的事情都发生在我身上,想起了以前做比赛师兄没问题的方案在我身上就有问题。靓仔语塞。。。可能比较靓仔吧哈哈
四、MATLAB验证之直方图
MATLAB代码如下:
H= imread('C:\Users\xinni\Desktop\DSP\xiong1.bmp');
K= imread('C:\Users\xinni\Desktop\DSP\four.bmp');
if length(size(H))>2
H=rgb2gray(H);
end
if length(size(K))>2
K=rgb2gray(K);
end
subplot(4,2,1);
imshow(H); title('原图');
subplot(4,2,2);
imhist(H); title('原图直方图');
subplot(4,2,3);
imshow(K); title('C得到的图');
subplot(4,2,4);
imhist(K); title('C直方图');
subplot(4,2,5);
H1=adapthisteq(H);
imshow(H1); title('adapthisteq均衡后图');
subplot(4,2,6);
imhist(H1);title('adapthisteq均衡后直方图');
subplot(4,2,7);
H2=histeq(H);
imshow(H2); title('histeq均衡后图');
subplot(4,2,8);
imhist(H1); title('histeq均衡后直方图');
输出结果:
这里需要注意的是,我老师说MATLAB的方法与我们所用的方法有一定的差异,具体的实现结果自然是MATLAB实现出来的结果好看些(确实…)
结语
终于到了吐槽时间了,总体实现方案其实很简单的,只要学会如何在文件中定位每一块的位置,学会用fread、fseek和fwrite,并且知道每一块包含了什么,BMP文件的读取并不困难。当然了,在均衡化处理的过程中,我们使用的方法是这样,可能也有其他的实现方式,建议一起分享哈哈,尽管我得出来的结果似乎并不怎么理想,起码离我的理想目标差了那么 一点(???真的只是一点吗???),但是作为一个很久不写C,对C语言的了解还停留在指针以前的人来说已经很自豪了哈哈。
最后感谢老师的指导以及一些CSDN博客的参考~
参考链接
- BMP百度百科
- 《BMP文件格式详解及实例分析》PDF——梦断
- 图像直方图统计及其C语言实现——A妙手A