C语言实现二值图像模拟灰度显示:图案法与抖动法
在硬件资源受限或仅支持二值显示的终端上,如何利用C语言将传统的256级灰度或彩色BMP图像转化为二值图像?本文将详细解析图案法(Pattern/Dithering)与Floyd-Steinberg误差扩散抖动法的原理,并提供完整的C语言代码实现,包括BMP文件头解析、内存处理及文件输出。
结果目标与准备条件
目标是将输入的彩色或灰度BMP图像(通常为24位或8位),转换为1位深度的二值图像,同时保留视觉上的灰度过渡感。这需要处理BMP文件的二进制结构,包括文件头、信息头、调色板以及实际的像素数据。
第1步:理解BMP数据结构
BMP文件由头部信息和像素数据组成。在处理像素之前,必须正确读取并解析以下关键结构:
BITMAPFILEHEADER:包含文件类型、大小、偏移量等。BITMAPINFOHEADER:包含图像宽度、高度、颜色位数、压缩方式等。RGBQUAD:调色板数组(对于索引色图像)。Image Data:实际的像素字节数组,存储顺序通常为自底向上、自左向右。
注意:代码中使用了 #pragma pack(1) 确保结构体内存对齐紧凑,直接匹配BMP文件中的二进制布局。
方法一:Limb图案法(Bayer矩阵抖动)
图案法(Patterning)的核心思想是利用黑白点密度的比例来模拟灰度。Limb在1969年提出的算法通过递归生成标准图案矩阵。最常用的是 M3 矩阵,即8x8的Bayer抖动表。该矩阵将0-63的值分散排列,使得相邻像素的阈值不同,从而产生纹理感。
8x8 Bayer矩阵示例如下:
通过递归生成的更大矩阵如M4(16x16)具有更细腻的灰度表现力,但计算量稍大。对于大多数应用场景,8x8矩阵足以提供不错的视觉效果。
递归关系说明:
其中 M_n 为生成的矩阵,U_n 为全1矩阵。M2(4x4)和M3(8x8)的具体数值决定了每个像素点的判定阈值。
第2步:构建Bayer矩阵并应用阈值比较
在实际操作中,我们只需硬编码8x8的Bayer矩阵。对于输入图像的每个像素,将其灰度值右移2位(从256级映射到0-63范围),然后与对应坐标的Bayer矩阵值进行比较。若大于矩阵值则设为白(255),否则设为黑(0)。
标准8x8 Bayer矩阵定义(0-63范围):
第3步:C语言代码实现 - 图案法
以下是完整的C语言实现,包含BMP读取、Bayer矩阵处理及二值化写入。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#pragma pack(1) //全紧凑模式
typedef struct {
unsigned char bfType[2];
unsigned long bfSize;
unsigned short bfReserved1;
unsigned short bfReserved2;
unsigned long bfOffBits;
}bitmapFileHeader;
typedef struct {
unsigned long biSize;
unsigned long biWidth;
unsigned long biHeight;
unsigned short biPlanes;
unsigned short biBitCount;
unsigned long biCompression;
unsigned long biSizeImage;
long biXPixPerMeter;
long biYPixPerMeter;
unsigned long biClrUsed;
unsigned long biClrImportant;
}bitmapInfoHeader;
typedef struct{
unsigned char rgbBlue;
unsigned char rgbGreen;
unsigned char rgbRed;
unsigned char rgbReserved;
}rgbQUAD;
typedef struct{
bitmapFileHeader bfHeader;
bitmapInfoHeader biHeader;
rgbQUAD palette[256];
unsigned char *imgData;
}bmp;
int main(){
FILE *fp;
// 请确保此路径下有有效的BMP测试文件
if((fp=fopen("d:\\Temp\\test_gray.bmp","rb"))==NULL){
perror("can not open file!");
return -1;
}
// 读入文件头和信息头
bitmapFileHeader bfHeader;
fread(&bfHeader,14,1,fp);
bitmapInfoHeader biHeader;
fread(&biHeader,40,1,fp);
int imSize=biHeader.biSizeImage;
int width=biHeader.biWidth;
int height=biHeader.biHeight;
int bitCount=biHeader.biBitCount;
// 计算每行字节数(需4字节对齐)
int lineBytes=(width*bitCount+31)/32*4;
fseek(fp,bfHeader.bfOffBits,SEEK_SET);
unsigned char*imageData=(unsigned char*)malloc(imSize*sizeof(unsigned char));
fread(imageData,imSize*sizeof(unsigned char),1,fp);
fclose(fp);
bmp b;
memcpy(&(b.bfHeader),&bfHeader,sizeof(bfHeader));
memcpy(&(b.biHeader),&biHeader,sizeof(biHeader));
b.imgData=(unsigned char*)malloc(sizeof(unsigned char)*imSize);
memset(b.imgData,0,sizeof(unsigned char)*imSize);
// 设置灰度调色板(256级灰阶)
for(int i=0;i<256;i++){
b.palette[i].rgbBlue=i;
b.palette[i].rgbGreen=i;
b.palette[i].rgbRed=i;
}
int i,j,temp;
// 8x8 Bayer Matrix
unsigned char bayer[8][8]={
0,32,8,40,2,34,10,42,
48,16,56,24,50,18,58,26,
12,44,4,36,14,46,6,38,
60,28,52,20,62,30,54,22,
3,35,11,43,1,33,9,41,
51,19,59,27,49,17,57,25,
15,47,7,39,13,45,5,37,
63,31,55,23,61,29,53,21
};
// 遍历像素进行二值化处理
for(i=0;i<height;i++){
for(j=0;j<width;j++){
temp=imageData[lineBytes*i+j];
// 右移2位将0-255映射到0-63,与Bayer矩阵值比较
if((temp>>2)>bayer[i&7][j&7])
b.imgData[lineBytes*i+j]=255;
else
b.imgData[lineBytes*i+j]=0;
}
}
char savePath[]="D:\\Temp\\save_test.bmp";
FILE *f_save=fopen(savePath,"wb");
if(f_save==NULL){
perror("can not open file!");
return -2;
}
fwrite(&b.bfHeader,sizeof(bitmapFileHeader),1,f_save);
fwrite(&b.biHeader,sizeof(bitmapInfoHeader),1,f_save);
fwrite(&b.palette,1024,1,f_save);
fwrite(b.imgData,sizeof(unsigned char)*b.biHeader.biSizeImage,1,f_save);
fclose(f_save);
free(imageData);
free(b.imgData);
getchar();
return 0;
}
处理后的视觉效果如下所示,图像通过噪点密度呈现灰度层次:
方法二:Floyd-Steinberg 误差扩散抖动法
相比图案法,Floyd-Steinberg抖动法能提供更平滑的灰度过渡和更少的纹理噪点。其核心原理是将当前像素量化产生的误差,按比例分配给尚未处理的右侧、下方及右下方的邻居像素。
算法逻辑:
- 设定阈值
t = 127.5(对于256级灰度)。 - 若当前像素
g > t,则设为白色(255),误差e = g - 255(负值)。 - 若当前像素
g <= t,则设为黑色(0),误差e = g - 0(正值)。 - 将误差扩散到周围像素:
- 右侧像素:增加
3/8 * e - 正下方像素:增加
3/8 * e - 右下方像素:增加
1/4 * e
第4步:C语言代码实现 - 抖动法
以下代码实现了上述误差扩散逻辑。关键在于需要维护一个缓冲区来存储已处理但未完全传播完误差的像素值,因此通常需要对原数据进行拷贝处理。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#pragma pack(1) //全紧凑模式
typedef struct {
unsigned char bfType[2];
unsigned long bfSize;
unsigned short bfReserved1;
unsigned short bfReserved2;
unsigned long bfOffBits;
}bitmapFileHeader;
typedef struct {
unsigned long biSize;
unsigned long biWidth;
unsigned long biHeight;
unsigned short biPlanes;
unsigned short biBitCount;
unsigned long biCompression;
unsigned long biSizeImage;
long biXPixPerMeter;
long biYPixPerMeter;
unsigned long biClrUsed;
unsigned long biClrImportant;
}bitmapInfoHeader;
typedef struct{
unsigned char rgbBlue;
unsigned char rgbGreen;
unsigned char rgbRed;
unsigned char rgbReserved;
}rgbQUAD;
typedef struct{
bitmapFileHeader bfHeader;
bitmapInfoHeader biHeader;
rgbQUAD palette[256];
unsigned char *imgData;
}bmp;
int main(){
FILE *fp;
if((fp=fopen("d:\\Temp\\test_gray.bmp","rb"))==NULL){
perror("can not open file!");
return -1;
}
bitmapFileHeader bfHeader;
fread(&bfHeader,14,1,fp);
bitmapInfoHeader biHeader;
fread(&biHeader,40,1,fp);
int imSize=biHeader.biSizeImage;
int width=biHeader.biWidth;
int height=biHeader.biHeight;
int bitCount=biHeader.biBitCount;
int lineBytes=(width*bitCount+31)/32*4;
fseek(fp,bfHeader.bfOffBits,SEEK_SET);
unsigned char*imageData=(unsigned char*)malloc(imSize*sizeof(unsigned char));
fread(imageData,imSize*sizeof(unsigned char),1,fp);
fclose(fp);
bmp b;
memcpy(&(b.bfHeader),&bfHeader,sizeof(bfHeader));
memcpy(&(b.biHeader),&biHeader,sizeof(biHeader));
b.imgData=(unsigned char*)malloc(sizeof(unsigned char)*imSize);
memset(b.imgData,0,sizeof(unsigned char)*imSize);
// 初始化灰度调色板
for(int i=0;i<256;i++){
b.palette[i].rgbBlue=i;
b.palette[i].rgbGreen=i;
b.palette[i].rgbRed=i;
}
// 先将原始数据复制到工作缓冲区,以便进行误差扩散
for(int k=0;k<imSize;k++){
b.imgData[k] = imageData[k];
}
int i,j,temp;
double e,f;
// 遍历每个像素进行Floyd-Steinberg抖动
for(i=0;i<height;i++){
for(j=0;j<width;j++){
temp=b.imgData[lineBytes*i+j];
// 阈值判断与二值化
if(temp>128){
b.imgData[lineBytes*i+j]=255;
e=(double)(temp-255); // 误差为负
}
else{
b.imgData[lineBytes*i+j]=0;
e=(double)temp; // 误差为正
}
// 向右传播 3/8
if(j<width-1){
f=b.imgData[lineBytes*i+j+1];
f+=3.0/8.0*e;
b.imgData[lineBytes*i+j+1]=(unsigned char)f;
}
// 向下传播 3/8
if(i<height-1){
f=b.imgData[lineBytes*(i+1)+j];
f+=3.0/8.0*e;
b.imgData[lineBytes*(i+1)+j]=(unsigned char)f;
// 向右下传播 1/4
f=b.imgData[lineBytes*(i+1)+j+1];
f+=1.0/4.0*e;
b.imgData[lineBytes*(i+1)+j+1]=(unsigned char)f;
}
}
}
char savePath[]="D:\\Temp\\save_test.bmp";
FILE *f_save=fopen(savePath,"wb");
if(f_save==NULL){
perror("can not open file!");
return -2;
}
fwrite(&b.bfHeader,sizeof(bitmapFileHeader),1,f_save);
fwrite(&b.biHeader,sizeof(bitmapInfoHeader),1,f_save);
fwrite(&b.palette,1024,1,f_save);
fwrite(b.imgData,sizeof(unsigned char)*b.biHeader.biSizeImage,1,f_save);
fclose(f_save);
free(imageData);
free(b.imgData);
getchar();
return 0;
}
Floyd-Steinberg算法处理后的图像效果更加平滑,人眼识别的灰度等级更多:
完成判断与排错
完成判断:
- 程序成功运行无报错。
- 输出的
save_test.bmp文件可正常打开,且内容为由黑白像素组成的二值图像。 - 通过观察图像,能够隐约看到原图的轮廓和明暗变化。
常见排错:
- 图像倒置: BMP格式通常是自底向上存储的。如果输出的图像上下颠倒,检查像素遍历顺序或文件保存时是否忽略了这种存储特性。上述代码遵循了标准的BMP写入流程,通常无需额外处理反转,但若自定义绘制需注意。
注:上述简单代码直接复用原图尺寸和数据块大小,适用于标准未压缩BMP。若涉及不同色彩深度转换,需额外处理行对齐填充(Padding)。 - 乱码或打不开: 检查BMP文件头是否正确写入,特别是
bmiHeader.biSizeImage是否与分配的imgData大小一致。 - 路径权限问题: 示例中使用
D:\Temp\,请确保该目录存在且有写入权限。
简要总结
本文展示了如何使用C语言结合BMP文件格式规范,通过Limb图案法和Floyd-Steinberg误差扩散算法,将灰度图像转换为二值图像。前者实现简单,适合对性能要求极高或纹理接受度高的场景;后者视觉上更平滑,是通用的抗锯齿和灰度模拟方案。以上内容就是《C语言实现二值图像模拟灰度显示:图案法与抖动法》的详细内容,更多关于C语言图像处理技术的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













