一、前言
图像处理中,大部分的处理方法都需要事先把彩色图转换成灰度图才能进行相关的计算、识别。
彩色图转换灰度图的原理如下:
我们知道彩色位图是由R/G/B三个分量组成,其文件存储格式为
BITMAPFILEHEADER+BITMAPINFOHEADER,紧跟后面的可能是:
- 如果是24位真彩图,则每个点是由三个字节分别表示R/G/B,所以这里直接跟着图像的色彩信息;
- 如果是8位(256色),4位(16色),1位(单色)图,则紧跟后面的是调色板数据,一个RGBQUAD类型的数组,其长度由BITMAPINFOHEADER.biClrUsed来决定。
然后后面紧跟的才是图像数据(24位图是真实的图像数据,其他的则是调色板的索引数据)。
灰度图是指只含亮度信息,不含色彩信息的图象,就象我们平时看到的黑白照片:亮度由暗到明,变化是连续的。因此,要表示灰度图,就需要把亮度值进行 量化。通常划分成0到255共256个级别,其中0最暗(全黑),255最亮(全白)。在表示颜色的方法中,除了RGB外,还有一种叫YUV的表示方法, 应用也很多。电视信号中用的就是一种类似于YUV的颜色表示方法。在这种表示方法中,Y分量的物理含义就是亮度,Y分量包含了灰度图的所有信息,只用Y分 量就能完全能够表示出一幅灰度图来。
从 RGB 到 YUV 空间的 Gray 转换公式为:
Gray = R0.299 + G0.587 + B*0.114
在 WINDOWS 中,表示 16 位以上的图和以下的图有点不同; 16 位以下的图使用一个调色板来表示选择具体的颜色,调色板的每个单元是 4 个字节,其中一个透明度;而具体的像素值存储的是索引,分别是 1 、 2 、 4 、 8 位。 16 位以上的图直接使用像素表示颜色。
那么如何将彩色图转换为灰度图呢?
灰度图中有调色板,首先需要确定调色板的具体颜色取值。我们前面提到了,灰度图的三个分量相等。
当转换为 8 位的时候,调色板中有 256 个颜色,每个正好从 0 到 255 个,三个分量都相等。
当转换为 4 位的时候,调色板中 16 个颜色,等间隔平分 255 个颜色值,三个分量都相等。
当转换为 2 位的时候,调色板中 4 个颜色,等间隔平分 255 个颜色,三个分量相等。
当转换为 1 位的时候,调色板中两个颜色,是 0 和 255 ,表示黑和白。
将彩色转换为灰度时候,按照公式计算出对应的值,该值实际上是亮度的级别;亮度从 0 到 255 ;由于不同的位有不同的亮度级别,所以 Y 的具体取值如下:Y = Y/ (1<<(8- 转换的位数 ));
所以,我们要转化成灰度图,并且存储成一幅可以看到的图像,需要做如下转换:
16位以上的图像不带调色板,只需要把图像数据按每个点的位数都转换成相同的灰度值即可
16位以下的图像,则需要修改调色板的数值,并且按照每个点所占位数修改灰度值索引即可。
什么?说了这么多还不明白???
- 24位真彩图 文件信息头+图片信息头+位图数据(就是你普通看的的那种图片)
- 8位灰度图 文件信息头+图片信息头+调色板+位图数据(就是那种黑白相机找出来的那种效果的图片)
二值图 (也叫单值图像)每个像素一个bit ,即黑白图像,每个像素点的值非 0 即 1。
灰度图像 每个像素8bit,范围从 0 ~ 255. 具有调色板,像素值是表项入口。
伪彩图像 每个像素8bit,范围从0-255.具有调色板,像素值是表项入口。
真彩图像 每个像素 24bit ,每个像素由独立的 R,G,B 分量组成,每个分量各占8bit。
我们现在要做的就是把一张24位图
转换成8位灰度图
。
二、灰度化
头文件
# ifndef BMP_H
# define BMP_H
/*位图文件头*/
#pragma pack(1)//单字节对齐
typedef struct tagBITMAPFILEHEADER
{
unsigned char bfType[2];//文件格式
unsigned int bfSize; // 文件大小 以字节为单位(2-5字节)
unsigned short bfReserved1; // 保留,必须设置为0 (6-7字节)
unsigned short bfReserved2; // 保留,必须设置为0 (8-9字节)
unsigned int bfOffBits; // 从文件头到像素数据的偏移 (10-13字节)
}BITMAPFILEHEADER;
#pragma pack()
/*位图信息头*/
#pragma pack(1)
typedef struct tagBITMAPINFOHEADER
{
unsigned int biSize; // 此结构体的大小 (14-17字节)
long biWidth; // 图像的宽 (18-21字节)
long biHeight; // 图像的高 (22-25字节)
unsigned short biPlanes; // 表示bmp图片的平面属,显然显示器只有一个平面,所以恒等于1 (26-27字节)
unsigned short biBitCount; // 一像素所占的位数,(28-29字节)当biBitCount=24时,该BMP图像就是24Bit真彩图,没有调色板项。
unsigned int biCompression; // 说明图象数据压缩的类型,0为不压缩。 (30-33字节)
unsigned int biSizeImage; // 像素数据所占大小, 这个值应该等于上面文件头结构中bfSize-bfOffBits (34-37字节)
long biXPelsPerMeter; // 说明水平分辨率,用象素/米表示。一般为0 (38-41字节)
long biYPelsPerMeter; // 说明垂直分辨率,用象素/米表示。一般为0 (42-45字节)
unsigned int biClrUsed; // 说明位图实际使用的彩色表中的颜色索引数(设为0的话,则说明使用所有调色板项)。 (46-49字节)
unsigned int biClrImportant; // 说明对图象显示有重要影响的颜色索引的数目,如果是0,表示都重要。(50-53字节)
}BITMAPINFOHEADER;
#pragma pack()
/*调色板结构*/
#pragma pack(1)
typedef struct tagRGBQUAD
{
unsigned char rgbBlue; //该颜色的蓝色分量 (值范围为0-255)
unsigned char rgbGreen; //该颜色的绿色分量 (值范围为0-255)
unsigned char rgbRed; //该颜色的红色分量 (值范围为0-255)
unsigned char rgbReserved;// 保留,必须为0
}RGBQUAD;
#pragma pack()
#endif
这个里你看到了#pragma pack(1) #pragma pack()
。这玩意你没见过啊?请看我的另一篇文章:结构体对齐
C文件
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
#include"bmp.h"
int main()
{
unsigned char ImgData[3000][3];//将位图信息转为灰度 存储RGB图像的一行像素点
unsigned char ImgData2[3000];//将灰度图的像素存到一个一维数组中
int i, j, k;
FILE * fpBMP, *fpGray;
BITMAPFILEHEADER *fileHeader;
BITMAPINFOHEADER *infoHeader;
RGBQUAD * ipRGB;
char filename1[20], filename2[20];
printf("输入图像文件名:");
scanf("%s", filename1);
if ((fpBMP = fopen(filename1, "rb")) == NULL)
{
printf("打开图片失败");
exit(0);
}
printf("输出图像文件名:");
scanf("%s", filename2);
if ((fpGray = fopen(filename2, "wb")) == NULL)
{
printf("创建图片失败");
exit(0);
}
//给定义的结构体变量申请这个结构体大小的内存空间
fileHeader = (BITMAPFILEHEADER *)malloc(sizeof(BITMAPFILEHEADER));
infoHeader = (BITMAPINFOHEADER *)malloc(sizeof(BITMAPINFOHEADER));
//从bmp文件中读取数据块给文件信息头和图片信息头
fread(fileHeader, sizeof(BITMAPFILEHEADER), 1, fpBMP);
fread(infoHeader, sizeof(BITMAPINFOHEADER), 1, fpBMP);
//经过这两条程序把BMP图像的信息头、文件头赋给fileHeader和infoHeader变量,可以根据fileHeader和infoHeader得到图像的各种属性。
printf("原始图片每个像素的位数:%d\n" ,infoHeader->biBitCount);
printf("原始图片每个像素像素数据偏移:%d\n" ,fileHeader->bfOffBits);
//修改信息头
//信息头共有11部分,灰度化时需要修改两部分
infoHeader->biBitCount = 8;//将24位真彩图转换成8位灰度图
infoHeader->biSizeImage = ((infoHeader->biWidth * 3 + 3) / 4) * 4 * infoHeader->biHeight;//24Bit真彩图每一行占的实际字节数
//修改文件头
//文件头共有5部分,灰度化时需要修改两部分
fileHeader->bfOffBits = sizeof( BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD);
fileHeader->bfSize = fileHeader->bfOffBits + infoHeader->biSizeImage;
printf("修改后的图片每个像素的位数:%d\n" ,infoHeader->biBitCount);
printf("修改后的图片每个像素数据偏移:%d\n" ,fileHeader->bfOffBits);
//创建调色版 应为灰度图像的调色板是 R=G=B.而真彩图的三值不相等
ipRGB = (RGBQUAD *)malloc(256 * sizeof(RGBQUAD));
for (i = 0; i < 256; i++)
{
ipRGB[i].rgbBlue = ipRGB[i].rgbGreen = ipRGB[i].rgbRed = i;
}
//读取BMP图像的信息头、文件头、BMP调色板到新建的图片
fwrite(fileHeader, sizeof(BITMAPFILEHEADER), 1, fpGray);
fwrite(infoHeader, sizeof(BITMAPINFOHEADER), 1, fpGray);
fwrite(ipRGB , sizeof(RGBQUAD) , 256, fpGray);
//读取RGB图像素并转换为灰度值
for (i = 0; i < infoHeader->biHeight; i++)//一行一行的扫描
{
//24Bit真彩图每一行占的实际字节数:[(biWidth*3+3)/4*4] 故biHeight行每行扫描[(biWidth*3+3)/4*4]次
for (j = 0; j < (infoHeader->biWidth + 3) / 4 * 4; j++)//写BMP图像的位图数据部分
{
for (k = 0; k < 3; k++)
fread(&ImgData[j][k], 1, 1, fpBMP);////每次只读取一个字节,存入数组
}
//修改位图数据部分这部分主要是由原真彩图的rgbRed、rgbGreen、rgbBlue分量值得到灰度图像的灰度值Y,可以用下面公式得到:
for (j = 0; j < (infoHeader->biWidth + 3) / 4 * 4; j++)
{
ImgData2[j] = (int)((float)ImgData[j][0] * 0.114 +
(float)ImgData[j][1] * 0.587 +
(float)ImgData[j][2] * 0.299 );
}
//将灰度图信息写入
fwrite(ImgData2, j, 1, fpGray);//按顺序写入BMP图像的各个部分
}
free(fileHeader);
free(infoHeader);
free(ipRGB);
fclose(fpBMP);
fclose(fpGray);
printf("bmp图片背景去除完成\n");
return 0;
}
至此24位图就转换位8位灰度图就完成了。
看一下效果。
这里要输入图片的路径,建议直接放到工程目录一起。放到其他地方也可以,只要把图片的路径填对就行.我也可以输入 D:\c_test\bmp-cleancover\1.bmp
。因为我的图片1.bmp
是放在D盘
的c_test
的bmp-cleancover
文件夹下。
对比一下效果:
怎么感觉不一样?
我以为我要完成的是这种效果。请教了别人,告诉我要二值化,我这只是灰度化。二值化就是图片非黑即白。
三、二值化
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
#include"bmp.h"
int main()
{
/*变量声明*/
FILE *fpBMP,*fpTwoValue;//源文件fpBMP,目标文件fpTwoValue
char filename1[20], filename2[20];
BITMAPFILEHEADER *fileHeader;//位图文件头
BITMAPINFOHEADER *infoHeader;//位图信息头
RGBQUAD *ipRGB;//调色板
int i,j,k=0;
unsigned char *a;//存储源图每行像素值
unsigned char b;//存储每个像素的灰度值或二值
unsigned char *c;//存储每行像素的二值
printf("输入图像文件名:");
scanf("%s", filename1);
if ((fpBMP = fopen(filename1, "rb")) == NULL)
{
printf("打开图片失败");
exit(0);
}
printf("输出图像文件名:");
scanf("%s", filename2);
if ((fpTwoValue = fopen(filename2, "wb")) == NULL)
{
printf("创建图片失败");
exit(0);
}
/********************************************************************/
/*创建位图文件头,信息头,调色板*/
fileHeader=(BITMAPFILEHEADER *)malloc(sizeof(BITMAPFILEHEADER));
infoHeader=(BITMAPINFOHEADER *)malloc(sizeof(BITMAPINFOHEADER));
ipRGB=(RGBQUAD *)malloc(2*sizeof(RGBQUAD));
/*读入源位图文件头和信息头*/
fread(fileHeader,sizeof(BITMAPFILEHEADER),1,fpBMP);
fread(infoHeader,sizeof(BITMAPINFOHEADER),1,fpBMP);
//经过这两条程序把BMP图像的信息头、文件头赋给fileHeader和infoHeader变量,可以根据fileHeader和infoHeader得到图像的各种属性。
printf("原始图片每个像素的位数:%d\n" ,infoHeader->biBitCount);
printf("原始图片每个像素像素数据偏移:%d\n" ,fileHeader->bfOffBits);
//修改信息头
//信息头共有11部分,灰度化时需要修改4部分
infoHeader->biBitCount=8;//转换成二值图后,颜色深度由24位变为8位
infoHeader->biSizeImage=((infoHeader->biWidth+3)/4)*4*infoHeader->biHeight;//每个像素由三字节变为单字节,同时每行像素要四字节对齐
infoHeader->biClrUsed=2;//颜色索引表数量,二值图为2
infoHeader->biClrImportant=0;//重要颜色索引为0,表示都重要
//修改文件头
//文件头共有5部分,灰度化时需要修改两部分
fileHeader->bfOffBits=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)+2*sizeof(RGBQUAD);//数据区偏移量,等于文件头,信息头,索引表的大小之和
fileHeader->bfSize=fileHeader->bfOffBits+infoHeader->biSizeImage;//文件大小,等于偏移量加上数据区大小
ipRGB[1].rgbBlue=ipRGB[1].rgbGreen=ipRGB[1].rgbRed=ipRGB[1].rgbReserved=0;//调色板颜色为黑色对应的索引为0
ipRGB[0].rgbBlue=ipRGB[0].rgbGreen=ipRGB[0].rgbRed=190;//白色对应的索引为150-255
ipRGB[1].rgbReserved=0;
printf("修改后的图片每个像素的位数:%d\n" ,infoHeader->biBitCount);
printf("修改后的图片每个像素数据偏移:%d\n" ,fileHeader->bfOffBits);
/********************************************************************/
//读取BMP图像的信息头、文件头、BMP调色板到新建的图片
fwrite(fileHeader,sizeof(BITMAPFILEHEADER),1,fpTwoValue);
fwrite(infoHeader,sizeof(BITMAPINFOHEADER),1,fpTwoValue);
fwrite(ipRGB,2*sizeof(RGBQUAD),1,fpTwoValue);
/*将彩色图转为二值图*/
a=(unsigned char *)malloc((infoHeader->biWidth*3+3)/4*4);//给变量a申请源图每行像素所占大小的空间,考虑四字节对齐问题
c=(unsigned char *)malloc((infoHeader->biWidth+3)/4*4);//给变量c申请目标图每行像素所占大小的空间,同样四字节对齐
for(i=0;i<infoHeader->biHeight;i++)//遍历图像每行的循环
{
for(j=0;j<((infoHeader->biWidth*3+3)/4*4);j++)//遍历每行中每个字节的循环
{
fread(a+j,1,1,fpBMP);//将源图每行的每一个字节读入变量a所指向的内存空间
}
for(j=0;j<infoHeader->biWidth;j++)//循环像素宽度次,就不会计算读入四字节填充位
{
b=(int)(0.114*(float)a[k]+0.587*(float)a[k+1]+0.299*(float)a[k+2]);//a中每三个字节分别代表BGR分量,乘上不同权值转化为灰度值
if(160<=(int)b) //将灰度值转化为二值,这里选取的阈值为160-190都可以
b=1;
else b=0;
c[j]=b; //存储每行的二值
k+=3;
}
fwrite(c,(infoHeader->biWidth+3)/4*4,1,fpTwoValue);//将二值像素四字节填充写入文件,填充位没有初始化,为随机值
k=0;
}
/*释放内存空间,关闭文件*/
free(fileHeader);
free(infoHeader);
free(ipRGB);
free(a);
free(c);
fclose(fpBMP);
fclose(fpTwoValue);
printf("bmp图片背景去除完成\n");
return 0;
}
只要把C文件提成上面这个就可以了。头文件不用改。因为bmp图片的结构体成员变量都是固定不变的,只需要改参数就行了。
这回再看效果就差不多可以了。可以明显的看出背景已经去除了。
直接把C文件和H文件复制到你的程序就可以了。程序的原理大家还要细细体会,遇到问题就去在网上搜索,涉及结构体和文件指针。我也是看了很多资料。
哎,码字不易,路过的如果对你有帮助,点这个赞吧>>__<<。
关注微信公众号:[<font=red>果果小师弟],获取更多精彩内容!