更新:代码仓库
JJLi0427/CNN_Autoencoder (github.com)https://github.com/JJLi0427/CNN_Autoencoder
车道线识别是智能驾驶中很重要的一环,这里介绍以下使用传统的机器视觉和数学变换的方式实现图片或者视频中的车道线检测,不涉及深度学习等更加进阶的内容。
1.ROI区域
又叫做感兴趣区域,其实就是我们的机器视觉是没必要对整个图片都去计算和处理的,比如车道线只会汇聚在视频的下半部分,我们就标定下半部分为ROI区域。大部分情况下,ROI区是一个梯形区域,程序中也附带了ROI区域的选取功能,从左下到右下顺时针标定即可
def click_event(event, x, y, flags, param):# 鼠标点击函数
global selecting_roi, selected_pts
if event == cv2.EVENT_LBUTTONDOWN:
if len(selected_pts) < 4:
selected_pts.append((x, y))
cv2.circle(color_frame, (x, y), 2, (0, 0, 255), -1)
if len(selected_pts) == 4:
selecting_roi = False
def select_roi(frame): # 选取ROI区域
'''顺时针选取类梯形区域'''
global color_frame, selecting_roi, selected_pts
color_frame = frame.copy()
selected_pts = []
selecting_roi = True
cv2.namedWindow("Select ROI")
cv2.setMouseCallback("Select ROI", click_event)
while selecting_roi:
cv2.imshow("Select ROI", color_frame)
cv2.waitKey(1)
if cv2.waitKey(1000) == ord(' '): # 按空格键退出
sys.exit() # 退出程序
def roi_mask(gray_img, pts): # 掩膜, 选定ROI区域
mask = np.zeros_like(gray_img)
mask = cv2.fillPoly(mask, pts=[np.array(pts)], color=255)
img_mask = cv2.bitwise_and(gray_img, mask)
return img_mask
2.canny边缘检测
使用canny边缘检测可以很好的检测出图片的边缘。第一步我们是要把图片二值化,即把一个图片转换为只有纯黑和纯白两种颜色的图片。opencv已经封装了canny的方法我们可以直接调用,但是需要了解以下canny的两个阈值:最大值和最小值。这两个阈值关系到了边缘检测的明显程度,阈值越低越容易检测到边缘,但是可能并不是真实的边缘,是噪点干扰的结果。但是阈值太高了又不容易检测到边缘,所以我们需要设置一个合适的阈值。下面是一个视频逐帧提取和阈值分析的小程序,可以从上面学习一下合理的阈值设置
import cv2
# 拖动进度条对视频逐帧观测, canny上下阈值设置和边缘检测效果的关系
cv2.namedWindow('edge_detection')
cv2.createTrackbar('minThreshold', 'edge_detection', 50, 1000, lambda x: x)
cv2.createTrackbar('maxThreshold', 'edge_detection', 100, 1000, lambda x: x)
video_path = 'Alan_.avi'# 选取视频
cap = cv2.VideoCapture(video_path)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 获取视频的总帧数
cv2.createTrackbar('framePosition', 'edge_detection', 0, total_frames, lambda x: x) # 添加滑动条 'framePosition'
while True:# 逐帧显示视频
frame_pos = cv2.getTrackbarPos('framePosition', 'edge_detection')
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_pos)
ret, frame = cap.read()
if not ret:
break
img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
minThreshold = cv2.getTrackbarPos('minThreshold', 'edge_detection')# 获取滑动条的值
maxThreshold = cv2.getTrackbarPos('maxThreshold', 'edge_detection')
edges = cv2.Canny(img, minThreshold, maxThreshold)
cv2.imshow('edge_detection', edges)# 显示canny边缘检测效果
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
可以明显看到第二个图更准确识别了两侧的车道线
3.Hough变换
又称作霍夫变换,是在视频中提取直线的很好的方式。他的中心思想是把一条直线,用一个坐标点来表示。无论极坐标还是直角坐标系,都只需要两个参数就能表示一条直线。
所以我们可以理解为斜率和偏移值相近的直线其实是可以用相近的坐标点来表示的,利用这个思路就可以实现在边缘检测图像中对于车道线这种直线的提取
这就是opencv中自带的霍夫变换函数,设置基本的参数就能实现直线的提取
4.RANSAC拟合直线
在拟合直线之前,可以先用分类的方法,把斜率为正的直线当作左车道线,斜率为负数的直线当作右车道线,这样就能更好的在下一步拟合直线
def calculate_slope(line):# 计算斜率
x_1, y_1, x_2, y_2 = line[0]
return (y_2 - y_1) / (x_2 - x_1)
left_lines = [line for line in lines if calculate_slope(line) > 0]
right_lines = [line for line in lines if calculate_slope(line) < 0]
RANSAC是一种把许多条直线拟合成一个最大可能直线的算法,通过不断的拟合,我们就可以把车道线获取出来。RANSAC和传统的最小二乘法拟合还是有一定的区别的,RANSSAC显然比最小二乘法更加激进,但是在大部分情况下效果都是好于最小二乘法的,下图是RANSC在平面中拟合直线的思路图:他会在各个方位拟合直线,最多直线聚合的区域即认为是最可能直线
5.特殊情况处理
程序在有一些情况下会出错,比如视频中的当前帧没有检测到车道线,我们可以继续把前面检测到的有效车道线继续使用
lines = cv2.HoughLinesP(edge_img, 1, np.pi / 180, 15, minLineLength=35, maxLineGap=20)# 霍夫变换检测直线
'''Hough, 参数可调'''
if lines is None:# 如果没有检测到直线, 则使用上一帧的直线
left_line = left_line_prev
right_line = right_line_prev
else:# 如果检测到直线, 则使用RANSAC拟合直线
left_lines = [line for line in lines if calculate_slope(line) > 0]
right_lines = [line for line in lines if calculate_slope(line) < 0]
left_lines = reject_abnormal_lines(left_lines)
right_lines = reject_abnormal_lines(right_lines)
if len(left_lines) == 0:
left_line = left_line_prev
else:
left_line = ransac_fit(left_lines)
if len(right_lines) == 0:
right_line = right_line_prev
else:
right_line = ransac_fit(right_lines)
再比如在有一侧车道线为虚线的时候,两侧的车道线显示就会一长一短,我们可以用算法使得他们在纵坐标上是等高的
def draw_lines(img, lines):
left_line, right_line = lines
cv2.line(img, tuple(left_line[0]), tuple(left_line[1]), color=(0, 0, 255), thickness=3)# 用红色线表示检测到的车道线
cv2.line(img, tuple(right_line[0]), tuple(right_line[1]), color=(0, 0, 255), thickness=3)
y_coords = [left_line[0][1], left_line[1][1], right_line[0][1], right_line[1][1]]
y_min, y_max = min(y_coords), max(y_coords)
slope_left = (left_line[1][1] - left_line[0][1]) / (left_line[1][0] - left_line[0][0])
intercept_left = left_line[0][1] - slope_left * left_line[0][0]
slope_right = (right_line[1][1] - right_line[0][1]) / (right_line[1][0] - right_line[0][0])
intercept_right = right_line[0][1] - slope_right * right_line[0][0]# 根据最大和最小纵坐标计算新的端点横坐标
if slope_left != 0:
x_min_left = (y_min - intercept_left) / slope_left
x_max_left = (y_max - intercept_left) / slope_left
else:
x_min_left = x_max_left = 0
if slope_right != 0:
x_min_right = (y_min - intercept_right) / slope_right
x_max_right = (y_max - intercept_right) / slope_right
else:
x_min_right = x_max_right = 0
cv2.line(img, (int(x_min_left), y_min), (int(x_max_left), y_max), color=(0, 255, 0), thickness=1)# 重新计算绘制延长的且高度平行的绿色线
cv2.line(img, (int(x_min_right), y_min), (int(x_max_right), y_max), color=(0, 255, 0), thickness=1)