关于交通标牌检测的博客和论文非常多,例如,本人最近在博客上就看到有一篇简单的交通标志检测与识别介绍文章《自动驾驶之眼——摄像头是如何认识交通标志的?》,该文很简洁明了地阐述了交通标志检测识别的主要流程。本文将结合上学期的课程设计,来整理一下交通标识牌检测与识别的思路与实现方法。
一、要求
首先要明确一下本文到底是要干什么。本文要完成基于视觉的交通标识牌检测与识别,说白了,就两个事:1)在一张图中找到交通标识牌在哪里(检测);2)认清楚这个标识牌是啥,表达的什么意思(识别)。那么最后得到的结果预览如下:
二、使用数据
交通标识牌种类数不胜数,我国的交通标志一共有一百余种,按类别可分为黄底黑边的警告标志、白底红圈的禁令标志、蓝底白字的指示标志,形状上以三角形、圆形和矩形为主。本文主要是为了介绍一下交通标识牌的识别流程和一些主要方法的实现,为了简化工作,本文挑选了以下五类交通标识牌。
可以看出来,博主用心良苦,选择的交通标识牌具有很清楚的特征:1)颜色上,这五类交通标识牌的外边框都是红色的;2)形状上,标识牌都是标准的圆形。这事实上也表明了,交通标识牌具有着鲜明的特征,故无论是人眼还是机器,都较易识别。(其他种类的交通标志牌也是一样,利用形状和颜色特征来处理)
三、使用方法
在我看来,目前处理交通标牌识别的主要有两种方法,1)传统的图像处理+机器学习办法;2)最近很火的深度学习。那么本文采用的是前者,后者后续再进行介绍。
1>检测:颜色和形状。
交通标志牌为了起到其警示作用,在颜色和形状上都有着易区分性,如本文所讨论的五类标志牌,颜色特征为外框均为鲜艳的红色;形状特征为均为圆形。于是,检测的思路如下,最终得到了圆形部分的交通标牌:
接下来,主要分为颜色分割和形状检测两部分进行讨论:
基于颜色分割的图像二值化处理:
最直观、简单的是利用RGB颜色空间来描述图像的色彩情况,但是,RGB色彩空间极易受到光线情况的影响,鲁棒性并不是很好,所以在相关论文中,你会发现,很少有人直接使用RGB色彩空间进行色彩分割。而实际上,本人拿有限的样本和测试集进行测试,RGB色彩分割效果在图像成像质量较理想的时候效果极佳,但是的确容易受到干扰。本文此处选择了HSI色彩空间模型进行色彩分割。先来点理论知识:
色调H(Hue):与光波的波长有关,它表示人的感官对不同颜色的感受,如红色、绿色、蓝色等,它也可表示一定范围的颜色,如暖色、冷色等。
饱和度S(Saturation):表示颜色的纯度,纯光谱色是完全饱和的,加入白光会稀释饱和度。饱和度越大,颜色看起来就会越鲜艳,反之亦然。
亮度I(Intensity):对应成像亮度和图像灰度,是颜色的明亮程度。
从理论上看,HSI色彩空间将饱和度和亮度信息独立了出来,这样一定程度上就降低了光线带来的影响。听上去很有道理,但是,实际上,这也仅仅是一定程度上降低了亮度和色彩的耦合关系,并不是完全地进行了解耦,所以,效果会有提升,但是很难带来质的改变(这是笔者自己的体验,也许是笔者能力不足,实现得不是很理想)
那么从RGB色彩空间转换到HSI空间的转换公式如下:
函数RGB2HSI是将RGB色彩空间转换到HSI色彩空间,其转换的过程参照式(2.2),最后将饱和度S和强度I均放大100倍,便于操作。 最后得到的H、 S、 I的取值范围分别为[0,360]、 [0,100]、 [0,100]。
void RGB2HSV(double red, double green, double blue, double& hue, double& saturation, double& intensity ) { double r,g,b; double h,s,i; double sum; double minRGB,maxRGB; double theta; r = red/255.0; g = green/255.0; b = blue/255.0; minRGB = ((r<g)?(r):(g)); minRGB = (minRGB<b)?(minRGB):(b); maxRGB = ((r>g)?(r):(g)); maxRGB = (maxRGB>b)?(maxRGB):(b); sum = r+g+b; i = sum/3.0; if( i<0.001 || maxRGB-minRGB<0.001 ) { h=0.0; s=0.0; } else { s = 1.0-3.0*minRGB/sum; theta = sqrt((r-g)*(r-g)+(r-b)*(g-b)); theta = acos((r-g+r-b)*0.5/theta); if(b<=g) h = theta; else h = 2*PI - theta; if(s<=0.01) h=0; } hue = (int)(h*180/PI); saturation = (int)(s*100); intensity = (int)(i*100); }在得到HSI空间的基础上,分割出红色像素,事实上这个阈值最好时自己调出来,无论是基于哪个色彩空间,网上的代码或者论文中的数值都是个参考,自己调出来的才靠谱嘛,代码如下:
//得到图像参数 int width = src.cols; //图像宽度 int height = src.rows; //图像高度 //色彩分割 double B=0.0,G=0.0,R=0.0,H=0.0,S=0.0,I=0.0; Mat Mat_rgb = Mat::zeros( src.size(), CV_8UC1 ); int x,y,px,py; //循环 for (y=0; y<height; y++) { for ( x=0; x<width; x++) { // 获取 BGR 值 B = src.at<Vec3b>(y,x)[0]; G = src.at<Vec3b>(y,x)[1]; R = src.at<Vec3b>(y,x)[2]; RGB2HSV(R,G,B,H,S,I); //红色:337-360 if((H>=337 && H<=360||H>=0&&H<=10)&& S>=12&&S<=100&&V>20&&V<99) { Mat_rgb.at<uchar>(y,x) = 255; //分割出红色 } } }分割效果可见如下组图,由近至远:
⚠️⚠️⚠️ 注意:有一个很严肃的问题我这里没有提,那就是图像预处理!做图像处理的很重要的一个步骤就是图像预处理,预处理做好了,后面的问题复杂度也就降低了许多。实际上,用颜色分割来二值化图像也可以看作一种预处理。那么颜色分割之前有不有必要做图像预处理呢?是有的。举个例子,我那我的MATE8在学校里拍了一张照,然后使用手机相机自带的功能,调整其色彩饱和度,亮度等,得到以下两种图片:
相机拍的原图 手机调整饱和度、亮度后 这两种图片,显然右边的将更有利于颜色分割!(不信可以试试哦)。本文主要以介绍交通标牌的主要流程为主,预处理的方法包括直方图均衡化、白平衡、亮度调节等等这些就不仔细纠结了,但是,不代表这部分不重要,图像预处理往往一定程度上决定了最后的效果。 基于形状(圆形)检测的ROI提取
在进行颜色分割之后,得到的只是一个粗略的交通标志牌ROI区域, 还会留下一些噪声以及一些和目标区域面积相当或者比目标面积略大的区域,这时候就还需要进行一些图像预处理,为准确检测交通标志牌打下坚实基础。由于交通标志最明显的特征是其颜色和形状,在用颜色分割之后,我们可以通过形状特征来去除其余的干扰。对于本文的研究对象而言,交通标志牌的形状为圆形,可以采用经典的Hough变换进行圆检测,该方法准确性高,但是计算量大,耗时且占用较大内存;也可以采用圆度的方法来提取圆形,该方法原理简单,计算量小,准确率高。综合考虑,本文使用基于圆度的圆检测算法。大概流程如下,后文还会详细介绍:
图有点不太清楚,下文中对于关键的部分会再次给出效果图。 中值滤波,这个没啥好说的,图上效果不是很明显,但是实际上可以一定程度上滤掉单个噪点,对得到准确的结果会有一定的帮助; 形态学处理,最后我们的目的是要得到一个封闭的区域,所以,颜色分割后的结果很可能不会是比较理想封闭的圆形,那么选用的3×3腐蚀模板,7×7膨胀模板,这样检测到的圆形将基本不会产生缺口,保证是一个封闭的形状。
图像填充,有了上述步骤得到的封闭圆形,我们接下来就可以填充封闭图形了(这里你可能会问,为啥要这样做。实际上直接进行Hough圆检测可以得到ROI结果,但是本文是换了一个思路,使用圆度来判断圆形,所以算法需要一个实心区域),代码如下:
void fillHole(const Mat srcBw, Mat &dstBw) { Size m_Size = srcBw.size(); Mat Temp=Mat::zeros(m_Size.height+2,m_Size.width+2,srcBw.type());//延展图像 srcBw.copyTo(Temp(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1))); cv::floodFill(Temp, Point(0, 0), Scalar(255));//填充区域 Mat cutImg;//裁剪延展的图像 Temp(Range(1, m_Size.height + 1), Range(1, m_Size.width + 1)).copyTo(cutImg); dstBw = srcBw | (~cutImg); }
轮廓检测,初步筛选ROI,要想使用基于圆度的圆检测算法,则需要从图像中提取初步的ROI来进行筛选。这里使用轮廓检测法来检测图片中的ROI区域。可以看到,一些细小的噪声也被检测进来。
代码如下:
代码如下:
//找轮廓 vector<vector<Point> > contours; vector<Vec4i> hierarchy; findContours( Mat_rgb, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) ); /// 多边形逼近轮廓 + 获取矩形和圆形边界框 vector<vector<Point> > contours_poly( contours.size() ); vector<Rect> boundRect( contours.size() ); vector<Point2f>center( contours.size() ); vector<float>radius( contours.size() ); //得到轮廓矩形框 for( int i = 0; i < contours.size(); i++ ) { approxPolyDP( Mat(contours[i]), contours_poly[i], 3, true ); boundRect[i] = boundingRect( Mat(contours_poly[i]) ); minEnclosingCircle( contours_poly[i], center[i], radius[i] ); } /// 画多边形轮廓 + 包围的矩形框 Mat drawing = Mat::zeros( Mat_rgb.size(), CV_8UC3 ); for( int i = 0; i< contours.size(); i++ ) { Rect rect = boundRect[i]; //首先进行一定的限制,筛选出区域 //高宽比限制 float ratio = (float)rect.width / (float)rect.height; //轮廓面积 float Area = (float)rect.width * (float)rect.height; float dConArea = (float)contourArea(contours[i]); float dConLen = (float)arcLength(contours[i],1); if(dConArea <400)//ROI 区域面积限制 continue; if(ratio>2||ratio<0.5)//ROI 区域宽高比限制 continue; //检测到了! Scalar color = Scalar( rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255) ); //绘制轮廓和检测到的轮廓外接矩形 drawContours( drawing, contours_poly, i, color, 1, 8, vector<Vec4i>(), 0, Point() ); rectangle( drawing, boundRect[i].tl(), boundRect[i].br(), color, 2, 8, 0 ); rectangle( src, boundRect[i].tl(), boundRect[i].br(), color, 2, 8, 0 ); }