FPGA实现的实时流水线连通域标记算法

本文要介绍的是两年前我自己琢磨出来的一种用FPGA实现的二值图像连通域标记算法。这个算法的特点是它是一个基于逐行扫描的流水线算法,也就是说这个算法只需要缓存若干行的图像数据,并在这若干行的固定延时内就给出结果,实时性很高,计算延时就只有这若干行,FPGA也无需外界SRAM或DDR来缓存图像数据。算法也不会因为图像中的连通区域数目多了就会变慢,因为这是流水线算法,就没有处理目标多了还会变慢这个概念。而该算法在PC上也有高速实现的潜力

什么是连通域标记算法呢?

一般不搞图像处理的人是不会接触到这个术语的,搞图像处理的人则会知道这个算法是图像处理里面的一个基本算法。那它是干啥的呢?

《FPGA实现的实时流水线连通域标记算法》

请看上图,图片中的灰白色背景上有两个物体,其实就是两个花生米啦。那如果让你编程处理这个图像,告诉这两个花生米在图片中的位置,这个程序该怎么编?

《FPGA实现的实时流水线连通域标记算法》

或者换句话说,让你编程达到上图效果,也就是在图片上花生米的外面画一个外切矩形框,这个该怎么编程实现?

你要问人说:图片中的这两个花生米在什么位置?那这简直就是废话,一个花生米在左边,一个在右边,一个是竖着的,一个是斜着的,这还用问吗,我一眼就看出来了。

呵呵,人当然是一眼就看出来了。但在电脑看来,这幅图片是由一个一个像素点组成的,再说基本一点,在电脑里,这幅图片只是存在内存里的一组像素点数据,彩色图像的每个像素点是由三个字节的数据组成的,这个三个字节数据分别代表这个像素点的RGB值,也就是红绿蓝三种颜色的成分。

《FPGA实现的实时流水线连通域标记算法》

如上图所示,左边是花生米尖端的局部放大,一个方格就代表了一个像素点,右边就是每个像素点对应的RGB数据,电脑里存的就是这些数据,在显示器上显示出来就是这个花生米。电脑处理的也就是这些数据。所以电脑并没有人的“一眼就看出来”的能力,电脑只能读取分析这些数据,然后给出结果,比如去在花生米外边画个框,以表示本电脑也看出来花生米在哪了。

那电脑里的算法是如何识别这个花生米的位置的呢?首先第一步是根据背景或物料颜色的不同把图像二值化,变成下面的只有纯黑和纯白这两种颜色的二值图像:

《FPGA实现的实时流水线连通域标记算法》

其实二值化之后这幅图片在电脑里已经不是图片或图像数据了,而是二值化数据。因为非黑即白只需要一位数据,0(白)或1(黑),就能代表了,而原来的RGB数据是三个字节二十四位,这一下子可省了好多数据空间。当然其实在电脑里处理也没省多少,因为电脑处理数据的单位至少是字节,你要是按位存储和处理实际上反而会带来更多麻烦。但在FPGA里可以按位处理,二值化后的确能起到节省存储空间的作用。

二值化的主要目的其实就是略去不必要的信息,因为我要完成定位这个任务,只需要上图的这个二值黑白轮廓图就可以了,不需要其它的颜色信息。图像二值化之后接下来的步骤就是连通域标记。所以该算法的全称是:二值图像的连通域标记算法。

啥叫连通区域呢?为啥一个确定花生米位置的算法要叫连通区域标记?标记个啥?

《FPGA实现的实时流水线连通域标记算法》

我们再来看看上面这个二值化并放大后的示意图。花生米变成了两坨黑黑的像素点,那这两坨黑点看起来有什么规律?这还能有什么规律,不就是两坨像素点么?其实是有规律的,左边这坨黑像素点中,像素点之间是相互连通的,也就是一个挨着一个的,右边那坨也是,但这两坨之间并没有连通。这也能叫规律,我一眼就看出来了?哈哈,又来了,记住电脑是没有一眼看能力的,电脑就是靠这个连通不连通的规律,识别出这有两个连通域,也就是代表着两个物体,然后能统计出这两个连通域的有关信息,比如其在图像中所处的位置,大小(所包含像素点数),边长(边缘上所包含像素点数)等等。

电脑在处理上面这个二值化图片数据时,它会从图像的左上角那个点开始往右逐个像素点的扫描图像数据,当它扫描发现一个黑点时,就会继续去找这个黑点附近有没有和它相邻的黑点,找到了一个就再找下一个。也就是按照一定顺序,要把这一坨黑点从最上一行最左的那一个开始,挨个的都找到并做上标记,比如标记个值“2”(原来它的值是“1”,代表黑点)。然后再扫描到另一片黑点时把这一片黑点都标记成“3”,最后这两片黑点连通区域都被标记上了不同的数值已示区别。然后再扫描一遍图像就可以统计出这两个不同连通区域的相关信息,其实在第一遍扫描的时候边扫描边统计也可以,这样就不用扫描两遍。

《FPGA实现的实时流水线连通域标记算法》

如上图所示,我们用红色代表被标记为“2”的点,用绿色代表被标记为“3”的点,那连通区标记完之后的效果就如上图那样。或许你会说,这不是就是把黑块染了个色吗,这有啥。这对电脑来说意味着它已经认出了这两个连通区,终于在一片黑白像素点中建立起了两个连通区目标的概念,不容易啊,这是让电脑从一堆数据中建立“这有两个物体”这个抽象概念的第一步啊!

以上我只是大致用语言描述了一下连通域标记算法是干啥的,至于具体实现还有很多细节需要考虑,也有不同的实现方法,这个大家在网上搜一下就能搜到很多这方面的资料。我当初搞这个研究的时候也搜了的,了解了连通区识别这个概念并大概看了一下常见的实现算法,但并没有细看,也没有动手去编程实现网上找到的这些算法。因为我当初研究这个算法就是为了在FPGA上实现连通区域标记算法,而且有很高的实时性要求。而在网上看到的算法大部分都是基于电脑(PC)上实现的。PC上的图像处理算法都是以一帧图像为处理单位,算法会随机的读取图像数据,一般连通区域标记算法还会要扫描两遍图像。

我作为一个搞FPGA的工程师一看这些算法就知道它们统统不适合在FPGA上实现。在说为什么之前先来说一下图像处理的实时性这个问题。

一般在PC上所说的实时图像处理都是以帧为单位的。比如一般视频的帧率是每秒30帧,也就是每秒30幅图像,要是电脑对这个图像数据流的处理速度能达到每秒30帧,也就是能在三十分之一秒内就能完成一帧图像的处理,那这就能算的上是实时处理。

但在有些应用场合下,实时处理是以行为单位的。现在一幅图像大概有上千行。比如1920*1080分辨率的图像就有1080行。在有些应用中,延时一帧,一千多行,这个延时还是太长了。可能在这样的应用中就没有帧的概念,图像就是一行一行的这样不停处理下去的,比如用线阵相机扫描在皮带上运动的物体。而对物体的识别的速度要求也很高,比如只能允许上百行的延时,而且不论皮带上有多少目标,这个延时都必须是固定的,这又该怎么办?

一般这种高实时性要求场合的图像处理都会用FPGA来做,也只能用FPGA才能实现。而FPGA的片上存储资源,比如Block Ram是很有限的,也缓存不了多少行的图像数据。PC上内存大,随机读取数据很方便,编程者一般都不用考虑程序是怎么读数据的问题,写个for循环,数组里面的数据就都来了。而在FPGA里内存很有限,数据该怎么缓存,Ram该怎么读取,都要自己去考虑。所以那些在PC上运行的连通域识别算法都不大适合在FPGA上实现,尤其是那些还要扫描两遍图像的。

当然你可能会想,我在FPGA上接一片DDR,这样内存不就够了吗,然后再把PC上的算法照搬来不就行了吗?这样或许也是可以的,但这样照搬PC的算法并不能体现FPGA能并行流水线处理数据的优势。

再来说一下什么叫并行流水线算法。我们都知道在生产制造领域,流水线制造能提高生产效率。但也并不是所有的事情都适合用流水线的方法来完成。比如产品的维修,你就不能让一个人只检查这个部分,再让下个人检查那个部分,这样可能永远都找不出问题在哪。在数据流处理的算法中也有流水线算法,和非流水线算法。流水线算法就是处理的节奏和数据来的节奏是一样的,来多少我处理多少,会有一个初始的固定延时,但在这个延时之后就必然会出结果。也就是说流水线算法就是这样以固定的节奏处理数据,不会快也不会慢,不会停下来思考分析。

图像处理中一些简单的算法都是流水线算法,比如用个n*n的算子对图像进行膨胀腐蚀,边缘提取等等。因为这些算法本来就是只用个算子和原来的图像乘一下就完了,没有分析判断流程啥的,自然就是流水线算法。那连通区域识别算法能不能也流水线实现呢?按照前面的描述,连通区域识别算法似乎不只是用个算子乘一下那么简单,好像它扫描遇到一个黑点的时候还要去找周围有没有黑点,找到了一个还要找下一个,这个连通区找完了呆会还要找下个连通区。遇到连通区之后这个顺序扫描的算法流程就会被打断,就会停下扫描对这片连通区,这似乎就不像是能流水线处理的算法了。真的是这样的吗?

俺当年想了好久,终于发现其实也是可以流水线处理的。不过在讲这个流水线算法之前,先来讲一下当时俺先想出来的一个适合在PC上实现的连通区域标记算法。俺这人不大喜欢去细看别人写的学术文章和算法,因为我觉得看别人的东西理解起来挺费劲,有这个时间去看别人的,还不如自己想自己写。所以当初在了解连通区标记这个概念之后,我没细看任何别人的算法,就开始自己琢磨着去写。

《FPGA实现的实时流水线连通域标记算法》

这个算法很简单,比你在网上能找到的那些算法都要简单直观。算法会先把二值化图像进行一个取边缘处理,也就是如上图那样只留下边缘上的点,这用一个3*3的算子处理一下就可以了。然后进行连通域扫描,扫描过程中发现一个未被标记的黑点时,算法就会向这第一个点的右边找相邻的点,并标记上。然后就这样按顺时针方向找下去,转一圈回到起始点标记就完成了。

这个算法的优点是只扫描了边缘上的点,所以速度较快,而且对于U形,螺旋形等奇怪形状都是直接扫一圈就完事,没有复杂的问题要处理。缺点就是不能统计面积大小,因为它没扫描内部的点,还有就是对于环形连通区它会扫描出两个圈,如下图所示:

《FPGA实现的实时流水线连通域标记算法》

其实这一个黑块连通域的信息主要都包含在这个连通区边缘的点上,内部的点就只能提供一个面积信息。关于位置,形状等信息,只通过边缘上的就能获得。

《FPGA实现的实时流水线连通域标记算法》

这个扫描一圈边缘的方法的确简单,但它要能成功运行,关键是先要获得,我管它叫“严格连通”的边缘。如上图所示,左边的边缘就是一个严格连通的边缘。而右边的点就不是,有的地方多了些点,有些地方缺了个点。如果用这个转圈扫描的方法处理这样的边缘,那就必然得不到结果。那就意味着在取边缘点这步操作之前,算法必须对连通区的边缘进行平滑去噪处理,以保证取边缘步骤能得到严格连通的边缘,进而保证扫描步骤不会出错。

这个平滑去噪处理我是先用了一个较大的算子,比如11*11的算子,进行一个类似腐蚀的去噪处理,然后在用一个3*3的算子进行一个像素级别的精细去噪处理,这个3*3的精细处理一般会对图像扫三遍,甚至更多,越多越可靠。根据我的经验,经过这一大三小的算子处理之后,基本就能保证能获得严格连通的边缘。但我从没理论上去证明过这个事情,俺才懒得去证明,作为一个工程师,能用就行。

《FPGA实现的实时流水线连通域标记算法》

上图就是大算子去噪的效果,这个算子就是把膨胀腐蚀加上了个阈值,比如11*11的算子中共有121个点,当算子中包含的黑点数大于某个阈值时,就把这个点留下,小于就腐蚀掉。如果把这个阈值设为总点数的一半,比如60,那就会得到上图的效果,就是噪点被去掉了,但大小和轮廓还基本保持一样。如果阈值大于半数,就会有腐蚀的效果,小于半数就会有膨胀的效果。或许你发现了上图中的图像轮廓似乎损失掉了一些细节,这主要是因为这个连通区较小所致。

《FPGA实现的实时流水线连通域标记算法》

3*3算子去噪的效果如上图所示,它做的事情就是把多出来的孤点去掉,缺的地方补起来。因为这些情况都会影响严格连通边缘的获取。而这个算子之所以要处理图像几遍是因为在测试中发现,只运行一次往往还不能完全去掉所有的这种情况。运行三遍之后基本就可以了,极少数情况还需要更多遍。

再来捋一下这个扫边一圈连通域识别算法的步骤:

1、用一个较大,比如11*11的算子扫描一遍图像进行初步去噪;

2、再用3*3的算子进行精细去噪3遍或以上;

3、用3*3的算子进去取边缘处理,把内部点去掉,仅留下边缘上的点;

4、进行连通域标记扫描,遇到未标记黑点就继续扫描一圈标记完整个连通域的边缘。

以上算法在PC上很好实现,速度也较快,逻辑也简单,把U形等奇怪形状带来的逻辑上的问题都避免掉了。但是这个算法并不适合在FPGA上实现,确切的说,以上算法步骤中的第4步,连通区标记扫描,不适合在FPGA中用流水线算法实现。

前三步算法在FPGA里面自然就是用流水线算法来实现的。在PC上我们进行这前三步编程的时候,一般是先让算子把一帧图像扫描处理一遍,然后再扫描一遍。而FPGA里面的流水线算法是指,前一个算子扫描出若干行的结果之后,后面的算子就紧跟着扫描这几行结果,然后后面的算子再紧跟着。当然在PC里也可以把算法写成这样“行流水线”形式的,这样的好处就是让数据的读写更集中,可以提高Cache的命中率。

流水线算法的特点是,我这个步骤只能处理前一个步骤给出的若干行的结果,到底多少行可以看你的需要以及你有多大的缓存能力,比如11*11的算子就要缓存至少10行的数据,这个行数可以更大,只要你FPGA上有足够的RAM,但是,这个行数必须是确定的。

而上面这个扫描算法,当它开始扫描一个连通区域时,就会一直扫下去直到扫完,这往下扫的行数是不确定的,是和连通区域的大小有关的。这在PC上不是什么问题,因为PC上这一幅图像都给你了。但在FPGA上就不行了,流水处理的意思就是,你图像一边来我一边处理,不是一帧完了我再来处理。你的算法就不能假定图像数据都已经有了,那也就不能这样发现一个连通区域的开头,就想一下子把它扫完,因为可能下面的图像数据还没有来呢。

那么要想流水线化这个连通域标记算法,就必须换个思路。先来回顾一下连通域标记算法到底是干了一个什么事情。简单的说,当发现一个黑点的,我们要继续找和它相邻的黑点,把它标记上,再找和这个刚找到的黑点相邻的黑点… 就是这样把这个相邻的属性传递下去。前面这个算法的做法是,找到一个黑点,就一直继续找相邻的黑点,直到把一圈都找完为止。然后我们发现这不适合流水线实现,那该怎么换下思路,才能让它适合用流水线的方法实现呢?

我们不能发现一个就一直找下去了,但可以这样:当我们扫描到某行发现一个未标黑点时,可以先把在这一行中与其相邻的黑点标记上,然后再把下一行与其相邻的黑点都标记上,然后就不再扫描下下一行了,而是继续回来扫描这一行的后面。这样当我们再扫描下一行时,就会发现上一行刚标记过的黑点,然后我们再把前面未完成的工作继续下去,找这个已标记黑点在下一行的相邻黑点,并把它标记上和这个已标记黑点同样的标号。就这样通过扫这一行时标记下一行,一行一行的把这个相邻的属性传递下去。

也就是说当图像全部都给我时我可以一下子扫完,当图像是一行一行的给我时,我可以用“这一行再下一行”这样一行传递一行的方式把图像中的连通区域扫完。

以上只是简单的说了一下这个算法该如何流水线化的思路,但其具体实现的细节,还真的是有点复杂。会有很多细节问题:

这个算法是只能扫描图像一遍的,因为流水线算法数据来了处理完了也就走了,不会缓存前面的很多数据,也就是当我们扫到后面时,前面的数据已经丢弃了,也不可能再扫一遍。并且这个算法不是一下子扫完一个连通区,然后再扫下一个;而是在逐行扫描的时候并行扫描所遇到的所有连通区。那这样我们就需要另外开辟一个存储空间来存放每个连通区域的统计数据,比如边缘点数,内部点数,座标最大最小值等。边扫描边统计,扫描到某个连通区时就把这个连通区的统计数据调出来继续统计。当算法发现一个连通区域扫描完成时,就可以直接输出这个统计结果。

接下来就是具体扫描流程中会遇到的问题,比如如何判断一个连通域扫描的结束?怎么处理U,w,n,m形连通域扫描时会遇到的问题?一个连通域在开始的时候该以什么顺序扫描?(这也是个问题??)

在讲这些细节问题之前还得先讲一下我的一个发现。那就是在经过去噪、取边之后获得的严格连通的边缘中,会有一些自然就存在的确定规律在里面。而扫描算法就是运用了这些规律才得以实现。

《FPGA实现的实时流水线连通域标记算法》

比如任何形状的连通域的边缘,在其上下四角处的点的形态都是如上图右边那样的。发现了这些规律有啥用呢?例如当算法发现某个连通区域最上一行最左的那第一个点时,就会发现它必然会有一个右边的相邻点和左下的相邻点,除了这两个之外就也不会有其它的。那算法在处理的时候就不需要去考虑更多的情况。也就是说,这些自然就有的规律让我们发现要实现这个算法其实也没那么复杂,要处理的情况是有限的且是有规律的,凭借这些规律,算法就可以知道连通区域什么时候开始,什么时候结束,什么时候会遇到U形右边那个分支和左边的合并,也不需要额外考虑遇到了W形会怎样。总之,发现了这些规律后我们就能发现能适用于它的算法。

《FPGA实现的实时流水线连通域标记算法》

上图是这个逐行扫描连通域标记算法的大致流程图,只是大致的流程图,并没有包含所有问题的处理细节。至于为什么要区分个左端点和右端点,我好像都忘了,也可能是不必要的。再看看上面流程图最左边发现新的未标点这一支,在这里回答了前面那个发现一个新连通域后该以什么顺序扫描的问题。也就是这第一个点有右边的相邻点和左下的相邻点,我该先去看哪一个呢?答案是应该先去看左下的。

另外这个算法在逐行扫描的时候还会顺便统计一下连通区域内部的点,也就是说这个算法也能统计出面积,在遇到U形等奇怪形状时,由于进行了数据合并,所以最后的统计数据结果不会出错。不过这个算法好像应付螺旋形会有问题,环形也是会被识别成内外两个,不过这两种情况在实际应用中也很少遇到。

《FPGA实现的实时流水线连通域标记算法》

上图就是该算法在FPGA中实际运行的结果,我在蓝色的背景上放了两片纸片,下面那条绿杠代表了算法已成功识别了这个连通域并给出了统计结果。可以看出该算法的延时是很小的。大概十几行的样子。这是可以算出来的。11*11的算子延时了5行,3*3的算子延时了2行,3遍就是6行,逐行扫描需要缓存两行数据,延时是一行,这样总延时加起来就是12行。也就是当一个连通域最后一行图像被给出后,算法会在12行延时之后给出结果。这个算法大概用了二十几个Block Ram,并不算多,图像宽度是1024的。

总之流水线算法的好处就是并行速度快,延时固定。读取数据是固定但顺序来的,这样就不需要缓存很多数据,适合FPGA实现。如果在PC上实现利用了这个特点也就不需要大范围随机读取数据,能提高Cache命中率,进而能提升运行速度。

用FPGA做图像处理的人肯定会想,要是所有的图像处理算法都能流水线化该多好啊。在经历这个算法的研究之后,我觉得这也是有一定可能性的,只要我们换个思路,原来好像不能流水线的算法其实被发现也能流水线。

通过研究出这个算法我也悟出了几个哲理,那就是:

信息在有变化的地方

这个黑白连通区图像的信息主要就在它的边缘上,一般图像中有信息的地方也是在图像颜色亮度啥的有变化的地方。那既然我们要从图像中获取信息,那就要去有信息的地方找,所以这个连通区标记你要去标记那些内部的点干啥呢?扫描一次边缘不就完了吗,大部分信息都能获取了,而且能避免遇到U形等奇怪形状时的额外处理过程。需要面积信息可以再去扫描一遍,其实这个顺时针扫描算法还可以在扫完边缘之后继续扫贴着边缘内部的那一圈点,扫完之后再扫内圈,就这样一圈圈的把连通区内部也扫完。

解决复杂的问题要一层一层的来

前面说了扫边缘算法在运行之前先要经过去噪处理得到严格连通的边缘。但我在一开始遇到噪点问题时也并没有想到先去噪,而是想着是不是能搞一个能很智能,能模糊处理的算法来应对所有的噪点情况。后来可能是因为我智商不够,没想出来该怎么“模糊处理”。后来终于想到可以先去噪,保证我的边缘是不会有问题的,不就不用这么麻烦了吗。于是我又悟出了解决复杂的问题要尽量一个层面一个层面的来,不要一上来就尝试去想一个“智能,万能”的算法一下子就把这个问题解决的很完美。这个边缘扫描的算法,经过前面几遍简单的去噪之后,就能正确的运行给出结果。而前面的去噪算法很简单,扫描的算法由于处理的是严格有规律的情况,没有意外随机的情况要处理,所以也没那么复杂。这样由几层简单的算法前后组合就把这个咋看起来有点复杂的问题很简单的解决了,根本不需要去搞什么“智能模糊”算法。

万事万物都有它自然就有的规律,只要我们观察思考的够深入,就能找到解决问题的办法(算法)

由一个从图像处理算法中得到的体会就上升到万事万物,似乎有点夸大了,哈哈。但事实就是这样的,规律都是本来就有的,不是我们发明的,只是我们发现的。当我们发现规律的时候,就能利用这些规律解决问题,而不需要再去瞎摸索。这个连通域扫描标记算法也是在我发现了图像的一些自然就有的规律之后才琢磨出这个算法是可行的,所以说也并不是我发明了这个算法,算法也是本来就有的,只是我发现了它。

算法源码链接:http://download.csdn.net/detail/github_33678609/9743705

原址链接:http://blog.sina.com.cn/s/blog_539ee1ae0102wz81.html

点赞