在线咨询
eetop公众号 创芯大讲堂 创芯人才网
切换到宽版

EETOP 创芯网论坛 (原名:电子顶级开发网)

手机号码,快捷登录

手机号码,快捷登录

找回密码

  登录   注册  

快捷导航
搜帖子
查看: 4092|回复: 10

[原创] 手写Verilog用FPGA实现实时图像卷积,用Block Ram缓存图像

[复制链接]
发表于 2022-3-15 15:24:27 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册

x
如何用FPGA做图像卷积可能是大家最想了解的,因为现在的卷积神经网络就是要连算很多层卷积。而实现卷积的Verilog代码其实很简单,比上个视频讲的《RGB转HSL的FPGA实现》要简单不少,所以根本不需要用HSL来写卷积。在开讲之前先来回顾一下“FPGA图像处理的一些基础知识,FPGA是如何实现最高实时性的?相比于GPU的优势在哪?”这个视频中的一点内容。
v2-2dfd2ba97f11878ad5fb91e09543547d_720w.jpg
​视频里引用的这张ppt告诉我们读写DDR的能耗是非常高的,而且延时也较大,还有带宽限制。用FPGA加速卷积等各种运算,就是要充分利用FPGA的片上存储资源,比如让第一层卷积的结果缓存在片上Ram里就开始第二层卷积,这样就不需要把每层卷积的结果存回DDR再读出来进行下一层的计算。这样既提升了速度又降低了功耗,也减轻了DDR带宽的压力,不让它成为瓶颈。
v2-2aecdfd003c45a05d094a56c63802834_720w.jpg
[color=inherit !important]https://github.com/becomequantum/Kryon​github.com/becomequantum/Kryon
上图对比了错误的和正确的FPGA进行视频流处理的方式。FPGA在工业应用场景中往往是直接连接图像传感器获得视频流数据的。FPGA要进行的就是实时图像数据的处理。如果一上来啥也不做,就把图像数据存到DDR里,然后再读出来让FPGA进行处理,那这显然是错误的做法。因为这样一写一读,浪费了很多DDR带宽却啥处理都没有做,纯粹就是无意义的操作。再说很多图像处理应用场景中,原始图像数据是不需要保留下来的,只需要最后输出的结果就行了。比如进行实时视频流压缩,最后只需要输出压缩后的视频流。再比如之前视频中讲过的物体识别,最后也只需要一个识别结果。所以用DDR缓存原始图像数据几乎就是没有必要的。
v2-df73fc2b7b7edeee529185f5d6259bd8_720w.jpg
​正确的处理流程是图像数据进来后用FPGA片上Ram进行缓存、并进行处理,一级处理的中间结果仍然缓存在片上、继续进行下一级的处理。也就是进行多级流水线处理。如果片上存储够用,那整个流水线处理过程中就没有中间数据需要写入DDR缓存再读出来。多层卷积就是能这样进行流水线处理的典型例子。我没有用过HLS,不知道用HLS实现的卷积运算流程是怎样的,如果需要经常把中间结果写回DDR再读出来,那就和CPU运算差不多了,没起到多少加速效果。
v2-ad2c00595ca31106e3b5244cb9ffbce6_720w.jpg
​在讲卷积的Verilog代码之前先来讲一下做图像处理仿真所需的测试激励代码。上图这个代码可以直接读取bmp位图文件里的图像数据,并生成类似VGA时序的仿真波形,用来给卷积等图像处理模块提供测试激励。还可以采样模块输出的结果并写入到另一个位图文件中。这样我们在跑图像处理相关的仿真时,就只需要先把测试图片放到ISE或Vivado跑仿真的目录下,等仿真跑完就可以直接打开输出的位图文件查看处理结果了。要知道跑仿真是比较慢的,所以测试图片不要整的太大,能说明意思就行。

在做这个视频之前,我也在网上调研了一下讲用FPGA实现图像卷积的文章或视频,这些资料里讲的基本都是用移位寄存的IP来实现一行图像数据缓存的,好像就是上个视频讲的Ram based shift register。这个IP用的是分布式Ram,也就是用LUT、寄存器等片上逻辑资源搭起来的Ram,这也就意味着用它要消耗片上的逻辑资源。而Block Ram这是另外专门的硬件Ram资源,用它不消耗片上逻辑资源。

它们在使用上的区别就是:分布式Ram小巧灵活,适用于少量数据的存储。Block Ram大而固定,大量数据得用它。Block Ram一般一个的大小是18×1024,或是9×2048。要用就是一个,用不满剩下的也是浪费。所以要卷积的图像一行只有几百个像素,是可以考虑用分布式Ram来缓存,但要缓存的行数太多也不行,因为片上逻辑资源都被拿去当Ram缓存了,就没有别的资源用于计算和走线了。如果FPGA连接了图像传感器,那采样到的图像一般至少都是1k或2k宽度,这时肯定就需要用Block Ram来缓存了。所以本视频要讲的就是如何用Block Ram缓存图像来实现卷积。
v2-afb3c4cf28b84edc514de6a58d9b16ab_720w.jpg
​上图是以3×3的卷积为例,展示的卷积运算流程图。首先需要知道的一个知识点是,卷积所需缓存图像的行数、等于卷积算子高度减一。所以3×3的卷积只需要缓存两行图像数据,5x5的需要缓存4行。那为啥缓存的行数可以比算子高度少一行呢?看上图就知道了,因为当前正在输入的数据也是一行,加上读出来的之前缓存的两行数据,就形成了三行图像的数据流。把这个数据流移位寄存进3×3的寄存器阵列,每个周期9个寄存器都会乘以相应的参数、得到中心像素点的卷积结果。

这个每个周期串行移位寄存,并且并行计算9个乘法的流程还是很好理解的。需要重点了解一下的第二个知识点就是当前输入的图像数据是怎么存到Ram中去的,因为缓存了两行图像,所以每个图像数据都要被缓存两行的时间。这个过程由上图中绿色斜向上的箭头示意,当前输入的数据在被移入寄存器阵列的同时也被写入了下面这行Ram,同时下面这行Ram读出的数据被写入了上面这行的Ram。上面行读出的数据没有被写回,因为它已经不再用得着了,就算是被丢弃了。也就是说三行数据在右移进寄存器的同时还进行了看似的上移,下面两个数据移入上面两行Ram中存着。
v2-7768b3f1cef98959adc26f67be82e735_720w.jpg
​这整个一套操作我在写代码时把它分成了三个模块,其中“Ram控制模块”负责图像缓存Block Ram的读写控制,是核心模块。“Block Ram模块”是要我们自己生成的,Ram的宽、高参数是根据实际需要来的。“算子模块”是顶层模块,里面除了写寄存器阵列移位和计算的代码外,还例化了“Ram控制模块”和“Block Ram”模块。
v2-63520b46ba3ad191f3c5195cc1ede1ce_720w.jpg
​先来看比较核心的Block Ram控制模块,我给它起的名字叫“LineBuffer”,全部代码都在上图,除了参数和输入输出外,干活的代码其实没有几行。这是一个完全参数化的模块,里面的主要变量的位宽都是根据参数变化的。在例化这个模块时要设定好三个参数:DATA_WIDTH、数据位宽,ADDR_WIDTH、地址位宽,还有OPERATOR_HEIGHT、算子高度。

算子高度就不用解释了,数据位宽指的是每个像素数据的位宽,二值化数据它的值就是1,八位灰度数据它的值就设为8,二十四位RGB数据它就是24。地址位宽和所需处理图像的宽度有关,2的11次方等于2048,所以宽度小于2048的图像这个值就设为11,小于1024的就设为10。通过设置不同的参数,这个模块可以适应不同算子大小,不同数据位宽的各种卷积运算缓存控制需求。

模块的IO信号分为三个部分:DataEn和PixelData分别是输入使能和数据信号。如果是三个8位的RGB信号,可以并成一个24位的信号放在这里输入,等到算子运算模块时再把它拆开算就行了。中间的addra到dinb这几个信号就是用来控制Block Ram的读写信号。最下面的两个则是算子数据信号和使能信号。需要进行移位寄存的就是这个算子数据信号:OperatorData,它包含了算子高度个像素数据信号。下面具体的实现代码就不细讲了,它做的事情其实也是把Ram里的数据读出来再写回,和前面《FPGA如何实现图像直方图统计?》这个视频里讲的对Ram的操作基本是一样的。 注意比较输出的算子信号OperatorDataReg和写回Ram的数据信号dinb,就知道前面所说的看似向上移位是怎么实现的了,在OperatorDataReg中新来的数据被放到了高位上,所以在写回的dinb信号中丢弃了处在最低位的那一行数据,这一行数据就是上图中最上面一行的数据。
v2-c71012f232955b9f36fe905a76168cd6_720w.jpg
​再来看顶层算子模块的部分代码,上图这个代码实现的是9×9的二值算子,在《FPGA图像处理中二值算子的一些妙用》这个视频中已经讲了二值算子的诸多用处。由于是9×9的二值算子,所以数据位宽参数设为1,算子高度参数设为9,地址宽度设的是11,可以适用图像宽度小于2048的情况。在这个模块中还要例化LineBuffer 模块和需要我们自己生成的Block Ram IP模块。在这个例子中所需要的块Ram深度自然是2048,而Ram位宽值则是要大于等于数据位宽乘以算子高度减一的差。所以本例中所用的Ram宽度就是9×2048的,其实是8×2048就可以了。
v2-4f017d49363e1700c7b6cd2adcd0779e_720w.jpg
v2-adb0d56c9b5e4351f6b4e19e75554c2e_720w.jpg
​代码中一堆Array值相加其实就是在把九九八十一个算子寄存器的值加起来,下面InBetween这个结果当加起来的和大于等于33时就为1。这个运算干的事情就和“无限次元”这个软件中“阈值胀蚀”这个功能是一样的。把在“阈胀蚀”右边的直径设为9,阈值设为33,得到的结果就会上面这个Verilog模块仿真的结果一样。大家拿到了代码,整个二值图像跑一下仿真,再和“无限次元”的处理结果对比一下就知道了。当然我在代码里直接写个33其实是偷懒的行为,因为这个阈值应该作为此模块的输入,可以由上位机进行调整的。这个阈值可调到是好弄,那要是想算子半径也可调该怎么改代码呢?
v2-cf80ef63c11464fd3491d4006993001d_720w.jpg
​最后再来看一个灰度图像3x3算子卷积的部分代码,长得和上一个差不多,最关键的地方就是要把前面的两个参数改一下,数据宽度改为8,算子高度改为3,此时需要的Ram宽度就是大于等于16。下面就是对算子数据进行各种运算的代码,再下面还有对寄存器进行移位寄存的代码没有展示。这只是进行一层卷积的代码,如果想对结果再进行一层卷积也很简单,只要整两个卷积模块,把第一个模块的输出的结果数据和使能信号接到第二个模块对应的输入中即可。
本集视频就到这了,视频中的代码都是亲测可用的,暂时还没有传到库里,等B站有八千粉了再传,想早点看到代码就请帮忙点赞转发,帮俺涨粉,谢谢啦!​

发表于 2022-3-16 10:28:42 | 显示全部楼层
好东西,实战讲解,还有视频
发表于 2022-3-16 10:52:06 | 显示全部楼层
谢谢分享
发表于 2022-3-28 14:32:44 | 显示全部楼层
先赞再看,良好习惯
发表于 2022-4-1 08:23:19 来自手机 | 显示全部楼层
楼主厉害,我也学习一下
发表于 2022-4-1 16:54:07 | 显示全部楼层
great
发表于 2022-4-1 17:08:54 | 显示全部楼层
好东西
发表于 2022-4-1 21:19:56 | 显示全部楼层
mark 下
发表于 2022-4-1 22:49:55 | 显示全部楼层
难得的技术贴,赞美楼主。
发表于 2022-5-31 13:22:12 | 显示全部楼层
学习学习
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

站长推荐 上一条 /2 下一条


小黑屋| 手机版| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-12-23 20:49 , Processed in 0.031621 second(s), 8 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
快速回复 返回顶部 返回列表