汽車在道路上行駛需要遵循一定的行駛規則,路面的車道則起到規範汽車行駛規則的作用。車道的種類有很多種,如單行線、雙行線,虛線、網格線等,不同顔色、形狀的車道線代表着不同的行駛規則,汽車和行人可以根據這些規則來使用道路,避免沖突。是以,準确檢測并識别車道類型,并按照相應規則正确行駛,是汽車實作自動駕駛的基礎。
優達學城的自動駕駛項目課程包含了一個車道線檢測項目,其主要目的就是教給無人車如何檢測并識别車道,本文檔将該項目内容進行總結整理。
車道線檢測方法主要分為兩類:(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)