MATLAB基于传统方法的车道线检测实现
本文实现的是基于传统方法的车道线检测,所谓传统方法就是没有涉及到深度学习算法,基于直观的手段和数学知识来实现,后期会实现基于深度学习的车道线检测方法。
实现步骤:
- Canny边缘检测
- 手动分割路面区域
- 霍夫变换得到车道线
- 获取车道线并叠加到原始图像中
算法演示视频如下:
Canny边缘检测
Canny边缘检测就是检测出视频中出现的所有的线,如图1所示:
图1
基本原理:检测亮度的急剧变化(常见的就是大梯度,如从白色到黑色),在给定阈值下定义为边。
Canny检测的步骤:
(1)对原始图像进行灰度化
Canny算法通常处理的图像为灰度图,因此如果摄像头获取的是彩色图像,那首先就得进行灰度化。对一幅彩色图进行灰度化,就是根据图像各个通道的采样值进行加权平均。以RGB格式的彩图为例,通常灰度化采用的方法主要有:
-
- Gray=(R+G+B)/3;
- Gray=0.299R+0.587G+0.114B;(这种参数考虑到了人眼的生理特点)
(2)高斯滤波
滤波的主要目的是降噪,一般的图像处理算法都需要先进行降噪。而高斯滤波主要使图像变得平滑(模糊),同时也有可能增大了边缘的宽度。本文实现的代码采用 5*5 的高斯滤波器(正太分布核)对图像做卷积(平滑图像)。
高斯函数是一个类似与正态分布的中间大两边小的函数。
对于一个位置(m,n)的像素点,其灰度值(这里只考虑二值图)为f(m,n)。
那么经过高斯滤波后的灰度值将变为:
gσ(m,n)=12πσ2e−m2+n22σ2⋅f(m,n)
简单说就是用一个高斯矩阵乘以每一个像素点及其邻域,取其带权重的平均值作为最后的灰度值。
(3)用一阶偏导的有限差分来计算梯度的幅值和方向
边缘是什么?边缘就是灰度值变化较大的的像素点的集合。一道黑边一道白边中间就是边缘,它的灰度值变化是最大的,在图像中,用梯度来表示灰度值的变化程度和方向。
它可以通过点乘一个Sobel、Roberts、Prewitt等算子沿x轴和y轴检测边缘是水平的、垂直的或者是对角线,得到不同方向的梯度值gx(m,n),gy(m,n)。
综合梯度通过以下公式计算梯度值和梯度方向:
G(m,n)=gx(m,n)2+gy(m,n)2
θ=arctangy(m,n)gx(m,n)
(5)非极大值抑制
图像梯度幅值矩阵中的元素值越大,说明图像中该点的梯度值越大,但不能说明该点就是边缘(这仅仅是属于图像增强的过程)。在Canny算法中,非极大值抑制是进行边缘检测的重要步骤,简单说就是寻找像素点局部最大值,将非极大值点所对应的灰度值置为0,这样可以剔除掉许多非边缘的点,因为如果一个像素点属于边缘,那么这个像素点在梯度方向上的梯度值是最大的,否则不是边缘,将灰度值设为0。
(6)使用上下阀值来检测边缘
非极大值抑制后可以确认强像素在最终边缘映射中。但还要对弱像素进行进一步分析确定它是边缘还是噪声。
通过设置两个阀值(threshold),如图2所示,分别为maxVal和minVal。
通过如下方式来确定是否为边缘:
- 大于maxVal的都被检测为边缘
- 小于minVal的都被检测为非边缘
- 对于大于minVal而小于maxVal的像素点,如果与确定为边缘的像素点邻接,则判定为边缘;否则为非边缘。
Canny边缘检测实现代码
def do_canny(frame):
# Converts frame to grayscale because we only need the luminance channel for detecting edges - less computationally expensive
gray = cv.cvtColor(frame, cv.COLOR_RGB2GRAY)
# Applies a 5x5 gaussian blur with deviation of 0 to frame - not mandatory since Canny will do this for us
blur = cv.GaussianBlur(gray, (5, 5), 0)
# Applies Canny edge detector with minVal of 50 and maxVal of 150
canny = cv.Canny(blur, 50, 150)
return canny
二、手动分割路面区域
图3
Canny边缘检测出了所有的边缘(图3左),很显然,我们需要的是表示道路的线(图3右),因此需要排除其他的线条。
我们采用手动指定一个三角形来分割出路面区域,去除其它干扰边缘。
图4
具体的说就是以原始的图片建立直角坐标系,指定三角形的三个顶点,保留三角形区域中的边缘线条,去除其他多余的线条,如图4所示,实现代码如下:
def calculate_coordinates(frame, parameters):
slope, intercept = parameters
# Sets initial y-coordinate as height from top down (bottom of the frame)
y1 = frame.shape[0]
# Sets final y-coordinate as 150 above the bottom of the frame
y2 = int(y1 - 150)
# Sets initial x-coordinate as (y1 - b) / m since y1 = mx1 + b
x1 = int((y1 - intercept) / slope)
# Sets final x-coordinate as (y2 - b) / m since y2 = mx2 + b
x2 = int((y2 - intercept) / slope)
return np.array([x1, y1, x2, y2])
实现代码
def do_segment(frame):
height = frame.shape[0]
# Creates a triangular polygon for the mask defined by three (x, y) coordinates
polygons = np.array([
[(0, height), (800, height), (380, 290)]
])
#(0, height)对应图4中的点A,(800, height)对应图4中的点B,(380, 290)对应图4中的点C,由于是认为指定,所以存在误差。
mask = np.zeros_like(frame)
# Allows the mask to be filled with values of 1 and the other areas to be filled with values of 0
cv.fillPoly(mask, polygons, 255)
# A bitwise and operation between the mask and frame keeps only the triangular area of the frame
segment = cv.bitwise_and(frame, mask)
return segment
三、霍夫变换得到车道线
想象一下,人类在一副图片中找出一条直线或者圆,相信对于大家应该都是一件相当容易的事,但是对于计算机来说,一副图像所呈现的只是灰度值从0-255的庞大矩阵而已,它可不容易知道复制的矩阵中哪些是直线哪些不是,霍夫变换便是帮助计算机'看到'图像中的直线或圆的一种算法。
1.基本思想
将传统的图像从x 、y轴坐标系变换到参数空间(m, b)或者霍夫空间(Hough space)中,通过在参数空间(parameter space)或可称为累加空间(accumulator space)中计算局部最大值从而确定原始图像直线或圆所在位置。
2.常见的霍夫变换
- 基于笛卡尔坐标空间的霍夫变换
- 基于极坐标空间的霍夫变换
(1)基于笛卡尔坐标空间的霍夫变换
在平面直角坐标中,一条直线的表示通常用y=m0x+b0表示,其中m0表示的是直线的斜率,b0表示的是直线的截距,一条直线上的点所使用的是同一个m0b0,因此我们可以设想一下,如果有一个坐标轴体系是以m0为横轴,b0为竖轴,形成以(m0,b0)为参数的参数空间,是不是在平面坐标中同一条直线上的点在参数空间表示为一个点呢,霍夫变换即是基于这种思想而诞生的,如图5所示。
图5
图6
从数学上来解释两者的转换,如图6所示。
让我们换一种思维来更深层次理解一下霍夫变换,在xy笛卡尔坐标轴上的任意一点(x,y)随着x斜率m0和截距b0的改变,在参数空间各种m0b0的组合将会呈现为一条直线,如图 7所示,为点A(1,6)在参数空间的投影
图7
现在我们在xy笛卡尔坐标轴做另一点B(2, 8)的参数空间投影,如图 8所示
图8
可以发现,在参数空间中两直线交于粉色点位(2, 4),这里所体现的信息为:点A、B两点连接的直线斜率为m0=4,截距为b0=4,因此,现在我们可以理解霍夫变换其实就是计算参数空间累加点的值大小,值越大越说明这个点的参数m0b0所代表的直线置信度越高。
但是,基于笛卡尔坐标空间的霍夫变换存在一种特殊情况:当线垂直时梯度无穷大,无法在霍夫空间中表示出来。为了解决这个问题,我们在笛卡尔坐标系中用极坐标法表示直线。对应到霍夫空间也做对应变化。
(2)基于极坐标空间的霍夫变换
极坐标是指在平面内由极点、极轴和极径组成的坐标系,其中假设有一点P(ρ,θ),其中参数ρ表示极径,即极点O到P点的距离,参数θ表示极角,即OX到OP的角度。
假设有一条直线,原点到该直线的垂直距离为 ρ ,垂线与x轴的夹角为 θ ,那么这条直线是唯一的,且直线方程为:
d=xcos(θ)+ysin(θ)
和直角坐标系类似,霍夫空间中相交的曲线越多,交点表示的线在笛卡尔坐标系对应的点越多。我们在霍夫空间中定义交点的最小阈值来检测线,霍夫变换跟踪了帧中的每个点的霍夫空间交点,如果交点数量超过了阈值就确定一条对应参数 θ 和 d的线。
之所以用阈值来确定参数,是因为在现实的应用场景中,许多直线并不是非常精细,或多或少存在偏差,导致参数空间各曲线不能交于精确的一点,因此我们需要将参数空间分块,分块的步长则为单位长度的ρ和θ,其次计算单位区域内累加的交点数量,将大于阈值(threshold)的区域值认定为直线存在,存储其参数(ρ,θ)。
但是,分块的步长对检测的精准度也有影响,分的太细,计算代价就会上升,分的太大,计算的准确率就会下降,因此现在通用的常用做法是:ρ步长设为单像素单位,θ步长设为π180,并且现实场景中的应用也会使用一种Mask掩模的做法,提取我们感兴趣的图像区域,以此来大大减少计算量。
霍夫变换代码:
hough = cv.HoughLinesP(segment, 2, np.pi / 180, 100, np.array([]), minLineLength = 100, maxLineGap = 50)
霍夫变换画出的车道线
四、获取车道线并叠加到原始图像中
这一步是将彩色图像与我们上一步绘制的车道线图像进行比例的融合。
首先,综合所有线,求得左右两条车道线的平均斜率和截距。
实现代码:
def calculate_lines(frame, lines):
# Empty arrays to store the coordinates of the left and right lines
left = []
right = []
# Loops through every detected line
for line in lines:
# Reshapes line from 2D array to 1D array
x1, y1, x2, y2 = line.reshape(4)
# Fits a linear polynomial to the x and y coordinates and returns a vector of coefficients which describe the slope and y-intercept
parameters = np.polyfit((x1, x2), (y1, y2), 1)
slope = parameters[0]
y_intercept = parameters[1]
# If slope is negative, the line is to the left of the lane, and otherwise, the line is to the right of the lane
if slope < 0:
left.append((slope, y_intercept))
else:
right.append((slope, y_intercept))
# Averages out all the values for left and right into a single slope and y-intercept value for each line
left_avg = np.average(left, axis = 0)
right_avg = np.average(right, axis = 0)
# Calculates the x1, y1, x2, y2 coordinates for the left and right lines
left_line = calculate_coordinates(frame, left_avg)
right_line = calculate_coordinates(frame, right_avg)
return np.array([left_line, right_line])
然后,将原始彩色图像与我们刚画的车道线图像进行比例的融合。
这里需要用到的函数cv.addWeighted(src1, alpha, src2, beta, gamma, dst=None, dtype=None),参数src1表示第一张图像或矩阵,alpha是它对应的权重(Weight),src2表示的则是第二副图像或矩阵,beta是它对应的权重,第五个参数gamma表示整体添加到数值,默认为0即可。
我们是按照0.9:1进行融合。
实现代码: output = cv.addWeighted(frame, 0.9, lines_visualize, 1, 1)