连通域标记算法FPGA流水线实现思路-无需DDR缓存图像

视频在这,有些内容需要看视频:

连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_哔哩哔哩_bilibili连通域标记算法的本质,如何用FPGA实时流水线的实现?连通域算法的用处和局限性github.com/becomequantum/Kryonhttps://www.bilibili.com/video/BV1c94y1Z7dV用FPGA实现连通域识别算法我之前做过两个版本,还写过两篇文章、做了一个动画视频。但之前写的这两个版本最终在逻辑上都还有点问题,写第二个版本时就是想解决第一个版本遗留下来的问题,但最后改进的还是不够彻底,有些方面可以说还退步了。上面这个动画视频展示的就是第二个版本的算法流程,现在我已经明白这个流程是有些问题的了,所以大家不用再仔细研究这个老视频了,我会在这个视频里把连通域算法的本质和FPGA实现它的难点在哪讲清楚,最近我已经把逻辑完备版的Verilog代码调通了,大家看这个视频的讲解就行了,之前写的文章也不用看了。

连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第1张图片

先来讲一下好几年前我要做FPGA实现连通域识别算法的动机,其实就是当时的项目需求,FPGA要处理的是线阵相机传过来的图像数据,在“FPGA图像处理的一些基础知识”这个视频中已经讲过,线阵传感器传来的图像是没有帧的概念的,而当时项目里用到的FPGA也没有接DDR,只能用片上Ram缓存有限行数的图像。

连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第2张图片

当时也调研了一下连通域标记算法,自然也看到了类似上图所示的文章。其中这个Two-Pass法我当时一看就觉得不适合FPGA实现,因为在PC上对于有限高度的一帧图像来说,是可以扫描一遍再扫一遍。但FPGA要处理的线阵图像就没有帧的概念,图像数据流过一遍就没了,没法实现缓存一帧图像之后再扫描一遍。所以只能想只需扫描一遍,能用实时流水线模式处理图像的连通域识别算法。后来我也把这个算法弄出来了,但从现在的视角看,我并没有发明一种新的连通域识别算法,只是相当于把这个两遍扫描法改造了一下, 把原来扫完一遍图像再扫一遍的扫描模式改了,改成了“冰壶两把刷子”模式。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第3张图片

 也就是FPGA里面的流水线连通域识别算法从逻辑上说自然也是需要扫两遍的,但流水线模式的意思就是扫完了一遍紧跟着就扫第二遍,就好比冰壶比赛里的那两把刷子一样。这样这个流水线算法只需要缓存两行图像信息就能实现连通域识别,并能在一个连通域完结后的下一行输出对这个连通域的识别统计结果。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第4张图片

上位机里也可以写这样流程的算法,但要比最简单的递归法稍微复杂一点。先来讲一下连通域识别算法的本质是什么。如上图所示,在二值图像中,如果是考虑八邻域的话,我们把一个像素点看作是一个图节点,那么它就和它周围的八个节点有连接。这其实就是把二值图像也看成了图论中的一张图,而它的确也就是一张图。图论里有图遍历算法,连通域识别要把和一个节点有直接或间接连接的节点、也就是邻阈像素点都找到,其实就是在做图的遍历。图的遍历用的就是递归遍历算法,那么连通域识别算法自然也能用递归遍历的方法来实现。上面那篇文章中说的第二种Seed-Filling法,里面要用到堆栈,一提起堆栈是不是就想到了递归呢?所以这个方法的本质就是递归遍历法。扫描两遍的方法的本质其实也是递归法,可以说它是把递归法展开了,所以看起来要稍微复杂一点。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第5张图片

所以连通域识别算法最简单的实现方法就是用递归遍历,上图就是“无限次元”这个软件中递归连通域识别的代码。先是一个双循环遍历图像,发现一个前景点就调用会递归调用自己的“递归标邻域”方法进行递归扫描标邻域。两部分核心代码加起来才大概十行,是不是最简单呢?

网上介绍的哪种算法看起来都比这复杂。当然有人会觉得递归算法本身就比较难以让人理解,大家都不喜欢套娃,都想禁止套娃。我当初在大学里最初接触 递归算法时也觉的递归算法很难理解,就觉得算个阶乘写for循环不好么?为啥非要整递归呢?后来才自己研究了算法后才明白,线性结构的遍历的确是只用循环就可以了。但对于有各种分支结构的图遍历,还是递归好使。这时如果禁止套娃,也就是不准用递归法来写,那你就会发现这才让问题变得更加难了。连通域识别算法也是这样,不用递归法写就是要复杂一些。所以大家学算法时一定要多在头脑里思考一下递归算法的执行过程,多写写递归算法,跨过觉得递归难的这道坎。递归法在有些应用场景很简单,禁止套娃才是更难的。

我们知道图的递归遍历算法有深度和广度优先遍历这两种,那请问上面这个递归遍历算是哪种呢?

上面是递归法连通域识别的过程演示。在“无限次元”软件中打开一幅二值图片,在中文输入法没开启时按加号可以进入放大显示模式。在这个模式下把“显示过程”勾上再点左边的两个按钮就能看到这两种连通域识别算法的扫描过程演示。递归法的特点就是遇到一个连通域就会把它扫描完,它的效率其实并不高,也不适合FPGA实时流水线实现。效率不高是因为它扫描每个点时都需要进栈和出栈操作,会有很多冗余。

逐行扫描法,也就是前面说的流水线两遍扫描法、则是一行一行的来,一行中有好几个连通域时、它也会按顺序分别扫描和判断是否和上一行有连通。它的执行流程简单的说就是先进行横向一维的连通域识别,再进行纵向二维的连通判断。它的效率会更高一些,也需要查表进行连通性判断,但不是每个点都要查,而是每个一维连通域才需要查一次。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第6张图片

上图是逐行扫描法所需要的两个记录信息的类。下面的是用来记录单行连通域信息的,只需记录一行中连续前景点的起始和终止坐标,以及在进行和前一行的连通判断之后分配给它的标号即可。上面的是用来记录整体连通域信息的,一个连通域完结后输出的就是总点数、边框坐标等统计信息,也能再顺便统计一些其它信息,后面会说。最后两条信息一个是用来判断连通域是否完结用的,“真实标号”其实是就是个链表指针,用于指向另一条连通域信息。它的用处我们来梳理一下流水线扫描法的处理流程就知道了。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第7张图片

动画去视频里看 

大家请看上图,并把自己的大脑切换成电脑,开始执行算法。当双循环遍历到最上面这行最左边的这个点时,会先记录它起始点的坐标位置,等这一行后面和它相邻的点没有后就会记录它结束时的坐标。然后算法会去查上一行里的连通域信息,此时上一行信息表里啥都没有,所以算法就会给这个连通域一个新的标号,假如就是从1开始的,那这个单行连通域信息就会被给与标号1。接下来会在整体连通域信息表的第一条位置里写入这个新建连通域的信息,此时它的“真实标号”会被设为-1,代表它并没有指向其它连通域。标号其实就是查连通域信息表的索引。

对于这一个新连通域的处理到这就完了,这一行后面两个点也会依次这样处理,被标上2和3。等这一行完事之后刚才记录的这三条当前行信息就会变成上一行信息。这时再扫到下面这行的最左边点时,上一行信息就不是空的了。算法会去读取上一行信息,并判断有没有哪个连通域是和当前这个连通域是连通的,结果是有,就是上一行的1号。那此时这个点就会被赋给上一行的标号1,而不是新建一个,它的信息也是被统计进了连通域信息表的1号条目中。到目前为止这个算法流程看起来还是挺简单的,无非是一横一竖的二维线性扫描。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第8张图片

直到扫描到下面这行第二个点时问题就来了。这时算法会发现上一行有两个点和它连通,而且标号还不相同。也就是说这个点的出现把在上一行并不连通的两个点给连起来了。正是因为这种情况的出现,才让连通域标记算法变复杂了。因为它让连通图出现了分支结构,不再是简单线性结构了,如上图所示。那这时算法该咋办呢?按照先后顺序,它应该先给下一行的这个点赋予标号2,并把它的信息并入到连通域信息表中的第二条。然后再来处理下面这个2号点遇到右上3号连通域的情况,这时算法会把3号连通域的信息并入到2号,再把2号信息中“真实标号”这个指针的值改为2,意思就是我的信息现在已经无效了,已经并入到2号中了。这其实就形成了一个3指向2的链表结构。

对下面这行第二个点的处理到这就完成了。再来看下面这行第三个点。上面的算法流程其实少说了一件事情,那就是通过上一行单行信息中的标号去查连通域信息表时,要先检查查到的信息中“真实标号”是否有效,也就是看看这条信息是不是已经被合并走了。如果是,那就要继续用“真实标号”去查表,而且要一查到底,直到查到“真实标号”无效的那条信息为止。为啥这里要一查到底呢?而不是查一次就完了呢?那是因为刚才说的链表的长度是有可能大于2的,也就是在3指向2之后、可能还会继续有4指向3。所以要一查到底。那在什么样的图像结构下这个链表长度会大于2呢?大家想想看。

当下面这行第三个点在查连通域信息表时就会遇到上述情况,先是会查到3号信息、然后发现它已经指向2号了、再去查2号、查到底了,于是算法会把标号2赋给这个点,并把它的信息合并到2号信息中,而不是3号。这个查链表的操作就是前述两遍扫描法中第二遍扫描所做的事情,就是把一开始被分配了不同标号,但后来又发现是相连通的连通域信息合并起来。流水线实现的不同之处就是,在当前行发现这个问题了就去合并,是实时的去解决了这个问题,而不是等一帧第一遍标记完了再扫第二遍去解决这个问题。

而这个查表要查到底,就是我前两次写的FPGA版本都没有完全实现的地方,写第一版本时还没完全意识到这个问题,直到现在才终于把这个问题梳理明白了。其实查表查到底这个事情在上位机和FPGA里实现都不难,上位机里写个While循环就可以了,FPGA里用双口Ram就能简单实现。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第9张图片

上图就是流水线扫描法的主流程代码,就是一个双循环,看起来也不复杂,复杂的地方都隐藏在了画红框的那两个方法里。其中下面那个方法是在当前行扫描完成之后再去遍历一遍上一行连通域信息,看看有没有完结了的连通域,如果有就把它的信息添加到已完结连通域列表中。在FPGA代码里则是会升起一个周期的输出使能信号,把信息输出交给后面的模块处理。这里面的问题就是该如何实时的判断一个连通域是不是已经完结了。这个问题在有帧概念的图像数据中可以被绕过,因为等一帧扫描结束了所有的连通域肯定都已经完结了。但在实时算法中却不能等帧结束,因为就没有结束的时候。那这该咋判断呢?上面这个C Sharp代码后面有写,不过要等B站1万粉了再开源,在这之前大家先想想吧。提示就是在当前行判断不了,要等到连通域完结后的下一行,所以上面的代码是在当前行结束后去判断上一行的信息。

上面代码中的上一行和当前行单行连通域信息表在FPGA里可以用FIFO来实现,“连通域统计表”可以用一个比较宽的双口Ram来实现,查表、合并数据都是对这个Ram进行读写操作。算法流程的实现则要写状态机。既然在FPGA里那两个表可以用FIFO实现,那为啥在C Sharp里没有用队列Queue,而是用了List呢?这是为了偷懒,上面的C Sharp代码其实要比Verilog简单一些,简单就简单在“找出完结连通域”这个函数被放在后面执行了,这时它又要遍历一遍“上一行信息”,其实连通域完结判断是可以放到“连通域判断统计”这个方法中一起按照线性的读取顺序完成的,这样做就只需顺序读取上一行列表一遍,那自然就可以用FIFO替代列表。只不过这样写要把流程想清楚比较费脑子,所以在写C Sharp代码时就偷懒了。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第10张图片

如果想尝试写FPGA实现代码,那一定要先把上位机版本的代码写出来,毕竟在上位机上把逻辑流程调通更容易一些。写上位机流水线版本时,可以用递归法的标记结果来验证流水线版写的对不对,因为递归法原理简单,我们可以非常确信它的结果不会错,所以可以把递归法的结果当做标准答案来检验其它方法的结果,如上图所示。连通域标记算法FPGA流水线实现思路-无需DDR缓存图像_第11张图片

 最后再来讲一下连通域识别算法的一些用处,如上图所示,连通域识别算法的主要功能除了识别出一个一个的连通域之外、就是统计每个连通域里的各种信息。比如边缘点数、也就是周长,总点数、也就是面积。这是可以分开统计的,还能顺便统计连通域内部某种类型的点数目,比如上图左边矩形连通域内部的蓝色和红色区域点数,也是可以分别统计出来的。连通域质心的位置也可以经过统计之后再算出来。

但如果只有点数之类的统计信息,有些事情也判断不了。比如想识别一个连通域的形状、是三角的、是方的、还是细长的,单凭连通域识别就还做不到,或许再加一些后续分析边缘点坐标的算法可以做到,但我一直也没想出来这用FPGA该如何实现。但用FPGA能实现的二值大算子法也是可以识别简单形状的,这点在“FPGA图像处理中二值算子的一些妙用”这个视频后面已经讲过了。

上面展示的C Sharp流水线版代码会在B站一万粉之后开源,这个新版“无限次元”的可执行文件和这个测试图已经上传了。Verilog版暂不开源,至少等到哪天能拿到小电视了再考虑。还有关于FPGA和Verilog的科普视频也暂时停更了,俺要继续研究人工智能,也就是前面“反思神经网络”这系列视频提到的未完工作。

你可能感兴趣的:(fpga开发,图像处理)