opencv 斑馬線,條紋檢測

斑馬線檢測 基於OpenCVgit

效果不是很好
設置DEBUG變量爲True時會輸出每一步圖像用於逐幀debug和調參(按下任意鍵或者按住不放下一步),設爲False則只畫最後結果圖。
紅色方框是判斷爲斑馬線的滑窗,紫色方框是最終輸出的斑馬線位置(紫框計算目前默認了圖內只會出現一條斑馬線)

github

github: https://github.com/TomMao23/ZebraCrossing_Detection算法

基本思路

總結斑馬線的四個特徵:
梯度一致性
等間隔
多根線
斑馬線比車道線寬
梯度一致性和等間隔是兩個強分類特徵,其中梯度一致性特徵易召回,等間隔特徵強精度。目前算法未使用等間隔特徵,主要利用梯度一致,另外兩個特徵輔助。




ide

轉逆透視圖, 中值濾波,開運算,閉運算預處理後Canny邊緣檢測,對邊緣檢測圖用sobel求橫縱梯度,獲得梯度的模值和方向(方向歸爲0到90度, 即不區分正反),先過濾梯度過小的點。在滑窗內統計0:70度間隔5度的點直方圖,取峯值方向的點數做爲判斷量,高於閾值判爲斑馬線。學習

問題及當前解決思路

  1. 過濾減速帶:減速帶一樣具備梯度一致性,等間隔和線多的三個特徵,可是有線短的特色,因此滑窗高度取得越大減速帶越容易過濾,這是永遠管用的。另外一方面, 減速帶是黃色(紅+綠)黑色相間的,因此單使用藍色通道而不是正常RGB轉成的灰度圖能夠大大衰減減速帶的梯度影響。
  2. 開運算有兩個做用:可以減小噪點,能夠利用斑馬線比車道線更寬的特色用多iterations參數的開運算在保留斑馬線的同時直接去除車道線干擾(缺點:若斑馬線有磨損開運算會有必定損失,灰度圖開運算可能會產生弱的虛假邊緣,能夠調參不檢測出來)。
  3. 閉運算有兩個做用:一是可以填補線磨損加強斑馬線的邊緣識別效果, 二是路面箭頭破損時容易產生少許梯度雜亂的邊緣增長誤檢的機率(若是斑馬線無破損,閾值能夠設得很高就沒有這個問題)。即便無破損閉運算仍然很好地減小了其餘元素的虛假邊緣。
  4. 中值濾波的重要性:比起均值濾波等濾波器,一樣是平滑的效果,並提高邊緣檢測效果,中值濾波保留了原始梯度大小更利於邊緣的檢出,同時能夠完美地消除人行道的小磚縫紋理,因此中值濾波是必選。
  5. canny的重要性:試過直接用sobel x,y來作可是:
    1. 噪點虛假邊緣較多。
    2. 一個邊緣多個點,斑馬線線多的特徵被衰弱。
    3. 這麼作只有一個梯度閾值判斷過濾邊緣,以後再根據梯度方向過濾,canny兩個閾值並考慮連通性解決了前兩個問題而且調參更方便,好效果的參數的範圍也更大。因此用canny。
  6. 過濾中止線:因爲當前方法利用線多特徵,是在寬大於高的矩形滑窗內統計峯值梯度方向的點數(縱線斜線天然是線越多這樣統計的點越多),直行時的中止線剛好在滑窗內且是橫線峯值梯度方向點也會較多,因此統計時只取到0到70度(x軸方向梯度爲0度),當轉彎或斜行時中止線在滑窗內峯值梯度方向點很少,天然過濾。
  7. 主要干擾:中止線和減速帶的干擾很小,容易調節閾值, 不成問題。路面箭頭等標誌是目前主要干擾,由於斑馬線有缺損綜合考慮召回不能把閾值調得很是高(目前閾值是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

相關文章
相關標籤/搜索