常见的车道线检测的方法大致可以分为三类:
该文章主要介绍如何实现一个简单的传统车道线检测项目,详细阐述了传统车道线检测的流程并对霍夫变换的思想及canny边缘检测流程进行了概述。
霍夫变换是一种特征检测,被广泛应用在图像分析、计算机视觉以及数位影像处理。经典的霍夫变换是侦测图片中的直线,之后,霍夫变换不仅能够识别直线,也能够识别任何形状,常见的有圆形、椭圆形。
问题:对于人类而言,识别一副图像中的直线或是圆,是一件非常容易的事情。但对于计算机而言,一副图像所呈现的只是灰度值从0~255的矩阵而已,它无法判断该矩阵中的哪些是直线哪些不是,而霍夫变化就是帮助计算机“看到”图像中直线或圆的一种算法。
1. 基本思想
将传统的图像从X,Y轴坐标系变化到参数空间(m,b)或者霍夫空间(hough space)中,通过参数空间(可称为累加空间)计算局部最大值从而确定原始图像中直线或圆的位置。
2. 常见的霍夫变换
在平面直角坐标系中,一条直线是由其斜率m0和截距b0所确定的,换句话而言,一条直线上的点所使用的是同一对m0和b0。所以,如果存在一个坐标体系是由m0为横轴,b0为纵轴,形成以(m0,b0)为参数的参数空间,那么平面直角坐标系中的一条直线上的点在参数空间上可表示为一个点。
当平面直角坐标系中的任意一点随着m0和b0的改变,在参数空间的所呈现的就是一条直线。在平面直角坐标系中任取两点A,B,并在参数空间投影,两条直线交于一点C(p,q)。交点C所体现的信息就是,点A,B两点连接的直线斜率为p,截距为q。因此我们可以理解霍夫变换其实就是计算参数空间累加点的值大小,值越大说明该点的参数m0,b0所代表的直线置信度越高。
但当直线垂直于X轴时,斜率m0将会变得无穷大,在计算机数值运算时会造成无法进行的现象。因此提出基于极坐标空间的霍夫变换,来解决这一问题。
极坐标是由极点,极轴,极径组成的坐标系。那么,一条直线可以被原点到该直线的垂直距离和垂线与X轴的夹角所唯一确定。投影到参数空间都可以表示为一条正弦曲线。那么极坐标系参数空间就与笛卡尔坐标系参数空间类似,都可以看成累加空间计算局部最大值,进行直线的预测。
但在现实的应用场景中,许多直线并不是非常精细,或多或少存在偏差,导致参数空间各曲线不能交于精确的一点,因此我们需要将参数空间分块,分块的步长则为单位长度的极径和极角。其次计算单位区域内累加的交点数量,将大于阈值的区域值认定为直线存在,存储其参数。
但是分块的步长对检测的精准度也会有影响,分的太细,计算代价就会上升,分的太大,计算的准确率就会下降。现实场景的应用会使用到一种Mask掩膜的做法,提取我们感兴趣的区域,来大大减少计算量。
边缘检测算法本质上就是一种滤波算法,区别在于滤波器的选择,其与滤波的规则是一致的。为了理解边缘检测算子,我们引入梯度这个概念,梯度在数字图像处理领域可以理解为像素灰度值变化速度,但在数字图像处理中,实际的应用是不需要求导的,只需要进行简单的加减运算。
几种基本的边缘检测滤波器:sobel、prewitt、roberts算子。
问题:直接使用基本的边缘算子求得的边缘图存在很多问题,如噪声污染没有被排除、边缘线太过粗宽等。因此我们介绍一个先进的边缘检测算子——canny算子。
1. 高斯滤波
高斯滤波的原理:根据待滤波的像素点及其邻域点的灰度值按照高斯公式生成的参数规则进行加权平均。
2. 计算梯度图像与角度图像
canny中使用的梯度检测算子是使用高斯滤波器进行梯度计算得到的滤波器,得到的结果也类似于sobel算子,即距离中心点越近的像素点权重越大。
角度图像的计算则较为简单,其作用为非极大值抑制的方向提供指导。
3. 对梯度图像进行非极大值抑制
上一步得到的梯度图像存在边缘粗宽、弱边缘干扰等众多问题,现在可以使用非极大值抑制来寻找像素点局部最大值,将非极大值所对应的灰度值置0,极大值点置1,这样可以剔除一大部分非边缘的像素点,因此最后生成的图像应为一副二值图像,边缘理想状态下都为单像素边缘。
4. 使用双阈值进行边缘连接
经过以上三步得到的边缘质量已经很高了,但是还是存在许多伪边缘,因此canny算法采用的算法是双阈值法,具体思路是:选取两个阈值,将小于低阈值的点认为是假边缘置0,将大于高阈值的点认为是强边缘置1,介于中间的像素点需要进一步的检查。
Pycharm2018.2.4 x64 + opencv2.4.10
1. 彩色图像转换成灰度图
将彩色图像(Color Image)转换为灰度图(Gray Scale Image),即从三通道RGB图像转为单通道图像。
我们这一步需要用到opencv库中的cv.cvtColor函数,该函数功能实现了色彩空间转换。需要用到两个参数:src——输入图片,code——颜色转换代码,具体代码如下:
#灰度图转换
def grayscale(image):
return cv.cvtColor(image, cv.COLOR_RGB2GRAY)
原图:
灰度化之后颜色信息丢失,许多简单的识别算法对颜色的依赖性不强,并且颜色本身非常容易受到光照等因素的影响。识别物体最关键最本质的部分是计算梯度,颜色本身难以提供关键信息,仅仅需要灰度图像中的信息就足够了。并且灰度化后,简化了矩阵,提高了运算速度。
2. 高斯滤波
高斯滤波算法是一种去除高频噪声的常用方式,通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值都是由其本身和邻域内的其他像素值经过加权平均后得到的。高斯滤波的原理是根据待滤波的像素点及其邻域点的灰度值按照高斯公式生成的参数规则进行加权平均。
我们这一步需要用到opencv库中的cv.GaussianBlur函数,其中使用到的参数为:src——输入图像,kernel_size——高斯核的大小,sigma——高斯标准差(一般默认为0),具体代码如下:
# 高斯滤波
def gaussian_blur(image, kernel_size):
return cv.GaussianBlur(image, (kernel_size, kernel_size), 0)
高斯滤波后的效果图:
通过滤波来提取图像特征,简化图像所带的信息作为后续其他图像处理。
为适应图像处理的需求,通过滤波消除图像数字化时混入的噪声。
3. Canny边缘检测
我们这一步需要用到opencv库中的cv.Canny函数,其中使用到的参数为:src——输入图像,low_threshold ——低阈值,high_threshold——高阈值,具体代码如下:
# Canny边缘检测
def canny(image, low_threshold, high_threshold):
return cv.Canny(image, low_threshold, high_threshold)
Canny边缘检测后的效果图:
4. 生成Mask掩膜,提取 ROI
Mask掩模的作用为降低计算代价,即只在我们感兴趣部分进行算法的计算,设计思路如下:
这里设计的感兴趣区域是采用粗略估计的方式,确定四个顶点坐标,图形为梯形。
具体实现代码如下:
# 生成感兴趣区域即Mask掩模
def region_of_interest(image, vertices):
mask = np.zeros_like(image) # 生成图像大小一致的zeros矩
# 填充顶点vertices中间区域
if len(image.shape) > 2:
channel_count = image.shape[2]
ignore_mask_color = (255,) * channel_count
else:
ignore_mask_color = 255
# 填充函数
cv.fillPoly(mask, vertices, ignore_mask_color)
masked_image = cv.bitwise_and(image, mask)
return masked_image
# 生成Mask掩模
vertices = np.array([[(0, imshape[0]), (9 * imshape[1] / 20, 11 * imshape[0] / 18),
(11 * imshape[1] / 20, 11 * imshape[0] / 18), (imshape[1], imshape[0])]], dtype=np.int32)
masked_edges = region_of_interest(edge_image, vertices)
感兴趣区域和canny边缘检测后的图进行按位与后,产生的效果图如下:
5. 基于霍夫变换的直线检测
用到的是Opencv封装好的函数cv.HoughLinesP函数,使用到的参数如下:
具体代码如下:
def hough_lines(img, rho, theta, threshold, min_line_len, max_line_gap):
# rho:线段以像素为单位的距离精度
# theta : 像素以弧度为单位的角度精度(np.pi/180较为合适)
# threshold : 霍夫平面累加的阈值
# minLineLength : 线段最小长度(像素级)
# maxLineGap : 最大允许断裂长度
lines = cv.HoughLinesP(img, rho, theta, threshold, np.array([]), minLineLength=min_line_len, maxLineGap=max_line_gap)
return lines
这一步是将上一步检测到的直线绘制出来,但是会发现霍夫变换后直接绘制的直线可以看到出现了多条线段相互相邻的情况,而我们所期望的是单车道线检测。为了直观体验与后续处理,需要对上一步检测到的直线进行进一步的预处理,处理的方法具体为:
具体代码如下:
line_image = np.zeros_like(image)
def draw_lines(image, lines, color=[255,0,0], thickness=2):
right_y_set = []
right_x_set = []
right_slope_set = []
left_y_set = []
left_x_set = []
left_slope_set = []
slope_min = .35 # 斜率低阈值
slope_max = .85 # 斜率高阈值
middle_x = image.shape[1] / 2 # 图像中线x坐标
max_y = image.shape[0] # 最大y坐标
for line in lines:
for x1, y1, x2, y2 in line:
fit = np.polyfit((x1, x2), (y1, y2), 1) # 拟合成直线
slope = fit[0] # 斜率
if slope_min < np.absolute(slope) <= slope_max:
# 将斜率大于0且线段X坐标在图像中线右边的点存为右边车道线
if slope > 0 and x1 > middle_x and x2 > middle_x:
right_y_set.append(y1)
right_y_set.append(y2)
right_x_set.append(x1)
right_x_set.append(x2)
right_slope_set.append(slope)
# 将斜率小于0且线段X坐标在图像中线左边的点存为左边车道线
elif slope < 0 and x1 < middle_x and x2 < middle_x:
left_y_set.append(y1)
left_y_set.append(y2)
left_x_set.append(x1)
left_x_set.append(x2)
left_slope_set.append(slope)
# 绘制左车道线
if left_y_set:
lindex = left_y_set.index(min(left_y_set)) # 最高点
left_x_top = left_x_set[lindex]
left_y_top = left_y_set[lindex]
lslope = np.median(left_slope_set) # 计算平均值
# 根据斜率计算车道线与图片下方交点作为起点
left_x_bottom = int(left_x_top + (max_y - left_y_top) / lslope)
# 绘制线段
cv.line(image, (left_x_bottom, max_y), (left_x_top, left_y_top), color, thickness)
# 绘制右车道线
if right_y_set:
rindex = right_y_set.index(min(right_y_set)) # 最高点
right_x_top = right_x_set[rindex]
right_y_top = right_y_set[rindex]
rslope = np.median(right_slope_set)
# 根据斜率计算车道线与图片下方交点作为起点
right_x_bottom = int(right_x_top + (max_y - right_y_top) / rslope)
# 绘制线段
cv.line(image, (right_x_top, right_y_top), (right_x_bottom, max_y), color, thickness)
绘制车道线效果图如下:
7. 图像融合
这一步是将原始彩色图像与我们刚绘制的车道线图像进行比例的融合,这里需要介绍一个函数cv.addWeighted,参数src1——原始图像或矩阵;alpha——其原始图像对应的权重;src2——第二幅图像;beta——第二副图像对应的权重;gamma——整体添加到数值,默认为0即可。
这里我们将原图权重设为0.8,车道线图像设为1,则最后呈现的效果为车道线较为明显,可视化程度提高,具体代码如下:
# 原图像与车道线图像按照a:b比例融合
def weighted_img(img, initial_img, a=0.8, b=1., c=0.):
return cv.addWeighted(initial_img, a, img, b, c)
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
# 灰度图转换
def grayscale(image):
return cv.cvtColor(image, cv.COLOR_RGB2GRAY)
# Canny边缘检测
def canny(image, low_threshold, high_threshold):
return cv.Canny(image, low_threshold, high_threshold)
# 高斯滤波
def gaussian_blur(image, kernel_size):
return cv.GaussianBlur(image, (kernel_size, kernel_size), 0)
# 生成感兴趣区域即Mask掩模
def region_of_interest(image, vertices):
mask = np.zeros_like(image) # 生成图像大小一致的zeros矩
# 填充顶点vertices中间区域
if len(image.shape) > 2:
channel_count = image.shape[2]
ignore_mask_color = (255,) * channel_count
else:
ignore_mask_color = 255
# 填充函数
cv.fillPoly(mask, vertices, ignore_mask_color)
masked_image = cv.bitwise_and(image, mask)
return masked_image
# 原图像与车道线图像按照a:b比例融合
def weighted_img(img, initial_img, a=0.8, b=1., c=0.):
return cv.addWeighted(initial_img, a, img, b, c)
# def reset_global_vars():
#
# global SET_LFLAG
# global SET_RFLAG
# global LAST_LSLOPE
# global LAST_RSLOPE
# global LAST_LEFT
# global LAST_RIGHT
#
# SET_RFLAG = 0
# SET_LFLAG = 0
# LAST_LSLOPE = 0
# LAST_RSLOPE = 0
# LAST_RIGHT = [0, 0, 0]
# LAST_LEFT = [0, 0, 0]
def draw_lines(image, lines, color=[255,0,0], thickness=2):
right_y_set = []
right_x_set = []
right_slope_set = []
left_y_set = []
left_x_set = []
left_slope_set = []
slope_min = .35 # 斜率低阈值
slope_max = .85 # 斜率高阈值
middle_x = image.shape[1] / 2 # 图像中线x坐标
max_y = image.shape[0] # 最大y坐标
for line in lines:
for x1, y1, x2, y2 in line:
fit = np.polyfit((x1, x2), (y1, y2), 1) # 拟合成直线
slope = fit[0] # 斜率
if slope_min < np.absolute(slope) <= slope_max:
# 将斜率大于0且线段X坐标在图像中线右边的点存为右边车道线
if slope > 0 and x1 > middle_x and x2 > middle_x:
right_y_set.append(y1)
right_y_set.append(y2)
right_x_set.append(x1)
right_x_set.append(x2)
right_slope_set.append(slope)
# 将斜率小于0且线段X坐标在图像中线左边的点存为左边车道线
elif slope < 0 and x1 < middle_x and x2 < middle_x:
left_y_set.append(y1)
left_y_set.append(y2)
left_x_set.append(x1)
left_x_set.append(x2)
left_slope_set.append(slope)
# 绘制左车道线
if left_y_set:
lindex = left_y_set.index(min(left_y_set)) # 最高点
left_x_top = left_x_set[lindex]
left_y_top = left_y_set[lindex]
lslope = np.median(left_slope_set) # 计算平均值
# 根据斜率计算车道线与图片下方交点作为起点
left_x_bottom = int(left_x_top + (max_y - left_y_top) / lslope)
# 绘制线段
cv.line(image, (left_x_bottom, max_y), (left_x_top, left_y_top), color, thickness)
# 绘制右车道线
if right_y_set:
rindex = right_y_set.index(min(right_y_set)) # 最高点
right_x_top = right_x_set[rindex]
right_y_top = right_y_set[rindex]
rslope = np.median(right_slope_set)
# 根据斜率计算车道线与图片下方交点作为起点
right_x_bottom = int(right_x_top + (max_y - right_y_top) / rslope)
# 绘制线段
cv.line(image, (right_x_top, right_y_top), (right_x_bottom, max_y), color, thickness)
def hough_lines(img, rho, theta, threshold, min_line_len, max_line_gap):
# rho:线段以像素为单位的距离精度
# theta : 像素以弧度为单位的角度精度(np.pi/180较为合适)
# threshold : 霍夫平面累加的阈值
# minLineLength : 线段最小长度(像素级)
# maxLineGap : 最大允许断裂长度
lines = cv.HoughLinesP(img, rho, theta, threshold, np.array([]), minLineLength=min_line_len, maxLineGap=max_line_gap)
return lines
def process_image(image):
rho = 1 # 霍夫像素单位
theta = np.pi / 180 # 霍夫角度移动步长
hof_threshold = 20 # 霍夫平面累加阈值threshold
min_line_len = 30 # 线段最小长度
max_line_gap = 60 # 最大允许断裂长度
kernel_size = 5 # 高斯滤波器大小size
canny_low_threshold = 75 # canny边缘检测低阈值
canny_high_threshold = canny_low_threshold * 3 # canny边缘检测高阈值
alpha = 0.8 # 原图像权重
beta = 1. # 车道线图像权重
lambda_ = 0.
imshape = image.shape # 获取图像大小
# 灰度图转换
gray = grayscale(image)
# 高斯滤波
blur_gray = gaussian_blur(gray, kernel_size)
# Canny边缘检测
edge_image = canny(blur_gray, canny_low_threshold, canny_high_threshold)
# 生成Mask掩模
vertices = np.array([[(0, imshape[0]), (9 * imshape[1] / 20, 11 * imshape[0] / 18),
(11 * imshape[1] / 20, 11 * imshape[0] / 18), (imshape[1], imshape[0])]], dtype=np.int32)
masked_edges = region_of_interest(edge_image, vertices)
# 基于霍夫变换的直线检测
lines = hough_lines(masked_edges, rho, theta, hof_threshold, min_line_len, max_line_gap)
line_image = np.zeros_like(image)
# 绘制车道线线段
draw_lines(line_image, lines, thickness=10)
# 图像融合
lines_edges = weighted_img(image, line_image, alpha, beta, lambda_)
return lines_edges
if __name__ == '__main__':
# cap = cv.VideoCapture("./test_videos/solidYellowLeft.mp4")
# while(cap.isOpened()):
# _, frame = cap.read()
# processed = process_image(frame)
# cv.imshow("image", processed)
# cv.waitKey(1)
image = cv.imread('./test_images/solidYellowCurve2.jpg')
line_image = process_image(image)
cv.imshow('image',line_image)
cv.waitKey(0)