老齐教室

自动驾驶中的车道识别

车道识别,是自动驾驶中必不可少的,且实现方法也不止一种。

车道的基本概念

“车道”,其相关解释在维基百科或者百度百科上都有,不过,正如我们日常所言,都是用来专指“机动车道”。所以,“车道识别”、“自动驾驶”等术语,也是针对机动车而言。

自行车的“自动驾驶”也值得探索。

其实已经有了,比如:https://www.designboom.com/technology/self-driving-bicycle-huawei-engineers-operate-unmanned-06-14-2021/

问题描述

如同前面视频演示的实时车道识别,可以用多种方法实现。比如可以用基于学习的方法,也就是训练一个深度学习模型。

本文不用这种方法,本文要介绍一种更简单的方法:用 OpenCV 实现车道识别。

如上图所示,车道识别的重要任务就是要识别出车道两边的分道线,这是问题的关键。

那么,怎么识别这些车道的分道线呢?

从机动车的角度开出去,所看到的场景范围中,除了分道线之外,还有许多其他物体,比如车辆、路边障碍物、路灯等。通过前面的视频以及生活尝试,都容易知道,场景中的每一帧都在变化。这就是现实生活中的驾驶情况——有点复杂。

所以,要解决车道识别问题,首先要找到一种方法,能忽略场景中不应该看到的物体——只看到分道线。即如下图所示,除了分道线,别的物体都没有了。随着机动车的行驶,分道线只呈现在这个场景中。

在接下来的内容中,会演示如何从视频中选择指定的区域,顺带介绍必要的图像预处理技巧。

图像掩膜

图像掩模(image mask):用选定的图像、图形或物体,对待处理的图像(局部或全部)进行遮挡来控制图像处理的区域或处理过程。在图像处理中,对图像掩膜会有多种要求。

图像掩膜的本质就是 Numpy 的数组,如下图所示,改变图中选定区域的像素值,比如都改为 0 ,就实现了图中所示的遮罩效果。

这是一种非常简单而有效的移除不想看到的物体的方法。

基本思路

首先是要将图片转化为“黑白”,即设置一个转化的阈值,这样就能得到如下图中右侧的效果。

接下来要解决的问题就是如何让机器“看到”分道线。通常使用霍夫变换(Hough Transformation),这是一种特征提取的技术,其数学原理请参阅:http://math.itdiffer.com 中的有关内容。

用 OpenCV 实现车道识别

下面就开始编写实现车道识别的代码。建议使用谷歌的 Colab 或者百度的 AiStudio 执行代码,因为在本地跑的话,对计算能力要求有点高。本文代码就发布到了 AiStudio 上,并且相关视频也可以在该项目中下载,地址是:https://aistudio.baidu.com/aistudio/projectdetail/3215224?contributionType=1。

先引入下列各模块和库,如果本地没有安装,请自行安装。

1
2
3
4
5
6
import os
import re
import cv2
import numpy as np
from tqdm import tqdm_notebook
import matplotlib.pyplot as plt

然后读入视频。此视频文件已经放到本项目中,可以自行下载。

准备

加载视频文件,并从视频中抽取若干帧作为后续应用的图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 将视频按照指定的帧率,将帧转化为图片
image_path = 'frames'

def get_frame(video, image_path, frame_rate=0.5):
vidcap = cv2.VideoCapture(video)
sec = 0
count = 1
image_lst = []
while 1:
vidcap.set(cv2.CAP_PROP_POS_MSEC, sec*1000)
has_frames, image = vidcap.read()
if has_frames:
image_name = f"imge{count}.jpg"
cv2.imwrite(f"{image_path}/{image_name}", image)
image_lst.append(image_name)
else:
break
count += 1
sec += frame_rate
sec = round(sec, 2)
return image_lst

images = get_frame("road.mp4", 'frames', frame_rate=5) # 此处每隔5秒取一帧,在真实的业务中,时间较长。
1
len(images)    # 共计得到了167张图片

特别注意,上面设置的 frame_rate=5 显然比较粗糙,这是为了演示而设置的。

下面显示其中一张图片。

1
2
3
4
5
6
7
8
9
10
image_path = 'frames/'

# 显示其中一张图片
idx = 2 # 指定一个索引
image_example = image_path + os.listdir("frames")[idx]
img = cv2.imread(image_example)
# 显示图片
plt.figure(figsize=(10,10))
plt.imshow(img, cmap= "gray")
plt.show()

创建掩膜

显然,我们感兴趣的区域是一个多边形范围,其他区域都是应该被遮罩起来的。 因此,首先要指定多边形的坐标,然后通过它创建掩膜。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建 0 数组
stencil = np.zeros_like(img[:,:,0])

# 确定多边形的坐标
polygon = np.array([[80,370], [300,250], [450,250], [580,370]])

# 用 1 填充多边形
cv2.fillConvexPoly(stencil, polygon, 1)

# 显示多边形效果
plt.figure(figsize=(10,10))
plt.imshow(stencil, cmap= "gray")
plt.show()

将多边形作为掩膜,用到其中一帧图片上。

1
2
3
4
5
6
7
8
mask_img = cv2.bitwise_and(img[:,:,0], 
img[:,:,0],
mask=stencil)

# 显示效果
plt.figure(figsize=(10,10))
plt.imshow(mask_img, cmap= "gray")
plt.show()

图片预处理

为了识别视频中每帧图片中的车道,必须对所有图片进行预处理,主要是前面提过的两个方面:阈值和霍夫变换

  1. 阈值
1
2
3
4
5
6
ret, thresh = cv2.threshold(img, 130, 145, cv2.THRESH_BINARY)

# plot image
plt.figure(figsize=(10,10))
plt.imshow(thresh, cmap= "gray")
plt.show()

  1. 霍夫变换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
lines = cv2.HoughLinesP(thresh, 1, np.pi/180, 30, maxLineGap=200)

# 拷贝帧图片
dmy = img[:,:,0].copy()

# 绘制霍夫线
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(dmy, (x1, y1), (x2, y2), (255, 0, 0), 3)

# 画图显示
plt.figure(figsize=(10,10))
plt.imshow(dmy, cmap= "gray")
plt.show()

现在,将上面的操作用在每一帧上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cnt = 0

for image in images:
print(image)
img = cv2.imread(f'frames/{image}')
# 对帧掩膜
masked = cv2.bitwise_and(img[:,:,0], img[:,:,0], mask=stencil)

# 阈值
ret, thresh = cv2.threshold(masked, 130, 145, cv2.THRESH_BINARY)

# 霍夫变换
lines = cv2.HoughLinesP(thresh, 1, np.pi/180, 30, maxLineGap=200)
dmy = img[:,:,0].copy()

# 识别到的线
try:
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(dmy, (x1, y1), (x2, y2), (255, 0, 0), 3)

cv2.imwrite('detected/'+str(cnt)+'.png',dmy)

except TypeError:
cv2.imwrite('detected/'+str(cnt)+'.png',img)

cnt+= 1

用于视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 输入路径
pathIn= 'detected/'

# 输出视频
pathOut = 'roads_v2.mp4'

# 设定每秒的帧数
fps = 30.0

from os.path import isfile, join

# 读取文件
files = [f for f in os.listdir(pathIn) if isfile(join(pathIn, f))]
files.sort(key=lambda f: int(re.sub('\D', '', f)))

将所有检测到车道的帧保存到列表中。

1
2
3
4
5
6
7
8
9
10
11
frame_list = []

for i in range(len(files)):
filename=pathIn + files[i]
#reading each files
img = cv2.imread(filename)
height, width, layers = img.shape
size = (width,height)

#inserting the frames into an image array
frame_list.append(img)

最后,将所有的帧合并为视频。

1
2
3
4
5
6
7
8
9
# write the video
out = cv2.VideoWriter(pathOut,cv2.VideoWriter_fourcc(*'DIVX'),
fps, size)

for i in range(len(frame_list)):
# writing to a image array
out.write(frame_list[i])

out.release()

齐活儿。

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能