一次不太成功的项目实战:HOG特征+SVM实现交通标志的检测

您所在的位置:网站首页 道路标识识别功能的车有哪些品牌好用 一次不太成功的项目实战:HOG特征+SVM实现交通标志的检测

一次不太成功的项目实战:HOG特征+SVM实现交通标志的检测

2024-06-29 09:22:59| 来源: 网络整理| 查看: 265

**本文主要讲如何通过HOG特征和SVM分类器实现部分交通标志的检测。由于能力有限,本文的检测思路很简单,主要是用来自己练习编程用,也顺便发布出来供需要的人参考。本项目完整的代码(包括数据集)可以在我的github上下载:traffic-sign-detection。博客或代码中遇到的任何问题,欢迎指出,希望能相互学习。废话不多说了,下面就来一步步介绍我的检测过程。

数据集

数据集都是我的一个学妹帮忙采集的。在此表示感谢。本文一共选用了6种交通标志,分别为:

data

数据预处理

一共拍了1465张照片,由于是用手机在路上拍的,图像像素过大且大小不一(有的是横着拍的,有的数竖着拍的),影响检测效率。因此,我先将所有的图片进行了预处理,具体处理步骤为: (1)以图片宽和高较小的值为裁剪的边长S,从原图中裁剪出S×S的正方形中心区域; (2)将裁剪出的区域resize为640×640; 处理的主要函数如下:

def center_crop(img_array, crop_size=-1, resize=-1, write_path=None): """ crop and resize a square image from the centeral area. Args: img_array: image array crop_size: crop_size (default: -1, min(height, width)). resize: resized size (default: -1, keep cropped size) write_path: write path of the image (default: None, do not write to the disk). Return: img_crop: copped and resized image. """ rows = img_array.shape[0] cols = img_array.shape[1] if crop_size==-1 or crop_size>max(rows,cols): crop_size = min(rows, cols) row_s = max(int((rows-crop_size)/2), 0) row_e = min(row_s+crop_size, rows) col_s = max(int((cols-crop_size)/2), 0) col_e = min(col_s+crop_size, cols) img_crop = img_array[row_s:row_e,col_s:col_e,] if resize>0: img_crop = cv2.resize(img_crop, (resize, resize)) if write_path is not None: cv2.imwrite(write_path, img_crop) return img_crop def crop_img_dir(img_dir, save_dir, crop_method = "center", rename_pre=-1): """ crop and save square images from original images saved in img_dir. Args: img_dir: image directory. save_dir: save directory. crop_method: crop method (default: "center"). rename_pre: prename of all images (default: -1, use primary image name). Return: none """ img_names = os.listdir(img_dir) img_names = [img_name for img_name in img_names if img_name.split(".")[-1]=="jpg"] index = 0 for img_name in img_names: img = cv2.imread(os.path.join(img_dir, img_name)) rename = img_name if rename_pre==-1 else rename_pre+str(index)+".jpg" img_out_path = os.path.join(save_dir, rename) if crop_method == "center": img_crop = center_crop(img, resize=640, write_path=img_out_path) if index%100 == 0: print "total images number = ", len(img_names), "current image number = ", index index += 1 数据标注

标注信息采用和PASCAL VOC数据集一样的方式,对于正样本,直接使用labelImg工具进行标注,这里给出我用的一个版本的链接:https://pan.baidu.com/s/1Q0cqJI9Dnvxkj7159Be4Sw。对于负样本,可以使用python中的xml模块自己写xml标注文件,主要函数如下:

from xml.dom.minidom import Document import os import cv2 def write_img_to_xml(imgfile, xmlfile): """ write xml file. Args: imgfile: image file. xmlfile: output xml file. """ img = cv2.imread(imgfile) img_folder, img_name = os.path.split(imgfile) img_height, img_width, img_depth = img.shape doc = Document() annotation = doc.createElement("annotation") doc.appendChild(annotation) folder = doc.createElement("folder") folder.appendChild(doc.createTextNode(img_folder)) annotation.appendChild(folder) filename = doc.createElement("filename") filename.appendChild(doc.createTextNode(img_name)) annotation.appendChild(filename) size = doc.createElement("size") annotation.appendChild(size) width = doc.createElement("width") width.appendChild(doc.createTextNode(str(img_width))) size.appendChild(width) height = doc.createElement("height") height.appendChild(doc.createTextNode(str(img_height))) size.appendChild(height) depth = doc.createElement("depth") depth.appendChild(doc.createTextNode(str(img_depth))) size.appendChild(depth) with open(xmlfile, "w") as f: doc.writexml(f, indent="\t", addindent="\t", newl="\n", encoding="utf-8") def write_imgs_to_xmls(imgdir, xmldir): img_names = os.listdir(imgdir) for img_name in img_names: img_file = os.path.join(imgdir,img_name) xml_file = os.path.join(xmldir, img_name.split(".")[0]+".xml") print img_name, "has been written to xml file in ", xml_file write_img_to_xml(img_file, xml_file) 数据集划分

这里我们将1465张图片按照7:2:1的比例随机划分为训练集、测试集和验证集。为了方便运行,我们先建立一个名为images的文件夹,下面有JPEGImages和Annotations分别存放了所有的图片和对应的标注文件。同样,最后附上划分数据集的主要函数:

import os import shutil import random def _copy_file(src_file, dst_file): """copy file. """ if not os.path.isfile(src_file): print"%s not exist!" %(src_file) else: fpath, fname = os.path.split(dst_file) if not os.path.exists(fpath): os.makedirs(fpath) shutil.copyfile(src_file, dst_file) def split_data(data_dir, train_dir, test_dir, valid_dir, ratio=[0.7, 0.2, 0.1], shuffle=True): """ split data to train data, test data, valid data. Args: data_dir -- data dir to to be splitted. train_dir, test_dir, valid_dir -- splitted dir. ratio -- [train_ratio, test_ratio, valid_ratio]. shuffle -- shuffle or not. """ all_img_dir = os.path.join(data_dir, "JPEGImages/") all_xml_dir = os.path.join(data_dir, "Annotations/") train_img_dir = os.path.join(train_dir, "JPEGImages/") train_xml_dir = os.path.join(train_dir, "Annotations/") test_img_dir = os.path.join(test_dir, "JPEGImages/") test_xml_dir = os.path.join(test_dir, "Annotations/") valid_img_dir = os.path.join(valid_dir, "JPEGImages/") valid_xml_dir = os.path.join(valid_dir, "Annotations/") all_imgs_name = os.listdir(all_img_dir) img_num = len(all_imgs_name) train_num = int(1.0*img_num*ratio[0]/sum(ratio)) test_num = int(1.0*img_num*ratio[1]/sum(ratio)) valid_num = img_num-train_num-test_num if shuffle: random.shuffle(all_imgs_name) train_imgs_name = all_imgs_name[:train_num] test_imgs_name = all_imgs_name[train_num:train_num+test_num] valid_imgs_name = all_imgs_name[-valid_num:] for img_name in train_imgs_name: img_srcfile = os.path.join(all_img_dir, img_name) xml_srcfile = os.path.join(all_xml_dir, img_name.split(".")[0]+".xml") xml_name = img_name.split(".")[0] + ".xml" img_dstfile = os.path.join(train_img_dir, img_name) xml_dstfile = os.path.join(train_xml_dir, xml_name) _copy_file(img_srcfile, img_dstfile) _copy_file(xml_srcfile, xml_dstfile) for img_name in test_imgs_name: img_srcfile = os.path.join(all_img_dir, img_name) xml_srcfile = os.path.join(all_xml_dir, img_name.split(".")[0]+".xml") xml_name = img_name.split(".")[0] + ".xml" img_dstfile = os.path.join(test_img_dir, img_name) xml_dstfile = os.path.join(test_xml_dir, xml_name) _copy_file(img_srcfile, img_dstfile) _copy_file(xml_srcfile, xml_dstfile) for img_name in valid_imgs_name: img_srcfile = os.path.join(all_img_dir, img_name) xml_srcfile = os.path.join(all_xml_dir, img_name.split(".")[0]+".xml") xml_name = img_name.split(".")[0] + ".xml" img_dstfile = os.path.join(valid_img_dir, img_name) xml_dstfile = os.path.join(valid_xml_dir, xml_name) _copy_file(img_srcfile, img_dstfile) _copy_file(xml_srcfile, xml_dstfile)

代码运行的结果是在指定的文件夹下分别创建训练集、测试集和验证集文件夹,并且每个文件夹下包含了JPEGImages和Annotations两个子文件夹来存放结果。

到这里用于目标检测的数据集已经准备好了。下面我们介绍整个检测模型的框架。

检测框架

本文用的检测思路非常直观,总的来讲分为候选区域提取、HOG特征提取和SVM分类。

候选区域提取

理论上可以通过设置不同的滑动窗口对整张图像进行遍历,但是这样做不仅计算太大,而且窗口的大小也不好把握。考虑到我们要检测的交通标志都有比较规则的几何形状和颜色信息,我们可以通过检测形状(平行四边形、椭圆)和颜色(红色、蓝色等)来实现初步的预处理以减少计算量,提高检测效率。这里我们以仅颜色信息为例介绍。

由于需要检测的6类标志主要是红色和蓝色(或者红蓝结合),环境中的不同光照强度可能会使颜色变化较大因此给定一张图像,先在HSV空间中通过颜色阈值分割选出蓝色和红色对应的区域得到二值化图像。然后对二值化图像进行凸包检测(可通过OpenCV实现),下图给出了一个示例:

bin_img

可以看出,经过二值化处理后,图像中的3个标志(其中2个标志是我们需要检测识别的)的轮廓信息都被保留下来了。但是存在依然存在一些问题:(1)背景噪声较多,这会导致检测更多的凸包,从而影响检测速度和精度;(2)三个标志离得很近,可能会导致只检测出一个凸包。我之前考虑过用腐蚀膨胀来滤除一部分的噪声,但在实验的时候发现这会导致更多的漏检。这是因为在腐蚀膨胀的时候部分标志的轮廓信息很有可能会被破坏(尤其是禁止鸣笛标志),导致在凸包检测的阶段被遗漏。所以在最终测试的时候并没有使用腐蚀膨胀操作。下面给出阈值化处理和凸包检测的函数:

def preprocess_img(imgBGR, erode_dilate=True): """preprocess the image for contour detection. Args: imgBGR: source image. erode_dilate: erode and dilate or not. Return: img_bin: a binary image (blue and red). """ rows, cols, _ = imgBGR.shape imgHSV = cv2.cvtColor(imgBGR, cv2.COLOR_BGR2HSV) Bmin = np.array([100, 43, 46]) Bmax = np.array([124, 255, 255]) img_Bbin = cv2.inRange(imgHSV,Bmin, Bmax) Rmin1 = np.array([0, 43, 46]) Rmax1 = np.array([10, 255, 255]) img_Rbin1 = cv2.inRange(imgHSV,Rmin1, Rmax1) Rmin2 = np.array([156, 43, 46]) Rmax2 = np.array([180, 255, 255]) img_Rbin2 = cv2.inRange(imgHSV,Rmin2, Rmax2) img_Rbin = np.maximum(img_Rbin1, img_Rbin2) img_bin = np.maximum(img_Bbin, img_Rbin) if erode_dilate is True: kernelErosion = np.ones((3,3), np.uint8) kernelDilation = np.ones((3,3), np.uint8) img_bin = cv2.erode(img_bin, kernelErosion, iterations=2) img_bin = cv2.dilate(img_bin, kernelDilation, iterations=2) return img_bin def contour_detect(img_bin, min_area=0, max_area=-1, wh_ratio=2.0): """detect contours in a binary image. Args: img_bin: a binary image. min_area: the minimum area of the contours detected. (default: 0) max_area: the maximum area of the contours detected. (default: -1, no maximum area limitation) wh_ratio: the ration between the large edge and short edge. (default: 2.0) Return: rects: a list of rects enclosing the contours. if no contour is detected, rects=[] """ rects = [] _, contours, _ = cv2.findContours(img_bin.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) if len(contours) == 0: return rects max_area = img_bin.shape[0]*img_bin.shape[1] if max_area= min_area and area 'right': 117, 'straight': 334, 'stop': 224, 'no hook': 168, 'crosswalk': 128, 'left': 208, 'background': 1116}

裁剪的部分正负样本如下:

pos_neg

前面几行对应6类正样本,最后一行是背景,可以发现,代码中找出来的背景主要是和我们交通标志颜色(蓝色和红色)相似的区域。我们用相同的方法从我们的验证集中提取正负样本用于SVM模型参数的调整和评估。这里就不再赘述。

训练数据扩充

从上面各个类别样本数量上来看,正样本的各类标志数量相对背景(负样本)很少。为了近些年数据的平衡,我们对正样本进行了扩充。由于我们的数据中包含了向左向右等标志,如何通过旋转或者镜像变换会出问题(当然可以旋转小范围旋转),我也考虑过亮度变换,但是由于HOG特征中引入了归一化方法使得HOG特征对光照不敏感。最终我选用的是仿射变换,这个可以通过OpenCV很方便地实现,具体的仿射变换理论和代码示例可以参考OpenCV官方教程中的Affine Transformations ,这里也给出我对数据集仿射变换的函数:

def affine(img, delta_pix): """affine transformation Args: img: a numpy image array. delta_pix: the offset for affine. Return: res: affined image. """ rows, cols, _ = img.shape pts1 = np.float32([[0,0], [rows,0], [0, cols]]) pts2 = pts1 + delta_pix M = cv2.getAffineTransform(pts1, pts2) res = cv2.warpAffine(img, M, (rows, cols)) return res def affine_dir(img_dir, write_dir, max_delta_pix): """ affine transformation on the images in a directory. Args: img_dir: image directory. write_dir: save directory of affined images. max_delta_pix: the maximum offset for affine. """ img_names = os.listdir(img_dir) img_names = [img_name for img_name in img_names if img_name.split(".")[-1]=="jpg"] for index, img_name in enumerate(img_names): img = cv2.imread(os.path.join(img_dir,img_name)) save_name = os.path.join(write_dir, img_name.split(".")[0]+"f.jpg") delta_pix = np.float32(np.random.randint(-max_delta_pix, max_delta_pix+1, [3,2])) img_a = affine(img, delta_pix) cv2.imwrite(save_name, img_a)

上面函数输入参数max_delta_pix用来控制随机仿射变换的最大强度(正整数),max_delta_pix的绝对值越大,变换越明显(太大可能导致目标信息的完全丢失),我在扩充时这个参数取为10。需要注意的是,10只是变换的最大强度,在对每一张图片进行变换前,会在[-max_delta, max_delta]生成一个随机整数delta_pix(当然你也可以多取几次不同的值来生成更多的变换图片),这个整数控制了当前图片变换的强度。以下是一些变换的结果示例:

affine examples

模型训练和测试

模型的训练我是直接调用sklearn中的svm库,很多参数都使用了默认值,在训练时发现,惩罚因子C的取值对训练的影响很大,我这边就偷个懒,大概设置了一个值。(超参数可以利用之前的验证集去调整,这里就不赘述了。)用到的函数如下:

def load_hog_data(hog_txt): """ load hog features. Args: hog_txt: a txt file used to save hog features. one line data is formated as "img_path \t cls_num \t hog_feature_vector" Return: img_names: a list of image names. labels: numpy array labels (1-dim). hog_feature: numpy array hog features. formated as [[hog1], [hog2], ...] """ img_names = [] labels = [] hog_features = [] with open(hog_txt, "r") as f: data = f.readlines() for row_data in data: row_data = row_data.rstrip() img_path, label, hog_str = row_data.split("\t") img_name = img_path.split("/")[-1] hog_feature = hog_str.split(" ") hog_feature = [float(hog) for hog in hog_feature] #print "hog feature length = ", len(hog_feature) img_names.append(img_name) labels.append(int(label)) hog_features.append(hog_feature) return img_names, np.array(labels), np.array(hog_features) def svm_train(hog_features, labels, save_path="./svm_model.pkl"): """ SVM train Args: hog_feature: numpy array hog features. formated as [[hog1], [hog2], ...] labels: numpy array labels (1-dim). save_path: model save path. Return: none. """ clf = SVC(C=10, tol=1e-3, probability = True) clf.fit(hog_features, labels) joblib.dump(clf, save_path) print "finished." def svm_test(svm_model, hog_feature, labels): """SVM test Args: hog_feature: numpy array hog features. formated as [[hog1], [hog2], ...] labels: numpy array labels (1-dim). Return: accuracy: test accuracy. """ clf = joblib.load(svm_model) accuracy = clf.score(hog_feature, labels) return accuracy

最后,我在3474张训练集(正样本扩充为原来的2倍,负样本没有扩充)上训练,在C=10的时候(其他参数默认),在验证集上(322张)的准确率为97.2%。也就是说有9张图片分类错误,还是可以接受的。

检测结果

回顾一下,我们现在已经可以提取候选区域提取并分类了,也就是说,已经可以对一张完整的图片进行检测了。这里给出我的检测代码和检测结果示例。

import os import numpy as np import cv2 from skimage import feature as ft from sklearn.externals import joblib cls_names = ["straight", "left", "right", "stop", "nohonk", "crosswalk", "background"] img_label = {"straight": 0, "left": 1, "right": 2, "stop": 3, "nohonk": 4, "crosswalk": 5, "background": 6} def preprocess_img(imgBGR, erode_dilate=True): """preprocess the image for contour detection. Args: imgBGR: source image. erode_dilate: erode and dilate or not. Return: img_bin: a binary image (blue and red). """ rows, cols, _ = imgBGR.shape imgHSV = cv2.cvtColor(imgBGR, cv2.COLOR_BGR2HSV) Bmin = np.array([100, 43, 46]) Bmax = np.array([124, 255, 255]) img_Bbin = cv2.inRange(imgHSV,Bmin, Bmax) Rmin1 = np.array([0, 43, 46]) Rmax1 = np.array([10, 255, 255]) img_Rbin1 = cv2.inRange(imgHSV,Rmin1, Rmax1) Rmin2 = np.array([156, 43, 46]) Rmax2 = np.array([180, 255, 255]) img_Rbin2 = cv2.inRange(imgHSV,Rmin2, Rmax2) img_Rbin = np.maximum(img_Rbin1, img_Rbin2) img_bin = np.maximum(img_Bbin, img_Rbin) if erode_dilate is True: kernelErosion = np.ones((9,9), np.uint8) kernelDilation = np.ones((9,9), np.uint8) img_bin = cv2.erode(img_bin, kernelErosion, iterations=2) img_bin = cv2.dilate(img_bin, kernelDilation, iterations=2) return img_bin def contour_detect(img_bin, min_area=0, max_area=-1, wh_ratio=2.0): """detect contours in a binary image. Args: img_bin: a binary image. min_area: the minimum area of the contours detected. (default: 0) max_area: the maximum area of the contours detected. (default: -1, no maximum area limitation) wh_ratio: the ration between the large edge and short edge. (default: 2.0) Return: rects: a list of rects enclosing the contours. if no contour is detected, rects=[] """ rects = [] _, contours, _ = cv2.findContours(img_bin.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) if len(contours) == 0: return rects max_area = img_bin.shape[0]*img_bin.shape[1] if max_area= min_area and area


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭