架构的建立一直是一个重中之重的问题。在初期学习FPGA或者ZYNQ的过程,我们大多数人都是通过网上开发板厂商的例程走,有些厂商的代码书写风范不错,但是整体架构过于冗余,有的厂商代码冗余,却整体架构不错。不是点名某个具体的厂商,而是真的有此类情况出现,许多新入门的小伙伴学着学着就弄糊涂了,到最后买了一块板子回来学习,由于架构太过于复杂或者是代码风格太冗余,到后面只会看代码,自己真正去做却无从下手,特别是在bd里面密密麻麻的ip核,无比复杂的连线,有时候搞得头都晕了,对于初学者无比的不友好。要知道,我也是从初学者过来的(当然现在也是刚刚入门),很多厂商的代码只告诉了你这个ip核需要放在这里,但是为什么要这么放,这个ip核为什么这么配置,怎么跟你的初始化结合在一起的,能把这个东西讲清楚的少之又少!而并非每个人都是天才,都能很快的掌握并去学习,有时候你连学习的目标都没有,找问题都不知道从何入手,FPGA是这样的,甚至有时候你都不知道问题从何搜起。为了能够更加清楚我们怎么样去搭建属于自己的一套图像通路,这篇文章希望能帮你解惑。
首先,我声明一点,图像处理框架中的部分核心地方,需要你自己进行搭建,我的框架与我自己个人习惯相关,且部分代码非我个人撰写,带有人家的知识产权,故无法进行公开,我尽可能地呈现这一套的流程出来。(需要学习的请找公子哥企鹅号793569530)
图像框架这一方面,由于我主要学习的是ZYNQ系列,因此我将以ZYNQ作为图像框架的呈现单元,FPGA只需将PS端改成FPGA去实现即可。在使用ZYNQ开发板的过程中,许多厂商喜欢将所有的代码封装成ip弄进bd里面,这种方法的优点是,你移植起来不困难,只需要按部就班的把所有ip核加入进去,然后连线即可。但是缺点同样有,当调用ip核数量过大时,屏幕上密密麻麻的都是线,对于移植起来脑袋都会看晕,而且还有一点,一旦你修改了其中某个ip核的参数,就需要重新编译并且generator,这个挺占用时间。且这种bd里面进行操作的方式,很难形成属于你自己的图像架构,所以我的建议是:bd里面只放核心的zynq核,中断信号引出,EMIO信号引出,DDR信号引出,PS和PL端少量数据交互的ip核,将视频流数据接口引出。这样整体的界面就会非常的简洁,如下图所示:
我不知道你们怎么看待这种信号引出的方式,但是我这个框架是我最喜欢的!比如我在做图像数据流传输的时候,若需要多增加一路图像进行交互,我只需要增加AXI_Interconnect核的输入信号即可,再将名字重新赋值,在source界面例化一个dma传输信号即可。并且这种架构,用到的ip核体量较小,且引出的基本是接口,所以编译时间非常短,不会影响到我们整体的工程编译时间。而且这种架构定下来了以后,bd基本不需要有过多的变化即可实现视频流数据的交互(大批量的数据)、控制数据的交互(小批量的数据)、突发数据的交互或控制(中断)。
外部采用顶层至上而下的设计方式,一条图像通路走下去。首先是图像数据的采集,其次是图像算法的处理,再就是送入DDR缓存之中,这个缓存是在PS端的,因此我们可以在PS端通过CPU做操作将DDR中的数据读取出来,比如做SD卡存储,做以太网传输。同时DDR中也设置了读出的通道,因此我们可以在PL端也同样进行一些数据的交互,比如HDMI显示,PL端以太网视频流数据传输等等。整体代码设计都放在top顶层文件中,图像流程从采集输入,到做图像的预处理,执行图像算法通路,经过图像算法处理过的图像视频流数据通过DMA模块将数据存储至PS端的DDR中,HDMI等图像视频流输出接口则通过VGA_CHOICE等模块实现从DDR中读取图像数据,并转为HDMI等显示器件的数据流格式。
RGB565视频流数据输入和Bayer阵列数据输入应该是我们新手遇到过最多的视频流数据输入了,尤其是RGB565,在OV5640摄像头采集图像数据的过程中,往往通过寄存器配置采集格式为RGB565,这种数据流格式意味着我们需要两个时钟周期才能得到完整的一个像素点数据,为什么需要两个周期呢,因为RGB565这个数据格式已经表明了三基色就是红色绿色和蓝色按照五位,六位以及五位的格式输入,由于一般的摄像头为八位并行输出的输出,所以想要输出十六位数据就需要两个时钟周期。因此,我们采集了RGB565数据后往往需要做一个数据和时钟域的同步,因为有些摄像头上的时钟信号是通过24M的晶振产生倍频而来的,与我们FPGA中的内部时钟并不属于一个时钟域,所以我们在采集了视频流数据进来以后通过FIFO做一个跨时钟域的操作,具体操作就是读时钟速率一般比写时钟要快,当输入进来的数据写入FIFO到达指定的数量级后,再将其读出,这样输出就容易弄成我们习惯的视频流数据输出格式。
Bayer阵列图像也是比较常见的,很多摄像头用的就是原始的Bayer阵列数据数据,Bayer阵列输出格式下每一个像素点大小只有8位,因此我们一个时钟周期就可以读取一个像素点的数据,相对于RGB565,他自然拥有速度上的优势。但是相对应的,他出来的数据是以灰度的形式存在,而且图像会带有一定的小点,用肉眼都可以看的清楚,因此我们为了让Bayer阵列输出的图像数据转换成我们人肉眼可见的彩色数据,就需要通过去马赛克算法,将Bayer阵列数据转换成RGB888数据,再做后续的处理。由于我们的FPGA天然具有流水线架构的优势,因此我们不需要耗费多少的资源,最终就能够将Bayer阵列转换成我们需要的彩色数据。如果对速度又要求,就可以采用Bayer阵列,只不过需要通过算法将其还原,后面会针对性的讲一些算法。
本设计中提到过图像预处理过程中有一个时钟域同步,可以通过部署FIFO的方式,将输入进来不规则的数据转换成我们制定的视频流数据格式。无非就是VS,DE以及DATA数据,其中VS代表我们这一帧开始的信号,此处我的习惯在设计中将输入进来的VS进行反相,让使得视频流数据输出的时候,VS能够包裹住数据。例如OV5640的时序就规定了,VS信号出现上升沿时代表一帧的开始,拉高一段时间后再拉低,我们将VS信号进行反向,这样下降沿就代表一帧的开始,图像视频流传输过程中VS一直是拉高的。DE信号就是代表我们的数据有效信号,当DE信号拉高以后,就代表我们就已经正常输出像素数据,前文由于提到了RGB565格式下,每隔两个时钟周期才能够输出一次有效数据,因此DE信号在输出阶段基本上是每隔一个时钟周期反向一次的,为了让DE信号始终处于高电平状态,我们就通过时钟域同步的方式,将不连续的数据转换成连续的数据进行输出。只要充分利用好了FIFO的先进先出功能,就能够实现时钟域的同步,更有利于我们的时序。
去马赛克是专门针对Bayer图像输出的,简单说明一下,Bayer阵列输出的数据虽然是八位数据,但是它可以通过这个数据四周的数据进行算法,从而将此像素点对应的RGB888数据推算出来,具体的公式就被称之为去马赛克,而且不同行不同列对应的算法不同,大致算下来就四种格式,后期会专门开一期对去马赛克算法进行一个讲解。通过这个算法我们就能将Bayer图像转换成RGB888彩色图像,为了减少传输过程中的带宽,我们一般最终进行彩色图像传输的过程中只需要RGB565的格式,所以从中进行截位即可。
色彩空间转换的意义是将彩色图像转换成灰度值,也就是俗称的色彩空间向亮度空间转换,亮度空间可以反馈出我们很多的数据信息,因此很多图像算法都是在色彩空间转换后的灰度图数据做一个运算,比如我们常用sobel边缘检测,直方图均衡化等等,都可以在灰度图上面做文章。由于灰度图只有八位数据,对我们的算法数据量压力也比较小,不然动辄二十四位数据进行算法,执行起来也会比较困难。而且灰度图可以反馈出很多特征数据,所以色彩空间的转换是我们必不可缺的基础算法。
滤波的意义在于,我们采集进来的图像往往存在许多的噪声,这些噪声往往会对图像的展示产生影响,为了减少这种影响,需要通过图像滤波的方式对图像进行处理,以求减少这些噪声带来的影响。比如椒盐噪声,这些散点分布在图像平面上,周围都是正常的图像数据,因此通过中值滤波的方式就可以解决这个问题,通过排列当前宫格内的多个数据,选出中位值进行赋值,这样可以避免这个图像中出现突出的点。还有的一些噪声,比较高斯噪声,有的相机拍摄过程中就会出现这个问题,我们就可以通过高斯滤波的方式去解决这个问题,总之常用的滤波方式就几种,算是比较基础的:均值滤波,中值滤波,高斯滤波等等。
我在这里就举几种典型的,也是比较常见几个大类中比较典型的。
这个算法本质上就是通过对像素图片进行灰度值的统计,统计以后再根据灰度值所占的比例进行拉伸,使其能够均衡的分布在图像的整个灰度范围,这样就可以有效解决某个图像过亮或者过暗的现象产生。其中用这个算法进行去雾操作就是很好的一个应用点,用我们人眼去观察雾中的图像总会觉得朦朦胧胧的,似云似雾的感觉,但是当我们用图像进行算法的时候,就能够将其中细小的像素点给提取出来,最终还原给我们一幅清晰的图片。这个算法需要我们进行图像的灰度化,直方图统计,最终实现灰度均衡,相当于几个小算法的叠加,后期也会开一篇文章详细讲讲思路。认真的告诉大家,图像算法难的一点,或者说FPGA去实现算法难的一点不在于算法本身,更多的在于数据调度的这个过程,以及时序的优化。所以说,在实际学习的过程中,多提升自己对于数据调度的能力,对自己是有很大帮助的。
缩放算法其实属于图像算法中的几何变换,除了缩放,也有对图像进行水平或者垂直镜像,对图像进行平移等等。这些算法往往是对整幅图进行图像的运算,根据图像几何变换的算法求出这一帧图像中新的像素点,比如缩放算法的精髓就在于不同的插值算法,我所学习的是双线性插值实现图像的缩放。本质上就是通过老的像素点求出新像素点所对应的位置,位置求出来了以后再求出对应的灰度值即可。在这个过程中需要熟练的运行数据的调度,明白这个像素点新的位置和新的灰度值,再通过FPGA去实现这个数据的显示。只需要我们摸清楚了数据的带宽大小,就能够实现整体图像的缩放,实现图像的几何变换。
边缘检测属于一种较为常规的特征提取方法,一般边缘检测是在灰度化图像基础上进行运算的。边缘检测有很多的方法,也有许多对应的边缘检测算子,比如sobel边缘检测,其构造了X方向和Y方向的卷积矩阵,将原像素N宫格之内的像素乘以N宫格卷积矩阵,最终运算得出的结果就是经过边缘检测以后的值。主要目的就是把灰度值迅速变化的变量提取出来,我们知道物体的边缘特征处往往伴随着灰度值的急剧变化,比如我们去看一个盒子,那么盒子的边界处必然是与外接有所去别的,不然我们也不可能很快的分辨出盒子的体积轮廓,那么这个灰度值迅速变化的区域线被我们通过边缘检测算子检测到后,人为设定一个阈值,当大于这个阈值时对应像素点变纯白,小于这个阈值后,对应像素点变纯黑,这样我们就能够得到一幅黑白轮廓分明的边缘检测图。
白平衡算法也是我们用得比较多的,归根到底白平衡其实属于图像变换中的色彩分析,有时候我们通过摄像头拍摄出来的图片容易呈现偏三基色其中一个颜色的现象,这个时候为了让我们人眼看的更舒服,就需要平衡三个色彩的占比,得到一个较为友好的数据图像。常见的有基于灰度世界的白平衡算法,本质上就是获得RGB三个通道的增益,拟定三个颜色都是接近于中间阈值128,通过这种方法得出RGB三通道增益,再乘以原先的像素数据,最终得到一幅经过白平衡后的图像。无论是在工业相机还是机器识别中,白平衡对于我们获取一幅具备良好观感的图像至关重要,也可以相对应的做一些工作,实现比如暖光图片,冷光图片等等。
这个算法其实属于图像分割中的一种,图像分割的主要目的就是为了提取有效的特征,如前文提到的边缘检测算法,辅助我们进一步进行图像的识别。霍夫变换的英文名称叫做Hough变换,一般是用来检测直线或者检测圆的,霍夫变换的本质就是进行特征的变换,将一条直线的方程式y=kx+b,变换成k和b的表达式,比如b=-kx1+y1,b=-kx2+y2。这样在我们原先的空间的一条直线就会变换成霍夫空间的一个点,反过来霍夫空间的一个点就会变换成原先空间的一条线。根据这个原理我们把霍夫变换引入极坐标的概念,最终针对一个像素点进行0-180度的遍历获得,通过极坐标的参数空间,将平面散点一一映射到其中,再从中找出相交最多的点,则为散点集中的直线,也就是多个点同时在一条直线函数的坐标。
图像压缩算法其实是为了传输过程中传输更多数据量的图像数据,通过对图像进行编码进行压缩,可以有效减少和描述图像的数据量,以求能够节省图像传输过程中的处理时间和传输过程中占用存储器的容量。压缩的话并不一定要求完完全全还原,也可以允许在失真条件下进行,而编码则是图像压缩算法中最为核心关键的地方,一个好的编码技术可以大大增大图像压缩的倍率。像博主使用的ZYNQ Ultra+mpsoc系列开发板中的EV系列,里面就含有视频编解码的硬核VCU,专门对图像数据进行H264/H265编解码,其中后者压缩倍率可以到到一百倍以上,使其我们能够实现4K视频的60帧稳定传输。
当你的图像经过预处理的步骤处理完毕以后,最终肯定要送入缓存中进行缓存。由于我常用的是ZYNQ的架构,ZYNQ的PS端有一片DDR,与PL数据交互的方式就是通过将视频流数据用AXI总线传送到DDR中去,这个过程中设计到视频流数据的采集,已经信息的流转。前面我们已经提到了数据流能够正常的采集并已经规范好了固定的视频流数据,那我们就通过FIFO的方式,写的速度比读的方式要慢,当写入了一定量的数据后,自动将内部的数据通过AXI总线传输至PS的DDR,后面的事情我们可以通过CPU,通过cache去读取PS端DDR中内容,也可以直接在PL端通过我们自定义的AXI_DMA函数的读出接口将数据读出。
若你的HDMI显示为芯片控制,则需要在初始化之前在PC端进行初始化。
若为普通的串并转换,那就建议调用市场上常见的HDMI驱动源码,不需要弄得太清楚,只需要对着versal标准规范好视频流数据的格式即可。
今天吃的一碗鱼粉,味道不错,就是酸笋太酸了,差点没酸死