使用霍夫变换检测车道线

小白学视觉

共 9728字,需浏览 20分钟

 ·

2021-09-14 15:22

点击上方小白学视觉”,选择加"星标"或“置顶

重磅干货,第一时间送达


车道线检测是自动驾驶汽车的重要组成部分之一,有很多方法可以做到这一点。本文,我们将使用最简单的霍夫变换方法。

本文分为三个部分:
  • 第一部分:高斯模糊+ Canny边缘检测

  • 第二部分:霍夫变换

  • 第三部分:优化+显示线条


1部分和第3部分的重点是编码,第2部分更面向理论。接下来,让我们开始第一部分。

第一部分:高斯模糊+Canny边缘检测


导入必需的库:
import numpy as np import cv2 import matplotlib.pyplot as plt
  • 1:Numpy用于执行数学计算,我们要用它来创建和操作数组。

  • 3:使用Matplotlib可视化图像。


接下来,让我们从集合中加载一张图片来测试算法
image_path = r"D:\users\new owner\Desktop\TKS\Article Lane Detection\udacity\solidWhiteCurve.jpg" image1 = cv2.imread(image_path) plt.imshow(image1)

在这里,我们在第4行将图像加载到笔记本中,然后我们将在第5行和第6行读取图像并将其可视化。现在是处理图像的时候了,主要分为以下三步:
def grey(image):     return cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)def gauss(image):     return cv2.GaussianBlur(image, (5, 5), 0)def canny(image):     edges = cv2.Canny(image,50,150) return edges 
在最后一个代码块中,我们定义了3个函数:
Greyscale the image:这有助于增加颜色的对比度,使它更容易识别像素强度的变化。

Gaussian Filter:高斯滤波器的目的是减少图像中的噪声。我们这样做是因为Canny中的梯度对噪声非常敏感,所以我们想尽可能地消除噪声。cv2.高斯模糊函数有三个参数:
  • img参数定义了我们要进行归一化(减少噪声)的图像。这个函数使用一个称为高斯核的核函数,用于对图像进行归一化。

  • sigma参数定义沿x轴的标准偏差。标准偏差衡量图像中像素的分布,我们希望像素扩散是一致的,因此标准偏差为0


Canny:这是我们检测图像边缘的地方,它所做的是计算像素强度的变化(亮度的变化)在一个图像的特定部分。幸运的是,OpenCV使它变得非常简单。
cv2.Canny函数有3个参数,(img, threshold-1, threshold-2)
  • img参数定义了我们要检测边缘的图像。

  • threshold-1参数过滤所有低于这个数字的梯度(它们不被认为是边缘)

  • threshold-2参数决定了边缘的有效值。

  • 如果两个阈值之间的任何梯度连接到另一个高于阈值2的梯度,则将考虑该梯度。

 

现在我们已经定义了图像中的所有边缘,我们需要分割与车道线相对应的边缘,操作步骤如下:
def region(image):     height, width = image.shape     triangle = np.array([                        [(100, height), (475, 325), (width, height)]                        ])     mask = np.zeros_like(image)     mask = cv2.fillPoly(mask, triangle, 255)     mask = cv2.bitwise_and(image, mask) return mask
这个函数将分割图像中车道线所在的某个硬编码区域,它以Canny图像为参数,输出孤立区域。

在第1行中,我们将使用numpy.shape函数提取图像的维数。
在第2-4行中,我们要定义一个三角形的尺寸,也就是我们要隔离的区域。
在第5和第6中,我们要创建一个黑色的平面,然后我们要定义一个白色的三角形,它的尺寸和第2行中定义的一样。
在第7行中,我们将执行位运算和运算,使我们能够隔离与车道线对应的边缘。

更深入的解释位运算
在我们的图像中,有两种像素强度:黑色和白色。黑色像素的值为0,白色像素的值为255。在8位二进制中,0转换为00000000255转换为11111111。对于位运算和运算,我们将使用像素的二进制值。现在,我们将在img1img2相同的位置上乘以两个像素(我们将img1定义为带有边缘检测的平面,img2定义为我们创建的掩码)

:Img1,右图:Img2(实际上,它是白色的,但我们把它改成了黄色)

例如,img1(0,0)处的像素将与img2(0,0)处的像素相乘(同样地,图像上其他位置的每一个像素也是如此)

如果img1中的(0,0)像素是白色的(意味着它是一条边)img2中的(0,0)像素是黑色的(意味着这个点不是我们的车道线所在的孤立区域的一部分),操作看起来像11111111* 0000000,等于0000000(一个黑色像素)

我们将对图像上的每个像素重复这个操作,导致只输出掩码中的边缘。

其他一切都被忽略了,仅输出隔离区域中的边。

现在我们已经定义了我们想要的边,接着让我们定义一个函数把这些边变成线:
lines = cv2.HoughLinesP(isolated, rho=2, theta=np.pi/180, threshold=100, np.array([]), minLineLength=40, maxLineGap=5)
这一行代码是整个算法的核心,它被称为霍夫变换(Hough Transform),将孤立区域的白色像素簇转换为实际的线条。

  • 参数1:孤立梯度

  • 参数5:占位符数组

  • 参数6:最小行长

  • 参数7:最大行间距


下面的部分将深入到算法背后的具体细节,所以在你们读完第二部分后,你们可以回到这部分,希望这部分会更有意义。

第二部分:霍夫变换


简单说明一下,这部分仅仅是理论,如果你们想跳过这一部分,可以继续阅读第3部分,但鼓励小伙伴们通读一遍。

来谈谈霍夫变换。在笛卡尔平面(xy)中,直线由公式y=mx+b定义,其中xy对应于直线上的一个特定点,mb分别对应于斜率和y轴截距。

笛卡尔坐标空间中的直线

平面被绘制成xy值的函数,这意味着我们显示的是这条直线有多少(x, y)对组成(有无穷多的x, y对组成任何一条线,这就是为什么线延伸到无穷远的原因)。

但是,可以用它的mb值绘制直线,这是在一个叫做霍夫空间的平面上完成的。为了理解Hough变换算法,我们需要了解Hough空间是如何工作的。

霍夫空间的解释
在我们的用例中,我们可以将霍夫空间总结为两行:
  • 笛卡尔平面上的点在霍夫空间中变成直线

  • 笛卡尔平面上的直线在霍夫空间上变成点


想想线的概念,一条线基本上是由一个接一个有序排列的无穷长的点组成的。因为在笛卡尔平面上,我们画的线是xy的函数,线被显示为无限长因为有无限多的(x, y)对组成了这条线。

现在在霍夫空间中,我们画出直线作为mb值的函数。因为每条笛卡尔直线上只有一个mb值,所以这条直线可以表示为一个点。

例如,方程y=2x+1表示笛卡尔平面上的一条直线。它的mb值分别是' 2 '' 1 ',这是这个方程唯一可能的mb值。另一方面,这个方程可以有很多xy的值,使得这个方程成立(左边=右边)


如果我们要用mb的值来画这个方程,我们只会用点(2,1);如果我们要用xy的值来画这个方程,我们将会有无穷多的选择因为有无穷多的(x, y)对。

把θ看成b, r看成m。稍后我们会在文章中解释θ和r的相关性。

那么为什么霍夫空间中的线在笛卡尔平面上被表示为点(如果你们从之前的解释中很好地理解了这个理论,我们希望小伙伴们在没有阅读解释的情况下就能解决这个问题)

现在我们考虑笛卡尔平面上的一点。笛卡尔平面上的一个点只有一个可能的(x, y)对可以表示它,因此它是一个点,不是无限长。关于一个点,还有一个事实就是有无限多的可能的线可以通过这个点,换句话说,这个点可以满足无穷多个方程(y=mx + b)(LS=RS)


目前,在笛卡尔平面中,我们根据xy值绘制这个点。但是在霍夫空间中,我们根据它的mb值来画这个点,因为有无限条线穿过这个点,所以在霍夫空间中会得到一条无限长的线。

以点(3,4)为例,可以通过该点的直线有:y= -4x+16, y= -8/3x + 12y= -4/3x + 8(直线有无穷多,但为了简单起见,我们用3条直线)

如果你们在霍夫空间中绘制每一条直线([- 4,16][-8/ 3,12][-4/ 3,8]),在笛卡尔空间中代表每条直线的点将在霍夫空间中形成一条直线(这条直线对应于点(3,4))

每个点代表前面显示的线(匹配颜色)

现在如果我们在个笛卡尔平面上放置另一个点呢?这在霍夫空间会有什么结果呢?通过霍夫空间,我们可以找到笛卡尔平面上最适合这两点的直线。

我们可以通过在霍夫空间中绘制与笛卡尔空间中两点相对应的直线,并找到这两条直线在霍夫空间中相交的点(a.k.a它们的POI,交叉点)

总结上述内容:
  • 笛卡尔平面上的直线在霍夫空间中表示为点

  • 笛卡儿平面上的点在霍夫空间中表示为直线

  • 通过求霍夫空间中与这两个点对应的两条直线的POImb坐标,可以找到笛卡尔空间中两点的最佳拟合直线,然后根据这些mb的值组成一条直线。‍‍


回到解释:
虽然这些概念比较好,但它们为什么重要呢?还记得我们之前提到过的Canny边缘检测吗?它使用梯度来测量图像中的像素强度并输出边缘。

在其核心,梯度只是图像上的点。所以我们能做的就是找到最适合每一组点的直线(图像左边的梯度和图像右边的梯度),这些最合适的线是我们的车道线。为了更好地理解它是如何工作的,让我们再深入了解一下!

我们只是解释了如何通过查看mb值来找到最合适的线对应于霍夫空间中的点的两条线的POI。然而,当我们的数据集增长时,并不总是有一条线完全适合我们数据。

这就是我们不得不使用容器的原因。当合并容器时,我们将霍夫平面划分为等距部分。每个部分都称为容器,通过关注容器中POI的数量,使我们能够确定一条与我们的数据具有良好相关性的线。一旦找到有最多交集的容器,我们就可以使用mb值,它们与该容器相对应,并在笛卡尔空间中形成一条直线,这条线就是最适合我们的数据的线。


但是在垂直线上,斜率是无穷大的,我们不能在霍夫空间中表示无穷,这将导致程序崩溃。所以我们不用y=mx+b来表示直线方程,我们用P()和θ()来定义直线,这也被称为极坐标系统。

在极坐标下,直线用方程P=xsinθ + ysinθ表示。在我们深入研究之前,让我们定义一下这些变量的含义:

  • P表示从原点垂直于直线的距离。

  • θ表示从正x轴到直线的俯角。

  • xcosθ表示x方向上的距离。

  • ysinθ表示y方向上的距离。

这是对极坐标含义的直观解释

用极坐标系统,即使有一条垂直线,也不会有任何误差。例如,取点(6,4)代入方程  P=xcosθ+ ysinθ。现在,我们取经过这个点x=6的垂直线,把它代入极坐标方程,P = 6cos(90) + 4sin(90)

  • θ是一条垂直线的90度,因为它从正x轴到直线本身的俯角是90度。θ的另一种表示方法是π/2(弧度)。如果你们想了解更多关于弧度的知识,以及我们为什么要使用它们,这里有一个很好的视频。然而,没有必要知道弧度是什么。

  • XY取点(6,4)的值因为这是我们在这个例子中使用的点。


现在我们把这个方程解出来:

P = 6cos(90) + 4sin(90)

P = 6(1) + 4(0)

P = 6



如我们所见,我们不会以错误结束。事实上,我们甚至不需要做这个计算,因为我们在开始之前就已经知道P是多少了。注意,这和从原点到x轴的距离是一样的。

我们想解释的东西的图像。

那么现在这已经解决了问题,我们准备好回去编码了吗?不是现在。还记得之前我们在笛卡尔平面上画点的时候吗?我们最终会得到霍夫空间中的直线?当我们使用极坐标时,我们会得到一条曲线而不是一条直线。然而,概念是一样的,我们将找到具有大多数交叉点并使用那些mb值来确定最佳拟合线。

第三部分:优化+显示


这一节是为了优化算法,如果我们不平均这些线,它们看起来很不稳定,因为cv2.HoughLinesP输出一串小线段,而不是一条大线。

为了平均这些线,我们将定义一个“average”函数。
def average(image, lines):     left = []     right = [] for line in lines:    slope = parameters[0]         y_int = parameters[1]         if slope < 0:             left.append((slope, y_int))         else:             right.append((slope, y_int))
这个函数对cv2.HoughLinesP函数中生成的行进行平均,它会找到左右两个线段的平均斜率和y轴截距,并输出两条实线(一条在左边,另一条在右边)cv2.HoughLinesP函数的输出中,每个线段有两个坐标:一个表示直线的开始,另一个表示直线的结束。利用这些坐标,我们要计算每条线段的斜率和y轴截距。


然后,我们将收集所有线段的斜率,并将每个线段分为与左线或右线对应的列表(负斜率=左线,正斜率=右线)

  • 4:通过直线数组进行循环。

  • 5:从每个线段中提取两个点的(x, y)值。

  • 6-9:确定每个线段的斜率和y轴截距。

  • 10-13:将负斜率添加到左行列表中,将正斜率添加到右行列表中。


注意:通常情况下,正斜率=左直线,负斜率=右直线,但在我们的例子中,图像的y轴是反的,这就是为什么斜率是反的(OpenCV中的所有图像都是反的y)

接下来,我们要从两个表中求斜率和y轴截距的平均值。
right_avg = np.average(right, axis=0)     left_avg = np.average(left, axis=0)     left_line = make_points(image, left_avg)     right_line = make_points(image, right_avg) return np.array([left_line, right_line])
  • 1-2:对两个列表(左边和右边)的所有线段取平均值。

  • 3-4:计算每一行的起始点和端点。(我们将在下一节定义make_points函数)

  • 5:输出每一行的2个坐标。


现在我们有了两个列表的平均斜率和y轴截距,让我们定义两个列表的起点和终点。
def make_points(image, average):   slope, y_int = average   y1 = image.shape[0]  y2 = int(y1 * (3/5))  x1 = int((y1 — y_int) // slope)  x2 = int((y2 — y_int) // slope)  return np.array([x1, y1, x2, y2])
这个函数有两个参数,一个是带有车道线的图像,另一个是有平均斜率和y_int的列表,输出每条线的起点和终点。

  • 1:定义函数

  • 2:得到平均斜率和y截距

  • 3 - 4:定义的高度线(左右两边都一样)

  • 5 - 6:通过重新排列一条线的方程计算x坐标,y=mx+b to x = (y-b) / m

  • 7:输出坐标集


为了进一步说明,在第一行,我们用y1值作为图像的高度。这是因为在OpenCV中,y轴是倒转的,所以0在顶部,而图像的高度在原点(参考下图)。同样,在第二行,y1乘以3/5,这是因为我们想让直线从原点y1开始,以图像的2/5结束。


应用于左线的make_points函数的可视化示例

但是,这个函数并不显示这些线,它只计算显示这些线所需的点。接下来,我们要创建一个函数,它取这些点,并用它们来画线。

def display_lines(image, lines):  lines_image = np.zeros_like(image)  if lines is not None:    for line in lines:      x1, y1, x2, y2 = line      cv2.line(lines_image, (x1, y1), (x2, y2), (255, 0, 0), 10)  return lines_image
这个函数有两个参数:我们想要显示线条的图像以及从平均函数输出的车道线。

  • 2:创建一个与原始图像相同尺寸的黑色图像

  • 3:确保包含线点的列表不是空的

  • 4-5:循环遍历列表,并提取两对(x, y)坐标


我们可能会想,为什么我们不把这些线添加到真实图像上,而是黑色图像上。因为原始图像有点太亮了,所以如果我们把它调暗一点,让车道线看得更清楚一点就好了(是的,我们知道,这不是大不了的,但找到改进算法的方法总是很好的)

:直接添加线条到图像。右:使用cv2.addddled函数

所以我们要做的就是调用cv2.addWeighted函数:
lanes =cv2.addWeighted(copy, 0.8, black_lines, 1, 1)

这个函数为实际图像中的每个像素赋予0.8的权重,使它们稍微暗一些(每个像素乘以0.8)。同样地,我们给所有车道线的黑色图像赋予1的权重,这样所有像素都保持相同的强度,使其突出。接下来我们要做的就是调用这些函数:

copy = np.copy(image1) grey = grey(copy) gaus = gauss(grey) edges = canny(gaus,50,150) isolated = region(edges)lines = cv2.HoughLinesP(isolated, 2, np.pi/180, 100, np.array([]), minLineLength=40, maxLineGap=5) averaged_lines = average(copy, lines) black_lines = display_lines(copy, averaged_lines)
在这里,我们简单地调用前面定义的所有函数,然后在第12行输出结果,cv2.waitKey函数用于告诉程序图像显示需要多长时间。我们将“0”传递给函数,这意味着它将等待,直到按下一个键关闭输出窗口。

输出结果:

我们也可以把这个算法应用到视频上。
video = r”D:\users\new owner\Desktop\TKS\Article Lane Detection\test2_v2_Trim.mp4" cap = cv2.VideoCapture(video) while(cap.isOpened()):    ret, frame = cap.read()   if ret == True:#----THE PREVIOUS ALGORITHM----#     gaus = gauss(frame)     edges = cv2.Canny(gaus,50,150)     isolated = region(edges) lines = cv2.HoughLinesP(isolated, 2, np.pi/180, 50,)  lanes = cv2.ad1dWeighted(frame, 0.8, black_lines, 1, 1)     cv2.imshow(“frame”, lanes) #----THE PREVIOUS ALGORITHM----#     if cv2.waitKey(10) & 0xFF == ord(‘q’):        break   else:     break cap.release()  cv2.destroyAllWindows()
这段代码将我们为图像创建的算法应用到视频中。记住,一个视频就是一串快速出现的图片。

  • 1-2:定义视频的路径。

  • 3-4:捕获视频(使用cv2. videcapture),并循环遍历所有帧。

  • 5-6:读取帧,如果有帧,继续。

  • 10-18:从前面的算法复制代码,并将所有使用Copy的地方替换为frame,因为我们想确保我们操作的是视频的帧,而不是前面函数中的图像。

  • 22-23:显示每一帧10秒,如果按下“q”按钮,退出循环。

  • 24-25:它是第5-6if语句的延续,但它所做的只是如果没有任何帧,就退出循环。

  • 26-27:关闭视频


我们刚刚建立了一个可以检测车道线的算法,希望小伙伴们喜欢构建这个算法,但不要止步于此,这只是一个关于计算机视觉世界的入门项目。

 

关键点:

  • 使用高斯模糊去除图像中的所有噪声

  • 使用canny边缘检测来分离图像中的边缘

 

关键字:

如果小伙伴们好奇,这里有一些与这个算法相关的关键术语,小伙伴们可以更深入地研究。
  • 高斯模糊

  • 位和二进制

  • 精明的边缘检测

  • 霍夫变换

  • 梯度

  • 极坐标

  • OpenCV车道线检测

其他需要考虑的资源:

  • youtube视频。


Github代码连接:
https://github.com/Nushaine/lane-detection/blob/master/Untitled33.ipynb

好消息,小白学视觉团队的知识星球开通啦,为了感谢大家的支持与厚爱,团队决定将价值149元的知识星球现时免费加入。各位小伙伴们要抓住机会哦!


下载1:OpenCV-Contrib扩展模块中文版教程
在「小白学视觉」公众号后台回复:扩展模块中文教程即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。

下载2:Python视觉实战项目52讲
小白学视觉公众号后台回复:Python视觉实战项目即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。

下载3:OpenCV实战项目20讲
小白学视觉公众号后台回复:OpenCV实战项目20讲即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。

交流群


欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~


浏览 29
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报