在本次分享中,我將以優達學城(Udacity)無人駕駛工程師學位中提供的高級車道線檢測項目為例,介紹普適性更好,且更為魯棒的車道線檢測技術,用于處理那些無人駕駛中常見的(如路面顏色變化、路邊障礙物陰影等導致的)光線變化劇烈和彎道的場景。
按照慣例,在介紹計算機視覺技術前,我們先討論一下這次分享的輸入和輸出。
輸入
一個連續的視頻,視頻中的左車道線為黃色實線,右車道線為白色虛線。無人車會經過路面顏色突變、路邊樹木影子干擾、車道線不清晰和急轉彎的路況。
輸出
左、右車道線的三次曲線方程,及其有效距離。最后將車道線圍成的區域顯示在圖像上,如下圖所示。
輸入和輸出都定義清楚后,我們開始探討高級車道線檢測的算法,并對每幀視頻圖像中的車道線進行檢測。
攝像機標定
相信大家都多少聽說過魚眼相機,最常見的魚眼相機是輔助駕駛員倒車的后向攝像頭。也有很多攝影愛好者會使用魚眼相機拍攝圖像,最終會有高大上的大片效果,如下圖所示。
圖片來源:優達學城(Udacity)無人駕駛工程師課程
使用魚眼相機拍攝的圖像雖然高大上,但存在一個很大的問題——畸變(Distortion)。如上圖所示,走道上的欄桿應該是筆直延伸出去的。然而,欄桿在圖像上的成像卻是彎曲的,這就是圖像畸變,畸變會導致圖像失真。
使用車載攝像機拍攝出的圖像,雖然沒有魚眼相機的畸變這么夸張,但是畸變是客觀存在的,只是人眼難以察覺。使用有畸變的圖像做車道線的檢測,檢測結果的精度將會受到影響,因此進行圖像處理的第一步工作就是去畸變。
為了解決車載攝像機圖像的畸變問題,攝像機標定技術應運而生。
攝像機標定是通過對已知的形狀進行拍照,通過計算該形狀在真實世界中位置與在圖像中位置的偏差量(畸變系數),進而用這個偏差量去修正其他畸變圖像的技術。
原則上,可以選用任何的已知形狀去校準攝像機,不過業內的標定方法都是基于棋盤格的。因為它具備規則的、高對比度圖案,能非常方便地自動化檢測各個棋盤格的交點,十分適合標定攝像機的標定工作。如下圖所示為標準的10x7(7行10列)的棋盤格。
OpenCV庫為攝像機標定提供了函數cv2.findChessboardCorners(),它能自動地檢測棋盤格內4個棋盤格的交點(2白2黑的交接點)。我們只需要輸入攝像機拍攝的完整棋盤格圖像和交點在橫縱向上的數量即可。隨后我們可以使用函數cv2.drawChessboardCorners()繪制出檢測的結果。
棋盤格原圖如下所示:
圖片出處:https://github.com/udacity/CarND-Advanced-Lane-Lines/blob/master/camera_cal/calibration2.jpg
使用OpenCV自動交點檢測的結果如下:
獲取交點的檢測結果后,使用函數cv2.calibrateCamera()即可得到相機的畸變系數。
為了使攝像機標定得到的畸變系數更加準確,我們使用車載攝像機從不同的角度拍攝20張棋盤格,將所有的交點檢測結果保存,再進行畸變系數的的計算。
我們將讀入圖片、預處理圖片、檢測交點、標定相機的一系列操作,封裝成一個函數,如下所示:
# Step 1 : Calculate camera distortion coefficientsdef getCameraCalibrationCoefficients(chessboardname, nx, ny): # prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0) objp = np.zeros((ny * nx, 3), np.float32) objp[:,:2] = np.mgrid[0:nx, 0:ny].T.reshape(-1,2) # 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. images = glob.glob(chessboardname) if len(images) > 0: print("images num for calibration : ", len(images)) else: print("No image for calibration.") return ret_count = 0 for idx, fname in enumerate(images): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img_size = (img.shape[1], img.shape[0]) # Finde the chessboard corners ret, corners = cv2.findChessboardCorners(gray, (nx, ny), None) # If found, add object points, image points if ret == True: ret_count += 1 objpoints.append(objp) imgpoints.append(corners) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, img_size, None, None) print('Do calibration successfully') return ret, mtx, dist, rvecs, tvecs
需要標定的一系列圖片如下圖所示:
圖片出處:https://github.com/udacity/CarND-Advanced-Lane-Lines/tree/master/camera_cal
調用之前封裝好的函數,獲取畸變參數。
nx = 9 ny = 6 ret, mtx, dist, rvecs, tvecs = getCameraCalibrationCoefficients('camera_cal/calibration*.jpg', nx, ny)
隨后,使用OpenCV提供的函數cv2.undistort(),傳入剛剛計算得到的畸變參數,即可將畸變的圖像進行畸變修正處理。
# Step 2 : Undistort image def undistortImage(distortImage, mtx, dist): return cv2.undistort(distortImage, mtx, dist, None, mtx)
以畸變的棋盤格圖像為例,進行畸變修正處理
# Read distorted chessboard image test_distort_image = cv2.imread('./camera_cal/calibration4.jpg') # Do undistortion test_undistort_image = undistortImage(test_distort_image, mtx, dist)
畸變圖像如下圖所示:
圖像出處:https://github.com/udacity/CarND-Advanced-Lane-Lines/blob/master/camera_cal/calibration4.jpg
復原后的圖像如下圖所示:
同理,我們將攝像機拍攝到的實際路況進行畸變修正處理。
test_distort_image = cv2.imread('test_images/straight_lines1.jpg') # Do undistortion test_undistort_image = undistortImage(test_distort_image, mtx, dist)
原始畸變圖像如下所示:
圖片出處:https://github.com/udacity/CarND-Advanced-Lane-Lines/blob/master/test_images/straight_lines1.jpg
畸變修正后的圖像如下所示:
可以看到離鏡頭更近的左側、右側和下側的圖像比遠處的畸變修正更明顯。
篩選圖像
從我們作為輸入的視頻可以看出,車輛會經歷顛簸、車道線不清晰、路面顏色突變,路邊障礙物陰影干擾等復雜工況。因此,需要將這些復雜的場景篩選出來,確保后續的算法能夠在這些復雜場景中正確地檢測出車道線。
使用以下代碼將視頻中的圖像數據提取,進行畸變修正處理后,存儲在名為original_image的文件夾中,以供挑選。
video_input = 'project_video.mp4' cap = cv2.VideoCapture(video_input) count = 1 while(True): ret, image = cap.read() if ret: undistort_image = undistortImage(image, mtx, dist) cv2.imwrite('original_image/' + str(count) + '.jpg', undistort_image) count += 1 else: break cap.release()
在original_image文件夾中,挑選出以下6個場景進行檢測。這6個場景既包含了視頻中常見的正常直道、正常彎道工況,也包含了具有挑戰性的陰影、明暗劇烈變化的工況。如下圖所示:
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由暗到亮的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
如果后續的高級車道線檢測算法能夠完美處理以上六種工況,那將算法應用到視頻中,也會得到完美的車道線檢測效果。
透視變換
在完成圖像的畸變修正后,就要將注意力轉移到車道線。與《無人駕駛技術入門(十四)| 初識圖像之初級車道線檢測》中技術類似,這里需要定義一個感興趣區域。很顯然,我們的感興趣區域就是車輛正前方的這個車道。為了獲取感興趣區域,我們需要對自車正前方的道路使用一種叫做透視變換的技術。
“透視”是圖像成像時,物體距離攝像機越遠,看起來越小的一種現象。在真實世界中,左右互相平行的車道線,會在圖像的最遠處交匯成一個點。這個現象就是“透視成像”的原理造成的。
以立在路邊的交通標志牌為例,它在攝像機所拍攝的圖像中的成像結果一般如下下圖所示:
圖片來源:優達學城(Udacity)無人駕駛工程師課程
在這幅圖像上,原本應該是正八邊形的標志牌,成像成為一個不規則的八邊形。
通過使用透視變換技術,可以將不規則的八邊形投影成規則的正八邊形。應用透視變換后的結果對比如吐下:
圖片來源:優達學城(Udacity)無人駕駛工程師課程
透視變換的原理:首先新建一幅跟左圖同樣大小的右圖,隨后在做圖中選擇標志牌位于兩側的四個點(如圖中的紅點),記錄這4個點的坐標,我們稱這4個點為src_points。圖中的4個點組成的是一個平行四邊形。
由先驗知識可知,左圖中4個點所圍成的平行四邊形,在現實世界中是一個長方形,因此在右邊的圖中,選擇一個合適的位置,選擇一個長方形區域,這個長方形的4個端點一一對應著原圖中的src_points,我們稱新的這4個點為dst_points。
得到src_points,dst_points后,我們就可以使用OpenCV中計算投影矩陣的函數cv2.getPerspectiveTransform(src_points, dst_points)算出src_points到dst_points的投影矩陣和投影變換后的圖像了。
使用OpenCV庫實現透視變換的代碼如下:
# Step 3 : Warp image based on src_points and dst_points # The type of src_points & dst_points should be like # np.float32([ [0,0], [100,200], [200, 300], [300,400]]) def warpImage(image, src_points, dst_points): image_size = (image.shape[1], image.shape[0]) # rows = img.shape[0] 720 # cols = img.shape[1] 1280 M = cv2.getPerspectiveTransform(src, dst) Minv = cv2.getPerspectiveTransform(dst, src) warped_image = cv2.warpPerspective(image, M,image_size, flags=cv2.INTER_LINEAR) return warped_image, M, Minv
同理,對于畸變修正過的道路圖像,我們同樣使用相同的方法,將我們感興趣的區域做透視變換。
如下圖所示,我們選用一張在直線道路上行駛的圖像,沿著左右車道線的邊緣,選擇一個梯形區域,這個區域在真實的道路中應該是一個長方形,因此我們選擇將這個梯形區域投影成為一個長方形,在右圖橫坐標的合適位置設置長方形的4個端點。最終的投影結果就像“鳥瞰圖”一樣。
圖片出處:https://github.com/udacity/CarND-Advanced-Lane-Lines/tree/master/examples/warped_straight_lines.jpg
使用以下代碼,通過不斷調整src和dst的值,確保在直線道路上,能夠調試出滿意的透視變換圖像。
test_distort_image = cv2.imread('test_images/test4.jpg')# 畸變修正test_undistort_image = undistortImage(test_distort_image, mtx, dist)# 左圖梯形區域的四個端點src = np.float32([[580, 460], [700, 460], [1096, 720], [200, 720]])# 右圖矩形區域的四個端點dst = np.float32([[300, 0], [950, 0], [950, 720], [300, 720])test_warp_image, M, Minv = warpImage(test_undistort_image, src, dst)
最終,我們把篩選出的6幅圖統一應用調整好的src、dst做透視變換,結果如下:
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由暗到亮的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
可以看到,越靠圖片下方的圖像越清晰,越上方的圖像越模糊。這是因為越遠的地方,左圖中的像素點越少。而無論是遠處還是近處,需要在右圖中填充的像素點數量是一樣的。左圖近處有足夠多的點去填充右圖,而左圖遠處的點有限,只能通過插值的方式創造“假的”像素點進行填充,所以就不那么清晰了。
提取車道線
在《無人駕駛技術入門(十四)| 初識圖像之初級車道線檢測》中,我們介紹了通過Canny邊緣提取算法獲取車道線待選點的方法,隨后使用霍夫直線變換進行了車道線的檢測。在這里,我們也嘗試使用邊緣提取的方法進行車道線提取。
需要注意的是,Canny邊緣提取算法會將圖像中各個方向、明暗交替位置的邊緣都提取出來,很明顯,Canny邊緣提取算法在處理有樹木陰影的道路時,會將樹木影子的輪廓也提取出來,這是我們不愿意看到的。
因此我們選用Sobel邊緣提取算法。Sobel相比于Canny的優秀之處在于,它可以選擇橫向或縱向的邊緣進行提取。從投影變換后的圖像可以看出,我們關心的正是車道線在橫向上的邊緣突變。
封裝一下OpenCV提供的cv2.Sobel()函數,將進行邊緣提取后的圖像做二進制圖的轉化,即提取到邊緣的像素點顯示為白色(值為1),未提取到邊緣的像素點顯示為黑色(值為0)。
def absSobelThreshold(img, orient='x', thresh_min=30, thresh_max=100): # Convert to grayscale gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # Apply x or y gradient with the OpenCV Sobel() function # and take the absolute value if orient == 'x': abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 1, 0)) if orient == 'y': abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 0, 1)) # Rescale back to 8 bit integer scaled_sobel = np.uint8(255*abs_sobel/np.max(abs_sobel)) # Create a copy and apply the threshold binary_output = np.zeros_like(scaled_sobel) # Here I'm using inclusive (>=, <=) thresholds, but exclusive is ok too ? ?binary_output[(scaled_sobel >= thresh_min) & (scaled_sobel <= thresh_max)] = 1 ? ?# Return the result ? ?return binary_output
使用同一組閾值對以上6幅做過投影變換的圖像進行x方向的邊緣提取,可以得到如下結果:
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由暗到亮的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
由以上結果可以看出,在明暗交替明顯的路面上,如圖1和圖2,橫向的Sobel邊緣提取算法在提取車道線的表現上還不錯。不過一旦道路的明暗交替不那么明顯了,如圖3和圖4的白色路面區域,很難提取到有效的車道線待選點。當面對有樹木陰影覆蓋的區域時,如圖5和圖6,雖然能提取出車道線的大致輪廓,但會同時引入的噪聲,給后續處理帶來麻煩。
因此,橫向的Sobel邊緣提取算法,無法很好地處理路面陰影、明暗交替的道路工況。
無法使用邊緣提取的方法提取車道線后,我們開始將顏色空間作為突破口。
在以上6個場景中,雖然路面明暗交替,而且偶爾會有陰影覆蓋,但黃色和白色的車道線是一直都存在的。因此,我們如果能將圖中的黃色和白色分割出來,然后將兩種顏色組合在一幅圖上,就能夠得到一個比較好的處理結果。
一幅圖像除了用RGB(紅綠藍)三個顏色通道表示以外,還可以使用HSL(H色相、S飽和度、L亮度)和Lab(L亮度、a紅綠通道、b藍黃)模型來描述圖像,三通道的值與實際的成像顏色如下圖所示。
圖片出處:https://blog.csdn.net/wsp_1138886114/article/details/80660014
我們可以根據HSL模型中的L(亮度)通道來分割出圖像中的白色車道線,同時可以根據Lab模型中的b(藍黃)通道來分割出圖像中的黃色車道線,再將兩次的分割結果,去合集,疊加到一幅圖上,就能得到兩條完整的車道線了。
使用OpenCV提供的cv2.cvtColor()接口,將RGB通道的圖,轉為HLS通道的圖,隨后對L通道進行分割處理,提取圖像中白色的車道線。封裝成代碼如下:
def hlsLSelect(img, thresh=(220, 255)): hls = cv2.cvtColor(img, cv2.COLOR_BGR2HLS) l_channel = hls[:,:,1] l_channel = l_channel*(255/np.max(l_channel)) binary_output = np.zeros_like(l_channel) binary_output[(l_channel > thresh[0]) & (l_channel <= thresh[1])] = 1 ? ?return binary_output
使用同一組閾值對以上6種工況進行處理,處理結果如下圖所示。
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由暗到亮的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
使用OpenCV提供的cv2.cvtColor()接口,將RGB通道的圖,轉為Lab通道的圖,隨后對b通道進行分割處理,提取圖像中黃色的車道線。封裝成代碼如下:
def labBSelect(img, thresh=(195, 255)): # 1) Convert to LAB color space lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) lab_b = lab[:,:,2] # don't normalize if there are no yellows in the image if np.max(lab_b) > 100: lab_b = lab_b*(255/np.max(lab_b)) # 2) Apply a threshold to the L channel binary_output = np.zeros_like(lab_b) binary_output[((lab_b > thresh[0]) & (lab_b <= thresh[1]))] = 1 ? ?# 3) Return a binary image of threshold result ? ?return binary_output
使用同一組閾值對以上6種工況進行處理,處理結果如下圖所示。
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由暗到亮的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
根據以上試驗可知,L通道能夠較好地分割出圖像中的白色車道線,b通道能夠較好地分割出圖像中的黃色車道線。即使面對樹木陰影和路面顏色突變的場景,也能盡可能少地引入噪聲。
最后,我們使用以下代碼,將兩個通道分割的圖像合并
hlsL_binary = hlsLSelect(test_warp_image) labB_binary = labBSelect(test_warp_image) combined_binary = np.zeros_like(hlsL_binary) combined_binary[(hlsL_binary == 1) | (labB_binary == 1)] = 1
最終合并的效果如下圖所示:
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由亮到暗的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
以上僅僅是車道線提取的方法之一。除了可以通過HSL和Lab顏色通道,這種基于規則的方法,分割出車道線外,還可以使用基于深度學習的方法。它們目的都是為了能夠穩定地將車道線從圖像中分割出來。
檢測車道線
在檢測車道線前,需要粗定位車道線的位置。為了方便理解,這里引入一個概念——直方圖。
以下面這幅包含噪點的圖像為例,進行直方圖的介紹。
圖片出處:優達學城(Udacity)無人駕駛工程師學位
我們知道,我們處理的圖像的分辨率為1280*720,即720行,1280列。如果我將每一列的白色的點數量進行統計,即可得到1280個值。將這1280個值繪制在一個坐標系中,橫坐標為1-1280,縱坐標表示每列中白色點的數量,那么這幅圖就是“直方圖”,如下圖所示:
圖片出處:優達學城(Udacity)無人駕駛工程師學位
將兩幅圖疊加,效果如下:
圖片出處:優達學城(Udacity)無人駕駛工程師學位
找到直方圖左半邊最大值所對應的列數,即為左車道線所在的大致位置;找到直方圖右半邊最大值所對應的列數,即為右車道線所在的大致位置。
使用直方圖找左右車道線大致位置的代碼如下,其中leftx_base和rightx_base即為左右車道線所在列的大致位置。
# Take a histogram of the bottom half of the image histogram = np.sum(combined_binary[combined_binary.shape[0]//2:,:], axis=0) # Create an output image to draw on and visualize the result out_img = np.dstack((combined_binary, combined_binary, combined_binary)) # Find the peak of the left and right halves of the histogram # These will be the starting point for the left and right lines midpoint = np.int(histogram.shape[0]//2) leftx_base = np.argmax(histogram[:midpoint]) rightx_base = np.argmax(histogram[midpoint:]) + midpoint
確定了左右車道線的大致位置后,使用一種叫做“滑動窗口”的技術,在圖中對左右車道線的點進行搜索。先看一個介紹"滑動窗口"原理的視頻(視頻大小1.18M)。
滑動窗口原理
首先根據前面介紹的直方圖方法,找到左右車道線的大致位置,將這兩個大致位置作為起始點。定義一個矩形區域,稱之為“窗口”(圖中棕色的部分),分別以兩個起始點作為窗口的下邊線中點,存儲所有在方塊中的白色點的橫坐標。
隨后對存儲的橫坐標取均值,將該均值所在的列以及第一個”窗口“的上邊緣所在的位置,作為下一個“窗口”的下邊線中點,繼續搜索。
以此往復,直到把所有的行都搜索完畢
所有落在窗口(圖中棕色區域)中的白點,即為左右車道線的待選點,如下圖藍色和紅色所示。隨后將藍色點和紅色點做三次曲線擬合,即可得到車道線的曲線方程。
使用直方圖、滑動窗口檢測車道線的代碼如下:
# Step 5 : Detect lane lines through moving window def find_lane_pixels(binary_warped, nwindows, margin, minpix): # Take a histogram of the bottom half of the image histogram = np.sum(binary_warped[binary_warped.shape[0]//2:,:], axis=0) # Create an output image to draw on and visualize the result out_img = np.dstack((binary_warped, binary_warped, binary_warped)) # Find the peak of the left and right halves of the histogram # These will be the starting point for the left and right lines midpoint = np.int(histogram.shape[0]//2) leftx_base = np.argmax(histogram[:midpoint]) rightx_base = np.argmax(histogram[midpoint:]) + midpoint # Set height of windows - based on nwindows above and image shape window_height = np.int(binary_warped.shape[0]//nwindows) # Identify the x and y positions of all nonzero pixels in the image nonzero = binary_warped.nonzero() nonzeroy = np.array(nonzero[0]) nonzerox = np.array(nonzero[1]) # Current positions to be updated later for each window in nwindows leftx_current = leftx_base rightx_current = rightx_base # Create empty lists to receive left and right lane pixel indices left_lane_inds = [] right_lane_inds = [] # Step through the windows one by one for window in range(nwindows): # Identify window boundaries in x and y (and right and left) win_y_low = binary_warped.shape[0] - (window+1)*window_height win_y_high = binary_warped.shape[0] - window*window_height win_xleft_low = leftx_current - margin win_xleft_high = leftx_current + margin win_xright_low = rightx_current - margin win_xright_high = rightx_current + margin # Draw the windows on the visualization image cv2.rectangle(out_img,(win_xleft_low,win_y_low), (win_xleft_high,win_y_high),(0,255,0), 2) cv2.rectangle(out_img,(win_xright_low,win_y_low), (win_xright_high,win_y_high),(0,255,0), 2) # Identify the nonzero pixels in x and y within the window # good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & ? ? ? ?(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0] ? ? ? ?good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) & ? ? ? ?(nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0] ? ? ? ? ? ? ? ?# Append these indices to the lists ? ? ? ?left_lane_inds.append(good_left_inds) ? ? ? ?right_lane_inds.append(good_right_inds) ? ? ? ? ? ? ? ?# If you found > minpix pixels, recenter next window on their mean position if len(good_left_inds) > minpix: leftx_current = np.int(np.mean(nonzerox[good_left_inds])) if len(good_right_inds) > minpix: rightx_current = np.int(np.mean(nonzerox[good_right_inds])) # Concatenate the arrays of indices (previously was a list of lists of pixels) try: left_lane_inds = np.concatenate(left_lane_inds) right_lane_inds = np.concatenate(right_lane_inds) except ValueError: # Avoids an error if the above is not implemented fully pass # Extract left and right line pixel positions leftx = nonzerox[left_lane_inds] lefty = nonzeroy[left_lane_inds] rightx = nonzerox[right_lane_inds] righty = nonzeroy[right_lane_inds] return leftx, lefty, rightx, righty, out_img def fit_polynomial(binary_warped, nwindows=9, margin=100, minpix=50): # Find our lane pixels first leftx, lefty, rightx, righty, out_img = find_lane_pixels( binary_warped, nwindows, margin, minpix) # Fit a second order polynomial to each using `np.polyfit` left_fit = np.polyfit(lefty, leftx, 2) right_fit = np.polyfit(righty, rightx, 2) # Generate x and y values for plotting ploty = np.linspace(0, binary_warped.shape[0]-1, binary_warped.shape[0] ) try: 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] except TypeError: # Avoids an error if `left` and `right_fit` are still none or incorrect print('The function failed to fit a line!') left_fitx = 1*ploty**2 + 1*ploty right_fitx = 1*ploty**2 + 1*ploty ## Visualization ## # Colors in the left and right lane regions out_img[lefty, leftx] = [255, 0, 0] out_img[righty, rightx] = [0, 0, 255] # Plots the left and right polynomials on the lane lines #plt.plot(left_fitx, ploty, color='yellow') #plt.plot(right_fitx, ploty, color='yellow') return out_img, left_fit, right_fit, ploty
對以上6種工況進行車道線檢測,處理結果如下圖所示。
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由暗到亮的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
跟蹤車道線
視頻數據是連續的圖片,基于連續兩幀圖像中的車道線不會突變的先驗知識,我們可以使用上一幀檢測到的車道線結果,作為下一幀圖像處理的輸入,搜索上一幀車道線檢測結果附近的點,這樣不僅可以減少計算量,而且得到的車道線結果也更穩定,如下圖所示。
圖片出處:優達學城(Udacity)無人駕駛工程師學位
圖中的細黃線為上一幀檢測到的車道線結果,綠色陰影區域為細黃線橫向擴展的一個區域,通過搜索該區域內的白點坐標,即可快速確定當前幀中左右車道線的待選點。
使用上一幀的車道線檢測結果進行車道線跟蹤的代碼如下:
# Step 6 : Track lane lines based the latest lane line result def fit_poly(img_shape, leftx, lefty, rightx, righty): ### TO-DO: Fit a second order polynomial to each with np.polyfit() ### left_fit = np.polyfit(lefty, leftx, 2) right_fit = np.polyfit(righty, rightx, 2) # Generate x and y values for plotting ploty = np.linspace(0, img_shape[0]-1, img_shape[0]) ### TO-DO: Calc both polynomials using ploty, left_fit and right_fit ### 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] return left_fitx, right_fitx, ploty, left_fit, right_fit def search_around_poly(binary_warped, left_fit, right_fit): # HYPERPARAMETER # Choose the width of the margin around the previous polynomial to search # The quiz grader expects 100 here, but feel free to tune on your own! margin = 60 # Grab activated pixels nonzero = binary_warped.nonzero() nonzeroy = np.array(nonzero[0]) nonzerox = np.array(nonzero[1]) ### TO-DO: Set the area of search based on activated x-values ### ### within the +/- margin of our polynomial function ### ### Hint: consider the window areas for the similarly named variables ### ### in the previous quiz, but change the windows to our new search area ### left_lane_inds = ((nonzerox > (left_fit[0]*(nonzeroy**2) + left_fit[1]*nonzeroy + left_fit[2] - margin)) & (nonzerox < (left_fit[0]*(nonzeroy**2) + ? ? ? ? ? ? ? ? ? ?left_fit[1]*nonzeroy + left_fit[2] + margin))) ? ?right_lane_inds = ((nonzerox > (right_fit[0]*(nonzeroy**2) + right_fit[1]*nonzeroy + right_fit[2] - margin)) & (nonzerox < (right_fit[0]*(nonzeroy**2) + ? ? ? ? ? ? ? ? ? ?right_fit[1]*nonzeroy + right_fit[2] + margin))) ? ? ? ?# Again, extract left and right line pixel positions ? ?leftx = nonzerox[left_lane_inds] ? ?lefty = nonzeroy[left_lane_inds] ? ?rightx = nonzerox[right_lane_inds] ? ?righty = nonzeroy[right_lane_inds] ? ?# Fit new polynomials ? ?left_fitx, right_fitx, ploty, left_fit, right_fit = fit_poly(binary_warped.shape, leftx, lefty, rightx, righty) ? ? ? ?## Visualization ## ? ?# Create an image to draw on and an image to show the selection window ? ?out_img = np.dstack((binary_warped, binary_warped, binary_warped))*255 ? ?window_img = np.zeros_like(out_img) ? ?# Color in left and right line pixels ? ?out_img[nonzeroy[left_lane_inds], nonzerox[left_lane_inds]] = [255, 0, 0] ? ?out_img[nonzeroy[right_lane_inds], nonzerox[right_lane_inds]] = [0, 0, 255] ? ?# Generate a polygon to illustrate the search window area ? ?# And recast the x and y points into usable format for cv2.fillPoly() ? ?left_line_window1 = np.array([np.transpose(np.vstack([left_fitx-margin, ploty]))]) ? ?left_line_window2 = np.array([np.flipud(np.transpose(np.vstack([left_fitx+margin, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?ploty])))]) ? ?left_line_pts = np.hstack((left_line_window1, left_line_window2)) ? ?right_line_window1 = np.array([np.transpose(np.vstack([right_fitx-margin, ploty]))]) ? ?right_line_window2 = np.array([np.flipud(np.transpose(np.vstack([right_fitx+margin, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?ploty])))]) ? ?right_line_pts = np.hstack((right_line_window1, right_line_window2)) ? ?# Draw the lane onto the warped blank image ? ?cv2.fillPoly(window_img, np.int_([left_line_pts]), (0,255, 0)) ? ?cv2.fillPoly(window_img, np.int_([right_line_pts]), (0,255, 0)) ? ?result = cv2.addWeighted(out_img, 1, window_img, 0.3, 0) ? ? ? ?# Plot the polynomial lines onto the image ? ?#plt.plot(left_fitx, ploty, color='yellow') ? ?#plt.plot(right_fitx, ploty, color='yellow') ? ?## End visualization steps ## ? ? ? ?return result, left_fit, right_fit, ploty
對以上6種工況進行車道線跟蹤,處理結果如下圖所示。
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由暗到亮的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
以上,我們就完成了在透視變換結果上的車道線檢測和跟蹤。
逆投影到原圖
我們在計算透視變換矩陣時計算了兩個矩陣M和Minv,使用M能夠實現透視變換,使用Minv能夠實現逆透視變換。
M = cv2.getPerspectiveTransform(src, dst) Minv = cv2.getPerspectiveTransform(dst, src)
我們將兩條車道線所圍成的區域涂成綠色,并將結果繪制在“鳥瞰圖”上后,使用逆透視變換矩陣反投到原圖上,即可實現在原圖上的可視化效果。代碼如下:
# Step 7 : Draw lane line result on undistorted image def drawing(undist, bin_warped, color_warp, left_fitx, right_fitx): # Create an image to draw the lines on warp_zero = np.zeros_like(bin_warped).astype(np.uint8) color_warp = np.dstack((warp_zero, warp_zero, warp_zero)) # Recast the x and y points into usable format for cv2.fillPoly() pts_left = np.array([np.transpose(np.vstack([left_fitx, ploty]))]) pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))]) pts = np.hstack((pts_left, pts_right)) # Draw the lane onto the warped blank image cv2.fillPoly(color_warp, np.int_([pts]), (0,255, 0)) # Warp the blank back to original image space using inverse perspective matrix (Minv) newwarp = cv2.warpPerspective(color_warp, Minv, (undist.shape[1], undist.shape[0])) # Combine the result with the original image result = cv2.addWeighted(undist, 1, newwarp, 0.3, 0) return result
以上6個場景的左右車道線繪制結果如下所示:
無陰影、顏色無明暗變換的直道
無陰影、顏色無明暗變換的彎道
有小面積陰影、顏色由暗到亮的直道
無陰影、道路標志線不清晰的彎道
有大面積陰影、顏色由暗到亮的彎道
有大面積陰影、顏色由亮到暗的彎道
處理視頻
在一步步完成攝像機標定、圖像畸變校正、透視變換、提取車道線、檢測車道線、跟蹤車道線后,我們在圖像上實現了復雜環境下的車道線檢測算法。現在我們將視頻轉化為圖片,然后一幀幀地對視頻數據進行處理,然后將車道線檢測結果存為另一段視頻。
高級車道線檢測算法效果
視頻中左上角出現的道路曲率和車道偏離量的計算都是獲取車道線曲線方程后的具體應用,這里不做詳細討論。
結語
以上就是《再識圖像之高級車道線檢測》的全部內容,本次分享中介紹的攝像機標定、投影變換、顏色通道、滑動窗口等技術,在計算機視覺領域均得到了廣泛應用。
處理復雜道路場景下的視頻數據是一項及其艱巨的任務。僅以提取車道線的過程為例,使用設定規則的方式提取車道線,雖然能夠處理項目視頻中的場景,但面對變化更為惡劣的場景時,還是無能為力。現階段解決該問題的方法就是通過深度學習的方法,拿足夠多的標注數據去訓練模型,才能盡可能多地達到穩定的檢測效果。
-
無人駕駛
+關注
關注
98文章
4054瀏覽量
120448
原文標題:無人駕駛技術入門 —— 再識圖像之高級車道線檢測
文章出處:【微信號:IV_Technology,微信公眾號:智車科技】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論