内容: 旨在对车载摄像头采集的车辆行驶视频中的车道线进行跟踪识别。
实现过程:首先对车辆行驶视频中提取出的帧图像进行“去畸变”、“透视变换”等操作,得到“鸟瞰图”视角的“规则的”车道线图像;然后通过“组合梯度和色彩过滤”提取出“可能的车道线像素”,使用“滑动窗口”确定“真实的车道线像素”,利用“曲线多项式”拟合“真实的车道线像素”;再然后基于拟合的车道线曲线,结合相机与真实世界比例参数,计算出车道线的曲率和车辆偏离车道线中心线的距离;最后测试跟踪一段 50 S左右的车载摄像头采集行车视频以验证程序可行性。
注意:
(1)本次博客内容注重代码功能实现讲解,原理部分可能需要参考之前的博客内容。
(2)使用 OpenCV 的 cv2.imread() 读取RGB三通道图像的结果是以 BGR 顺序排列的,而使用matplotlib的
plt.imread() 读取RGB三通道图像的结果是以 RGB 顺序排列的,应当注意区别。
(3)一些opencv库函数、python操作,博客内容中只提供了代码并未对其解释,需要读者阅读时自行百度查阅
即可。
准备工作:
1、用于相机校准的棋盘图像文件
2、用于编写代码过程中需要使用到的以及测试时需要用到的车道线图像(来源于车载摄像头行车视频中截取的帧图像)
3、用于最后测试“车载摄像头行车视频车道线跟踪”的测试视频
本次博客内容较多且代码过长,整体讲解容易引起读者阅读烦躁,故分为多段讲解,读者需要测试时只需将代码进行拼接,即可实现全部功能。
(一)、导入需要用到的包
# importing some useful packages
import matplotlib.pyplot as plt
import numpy as np
import cv2
import glob
import pickle
from moviepy.editor import VideoFileClip
from ipywidgets import interact, interactive, fixed
代码讲解:
(1)glob:python 自带的文件操作相关模块,用来查找符合自己目的的文件,类似于Windows下
的文件搜索,支持通配符操作,, ? , [ ] 这三个通配符 。,代表0个或多个字符,?代表一个字
符,[ ]匹配指定范围内的字符,如[0-9]匹配数字。
示例:获得指定目录下的所有 jpg 文件
使用相对路径:
glob.glob(’…/*.py’)
(2)pickle:用于 python 特有的类型与 python 的数据类型间进行转换
(3)moviepy:用于 python 处理视频的库
(4)ipywidgets :用于实现在 Jupyter 中实现交互操作使用(如果运行环境是pyther shell 可不予
理会,边缘检测部分阈值使用默认即可,不影响最后的结果)
(二)、使用棋盘图像计算相机校准(图像去畸变)
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6 * 9, 3), np.float32)
objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2)
# termination criteria
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# Arrays to store object points and image points from all the images
objpoints = [] # 3d points in real world space
imgpoints = [] # 2d points in image plane
# Make a list of calibration images
images = glob.glob('./camera_cal/calibration*.jpg')
# Step through the list and search for chessboard corners
for i, fname in enumerate(images):
img = cv2.imread(fname)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# Find the chessboard corners
ret, corners = cv2.findChessboardCorners(gray, (9,6),None)
# If found, add object points, image points
if ret == True:
objpoints.append(objp)
# refine image points
corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
imgpoints.append(corners2)
# Test undistortion on an image
img = cv2.imread('./camera_cal/calibration01.jpg')
img_size = (img.shape[1], img.shape[0])
# Do camera calibration given object points and image points
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, img_size,None,None)
dst = cv2.undistort(img, mtx, dist, None, mtx)
# Save the camera calibration result for later use
dist_pickle = {}
dist_pickle["mtx"] = mtx
dist_pickle["dist"] = dist
pickle.dump( dist_pickle, open( "calibration.p", "wb" ) )
# Test Code:
# Visualize undistortion
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20,10))
f.subplots_adjust(hspace = .2, wspace=.05)
ax1.imshow(img)
ax1.set_title('Original Image', fontsize=30)
ax2.imshow(dst)
ax2.set_title('Undistorted Image', fontsize=30)
f.show()
# Choose an image from which to build and demonstrate each step of the pipeline
exampleImg = cv2.imread('./test_images/challenge03.jpg')
exampleImg = cv2.cvtColor(exampleImg, cv2.COLOR_BGR2RGB)
# undistort image using camera calibration matrix from above
def undistort(img):
undist = cv2.undistort(img, mtx, dist, None, mtx)
return undist
exampleImg_undistort = undistort(exampleImg)
# Test Code:
# Visualize undistortion
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20,10))
f.subplots_adjust(hspace = .2, wspace=.05)
ax1.imshow(exampleImg)
ax1.set_title('Original Image', fontsize=30)
ax2.imshow(exampleImg_undistort)
ax2.set_title('Undistorted Image', fontsize=30)
f.show()
代码讲解:
(1)相机校准部分使用的棋盘图像有效黑白对角点数为 9×6 ,所以我们需要构造这些对角点在现
实世界中的相对位置,我们将这些位置简化设置成整数值,比如说第二行的第1个点就表示为
[0,1,0] , 即第0列第1行(第三位均为0值),第九行的第6个点就表示为[8,5,0],即第5列第8
行。
(2)设置细化图像角点的终止条件(也就是下面用到的一个参数)。
(3)创建两个列表用来存储目标点与图像点。
(4)使用 glob 读取文件夹中所有棋盘图像文件。
(5)遍历棋盘图像,找到所有角点。(objpoints:目标点,imgpoints:图像点)。
(6)使用 cv2.calibrateCamera() 得到相机校准矩阵和畸变系数。(mtx:相机校准矩阵,
dist:畸变系数)
(7)保存相机校准的结果在“calibration.p”文件中
(8)定义去畸变函数,返回去畸变之后的图像
结果展示:
(1)保存的相机校准结果(内含:相机校准矩阵和畸变系数)
(2)相机校准棋盘测试(棋盘图像校准效果明显)
(3)车道线图像去畸变测试 (效果:观察汽车引擎盖形状的变化)
(三)、图像透视变换(提取车道线信息需要)
# Perspective Transform
def unwarp(img, src, dst):
h, w = img.shape[:2]
# use cv2.getPerspectiveTransform() to get M, the transform matrix, and Minv, the inverse
M = cv2.getPerspectiveTransform(src, dst)
Minv = cv2.getPerspectiveTransform(dst, src)
# use cv2.warpPerspective() to warp your image to a top-down view
warped = cv2.warpPerspective(img, M, (w,h), flags=cv2.INTER_LINEAR)
return warped, M, Minv
h, w = exampleImg_undistort.shape[:2]
# define source and destination points for transform
src = np.float32([(575,464),(707,464), (258,682), (1049,682)])
dst = np.float32([(450,0),(w-450,0),(450,h),(w-450,h)])
exampleImg_unwarp, M, Minv = unwarp(exampleImg_undistort, src, dst)
# Test Code:
# Visualize unwarp
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(20,10))
f.subplots_adjust(hspace = .2, wspace=.05)
ax1.imshow(exampleImg_undistort)
x = [src[0][0],src[2][0],src[3][0],src[1][0],src[0][0]]
y = [src[0][1],src[2][1],src[3][1],src[1][1],src[0][1]]
ax1.plot(x, y, color='#33cc99', alpha=0.4, linewidth=3, solid_capstyle='round', zorder=2)
ax1.set_ylim([h,0])
ax1.set_xlim([0,w])
ax1.set_title('Undistorted Image', fontsize=30)
ax2.imshow(exampleImg_unwarp)
ax2.set_title('Unwarped Image', fontsize=30)
f.show()
代码讲解:
(1)定义透视变换函数,返回透视变换后的图像、投影矩阵、反投影矩阵
对于车道线图像,通过透视变换可以获得一个相对更加直观的视角(鸟瞰图),在新的视平面中能够更简单
更完美的实现对车道线的识别。
使用 cv2.getPerspectiveTransform() 得到投影矩阵
使用cv2.warpPerspective() 得到反投影矩阵
使用cv2.warpPerspective() 得到透视变换后的图像
(2)cv2.getPerspectiveTransform() 需要两个参数 src 和 dst,他们分别为原图像中能够表示一个矩形
四个点的坐标以及扭曲以后图像的边缘四角在当前图像中的坐标,这两个矩形的坐标不同的相机的数值也不
同,比如说本例中相机分辨率为 1280×720 , 设置[(575,464),(707,464), (258,682), (1049,682)]在图像中构
成一个梯形(如下图中所示),这个梯形在俯视图(鸟瞰图)中将变成一个长方形,我们以这个梯形的高作
为目标图像的高,前后各减去一个偏移,就是我们的目标图像。
说明:这部分的上述偏移的大小设置,作者没有理解,故对dst中的数值设置也就没理解,猜测这个偏移值应
该是需要相机测定后再设置的吧。
结果展示:
(1)去畸变后的图像进行透视变换,得到变换视角后的图像结果
(四)、测试不同色彩空间不同通道下车道线的显示效果
# Visualize multiple color space channels
exampleImg_unwarp_R = exampleImg_unwarp[:,:,0]
exampleImg_unwarp_G = exampleImg_unwarp[:,:,1]
exampleImg_unwarp_B = exampleImg_unwarp[:,:,2]
exampleImg_unwarp_HSV = cv2.cvtColor(exampleImg_unwarp, cv2.COLOR_RGB2HSV)
exampleImg_unwarp_H = exampleImg_unwarp_HSV[:,:,0]
exampleImg_unwarp_S = exampleImg_unwarp_HSV[:,:,1]
exampleImg_unwarp_V = exampleImg_unwarp_HSV[:,:,2]
exampleImg_unwarp_LAB = cv2.cvtColor(exampleImg_unwarp, cv2.COLOR_RGB2Lab)
exampleImg_unwarp_L = exampleImg_unwarp_LAB[:,:,0]
exampleImg_unwarp_A = exampleImg_unwarp_LAB[:,:,1]
exampleImg_unwarp_B2 = exampleImg_unwarp_LAB[:,:,2]
# Test Code:
# Visualize test multiple color space channels
f, axs = plt.subplots(3,3, figsize=(16, 12))
f.subplots_adjust(hspace = .2, wspace=.001)
axs = axs.ravel()
axs[0].imshow(exampleImg_unwarp_R, cmap='gray')
axs[0].set_title('RGB R-channel', fontsize=30)
axs[1].imshow(exampleImg_unwarp_G, cmap='gray')
axs[1].set_title('RGB G-Channel', fontsize=30)
axs[2].imshow(exampleImg_unwarp_B, cmap='gray')
axs[2].set_title('RGB B-channel', fontsize=30)
axs[3].imshow(exampleImg_unwarp_H, cmap='gray')
axs[3].set_title('HSV H-Channel', fontsize=30)
axs[4].imshow(exampleImg_unwarp_S, cmap='gray')
axs[4].set_title('HSV S-channel', fontsize=30)
axs[5].imshow(exampleImg_unwarp_V, cmap='gray')
axs[5].set_title('HSV V-Channel', fontsize=30)
axs[6].imshow(exampleImg_unwarp_L, cmap='gray')
axs[6].set_title('LAB L-channel', fontsize=30)
axs[7].imshow(exampleImg_unwarp_