[图像算法]-无人驾驶-车辆+车道检测

任务

1.车辆检测

2.车道检测


1.车辆检测

2.车道检测

一共要完成两项任务:

1. 在所提供的公路图片上检测出车道线并标记

image

2. 在所提供的公路视频上检测出车道线并标记

方案:

要检测出当前车道,就是要检测出左右两条车道直线。由于无人车一直保持在当前车道,那么无人车上的相机拍摄视频中,车道线的位置应该基本固定在某一个范围内:

image

如果我们手动把这部分ROI区域抠出来,就会排除大部分干扰。接下来检测直线肯定用霍夫变换,但ROI区域内的边缘直线信息还是很多,考虑到只有左右两条车道,一条斜率为正、一条斜率为负,可将所有的线分为两组,每组再通过均值或最小二乘法拟合的方式确定唯一一条线就可以完成检测。具体步骤如下:
1. 灰度化
2. 高斯模糊
3. Canny边缘检测
4. 不规则ROI区域截取
5. 霍夫直线检测
6. 车道计算
对于视频来说,只要能检测出一幅图,后面将图像合成一下即可。

图像预处理

灰度化和滤波操作是大部分图像处理的必要步骤。灰度化是因为不需要色彩信息,可以减少计算量。而滤波会削弱图像噪点,排除干扰信息。另外,边缘提取是基于图像梯度的,梯度对噪声很敏感,所以平滑操作必不可少。

image

这里我们用分模块来写,方便调用:

import cv2
import numpy as np
# 高斯滤波核大小
blur_ksize = 5
# Canny边缘检测高低阈值
canny_lth = 50
canny_hth = 150
def process_an_image(img):
    # 1. 灰度化、滤波和Canny
    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    blur_gray = cv2.GaussianBlur(gray, (blur_ksize, blur_ksize), 1)
    edges = cv2.Canny(blur_gray, canny_lth, canny_hth)
if __name__ == "__main__":
    img = cv2.imread('test_pictures/lane.jpg')
    result = process_an_image(img)
    cv2.imshow("lane", np.hstack((img, result)))
    cv2.waitKey(0)
边缘检测结果图

ROI截取

按前面方案中提到的,只需保留边缘图中红线部分区域用于后续的霍夫直线检测,其余的都是无用的信息:

image

我们可以穿件一个梯形的掩膜,然后与边缘检测结果图混合运算,掩膜中白色部分保留,黑色部分舍弃。梯形的四个坐标需要手动标记:

image
def process_an_image(img):
    # 1. 灰度化、滤波和Canny
    # 2. 标记四个坐标点用于ROI截取
    rows, cols = edges.shape
    points = np.array([[(0, rows), (460, 325), (520, 325), (cols, rows)]])
    # [[[0 540], [460 325], [520 325], [960 540]]]
    roi_edges = roi_mask(edges, points)
    
def roi_mask(img, corner_points):
    # 创建掩膜
    mask = np.zeros_like(img)
    cv2.fillPoly(mask, corner_points, 255)
    masked_img = cv2.bitwise_and(img, mask)
    return masked_img

结果图 roi_edges如下:

只保留关键区域的边缘检测图

霍夫直线提取

为了方便后续计算直线的斜率,我们使用统计概率霍夫直线变换(因为它能得到直线的起点和终点坐标):

# 霍夫变换参数
rho = 1
theta = np.pi / 180
threshold = 15
min_line_len = 40
max_line_gap = 20
def process_an_image(img):
    # 1. 灰度化、滤波和Canny
    # 2. 标记四个坐标点用于ROI截取
    # 3. 霍夫直线提取
    drawing, lines = hough_lines(roi_edges, rho, theta, threshold, min_line_len, max_line_gap)
def hough_lines(img, rho, theta, threshold, min_line_len, max_line_gap):
    # 统计概率霍夫直线变换
    lines = cv2.HoughLinesP(img, rho, theta, threshold, minLineLength=min_line_len, maxLineGap=max_line_gap)
    # 新建一副空白画布
    drawing = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
    # draw_lines(drawing, lines)     # 画出直线检测结果
    return drawing, lines
def draw_lines(img, lines, color=[0, 0, 255], thickness=1):
    for line in lines:
        for x1, y1, x2, y2 in line:
            cv2.line(img, (x1, y1), (x2, y2), color, thickness)

draw_lines()用来画直线检测的结果,后面我们会接着处理直线,所以这里注释掉了。可以取消注释看看效果:

霍夫变换结果图

对本例的这张测试图来说,如果打印出直线的条数print(len(lines)),应该是16条

车道计算

前面通过霍夫变换得到了多条直线的地点和终点,我们的目的是通过某种算法只得到左右两条车道线

第一步、 根据斜率正负划分某条线是左车道还是右车道。

image

其中左车道(斜率 < 0),右车道(斜率 > 0)。原因如下图所示:


image

左车道与图像坐标系成钝角,斜率为负,右车道与图像坐标系成锐角,斜率为正。

第二步、迭代计算各直线斜率与斜率均值的差,排除掉差值过大的异常数据

第一次计算完斜率均值并排除掉异常值后,再在剩余的斜率中取均值,继续排除。一直迭代下去。

第三步、最小二乘法拟合左右车道线

经过第二步的筛选,就只剩下可能的左右车道线了。我们需要从多条直线中拟合出一条。使用最小二乘法,它通过最小化误差的平方和来寻找数据的最佳匹配函数。

假设目前可能的左车道线有6条,也就是12个坐标点。

我们的目标是拟合出这样一条直线:

image

使得误差平方和最小:

image

Python中可以直接使用 np.polyfit() 进行最小二乘法拟合

def process_an_image(img):
    # 1. 灰度化、滤波和Canny
    # 2. 标记四个坐标点用于ROI截取
    # 3. 霍夫直线提取
    # 4. 车道拟合计算
    draw_lanes(drawing, lines)
    # 5. 最终将结果合在原图上
    result = cv2.addWeighted(img, 0.9, drawing, 0.2, 0)
    return result
def draw_lanes(img, lines, color=[255, 0, 0], thickness=8):
    # a. 划分左右车道
    left_lines, right_lines = [], []
    for line in lines:
        for x1, y1, x2, y2 in line:
            k = (y2 - y1) / (x2 - x1)
            if k < 0:
                left_lines.append(line)
            else:
                right_lines.append(line)
    if (len(left_lines) <= 0 or len(right_lines) <= 0):
        return
    # b. 清理异常数据
    clean_lines(left_lines, 0.1)
    clean_lines(right_lines, 0.1)
    # c. 得到左右车道线点的集合,拟合直线
    left_points = [(x1, y1) for line in left_lines for x1, y1, x2, y2 in line]
    left_points = left_points + [(x2, y2) for line in left_lines for x1, y1, x2, y2 in line]
    right_points = [(x1, y1) for line in right_lines for x1, y1, x2, y2 in line]
    right_points = right_points + [(x2, y2) for line in right_lines for x1, y1, x2, y2 in line]
    left_results = least_squares_fit(left_points, 325, img.shape[0])
    right_results = least_squares_fit(right_points, 325, img.shape[0])
    # 注意这里点的顺序
    vtxs = np.array([[left_results[1], left_results[0], right_results[0], right_results[1]]])
    # d. 填充车道区域
    cv2.fillPoly(img, vtxs, (0, 255, 0))
    # 或者只画车道线
    # cv2.line(img, left_results[0], left_results[1], (0, 0, 255), thickness)
    # cv2.line(img, right_results[0], right_results[1], (0, 0, 255), thickness)
    
def clean_lines(lines, threshold):
    # 迭代计算斜率均值,排除掉与差值差异较大的数据
    slope = [(y2 - y1) / (x2 - x1) for line in lines for x1, y1, x2, y2 in line]
    while len(lines) > 0:
        mean = np.mean(slope)
        diff = [abs(s - mean) for s in slope]
        idx = np.argmax(diff)
        if diff[idx] > threshold:
            slope.pop(idx)
            lines.pop(idx)
        else:
            break
            
def least_squares_fit(point_list, ymin, ymax):
    # 最小二乘法拟合
    x = [p[0] for p in point_list]
    y = [p[1] for p in point_list]
    # polyfit第三个参数为拟合多项式的阶数,所以1代表线性
    fit = np.polyfit(y, x, 1)
    fit_fn = np.poly1d(fit)  # 获取拟合的结果
    xmin = int(fit_fn(ymin))
    xmax = int(fit_fn(ymax))
    return [(xmin, ymin), (xmax, ymax)]

最后得到的是左右两条车道线的起点和终点坐标。可以选择画出车道线,也可以填充整个区域:


image
image

画出车道线的效果不是很好,还是选用填充比较直观。

视频处理

搞定了一张图,视频的话也就没什么难度了。关键是视频帧的提取和合成,为此我们需要用到Python的视频编辑包 moviepy

pip install moviepy

另外还需要 ffmpeg,首次运行moviepy时会自动下载。也可手动下载。建议手动下载,不知为什么,博主自动下载老是失败。ffmpeg-win32-v3.2.4.exe

只需要在开头导入 moviepy,然后主函数改掉就可以了,其余代码不需要修改:

# 开头导入moviepy
from moviepy.editor import VideoFileClip
# 主函数更改为:
if __name__ == "__main__":
    output = 'test_videos/output.mp4'
    clip = VideoFileClip("test_videos/cv2_white_lane.mp4")
    out_clip = clip.fl_image(process_an_image)
    out_clip.write_videofile(output, audio=False)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容