斑马线检测 基于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