大家五一快乐呀,我是李慢慢。
前情提要
距离上一次正儿八经发文,貌似已经过去两个月了,因为疫情原因我一直都是居家办公,不过好在自己有“树莓派小车”可以倒腾一下,倒也不是那么无聊。这两个月的周末时间,大部分都花在“树莓派小车”的下一个课题上了【如何基于图像识别让“树莓派小车”实现自动驾驶功能】。
关于这个课题,此前已经完成了一些内容了,可以从下面的链接看看背景。
1、智驾小车的桌面环境搭建
2、智驾小车的摄像头模块介绍
本期是第4篇:
4、智驾小车的车道线识别的基本原理
本篇主要内容其实是扒了知乎两位大神的杰作(如下所示),照着做了一遍,所以,可以想到的是,我后续还会有第5篇文章或者视频,那是树莓派小车最终实现的基于车道线检测的自动驾驶功能。
本次文章的编程语言是python,所用到的最重要的两个包,一个是opencv,它是用来处理图像的,另一个是numpy,它是用来处理矩阵的。
本文所有涉及到的图片、视频及测试脚本,均保存到了网盘,请移至文末下载。
废话不多说了,以下正文。
1、读入图片
利用python读入图片并显示相关信息:
#
import matplotlib.pyplot as plt
import matplotlib.image as mping
import cv2
#
image = mping.imread("./test_images/test3.jpg")
print("image: ",image)
print("image shape: ",image.shape)
plt.imshow(image)
plt.show()
上述代码保存为【LKA_01_read_img.py】
运行下上述程序。
python LKA_01_read_img.py
就可以看到以下的画面。
上图是桌面的截图,图中左边命令行窗口打印的信息,就是图片的信息,解读下的话,你会发现其实图片就是个矩阵,它横着有720行,竖着有1280列,特定的行中 特定了一个像素点(上图其实就有720*1280个像素点),每个像素点里存放着3个值,通常情况下分别代表着R、G和B值。这三个值共同确定了一个像素点的颜色,众多的像素点组成了这一张图。
好了,读好了图,就可以对图片进行一些处理了。
2、图像检测
然后就是怎么才能从图像中检测出来车道线在哪儿呢?
人眼可能一眼就看出来车道线在哪儿了,但计算机却一时分辨不了,它需要做一系列的判断。
以下我们简单做些测试。
2.1、梯度(边缘)检测
所谓梯度检测或者边缘检测,是测量图片中像素点之间的变化率,来识别出来变化特别明显的部分。就拿车道线来讲,车道线和车道线旁边的公路,他们两者之间的颜色是有很大差异的,通过检测这种差异,我们可以找到车道线的“边缘”。
针对上面说的这个边缘检测,cv2里提供了一个叫做Canny的包,用这个包就可以简单做下测试。
稍微修改下上面的代码,就可以了,如下(代码保存为【LKA_02_canny.py】):
# import packages
import matplotlib.pyplot as plt
import matplotlib.image as mping
import numpy as np
import cv2
image = mping.imread("./test_images/test3.jpg")# RGB format
#print("image: ",image)
#print("image shape: ",image.shape)
#plt.imshow(image)
#plt.show()
img_canny = cv2.Canny(image,0,255)
plt.imshow(img_canny,cmap="gray")
plt.show()
方便对比,先贴上原图,如下:
下图就是用Cannny包生成的边缘检测图。
python LKA_02_canny.py
用上面的Canny工具生成的边缘检测图,我们能看到车道线边缘确实被提取出来了,但也是断断续续的,而且还有很多其他的不必要的边缘(也就是噪声)点也被提取出来了。
这肯定不是我们想要的。
所以下面也简单探索下其他的边缘检测的方法。
那么用cv2的Sobel包来试试吧。
Sobel相比Canny的优势是可以自己定义梯度方向,比如获得x方向的梯度图,或者y方向的梯度图,也可以综合考虑x和y方向。
为此我准备了以下的代码(保存为【LKA_03_sobel.py】):
# import packages
import matplotlib.pyplot as plt
import matplotlib.image as mping
import numpy as np
import cv2
image = mping.imread("./test_images/test3.jpg")# RGB format
def abs_sobel_thresh(image,orient='x',sobel_kernel=3,thresh=(0,255)):
# this is to calculate the gradient in x/y direction.
# generating the gray image.
gray = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY)
# to calculate the gradient in x/y
if orient == 'x':
abs_sobel = np.absolute(cv2.Sobel(gray,cv2.CV_64F,1,0,ksize=sobel_kernel))
if orient == 'y':
abs_sobel = np.absolute(cv2.Sobel(gray,cv2.CV_64F,0,1,ksize=sobel_kernel))
# to scale the data.
scaled_sobel = np.uint8(255*abs_sobel/np.max(abs_sobel))
# to generate a blank array,as black picture.
grad_binary = np.zeros_like(scaled_sobel)
#
grad_binary[ ( scaled_sobel >= thresh[0] ) & ( scaled_sobel <= thresh[1] ) ] = 1
#print((scaled_sobel>=thresh[0])&(scaled_sobel<=thresh[1]))
return grad_binary
if __name__ == "__main__":
ksize = 15
gradx = abs_sobel_thresh(image,orient='x',sobel_kernel=ksize,thresh=(50,180))
grady = abs_sobel_thresh(image,orient='y',sobel_kernel=ksize,thresh=(30,90))
plt.imshow(gradx,cmap="gray")
plt.show()
运行上述代码,可以获得x或者y方向的梯度图。
python LKA_03_sobel.py
下图是x方向的梯度图。
下图是y方向的梯度图。
从上面可以看出,x方向的梯度图噪声少,更适合用来检测车道线。
用梯度图就可以实现车道线的检测了吗?No!
现实情况往往无比复杂,车道线有可能颜色不清晰,有可能和周边梯度值区分不开,所以,一般会用多种方式来综合检测。
另一种检测方式就是颜色域检测。
2.2 颜色域值检测
RGB空间:
最常见的颜色域值检测就是RGB检测了,我们可以通过下面的代码,将原图分这三3个通道取出来看看效果。我们可以通过如下的代码【LKA_04_rgb.py】,分别提取出来三个通道并显示出来。
# import packages
import matplotlib.pyplot as plt
import matplotlib.image as mping
import numpy as np
import cv2
image = mping.imread("./test_images/test3.jpg")# RGB format
def img_rgb(img):
r_channel = img[:,:,0]
g_channel = img[:,:,1]
b_channel = img[:,:,2]
#print("r_channel: ",r_channel)
#print("r_channel shape: ",r_channel.shape)
#return r_channel
#return g_channel
return b_channel
if __name__ == "__main__":
rgb_binary = img_rgb(image)
plt.imshow(rgb_binary)
plt.show()
如下是上述代码运行后的结果。
看上面三个图,你就会发现,R通道中的车道线还算清晰,G通道和B通道的车道线就模糊甚至是没有了。
但是G通道和B通道也不是完全没有用,我们将三个通道的数据进行个简单的删选和组合,重新生成个黑白图,效果就好很多了。
看一下下面这个代码的运行效果。
代码文件【LKA_05_RGB_combined.py】内容如下:
# import packages
import matplotlib.pyplot as plt
import matplotlib.image as mping
import numpy as np
import cv2
image = mping.imread("./test_images/test3.jpg")# RGB format
def rgb_select(img,r_thresh,g_thresh,b_thresh):
r_channel = img[:,:,0]
g_channel = img[:,:,1]
b_channel = img[:,:,2]
r_binary = np.zeros_like(r_channel)
r_binary[(r_channel > r_thresh[0]) & (r_channel <= r_thresh[1])] = 1
g_binary = np.zeros_like(g_channel)
g_binary[(g_channel > g_thresh[0]) & (g_channel <= g_thresh[1])] = 1
b_binary = np.zeros_like(b_channel)
b_binary[(b_channel > b_thresh[0]) & (b_channel <= b_thresh[1])] = 1
combined = np.zeros_like(r_channel)
combined[((r_binary == 1) & (g_binary == 1) & (b_binary == 1))] = 1
return combined
if __name__ == "__main__":
rgb_binary = rgb_select(image,r_thresh=(225,255),g_thresh=(180,255),b_thresh=(0,255))
plt.imshow(rgb_binary,cmap="gray")
plt.show()
上述代码其实是把设定了一个上下限,给R通道G通道B通道都设定上下限,只有满足这些上下限的通道里的像素点,才会被保留。其他的都置零(抹黑)。所以,上述代码运行结果如下,单纯通过颜色空间的删选,貌似也可以得到挺不错的结果呢。
拓展:
除了RGB空间,其实图片还有其他的空间类型,比如hsv空间、lab空间、luv空间、hls空间,也可以分别从各个空间中分离出某个通道,看其效果。
综合不同的图片空间,综合考虑,可以获得更为精确的检测效果。
但鉴于上述根据RGB获得的检测效果我已经挺满意了,所以就不折腾了。
接下来,我仅仅是综合下RGB颜色空间的检测结果和Sobel在x方向的梯度检测结果,获得最终的图片检测结果。综合检测的代码如下所示【LKA_06_RGB-and-Sobel.py】:
# import packages
import matplotlib.pyplot as plt
import matplotlib.image as mping
import numpy as np
import cv2
image = mping.imread("./test_images/test3.jpg")# RGB format
def abs_sobel_thresh(image,orient='x',sobel_kernel=3,thresh=(0,255)):
# this is to calculate the gradient in x/y direction.
# generating the gray image.
gray = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY)
# to calculate the gradient in x/y
if orient == 'x':
abs_sobel = np.absolute(cv2.Sobel(gray,cv2.CV_64F,1,0,ksize=sobel_kernel))
if orient == 'y':
abs_sobel = np.absolute(cv2.Sobel(gray,cv2.CV_64F,0,1,ksize=sobel_kernel))
# to scale the data.
scaled_sobel = np.uint8(255*abs_sobel/np.max(abs_sobel))
# to generate a blank array,as black picture.
grad_binary = np.zeros_like(scaled_sobel)
#
grad_binary[ ( scaled_sobel >= thresh[0] ) & ( scaled_sobel <= thresh[1] ) ] = 1
#print((scaled_sobel>=thresh[0])&(scaled_sobel<=thresh[1]))
return grad_binary
def rgb_select(img,r_thresh,g_thresh,b_thresh):
r_channel = img[:,:,0]
g_channel = img[:,:,1]
b_channel = img[:,:,2]
r_binary = np.zeros_like(r_channel)
r_binary[(r_channel > r_thresh[0]) & (r_channel <= r_thresh[1])] = 1
g_binary = np.zeros_like(g_channel)
g_binary[(g_channel > g_thresh[0]) & (g_channel <= g_thresh[1])] = 1
b_binary = np.zeros_like(b_channel)
b_binary[(b_channel > b_thresh[0]) & (b_channel <= b_thresh[1])] = 1
combined = np.zeros_like(r_channel)
combined[((r_binary == 1) & (g_binary == 1) & (b_binary == 1))] = 1
return combined
def color_gradient_threshold(image):
ksize = 15
gradx = abs_sobel_thresh(image,orient='x',sobel_kernel=ksize,thresh=(50,180))
rgb_binary = rgb_select(image,r_thresh=(225,255),g_thresh=(180,255),b_thresh=(0,255))
combined_binary = np.zeros_like(image)
combined_binary[((gradx==1)|(rgb_binary==1))] = 255
color_binary = combined_binary
return color_binary
if __name__ == "__main__":
ksize = 15
color_img = color_gradient_threshold(image)
plt.imshow(color_img)
plt.show()
下图就是上面代码的运行结果了。基于图像的车道线检测就先进行到这吧。主要是通过上述的测试,自己对于边缘检测/梯度检测/颜色空间检测等名词有了些直观的理解,成就感拉满。
3、感兴趣区域提取
在上面的黑白图中,虽然车道线已经很明显了,但还是有一些噪声点,挺影响后续研究的,而且鉴于车道线在图片中的位置一般都不会有太大的改变,所以一般都会把不重要的区域“抹黑”,只保留感兴趣的区域。如下图所示。
上图中选择了保留三角形区域内的像素点,其实也可以定义一四边形,更多边形等,但我觉得麻烦,三角形够我测试用了,所以,在上述代码的基础上,添加新的感兴趣区域的函数体,帮助我们实现对感兴趣区域的提取。
【LKA_07_interest.py】中感兴趣区域的函数定义如下所示:
def region_of_interest(img,vertices):
mask = np.zeros_like(img)
cv2.fillPoly(mask,[vertices],[255,255,255])
masked_image = cv2.bitwise_and(img,mask)
return masked_image
上述代码运行的结果如下所示。
就这样,我们只把感兴趣的区域内的像素值保留了下来。
看起来清爽多了呢。
4、透视变换
透视变换的作用是让我们从另外一个角度观察图片中的场景,例如俯视。
我们所获得的的图像由于“近大远小”的透视关系造成了图像世界与现实世界的偏差(两条车道线在真实世界中理应是平行的)。
透视变换的本质是“映射”,将原图像的像素点按照一定的“比例”映射到另外一张图上。
这个“比例”关系可以通过4个点的映射规律得到,从而推广到图片中的其他像素点。
opecv中有个函数可以帮助我们完成这些透视变化过程,这个函数是【cv2.getPerspectiveTransform(src, dst)】
输入是:原图中的4个点坐标,映射后的4个点的坐标;
输出是:一个矩阵,也就是这个映射后的“比例”关系。
原图中的4个点的坐标可以直接利用画图工具,找原图上对应的一个矩形的四个角点,映射后的4个点的坐标是自己定义的,需要不断尝试调整。
整个透视变换的脚本设置如下(在原来的基础上增加了一个做透视变换的函数)。
【LKA_08_perspective.py】中做透视变换的函数如下:
def perspective_transform(image):
# give 4 points as original coordinates.
top_left =[590,460]
top_right = [750,460]
bottom_left = [330,650]
bottom_right = [1130,650]
# give 4 points to project.
proj_top_left = [250,100]
proj_top_right = [1150,100]
proj_bottom_left = [330,650]
proj_bottom_right = [1130,650]
# to get image size.
img_size = (image.shape[1],image.shape[0])
#
pts1 = np.float32([top_left,top_right,bottom_left,bottom_right])
pts2 = np.float32([proj_top_left,proj_top_right,proj_bottom_left,proj_bottom_right])
matrix_K = cv2.getPerspectiveTransform(pts1,pts2)
img_k = cv2.warpPerspective(image,matrix_K,img_size)
return img_k
上述代码运行后的结果如下所示。
5、车道线提取-滑移窗口
接下来,继续对透视变换后的上图进行车道线的提取分析。
在车道提取前,需要先大概知道车道线的位置,因此,提取出上图中的像素点的直方图。
直方图:横坐标为1280列像素点的位置,纵坐标为每一列中像素点真值的总和。如下所示。
代码方面,画直方图和画图片是不一样的,需要注意下。
def histogram_img(image):
histogram_binary = np.zeros((image.shape[0],image.shape[1]),dtype=np.int)
histogram_binary[image[:,:,0]>0] = 1
#print("histogram_binary:",histogram_binary)
histogram = np.sum(histogram_binary[:,:],axis=0)
#print("histogram: ",histogram)
#print("histogram shape: ",histogram.shape)
return histogram
if __name__ == "__main__":
ksize = 15
img_color = color_gradient_threshold(image)
#
left_bottom = [0, img_color.shape[0]]
right_bottom = [img_color.shape[1],img_color.shape[0]]
apex = [ img_color.shape[1]/2, 420 ]
vertices = np.array([ left_bottom, right_bottom, apex ],np.int32)
#
img_interest = region_of_interest(img_color,vertices)
img_perspective = perspective_transform(img_interest)
img_histogram = histogram_img(img_perspective)
plt.plot(img_histogram)
plt.show()
完整代码见【LKA_09_histogram.py】。
从上图中可以观察到,对应有车道线那几列附近像素值较大,因此出现了两个波峰。
接下来就是寻找这两个波峰的位置:
def lane_position(histogram):
histogram_size = histogram.shape
#print(histogram_size[0])
middle_point = histogram_size[0]/2
print("middle_point: ",middle_point)
#
left_point = [0,0]
for i in range(middle_point):
if histogram[i] > left_point[1]:
left_point[1] = histogram[i]
left_point[0] = i
#
right_point = [0,0]
for j in range(middle_point,histogram_size[0]):
if histogram[j] > right_point[1]:
right_point[1] = histogram[j]
right_point[0] = j
result_points = [left_point,right_point]
print("result_points: ",result_points)
return result_points
完整代码见【LKA_10_lanePosition.py】
运行代码后,可以获得结果,返回值显示:
('result"_points: ',[[342,566],[1014,291]])
这样我们就知道了左边车道大概在x为342的地方,右边车道x大概在1014的地方。
从而我们可以利用这个两个波峰定位车道线在X方向的中心位置,以这个位置为起点,通过【滑移窗口】找到图片中的车位线的位置。
滑移窗口法:以左边车道线为例,首先根据前面介绍的直方图方法,找到左车道线的大致位置,将这个大致位置作为起始点。定义一个矩形区域,称之为“窗口”,我们将起始点作为窗口的下边线的中点,然后找到“窗口”方块中的所有白色点的横坐标,计算出其均值,再将这个均值作为下一个窗口的下边缘的起始点。
为此,我准备了一个的函数来进行滑移窗口的测试。
def sliding_window(image,lanes_pos):
# starting original points for windows.
left_x_current = lanes_pos[0][0]
right_x_current = lanes_pos[1][0]
nWindows = 10
window_height = np.int(image.shape[0]//nWindows)
window_width = 80
# to get the non-zero data in the input image.
nonzero = image.nonzero()
nonzero_y = nonzero[0]
nonzero_x = nonzero[1]
#
# create a empty list to receive left/right line pixel.
left_lane_inds = []
right_lane_inds = []
#
out_img = np.dstack((image,image,image))
#print("out_img: ",out_img)
# create window by window
for window in range(nWindows):
# window size.
win_y_top = image.shape[0] - (window +1)*window_height
win_y_bottom = image.shape[0] - window*window_height
win_x_left_left = left_x_current - window_width
win_x_left_right = left_x_current + window_width
win_x_right_left = right_x_current - window_width
win_x_right_right = right_x_current + window_width
# define a rectangle for left+right lane.
# and add the rectangle to the input image.
cv2.rectangle(image,(win_x_left_left,win_y_top),(win_x_left_right,win_y_bottom),(0,255,0),2)
cv2.rectangle(image,(win_x_right_left,win_y_top),(win_x_right_right,win_y_bottom),(0,255,0),2)
good_left_inds = ((nonzero_y >= win_y_top)&(nonzero_y < win_y_bottom)&(nonzero_x >= win_x_left_left)&(nonzero_x < win_x_left_right)).nonzero()[0]
good_right_inds = ((nonzero_y >= win_y_top)&(nonzero_y < win_y_bottom)&(nonzero_x >= win_x_right_left)&(nonzero_x < win_x_right_right)).nonzero()[0]
#print(good_left_inds)
left_lane_inds.append(good_left_inds)
right_lane_inds.append(good_right_inds)
#
#print("nonzero_x_left:",nonzero_x[good_left_inds])
#print("non_zero_x_right:",nonzero_x[good_right_inds])
if len(good_left_inds)>50:
left_x_current = np.int(np.mean(nonzero_x[good_left_inds]))
if len(good_right_inds)>50:
right_x_current = np.int(np.mean(nonzero_x[good_right_inds]))
# ending of lop.
#print("left_lane_inds",left_lane_inds)
# to transfom a list of list to a list.
left_lane_inds = np.concatenate(left_lane_inds)
right_lane_inds = np.concatenate(right_lane_inds)
#print("left_lane_inds",left_lane_inds)
left_x = nonzero_x[left_lane_inds]
left_y = nonzero_y[left_lane_inds]
right_x = nonzero_x[right_lane_inds]
right_y = nonzero_y[right_lane_inds]
#
results = [image,left_x,left_y,right_x,right_y]
#print("sliding windows results: ",results)
return results
完整代码见【LKA_11_sliding_windows.py】。
本代码运行后,就能得到如下的结果啦。
上面的绿色的一个个小线框就是“窗口”,通过不断计算窗口中的白色像素点的横坐标均值,就能不断得到下一个窗口的起始点,从而不断的累加,从下往上找出所有的窗口。
由于是一根直线车道,所以上述窗口的作用并不是很明显,试想一下,如果有一个车辆弯道在前方,那么用这个窗口滑移方法来检测车道线的效果就会很明显了。
6、车道线提取-二次曲线拟合
为什么要拟合曲线呢?
因为我们需要根据识别的结果,构建出一根理论曲线,来代表车道线。这样很方便我们后续根据理论曲线来计算出曲线中各个点的曲率,方便及时控制车辆转弯。还有一个用处就是,可以将生成的曲线,添加到图像上,来实时监控算法的有效性。
上图中,以左边车道为例,每个小窗口中的所有白色点,我们都可以求得其均值,得到一个均值坐标,这样我们就得到了一系列白色点,用一根丝滑的曲线把这些点串起来,就是拟合要做的事情了。
关于拟合的部分,写在了如下的函数中。完整代码见【LKA_12_fit_lines.py】。
def fit_polynominal(img_sliding_window):
# input messages.
image = img_sliding_window[0]
left_x = img_sliding_window[1]
left_y = img_sliding_window[2]
right_x = img_sliding_window[3]
right_y = img_sliding_window[4]
# to fit a line for the points.
# y = a*x^2 + b*x + c
# return of polyfit is [a,b,c]
left_fit = np.polyfit(left_y,left_x,2)
right_fit = np.polyfit(right_y,right_x,2)
#print("left_fit: ",left_fit)
# to generate x and y values for plotting.
ploty = np.linspace(0,image.shape[0]-1,image.shape[0])
left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
#print(ploty)
plt.plot(left_fitx,ploty,color='yellow')
plt.plot(right_fitx,ploty,color='red')
return 0
运行【LKA_12_fit_lines.py】可以得到如下的结果。
7、刻画车道蒙版
根据上述得到的车道线的拟合曲线,我们可以在两根曲线之间画一个半透明的蒙版,用来标识一个车道。标识出车道只是为了好看,没有这一步其实也可以的。
核心代码如下所示。
def drawing_poly(img_ori, img_fit):
# create an image to draw the lines on.
#
left_fitx = img_fit[0]
right_fitx = img_fit[1]
ploty = img_fit[2]
#
img_zero = np.zeros_like(img_ori)
#
#print("left_fitx:",left_fitx)
#print("ploty:",ploty)
pts_left = np.transpose(np.vstack([left_fitx,ploty]))
#print("pts_left:",pts_left)
#print("pts_left shape:",pts_left.shape)
pts_right = np.transpose(np.vstack([right_fitx,ploty]))
pts_right = np.flipud(pts_right)
#print("pts_right:",pts_right)
#print("pts_right shape:",pts_right.shape)
pts = np.vstack((pts_left,pts_right))
#print("pts_left+right:",pts)
#print("pts_left+right shape:",pts.shape)
img_mask = cv2.fillPoly(img_zero,np.int_([pts]),(0,255,0))
#print("img_mask:",img_mask)
#print("img_mask shape:",img_mask.shape)
return img_mask
完整代码见【LKA_13_drawing_poly.py】。完整代码运行结果如下所示:
8、反透视车道线
接下来,可以把我们提取出来的车道蒙版,通过反透视,添加到原始图像上。
反透视的原理原理是什么呢?就是此前所做的正视图转俯视图的逆过程。
matrix_K = cv2.getPerspectiveTransform(pts1,pts2)
在上述透视变化的系数的获取上,将两个参数兑换下位置,就能得到反透视的变换系数了。
matrix_K_back = cv2.getPerspectiveTransform(pts2,pts1)
是不是很简单?
接下来,就是将绿色蒙版就行反透视,并且和车道最开始的图片进行重叠。
核心代码如下所示:
def drawing_poly_perspective_back(img_ori, img_fit,matrix_K_back):
# create an image to draw the lines on.
#
left_fitx = img_fit[0]
right_fitx = img_fit[1]
ploty = img_fit[2]
#
img_zero = np.zeros_like(img_ori)
#
#print("left_fitx:",left_fitx)
#print("ploty:",ploty)
pts_left = np.transpose(np.vstack([left_fitx,ploty]))
#print("pts_left:",pts_left)
#print("pts_left shape:",pts_left.shape)
pts_right = np.transpose(np.vstack([right_fitx,ploty]))
pts_right = np.flipud(pts_right)
#print("pts_right:",pts_right)
#print("pts_right shape:",pts_right.shape)
pts = np.vstack((pts_left,pts_right))
#print("pts_left+right:",pts)
#print("pts_left+right shape:",pts.shape)
img_mask = cv2.fillPoly(img_zero,np.int_([pts]),(0,255,0))
#print("img_mask:",img_mask)
#print("img_mask shape:",img_mask.shape)
# to get image size.
img_size = (img_ori.shape[1],img_ori.shape[0])
img_mask_back = cv2.warpPerspective(img_mask,matrix_K_back,img_size)
return img_mask_back
完整代码见【LKA_14_perspective_back.py】。运行完整代码就可以得到如下的图片。慢慢长征路终于走到了这一步。
9、基于视频的车道线识别
上面是实现了一张图片能识别出车道线,并覆盖一层半透明的蒙版来标识车道。如果是一个视频,怎么来识别呢?
其实很简单,读入视频,然后把视频分解为一张一张的图片,每一张图片都按照上面的方法来一次识别,就可以了。
如下是上述原理的核心代码:
if __name__ == "__main__":
# video input.
video_input = "./test_video/project_video.mp4"
cap = cv2.VideoCapture(video_input)
# output setting.
video_output = "./test_video/project_video_output_v2.mp4"
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
width = 1280
height = 720
fps = 20
video_out = cv2.VideoWriter(video_output,fourcc,fps,(width,height))
# add some text to the output video.
content = "this is frame: "
pos = (64,90)
color = (0,255,0)
font = cv2.FONT_HERSHEY_SIMPLEX
weight = 2
size = 1
count = 0
#
# prcessing frame by frame.
while True:
ret,frame = cap.read()
if not ret:
print("video read error, exited...")
break
if cv2.waitKey(25) & 0xFF == ord('q'):
print(" you quit the program by clicking 'q'...")
break
image = frame
ksize = 15
img_color = color_gradient_threshold(image)
#
left_bottom = [0, img_color.shape[0]]
right_bottom = [img_color.shape[1],img_color.shape[0]]
apex = [ img_color.shape[1]/2, 420 ]
vertices = np.array([ left_bottom, right_bottom, apex ],np.int32)
img_interest = region_of_interest(img_color,vertices)
img_perspective,matrix_K_back = perspective_transform(img_interest)
img_histogram = histogram_img(img_perspective)
lanes_pos = lane_position(img_histogram)
img_sliding_window = sliding_window(img_perspective,lanes_pos)
img_fit_list = fit_polynominal(img_sliding_window)
## to set the transparency of img.
img_mask_back = drawing_poly_perspective_back(image,img_fit_list,matrix_K_back)
#img_mask_back_result = img_mask_back*0.5 + image*0.5
img_mask_back_result = cv2.addWeighted(image,1,img_mask_back,0.3,0)
results = img_mask_back_result
cv2.imshow("frame",results)
contents = content + str(count)
cv2.putText(results,contents,pos,font,size,color,weight,cv2.LINE_AA)
video_out.write(results)
#
count += 1
cap.release()
cv2.destroyAllWindows()
完整代码保存在【LKA_15_video_detection.py】中。
运行代码,可以将每一帧识别的图片汇总保存为视频,以下是保存的视频,可以来看下识别的结果。
后记
搞到这里,这篇文章算是功德圆满了。写到这里,想想这两个月的学习经历,也是觉得内心通达。不过,也不能骄傲,因为不足点实在是太多了。
1、首先,这种车道线检测的方法,在自动驾驶领域,非常传统,业界还有更高级的方法,比如基于模型的,基于机器学习的。类似上述的这中基于像素点提取的,有点low了,不过对于我后续智驾小车想要识别车道线的功能来讲,应该是够用了。
2、上述代码中的参数真的只是我自己随便调的,也没咋深入考虑,可能场景一复杂,车道线我就识别不出来了。
3、上述针对视频一帧一帧的处理的时候,特别慢,后续要针对智驾小车实时采集的图像来快速处理和识别,也是我后面要攻克的内容。
4、上述图像处理的环节缺少了摄像头标定的部分,我们默认的是现在的图像是没有啥畸变的。如果以后摄像头采集的图像畸变较大,还需要针对这个摄像头做些调节畸变的操作。
先说到这里吧,后面再继续分享。
最后,附上上述所有图像素材、视频素材、和所有代码的下载链接,感兴趣的朋友,可以自己玩玩儿。
下载链接见附件
本文完。