BMP文件格式解析(带颜色表)及Verilog的AXI-Stream接入仿真(一)
在本文中你将可能看到:
- 带颜色表(掩码)的BMP文件解析
- Verilog读取BMP文件并输出rgb888格式到文件(非AXIS)
其中,rgb1,rgb4,rgb8,rgb888经验证,rgb2, rgb565,rgb555,rgb32(argb), rgb16(bit fields)写了代码没验证。
另,本文默认BMP为Windows NT, 3.1x 版本,输入图片无压缩,长宽均为4倍数(没做压缩对齐)。
下一篇文中会介绍将本次读出来的数据转换成axis。
文章目录
BMP文件格式解析
在BMP文件中包括4部分:
- BMP文件头
- DIB文件头
- 颜色表、掩码
- 图像数据
以小端(little endian)方式存储,以一张1080p图片为例,每一部分包含:
网上查到的相关资料(传到verilog的)基本都是跑rgb888的。实际上在BMP文件格式中,只有当比特数/像素=16(rgb),24,32的时候,像素才是源数据,否则像素数据为对应颜色表的索引。
BMP文件头
name(size) | description | addr |
---|---|---|
header field(2B) | 文件头‘BM’ | 0000 |
size(4B) | 文件大小 | 0002 |
Reserved1(2B) | 预留1 | 0006 |
Reserved2(2B) | 预留1 | 0008 |
bfOffBits(4B) | 像素流开始位置 | 000A |
在BMP文件头中有两个字段比较重要:文件大小: size(4B)和bfOffBits(4B) 。bfOffBits指示像素流的地址头,所以如果是RGB24和RGB32可以知道长宽像素数之后,直接使用这个字段往后跳读就可以了。如果是RGB1/4/8也可以用这个字段获取颜色表的地址段。
bitmap information header
在这个字段中交代的是图像的规格和存储方式:
name(size) | description | addr |
---|---|---|
DIB_size(4B) | DIB字段大小 | 000E |
biWidth(4B) | 图像宽度 | 0012 |
biHeight(4B) | 图像高度(符号数) | 0016 |
biPlanes(2B) | 目标设备说明颜色平面数 | 001A |
biBitCount(2B) | 比特数/像素数 | 001C |
biCompression(4B) | 图像的压缩类型 | 001E |
biSizeImage(4B) | 位图数据的大小,当用BI_RGB格式时,可以设置为0 | 0022 |
biXPelsPerMeter(4B) | 横向分辨率(像素/米) | 0026 |
biYPelsPerMeter(4B) | 纵向分辨率(像素/米) | 002A |
biClrUsed(4B) | 调色板中的颜色数量 | 002E |
biClrImportant(4B) | 重要颜色数量 | 0032 |
其中需要说明的有以下几个:
- biHeight:图像高度是一个符号数,在他是正数时表示图像是从左下一直扫描到右上的,当他是负数是说明图像上下颠倒,即从左上扫描到右下。这个反人类的设计大家可以用“坐标轴”的概念去理解。
- biBitCount:比特数/像素数 ,举个例子,当他为4时,一个像素的占4个比特,即为RGB4。4个bit表示颜色空间最多只有$ 2^4 = 16 $种颜色可选,对应的颜色信息存在颜色表中。
- biCompression(4B) :图像的压缩类型 ,虽然本文讲的都是无压缩状态的,但是有两个特殊情况,当biBitCount为16或32时,图像可以选择使用掩码来决定每一种颜色(透明度)的位宽,也可以直接输出像素数据。以16为例,即存在熟悉的RGB565掩码存储和直接输出RGB555(最高位为0)两种方式。在biCompression为0时选择“不压缩”,在其为3时选择掩码输出。为了《不失一般性》,从wiki搬一张表:
Value | Identified by | Compression method | Comments |
---|---|---|---|
0 | BI_RGB | none | Most common |
1 | BI_RLE8 | RLE 8-bit/pixel | Can be used only with 8-bit/pixel bitmaps |
2 | BI_RLE4 | RLE 4-bit/pixel | Can be used only with 4-bit/pixel bitmaps |
3 | BI_BITFIELDS | OS22XBITMAPHEADER: Huffman 1D | BITMAPV2INFOHEADER: RGB bit field masks, BITMAPV3INFOHEADER+: RGBA |
4 | BI_JPEG | OS22XBITMAPHEADER: RLE-24 | BITMAPV4INFOHEADER+: JPEG image for printing[14] |
5 | BI_PNG | BITMAPV4INFOHEADER+: PNG image for printing[14] | |
6 | BI_ALPHABITFIELDS | RGBA bit field masks | only Windows CE 5.0 with .NET 4.0 or later |
11 | BI_CMYK | none | only Windows Metafile CMYK[4] |
12 | BI_CMYKRLE8 | RLE-8 | only Windows Metafile CMYK |
13 | BI_CMYKRLE4 | RLE-4 | only Windows Metafile CMYK |
这里再搬一个RGB32的例子:
- 剩下的参数在无压缩的时候都可以设置成0,具体后面会有例子表示
color table
单个颜色表格式是这样的:
那个reserve要给0.即每一个颜色表对应RGB888里面的一个颜色。颜色表的数量取决于biBitCount:比特数/像素数,即 2 b i B i t C o u n t 2^{biBitCount} 2biBitCount.注意由于是小端存储,所以这里的格式是BGR!
实例解析
下面使用windows的画图软件,新建一个256*256的图像,加一点魔法后保存为16色位图:
注意在图片的左下边缘值点几个不同颜色的值(可以在边缘用键盘方向键调)如:
然后用16进制修改器,笔者用的是Imhex,加载bmp pattern(为了直观一点,笔者用书签的方法标注了亿点点):
这里可以从下面的模式数据看到和上面的解析结果一模一样了,这里需要注意的是:
- windows画图生成的横向分辨率和纵向分辨率默认是0(因为也没有压缩)
- 总共用到的颜色和重要颜色在无压缩下面也可以设置成0
- 下面读出来的像素数据是 u8[32768] ,因为在这里是一个像素4个bit,所以(8*32768)/4 = 65536 = 256*256刚好就是长宽了
可以看到位图中一大堆BB,就大概知道颜色表B大概就是背景色了,这里为了验证,笔者将颜色B变成了类蓝色,此时保存重新打开就变成了:
有兴趣的读者还可以尝试一下1bit的情况,默认在画图保存出来是黑白的:
但是当你修改了颜色表后,他就可以变成:
Verilog读取BMP文件并输出到文件
在这里的部分代码来自参考资料,但是没有做调色板相关工作,只实现了rgb24的读取.
读取BMP Header和DIB Header
由于在verilog没办法动态调整reg,所以我们这里直接假设4k(3840*2160),rgb32的格式输入:
localparam header_size = 54 + 256*4; // header + max color palette
localparam height = 2160;
localparam width = 3840;
localparam total_size = height*width; // max 4k
integer iBmpFileId,iOutFileId,iIndex=0,iCode;
integer iBmpWidth,iBmpHight,iDataStartIndex,iBmpSize;
integer iBmpbitcount, iBmpCompress, iColor, iColourIndex=0;
reg [7:0] rBmpData [0:total_size*4 + header_size -1];
……………………
……………………
iBmpFileId = $fopen("./test.bmp","rb");
iOutFileId = $fopen("./test.txt","w+");
iCode = $fread(rBmpData,iBmpFileId);
$fclose(iBmpFileId);
iBmpWidth = {rBmpData[21],rBmpData[20],rBmpData[19],rBmpData[18]};
iBmpHight = {rBmpData[25],rBmpData[24],rBmpData[23],rBmpData[22]};
iDataStartIndex = {rBmpData[13],rBmpData[12],rBmpData[11],rBmpData[10]};
iBmpSize = {rBmpData[5],rBmpData[4],rBmpData[3],rBmpData[2]};
iBmpCompress = {rBmpData[33],rBmpData[32],rBmpData[31],rBmpData[30]};
iBmpbitcount = {rBmpData[29],rBmpData[28]};
这里主要的作用是:把图像的长宽和bitcount读取进来,BmpCompress会决定在16bit/pixel下是选择rgb555还是有掩码选择.
读取颜色表/掩码表
由于在verilog没办法动态调整reg,所以我们这里直接假设256色的颜色表
reg [31:0] rBmpColor [0:256-1];
integer k=0;
……………………
iColor = (iDataStartIndex - 'h36)/4;
……………………
for (k = 0; k< iColor; k=k+1) begin
iColourIndex = 'h36 + k*4;
rBmpColor[k] = {rBmpData[iColourIndex+3],rBmpData[iColourIndex+2],
rBmpData[iColourIndex+1],rBmpData[iColourIndex]}; // transform2ARGB
$display ("rBmpColor_%0x=0x%h",k, rBmpColor[k]);
end
这里如果直接由bitcount得到颜色表个数判断条件比较难搞,这里用的是像素偏移地址减去包头地址得到。
读取像素数据
基本就是拿bitcount做一个case,然后分别处理就可以了,这里着重介绍一个16色(4bit)和16bit的处理
首先最简单的是rgb24色的,只要计算好偏移地址,注意数据大小端就可以了:
24: begin
for (i = 0; i<iBmpHight; i=i+1) begin // 原序读(左下到右上)
// for (i = iBmpHight-1; i>=0; i=i-1) // 反序读(左上到右下)
for (j = 0; j< iBmpWidth; j=j+1) begin
iIndex = i * iBmpWidth * 3 + j*3 + iDataStartIndex;
$fwrite(iOutFileId,"%x",rBmpData[iIndex+2]);
$fwrite(iOutFileId,"%x",rBmpData[iIndex+1]);
$fwrite(iOutFileId,"%x\n",rBmpData[iIndex+0]);
end
end
end
在bitcount = 4或者说小于8的时候,一个byte需要分割出好几个像素,这里小何的处理方式是:
reg [0:0]index_4;
reg [31:0] pixel_data;
reg [3:0] pixel_idx;
……………………
4:begin
for (i = 0; i<iBmpHight; i=i+1) begin // 原序读(左下到右上)
// for (i = iBmpHight-1; i>=0; i=i-1) // 反序读(左上到右下)
for (j = 0; j< iBmpWidth; j=j+1) begin
iIndex = (i*iBmpWidth + j + iDataStartIndex*2)/2;
index_4 =~((i*iBmpWidth + j + iDataStartIndex*2)%2);
pixel_idx = rBmpData[iIndex][index_4*4 +: 4];
pixel_data = rBmpColor[pixel_idx];
$fwrite(iOutFileId,"%x",pixel_data[23:16]);
$fwrite(iOutFileId,"%x",pixel_data[15:8]);
$fwrite(iOutFileId,"%x\n",pixel_data[7:0]);
end
end
end
这里的iIndex指像素所在哪个byte,index_4是指在byte里面那个位置的。取反是因为小端存储的时候第一个像素是在高位的,所以按位取反就可以拿到他的正确位置了。同理bitcount = 1也是一样的:
reg [2:0]index_1;
……………………
1:begin
for (i = 0; i<iBmpHight; i=i+1) begin // 原序读(左下到右上)
// for (i = iBmpHight-1; i>=0; i=i-1) // 反序读(左上到右下)
for (j = 0; j< iBmpWidth; j=j+1)begin
iIndex = (i*iBmpWidth + j + iDataStartIndex*8)/8;
index_1 =~((i*iBmpWidth + j + iDataStartIndex*8)%8);
pixel_idx = rBmpData[iIndex][index_1*1 +: 1];
pixel_data = rBmpColor[pixel_idx];
$fwrite(iOutFileId,"%x",pixel_data[23:16]);
$fwrite(iOutFileId,"%x",pixel_data[15:8]);
$fwrite(iOutFileId,"%x\n",pixel_data[7:0]);
end
end
end
RGB-mask读取
这里因为没有验证,所以就大概写一下思路,
- 获取对应rgb掩码,并丢进处理函数中:
mask16_r = rBmpColor[0];
mask16_g = rBmpColor[1];
mask16_b = rBmpColor[2];
pixel_data24 = pixel_mask_cal(pixel_data16, {mask16_r[23:16],mask16_r[31:24]},
{mask16_g[23:16],mask16_g[31:24]},
{mask16_b[23:16],mask16_b[31:24]});
- 由掩码计算第一个1出现的位置,在计算完掩码后左移取高八位,以r通道为例:
function [23:0] pixel_mask_cal;
input [15:0] pixel_in;
input [15:0] mask_r;
reg [15:0] pixel_r16;
reg [15:0] pixel_r8;
……………………
first1_r = shift1_16(mask_r);
pixel_r16 = pixel_in & mask_r;
pixel_r8 = pixel_r16 << first1_r;
pixel_mask_cal = {pixel_r8[15:8], pixel_g8[15:8], pixel_b8[15:8]};
其中shift1_16这个函数就是一个casex遍历每一位1,太冗长了就不给大家打出来了。
仿真结果
bitcount = 4,包头,颜色表读取:
输出txt文件:
复查颜色表和上面的hex图就能对应上了,这里的颜色已经是转换成RGB888了,所以看起来跟在hex图中会反了。
下一篇文章中会介绍怎么把这些数据转换成axis,从而搭建fpga图像处理平台的仿真。
小结
Imhex这个软件是真难用啊!
最近想搞两个东西,一个是学SV,另一个是搭建一个fpga图像仿真平台。所以会想用verilog做一遍,后面再用SV做一遍,看看后者能“方便”多少。
that’s all, thanks!
如果你觉得有一点收获的话
欢迎 关注 微信公众号:
小何的芯像石头