斑馬線檢測 基於OpenCVgit
效果不是很好
設置DEBUG變量爲True時會輸出每一步圖像用於逐幀debug和調參(按下任意鍵或者按住不放下一步),設爲False則只畫最後結果圖。
紅色方框是判斷爲斑馬線的滑窗,紫色方框是最終輸出的斑馬線位置(紫框計算目前默認了圖內只會出現一條斑馬線)
github
github: https://github.com/TomMao23/ZebraCrossing_Detection算法
基本思路
總結斑馬線的四個特徵:
梯度一致性
等間隔
多根線
斑馬線比車道線寬
梯度一致性和等間隔是兩個強分類特徵,其中梯度一致性特徵易召回,等間隔特徵強精度。目前算法未使用等間隔特徵,主要利用梯度一致,另外兩個特徵輔助。
ide
轉逆透視圖, 中值濾波,開運算,閉運算預處理後Canny邊緣檢測,對邊緣檢測圖用sobel求橫縱梯度,獲得梯度的模值和方向(方向歸爲0到90度, 即不區分正反),先過濾梯度過小的點。在滑窗內統計0:70度間隔5度的點直方圖,取峯值方向的點數做爲判斷量,高於閾值判爲斑馬線。學習
問題及當前解決思路
- 過濾減速帶:減速帶一樣具備梯度一致性,等間隔和線多的三個特徵,可是有線短的特色,因此滑窗高度取得越大減速帶越容易過濾,這是永遠管用的。另外一方面, 減速帶是黃色(紅+綠)黑色相間的,因此單使用藍色通道而不是正常RGB轉成的灰度圖能夠大大衰減減速帶的梯度影響。
- 開運算有兩個做用:可以減小噪點,能夠利用斑馬線比車道線更寬的特色用多iterations參數的開運算在保留斑馬線的同時直接去除車道線干擾(缺點:若斑馬線有磨損開運算會有必定損失,灰度圖開運算可能會產生弱的虛假邊緣,能夠調參不檢測出來)。
- 閉運算有兩個做用:一是可以填補線磨損加強斑馬線的邊緣識別效果, 二是路面箭頭破損時容易產生少許梯度雜亂的邊緣增長誤檢的機率(若是斑馬線無破損,閾值能夠設得很高就沒有這個問題)。即便無破損閉運算仍然很好地減小了其餘元素的虛假邊緣。
- 中值濾波的重要性:比起均值濾波等濾波器,一樣是平滑的效果,並提高邊緣檢測效果,中值濾波保留了原始梯度大小更利於邊緣的檢出,同時能夠完美地消除人行道的小磚縫紋理,因此中值濾波是必選。
- canny的重要性:試過直接用sobel x,y來作可是:
- 噪點虛假邊緣較多。
- 一個邊緣多個點,斑馬線線多的特徵被衰弱。
- 這麼作只有一個梯度閾值判斷過濾邊緣,以後再根據梯度方向過濾,canny兩個閾值並考慮連通性解決了前兩個問題而且調參更方便,好效果的參數的範圍也更大。因此用canny。
- 過濾中止線:因爲當前方法利用線多特徵,是在寬大於高的矩形滑窗內統計峯值梯度方向的點數(縱線斜線天然是線越多這樣統計的點越多),直行時的中止線剛好在滑窗內且是橫線峯值梯度方向點也會較多,因此統計時只取到0到70度(x軸方向梯度爲0度),當轉彎或斜行時中止線在滑窗內峯值梯度方向點很少,天然過濾。
- 主要干擾:中止線和減速帶的干擾很小,容易調節閾值, 不成問題。路面箭頭等標誌是目前主要干擾,由於斑馬線有缺損綜合考慮召回不能把閾值調得很是高(目前閾值是1500,完好損的清晰斑馬線值會在4000到6000),當前閾值在測試圖上無誤檢但有風險(部分箭頭圖最高值達到1300), 如果完好損的清晰的斑馬線能夠把閾值調高解決這個問題。可能干擾:路面外邊緣可能形成干擾,目前無,若出現此干擾能夠調整滑窗位置大小盡量不掃路面外解決這個問題。
結果示例
代碼示例
//處理逆透視後的圖 #coding=utf-8 import time import os import cv2 import numpy as np from numpy.linalg import inv def sliding_window(img1, img2, patch_size=(100,302), istep=50):#, jstep=1, scale=1.0): """ get patches and thier upper left corner coordinates The size of the sliding window is currently fixed. patch_size: sliding_window's size' istep: Row stride """ Ni, Nj = (int(s) for s in patch_size) for i in range(0, img1.shape[0] - Ni+1, istep): #for j in range(0, img1.shape[1] - Nj, jstep): #patch = (img1[i:i + Ni, j:j + Nj], img2[i:i + Ni, j:j + Nj]) patch = (img1[i:i + Ni, 39:341], img2[i:i + Ni, 39:341]) yield (i, 39), patch def predict(patches, DEBUG): """ predict zebra crossing for every patches 1 is zc 0 is background """ #print(len(patches)) labels = np.zeros(len(patches)) index = 0 for Amplitude, theta in patches: mask = (Amplitude>25).astype(np.float32) h, b = np.histogram(theta[mask.astype(np.bool)], bins=range(0,80,5)) low, high = b[h.argmax()], b[h.argmax()+1] newmask = ((Amplitude>25) * (theta<=high) * (theta>=low)).astype(np.float32) value = ((Amplitude*newmask)>0).sum() if value > 1500: labels[index] = 1 index += 1 if(DEBUG): print(h) print(low, high) print(value) cv2.imshow("newAmplitude", Amplitude*newmask) cv2.waitKey(0) return labels def preprocessing(img): """ Take the blue channel of the original image and filter it smoothly """ kernel1 = np.ones((3,3),np.uint8) kernel2 = np.ones((5,5),np.uint8) gray = img[:,:,0] gray = cv2.medianBlur(gray,5) gray = cv2.morphologyEx(gray, cv2.MORPH_OPEN, kernel1,iterations=4) gray = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel2,iterations=3) return gray def getGD(canny): """ return gradient mod and direction """ sobelx=cv2.Sobel(canny,cv2.CV_32F,1,0,ksize=3) sobely=cv2.Sobel(canny,cv2.CV_32F,0,1,ksize=3) theta = np.arctan(np.abs(sobely/(sobelx+1e-10)))*180/np.pi Amplitude = np.sqrt(sobelx**2+sobely**2) mask = (Amplitude>30).astype(np.float32) Amplitude = Amplitude*mask return Amplitude, theta def getlocation(indices, labels, Ni, Nj): """ return if there is a zebra cossing if true, Combine all the rectangular boxes as its position assume a picture has only one zebra crossing """ zc = indices[labels == 1] if len(zc) == 0: return 0, None else: xmin = int(min(zc[:,1])) ymin = int(min(zc[:,0])) xmax = int(xmin + Nj) ymax = int(max(zc[:,0]) + Ni) return 1, ((xmin, ymin), (xmax, ymax)) if __name__ == "__main__": DEBUG = False #if False, won't draw all step srcs = sorted(os.listdir('images')) Ni, Nj = (100, 302) for ii, src in enumerate(srcs): print("frame: ", ii) # Load frame img = cv2.imread('images/'+src) img = cv2.resize(img, (400,400)) gray = preprocessing(img) if DEBUG: cv2.imshow("gray", gray) canny = cv2.Canny(gray,30,90,apertureSize = 3) if DEBUG: cv2.imshow("canny",canny) Amplitude, theta = getGD(canny) if DEBUG: cv2.imshow("Amplitude", Amplitude) indices, patches = zip(*sliding_window(Amplitude, theta, patch_size=(Ni, Nj))) #use sliding_window get indices and patches labels = predict(patches, DEBUG) #predict zebra crossing for every patches 1 is zc 0 is background indices = np.array(indices) ret, location = getlocation(indices, labels, Ni, Nj) #draw if DEBUG: for i, j in indices[labels == 1]: cv2.rectangle(img, (j, i), (j+Nj, i+Ni), (0, 0, 255), 3) if ret: cv2.rectangle(img, location[0], location[1], (255, 0, 255), 3) cv2.imshow("img", img) cv2.waitKey(0)
優勢
若完好損能夠把閾值調很高,此時直行斑馬線判斷很是準且不會有誤檢。在有缺損時適當調節閾值也能有較好效果。測試
後續改進方向
利用等間隔特徵加強精確度同時消除路邊干擾, 適當下降轉彎時的閾值, 利用邊緣共線特徵處理掉孤立點或不成線點(路面破損箭頭)
以前試過深度學習的目標檢測或滑窗加分類, 效果在當前數據集不錯, 但因爲可獲取的數據太少後續不易調參遂放棄, 後續將繼續採用傳統圖像處理改進識別效果
ui