两段程序分别测试视频和图片数据,算法参数相同(视频里没有对应改参数过滤车道线,但因为斑马线清晰所以不用过滤也无影响)
设置DEBUG变量为True时会输出每一步图像用于逐帧debug和调参(按下任意键或者按住不放下一步),设为False则只画最后结果图。
红色方框是判断为斑马线的滑窗,紫色方框是最终输出的斑马线位置(紫框计算目前默认了图内只会出现一条斑马线)

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

基本思路

总结斑马线的四个特征:
梯度一致性
等间隔
多根线
斑马线比车道线宽
梯度一致性和等间隔是两个强分类特征,其中梯度一致性特征易召回,等间隔特征强精度。目前算法未使用等间隔特征,主要利用梯度一致,另外两个特征辅助。

转逆透视图, 中值滤波,开运算,闭运算预处理后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)
      

优点

若无缺损可以把阈值调很高,此时直行斑马线判断非常准且不会有误检。在有缺损时适当调节阈值也能有较好效果。

缺点和不足

没有利用等间隔特征,转弯时的斜向斑马线召回率不算高。

后续改进方向

利用等间隔特征增强精确度同时消除路边干扰, 适当降低转弯时的阈值, 利用边缘共线特征处理掉孤立点或不成线点(路面破损箭头)
之前试过深度学习的目标检测或滑窗加分类, 效果在当前数据集不错, 但由于可获取的数据太少后续不易调参遂放弃, 后续将继续采用传统图像处理改进识别效果

未完待续