霍夫变换技术是一个神奇的工具,可用于定位图像中的形状。它经常被用来检测圆、椭圆和线,以获得图像的确切位置或几何理解。Hough变换的这种识别形状的能力使它成为检测自驾车车道线的理想工具。
在试图理解自驾车如何工作时,车道检测是基本概念之一。在这篇文章中,我们将建立一个可以在图片或视频中识别车道线的程序,并学习Hough变换如何在实现这一任务中发挥巨大作用。Hough变换几乎是检测感兴趣区域中的直线的最后一步。由于知道我们是如何到达这个阶段的也很重要,所以在我们经历每一个步骤时要有耐心。
把这个项目带入生活
目录
- 项目设置
- 加载和显示图像
- 边缘检测。灰度处理、降噪、Canny方法
- 隔离感兴趣的区域(ROI)。
- Hough变换技术
- 实现Hough变换
- 检测视频中的车道
- 最后的思考
项目设置
当人类驾驶汽车时,我们用眼睛看到车道。但是,由于汽车无法做到这一点,我们使用计算机视觉来使它 "看到 "车道线。我们将使用OpenCV来读取和显示我们图像中的一系列像素。
要开始了。
- 安装这张图片,并将其作为JPEG文件保存在一个文件夹中。
- 打开一个IDE,在同一文件夹中创建一个Python文件。让我们把它命名为
lanes.py - 用以下命令从终端安装openCV --
pip install opencv-contrib-python
加载和显示图像
openCV库有一个叫做cv2.imread() 的函数,它从我们的文件中加载图像并以多维NumPy数组的形式返回。
import cv2
image = cv2.imread('lane.jpg')
NumPy数组表示图像中每个像素的相对强度。
我们现在有了数组形式的图像数据。下一步是用一个叫做imshow() 的函数来渲染我们的图像。这需要两个参数--第一个是显示图像的窗口名称,第二个是我们想要显示的图像。
import cv2
image = cv2.imread('lane.jpg')
cv2.imshow('result', image)
cv2.waitKey(0)
waitKey() 函数允许我们在指定的毫秒时间内显示图像。这里的'0'表示该函数可以无限地显示图像,直到我们按下键盘上的任何东西。
打开终端,用python lanes.py 运行程序,你应该看到屏幕上显示的图像。
Canny边缘检测
在本节中,我们将讨论Canny边缘检测,这是一种技术,我们将用它来编写一个程序来检测图像中的边缘。因此,我们试图找到图像中强度急剧变化和颜色急剧变化的区域。
重要的是要记住,图像可以作为一个矩阵(一个像素阵列)来阅读。 一个像素包含图像中某个位置的光强度。每个像素的强度由0到255的数值表示。0值表示没有强度(黑色),255表示最大强度(白色)。
注:梯度是对一系列像素上亮度变化的一种衡量。强烈的梯度表示剧烈的变化,而小的梯度则表示浅层的变化。
梯度的加强有助于我们识别图像中的边缘。边缘是由相邻像素的强度值的差异定义的。每当你看到强度的急剧变化或亮度的快速变化,在梯度图像中就有一个相应的明亮像素。通过追踪这些像素,我们得到了边缘。
现在,我们要应用这一直觉来检测我们图像上的边缘。在这个过程中,有三个步骤。
第1步:灰度缩放
将我们的图像转换为灰度的原因是为了方便处理它。灰度图像只有一个像素的强度值(0或1),而彩色图像则有三个以上的值。这将使灰度图像以单通道工作,这将使我们更容易和更快地处理,而不是三通道的彩色图片。
为了在代码中实现这一点,我们将在NumPy的帮助下,对之前创建的图像数组进行复制。
image = cv2.imread('lane.jpg')
lane_image = np.copy(image) #creating copy of the image
重要的是要创建一个image 变量的副本,而不是将新变量设置为等于图像。这样做将确保在lane_image 中所作的改变不会影响到image 。
现在,我们在openCV库中的cvtColor 函数的帮助下将图像转换成灰度。
gray = cv2.cvtColor(lane_image, cv2.COLOR_RGB2GRAY) #converting to gray-scale
标志COLOR_RGB2GRAY 作为第二个参数被传递,它有助于将RGB颜色转换为灰度图像。
为了输出这种转换,我们需要在我们的结果窗口中传递gray 。
import cv2
import numpy as np
image = cv2.imread('lane.jpg')
lane_image = np.copy(image)
gray = cv2.cvtColor(lane_image, cv2.COLOR_RGB2GRAY)
cv2.imshow('result', gray) #to output gray-scale image
cv2.waitKey(0)
如果我们运行这个程序,得到的图像应该如下图所示。
第2步:减少噪音和平滑化
尽可能多地识别图像中的边缘是很重要的。但是,我们也必须过滤任何可能产生虚假边缘并最终影响边缘检测的图像噪音。减少噪音和平滑图像的工作将由一个叫做高斯模糊的过滤器完成**。**
请记住,图像被存储为离散像素的集合。灰度图像中的每个像素都由一个描述该像素亮度的数字来表示。为了平滑图像,我们需要用它周围像素强度的平均值来修改一个像素的值。
对像素进行平均化以减少噪音,将用一个内核来完成。这个由正态分布的数字组成的内核窗口在我们的整个图像上运行,并将每个像素值设置为其邻近像素的加权平均值,从而使我们的图像变得平滑。
为了在代码中表示这种卷积,我们在我们的灰度图像上应用cv2.GaussianBlur() 函数。
blur = cv2.GaussianBlur(gray, (5, 5), 0)
在这里,我们在我们的图像上应用一个5*5的核窗。核的大小取决于情况,但5*5的窗口在大多数情况下是理想的。我们将blur 变量传递给imshow() ,以获得输出。因此,获得高斯模糊图像的最终代码看起来像这样。
import cv2
import numpy as np
image = cv2.imread('lane.jpg')
lane_image = np.copy(image)
gray = cv2.cvtColor(lane_image, cv2.COLOR_RGB2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
cv2.imshow('result', blur) #to output gaussian image.
cv2.waitKey(0)
得到的是一个模糊的图像,它是通过卷积一个高斯值的内核而得到的,它的噪声降低了。
注意:这是理解高斯模糊的一个可选的步骤。当我们在下一步执行Canny边缘检测时,它会自动为我们执行这个步骤。
第3步:Canny方法
为了理解这个概念,你必须记得,图像也可以用二维坐标空间来表示 - X和Y。
X对应的是图像的宽度(列数),Y对应的是图像的高度(行数)。宽度和高度的乘积给了我们图像中的总像素数。这告诉我们,我们不仅可以用数组表示图像,还可以用X和Y的连续函数即f(x, y)表示**。**
由于_f(x, y)_是一个数学函数,我们可以通过操作来确定图像中像素的亮度的快速变化。Canny方法将给我们的函数在x和y方向上的导数。我们可以用这个导数来测量关于相邻像素的强度变化。
导数的微小变化对应于强度的微小变化,反之亦然。
通过计算所有方向的导数,我们得到图像的梯度。在我们的代码中调用cv2.Canny() 函数,为我们执行所有这些动作。
cv2.Canny(image, low_threshold, high_threshold)
这个函数将以一系列白色像素的形式追踪最强的梯度。low_threshold 和high_threshold 这两个参数使我们能够分离出遵循最强梯度的相邻像素。如果梯度大于上限阈值,那么它就被识别为一个边缘像素。如果它低于较低的阈值,它就会被拒绝。介于阈值之间的梯度只有在它与强边缘相连时才会被接受。
对于我们的案例,我们将采取1:3的低-高阈值比率。这一次我们输出的是canny图像,而不是模糊的图像。
canny = cv2.Canny(blur, 50, 150) #to obtain edges of the image
cv2.imshow('result', Canny)
cv2.waitKey(0)
得到的图像看起来是这样的。
隔离感兴趣的区域(ROI
在我们教我们的模型进行检测之前,我们必须指定我们感兴趣的区域来检测车道线。
在这种情况下,让我们把感兴趣的区域作为道路的右侧,如图所示。
由于我们的代码中有很多变量和函数,现在是一个很好的时机,通过定义一个函数来包装一切。
import cv2
import numpy as np
def canny(image):
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
blur = cv2.GaussianBlur(gray,(5, 5), 0)
canny = cv2.Canny(blur, 50, 150)
return canny
image = cv2.imread('lane.jpg')
lane_image = np.copy(image)
canny = cv2.Canny(lane_image)
cv2.imshow('result', canny)
cv2.waitKey(0)
为了明确我们感兴趣的区域的确切位置,我们使用matplotlib 库来发现坐标并分离出该区域。
import cv2
import numpy as np
import matplotlib.pyplot as plt
def canny(image):
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
blur = cv2.GaussianBlur(gray,(5, 5), 0)
canny = cv2.Canny(blur, 50, 150)
return canny
image = cv2.imread('lane.jpg')
lane_image = np.copy(image)
canny = cv2.Canny(lane_image)
plt.imshow(canny)
plt.show()
运行这个程序,我们就可以得到一个与我们感兴趣的区域相对应的X和Y坐标的图像。
现在我们已经有了我们的投资回报率所需的测量值,我们要生成一个图像来掩盖其他的东西。我们得到的图像是我们原始图像中具有指定顶点的多边形的遮罩。
def region_of_interest(image):
height = image.shape[0]
polygons = np.array([
[(200, height), (1100, height), (550, 250)]
])
mask = np.zeros_like(image)
cv2.fillPoly(mask, polygons, 255)
return mask
...
cv2.imshow('result', region_of_interest(canny)) #changing it to show ROI instead of canny image.
...
上面的代码是不言自明的。它返回的是我们的场景的封闭区域,是三角形的。
这个图像很重要,因为它代表了我们感兴趣的具有强烈梯度差异的区域。三角形区域的像素强度值是255,对于图像的其他部分,它是零。由于这幅图像的测量值与我们的原始图像相同,它可以被用来从我们之前生成的canny图像中轻松提取线条。
我们可以通过利用OpenCV的cv2.bitwise_and() 函数来实现这一点,该函数计算了两幅图像的位数和,只显示由遮罩的多边形轮廓追踪的ROI。关于使用bitwise_and 的更多信息,请参考openCV文档。
def region_of_interest(image):
height = image.shape[0]
polygons = np.array([
[(200, height), (1100, height), (550, 250)]
])
mask = np.zeros_like(image)
cv2.fillPoly(mask, polygons, 255)
masked_image = cv2.bitwise_and(image, mask)
return masked_image
...
cropped_image = region_of_interest(canny)
cv2.imshow('result', cropped_image)
...
你应该期待一个看起来如下的输出---
这个过程的最后一步是使用Hough变换来检测我们感兴趣的孤立区域的直线。
哈夫变换技术
我们现在拥有的图片只是一系列的像素,我们无法直接找到一个几何表示法来知道斜率和截距。
由于图像从来都不是完美的,我们无法通过像素的循环来寻找斜率和截距,因为这将是一个非常困难的任务。这时就可以使用Hough变换了。它可以帮助我们找出突出的线条并连接图像中不相干的边缘点。
让我们通过比较正常的X-Y坐标空间和Hough空间(M-C空间)来理解这个概念。
在XY平面上的一个点可以有任何数量的线通过它。如果我们在同一个XY平面上取一条线,也是如此,许多点都会通过这条线。
为了识别我们图片中的线条,我们必须把每个边缘像素想象成坐标空间中的一个点,我们要把这个点转换成Hough空间中的一条线。
我们需要找到两条或更多代表其对应点(在XY平面上)的线,它们在Hough空间中相交,以检测线条。这样我们就知道这两个点属于同一条线。
从一系列的点中找到可能的线的想法就是我们如何在梯度图像中找到线。但是,该模型还需要线条的参数来识别它们。
为了得到这些参数,我们首先将hough空间划分为一个包含小方块的网格,如图所示。具有最多交叉点的方格所对应的c和m的值将被用于绘制最佳拟合线。
这种方法对有斜率的线条很有效。但是,如果我们处理的是一条直线,斜率将永远是无穷大,我们不能完全用这个值来工作。因此,我们在方法上做了一个小小的改变。
与其将我们的直线方程表示为 y = mx + c我们用极坐标系来表示,即:ρ = Xcosθ + Ysinθ。
- ρ = 垂直 离原点的距离。
- θ = 法线与x轴的倾斜角。
通过用极坐标表示法线,我们在hough空间中得到一条正弦曲线,而不是直线。这条曲线是针对所有通过我们的点的_ρ和θ_ 的 不同值而确定的。如果我们有更多的点,它们会在我们的hough空间中创造更多的曲线。与之前的情况类似,对应于最相交的曲线的值,将被用来创建最佳拟合线。
实现Hough变换
既然我们终于有了一种识别图像中线条的技术,那么就让我们在代码中实现它。幸运的是,openCV已经有一个叫做cv2.HoughLinesP() 的函数,可以用来为我们完成这个任务。
lines = cv2.HoughLinesP(cropped_image, 2, np.pi/180, 100, np.array([]), minLineLength=40, maxLineGap=5)
第一个参数是前面生成的裁剪后的图像,它是一个孤立的车道线的梯度图像。第二个和第三个参数指定hough累加器阵列的分辨率(为识别大多数交叉口而创建的网格)。第四个参数是确定检测一条线所需的最小票数的阈值。
在我们的真实图像中显示这些线之前,让我们定义几个函数来表示它们。我们将定义3个函数来完美地优化和显示车道线。
- **display_lines:**我们定义这个函数是为了在黑色图像上标记线条,其测量值与原始图像相似,然后将其混合到我们的彩色图像中。
def display_lines(image, lines):
line_image = np.zeros_like(image)
if lines is not None:
for x1, y1, x2, y2 in lines:
cv2.line(line_image, (x1, y1), (x2, y2), (255, 0, 0), 10)
return line_image
我们利用cv2.addWeight() ,将线条图像和彩色图像结合起来。
line_image = display_lines(lane_image, averaged_lines)
combo_image = cv2.addWeighted(lane_image, 0.8, line_image, 1, 1)
- make_coordinates:这 将为我们指定坐标,以便能够标记斜率和Y截距。
def make_coordinates(image, line_parameters):
slope, intercept = line_parameters
y1 = image.shape[0]
y2 = int(y1*(3/5))
x1 = int((y1 - intercept)/slope)
x2 = int((y2 - intercept)/slope)
return np.array([x1, y1, x2, y2])
- average_slope_intercept: 我们首先声明两个空列表--
left_fit和right_fit,它们将分别包含左边的平均线的坐标和右边的线的坐标。
def average_slope_intercept(image, lines):
left_fit = []
right_fit = []
for line in lines:
x1, y1, x2, y2 = line.reshape(4)
parameters = np.polyfit((x1, x2), (y1, y2), 1)
slope = parameters[0]
intercept = parameters[1]
if slope < 0:
left_fit.append((slope, intercept))
else:
right_fit.append((slope, intercept))
left_fit_average = np.average(left_fit, axis=0)
right_fit_average = np.average(right_fit, axis=0)
left_line = make_coordinates(image, left_fit_average)
right_line = make_coordinates(image, right_fit_average)
return np.array([left_line, right_line])
np.polyfit() 将给我们提供拟合我们的点的直线参数,并返回一个描述斜率和Y截距的系数向量。
最后,我们显示combo_image() ,在这里我们的车道线被完美地检测出来。
cv2.imshow('result', combo_image)
检测视频中的车道
检测视频中的车道也要遵循同样的过程。OpenCV的VideoCapture对象使之变得简单。这个对象可以让你逐帧读取并执行你需要的操作。
一旦你下载了这个视频,把它移到你目前工作的项目文件夹中。
我们为我们的视频设置一个视频捕捉对象,并应用我们已经实现的算法来检测视频中的线条而不是静态图像。
cap = cv2.VideoCapture('test2.mp4')
while(cap.isOpened()):
_, frame = cap.read()
canny_image = canny(frame)
cropped_image = region_of_interest(canny_image)
lines = cv2.HoughLinesP(cropped_image, 2, np.pi/180, 100, np.array([]), minLineLength=40, maxLineGap=5)
averaged_lines = average_slope_intercept(frame, lines)
line_image = display_lines(frame, averaged_lines)
combo_image = cv2.addWeighted(frame, 0.8, line_image, 1, 1)
cv2.imshow('result',combo_image)
if cv2.waitKey(1) == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
注意:在尝试实现上述功能时,最好把与静态图像有关的代码注释掉,留待以后使用。 留待以后使用。
#image = cv2.imread('lane.jpg')
#lane_image = np.copy(image)
#canny_image = canny(lane_image)
#cropped_image = region_of_interest(canny_image)
#lines = cv2.HoughLinesP(cropped_image, 2, np.pi/180, 100, np.array([]), minLineLength=40, maxLineGap=5)
#averaged_lines = average_slope_intercept(lane_image, lines)
#line_image = display_lines(lane_image, averaged_lines)
#combo_image = cv2.addWeighted(lane_image, 0.8, line_image, 1, 1)
#cv2.imshow('result',combo_image)
#cv2.waitKey(0)
如果一切运行良好,你将在你的视频中看到同样的车道线的作用。
这是个很大的解释,在某些时候感到困惑是很正常的。所以,这就是你在lanes.py ,当你完成后,你的整个代码应该是这样的。
import cv2
import numpy as np
def make_coordinates(image, line_parameters):
slope, intercept = line_parameters
y1 = image.shape[0]
y2 = int(y1*(3/5))
x1 = int((y1 - intercept)/slope)
x2 = int((y2 - intercept)/slope)
return np.array([x1, y1, x2, y2])
def average_slope_intercept(image, lines):
left_fit = []
right_fit = []
for line in lines:
x1, y1, x2, y2 = line.reshape(4)
parameters = np.polyfit((x1, x2), (y1, y2), 1)
slope = parameters[0]
intercept = parameters[1]
if slope < 0:
left_fit.append((slope, intercept))
else:
right_fit.append((slope, intercept))
left_fit_average = np.average(left_fit, axis=0)
right_fit_average = np.average(right_fit, axis=0)
left_line = make_coordinates(image, left_fit_average)
right_line = make_coordinates(image, right_fit_average)
return np.array([left_line, right_line])
def canny(image):
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
blur = cv2.GaussianBlur(gray,(5, 5), 0)
canny = cv2.Canny(blur, 50, 150)
return canny
def display_lines(image, lines):
line_image = np.zeros_like(image)
if lines is not None:
for x1, y1, x2, y2 in lines:
cv2.line(line_image, (x1, y1), (x2, y2), (255, 0, 0), 10)
return line_image
def region_of_interest(image):
height = image.shape[0]
polygons = np.array([
[(200, height), (1100, height), (550, 250)]
])
mask = np.zeros_like(image)
cv2.fillPoly(mask, polygons, 255)
masked_image = cv2.bitwise_and(image, mask)
return masked_image
#image = cv2.imread('lane.jpg')
#lane_image = np.copy(image)
#canny_image = canny(lane_image)
#cropped_image = region_of_interest(canny_image)
#lines = cv2.HoughLinesP(cropped_image, 2, np.pi/180, 100, np.array([]), minLineLength=40, maxLineGap=5)
#averaged_lines = average_slope_intercept(lane_image, lines)
#line_image = display_lines(lane_image, averaged_lines)
#combo_image = cv2.addWeighted(lane_image, 0.8, line_image, 1, 1)
#cv2.imshow('result',combo_image)
#cv2.waitKey(0)
cap = cv2.VideoCapture('test2.mp4')
while(cap.isOpened()):
_, frame = cap.read()
canny_image = canny(frame)
cropped_image = region_of_interest(canny_image)
lines = cv2.HoughLinesP(cropped_image, 2, np.pi/180, 100, np.array([]), minLineLength=40, maxLineGap=5)
averaged_lines = average_slope_intercept(frame, lines)
line_image = display_lines(frame, averaged_lines)
combo_image = cv2.addWeighted(frame, 0.8, line_image, 1, 1)
cv2.imshow('result',combo_image)
if cv2.waitKey(1) == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
最后的思考
这是一个车道检测模型的简单例子。你可以进一步推动这个项目,使算法适应实时视频数据,并在汽车移动时检测车道线。
祝贺你走到这一步!希望你觉得这个教程很有帮助,并探索与这个概念相关的更多领域。
今天就为你的机器学习工作流程增加速度和简单性吧
DiscourseEmbed = { discourseUrl: 'community.paperspace.com/', discourseEmbedUrl: 'blog.paperspace.com/understandi…; (function() { var d = document.createElement('script'); d.type = 'text/javascript'; d.async = true; d.src = DiscourseEmbed.discourseUrl + 'javascripts/embed.js'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(d); })()。