最近闲得蛋疼,所以诞生了搞些大新闻的念头。
由于我不擅长脚本语言,因此费力不讨好地去用C++来完成这项工作。
UPDATE 2016-03-18: 修复失效的图片链接
BMP简略介绍
位图一共有两种类型,即:设备相关位图(DDB)和设备无关位图(DIB). DDB位图在早期的Windows系统(Windows 3.0以前)中是很普遍的,事实上它也是唯一的。然而,随着显示器制造技术的进步,以及显示设备的多样化,DDB位图的一些固有的问题开始浮现出来了。比如,它不能够存储(或者说获取)创建这张图片的原始设备的分辨率,这样,应用程序就不能快速的判断客户机的显示设备是否适合显示这张图片。为了解决这一难题,微软创建了DIB位图格式。
设备无关位图(Device-Independent Bitmap)包含下列的颜色和尺寸信息:
原始设备(即创建图片的设备)的颜色格式
原始设备的分辨率
原始设备的调色板
一个位数组,由红、绿、蓝(RGB)三个值代表一个像素
一个数组压缩标志,用于表明数据的压缩方案(如果需要的话)
BMP文件结构
位图文件由以下结构体依次构成:
结构体名称 | 可选 | 大小 | 用途 | 备注 |
---|---|---|---|---|
位图文件头 | 否 | 14字节 | 存储位图文件通用信息 | 仅在读取文件时有用 |
DIB头 | 否 | 固定(存在7种不同版本) | 存储位图详细信息及像素格式 | 紧接在位图文件头后 |
附加位掩码 | 是 | 3或4 DWORD(12或16字节) | 定义像素格式 | 仅在DIB头是BITMAPINFOHEADER时存在 |
调色板 | 见备注 | 可变 | 定义图像数据(像素数组)所用颜色 | 色深≤ 8时不能省略 |
填充区A | 是 | 可变 | 结构体对齐 | 位图文件头中像素数组偏移量的产物 |
像素数组 | 否 | 可变 | 定义实际的像素数值 | 像素数据在DIB头和附加位掩码中定义。像素数组中每行均以4字节对齐 |
填充区B | 是 | 可变 | 结构体对齐 | DIB头中ICC色彩特性数据偏移量的产物 |
ICC色彩特性数据 | 是 | 可变 | 定义色彩特性 | 可以包含外部文件路径,由该文件来定义色彩特性 |
BITMAPFILEHEADER
BITMAPFILEHEADER
位于文件的开头,存储着整个文件的概要信息。MSDN上,该结构体的定义如下:
1 | typedef struct tagBITMAPFILEHEADER { |
注:WORD
, DWORD
为微软MFC的经典类型别名,类型的要求分别是16位无符号整型和32位无符号整型。
各变量作用如下:
bfType
: 文件签名,标识文件类型。这两个字节内容是0x42 0x4D, 如果以char
的方式来读取的话要求为'B'
,'M'
. 根据标准的不同,也可以是其他内容(见附表)。bfSize
: 文件大小,记录整个文件的大小(单位:字节)。bfReserved1
,bfReserved2
: 保留位,其内容必须是0(MSDN); 取决于具体创建图形文件的程序(Wikipedia).bfOffBits
: 偏移量(单位:字节),标识从BITMAPFILEHEADER
结构体的开头到位图像素数据的偏移量。
bfType的可能值
值 | 含义 |
---|---|
BM | Windows 3.1x, 95, NT, … etc. |
BA | OS/2 struct Bitmap Array |
CI | OS/2 struct Color Icon |
CP | OS/2 const Color Pointer |
IC | OS/2 struct Icon |
PT | OS/2 Pointer |
BITMAPINFOHEADER
紧跟着BITMAPFILEHEADER
的就是位图信息头,主要有BITMAPINFOHEADER
, BITMAPV4HEADER
, 以及BITMAPV5HEADER
这几种。这里我们介绍使用最广的BITMAPINFOHEADER
结构体。MSDN上定义如下:
1 | typedef struct tagBITMAPINFOHEADER { |
注:LONG
的要求为32位有符号整型。
各变量作用如下:
biSize
: 该结构体需要的字节数。biWidth
: 位图的宽度(单位:像素)。 如果biCompression
为BI_JPEG
或BI_PNG
, 那么biWidth
表示解压缩后图像的宽度。biHeight
: 位图的高度(单位:像素)。 如果为正,位图像素就是从下往上,从左往右存储,原点在左下角。如果为负,则是从上至下存储。此时,biCompression
必须为BI_RGB
或BI_BITFIELDS
. 从上至下存储的位图不能压缩。 如果biCompression
为BI_JPEG
或BI_PNG
, 那么biHeight
表示解压缩后图像的高度。biPlanes
: 目标设备的颜色面板值数量。该值必须为1.biBitCount
: 每像素的位(bit)数。即图像色深。该成员变量决定了位图的最大色彩数。该值必须为表格内容之一(表格附后)。biCompression
: 表示从下至上存储的位图的压缩方式。具体值见下表(表格附后)。biSizeImage
: 图像的大小(单位:字节),无压缩位图该值可能设为0. 如果biCompression
为BI_JPEG
或BI_PNG
, 那么biSizeImage
表示对应JPEG或PNG图像的缓冲区大小。biXPelsPerMeter
: 目标设备位图的横向分辨率(单位:像素/米)。程序可根据该值来选择合适大小的位图来显示。biYPelsPerMeter
: 目标设备位图的纵向分辨率(单位:像素/米)。biClrUsed
: 表示该位图中使用过的颜色索引数量。如果该值为0, 那么位图使用的是biCompression
压缩模式下对应biBitCount
的最大色彩数。 如果该值非0且biBitCount
小于16, 那么biClrUsed
表示图像引擎或设备驱动访问的色彩数。如果biBitCount
大于等于16, 那么biClrUsed
表示颜色表大小来优化系统调色板的性能。如果biBitCount
等于16或32, 优化过的调色板紧跟着接下来的3个DWORD
掩码后开始。 如果位图阵列紧跟在BITMAPINFO
后,那么它是填充位图(packed bitmap). 填充位图是由单个指针引用。填充位图要求该biClrUsed
必须是0或颜色表的实际大小。biClrImportant
: 为显示该位图所需要的颜色索引数量。如果该值为0, 那么所有颜色都是需要的。
biBitCount的可能值
值 | 含义 |
---|---|
0 | 色深由内含的JPEG或PNG格式图像显式确定或隐含 |
1 | 位图是单色图。BITMAPINFO 的bmiColors 包含两个条目。位图的每一bit表示一个像素,如果是0则像素颜色是bmiColors 的第一个条目内容;其余情况是第二个条目内容 |
4 | 位图色彩数量最大为16, bmiColors 包含16个条目。每四bit表示一个像素 |
8 | 位图色彩数量最大为256, bmiColors 包含256个条目。每一字节表示一个像素 |
16 | 位图色彩数量最大为$2^{16}$. 如果biCompression 为BI_RGB , 那么bmiColors 为NULL . 每一WORD 表示一个像素 |
24 | 位图色彩数量最大为$2^{24}$, 并且bmiColors 为NULL . 每3字节表示一个像素,每一字节分别表示一个像素的BGR分量 |
32 | 位图色彩数量最大为$2^{32}$. 如果biCompression 为BI_RGB , 那么bmiColors 为NULL . 每一DWORD 表示一个像素 |
biCompression的可能值
值 | 标识 | 压缩方法 | 备注 |
---|---|---|---|
0 | BI_RGB | 无 | 最常见 |
1 | BI_RLE8 | RLE8位/像素 | 只能用于格式为8位/像素的位图 |
2 | BI_RLE4 | RLE4位/像素 | 只能用于格式为4位/像素的位图 |
3 | BI_BITFIELDS | 位字段或者霍夫曼1D压缩(BITMAPCOREHEADER2) | 像素格式由位掩码指定,或位图经过霍夫曼1D压缩(BITMAPCOREHEADER2) |
4 | BI_JPEG | JPEG或RLE-24压缩(BITMAPCOREHEADER2) | 位图包含JPEG图像或经过RLE-24压缩(BITMAPCOREHEADER2) |
5 | BI_PNG | PNG | 位图包含PNG图像 |
6 | BI_ALPHABITFIELDS | 位字段 | 针对WindowsCE.NET4.0及之后版本 |
BITMAPINFO
该结构体包含了一个DIB的主要信息和色彩信息。MSDN定义如下:
1 | typedef struct tagBITMAPINFO { |
各变量作用如下:
bmiHeader
: 见上一部分bmiColors
: 一个bmiColors
包含以下二者之一:- 一个
RGBQUAD
的数组,数组元素组成了颜色表。 - 一个16位无符号整型的数组,元素是本地调色板的颜色索引。
- 一个
RGBQUAD
该结构体描述BGR色彩分量值。MSDN定义如下:
1 | typedef struct tagRGBQUAD { |
注: BYTE
的要求是宽为1字节的类型。一般为typedef unsigned char BYTE;
需要注意的是它的真实顺序是BGR而不是RGB.
读取BMP文件
有了上面的资料我们就能很快地写出读取文件的代码。
首先准备好两个文件头结构体,注意要禁止编译器自动对齐。在GCC/Clang下可以用__attribute__((packed))
, VS下可以用#pragma pack(1)
.
1 |
|
然后可以写一个简单的读取程序。除第一个参数表示文件路径外,其它的均是以指针形式返回的返回值。
我们直接为每个像素分配3字节的空间来存储RGB信息,而非使用RGBQUAD
结构体。为简单起见,假设我们读取的BMP文件都是24位真彩色的,无压缩,从下至上存储。
1 |
|
灰度转换
现在我们得到了每个像素的RGB信息(上一步得到的data
还需要经过处理变成RGB顺序),现在将其转换成灰度。我们使用这样的一个公式:
$$ Luminace = 0.299 Red + 0.587 Green + 0.114 Blue $$
最简单的一个写法如下:1 | for(auto i=0U;i<size;i+=3) |
最后加入+ 0.5
是为了实现四舍五入而不是全部截断。
由于涉及到了浮点数,导致比整型之间的乘除法慢了很多。因此,我们把公式做个近似,变成了:
$$ Luminance = { {2 Red + 5 Green + Blue} \over 8} $$
在此基础上使用移位运算优化,整个变成了:
1 | for(auto i=0U;i<size;i+=3){ |
转换到ASCII字符
参考资料中给出了两种思路,在此只简单介绍其中一种。
该方法的基本思路是,针对每一个点(一些像素组成的小区域),计算它平均灰度强度然后将其替换为强度差不多的字符。基于此我们需要一组字符,记为map
. 其强度我们可以采取线性分布,即满足:
$$ \begin{aligned} intensity(map_i) = intensity(map_{i-1})+Constant \end{aligned} $$
然后选取字符时可以像查表一样:
$$ character=map_{intensity(dot) \over Constant} $$
因此,大致步骤如下:
将图片平均分割为像素点或者是(矩形)小区域
计算每个部分的灰度强度
将每个部分替换为与它灰度强度最相近的字符
我们所选取的字符组最好是强度均匀分布。刚上手时可以使用const char *map=" .,:;ox%#@";
, 并按照强度递减排列处理。使用这个map
的话,选取对应的字符的代码则是char c=map[(255-intensity(dot))*strlen(map)/256];
.简略代码如下:
1 |
|
转换的效果可以看图: