汽車在道路上行駛需要遵循一定的行駛規則,路面的車道則起到規范汽車行駛規則的作用。車道的種類有很多種,如單行線、雙行線,虛線、網格線等,不同顏色、形狀的車道線代表着不同的行駛規則,汽車和行人可以根據這些規則來使用道路,避免沖突。因此,准確檢測並識別車道類型,並按照相應規則正確行駛,是汽車實現自動駕駛的基礎。
優達學城的自動駕駛項目課程包含了一個車道線檢測項目,其主要目的就是教給無人車如何檢測並識別車道,本文檔將該項目內容進行總結整理。
車道線檢測方法主要分為兩類:(1)基於道路特征的車道線檢測;(2)基於道路模型車道線檢測。基於道路特征的車道線檢測作為主流檢測方法之一,主要是利用車道線與道路環境的物理特征差異進行圖像的分割與處理,從而突出車道線特征,以實現車道線的檢測。該方法復雜度較低,實時性較高,但容易受到道路環境干擾。基於道路模型的車道線檢測主要是基於不同的二維或三維道路圖像模型(如直線型、拋物線型、樣條曲線型、組合模型等),采用相應方法確定各模型參數,然后進行車道線擬合。該方法對特定道路的檢測具有較高的准確度,但局限性強、運算量大、實時性較差。
本項目采用的是基於道路灰度特征的車道線檢測方法。主要流程如下:
(1)灰度圖像轉換
自動駕駛車載攝像頭實時拍攝的照片為RGB格式,為了能夠提取其灰度特征,首先要將3通道的RGB圖形轉換為單通道的gray圖。這里可以借助python中的opencv工具包來實現這一轉換。采用的代碼如下:
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
其中,image是攝像機采集的RGB原始圖像(左圖),image_gray是轉換后的灰度圖像(右圖)。
(2)邊緣提取
完成RGB圖形和灰度圖的轉換之后,圖片在計算機中被看作是一組由像素值組成的矩陣,像素值是介於0~255之間的數據,其中0代表圖片中的純黑色,255代表純白色。但是,這樣的數據組不能被計算機理解,接下來需要進行圖形的邊緣提取。
對圖形進行邊緣提取的目的是找出圖形中的各物體的輪廓特征。本項目采用canny邊緣檢測技術來完成圖形的邊緣提取,該方法的主要流程圖如下:
其中,消除噪聲模塊主要用於去除圖形中的噪點,避免噪點被識別為輪廓;計算像素梯度為canny算法的核心,通過計算各處的梯度值來判讀圖形中顏色的變化;通過非極大值抑制則可以去除虛假的邊緣點,而只保留最可能成為邊緣的像素點;最后通過設置雙滯后閾值來去除偽邊緣,進一步增強車道檢測的准確性。 下圖是完成邊緣提取之后的圖形,可以看出圖形中的物體輪廓及線條都已經被刻畫出來,只保留線條的圖形看上去更加簡潔,也更容易被計算機識別。
(3)划定region of interest(roi)
雖然原圖像中的線條及物體輪廓已經描繪出來了,但是我們始終要記得項目的目的:檢測車道線!
也就是說,圖形中除了車道線以外的部分都不是我們感興趣的目標。在進行下一步之前,我們可以通過划定感興趣領域(roi)來簡化圖形。
這里問題來了:划定感興趣領域是必須的嗎?答案是否定的,即使我們不進行這一步操作依然能實現最終的目標,但是通過划定roi,我們可以大大縮減圖形中的線條數量,從而使接下來的計算量大大減小;此外,把無關內容拋棄掉之后,我們同樣也就減小了了計算機出現錯誤的可能性。
另外一個問題是:如何選擇roi?
這個問題其實是沒有固定答案的,例如在本項目中,我們假定汽車采集圖像用的攝像頭是安裝在汽車的正前方的,大部分情況下汽車在道路上行駛於兩條相鄰的車道線中間(除非需要變道行駛),因此我們可以認為只需在圖形的中間選定一塊區域即可。
roi的形狀不是固定的,根據實際需求的不同,可以選擇三角形、矩形框、圓形、橢圓形、不規則多邊形等等。這里為了簡單,采用了三角形。三角形在原圖中的位置可以參照下圖:
三角形的大小是可以調整的。
划定好roi之后,可以編寫rigion_of_interest函數,將roi之外的線條去掉,只保留roi內部的線條。由於攝像頭拍出來的車道線有透視效應,因此形狀類似於三角形,因此可以用三角形作為roi。最終得到的過濾后的圖形為:
可以看到圖形得到大大精簡,只保留了部分車道線,遠方物體的輪廓線沒有被保留。
(4)Hough變換
至此,汽車攝像機拍到的原始RGB圖形已經逐步被簡化為只保留車道線的圖形,看上去我們的工作已經完成。這里還有一個問題需要提及,計算機可以通過比較像素值識別圖形中的白色點,然后計算線條的斜率來繪制圖形。然而,如果存在一條車道線與水平方向垂直,那么這時候線條的斜率趨近於無窮大,這中特殊情況計算機是無法處理的。
為了解決這一問題,需要對圖片進行霍夫變換(Hough Transform)。霍夫變換的原理這里不再贅述,讀者可以在網上查閱相關資料。總之,霍夫變換主要解決車道線斜率無窮大的情況。在python的霍夫變換opencv模塊中,有函數可以直接完成圖形的霍夫變換,函數調用代碼如下:
img_hough = cv2.HoughLinesP(img, rho, theta, threshold, np.array([]), minLineLength,maxLineGap)
函數的參數中,img為需要進行霍夫變換的圖形;rho和theta分別為距離和角度的分辨率;閾值threshold控制將一條線判定為直線的最少點數;minLineLength為將一條線識別為直線的最低長度,即只有線條長度大於該值時才將線條標記出來;maxLineGap則為線段上兩點之間的最大閾值,如果兩點之間的距離大於該值,則認為兩點不屬於同一條直線。經過霍夫變換后,得到的圖形為:
由於在本例中不存在車道線斜率無窮大的情況,因此變換前后的圖形無差別。為了強行加以區分,這里把原來的白色改為了藍色。
(5)畫車道線
現在,圖形中的車道線已經被識別出來,接下來只需要將車道線刻畫在原圖上,以便於檢查識別效果即可。
可以看到,在原圖中的車道線已經被識別出來,且效果非常好!
由於划定了roi,遠處的車道線沒有別標記出來。但是不用擔心,汽車在行駛過程中,車道線是實時識別的,因此可以通過不斷更新將遠處的車道線識別出來。 為了進一步驗證使用該方法進行車道檢測的效果,我們重新選擇一張圖看看效果:
通過這一實例可以驗證,該方法可以比較完美的檢測車道線。(左右兩幅圖色調有所不同,這是由於圖像讀取函數造成的。)
**************************************** 分割線 ****************************************
>>>>>>視頻檢測
以上部分是對單張圖片進行車道檢測的方法。在實際應用中,需要在汽車行駛過程中進行實時車道檢測,從而避免汽車偏離車道。此時汽車攝像機采集到的信息往往以視頻的形式呈現,因此需要在視頻中識別出車道線。這聽上去好像有點復雜,不過我們完全不用擔心,因為攝像頭采集到的視頻本質上就是多張圖片的疊加,因此只需要對視頻進行分割,將單位時間內的視頻分為多張圖片的疊加,然后將每張圖片分別按照上述方法進行車道檢測,最后將結果再重新疊加為視頻即可。
當然,對於不懂視頻編輯的新手來說,怎樣才能將一段視頻分割為圖片,然后再合成視頻呢?這里不得不誇一下強大的python,python已經提前准備好了VideoFileClip工具,只需按規則調用相關函數,即可完成圖片的提取與視頻的合成。然后采用圖片車道線提取方法,對各張圖片進行處理即可。下面的代碼中,example1_output是在我的電腦上存儲處理后視頻的地址;clip1后面的地址是待處理的視頻的地址。
example1_output = '.../test_videos_output/solidWhiteRight1.mp4' clip1 = VideoFileClip(".../test_videos/solidWhiteRight.mp4") print clip1 example1_clip = clip1.fl_image(process_image) #NOTE: this function expects color images!! %time example1_clip.write_videofile(example1_output, audio=False)
>>>>>>視頻檢測結果展示
處理后的視頻為本文件夾中的solidWhiteRight_output.mp4文件。可以看到,視頻處理結果令人滿意,道路左右兩側的實線和虛線均被檢測出來。
**************************************** 分割線 ****************************************
本項目中還提供了另外一段由汽車前置攝像頭采集到的視頻,並命名為challenge.mp4(可在本文件夾中查找)。這段視頻中路面存在陰影及破損的情況,給道路線檢測帶來干擾。采用上述方法,對該視頻進行處理,處理結果可查看文件challenge——output.mp4。從視頻中可以看出,在第3-6秒鍾的時間區間內,道路線檢測結果非常不理想,主要表現為:
(1)錯誤的將樹枝陰影識別為道路線;
(2)將汽車前沿識別為道路線;
(3)在道路有破損的路面上,無法識別出道路線。這些問題會嚴重影響自動駕駛汽車的行駛,因此需要想辦法加以改進。
>>>>>>改進方法
(1)增大Canny函數的閾值
python中調用Canny函數需要輸入一大一小兩個閾值,當某一像素點的梯度值小於較小閾值時,該像素點被剔除;當梯度值大於較大閾值時,像素點被保留;梯度值位於兩個閾值之間時,另行考慮。因此,這里同時增大兩個閾值的值,可以將梯度較小的像素點去除,從而排除掉樹枝在地上的陰影(因為這些陰影的梯度值往往小於車道線的梯度值)。
(2)將黃線轉換為白線
在道路破損部分,車道線無法識別的原因是黃色的車道線與路面顏色對比不明顯,即車道線上的像素點梯度值不夠大導致被剔除。為了解決該問題,這里首先通過編寫函數將黃色的線轉換為白色的線,來提高車道線上像素點的梯度值,從而提高像素點被識別的概率。函數代碼如下:
def yellow_transform(image): image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) lower_yellow = np.array([40, 100, 20]) upper_yellow = np.array([100, 255, 255]) mask = cv2.inRange(image_hsv, lower_yellow, upper_yellow) return mask
>>>>>>結果驗證
改進之后,我們在文件名文challenge.mp4的視頻上進行驗證,為了使效果更加明顯,這里對原視頻進行處理,使其更暗從而使檢測出的車道線更容易在圖上觀察到。
最后的結果為本文件夾中命名為challenge_output1.mp4的文件。
可以看出,經過改進之后,原來被錯誤的識別為車道線的樹枝陰影大部分被忽略。在車道破損部分,有些車道線可以被檢測出來。雖然結果依然不算完美,仍然存在很多問題,但是與原來的結果相比已經產生明顯提升,因此可以認為我們的改進方法是有效的。但是改進之后的程序運算量會加大,運算時間也會延長,這就需要更快的設備來完成運算。這也是改進方法的弊端。
# coding:UTF-8 # importing some useful packages import matplotlib.pyplot as plt import matplotlib.image as mpimg import numpy as np import cv2 % matplotlib inline import math def region_of_interest(img, vertices): """ Applies an image mask. Only keeps the region of the image defined by the polygon formed from `vertices`. The rest of the image is set to black. `vertices` should be a numpy array of integer points. """ # defining a blank mask to start with mask = np.zeros_like(img) # defining a 3 channel or 1 channel color to fill the mask with depending on the input image if len(img.shape) > 2: channel_count = img.shape[2] # i.e. 3 or 4 depending on your image ignore_mask_color = (255,) * channel_count else: ignore_mask_color = 255 # filling pixels inside the polygon defined by "vertices" with the fill color cv2.fillPoly(mask, vertices, ignore_mask_color) # returning the image only where mask pixels are nonzero masked_image = cv2.bitwise_and(img, mask) return masked_image def draw_lines(img,lines,color,thickness): for line in lines: for x1,y1,x2,y2 in line: cv2.line(img,(x1,y1),(x2,y2),color,thickness) # 霍夫變換函數 def hough_lines(img): """ `img` should be the output of a Canny transform. Returns an image with hough lines drawn. """ rho = 2 theta = np.pi/180 threshold = 15 min_line_len = 60 max_line_gap = 30 lines = cv2.HoughLinesP(img, rho, theta, threshold, np.array([]), minLineLength=min_line_len, maxLineGap=max_line_gap) line_img = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8) draw_lines(line_img, lines, [255,0,0], 2) cv2.imshow('hough_image',line_img) return lines,line_img def Lane_finding(image): image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) image_blur = cv2.GaussianBlur(image_gray, (5,5), 0) low_threshold = 50 high_threshold = 150 image_canny = cv2.Canny(image_blur, low_threshold, high_threshold) cv2.imshow('image_canny', image_canny) # 圖像像素行數 rows = image_canny.shape[0] # 540行 # 圖像像素列數 cols = image_canny.shape[1] # 960列 left_bottom = [0, rows] # [0,540] right_bottom = [cols, rows] # [960,540] apex = [cols / 2, rows*0.6] # [480,310] vertices = np.array([[left_bottom, right_bottom, apex]], np.int32) roi_image = region_of_interest(image_canny, vertices) cv2.imshow('roi_image', roi_image) lines,hough_image = hough_lines(roi_image) # 將得到線段繪制在原始圖像上 line_image = np.copy(image) draw_lines(line_image, lines, [255, 0, 0], 2) # line_image = cv2.addWeighted(line_image, 0.8, hough_image, 1, 0) cv2.imshow('src', line_image) cv2.waitKey(0) return hough_image image = mpimg.imread("/Users/zhangzheng/Desktop/lane_lines_finding/test_images/solidYellowLeft.jpg") if __name__ == '__main__': Lane_finding(image)