正点原子 发表于 2020-9-28 10:08:02

【正点原子FPGA连载】第十二章基于霍夫变换的直线检测实验--摘自【正点原子】领航者ZYNQ之HLS 开发指南

本帖最后由 正点原子 于 2020-9-28 10:08 编辑

1)实验平台:正点原子领航者ZYNQ开发板
2)购买链接:https://item.taobao.com/item.htm?id=606160108761
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz_linhanz.html
4) 正点原子官方B站:https://space.bilibili.com/394620890
5)对正点原子FPGA感兴趣的同学可以加群讨论:876744900 点击加入:                                                                                                                
6)关注正点原子公众号,获取最新资料





第十二章基于霍夫变换的直线检测实验


直线检测是计算机视觉中的重要内容,例如车辆自动驾驶技术中道路边缘的获取,航拍图像中建筑物、机场跑道等直线特征的确定。霍夫变换算法具有较强的鲁棒性和抗干扰性,是直线检测中效果最好的算法之一。霍夫变换也可用来检测任意几何形状(比如圆),在图像处理和模式识别领域得到了广泛的应用。本章我们将在HLS中实现基于霍夫变换的直线检测。
本章包括以下几个部分:
1212.1简介
12.2实验任务
12.3HLS设计
12.4IP验证
12.5下载验证


12.1简介
霍夫变换是由Paul Hough于1962年提出的一种形状匹配技术,其本质是从图像空间到“参数空间”的映射,也就是将X/Y坐标系中的点转换到变量空间(parameter space)。变量空间的选择是由要检测的目标形状决定的,本章我们只考虑直线的检测。
在X/Y坐标系中,经过两个点(x1,y1)和(x2,y2)的一条直线可以用下面的公式描述:
y = ax + b
上式是直线在笛卡尔坐标系(又称直角坐标系)中的方程式,式中a、b为直线的变量(斜率和截距)。但是针对直线的霍夫换不使用这两个变量,因为对于垂直于X轴的直线来说,它的斜率a是无穷大的。这就导致以a、b为变量的变量空间为无穷大。因此我们可以使用直线的另外一种表示方法,即使用角度θ和长度ρ来表示一条直线的方程,如下所示:
ρ= xcos(θ) + ysin(θ)
式中θ和ρ的几何含义如下图所示:

图 12.1.1 笛卡尔坐标系中的直线
图 12.1.1中红色的是目标直线,我们过原点作该直线的法线(蓝色)。原点到直线的距离为ρ,法线与X轴的角度为θ,那么直线上所有的点均满足方程式ρ= xcos(θ) + ysin(θ)。笛卡尔坐标系中不同的直线对应着不同的(θ,ρ)参数,因此我们可以用θ作为横坐标,ρ作为纵坐标从而构成变量空间。那么笛卡尔坐标系中的一条直线,对应于θ/ρ变量空间中的一个点。
相比于用斜率a和截距b作为变量空间,使用角度θ和距离ρ变量空间的优势是θ和ρ都是有限值。θ的取值范围为0到180°,而ρ的最大值为图像的对角线距离,不会出现无穷大的情况。
现在我们考虑笛卡尔坐标系中的一个固定点P(x1,y1),经过该点可以作出无数条直线。这些直线有不同的θ和ρ,但是每一组(θ,ρ)均满足方程式ρ= x1cos(θ) + y1sin(θ)。该方程式在θ/ρ变量空间中为一条正弦曲线,如下图所示:

图 12.1.2 变量空间中的正弦曲线
在图 12.1.2中,正弦曲线上的每一个点表示笛卡尔空间中经过点P(x1,y1)的一条直线。如图中红色点在θ/ρ变量空间中的坐标为(θ1,ρ1),它就表示笛卡尔空间中方程为ρ1= xcos(θ1) + ysin(θ1)的一条直线,且该直线经过点P(x1,y1)。
下面我们考虑笛卡尔空间中的3个点,假设他们位于同一条直线上。那么我们该如何找出经过这三个点的直线呢?
传统的思路是过其中的两个点作一条直线,求出其直线方程,然后将第三个点的坐标代入该直线方程,从而判断第三个点是否位于该直线上。这种计算方法看似简单,但是随着点的数目增多,计算量会大到让你开始怀疑人生:对于分辨率为640*480的一幅图像,共有307200个像素点,单单是由两个点连接而成的直线就有307200*(307200-1)/2种可能。而且对于其中每条直线,我们还要一一去判断在剩余的(307200-2)个点中,有多少点位于该直线上。
此时我们就可以借助霍夫变换,将笛卡尔空间中的直线检测问题转化成参数空间中相对简单的求最大值的问题。
首先在笛卡尔空间中,过其中一个点作出所有经过该点的直线。如果这三个点位于一条直线上,那么在所有经过第一个点的直线中,肯定有一条同时也经过其他两个点,我们最终的目的就是找到这条直线。
由于经过一个点可以作出角度不同的无数条直线,因此我们需要给角度指定一个精度。如果精度为1°,那么过第一个点一共可以作出180条直线。这里为了简单起见,我们将精度指定为30°,如下图所示,


图 12.1.3 霍夫变换第一步
图 12.1.3的左边表示过第一个点作出的所有直线,由于精度为30°,所以一共能作出6条直线(180/30=6)。相邻直线之间的角度差为30°,在下面的表格中列出了这些直线的角度θ以及原点到直线的距离ρ。由前面的介绍我们知道,这六组(θ,ρ)在变量空间中位于同一条正弦曲线上。
我们对第二个点和第三个点执行同样的操作,最后由三个表格里的数据,在变量空间中绘制相应的正弦曲线,如下图所示:

图 12.1.4 变量空间中正弦曲线的交点
从图 12.1.4中可以看到,在变量空间中,三条正弦曲线有一个共同的交点P。由于在变量空间中,曲线上每一个点代表笛卡尔空间中经过某固定点的一条直线。既然交点P同时位于三条正弦曲线上,那么它在笛卡尔空间中所代表的直线,就同时经过了三个点。由交点P所确定的直线,就是我们最终检测到的经过三个点的一条直线。我们从图 12.1.3中也可以看到,确实存在这样一条直线(粉红色),它同时经过三个点。
以上是使用霍夫变换来检测经过三个点的一条直线的过程。我们可以想象,如果在一幅图像中,有很多个点都位于同一条直线上,那么在霍夫空间中,就会出现很多条正弦曲线相交于同一点。如下图所示:

图 12.1.5 利用霍夫变换对图像进行直线检测
图 12.1.5左侧的输入图像中有很多个像素点(黑色),它们分别位于两条直线上,右侧是这些点经过霍夫变换在变量空间中形成的图像。
由于点的数目比较多,我们已经很难分辨出每个点在变量空间中所对应的正弦曲线。但是我们可以统计变量空间中,经过各个点的曲线数量。如果变量空间中经过某个点的曲线数量为2,那么它表示有两条正弦曲线相交于该点,它对应着笛卡尔空间中穿过两个像素点的一条直线。
在变量空间中,经过某一点的曲线数目越多,那么该点就越亮,从而就在变量空间中形成了图 12.1.5右侧所示的明暗分布。从图中可以看到有两个点最亮,说明变量空间中有很多曲线都相交于这两个点。它们对应着笛卡尔空间中的两条直线,这两条直线经过了很多像素点。根据这两个点在变量空间中的坐标(θ,ρ),我们就可以在笛卡乐空间绘制出对应的直线,也就是我们检测到的直线。
图 12.1.5以灰度大小(明暗对比)的方式直观地展示出如何从变量空间中寻找图像中可能存在的直线,即找到变量空间中最亮的点即可。那么在程序设计中,我们该如何理解霍夫变换实现直线检测的过程呢?
我们可以把变量空间想象成一个水平方向为θ,竖直方向为ρ的矩阵,矩阵中的每个元素像是一个盒子,盒子里将存储经过该点的正弦曲线数目。对于笛卡尔空间中的每一个像素点,我们都可以在变量空间中绘制出其经过霍夫变换之后的正弦曲线,然后在矩阵中给曲线经过的每一个元素的盒子里投一票(数据累加1)。遍历笛卡尔空间中图像的像素点,在所有的像素点都投票结束之后,我们统计变量空间中哪几个元素的票数最高。票数最高的几个元素在笛卡尔空间中所对应的直线,就是从图像中检测到的可能性最大的几条直线。
从上面的分析过程我们会发现,直线检测的问题在经过霍夫变换之后,最终转化成了在变量空间中统计最大值的问题。而这种直线检测的方法并不要求直线上的点是连续的,对图像中噪声的抗干扰能力也比较强。
最后我们给出Matlab中实现的利用霍夫变换进行直线检测的效果图。待检测的图像是一幅建筑物实拍图,如下所示:

图 12.1.6 待检测的楼宇
我们需要对图 12.1.6进行边缘检测,获取图像中的轮廓及边缘信息。接下来对边缘检测之后的图像进行霍夫变换,然后在变量空间中统计出最亮的几个点。如下图所示:

图 12.1.7 变量空间中最亮的几个点
图 12.1.7中最亮的几个点用白色方框标识出来了。接下来我们将这几个点在笛卡尔空间中对应的直线绘制在原图上,完成最终的直线检测,如下图中绿色线条所示:


图 12.1.8 完成直线检测之后的效果
12.2实验任务
本节的实验任务是使用Vivado HLS实现一个图像处理的IP核,该IP核能够实现基于霍夫变换的直线检测。
12.3HLS设计
我们在电脑中的“F:\ZYNQ\High_Level_Synthesis”目录下新建一个名为hough_transform_line的文件夹,作为本次实验的工程目录。然后打开Vivado HLS 2018.3,创建一个新的工程“hough_transform_line”,选择工程路径为刚刚创建的文件夹。需要注意的是,工程名以及路径只能由英文字母、数字和下划线组成,不能包含中文、空格以及其他特殊字符。如下图所示:

图 12.3.1 工程配置界面
设置好工程名及路径之后,点击“Next”,进入如下界面设置顶层函数:

图 12.3.2 设置顶层函数
然后选择ZYNQ器件所对应的型号,如下图所示:

图 12.3.3 设置时钟周期和器件型号
工程创建完成后,在工程面板中的“source”目录上点击右键,然后在打开的列表中选择“New File”新建源文件,在弹出的对话框中输入源文件的名称“hough_transform_line.cpp.cpp”,如下图所示。源文件默认的保存路径为HLS工程目录,为方便源文件的管理,我们在工程目录下新建一个名为“src”的文件下,将源文件保存在src目录下。

图 12.3.4 输入源文件名
我们输入的源文件的后缀名为“.cpp”,即使用C++语言进行设计。
“hough_transform_line.cpp”文件源代码如下:
1#include "hough_transform_line.h"
2
3//霍夫变换直线检测
4void hough_transform_line(AXI_STREAM & INPUT_STREAM,
5                  AXI_STREAM & OUTPUT_STREAM,
6                  int rows,
7                  int cols
8                  ){
9
10 #pragma HLS INTERFACE axis port=INPUT_STREAM
11 #pragma HLS INTERFACE axis port=OUTPUT_STREAM
12 #pragma HLS INTERFACE s_axilite port=rows
13 #pragma HLS INTERFACE s_axilite port=cols
14 #pragma HLS INTERFACE ap_ctrl_none port=return
15 #pragma HLS dataflow
16
17//hls::mat格式变量
18RGB_IMAGEimg_0(rows,cols);
19GRAY_IMAGE img_1(rows,cols);
20GRAY_IMAGE img_2(rows,cols);
21GRAY_IMAGE img_3(rows,cols);
22RGB_IMAGEimg_4(rows,cols);
23
24//直线有效标志,等于1表示像素点位于直线上
25int line_val = 0;
26
27//用于表示直线的极坐标变量
28hls::Polar_<float,int> polar_line;
29
30//将AXI4 Stream数据转换成hls::mat格式
31hls::AXIvideo2Mat(INPUT_STREAM,img_0);
32
33//将RGB888格式的彩色数据转换成灰度数据
34hls::CvtColor<HLS_RGB2GRAY,HLS_8UC3,HLS_8UC1>(img_0,img_1);
35
36//Sobel边缘检测
37hls::Sobel<1,0,3>(img_1,img_2);
38
39//霍夫线变换
40hls::HoughLines2<1,1,float,int,HLS_8UC1,MAX_HEIGHT,MAX_WIDTH,MAX_LINE_NUM>(
41          img_2,
42          polar_line,
43          HOUGH_THRES
44          );
45
46//绘制变换后得到的直线
47for(int y=0; y<rows; y++){
48      for(int x=0; x<cols; x++){
49          //将当前像素点的直线有效标志清零
50          line_val = 0;
51          //遍历霍夫变换后的各直线方程,判断当前像素点是否位于直线上
52          for(int k=0; k<MAX_LINE_NUM; k++){
53            if(polar_line.rho == (int)( x*hls::cosf(polar_line.angle)
54                                             + y*hls::sinf(polar_line.angle)))
55                  line_val = 1;
56          }
57          //将位于直线上的像素点绘制为白色,其他像素点为黑色
58          if(line_val == 1)
59            img_3 << 255;
60          else
61            img_3 << 0;
62      }
63}
64
65//将灰度数据转换成三个通道的灰度图像
66hls::CvtColor<HLS_GRAY2RGB,HLS_8UC1,HLS_8UC3>(img_3,img_4);
67
68//将hls::mat格式数据转换成AXI4 Stream格式
69hls::Mat2AXIvideo(img_4,OUTPUT_STREAM);
70 }
代码的主体部分与《基于OV5640的Sobel边缘测实验》非常类似,我们在边缘检测之后调用函数hls::HoughLines2()对获取的边缘图像进行霍夫线变换,如代码的第40至44行所示。
hls::HoughLines2()是HLS视频库中的函数,可以对输入的图像进行霍夫线变换,从而检测图像中的直线。其函数声明如下所示:

图 12.3.5 霍夫线变换结构体和函数声明
图 12.3.5中首先定义了一个名为“Polar_”的结构体,它包含两个成员angle和rho,实际上就是简介部分所介绍的变量角度θ和距离ρ。
我们在代码的第28行定义了hls::Polar_类型的变量“polar_line”,该数组用来保存霍夫线变换之后检测到的直线参数(angle,rho),该参数实际上就是直线的直坐标。其中MAX_LINE_NUM是一个宏定义,在头文件hough_transform_line.h中可以看到它的定义。它表示我们要从图像中检测到直线的最大数目,本实验中将其指定为10,它的值是可以修改的。
另外在代码的第28行我们还指定了结构体成员angle和rho的数据类型,分别为float和int。也就是说霍夫线变换得到的直线极坐标的角度θ为浮点型,而距离ρ为整型。需要留意的是变换之后的直线角度是用弧度制表示的,其范围为0~Pi(3.1415926)。
然后我们回到图 12.3.5中的HoughLines2()函数声明部分。尖括号<>中列出了该函数的模板,模板中的参数比较多,其中前两个int类型的参数theta和rho分别指角度θ和距离ρ的精度。theta的单位是度,其取值范围为1°至180°。在本次实验中我们将角度θ和距离ρ的精度均指定为1。接下来两个模板参数AT和RT是结构体hls::Polar_成员angle和rho的数据类型,分别为float和int。然后SRC_T、ROW、COL则是我们常见的参数,分别指输入图像的像素数据类型(只支持HLS_8UC1类型)和图像的行列数。最后一个参数lineMax我们将它指定为MAX_LINE_NUM,表示我们要从图像中检测到直线的最大数目。
函数HoughLines2()只有3个参数,其中_src表示输入的视频图像,_lines表示霍夫线变换得到的直线的极坐标,最后一个参数threshold是我们需要指定的霍夫变换阈值。霍夫变换过程中会对变量空间中的元素进行投票,只有那些得票数大于阈值threshold的直线,才被判定为检测到的直线。在程序的第43行,我们将该阈值指定为HOUGH_THRES,数值为40。也就是说在统计过程中,只有当直线连接超过40个像素点时,才会被判定为最终检测到的直线,并将其极坐标赋值到结构体数组_lines中。
在霍夫线变换之后,我们要根据检测到的直线极坐标,在图像img_3中绘制相应的直线,如代码第46至63行所示。
46//绘制变换后得到的直线
47for(int y=0; y<rows; y++){
48      for(int x=0; x<cols; x++){
49          //将当前像素点的直线有效标志清零
50          line_val = 0;
51          //遍历霍夫变换后的各直线方程,判断当前像素点是否位于直线上
52          for(int k=0; k<MAX_LINE_NUM; k++){
53            if(polar_line.rho == (int)( x*hls::cosf(polar_line.angle)
54                                             + y*hls::sinf(polar_line.angle)))
55                  line_val = 1;
56          }
57          //将位于直线上的像素点绘制为白色,其他像素点为黑色
58          if(line_val == 1)
59            img_3 << 255;
60          else
61            img_3 << 0;
62      }
63}
在代码第47、48行中,我们使用两个for循环来遍历图像中的行和列,从而对图像中的各像素点一一进行赋值。我们会给当前正在处理像素点设置一个标志信号line_val,它为1标志着该像素点位于检测到的某一条或多条直线上,那么我们就要把当前像素点指定为白色(灰度值255);如果line_val为0,表示没有任何一条直线经过当前像素点,就将其指定为黑色。
首先将当前像素点的标志信号line_val初始化为0,如代码第50行所示。然后使用第52行的for循环来遍历数组polar_line中各直线的极坐标,通过第53、54行的if语句判断当前像素点的坐标是不是位于各直线上。判断方法就是将当前像素点的坐标(x,y)代入各直线的方程,通过判断是否满足等式ρ= xcos(θ) + ysin(θ)。如果等式成立,就将标志信号line_val赋值为1。在52行的for循环结束之后,如果标志信号line_val仍然是0,那么就表示数组polar_line中没有直线经过当前像素点。
最后在代码58至61行的if语句中,通过判断line_val的值来决定图像img_3中当前像素点的灰度值。变量img_3的声明位于代码的第21行,它是GRAY_IMAGE类型,即单通道的灰度图像。因此绘制完成之后还要通过代码第66行的hls::CvtColor()函数转换成3通道的灰度图像,这样才能显示在LCD屏幕上。
由于代码的其他部分与《基于OV5640的Sobel边缘测实验》基本相同,在这里就不再赘述。如果大家不清楚的话,可以参考《正点原子HLS开发指南》的相应章节。
接下来以同样的方式在src目录下创建名为“hough_transform_line.h”的头文件,创建完成后输入下面的代码:
1#ifndef _HOUGH_TRANSFORM_LINE_H_
2#define _HOUGH_TRANSFORM_LINE_H_
3
4#include "hls_video.h"
5#include "hls_math.h"
6
7#define MAX_HEIGHT 480       //图像最大高度
8#define MAX_WIDTH800       //图像最大宽度
9
10 #define MAX_LINE_NUM 10      //能检测到直线的最大数目
11 #define HOUGH_THRES40      //霍夫变换域值
12
13 #define INPUT_IMAGE "road.jpg"
14 #define OUTPUT_IMAGE "road_hough_trnasform.jpg"
15
16 typedef hls::stream<ap_axiu<24,1,1,1> >AXI_STREAM;
17 typedef hls::Mat<MAX_HEIGHT,MAX_WIDTH,HLS_8UC3> RGB_IMAGE;
18 typedef hls::Mat<MAX_HEIGHT,MAX_WIDTH,HLS_8UC1> GRAY_IMAGE;
19
20 //霍夫变换直线检测
21 void hough_transform_line(AXI_STREAM & INPUT_STREAM,
22                   AXI_STREAM & OUTPUT_STREAM,
23                   int rows,
24                   int cols
25                   );
26
27 #endif
代码中定义了工程所需要包含的头文件、宏定义以及函数声明等。需要注意的是,在代码的第13和第14行通过宏定义指定了输入图像和输出图像的名称,这两个宏定义将用于程序的C仿真。
接下来在工程中“Test Bench”目录上右击,然后选择“New File”,在src文件夹中创建程序的测试文件“hough_transform_line_tb.cpp”。其代码如下所示:
1#include "hough_transform_line.h"
2#include "hls_opencv.h"
3
4int main(){
5   //加载待仿真图像
6   IplImage* src   = cvLoadImage(INPUT_IMAGE);
7   //创建图像,用于绘制直线的
8   IplImage* dst   = cvCreateImage(cvGetSize(src), src->depth, src->nChannels);
9   //创建图像,用于叠加原图和绘制的直线
10IplImage* dst_add = cvCreateImage(cvGetSize(src), src->depth, src->nChannels);
11
12//定义AXI_Stream格式数据
13AXI_STREAM src_axi,dst_axi;
14
15//将图像转成AXI_Stream格式数据流
16IplImage2AXIvideo(src,src_axi);
17
18//霍夫变换的直线检测
19hough_transform_line(src_axi,dst_axi,src->height,src->width);
20
21//将AXI_Stream数据流格式转成图像(绘制的直线)
22AXIvideo2IplImage(dst_axi,dst);
23
24//将原图与绘制的直线叠加
25cvAddWeighted(src,0.2,dst,0.8,0,dst_add);
26
27//将叠加后的图像保存成文件
28cvSaveImage(OUTPUT_IMAGE,dst_add);
29
30//显示原图
31cvShowImage(INPUT_IMAGE,src);
32//显示原图叠加直线后的图像
33cvShowImage(OUTPUT_IMAGE,dst_add);
34
35//等待键盘上按键被按下
36cv::waitKey(0);
37 }
在test bench中,我们会加载头文件“hough_transform_line.h”中所指定的输入图像“road.jpg”,经过hough_transform_line()函数进行霍夫线变换,得到的图像中绘制了检测到的直线。为方便对比,我们使用函数cvAddWeighted()将检测到的直线与原图叠加起来,如代码第25行所示。
在进行仿真之前,我们还要将名为“road.jpg”的图片拷贝到src文件夹中。如下图所示:

图 12.3.6 src目录中的待仿真图片
然后还要在工程中添加该仿真图片。在工程中“Test Bench”目录上右击,然后选择“Add File”,在src文件夹中选择刚刚拷贝进去的图片“road.jpg”。添加完成后工程目录如下图所示:

图 12.3.7 工程目录
点击图 12.3.7中红色圆圈标示的按键来运行C仿真,然后在弹出的对话框中勾选“Clean Build”,最后点击“OK”,如下图所示:

图 12.3.8 运行C仿真
C仿真运行成功后会弹出两张图片,其中名为“road.jpg”的是我们添加到工程中的,用于仿真的原图片。如下图所示:

图 12.3.9 用于仿真的原图
另外一张名为“road_hough_trnasform.jpg”的图片是基于霍夫变换的直线检测算法仿真之后的效果,如下图所示:

图 12.3.10 霍夫线变换检测到的直线
图 12.3.10是在原图上叠加了检测到的直线。由于我们在程序中指定能够检测到的最大的直线数目MAX_LINE_NUM为10,因此图中一共绘制了10条直线,它们是从图像中检测到的可能性最大的直线。
从上图中可以看到,我们成功地检测到了道路的直线边沿,说明本次实验仿真结果是正确的。在观察了仿真结果之后,按键盘上的任意键结束仿真过程。
因为我们在test bench中通过调用函数cvSaveImage(),将仿真的结果以.jpg格式保存成了图片。因此在工程目录下的solution1\csim\build文件夹中,我们可以看到名为“road_hough_trnasform.jpg”的图片,如下图所示:

图 12.3.11 保存的仿真结果
接下来点击工具栏中向右的绿色三角形对设计进行综合,综合完成后,会自动打开综合结果(solution)的报告。在综合报告中还给出了设计的性能评估、资源评估以及接口等信息。本次实验中我们重点关注基于霍夫变换的直线检测算法资源的使用情况,如下图所示:

图 12.3.12 ZYNQ7020资源使用情况
图 12.3.12是本实验中的算法在ZYNQ7020芯片上的资源占用情况,图中红色的数字表示算法消耗的BRAM、DSP以及LUT资源超过了所选芯片中可用的资源数目。
由于霍夫线变换算法占用的资源比较多,领航者开发板上的ZYNQ7010/7020芯片资源不足,因此无法进行下载验证过程。大家可以通过仿真来观察实验结果。

arris9 发表于 2020-10-19 22:04:30

需要这么高的运算呀
页: [1]
查看完整版本: 【正点原子FPGA连载】第十二章基于霍夫变换的直线检测实验--摘自【正点原子】领航者ZYNQ之HLS 开发指南