MJPEG、MPEG2、MPEG4、H264 是流行且兼容性很高的 4 种视频编码格式。其中 MJPEG 对每帧独立进行 JPEG 图象压缩,而不利用帧间相关性,因此压缩效果较差。 MPEG2、MPEG4 和 H264 会进行帧间压缩,但后两者更复杂,效果也更好。MPEG2 虽然比较老 (1994年),但在低压缩率的条件下与 MPEG4 和 H264 没有明显劣势,因此 MPEG2 在高质量、低压缩率要求的应用场合仍然可堪一用。。。
为了说明这一点,下图 展示了用 MJPEG、MPEG4、H264、MPEG2 对一个分辨率为1440x704的mp4 文件进行编码的对比,该视频的宽和高为 1440x704 ,具有11帧。 表中压缩率越高越好,质量 (信噪比) 越高越好。可以看出综合考虑质量和压缩率,本模块远强于 MJPEG ,且不显著差于 MPEG4 和 H264 。。。
本设计使用system verilog语言设计了一个MPEG2视频压缩加速器,输入数据为YUV 444 原始像素,输出数据为端口宽度为 256 bit ,每周期可能输出 32 字节的完整 MPEG2 码流,集成了4种量化级别,越大则压缩率越高,但输出视频质量越差,可通过顶层参数配置,实用性和灵活性很高;一并提供了加速器的仿真源文件,可通过vivado或其他软件进行仿真,文章后面有详细的仿真教程和输出演示视频,请耐心看到最后;
本文详细描述了FPGA实现MPEG2视频压缩加速器及其仿真的设计方案,工程代码可综合编译上板调试,但目前只做到了仿真层面,可直接项目移植,适用于在校学生、研究生项目开发,也适用于在职工程师做项目开发,可应用于医疗、军工等行业的数字成像和图像压缩领域;
提供完整的、跑通的工程源码和技术支持;
工程源码和技术支持的获取方式放在了文章末尾,请耐心看到最后;
我这里有图像的JPEG解压缩、JPEG-LS压缩、H264解码以及其他方案,后续还会出更多方案,我把他们整合在一个专栏里面,会持续更新,专栏地址:
直接点击前往
MPEG2视频压缩算法介绍可以百度一下或者csdn或者知乎搜一下看看,专业的讲解我不擅长,我只擅长用fpga实现算法,专业讲原理的大佬比我讲得好,这里可以推荐一篇阅读量很大的文章:直接点击前往
1:每周期可输入 4 个像素,每个像素包含 8-bit Y (明度) 、8-bit U (蓝色度) 、8-bit V (红色度) ;
2:在 Xilinx Kintex-7 XC7K325TFFG676-2 上最大时钟频率为 67MHz;
3:吞吐率达到 67*4= 268 MPixels/s ,对 1920x1152 的视频的编码帧率为 121 帧/秒;
4:无需外部存储器,典型配置在 Xilinx 7 系列 FPGA 上消耗 134k LUT, 125 个 DSP48E, 405个 BRAM36K;
5:静态可调参数 (综合前确定,部署后不可调)
5.1:视频的最大宽度、最大高度:越大则消耗 BRAM 越多;
5.2:动作估计的搜索范围:越大则压缩率越高,但消耗 LUT 越多;
5.3:量化级别 :越大则压缩率越高,但输出视频质量越差;
6:动态可调参数 (可在运行时针对每个视频序列进行调整)
6.1:视频的宽度和高度可调;
6.2:I帧间距 (两个I帧之间P帧的数量) 可调:越大则压缩率越高;
7:本MPEG2视频压缩器缺点:
对 LUT 和 DSP 资源的优化还需要优化,因为该模块目前消耗资源还不够小,一般只适用于 Xilinx Kintex-7 这种规模 (或更大规模) 的 FPGA,后续优化后,希望能部署在中等规模的 Artix-7 上。。。
首先看看MPEG2视频压缩加速器顶层接口:
注意仔细看每个接口对应的注释,看懂了接口才能理解接口时序;
顶层接口的4个参数解释如下:
在使用本模块前,需要根据你想要压缩的视频的尺寸来决定 XL 和 YL ,例如如果你想编码 1920x1152 的视频,则应该取 XL=YL=7 。如果你想编码 640x480 的视频,当然也可以取 XL=YL=7 ,但为了节省 BRAM 资源,可以取 XL=6, YL=5 ;
要使用本模块编码一个视频序列 (sequence, 即一系列帧,一般保存为一个独立的视频文件) ,需要按照逐帧、逐行、逐列的顺序把像素输入模块。每个时钟周期可以输入同一行内相邻的4个像素。当一个时钟周期时需要输入4个像素时,需要让i_en=1,同时让 i_Y0~i_Y3 i_U0~i_U3 和 i_V0~i_V3 上分别出现4个像素的 Y, U 和 V 分量。
例如,一个 64x64 的视频序列如下。其中 (Yijk, Uijk, Vijk) 代表第i帧、第j行、第k列的像素的 Y, U, V 值:
第0帧:
第0行: (Y000, U000, V000), (Y001, U001, V001), (Y002, U002, V002), (Y003, U003, V003), (Y004, U004, V004), (Y005, U005, V005), (Y006, U006, V006), (Y007, U007, V007), (Y008, U008, V008), (Y009, U009, V009), ..., (Y0063, U0063, V0063)
第1行: (Y010, U010, V010), (Y011, U011, V011), (Y012, U012, V012), (Y013, U013, V013), (Y014, U014, V014), (Y015, U015, V015), (Y016, U016, V016), (Y017, U017, V017), (Y018, U018, V018), (Y019, U019, V019), ..., (Y0163, U0163, V0163)
...
第1帧:
第0行: (Y100, U100, V100), (Y101, U101, V101), (Y102, U102, V102), (Y103, U103, V103), (Y104, U104, V104), ...
第1行: (Y110, U110, V110), (Y111, U111, V111), (Y112, U112, V112), (Y113, U113, V113), (Y114, U114, V114), ...
...
...
则第一个时钟周期应该输入第0帧第0行的前4个像素:
第 1 个时钟周期:
i_Y0 = Y000
i_Y1 = Y001
i_Y2 = Y002
i_Y3 = Y003
i_U0 = U000
i_U1 = U001
i_U2 = U002
i_U3 = U003
i_V0 = V000
i_V1 = V001
i_V2 = V002
i_V3 = V003
然后,下一周期应该输入:
第 2 个时钟周期:
i_Y0 = Y004
i_Y1 = Y005
i_Y2 = Y006
i_Y3 = Y007
i_U0 = U004
i_U1 = U005
i_U2 = U006
i_U3 = U007
i_V0 = V004
i_V1 = V005
i_V2 = V006
i_V3 = V007
以此类推,需要花费 64/4=16 个时钟周期来输入第0帧第0行,然后第 17 个时钟周期应该输入第0帧第1行的前4个像素:
第 17 个时钟周期:
i_Y0 = Y010
i_Y1 = Y011
i_Y2 = Y012
i_Y3 = Y013
i_U0 = U010
i_U1 = U011
i_U2 = U012
i_U3 = U013
i_V0 = V010
i_V1 = V011
i_V2 = V012
i_V3 = V013
继续以此类推,需要花费 64*64/4=1024 个时钟周期来输入完第0帧,然后第 1025 个时钟周期应该输入第1帧第0行的前4个像素:
第 1025 个时钟周期:
i_Y0 = Y100
i_Y1 = Y101
i_Y2 = Y102
i_Y3 = Y103
i_U0 = U100
i_U1 = U101
i_U2 = U102
i_U3 = U103
i_V0 = V100
i_V1 = V101
i_V2 = V102
i_V3 = V103
本模块可以连续每周期都输入4个像素而不需要任何等待 (没有输入反压握手),但当发送者没有准备好像素时,也可以断续地输入像素 (可以随时插入气泡) ,也即让 i_en=0;
当输入一个视频序列的最前面的4个像素 (也即第0帧第0行的前4个像素) 的同时,需要让 i_xsize16, i_ysize16 , i_pframes_count 有效,其中:
i_xsize16 :视频宽度/16 。例如对于 640x480 的视频,应该取 i_xsize16 = 640/16 = 40 。注意i_xsize16 取值范围为 4~(2^XL) ;
i_ysize16 :视频宽度/16 。例如对于 640x480 的视频,应该取 i_xsize16 = 480/16 = 30 。注意 i_ysize16 取值范围为 4~(2^YL);
i_pframes_count: 决定了相邻两个 I 帧之间 P 帧的数量,可以取 0~255 ,越大则压缩率越高,推荐的取值是 23;
本模块只支持宽和高都为 16 的倍数的视频,例如 1920x1152 。如果视频的宽和高不为 16 ,则应该填充为 16 的倍数后再送入本模块。例如 1910x1080 的视频应该填充为 1920x1088;
本模块不支持宽和高小于 64 的视频,因此 i_xsize16 和 i_ysize16 的最小合法取值是 4;
结束当前视频序列:
当输入若干帧后,如果你想结束当前视频序列 (一个视频序列的帧的数量不限),需要向模块发送“结束当前视频序列”的请求,具体方法是让 i_sequence_stop=1 保持一个周期,以下两种方式均可:
在输入该视频序列的最后4个像素的同时让 i_sequence_stop=1 ;
在输入该视频序列的最后4个像素后的若干周期后再让 i_sequence_stop=1 ;
然后需要等待模块完成对该视频序列的收尾工作,具体方法是检测 o_sequence_busy 信号, o_sequence_busy=1 代表模块正在编码一个序列;o_sequence_busy=0 代表模块处于空闲状态 。当你发送“结束当前视频序列”的请求后,应该等待 o_sequence_busy 从 1 变为 0 ,这才代表着该视频序列的编码工作已经完全结束。
开始输入下一个视频序列:
当上一个视频序列完全结束 ( o_sequence_busy 从 1 变为 0 ) 后,才可以开始输入下一个视频序列 (也即输入下一个视频序列的最前面的4个像素,同时让 i_xsize16, i_ysize16 , i_pframes_count 有效) 。
o_en, o_last, o_data 这三个信号负责输出编码后的 MPEG2 码流。
当 o_en=1 时, o_data 上会出现 32 字节的 MPEG2 码流数据。如果 o_en=1 的同时 o_last=1 ,说明这是该视频序列输出的最后一个数据,下一次 o_en=1 时就输出的是下一个视频序列的第一个数据了。
注意!! o_data 是 小端序 (Little Endian) ,也即 o_data[7:0] 是最靠前的字节, o_data[15:8] 是第二个字节, … o_data[255:248] 是最后一个字节。之所以要用小端序,是因为大多数总线也是小端序 (例如 AXI 总线) 。
注意!! o_en=1 的同时必然有 o_sequence_busy=1 。当模块空闲 (也即 o_sequence_busy=0 ) 时,它不可能输出数据 (不可能出现 o_en=1);
经过前面的接口解释,最后放出接口时序应该好理解一些:
最开始, o_sequence_busy=0 说明模块当前空闲,可以输入一个新的视频序列。
让 i_en=1 ,输入一个视频序列的最前面的4个像素,同时在 i_xsize16, i_ysize16 上输入该视频的宽、高信息;在 i_pframes_count 上输入你想要的 I 帧间距。
此后继续向该模块输入像素 (连续输入和断续输入均可),直到该视频序列的最后4个像素输入完为止。
让 i_sequence_stop=1 一个周期,结束该视频序列。
等待 o_sequence_busy 从 1 变成 0 ,然后才可以输入下一个视频序列。
在以上过程的同时, o_en 会断续出现 1 。在 o_en=1 时从 o_data 上拿到 MPEG2 输出流。当该视频序列的最后一个数据输出的同时 o_last=1 。
模块输入YUV444视频流,首先经过YUV444转YUV420模块降低数据量;
再根据前后2帧或几帧的图像差异进行动作预估,进而进行预测,如果连续到来的3帧图像差异很小,则认为这3帧相似度很高,可以视为1帧,这样就减少了2帧的数据量,以此为前提,则可进行新的预估帧图像的变、量化,到这里还要进行负反馈,对这个判断做出再次确认;然后是数据重新排序以及编码,最后输出数据为端口宽度为 256 bit ,每周期可能输出 32 字节的完整 MPEG2 码流;
开发板FPGA型号:Xilinx xc7k325tffg676-2;
开发环境:Vivado2019.1;
输入:YUV RAW 文件;
输出:.m2v 文件(可通过 VLC播放器播放);
工程代码架构如下:
我们的 testbench 需要给 helai_MPEG2_EC.sv 模块输入视频的原始像素,但电脑上的视频一般都是编码后的 (基本上不可能有以原始像素存储的视频),所以在进行仿真前,需要准备 YUV RAW 文件 (原始像素文件),然后 testbench 才能读取该文件并送入 helai_MPEG2_EC.sv 模块。
我提供了3个视频对应的 YUV RAW 文件,分别是 288x208.raw, 640x320.raw 和 1440x704.raw,它们实际上是 288x208.mp4, 640x320.mp4, 1440x704.mp4 这三个视频解码后得到的,位置如下:
仿真流程为:
第一步:
添加源码并开启行为仿真:
运行中可能会出现如下信息导致仿真失败:
解决办法:在Tcl中输入以下指令并回车:
set_property display_limit 33554432 [current_wave_config]
如果把我提供的三个视频都仿真完,需要3个小时左右,为了节省时间,在仿真代码里我只加入了一个视频做仿真,你可以修改了仿真三个,需要修改的地方如下:
第二步:
点击开始仿真:
288x208的视频共有84帧,需要仿真时间半小时左右,等仿真完成后可以看到打印信息:
FPGA的型号为 Xilinx Kintex-7 XC7K325TFFG676-2 。这些配置下的最大时钟频率均为 67MHz,不同压缩率下的综合后的资源消耗如下:
仿真后的输出文件位置如下:可打开播放视频:
打开播放截图如下:
综合仿真流程即输出结果验证的演示视频如下:
FPGA实现MPEG2视频压缩
福利:工程代码的获取
代码太大,无法邮箱发送,以某度网盘链接方式发送,
资料获取方式:文章末尾的V名片。
网盘资料如下: