首页/文章/ 详情

实操:自动驾驶的车道识别原理及演练

1年前浏览3585

大家五一快乐呀,我是李慢慢。

前情提要

距离上一次正儿八经发文,貌似已经过去两个月了,因为疫情原因我一直都是居家办公,不过好在自己有“树莓派小车”可以倒腾一下,倒也不是那么无聊。这两个月的周末时间,大部分都花在“树莓派小车”的下一个课题上了【如何基于图像识别让“树莓派小车”实现自动驾驶功能】。

关于这个课题,此前已经完成了一些内容了,可以从下面的链接看看背景。

1、智驾小车的桌面环境搭建

2、智驾小车的摄像头模块介绍

3、智驾小车摄像头模块测试

本期是第4篇:

4、智驾小车的车道线识别的基本原理

本篇主要内容其实是扒了知乎两位大神的杰作(如下所示),照着做了一遍,所以,可以想到的是,我后续还会有第5篇文章或者视频,那是树莓派小车最终实现的基于车道线检测的自动驾驶功能。

本次文章的编程语言是python,所用到的最重要的两个包,一个是opencv,它是用来处理图像的,另一个是numpy,它是用来处理矩阵的。

本文所有涉及到的图片、视频及测试脚本,均保存到了网盘,请移至文末下载。

废话不多说了,以下正文。

1、读入图片

利用python读入图片并显示相关信息:

    #import matplotlib.pyplot as pltimport matplotlib.image as mpingimport 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 packagesimport matplotlib.pyplot as pltimport matplotlib.image as mpingimport numpy as npimport cv2image = 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 packagesimport matplotlib.pyplot as pltimport matplotlib.image as mpingimport numpy as npimport cv2image = mping.imread("./test_images/test3.jpg")# RGB formatdef 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_binaryif __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 packagesimport matplotlib.pyplot as pltimport matplotlib.image as mpingimport numpy as npimport cv2image = mping.imread("./test_images/test3.jpg")# RGB formatdef 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 packagesimport matplotlib.pyplot as pltimport matplotlib.image as mpingimport numpy as npimport cv2image = mping.imread("./test_images/test3.jpg")# RGB formatdef 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 combinedif __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 packagesimport matplotlib.pyplot as pltimport matplotlib.image as mpingimport numpy as npimport cv2image = mping.imread("./test_images/test3.jpg")# RGB formatdef 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_binarydef 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 combineddef 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_binaryif __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 histogramif __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、上述图像处理的环节缺少了摄像头标定的部分,我们默认的是现在的图像是没有啥畸变的。如果以后摄像头采集的图像畸变较大,还需要针对这个摄像头做些调节畸变的操作。

                                            先说到这里吧,后面再继续分享。

                                            最后,附上上述所有图像素材、视频素材、和所有代码的下载链接,感兴趣的朋友,可以自己玩玩儿。

                                            下载链接见附件

                                            本文完。

                                            来源:车路慢慢

                                            附件

                                            免费实操:自动驾驶的车道识别原理及演练.txt
                                            python理论自动驾驶控制
                                            著作权归作者所有,欢迎分享,未经许可,不得转载
                                            首次发布时间:2023-06-22
                                            最近编辑:1年前
                                            李慢慢
                                            硕士 自动驾驶仿真工程师一枚
                                            获赞 11粉丝 63文章 122课程 0
                                            点赞
                                            收藏
                                            未登录
                                            还没有评论
                                            课程
                                            培训
                                            服务
                                            行家
                                            VIP会员 学习 福利任务 兑换礼品
                                            下载APP
                                            联系我们
                                            帮助与反馈