基于OpenCV的车道线检测算法(Traditional Method)!

0. Introduction

这篇文章主要是一种基于canny边缘检测的传统车道线检测算法,这种算法很轻量级,实时性较好,在进行基于深度学习的车道检测学习前,实现用Traditional Method的车道检测很有意义,其主要思想如下:代码将在文章后附上。

总体思想


1. Canny边缘检测

1.1 Canny的检测原理

Canny边缘检测是从不同视觉对象中提取有用的结构信息大大减少要处理的数据量的一种技术,目前已广泛应用于各种计算机视觉系统。Canny发现,在不同视觉系统上对边缘检测的要求较为类似,因此,可以实现一种具有广泛应用意义的边缘检测技术。

  • 边缘检测的一般标准包括:

① 以低的错误率检测边缘——尽可能准确的捕获图像中尽可能多的边缘

② 检测到的边缘应精确定位在真实边缘的中心。

③ 图像中给定的边缘应只被标记一次,并且在可能的情况下,图像的噪声不应产生假的边缘。边缘只要一个精确的点宽度。

  • 边缘检测算法的处理流程:
    • 高斯滤波,平滑图像;

为了尽可能减少噪声对边缘检测结果的影响,所以必须滤除噪声以防止由噪声引起的错误检测。为了平滑图像,使用高斯滤波器与图像进行卷积,该步骤将平滑图像,以减少边缘检测器上明显的噪声影响。


    • 计算图像中每个像素点的梯度强度和方向;

图像中边缘可以指向各个方向,Canny算法使用四个算子来检测图像中的水平、垂直和对角边缘。边缘检测的算子(如Roberts,Prewitt,Sobel等)返回水平Gx垂直Gy方向的一阶导数值,由此便可以确定像素点的梯度G和方向 \theta 。

G为梯度强度, G = \sqrt{G_{x}^2+G_{y}^2} (1)

\theta 表示梯度方向, \theta = arctan(G_{y}/G_{x}) (2)

下面以Sobel算子为例,描述如何计算梯度强度和方向

Sobel算子x和y方向分别为:

Sobel算子x和y方向

其中Sx表示x方向的Sobel算子,用于检测y方向的边缘; Sy表示y方向的Sobel算子,用于检测x方向的边缘(边缘方向和梯度方向垂直)。在直角坐标系中,Sobel算子的方向如下图所示。

Sobel算子方向

若图像中一个3x3的窗口为A,要计算梯度的像素点为e,则和Sobel算子进行卷积之后,像素点e在x和y方向的梯度值分别为:

像素点e在x和y方向的梯度值

其中*为卷积符号,sum表示矩阵中所有元素相加求和。根据公式(1)、(2)便可以计算出像素点e的梯度和方向。


    • 使用Non-Maximum Suppression(非极大值抑制)消除边缘检测带来的杂散响应;

如果直接把梯度作为边缘的话,将得到一个粗边缘的图像,在图像边缘区域,其附近梯度值往往都很大,这不满足上面提到的准则(最小响应标准),我们希望得到定位准确的单像素的边缘,所以将每个像素点的梯度与其梯度方向上的相邻像素比较,如果不是极大值,将其置0否则置为某一不大于255的数,非最大值抑制能帮助保留局部最大梯度而抑制所有其他梯度值。这意味着只保留了梯度变化中最锐利的位置,如下图:

NMS

点A在边缘上(在垂直方向上)。梯度方向与边缘垂直。B点和C点处于梯度方向。因此,点A,与点B和C进行比较,点A是否形成局部最大值。如果是这样,它被认为是边缘,否则,它被抑制(归零)。


    • 使用Double-Threshold(双阈值)检测确定真实和潜在的边缘;
    • 通过抑制鼓励的弱边缘最终完成边缘检测.

这个阶段决定哪些边缘是真正的边缘,哪些不是边缘。为此,我们需要两个阈值minVal和maxVal。强度梯度大于maxVal的任何边缘肯定是边缘,低于minVal的边缘肯定是非边缘,因此被丢弃。那些位于这两个阈值之间的点,则基于这些点是否与真正的边缘部分相连接如果它们连接到“真正边缘”像素,则它们被认为是边缘的一部分。否则,他们也被丢弃。看到下面的图片:

滞后阈值

边缘A在maxVal之上,因此被视为“真正的边缘”。虽然边C低于maxVal,但它连接到边A,所以也被认为是有效边缘,我们得到了完整的曲线。但是边B虽然高于minVal,但它并没有连接到任何“真正的边缘”因此被丢弃。因此,我们必须相应地选择minVal和maxVal以获得正确结果,这一点非常重要。


1.2 效果


2. 做Segment

2.1 方法

Segment的主要思想是,构建一个mask,这个mask包含主要的车道区域值均为1,其余为0,将这个mask与原frame进行叠加,就可以抠出主要的车道区域。

2.2 效果

可以看到,已经成功的抠出了车道线部分的图像。


3.做霍夫变换

3.1 Hough Transform原理

霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,常用来在图像中提取直线和圆等几何形状,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特征的集合作为霍夫变换的结果。

  • 霍夫直线变换

霍夫直线变换,比如在笛卡尔坐标系中,变量(x,y),参数(m,b),有:y=mx+b,则在霍夫空间中,变量(m,b),参数(x,y),有b=xm+y,可以看出,霍夫空间变量与参数进行互换;于是有,在笛卡尔坐标系中的一条线,y=mx+b,对应在霍夫空间,就是一点(m,b)(可以经过无穷多直线);在笛卡尔坐标系中的一个点:(x,y)(对应无穷的m,b),对应在霍夫空间中就是一条直线。

直线可以分别用直角坐标系和极坐标系来表示:

那么经过某个点(x0,y0)的所有直线都可以用这个式子来表示:

也就是说,每一个 (r,\theta) 都表示一条经过 (x_0,y_0) 的直线,则同一条直线上的点,必然会有相同的(r,\theta),如果将某个点所有的 (r,\theta) 绘制成曲线,同一条直线上的 (r,\theta) 曲线会相交于一点。通俗的解释就是霍夫空间中相交的曲线越多,交点表示的线在笛卡尔坐标系中对应的点越多。结合上面的分析,在笛卡尔坐标系与霍夫空间中,同样有:

OpenCV中,有两个API可以用来进行霍夫直线变换

①cv2.HoughLines(edges,0.8,np.pi/180,90),参数1,要检测的二值图像(一般是阈值分割或边缘检测后的图像);参数2,是距离r的精度,值越大,考虑越多的线;参数3,是角度 \theta 的精度,值越小,考虑越多的线;参数4,累加数阈值,即(r,\theta)的累加数,累加数超过一定值后就认为在同一直线上。值越小,考虑线越多;函数返回的是一组直线的(r,θ)数据.

②cv2.HoughLinesP(edges,0.8,np.pi/180,90,minLineLength=50, maxLineGap=10),是统计概率霍夫直线变换,前面的方法又称为标准霍夫变换,它会计算图像中的每一个点,计算量比较大,另外它得到的是整一条线(r和θ),并不知道原图中直线的端点。所以提出了统计概率霍夫直线变换(Probabilistic Hough Transform),是一种改进的霍夫变换;前面参数含义相同,minLineLength:最短长度阈值,比这个长度短的线会被排除;maxLineGap:同一直线两点之间的最大距离


4. 标定车道边界

4.1 基本方法

将从hough检测到的多条线平均成一条线表示车道的左边界, 一条线表示车道的右边界。基本思想很简单,就是先将霍夫变换的线段转换为一维信息,进行多项式拟合,在将得到的截距和斜率信息进行平均,在利用数值代换转换成cv坐标系的左边界线,和右边界线;其他的代码里有详细的注释,就不过多赘述。

4.2 效果

可以看到,车道中的标示线已经被很好的标出。


5. 可视化

最后的工作就是讲标定的车道边界在原始视频文件中输出,效果如下:


6.代码

#-*- coding: utf-8 -*- 

# 通过OpenCV实现车道线检测

# Key Point:
	# 1.打开视频文件
	# 2.循环遍历每一帧
	# 3.canny边缘检测,检测line
	# 4.去除多余图像直线
	# 5.霍夫变换
	# 6.叠加变换与原始图像
	# 7.车道检测

import numpy as np 
import cv2 as cv 
import matplotlib.pyplot as plt 

# Global Variables
VEDIO_LOCATION = "E:\\研究生三年级下学期\\写博客\\无人驾驶\\代码\\车道线检测\\input.mp4"

# Tools 
# Canny检测
def do_canny(frame):
	# 将每一帧转化为灰度图像,去除多余信息
	gray = cv.cvtColor(frame,cv.COLOR_BGR2GRAY)
	# 高斯滤波器,去除噪声,平滑图像
	blur = cv.GaussianBlur(gray,(5,5),0)
	# 边缘检测
	# minVal = 50
	# maxVal = 150
	canny = cv.Canny(blur,50,150)

	return canny

# 图像分割,去除多余线条信息
def do_segment(frame):
	# 获取图像高度(注意CV的坐标系,正方形左上为0点,→和↓分别为x,y正方向)
	height = frame.shape[0]

	# 创建一个三角形的区域,指定三点
	polygons = np.array([
		[(0,height), 
		 (800,height),
		 (380,290)]
		])

	# 创建一个mask,形状与frame相同,全为0值
	mask = np.zeros_like(frame)

	# 对该mask进行填充,做一个掩码
	# 三角形区域为1
	# 其余为0
	cv.fillPoly(mask,polygons,255) 

	# 将frame与mask做与,抠取需要区域
	segment = cv.bitwise_and(frame,mask) 

	return segment

# 车道左右边界标定
def calculate_lines(frame,lines):
	# 建立两个空列表,用于存储左右车道边界坐标
	left = []
	right = []

	# 循环遍历lines
	for line in lines:
		# 将线段信息从二维转化能到一维
		x1,y1,x2,y2 = line.reshape(4)

		# 将一个线性多项式拟合到x和y坐标上,并返回一个描述斜率和y轴截距的系数向量
		parameters = np.polyfit((x1,x2), (y1,y2), 1)
		slope = parameters[0] #斜率 
		y_intercept = parameters[1] #截距

		# 通过斜率大小,可以判断是左边界还是右边界
		# 很明显左边界slope<0(注意cv坐标系不同的)
		# 右边界slope>0
		if slope < 0:
			left.append((slope,y_intercept))
		else:
			right.append((slope,y_intercept))

	# 将所有左边界和右边界做平均,得到一条直线的斜率和截距
	left_avg = np.average(left,axis=0)
	right_avg = np.average(right,axis=0)
	# 将这个截距和斜率值转换为x1,y1,x2,y2
	left_line = calculate_coordinate(frame,parameters=left_avg)
	right_line = calculate_coordinate(frame, parameters=right_avg)

	return np.array([left_line,right_line])

# 将截距与斜率转换为cv空间坐标
def calculate_coordinate(frame,parameters):
	# 获取斜率与截距
	slope, y_intercept = parameters

	# 设置初始y坐标为自顶向下(框架底部)的高度
	# 将最终的y坐标设置为框架底部上方150
	y1 = frame.shape[0]
	y2 = int(y1-150)
	# 根据y1=kx1+b,y2=kx2+b求取x1,x2
	x1 = int((y1-y_intercept)/slope)
	x2 = int((y2-y_intercept)/slope)
	return np.array([x1,y1,x2,y2])

# 可视化车道线
def visualize_lines(frame,lines):
	lines_visualize = np.zeros_like(frame)
	# 检测lines是否为空
	if lines is not None:
		for x1,y1,x2,y2 in lines:
			# 画线
			cv.line(lines_visualize,(x1,y1),(x2,y2),(0,0,255),5)
	return lines_visualize

if __name__ == "__main__":

	# 视频读取
	cap = cv.VideoCapture(VEDIO_LOCATION) 

	# 当视频还是打开的时候,循环遍历每一帧
	while (cap.isOpened()): 
		ret,frame = cap.read() 

		# 边缘检测
		canny = do_canny(frame)
		# cv.imshow("canny", canny)

		# 图像分割,去除多余直线,只保留需要的直线
		# 原理见博文
		segment = do_segment(canny)
		# cv.imshow("segment", segment)

		# 原始空间中,利用Canny梯度,找到很多练成线的点
		# 利用霍夫变换,将这些点变换到霍夫空间中,转换为直线
		hough = cv.HoughLinesP(segment, 2, np.pi/180, 100,\
			minLineLength=100, maxLineGap=50) 
		# cv.imshow("hough", hough)

		# 将从hough检测到的多条线平均成一条线表示车道的左边界,
		# 一条线表示车道的右边界
		lines = calculate_lines(frame, hough)

		# 可视化
		lines_visualize = visualize_lines(frame, lines) #显示
		# cv.imshow("lines",lines_visualize)

		# 叠加检测的车道线与原始图像,配置两张图片的权重值
		# alpha=0.6, beta=1, gamma=1 
		output = cv.addWeighted(frame,0.6,lines_visualize,1,0.1)
		cv.imshow("output", output)

		# q键退出
		if cv.waitKey(10)&0xff == ord('q'):
			break

	# 释放,关闭
	cap.release()
	cv.destroyAllWindows()
		

Reference

猜你喜欢

转载自blog.csdn.net/qq_42156420/article/details/89401238