Verilog_JEPG_DECODER
前言/背景/废话
这是一个长期更新的项目,一开始找不到现成的代码,只能说国内的生态环境太差了。一方面是对于去年 FPGA 创新赛没写出来代码的不甘心,另一方面是这个学期的通关项目老师不满足于我对去年项目的更新,他并不认为画电路板是有什么创新的事,而事实也确实如此。在此基础下,就开始了新建文件夹的工程。既然要开始写,那就要先知其所以然。不得不说,在看完 JPEG 的编码过程后,不得不感慨前人的智慧。接下来一点点更新吧。
之前在做FPGA以太网图传,如果不进行图像压缩的话,对摄像头的原始图像数据进行传输fps很有限。最初的分析和实测,百兆网对于 640_480 分辨率的 RGB565 的数据进行传输,最高帧数不会超过 20fps;即使是千兆网络,对于 1920_1080 分辨率的 RGB565 图像,最高帧数也不会超过 28fps。对于视频流,有 H2.64 编码,我们这里只进行图像的压缩,很容易就会想到 JPEG 格式,如此就开始了代码的编写。
JPEG 格式的压缩率是目前各种图像文件格式中最高的,压缩比率通常在 10:1 到 40:1 之间。它用有损压缩的方式去除图像的冗余数据,但存在着一定的失真。由于其高效的压缩效率和标准化要求,目前已广泛用于彩色传真、静止图像、电话会议、印刷及新闻图片的传送。由于各种浏览器都支持 JPEG 这种图像格式,因此它也被广泛用于图像预览和制作 HTML 网页。利用 FPGA 来实现视频图像压缩,可以充分发挥 FPGA 并行计算能力的优势,实现更快速、高效的图像压缩与解压缩操作。因此,深入研究 FPGA 在视频图像压缩与解压缩中的应用,对于提高视频传输效率、节省存储空间以及提升图像处理性能具有重要意义。
这种压缩面对的一种情况是,当采集端的输出图片数据量太大,而接收端没有这么大的带宽,例如:摄像头的设置为720P60fps且进行简单压缩的 RGB565 图像,如这样的数据量,已经快到达千兆网的级别了,当你接收端只有百兆网来进行数据传输时候,如果传输原始数据,那么帧数就会很低,同时也是不连续的,如果对每一帧的 RGB 行实时的 JPEG 压缩,那么在同样的帧数下,就只需要传输压缩后的图像数据,尽管因为要对原始数据进行处理导致数据慢了帧,但这并不是什么太重要的事情,我们只需要期望其不要太长,而对于如今的硬件运算,延时并不会很高,尽管输出的压缩数据不像原始的 RGB 流那样连续有条理,但这已经大大降低了数据量,使得相对低带宽传输成为了可能。
而另一种情况则是需要硬件来加速压缩速率,尽管我们可以直接在 PC 机上进行这种处理,但是 FPGA 也提供了一种灵活的嵌入式可能。
JPEG 压缩原理
JPEG 其主要是采用预测编码(DPCM)、离散余弦变换(DCT)以及熵编码的联合编码方式,以去除冗余的图像和彩色数据,属于有损压缩格式,它能够将图像压缩在很小的储存空间,一定程度上会造成图像数据的损伤。尤其是使用过高的压缩比例,将使最终解压缩后恢复的图像质量降低,如果追求高品质图像,则不宜采用过高的压缩比例。
RGB 转换 YCBCR
假定输入的图像为长宽已知的 RGB888 数据流
首先将每个像素的 RGB888 转为 YCBCR 444(主要是为了硬件采样便于实现)或其他。
- RGB888 转 YCBCR444公式如下:
- 为方便硬件实现,所有数据需要左移若干位取整以便于运算。
PS:在编码过程中,笔者采用另一公式换算
8×8 DCT
- 将图像划分为 8x8 的块,单独处理每个块,从左到右,从上到下,顺序处理。对于熵编码部分,当采样为 444 时,块编码顺序为 Y、Cb、Cr、Y、Cb、Cr……当采样顺序为 422 时,以每 16x16 为单位,编码顺序为 Y、Y、Y、Y、Cb、Cb、Cr、Cr以此类推。
- 块内每个像素值原 [0,255] 减去 128,形成 [-128,127] 的对称范围。
- 8x8 的块进行 DCT 处理,DCT 矩阵如下,一般为了方便硬件计算会左移若干位取整。
- 令:
- 有 DCT 矩阵:
- 在FPGA实现中,都是采用拆成2个一维DCT的方式进行计算。因此,2维DCT便拆分为2次行、列方向的一维DCT变换,在FPGA中只需要实现1维DCT变换,然后复用2次即可。
量化编码
- DCT 后的数据按照量化表一对一整除,进行量化。
根据人眼的视觉特性,人眼对高频信息的不敏感,在 JPEG 中采用低频细量化,高频粗量化,以降低视觉冗余。同时由于人眼对色度信号的敏感度低于亮度信号,故对于亮度和色度信号有不同的量化系数 。
量化结果可表示为:
根据不同质量的要求,建立不同的量化系数表。
16 | 11 | 10 | 16 | 24 | 40 | 51 | 61 |
12 | 12 | 14 | 19 | 26 | 58 | 60 | 55 |
14 | 13 | 16 | 24 | 40 | 57 | 69 | 56 |
14 | 17 | 22 | 29 | 51 | 87 | 80 | 62 |
18 | 22 | 37 | 56 | 68 | 109 | 113 | 77 |
24 | 35 | 55 | 64 | 81 | 104 | 120 | 92 |
49 | 64 | 78 | 87 | 103 | 121 | 120 | 101 |
72 | 92 | 95 | 98 | 112 | 100 | 103 | 99 |
17 | 18 | 24 | 47 | 99 | 99 | 99 | 99 |
18 | 21 | 26 | 66 | 99 | 99 | 99 | 99 |
24 | 26 | 56 | 99 | 99 | 99 | 99 | 99 |
47 | 66 | 99 | 99 | 99 | 99 | 99 | 99 |
99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 |
99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 |
99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 |
99 | 99 | 99 | 99 | 99 | 99 | 99 | 99 |
DC 系数差分编码
- 8×8 图像块经过DCT变换之后得到的 DC 直流系数有两个特点
- 系数的数值比较大
- 相邻8×8图像块的 DC 系数值变化不大:冗余
- 对相邻图像块之间量化 DC 系数的差值 DIFF 进行编码:
AC 系数 Zigzag 扫描编码
- 量化后的小块按照 Zigzag 扫描成一维排列的数据流。
- 尤其在最后, 如果都是零,给出 EOB 即可。
在 JPEG 中的游程编码规定为:(run,level)
表示连续 run 个 0 ,后面跟值为 level 的系数
如: −3 0 −3 −2 −6 2 −4 1 −4 1 1 5 1 2 −1 1 −1 2 0 0 0 0 0 −1 −1 EOB
表示为:
(0,-3) ; (1,-3) ; (0,-2) ; (0,-6) ; (0,2) ; (0,-4) ; (0,1) ;(0,-4) ; (0,1) (0,1) ; (0,5) ; (0,1); (0,2) ; (0,-1) ; (0,1) ; (0,-1); (0,2) ; (5,-1) ; (0,-1) ; EOB
编码:
- Run: 最多 15 个,用 4 位表示 Z
- Level:类似 DC ,分成 16 个类别,用 4 位表示表示类别号 C
- 对(Z, C)联合用 Huffman 编码
- 对类内索引用定长码编码
霍夫曼编码
- 将一维排列进行 Huffman 熵编码。
- 查表参数 Run/size = 非零数前面 0 的个数 / 非零数值编码 Bit 长度。
比如(0, 6, 57)对应编码值为 0/6 对应的 111100,那么最后 57 的编码结果为 1111000 + 111001,单独处理的亮度 DC 值 50 会放在这串数据前面,即 1110 + 110010,最后以对应的 EOB 结尾,这样就完成了一个块(亮度)的编码,按每 8 位一个字节进行输出,有空余补 0,然后对 Cb、Cr 分块进行同样的处理。 - 读取下一个块,重复进行直至完成所有分块;对于最后一个数据可以不给 EOB,由 FFD9 直接结束。
数据打包
- 将编码数据和参数打包为标准 JPEG 格式(段 + 段的格式)输出,具体见下段的 JPEG 编码结构。
JPEG 编码结构
JPEG 文件以 segment 为数据流的组织形式,定义了一系列的标记(Marker)放在每个 segment 的开头。Marker 均以 0xFF 开始,后跟 1 字节的标记标识符和 2 字节的标记长度以及该标记所对应 payload。标记长度部分高位在前,低位在后,不包含该标记头两个字节。
常见 Maker 如下:
Name | Bytes | Payload | Comments |
---|---|---|---|
SOI (Start of Image) | 0xFFD8 | none | 文件开头 |
APPn (Application Specific) | 0xFFEn | variable size | Exif JPEG 使用APP1,JFIF JPEG 使用 APP0 |
DQT(Define Quantization Table) | 0xFFDB | variable size | 指定量化表 |
SOF0 (Start of Image Baseline DCT) | 0xFFC0 | variable size | Baseline DCT 所用的开头 |
SOF2(Start of Image Progressive DCT) | 0xFFC2 | variable size | Progressive DCT 所用的开头 |
DHT(Define Huffman Table) | 0xFFC4 | variable size | 指定 Huffman 表 |
SOS(Start of Scan) | 0xFFDA | variable size | 扫描的开头,包含编码通道顺序以及图像压缩数据等信息 |
EOI(End of Image) | 0xFFD9 | none | 文件结束 |
COM (Comment) | 0xFFFE | variable size | 注释 |
DRI (Define Restart Interval) | 0xFFDD | 2 bytes | RST 中的 marker |
另外不常见的段类型不展开。另外为避免编码过程中产生 FFXX 触发段类型,当数据内出现 FF 时,在后面强制插入 00 再接后面的数据。这一部分的输出并不是编码的核心,但是也是必要的一环。还是很需要对这部分进行编写。方便进一步的仿真综合验证,此处给出必要的段类型说明,如下。
SOI
字节编号 | 16进制值 | 说明 |
---|---|---|
1 | FF | 固定,段标志 |
2 | D8 | 固定,段类型 |
APP0
字节编号 | 16进制值 | 说明 |
---|---|---|
1 | FF | 固定,段标志 |
2 | E0 | 固定,段类型 |
3-4 | 00 10(默认) + 3 x N(缩略图像素数) | 自定义,段长度,如果没有缩略图就是00 10 |
5-9 | 4A46494600 | 固定,JFIF字符串 |
10-11 | 0101(默认) | 自定义,版本号,一般为0101,即JFIF版本 1.1 |
12 | 01 | 自定义,密度单位(0:无单位;1:点数/英寸;2:点数/厘米) |
13-14 | XX | 自定义,图像横向像素数 |
15-16 | XX | 自定义,图像纵向像素数 |
17 | 00(默认) | 自定义,缩略图横向像素数,没有就是00 |
18 | 00(默认) | 自定义,缩略图纵向像素数,没有就是00 |
19…… | XX | 自定义,缩略图RGB数据,每3个字节为一个像素,没有就不写 |
DQT
字节编号 | 16进制值 | 说明 |
---|---|---|
1 | FF | 固定,段标志 |
2 | DB | 固定,段类型 |
3-4 | 00 43(默认) | 自定义,段长度,对于8b的数据表来说,默认43,16b的数据此处为83 |
4 | XX | 自定义,字节高四位(0:8b 数据 1:16b数据),低四位(0:亮度表 1:色度表),例如此处截图的为亮度表 |
5…… | XX | 自定义,8x8共64个表格值,对于8b的格式,一个字节对应一个数据;对于16b的格式则需要两个字节。按照Zigzag顺序从左上往右下扫描。 |
SOF
字节编号 | 16进制值 | 说明 |
---|---|---|
1 | FF | 固定,段标志 |
2 | C0 | 固定,段类型,C2等硬件不方便实现的类型暂不说明,此处只说明C0,即Baseline类型 |
3-4 | 00 11(默认) | 自定义,段长度 |
5 | 08 | 自定义,采样精度,Baseline只有08 |
6~7 | XX XX | 自定义,图像横向像素数 |
8~9 | XX XX | 自定义,图像纵向像素数 |
10 | 03(默认) | 自定义,图像分量,对于YUV来说只有3个分量,灰度就只有1个 |
11~13 | XX XX XX | 自定义,分量ID 0 8b+H0分量水平采样 4b+V0分量垂直采样 4b+T0当前分量量化表ID 8b |
14~16 | XX XX XX | 自定义,分量ID 1 8b+H1分量水平采样 4b+V1分量垂直采样 4b+T1当前分量量化表ID 8b |
17~19 | XX XX XX | 自定义,分量ID 2 8b+H2分量水平采样 4b+V2分量垂直采样 4b+T2当前分量量化表ID 8b |
20…… | XX | 自定义,YUV此处已经结束,如果其他编码方式可能还有后续的分量编码 |
注:对于11~19来说,主要定义了YUV444还是422还是420采样,并且采用哪个量化表,当然这里一般是固定的Y U V 分量编码 0 1 2,并且Y使用量化表0,U V共同使用量化表1;对于其他颜色空间等,这里可能还有别的分量,所以看起来这个描述比较复杂
DHT
字节编号 | 16进制值 | 说明 |
---|---|---|
1 | FF | 固定,段标志 |
2 | C4 | 固定,段类型 |
3-4 | XX XX | 自定义,段长度 |
5 | XX | 自定义,第8~6位:固定0,第五位:0:DC直流 1:AC交流,低四位:0 亮度 1色度,因此一共有00 10 (亮度DC\AC),10 11(色度DC\AC)四张表 |
6~21 | XX…… | 自定义,Huffman Table 位表,共16个字节,表示在Huffman 表中,第1~第16bit的各有多少个数据,顺序摆放。 如上图所示,将数据分为6~21部分和22~37部分,第6个字节00表示长度bit 01的有0个数据;第7个字节01表示长度bit 01的有1个数据,将第22字节放入;第8个字节05表示长度bit 01的有5个数据,将23~27字节放入,以此类推可以得到下图值表; |
22…… | XX…… | 自定义,Huffman Table 值表 |
SOS
字节编号 | 16进制值 | 说明 |
---|---|---|
1 | FF | 固定,段标志 |
2 | DA | 固定,段类型 |
3-4 | 00 0C | 固定,段长度 |
5 | XX | 自定义,扫描行内组件,1:灰度图,3:YCBCR or YIQ,4:CMYK,一般为1 |
6,8,10 | XX | 自定义,组件ID,01:Y,02:CB,03:CR,后面45 YUV模式用不上 |
7,9,11 | XX | 自定义,Hufffman表号,高四位是AC表ID,低四位是DC表 |
12 | 00 3F 00 | 自定义,频谱选择的三个参数,在简化模式下可以当做常量 |
13…… | XX…… | 压缩的图像数据,顺序为从左到右,从上到下 |
EOI:
字节编号 | 16进制值 | 说明 |
---|---|---|
1 | FF | 段标志 |
2 | D9 | 段类型 |
工程实现
工程结构
jpeg_top.v
u26 (clock_div) clock_div.v
u19 (fifo_out) fifo_out.vu14 (pre_fifo) pre_fifo.v
u4 (RGB2YCBCR) rgb2ycbcr.v
u11 (crd_q h) crd_q_h.vu8 (cr_dct) cr_dct.v
u9 (cr_quantizen) cr_quantizer.v
u10 (cr_huff) cr_huff.vu12 (cbd_q h) cbd_q_h.v
u5 (cb_dct) cb_dct.v
u6 (cb_quantizen) cb_quantizer.v
u7 (cb_huff) cb_huff.vu13 (yd_q h) yd_q_h.v
u1 (y_dct) y_dct.v
u2 (y_quantizer) y_quantizer.v
u3 (y_huff) y_huff.vu15 (sync_fifo_32) sync_fifo_32.v
u25 (sync_fifo_32) sync_fifo_32.v
u16 (sync_fifo_32) sync_fifo_32.v
u24 (sync_fifo_32) sync_fifo_32.v
u17 (sync_fifo_32) sync_fifo_32.vu20 (ff_checker) ff_checker.v
u18 (sync_fifo_ff) sync_fifo_ff.v
u28 (parallel_to_serial) parallel_to_serial.v
u29 (jpeg_package) jpeg_package.v
主要module
jpeg_top
下表给出 module 说明:
Port name | Direction | Type | Description |
---|---|---|---|
clk | input | 时钟 | |
rst | input | 复位 | |
end_of_file_signal | input | 结束标志 | |
enable | input | 输入 有效 | |
Compress_data_rdy | input | 打包编码流输入有效 | |
data_in | input | [23:0] | 输入RGB_block信号 |
JPEG_bitstream | output | [31:0] | 输出编码流 |
data_ready | output | 输出信号有效 | |
end_of_file_bitstream_count | output | [4:0] | 编码流剩余数 |
eof_data_partial_ready | output | 输出编码流结束 | |
jpeg_data | output | [7:0] | JPEG数据流 |
jpeg_data_vaild | output | JPEG数据流有效 | |
enda1 | output | JPEG数据流结束标志 |
RGB2YCBCR
下表给出 module 说明:
Port name | Direction | Type | Description |
---|---|---|---|
clk | input | 时钟 | |
rst | input | 复位 | |
enable | input | 输入有效 | |
data_in | input | [23:0] | 输入信号 |
data_out | output | [23:0] | 输出信号 |
enable_out | output | 输出有效 |
y/Cb/Cr_DCT
下表给出 module 说明:
Port name | Direction | Type | Description |
---|---|---|---|
clk | input | 时钟 | |
rst | input | 复位 | |
enable | input | 输入有效 | |
data_in | input | [7:0] | 输入信号 |
Z_final | output | [10:0] | 输出矩阵信号 |
output_enable | output | 输出有效 |
其中
y/Cb/Cr_huff
下表给出 module 说明:
Port name | Direction | Type | Description |
---|---|---|---|
clk | input | 时钟 | |
rst | input | 复位 | |
enable | input | 输入使能 | |
Y | input | [10:0] | 输入矩阵信号 |
JPEG_bitstream | output | [31:0] | 输出编码流 |
data_ready | output | 输出开始 | |
output_reg_count | output | [4:0] | 输出计数 |
end_of_block_output | output | 结束标志 | |
end_of_block_empty | output | 结束清空标志 |
其中
y/Cb/Cr_quantizer
下表给出 module 说明:
Port name | Direction | Type | Description |
---|---|---|---|
clk | input | 时钟 | |
rst | input | 复位 | |
enable | input | 输入有效 | |
Z | input | [10:0] | 输入矩阵信号 |
Q | output | [10:0] | 输出矩阵信号 |
out_enable | output | 输出有效 |
其中
jpeg_package
下表给出 module 说明:
Generic name | Type | Value | Description |
---|---|---|---|
YUV_MODE | 编码模式 | ||
IMG_WIDTH | 图片宽度 | ||
IMG_HEIGHT | 图片高度 |
Port name | Direction | Type | Description |
---|---|---|---|
clk | input | 时钟 | |
rst | input | 复位 | |
data_ready1 | input | 输入开始标志 | |
data_en | input | 输入有效 | |
Compress_data_rdy | input | 编码流有效 | |
Compress_data_last | input | 编码流结束标志 | |
Compress_data | input | [7:0] | 编码流 |
Compress_data_rden | output | 打包开始标志 | |
jpeg_data | output | [7:0] | JPEG输出 |
jpeg_data_vaild | output | 输出有效 | |
jpeg_data_last | output | 输出结束 |
- 缺 状态转移图
待补充
仿真验证
RGB块数据流的生成
这一部分使用了 matlab 来进行图片数据的读取、转换、生成数据流和延时,并生成 tb.v 文件便于直接仿真。
1 | clc |
Testbench
宏定义图像尺寸,对于小尺寸的图片进行波形文件的绘制,以及将输出的数据直接写入 JPEG 以方便直接验证;大尺寸的图像直接写入 JPEG。
对于 128×128 的图片仿真时间只有 14s 左右,而对于 1080×720 的图片仿真时间相当之长,连 VScode 也救不了,要达到 7min 之久。
此处为了节约版面给出 8×8 分辨率的一个 tb.v
1 |
|
验证结果
图为编码输出打包的 JPEG,vcd 文件因为太大了就没有生成。同时给出 96×96 的关键仿真波形,因为量化编码方式的区别,JPEG 的输出可能会有差异。
总结
在进行了几个月的摸鱼、调研查找编码原理以及相关算法例程,最后经过不屑的缝合,终于是搓出来一个看的过去的工程,性能可能不是很好,因为没有借助任何 Xlinx 和其他家的相关 IP 核。
全部的编写验证在 Vscode 上完成,包括仿真波形以及输出数据流。
工程主要借鉴了猪蹄的 GitHub 工程(虽然他并没有写完),但是 package 模块被拿过来用了。同时借鉴(使用了)一个意外发现的开源核。整个工程没有外部 fifo 以及 RAM 之类的缓存,所以每次进入的是 8×8 数据块,需要在外写 8 行 RAM 缓存来进行原生态 RGB 图像硬件转换。考虑到输出数据的位宽大多规定为 8bit,而开源核输出位宽为 32bit,被迫给时钟分频降速 5 倍(实际上应该降 4 倍)然后再进行并串转换输出。在实际使用中可以使用锁相环进行系统时钟倍频,来给编码模块提供时钟,然后再输出接入 fifo 或 RAM 一类,进行异步转换。只是因为没有下板验证,尚且不知倍频上去的数据误码率怎么样。要注意每个数据块之间的间隔应该大于 13 个时钟周期,以保证输出数据的正确性。输出数据为打包好的 JPEG ,以 FFD8 开始,FFD9 结束,是标准的 JPEG 格式。然而压缩率并不是很高,只降低了 2 倍左右,也可以说图像质量没有下降很多。只能说给自己了一个交代吧,不算是什么都没有做出来了。
附录
工程链接:
- 待更新
参考资料
- https://github.com/BigPig-Bro/Encoder_JPEG
- https://www.cnblogs.com/buaaxhzh/p/9138307.html
- https://blog.csdn.net/MmikerR/article/details/107456094
- https://www.cnblogs.com/tinycoder/articles/2026178.html
- https://d.wanfangdata.com.cn/thesis/D009595
- https://xupsh.gitbook.io/pp4fpgas-cn
- https://people.ece.cornell.edu/land/courses/ece5760/FinalProjects/f2009/jl589_jbw48/jl589_jbw48/trans_tables.html
- https://fpga.eetrend.com/blog/2024/100578007.html
- https://blog.csdn.net/sshcx/article/details/1674224
- https://www.cnblogs.com/stnlcd/p/7261842.html
其他
Value | Size | Bits |
---|---|---|
0 | 0 | - |
-1, 1 | 1 | 0, 1 |
-3, -2, 2, 3 | 2 | 00, 01, 10, 11 |
-7, -6, -5, -4, 4, 5, 6, 7 | 3 | 000, 001, 010, 011, 100, 101, 110, 111 |
-15, …, -8, 8, …, 15 | 4 | 0000, …, 0111, 1000, …, 1111 |
-31, …, -16, 16, …, 31 | 5 | 0 0000, …, 0 1111, 1 0000, …, 1 1111 |
-63, …, -32, 32, …, 63 | 6 | 00 0000, …, …, 11 1111 |
-127, …, -64, 64, …, 127 | 7 | 000 0000, …, …, 111 1111 |
-255, …, -128, 128, …, 255 | 8 | 0000 0000, …, …, 1111 1111 |
-511, …, -256, 256, …, 511 | 9 | 0 0000 0000, …, …, 1 1111 1111 |
-1023, …, -512, 512, …, 1023 | 10 | 00 0000 0000, …, …, 11 1111 1111 |
-2047, …, -1024, 1024, …, 2047 | 11 | 000 0000 0000, …, …, 111 1111 1111 |
-4095, …, -2048, 2048, …, 4095 | 12 | 0000 0000 0000, …, …, 1111 1111 1111 |
-8191, …, -4096, 4096, …, 8191 | 13 | 0 0000 0000 0000, …, …, 1 1111 1111 1111 |
-16383, …, -8192, 8192, …, 16383 | 14 | 00 0000 0000 0000, …, …, 11 1111 1111 1111 |
-32767, …, -16348, 16348, …, 32767 | 15 | 000 0000 0000 0000, …, …, 111 1111 1111 1111 |