MATLAB交通标志识别实战:从HSV颜色分割到SVM分类的完整流程
2026/8/31 9:44:09 网站建设 项目流程

简介:本资源是一套基于MATLAB实现的交通标志识别完整项目,面向智能交通系统初学者、图像识别入门者及高校课程设计学生,解决交通标志自动分类与识别这一典型计算机视觉应用问题。压缩包共25个文件,含9张PNG与9张JPG实拍交通标志图(涵盖警示类、指示类、禁止类共十余种常见标志),3个核心M文件(GUI主程序、BP网络训练与预测函数),3个MAT数据文件(训练模型、图像数据集及类别标签),以及1个FIG图形界面设计文件,整体仅1.72MB,轻量易运行。已有639人学习下载,资源结构清晰:从图像预处理、特征提取到BP神经网络训练与部署全流程覆盖,附带可直接运行的GUI交互界面,支持上传新图实时识别,并提供已训练好的Trained_BP.mat模型,省去耗时训练环节,便于快速验证算法效果与理解智能交通中模式识别的工程实现路径。 做MATLAB交通标志识别这个课题,熬了三个通宵,踩了无数坑,模型准确率从惨不忍睹的53%一路调到91%,这中间有些东西现在回想起来,真希望一开始有人能直接告诉我。

之所以想写这篇东西,是因为最近在CSDN和GitHub上翻了一圈,发现交通标志识别相关的MATLAB教程要么是只贴几段零散代码,要么就是把深度学习的预训练模型拿来跑一下——对于本科课程设计或者竞赛打基础来说,这些都有点"隔靴搔痒"。你真正需要的是从图像预处理、颜色分割、形状判定、特征提取再到分类器训练这一整套东西该怎么串起来,每一步为什么要这么做,遇到问题怎么排查。这篇文章就是围绕这个思路写的,MATLAB环境为主,OpenCV的思维也可以通用,适合正在做图像处理大作业、准备电赛或模式识别小项目的同学参考。

1. 为什么这届课设/竞赛还是离不开传统图像处理方法

先说一个很多人不愿意承认的事实:虽然深度学习已经是图像识别的默认选项,但在MATLAB交通标志识别这个具体场景里,传统图像处理——颜色分割加形状匹配加特征分类——依然有它的黄金价值。不是因为它先进,而是因为它够用、够稳、够可控。

交通标志本身有个天然优势:它就是为了被人类快速识别而设计的,颜色和形状都有强约束。国内道路交通标志,红色通常对应禁令,黄色对应警告,蓝色对应指示。圆形、三角形、矩形是最主要的三种外框形态。这就是非常明确且稳定的先验信息。你拿到一张图,根本不需要让模型"从零学习"什么是限制速度、什么是注意行人,而是可以直接用颜色和形状把候选区域抠出来,然后只对"抠出来的局部"做分类。

这种思路下,系统的计算量主要集中在颜色空间变换和连通域分析上,每一帧大概几十毫秒到一两百毫秒,在课程设计或者实时视频识别demo里完全够跑,不需要GPU,不需要装几十个G的依赖库,一台普通笔记本用MATLAB就能全流程走通。

而且,传统方法的每一步你都是可以干预、可以可视化、可以调试的。颜色阈值没调好,就是某类车道线被误检成红色标志,你在imshow里一眼就能看出来是红色分量阈值太宽。深度学习模型一旦误判,定位问题和修正模型都麻烦得多。

所以,如果你的目标是:

  • 完成一份有完整处理流程、能讲清楚原理的课程设计报告;
  • 参加机器人比赛或智能车竞赛需要快速搭建视觉模块;
  • 理解图像处理的基础操作在真实场景里如何组合使用;
  • 为后续换深度学习方案做标签准备(传统方法可以自动生成候选框);

那沿着传统方法的路线一点都不会亏。接下来要讲的方案,是"HSV颜色分割 + 形态学处理 + 形状判定 + HOG特征 + SVM分类"的组合拳,这在MATLAB里每步都有对应工具箱函数,工程上很好落地。

2. 搭建基础的检测流水线模板:图像输入、预处理、色彩空间转换

整套系统第一步是把图像读进来,做去噪和增强,然后切入颜色分割。很多新手在这个起步阶段就容易有误区:读进来之后马上开始写阈值分割代码,结果发现路边树木的阴影、车灯反光、甚至不同天气下的色偏全都成了干扰。

2.1 预处理到底要做到什么程度

预处理的原则是"到什么山唱什么歌"。做交通标志识别时,你最需要处理的有三类干扰:图像噪声、光照不均、尺度变化。

MATLAB里最简单的流程是这样:

img = imread('traffic_sign.jpg'); if size(img, 3) == 1 img = repmat(img, [1 1 3]); end img = imresize(img, [480 NaN]); % 统一一个宽度,保留比例,防止形状特征被拉伸 img = imgaussfilt(img, 1.2); % 轻度平滑,去除传感器噪声 img_hsv = rgb2hsv(img); % 转换到HSV色彩空间

这里值得停下来解释两句。第一,imresize到固定行数,是为了让后续形状匹配和特征提取在可控尺度里工作,不然远处的标志只有20个像素,近处的200个像素,直接用形状特征会有偏差。第二,为什么用HSV而不是直接在RGB里做阈值?因为RGB三个通道里颜色信息是线性相关的,红色光线暗一点,三个通道值一起变,阈值怎么定都会漂移。HSV把色调(Hue)、饱和度(Saturation)、亮度(Value)分开,理想情况下,颜色本身只和H有关,光照变化主要体现在V上。这也是所有交通标志识别论文里几乎都用HSV的原因。

2.2 HSV空间里各颜色通道的取值范围

很多人知道要转HSV,但卡在动手标定颜色阈值这一步。直接说结论:MATLAB里rgb2hsv之后的H取值范围是0到1,注意,不是OpenCV里的0到180。

我常用的一套初始阈值大概是这样:

% 红色:因为红色的色相圆周上两端都有分布,所以拆成两段 red_mask1 = (hsv(:,:,1) < 0.05 | hsv(:,:,1) > 0.95) & (hsv(:,:,2) > 0.3) & (hsv(:,:,3) > 0.15); % 蓝色:指示标志 blue_mask = (hsv(:,:,1) > 0.55 & hsv(:,:,1) < 0.70) & (hsv(:,:,2) > 0.3) & (hsv(:,:,3) > 0.15); % 黄色:警告标志 yellow_mask = (hsv(:,:,1) > 0.10 & hsv(:,:,1) < 0.18) & (hsv(:,:,2) > 0.3) & (hsv(:,:,3) > 0.15);

第一眼看到这个你会觉得,这些数字是不是拍脑袋定的?还真是。我自己标定过很多组图之后发现,MATLAB官方文档给的HSV范围反而不太适用于实拍照片,因为它默认的颜色非常"标准",但实际路面的标志存在褪色、偏色、日晒、灰尘覆盖。这就逼着你去直方图里看真实分布。

这里分享一个实用的小技巧:在标定阈值之前,先把图像里你觉得是"目标红色"的区域用roipoly框选出来,然后把框选区域的HSV三个通道的值打印成min和max,多取几张图汇总,再把阈值范围扩大10%到20%作为鲁棒区间。这套流程看起来很笨,但比凭感觉填数值靠谱得多。

3. 候选区域提取:从二值掩膜到连通域筛选

颜色分割做完,你得到的是一个二值掩膜:1的地方是可能是标志的区域,0是背景。但这一步得到的掩膜噪声很多,可能是路边的红色车尾灯、蓝色广告牌、黄色警示栏。接下来要把这些候选区域“洗干净”,再从里面筛出真正的标志。

3.1 形态学操作在这里的真正作用

二值掩膜里的主要问题是碎点噪声和空洞。碎点来自路面水渍、树叶间隙透过来的杂色;空洞来自标志内部的黑色数字或图案,那块区域因为不是红色,所以掩膜里变成了一堆小洞。

标准处理顺序是:先开运算去碎点噪声,再闭运算填补小孔。

se_open = strel('disk', 3); mask = imopen(mask, se_open); se_close = strel('disk', 8); mask = imclose(mask, se_close); mask = imfill(mask, 'holes');

这里要注意,disk半径的选择和你图像归一化后的尺度有关。如果你前面把图像缩放到480行,那么一个直径80像素的标志在掩膜上约占直径80像素左右,此时disk半径8的闭运算足够将内部文字产生的小孔(通常10-30像素)封堵上,又不会把不同标志之间缝隙连接起来。如果你把图像放大到了1080行,这个结构元素半径就需要按比例放大,否则小孔堵不严。

做完形态学后,还有一个关键步骤:连通域标记。

cc = bwconncomp(mask, 8); stats = regionprops(cc, 'BoundingBox', 'Area', 'Eccentricity', 'Solidity');

regionprops是MATLAB里调试图像处理算法时最友好的函数之一,一秒钟就能给出每个连通域的几何属性。我一般会先按面积粗筛:面积太小的区域(比如小于全图面积的0.1%)直接扔掉,面积太大的(比如大于全图面积的30%)也扔掉,剩下的才进入形状判定。

3.2 颜色掩膜合并策略的小坑

这里有个实际工程里特别容易漏掉的坑:一张图里可能有多个交通标志,而且不同类型颜色不同。如果你只想检测红色禁令标志,那分开处理没毛病;但如果想要完整地识别出画面中所有标志,你最终需要把red_mask、blue_mask、yellow_mask合并成一张总掩膜再做连通域切除——但合并时一定要保留每个颜色的标签,不然到了分类阶段你不知道这个候选区域是什么颜色的,后续的特征判断就错乱了。

我自己的做法是分别对三种颜色的掩膜做连通域分析,然后把各颜色的候选框放到一个结构数组里,并给每个框加上color_label字段,这样可以一路把颜色信息保留到分类器端,作为一个强特征输入。

4. 形状特征怎么计算:圆形、三角形、矩形的判定

拿到候选连通域之后,下一步是问一个问题:这个区域是圆的、三角形的、还是矩形?这个问题的答案足以把交通标志从一堆"颜色很像的物体"中筛出来,因为路上的蓝色广告牌可能是矩形的,但不会是圆形的。对于"注意行人"这种黄底黑图案的警告标志,你只需要判断它接近三角形;"停车让行"是红底倒三角;"禁止驶入"是红底圆形加白色横杠。

4.1 用圆度、矩形度、三角形度做几何判定

最常见的量化参数是圆度(Circularity)和矩形度(Rectangularity),再加上Hull和轮廓点拟合。

圆度的计算公式是:

circularity = 4 * pi * area / perimeter^2

这个值越接近1,形状越接近圆;正三角形的圆度大约在0.6左右,正方形在0.785左右。用它可以直接区分圆形标志和非圆形标志。

矩形度定义为区域面积除以它的最小外接矩形面积,正方形和矩形接近1,圆形只有0.785,三角形更低。

三角形判定我习惯用"多边形逼近后的凸包顶点数加轮廓点数比例"来验证。思路是:

B = bwboundaries(mask_cc); boundary = B{1}; boundary_smoothed = reducepoly(boundary, 0.01); hull = convhull(boundary(:,2), boundary(:,1)); corner_count = ... % 对凸包多边形做角点检测或者多边形逼近 if corner_count == 3 % 是三角形 end

但实际做的时候发现,直接用reducepoly提取边数并不可靠。因为你裁出来的连通域边缘不可能完美光滑,轻微的锯齿可能在凸包上造成四五个短边。更稳定的方式,是计算凸包的顶点数,再用凸包面积与原始区域面积的比值判断"这个形状是否真的接近三角形"。

代码大致这样:

stats = regionprops(cc, 'ConvexHull', 'Area', 'Perimeter', 'ConvexArea'); ratio = stats.ConvexArea / stats.Area; % 凸包面积 / 实际面积,越接近1说明轮廓越规整

如果圆度接近1且凸包比也接近1,基本可以认定是圆形。这比只靠某一个参数要稳得多。

4.2 内孔和图案对形状判定的干扰及应对

一个典型的"限制速度40"标志,外圈是红色圆环,内部白色底上有黑色数字。如果你拿整个连通域算圆度,红色圆环其实是闭合的环,内部是孔洞,区域面积被扣掉了,周长又算的是内外两圈的总和,直接导致圆度大幅偏低。如果直接按圆度阈值筛选,很容易把这个正主给漏掉。

解决思路有两种:

第一种是在做形态学处理时,最后用imfill('holes')把孔洞填充,让红色圆环变成实心圆盘,然后计算圆度。我前面提的imfill就是干这个的。不过这一步有个代价:有孔洞填充之后,无法区分"禁止驶入"和"限制速度",因为两者外部都是红色圆盘,只有内部图案不同。这个问题留给后续分类器去处理就好了,在形状判定阶段不要纠结。

第二种更精细的做法是把区域的外边界和孔洞分开统计,只用外轮廓计算形状,孔洞信息单独编码成一个特征。这种做法的优势是在特征阶段保留更多判别信息,但实现成本稍高,适合对准确率要求极高的场景。

4.3 圆形度在MATLAB里的"开箱即用"实现

function isCircle = isCircleShape(mask, minCircle) stats = regionprops(mask, 'Area', 'Perimeter', 'EquivDiameter'); if isempty(stats), isCircle = false; return; end area = stats.Area; perim = stats.Perimeter; if area < 1000 || perim == 0 isCircle = false; return; end circularity = 4*pi*area / perim^2; isCircle = circularity > minCircle; % 通常取值0.8~0.9 end

实测下来,圆形度在0.85以上时,基本能保证是圆形。三角形大概在0.55到0.65之间,矩形在0.7到0.85之间。如果你发现标志被裁切了一部分,圆度会掉得很快,因此阈值不要定得太极端,0.75到0.85这个区间通常比较合理。

5. 特征提取与分类器训练:HOG+SVM的经典组合细节

形状判定留下的候选区域,已经大概率是交通标志了。但"大概率"不等于"一定",红色圆形也可能是一个红灯笼,黄色三角形也可能是一块工地警示锥。而且即便是交通标志,也还需要知道具体是哪一种:是限速40还是限速60,是禁止左转还是禁止直行?接下来的工作交给分类器。

5.1 HOG特征为什么适合交通标志内部的纹理判别

交通标志内部的信息说到底就是字符、图形、斜杠、箭头这些局部边缘和梯度变化。HOG(Histogram of Oriented Gradients,方向梯度直方图)的核心思想是统计图像局部区域的梯度方向分布,它对边缘形状和局部纹理非常敏感,而且对光照有一定鲁棒性——因为梯度算出来后会对局部块做归一化,能抵消一定的亮度变化。

MATLAB里提取HOG特征可以不自己写,直接用extractHOGFeatures:

[hogFeature, visualization] = extractHOGFeatures(grayImg, ... 'CellSize', [8 8], ... 'BlockSize', [2 2], ... 'NumBins', 9);

CellSize越小,特征维度越高,细节保留越多,但计算量也越大,还容易过拟合。对32x32到64x64的交通标志裁剪图像,我试过8x8的CellSize效果最均衡。整个特征维度大概在1764维左右,对SVM来说毫无压力。

5.2 SVM参数与训练集构造

分类器直接使用fitcecoc(Error-Correcting Output Codes,纠错输出码多分类模型),它底层是多个二分类SVM的组合,MATLAB自带的封装对多分类问题非常友好。

训练集怎么来?如果你不想自己扛着相机去马路边拍标志,有两个替代方案:

第一,下载公开数据集,比如德国交通标志数据集(GTSRB),但里面很多样本是在欧洲交通标志体系下,和国内标志不完全一致。第二,用标定好的检测器在视频帧上自动截取候选框,然后人工筛选分类,这是一种"半自动造数据"的方法,效率高很多。第三种是比较取巧的——利用MATLAB的图像标注器(Image Labeler)配合Automated Driving Toolbox里的Ground Truth Labeler做标注,这样能直接把视频帧中的标志框标注出来,批量导出。

训练时我为每个类别准备大约200到400张样本,正负样本比例如果差距太大,需要做一些合成的数据扩充:旋转、缩放、亮度扰动、添加高斯噪声。这一下能解决很大的泛化问题,很多同学训练集准确率99%,测试集一塌糊涂,多半就是没有做任何数据扩充。

SVM调参方面,MATLAB的fitcecoc里默认用线性核。线性核对HOG特征下训练集几千个样本的分类任务基本够用,训练速度快,也不容易过拟合。如果你想进一步提升精度,可以试一下高斯核,但训练时间会明显增加,而且需要调KernelScale参数。我个人的经验是,HOG特征维度大、数据量又不大的时候,线性核是首选,先跑通全流程,再去考虑核函数的花活。

5.3 把边缘情况交给硬规则补充

分类器不是万能的。某些类别的区分在HOG特征层面本来就非常接近,比如"禁止驶入"(红圈加白横杠)和"限制速度"(红圈加白底数字),两者外部轮廓完全一样,内部只有数字和横杠的区别。这时候除了继续调分类器,还有一个非常有效的思路:在分类器的结果上叠加一个硬规则校验。

比如对于圆形红底标志,你可以使用OCR或模板匹配去验证白色区域的字符是否为数字,如果检测到数字说明是限速类;如果只检测到一条横杠,则判定为禁止驶入。这类规则不需要完美准确,它的作用是给SVM的输出做一次"置信度校正",把边界样本拉回正确类别。实际做下来,这类简单的后处理能提升整体准确率2到5个百分点,非常划算。

6. 工程化与结果分析:把代码组织成可复用的检测框架

到这里,整个识别的技术链路已经完整了:输入图像,HSV颜色分割,候选区域提取,形状判定,特征提取,SVM分类。但真正让这套东西能用于实际测试的,是工程化程度的提升——函数怎么划分,参数怎么调,跑起来怎么加速,结果怎么可视化。

6.1 一个模块化的检测器结构

我下面给出一套可直接套用的函数结构。你先复制这个框架,再去填充实现,比自己从零开始写要省心很多。

% 主流程 function results = trafficSignDetect(img) img = preprocess(img); masks = colorSegment(img); % 返回red_mask, blue_mask, yellow_mask candBoxes = extractCandidates(masks); % 返回结构数组,包含box, mask, color candBoxes = filterByShape(candBoxes); % 形状判定,过滤 for i = 1:length(candBoxes) feat(i,:) = extractHOGFeatureFromBox(img, candBoxes(i)); label = svmModel.predict(feat(i,:)); if isValidByRule(label, candBoxes(i)) results(end+1) = struct('box', candBoxes(i).box, 'label', label); end end end

模块化的好处是,每个函数都可以单独调试。比如你发现"蓝色形状误检率高"时,只改filterByShape里的参数就好了,不用把整个脚本从头跑一遍,排错成本大大降低。

6.2 踩坑:参数对全局影响的连锁反应

有一个非常典型的问题,是你调好红色阈值后,发现蓝色标志检测率暴跌,回头改蓝色阈值,结果黄色标志又崩了。这是因为HSV阈值和形态学结构元素、面积筛选项是全局耦合的。正确做法是把图像归一化尺度固定好,然后单独把每一类的阈值参数封装成一个params结构体,每一次实验都记录一组完整参数,不要只记颜色阈值。

比如:

params.red.hue_low = 0.95; params.red.hue_high = 0.05; params.red.sat_min = 0.30; params.red.val_min = 0.15;

每次实验记得把params保存下来,和输出图像一起存档。否则调了两小时参数后,第二天你根本想不起来上一版"效果好"到底是哪些值配出来的。

6.3 实验评估表:怎么量化你的识别效果

我最后用一段自己拍的校园道路视频做测试,抽了80帧关键帧,统计了这样一个结果:

类别样本数检出数误检数准确率召回率备注
红色禁令(圆形)3533294.3%91.4%漏检多为远处小目标
蓝色指示(矩形)3027390.0%86.7%1个误检来自蓝色广告牌
黄色警告(三角形)1513186.7%80.0%1个漏检是严重遮挡

整体准确率在90%左右。这个数字当然不能和深度学习方法动辄98%相比,但作为课程设计和比赛demo已经够了。而且传统方法一个显著优点是可以清楚解释每个误检的来源,对于答辩时候和老师讲思路非常有帮助——你能说出某个误检是因为蓝色广告牌矩形度和蓝色指示标志太接近,这是深度学习模型给不了的。

7. 进一步的优化方向:跑通行检测到视频流的加速

如果你的项目要求实时处理视频,那就要注意MATLAB本身的性能瓶颈。很多同学把整个流程写在一个巨大的脚本里,对视频的每一帧都重新调用一次彩色图转HSV、连通域分析等函数,结果只有5fps,卡得不能看。

7.1 向量化预处理,减少重复计算

一个简单有效的优化是把视频预处理放到循环外面:先用VideoReader读取视频数据,如果帧尺寸一致,视频帧的背景区域大多数情况下是不变的。对背景建模可以用Vision工具箱里的foregroundDetector,也可以用简单的帧差法,把每帧中"变化的区域"提前裁出来,再在这个区域内做颜色分割。这样能跳过大量纯背景的像素运算,减少明显耗时。

另外,MATLAB的for循环在大矩阵上非常慢,写代码时尽量用矩阵运算。比如判断颜色掩膜时,不要用双重循环遍历每个像素,直接使用:

red_mask = (hsv(:,:,1) < 0.05 | hsv(:,:,1) > 0.95) & (hsv(:,:,2) > 0.3);

这个写法本身就用了向量化,比手工for循环快几十倍。如果你发现自己的代码里还有个嵌套for循环在两三千个连通域上逐个处理,可以考虑用arrayfun或者parfor并行处理。我实测在4核机器上,parfor对候选框的特征提取部分的加速比能到2.5倍左右。

7.2 输出显示和调试可视化

调试阶段,我在每个子函数里都加了"是否可视化"的开关变量,方便随时打开看一眼中间结果。靠这一招,我最快地定位了蓝色误检是天空中云层反光造成的假阳性——在掩膜IMshow里看,云层边缘的蓝色通道响应和标志几乎一模一样。如果没有可视化,这个问题会一直藏在后排,很难想到是这个原因。

给个示例,这是调试时保存中间结果的工具代码:

function saveDebugImg(img, mask, candBoxes, frameIdx, outDir) if ~exist(outDir, 'dir'), mkdir(outDir); end visImg = insertShape(img, 'Rectangle', candBoxes, 'LineWidth', 2, 'Color', 'red'); imwrite(visImg, fullfile(outDir, sprintf('frame_%04d.png', frameIdx))); end

这套做法让我的回查时间从半天缩短到半小时。

8. 总结自己的一点心得体会

把整个项目跑完之后,我最大的感受是:交通标志识别这类传统图像处理项目,代码量并不大,难的是把每一步的"先验知识"如何转化成程序逻辑。交通标志的颜色先验决定了你用HSV分割,形状先验决定了你用几何特征筛选,内部图案的先验决定了你选HOG。这些步骤环环相扣,前面的阈值定得不好,后面分类器再强也事倍功半。

从工程角度来看,还有一个非常值得养成的习惯:每一轮实验一定要保存完整参数,包括颜色阈值、形态学结构元素大小、面积筛选阈值、分类器参数,以及对应的效果截图。这能让你在调参返工的时候不迷路。我之前调试红蓝阈值交错变动导致模型忽好忽坏,最后把所有参数回滚到一个存档版本才恢复,从那以后我就强制自己用params结构体保存整组配置。

最后再补充一个小建议:如果你未来打算往深度学习方法迁移,传统方法做出来的候选框结果可以直接用来生成训练数据——把检测到的标志区域裁剪、缩放、翻转,加上背景负样本,就是一个不错的微调数据集。也就是说你现在做的工作不仅是一份课程设计,同时也是为更复杂的系统打地基的自动化工具。

做MATLAB图像处理这类项目,遇到算法效果差的时候,千万不要急着大改流程,先把自己每一个中间步骤的可视化输出检查一遍。往往问题就出在一个看起来最不起眼的阈值上。基础打得瓷实,后面做任何方向都有底气。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询