车道识别,是自动驾驶中必不可少的,且实现方法也不止一种。
车道的基本概念 “车道”,其相关解释在维基百科或者百度百科上都有,不过,正如我们日常所言,都是用来专指“机动车道”。所以,“车道识别”、“自动驾驶”等术语,也是针对机动车而言。
自行车的“自动驾驶”也值得探索。
其实已经有了,比如: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 osimport reimport cv2import numpy as npfrom tqdm import tqdm_notebookimport 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 )
特别注意,上面设置的 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 stencil = np.zeros_like(img[:,:,0 ]) polygon = np.array([[80 ,370 ], [300 ,250 ], [450 ,250 ], [580 ,370 ]]) 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 2 3 4 5 6 ret, thresh = cv2.threshold(img, 130 , 145 , cv2.THRESH_BINARY) plt.figure(figsize=(10 ,10 )) plt.imshow(thresh, cmap= "gray" ) plt.show()
霍夫变换
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, joinfiles = [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] img = cv2.imread(filename) height, width, layers = img.shape size = (width,height) frame_list.append(img)
最后,将所有的帧合并为视频。
1 2 3 4 5 6 7 8 9 out = cv2.VideoWriter(pathOut,cv2.VideoWriter_fourcc(*'DIVX' ), fps, size) for i in range(len(frame_list)): out.write(frame_list[i]) out.release()
齐活儿。