| 本文詳細描述了當前代碼中(git 版本: 16b521b12d2e3bdc00bd996acafe4526f1d1cb9a)道路識別的算法。 如果沒有特殊說明,下文中所說的“算法”均指本代碼中的道路識別算法。 目標本算法的目標是識別出道路上較為清晰的道路標線,并給出道路標線的位置信息。 算法簡要流程
 詳細流程為了結合代碼進行說明,將使用代碼導讀的方式慢慢分析算法。由于代碼仍在開發(fā)中,變動頻繁,建議你檢出 tag 為 lane-detection-code-tour 的代碼進行對照閱讀。 選取 ROImain() 函數位于 driveassist.cpp 文件中,該文件的開始部分定義了一些用于測試的視頻的參數。由于該版本的代碼僅做研究用,所以將測試視頻文件的地址寫死在了代碼中。 roiX、roiY、roiWidth、roiHeight 就是 ROI 的參數,由于車道線僅僅會出現在視頻畫面中的固定區(qū)域,所以我們可以選取這塊固定的區(qū)域作為 ROI,這樣可以加快處理的速度,也可以避開環(huán)境的干擾。 ROI 參數根據不同的視頻需要自行調節(jié)。 進行俯視變換從視頻畫面中來看,車道線是傾斜而且不平行的,但如果我們從空中俯視車道線,應該能看到平行的車道線。平行的車道線比不平行的車道線更容易處理。另外,當我們把 ROI 變換成 IPM 后,在直行的道路上,車道線會變成垂直的線,這就能夠讓我們更容易地識別出車道線。 要嚴格地進行俯視變換,需要考慮到攝像頭高度、傾角、仰角、光圈大小等不容易測量的量,但我們不需要進行精確的變換,我們只需要做一個簡單的梯形變形,就能夠得到一個俯視圖。雖然這樣做得到的俯視圖并不精確,但對車道線識別來說已經夠用了。 由于攝像頭安裝位置的不同,我們需要根據實際畫面調整一些參數,以便進行俯視變換,我們將這個過程叫做標定。下面介紹標定的過程。 首先運行程序,選擇一個車輛保持在車道線中央,且當前車道是直道的場景,接著開始進行標定。 請看下圖,圖片底部中央的綠色細線的矩形就是 ROI,舉行中央的兩條綠色細線是用于調整俯視變換參數的輔助線。 標定方法如下: 
 標定完成之后,可以將這些參數記下來,或者直接改寫代碼中預設的參數,這樣在下次運行程序時,就不需要重新進行標定了。 標定原理是這樣的,畫面中的車道線和 ROI 的上下兩條邊線組成了一個梯形,我們只要把這個梯形拉伸成一個矩形,就可以完成俯視變換。嚴格來說,這并不是一個標準的俯視變換,但我們只需要將車道線變換成平行的且垂直于地平線的兩條線,這樣的變換對我們來說已經足夠了。 如下圖所示,我們需要將 E、F 點分別拉伸到 A、B 兩點,C 和 D 點保持不動,也就是說,我們的變換是這樣的: 
 將這四個點的坐標傳給 OpenCV 提供的 getPerspectiveTransform 方法,我們就得到了我們想要的俯視變換的變換矩陣。同時,我們可以求這個矩陣的偽逆矩陣,這樣我們就可以將俯視圖轉換為 ROI 圖。 下面是一個 ROI 轉換成俯視變換的對比圖,可以看出車道線已經變成了兩條平行線,并且在屏幕上是垂直的。 車道線增強現在我們已經獲得了姿態(tài)比較好的車道線(車道線平行,且?guī)缀跏谴怪钡模瑸榱诉M一步突出車道線,我們要使用一個特殊的高斯核函對圖像進行卷積。 這個高斯核的數學表達是這樣的: 這個高斯核的水平和垂直方向看起來是這樣的( 和 均取 5): 兩個高斯核合并起來之后,看起來就是這個樣子的: 可以看到,這個高斯核在水平方向增強了中央的響應,弱化了周圍的相應,同時,在垂直方向拉伸了響應。用這個高斯核對圖像進行卷積后,就可以增強車道線的響應。 一般取車道線寬度在圖像上占據的像素個數,而 一般取虛線車道線的長度在圖像上占據的像素個數。 在代碼中,高斯核的計算在 onGaussianChange 函數中完成。首先分別計算水平方向和垂直方向的卷積核,然后調用 OpenCV 提供的 sepFilter2D 方法,分別傳入水平和垂直方向的卷積核,就可以得到最終的卷積結果。 完成卷積操作后,車道線會在圖片上顯現出最強的響應,這時候我們要做一個閾值化操作,將車道線過濾出來,把無關的背景(如路面紋理等)消除。 閾值化的操作很簡單,計算圖像中所有像素值的分布,確定一個閾值,然后保留所有像素值大于閾值的點,刪除所有像素值小于閾值的點。在本代碼中,我們使用了 97.5% 作為閾值,也就是取像素值分布中位于 97.5% 這個位置的像素值作為閾值。實際的閾值在每一幀中都不相同,而 97.5% 是固定的,需要根據每一幀的像素值分布來確定實際使用的閾值。 車道線經過增強后的效果如下: 使用簡化的霍夫變換識別車道線霍夫變換(Hough Transform)是一種識別圖像中直線的方法。標準的霍夫變換會識別出圖片中所有方向的直線,但耗時相對較長。對我們來說,我們只需要識別出幾乎是垂直的車道線,對于其他類型的直線我們不關注。這樣,我們可以極大地簡化霍夫變換,提高識別速度。 本代碼使用的簡化過的霍夫變換流程如下: 在閾值化后的圖片中,按列掃描圖像,對于每一列,計算該列上值不為 0 的像素點的個數。最后,我們可以得到一個函數 ,其中 是圖像的 x 坐標(也就是圖像的第幾列),函數值是 x 列上不為零的像素點的個數。 顯然,如果在 列上存在一條接近垂直的直線,那么 的值就應該很大。事實上,由于圖像經過閾值化處理,所以 應該是一個極大值。簡單來說,如果我們把 的圖像畫出來,那么圖像的波峰處應該存在一條直線。 在下圖中, 被繪制到了“二維高斯模糊”圖中,可以看到, 的波峰處確實是有一條幾乎垂直的直線。這些識別出來的直線,就是我們想要尋找的車道線的候選。 上圖中的 是經過處理的。原始的 函數在直線附近會出現多個波峰,這就會導致一條直線被識別為多條直線,所以,我們需要對 進行一些預處理后,再去尋找波峰(極大值)。 首先我們要對 做一個高斯模糊,這就可以合并大部分的極大值。高斯模糊的范圍應該根據車道線的寬度來確認。在本代碼中我們沒有進行進一步的研究,僅僅依靠實驗來確定了一個范圍。 完成高斯模糊后,一些相距相對較遠的極值點仍無法被合并(如雙實線類型的車道線,我們只希望將雙實線識別為一條車道線),所以我們仍需要手動對這些極值點進行合并,合并方法如下所述: 確定一個領域范圍 a,對距離小于 a 的兩個極值點進行合并。如果兩個極值點的位置分別為 和 ,那么合并后的極值點位置 應為: 其實這是一個按照 的值作為權重,對兩個極值點的位置進行加權求和的過程。對于兩個相鄰的極值點,合并后的極值點位置將會更偏向 值更大的那個極值點。 上面給出的公式是在代碼中使用的公式,如果改寫成下面的樣子,會更容易看出這個公式的加權思想: 其中 合并極值點的操作到此完成。合并后的極值點就可以認為是候選的車道線。接下來,就可以使用這些候選的車道線,結合圖像信息,擬合出實際的車道線。 以上合并極值點以及尋找極值點的代碼實現,位于 findPeaks 函數中。 擬合實際的車道線一般來說,車道線應該總是位于車輛的兩側,所以在俯視圖中,車道線應該位于圖片中央的兩側。在上一步中,我們已經得到了候選車道線在俯視圖中的 x 坐標,我們可以選取距離圖像中央最近的左右兩個候選車道線作為實際車道線的位置。利用這兩條車道線,結合圖像信息,可以擬合出實際的車道線。 擬合方法有很多種,可以大致分為曲線擬合和直線擬合兩類。在 github 的提交記錄中,你可以發(fā)現我們嘗試過曲線擬合,但最后實在沒有找到能夠比較好地擬合車道的曲線,所以現在的代碼中只使用了直線擬合。 直線擬合相對于曲線擬合的優(yōu)點是算法簡單,計算速度稍快。不過直線擬合的缺點也是比較明顯的:在彎道中,俯視圖中的車道線其實是一條曲線,而直線擬合是無法擬合曲線的。 直線擬合無法擬合彎道上的車道線,這個問題倒不是很嚴重。目前我們做車道識別的目的是為了實現車道保持功能,我們只要保證擬合出來的直線車道方向及位置大致上符合實際車道的方向和位置即可。 在現在的代碼中,存在幾種直線擬合扯到的方法,其中大部分被注釋掉了,只留下了一個名為“高級直線擬合道路”的方法。下面會詳細介紹這個算法。 擬合道路的方法位于 LineFit 類中,這個類接受的輸入是俯視圖像和候選車道線位置(在上一步中我們選出來的兩個極值點)。 對于每一個輸入的車道線位置 ,我們做如下計算: 
 很顯然,這樣選取出來的車道線最有可能是實際的車道線。 在代碼的實現上,有一些細節(jié)需要注意。計算直線的分數時,需要確定直線經過的像素點,直線經過哪些像素點,是這樣計算出來的: 取 ,其中 是圖像高度, 是整數。對于所有的 ,利用直線的表達式計算 ,于是直線經過的點就是 。最后,我們可以得到 個點。 按此方法選取直線經過的點,可以保證所有直線經過的點的數量都是固定的,最后計算出來的直線分數才具有可比性。 卡爾曼濾波實際的路面上,不可能總是有清晰無比的道路標線,破爛不堪模糊不清的道路標線是很常見的。對于此類標線,即使是人類都不太容易分別出來,以目前的技術水平就更不可能精確識別了。 我們希望,在無法識別車道線的時候,也能做出一個對于當前實際車道線位置的猜測,為實現這個目標,本代碼在最后使用了一個卡爾曼濾波器對識別出來的車道線進行濾波。 對識別出來的車道線進行濾波的好處有兩個,第一個好處是,在無法識別出車道線的時,也能給出車道線可能的位置。目前只能給出一個可能的位置,而無法給出車道線位于該位置上的概率,不過,可以在此基礎上繼續(xù)改進算法,使算法可以輸出一個概率,上層的自動駕駛程序可以根據此概率進行更好的決策。 第二個好處是,可以識別出來的車道線位置進行平滑。路面的實際狀況千變萬化,在進行實際的識別任務時,可能在某幀畫面上無法識別出車道線,但該幀之前及之后的幀上都能較好地識別出車道線。另外,由于復雜的環(huán)境的干擾,識別出來的車道線位置可能會在小范圍內擺動。對車道線進行濾波后,我們就可以得到一個足夠穩(wěn)定的車道線預測位置。 本代碼使用車道線的坐標點位置作為卡爾曼濾波器的測量變量。對于在一個圖像中識別出來的車道線,車道線應該在直線 y = 0 以及 y = h - 1 (h 是圖像高度)上分別經過點 和點 (. 和 就是我們輸入給卡爾曼濾波器的預測變量。由于一共有兩條車道線(左車道線和右車道線),所以我們一共有 4 個變量。 之所以使用兩個點的 x 坐標來表示車道線,而不使用截距和斜率來表示車道線的原因是:卡爾曼濾波器是一個線性濾波器,它不能處理非線性變化的變量。如果使用斜率和截距來表示車道線,圖像上車道線的斜率 k 與實際車道線的位置并不呈現線性變化關系。當然 與車道線的位置是呈現線性相關的,如果用 代替斜率,就可以使用卡爾曼濾波器。另外,在我們的處理過程中,使用車道線兩端點的水平坐標來表示車道線的變化顯然更直觀。 在這里,我們需要建立一個假設:在大部分時間下,車輛應該行駛在車道中央,且車道是直道。當車輛位置及道路條件滿足這個假設的時候,車輛線在圖像上的位置應該是固定的,我們把此時的車道線稱為理想車道線,理想車道線的位置則作為無法檢測到車道時,傳遞給卡爾曼濾波器的測量變量。 我們的卡爾曼濾波過程如下: 對左右兩條車道線,分別進行以下步驟:  在代碼的實現上,我們并沒有將兩條車道線分別傳給兩個卡爾曼濾波器,我們直接把兩條車道線的四個位置傳給了一個卡爾曼濾波器,這樣做的效果和上述步驟是一樣的。 本代碼中使用的過程噪聲協(xié)方差矩陣的對角線元素都是 ,這是一個經驗值。 測量噪聲矩陣是: 這也是一個經驗值,這里需要解釋一下為什么右側車道的測量誤差比左車道大。在我們用于測試的視頻中,車輛的左車道線是雙實線,而右側是虛線,雙實線顯然比虛線更容易檢測,這就是左車道線的測量誤差小于右車道線的原因。 需要指出的是,代碼中使用的這些經驗值,僅僅是根據很少的幾個測試視頻測試得到的經驗值。如果要用于更一般的情形,這些經驗值應該進行修改。最好的做法是記錄實際車道線與預測車道線的偏差,然后根據記錄的數據計算出協(xié)方差矩陣,不過這樣做的工作量太大,更簡單迅速但不夠嚴格的做法就是根據測試情況選取經驗值。 這里給出一個演示視頻:帶卡爾曼濾波的道路標線識別——一般城市道路。在視頻中可以看到,沒有經過卡爾曼濾波的車道線存在小范圍跳動的情況,而濾波后的車道線位置則更為平滑。如果車道線檢測難度增大,算法就會更多地使用理想車道線的位置(算法給出的車道線位置逐漸偏向理想車道線)。在完全無法檢測到車道線時,算法會直接使用理想車道線的位置作為預測車道線。 結束語整個車道檢測算法的介紹到這里就結束了。由于筆者水平有限,文中難免出現謬誤,還請多多拍磚。 | 
|  |