基于多项式拟合和透视变换的车道线检测及曲率计算
车道线的检测方法有很多,其中具有代表性的有
-
霍夫变换检测车道线(直线检测效过较好但较容易出现错帧),详见:基于霍夫变换的车道线检测
-
多项式拟合及透视变换检测车道线(可以较好的检测直道弯道,但实时性较差)
- 深度学习模型检测车道线(泛化能力较强,检测效果好,但检测较慢)基于深度学习的车道线检测方法
本文对在工程中更广泛采用的多项式拟合的方法对车道线进行拟合提取,在通过提取到的车道线对弯道进行曲率的检测和车辆与车道线中心的偏移,可以作为以后高级辅助驾驶或者智能驾驶控制感知的前提条件。因此本文采用了KITTI中采集的数据进行处理,可以将其和激光雷达等进行融合处理,得到更加可靠的结果。KITTI中的数据,及图片格式等问题详见:
https://blog.csdn.net/rhyijg/article/details/107775572
大部分文章中,第一部分都是为对摄像头的标定,标定出其内参外参,然后对需要处理的图片进行畸变矫正,本文中使用的KITTI数据集采用已经标定的数据,因此省略此步,有需要的读者可以查询相关资料。
一、对图片的读取操作
采用opencv对图片进行读取处理,从opencv函数imread()中读取的图片通道格式为BGR,需要对其进行转换成RBG格式,方便后续的处理。同时使用imshow()函数,可以使用鼠标得知当前的像素点,对后续的处理即数据的标定有一定的方便。
from moviepy.editor import VideoFileClip
import matplotlib.pyplot as plt
import matplotlib.image as mplimg
import numpy as np
import cv2
#######################################################################
# Main #
#######################################################################
#1读取图片 1280*720
img = cv2.imread('/home/liqi/dev/catkin_ws/src/KITTI_tutorials/2011_09_26_drive_0028_sync/image_02/data/0000000090.png')
# 格式转变,BGRtoRGB
img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
cv2.namedWindow('img')
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
二、设置检测的ROI区域
设置检测图片的ROI区域,可以去除噪声和以外的干扰,同时可以加快后续的图片处理速度。选择ROI区域 只保留此区域 其他区域用255填充即全黑。其中的roi_corners = [(550, 215), (698, 215), (825, 374), (385, 374)]为下图梯形中的四个顶点,分别为左上,右上,右下,左下的连接顺序。
# 选择ROI区域 只保留此区域 其他区域用255填充。
def roi_mask(img, vertices):
#定义mask全为黑
mask = np.zeros_like(img)
#defining a 3 channel or 1 channel color to fill the mask with depending on the input image
if len(img.shape) > 2:
channel_count = img.shape[2] # i.e. 3 or 4 depending on your image
mask_color = (255,) * channel_count
else:
mask_color = 255
#将区域和图片进行填充fillPoly和叠加and
cv2.fillPoly(mask, vertices, mask_color)
masked_img = cv2.bitwise_and(img, mask)
return masked_img
roi_corners = [(550, 215), (698, 215), (825, 374), (385, 374)]
#2、选择ROI区域
roi_vtx = np.array([roi_corners])
roi_mask = roi_mask(img, roi_vtx)
三、透视变换——鸟瞰图
我们可以通过透视变换来获得一个相对更加直观的视角(比如说在天空俯视的视角),然后在新的视角来圈出ROI。 透视变换(Perspective Transformation) 是将图片投影到一个新的 视平面(Viewing Plane) ,也称作 投影映射(Projective Mapping) 。在OpenCV中,通过使用函数 cv2.getPerspectiveTransform() 和 cv2.warpPerspective() 即可完成对一张图片的透视变换。
cv2.getPerspectiveTransform() 需要两个参数 src 和 dst,他们分别为原图像中能够表示一个矩形的四个点的坐标以及扭曲以后图像的边缘四角在当前图像中的坐标,这两个矩形的坐标不同的相机的数值也不同, , 那么[(550, 215), (698, 215), (825, 374), (385, 374)]在图像中构成一个梯形,这个梯形在俯视图(或者说鸟瞰图)中是一个长方形,然后我们以这个梯形的高作为目标图像的高,前后各减去一个偏移(在实例中这个偏移是150个像素),就是我们的目标图像这个目标图像,也即是我们的ROI。
通过透视变换,将上面设置好的ROI区域图片转换成鸟瞰图,将roi_corners作为未变换原图的源坐标及src_corners,然后通过公式即可计算出经过变换后的dst_corners各点坐标;其中wrap_offset = 150为变换的偏移系数,可以通过不断的试验得到最优结果。
3.# 将img转换成鸟瞰图 透视变换(Perspective Transformation)
def perspective_transform(img, M):
img_size = (img.shape[1], img.shape[0])
warped = cv2.warpPerspective(img, M, img_size, flags=cv2.INTER_LINEAR)
return warped
wrap_offset = 150
# 原图像中能够表示一个矩形的四个点的坐标 为实际图像中的车道线选框点
src_corners = roi_corners
dst_corners = [(src_corners[3][0] + wrap_offset, 0), (src_corners[2][0] - wrap_offset, 0), (src_corners[2][0] - wrap_offset, src_corners[2][1]), (src_corners[3][0] + wrap_offset, src_corners[2][1])]
M = cv2.getPerspectiveTransform(np.float32(src_corners), np.float32(dst_corners))
# 透视变换(Perspective Transformation)
wrap_img= perspective_transform(roi_mask, M)
四、基于sobel 的边缘检测
边缘检测可以使用以下几种算子,对于像车道线识别应用中,需要对高准确度和实时性重点关注,因此可以使用sobel算子进行边缘检测,未来可以使用canny算子进行边缘检测,可以看做是sobel算子的升级版,在文章中基于canny算子的边缘检测有介绍。
Sobel是基于梯度图像模值大小的检测算子,通常有水平和垂直两种算子。具体过程如下:
- 用一个高斯滤波器平滑图像去除噪声的干扰
- 计算梯度幅值图像和角度图像
- 对梯度图像应用非最大抑制(保证单一边缘响应)
- 对规定方向以外的梯度方向进行非最大抑制
- 用双阈值处理和连接分析来检测并连接边缘(保证了低错误率)
- 剔除小于阈值的边缘,保留大于阈值的边缘,处于高低阈值之间的边缘点,若其与大于阈值的像素点连接则保留,反之剔除。
注意在OPENCV中利用调用Sobel函数检测边缘时,应该分别求其水平和垂直两个方向的边缘,再进行加权叠加。
Sobel函数可选对图像求几阶导数,由实验结果可以看出,二阶导数求出的边缘比一阶更细,这也与之前给出的结论相吻合。
不难看出, x 方向的索贝尔算子倾向于检测垂直方向的边缘,而 y 方向的索贝尔算子则倾向于检测水平的边缘,而在车道线检测问题中,我们关注的对象(车道线)往往是垂直方向的线,同时我们希望过滤到一些水平方向的线,所以在本实例中,我们采用 x 方向的索贝尔算子。
通过使用索贝尔算子做两个方向的卷积,我们可以得到图像的每一个像素的横向及纵向 梯度近似值 ,更近一步,我们可以结合这两个近似值计算梯度的大小:
然后可用以下公式计算梯度方向:
# 基于sobel的边缘检测
def abs_sobel_thresh(img, sobel_kernel=3, orient='x', thresh=(0, 255)):
# 只计算某一方向上的sobel算子检测
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
if orient == 'x':
sobel = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
else:
sobel = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
abs_sobel = np.absolute(sobel)
scaled_sobel = np.uint8(255 * abs_sobel / np.max(abs_sobel))
sxbinary = np.zeros_like(scaled_sobel)
sxbinary[(scaled_sobel >= thresh[0]