BMP文件格式解析(带颜色表)及Verilog的AXI-Stream接入仿真(一)

19 篇文章 35 订阅

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部分:

  1. BMP文件头
  2. DIB文件头
  3. 颜色表、掩码
  4. 图像数据

以小端(little endian)方式存储,以一张1080p图片为例,每一部分包含:

image-20220323141126199

网上查到的相关资料(传到verilog的)基本都是跑rgb888的。实际上在BMP文件格式中,只有当比特数/像素=16(rgb),24,32的时候,像素才是源数据,否则像素数据为对应颜色表的索引。

BMP文件头

name(size)descriptionaddr
header field(2B)文件头‘BM’0000
size(4B)文件大小0002
Reserved1(2B)预留10006
Reserved2(2B)预留10008
bfOffBits(4B)像素流开始位置000A

在BMP文件头中有两个字段比较重要:文件大小: size(4B)和bfOffBits(4B) 。bfOffBits指示像素流的地址头,所以如果是RGB24和RGB32可以知道长宽像素数之后,直接使用这个字段往后跳读就可以了。如果是RGB1/4/8也可以用这个字段获取颜色表的地址段。

bitmap information header

在这个字段中交代的是图像的规格和存储方式:

name(size)descriptionaddr
DIB_size(4B)DIB字段大小000E
biWidth(4B)图像宽度0012
biHeight(4B)图像高度(符号数)0016
biPlanes(2B)目标设备说明颜色平面数001A
biBitCount(2B)比特数/像素数001C
biCompression(4B)图像的压缩类型001E
biSizeImage(4B)位图数据的大小,当用BI_RGB格式时,可以设置为00022
biXPelsPerMeter(4B)横向分辨率(像素/米)0026
biYPelsPerMeter(4B)纵向分辨率(像素/米)002A
biClrUsed(4B)调色板中的颜色数量002E
biClrImportant(4B)重要颜色数量0032

其中需要说明的有以下几个:

  1. biHeight:图像高度是一个符号数,在他是正数时表示图像是从左下一直扫描到右上的,当他是负数是说明图像上下颠倒,即从左上扫描到右下。这个反人类的设计大家可以用“坐标轴”的概念去理解。
  2. biBitCount:比特数/像素数 ,举个例子,当他为4时,一个像素的占4个比特,即为RGB4。4个bit表示颜色空间最多只有$ 2^4 = 16 $种颜色可选,对应的颜色信息存在颜色表中。
  3. biCompression(4B) :图像的压缩类型 ,虽然本文讲的都是无压缩状态的,但是有两个特殊情况,当biBitCount为16或32时,图像可以选择使用掩码来决定每一种颜色(透明度)的位宽,也可以直接输出像素数据。以16为例,即存在熟悉的RGB565掩码存储和直接输出RGB555(最高位为0)两种方式。在biCompression为0时选择“不压缩”,在其为3时选择掩码输出。为了《不失一般性》,从wiki搬一张表:
ValueIdentified byCompression methodComments
0BI_RGBnoneMost common
1BI_RLE8RLE 8-bit/pixelCan be used only with 8-bit/pixel bitmaps
2BI_RLE4RLE 4-bit/pixelCan be used only with 4-bit/pixel bitmaps
3BI_BITFIELDSOS22XBITMAPHEADER: Huffman 1DBITMAPV2INFOHEADER: RGB bit field masks, BITMAPV3INFOHEADER+: RGBA
4BI_JPEGOS22XBITMAPHEADER: RLE-24BITMAPV4INFOHEADER+: JPEG image for printing[14]
5BI_PNGBITMAPV4INFOHEADER+: PNG image for printing[14]
6BI_ALPHABITFIELDSRGBA bit field masksonly Windows CE 5.0 with .NET 4.0 or later
11BI_CMYKnoneonly Windows Metafile CMYK[4]
12BI_CMYKRLE8RLE-8only Windows Metafile CMYK
13BI_CMYKRLE4RLE-4only Windows Metafile CMYK

这里再搬一个RGB32的例子:

  1. 剩下的参数在无压缩的时候都可以设置成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色位图:

注意在图片的左下边缘值点几个不同颜色的值(可以在边缘用键盘方向键调)如:

a

然后用16进制修改器,笔者用的是Imhex,加载bmp pattern(为了直观一点,笔者用书签的方法标注了亿点点):

这里可以从下面的模式数据看到和上面的解析结果一模一样了,这里需要注意的是:

  1. windows画图生成的横向分辨率和纵向分辨率默认是0(因为也没有压缩)
  2. 总共用到的颜色和重要颜色在无压缩下面也可以设置成0
  3. 下面读出来的像素数据是 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读取

这里因为没有验证,所以就大概写一下思路,

  1. 获取对应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. 由掩码计算第一个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!
如果你觉得有一点收获的话

欢迎 关注 微信公众号:
小何的芯像石头

参考资料

wiki-BMP

cnblog-BMP图像格式

csdn-Verilog之从BMP图片中读取RGB数据

知乎-BMP图像文件完全解析

Graphics File Formats

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小何的芯像石头

谢谢你嘞,建议用用我的链接

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值