MATLAB交通标志识别系统:基于CNN与HSV颜色分割的完整实现
2026/9/15 20:30:31 网站建设 项目流程

简介:面向计算机、电子信息、数学等专业学生的课程设计与毕业设计需求,这套基于Matlab的交通标志识别系统资源自带GUI操作界面,源码、图片数据集和说明文档齐全。资源包共36个文件,整体约17.78MB,涵盖m源码脚本、mat模型与数据文件、jpg/png/bmp交通标志样本、txt说明文档及fig界面文件,既有可直接运行的程序,也有训练好的识别模型和图片库。目前已有658人学习/浏览,数据集中覆盖指示、警示、禁止三类交通标志,并附有不同颜色通道模型的精度数据;GUI支持选择图片并显示识别结果,便于对照源码理解完整流程。说明文档配合清晰的文件组织,能够帮助快速定位各功能模块,理解从样本预处理、模型训练到GUI集成的实现路径;同时也有较好的扩展空间,适合继续做算法改进、数据集扩充和功能优化,为毕业设计写作与系统演示提供支撑。

1. 交通标志识别系统的拆解思路:从数据集构成到识别流程

打开这个rar,第一眼看到的是三个带颜色的网络权重文件和一堆散落的脚本、图片。这套基于Matlab实现的交通标志识别系统,解决的是一件很具体的事:输入一张含交通标志的照片,输出标志的语义类别,并且通过GUI操作界面让整个过程可见。和现在流行的YOLO方案不同,它没有目标检测的边框回归,而是先用颜色把红、蓝、黄三类标志分开,再用三个小型CNN分别识别。这个思路在毕业设计和课程设计里很常见,优点是训练快、代码可读性好,缺点是要求标志在画面中有一定占比。理解了这个前提,后面看mcnn.m和Traffic_Iden.m才不会觉得结构奇怪。适合的人群是计算机、电子信息工程、数学等专业做课程设计或毕业设计的学生,把它当作二次开发的起点,而不是拿来即用的商业产品。

2. 数据集组织与颜色分割预处理:从三类文件夹到HSV掩膜

2.1 数据目录与标签映射

资源里的图片直接按“禁止类、警示类、指示类”三个文件夹存放,里面混着png、jpg、bmp三种格式。文件名一部分带语义,比如“18_靠左侧道路行驶015.bmp”,另一部分则是“0185.png”这类纯编号。这种组织方式维护起来直观,但直接喂给imageDatastore时,文件夹名只能生成大类标签,不能支撑细粒度识别。所以在训练之前,我会先写一个加载函数,把三类的图片路径收集起来,同时建立从文件名到最终类别名的映射表;对于没有语义关键词的文件,用大类名兜底,至少保证数据能跑通。

function [imgs, labels] = load_traffic_images(rootPath) folders = {'禁止类', '警示类', '指示类'}; imgs = {}; labels = {}; for i = 1:length(folders) folderPath = fullfile(rootPath, folders{i}); files = dir(fullfile(folderPath, '*.png')); files = [files; dir(fullfile(folderPath, '*.jpg'))]; files = [files; dir(fullfile(folderPath, '*.bmp'))]; for j = 1:length(files) imgs{end+1} = fullfile(folderPath, files(j).name); %#ok<AGROW> labels{end+1} = folders{i}; %#ok<AGROW> end end end

这段代码把三种常见位图格式全部扫出来,存的是绝对路径而不是图像矩阵,避免一次性载入大量高分辨率图片把内存占满。labels直接用文件夹名,后面的细粒度拆分可以在这个基础上按文件名关键字重新赋值。资源中redname.mat、bluename.mat、yellowname.mat里保存的,通常就是每个颜色桶对应的细粒度类别名字符串数组,加载后用categorical转换即可。

2.2 颜色空间选择与HSV阈值分割

为什么这套系统要分别保存rednet_cnn.mat、bluenet_cnn.mat、yellownet_cnn.mat三个模型?因为交通标志的底色有很强的颜色先验:红色表示禁止或者部分警告,蓝色表示指示,黄色表示警告。直接在RGB空间分割颜色,亮度变化会让阈值很难稳定;我一般会转HSV,把亮度信息剥离出来,用H分量做主判断,再用S和V过滤掉低饱和度和过暗的区域。

红色在HSV色相环上比较特殊,H=0附近被一分为二,所以掩膜条件要写成两个区间取或。下面的示例函数返回一个二值掩膜,用于后续裁剪标志区域:

function mask = extract_color_mask(img, colorType) hsvImg = rgb2hsv(img); H = hsvImg(:, :, 1); S = hsvImg(:, :, 2); V = hsvImg(:, :, 3); switch colorType case 'red' mask = ((H < 0.05) | (H > 0.95)) & (S > 0.3) & (V > 0.2); case 'blue' mask = (H > 0.55) & (H < 0.70) & (S > 0.3) & (V > 0.2); case 'yellow' mask = (H > 0.10) & (H < 0.20) & (S > 0.3) & (V > 0.2); end mask = imclose(imopen(mask, strel('disk', 3)), strel('disk', 5)); end

这里用strel构造圆盘形结构元素,开运算去掉孤立噪点,闭运算填补标志图形内部的细小裂缝。结构元素半径需要根据图片分辨率调整,如果你用的是手机拍摄的大图,半径3到5可能不够,我一般先统一缩放到宽度约400像素再做分割。HSV的参考区间如下表,注意这些值只适合作为起点,换一批照片后必须重新标定。

颜色H 范围S 最小V 最小备注
红色<0.05 或 >0.950.300.20红色在色相环两端
蓝色0.55~0.700.300.20覆盖深蓝和浅蓝
黄色0.10~0.200.300.20与橙色交界容易混

2.3 尺寸统一与数据增强

CNN的输入尺寸必须固定。资源里的png、jpg、bmp分辨率参差不齐,常见做法是从原图中按掩膜外接矩形裁剪出标志区域,缩放到64x64或32x32。缩放时最好保持宽高比,缺的部分用黑色或灰色填充,否则圆形标志会被压成椭圆,引入不必要的变形。

对于几百张规模的毕业设计数据集,直接训练CNN很容易过拟合。数据增强是必须的,我常用Matlab的augmentedImageDatastore做在线增强,这样不会在磁盘上生成额外的图片副本:

augImds = augmentedImageDatastore([64 64], trainImgs, trainLabels, ... 'DataAugmentation', imageDataAugmenter(... 'RandRotation', [-15 15], ... 'RandXTranslation', [-4 4], ... 'RandYTranslation', [-4 4], ... 'RandScale', [0.9 1.1]));

这段代码里RandRotation控制在±15度,是因为交通标志是标准化图形,旋转幅度太大会让模型学到不存在的姿态。RandScale控制在0.9到1.1,模拟拍摄距离的小幅变化。增强参数不是越大越好,过多的人为扰动反而会让验证集精度异常波动,这一点在后面的调试部分还会提到。

3. 基于颜色专属CNN的模型训练与推理:redmodle、bluemodle、yellowmodle的分工

3.1 网络结构选型:为什么用小型CNN而不是YOLO

现在聊到交通标志识别,很多人第一反应是YOLO。但这个资源里没有目标框标注,所有图片只有分类标签,YOLO依赖的box监督信号完全缺失。另一个原因是毕业设计运行环境一般就是普通笔记本,YOLO系列哪怕是轻量版,训练一轮的时间也远超小CNN。所以这里用三个小型CNN分别负责红、蓝、黄三类,是数据条件和算力约束下的合理选型。

这套“multi-CNN”结构从mcnn.m这个文件名也能猜出来:三个网络结构一致,只是训练数据不同。我的习惯是用两个卷积层加两个全连接,参数总量控制在百万以内。参考结构如下表:

类型参数输出尺寸
1卷积8个3x3,padding same64x64x8
2ReLU + 最大池化池化2x2 stride 232x32x8
3卷积16个3x3,padding same32x32x16
4ReLU + 最大池化池化2x2 stride 216x16x16
5全连接6464
6全连接类别数N

这个深度对交通标志识别足够,因为颜色分割已经把大部分背景剔除,网络真正要学的是标志内部的箭头、数字和符号组合。如果后面想提高准确率,优先加第三个卷积层而不是加大全连接,全连接层越大越容易过拟合。

3.2 redmodle.m / bluemodle.m / yellowmodle.m 的训练参数设置

资源中的redmodle.m、bluemodle.m、yellowmodle.m是三个独立训练脚本,分别读取红色、蓝色、黄色数据集并生成对应的net_cnn.mat。这里分享一个我在写这类训练脚本时的标准做法:先按标签分层划分训练集和验证集,再定义网络层和训练选项,最后保存网络。

rng(2024); [trainSet, valSet] = splitEachLabel(augImds, 0.8, 'randomized'); layers = [ imageInputLayer([64 64 3], 'Normalization', 'none') convolution2dLayer(3, 8, 'Padding', 'same') reluLayer() maxPooling2dLayer(2, 'Stride', 2) convolution2dLayer(3, 16, 'Padding', 'same') reluLayer() maxPooling2dLayer(2, 'Stride', 2) fullyConnectedLayer(64) reluLayer() fullyConnectedLayer(numClasses) softmaxLayer() classificationLayer()]; options = trainingOptions('sgdm', ... 'InitialLearnRate', 0.001, ... 'MiniBatchSize', 32, ... 'MaxEpochs', 20, ... 'ValidationData', valSet, ... 'Plots', 'training-progress'); net = trainNetwork(trainSet, layers, options); save('rednet_cnn.mat', 'net');

splitEachLabel加'randomized'参数是为了打乱样本顺序,避免同类图片连续出现导致一个batch内全是同一类。InitialLearnRate用0.001是sgdm的常规起点,如果你的数据集只有几十张,建议降到0.0005并适当增加epoch。训练完成后保存的是net变量本身,GUI加载时直接load('rednet_cnn.mat', 'net')就能用。三个脚本唯一的区别是数据源和保存文件名,所以复制修改时留意别把red保存成blue。

3.3 推理流程与精度矩阵的生成

模型训练完,下一步是在测试集上算精度。资源里的redprecision.mat、blueprecision.mat、yellowprecision.mat应该就是不同颜色模型的评估结果。我一般会保存整体精度和每类精度,而不是只存一个标量,这样答辩时能说清楚哪个类别拖了后腿。

function [acc, perClassAcc, C] = evaluate_model(net, testImgs, testLabels) pred = classify(net, testImgs); C = confusionmat(testLabels, pred); acc = sum(diag(C)) / sum(C, 'all'); perClassAcc = diag(C) ./ sum(C, 2); save('redprecision.mat', 'acc', 'perClassAcc', 'C'); end

confusionmat输出的是混淆矩阵,每一行代表真实类别,每一列代表预测类别。如果某一行精确率明显低于其他行,说明该类样本太少或者类内差异太大。perClassAcc是逐类精度,注意当某一类在测试集中一个样本都没有时,sum(C,2)会得到0,导致NaN,所以要在保存前检查测试集标签的分布是否均衡。

4. GUI操作界面与识别链路整合:从Traffic_Iden.fig到mcnn.m主控

4.1 GUI布局与回调函数设计

Traffic_Iden.fig和Traffic_Iden.m是配套的GUI文件,前者存界面布局,后者存回调函数。这是传统guide方式生成的工程,Matlab新版本打开.fig时会提示用guide命令编辑,但不要重新生成,直接双击进入布局编辑器即可。界面至少要有三个部分:显示原图的axes、显示预处理结果的axes、触发识别的按钮和显示文本。

控件的Tag属性是回调函数定位控件的唯一依据,布局时就要把Tag改成有意义的名称,比如btnLoad、btnRecognize、txtResult,而不是默认的pushbutton1。我的界面控件习惯如下表:

控件Tag类型作用
axesOriginalaxes显示原始图片
axesProcessedaxes显示裁剪和预处理后的标志区域
btnLoadpushbutton打开文件选择对话框
btnRecognizepushbutton执行识别流程
txtResulttext显示识别类别和置信度

回调函数的签名是统一的,以加载图片按钮为例:

function btnLoad_Callback(hObject, eventdata, handles) [filename, pathname] = uigetfile({'*.jpg;*.png;*.bmp', '图片文件'}); if isequal(filename, 0), return; end imgPath = fullfile(pathname, filename); axes(handles.axesOriginal); imshow(imgPath); handles.currentImgPath = imgPath; guidata(hObject, handles); end

uigetfile返回两个值,用户取消时filename是0,所以要提前返回。imshow直接作用于handles.axesOriginal指定的坐标区。最后一行guidata非常关键,它把更新后的handles写回GUI的handles结构,否则btnRecognize的回调里读不到currentImgPath这个字段。

4.2 mcnn.m主控逻辑:颜色判断与模型选择

mcnn.m负责把预处理、颜色判断、推理串起来。阅读这个脚本,建议从recognize_from_gui这个入口函数开始看,它接受图片路径和一个包含三个模型的models结构体,返回识别结果结构体。

function result = recognize_from_gui(imgPath, models) I = imread(imgPath); areas = [ sum(extract_color_mask(I, 'red'), 'all'), sum(extract_color_mask(I, 'blue'), 'all'), sum(extract_color_mask(I, 'yellow'), 'all')]; [~, idx] = max(areas); colorNames = {'red', 'blue', 'yellow'}; colorType = colorNames{idx}; [label, score] = predict_traffic_sign(I, colorType, models); result = struct('ColorType', colorType, 'Label', label, 'Score', score); end

这段代码先分别统计三种颜色掩膜的像素数,哪个区域面积最大就交给对应的颜色模型。逻辑很朴素,适合标志区域占画面主导的场景。如果图片里同时出现多个不同颜色的标志,max(areas)只会选中一个,这个局限可以在说明文档.txt里注明,答辩时老师问了也好解释。

predict_traffic_sign是实际推理函数,注意模型参数从外部传入:

function [label, score] = predict_traffic_sign(I, colorType, models) I = imresize(I, [64 64]); switch colorType case 'red' [label, score] = classify(models.red.net, I); case 'blue' [label, score] = classify(models.blue.net, I); case 'yellow' [label, score] = classify(models.yellow.net, I); end end

classify输出的label是categorical类型,score是概率向量。这里直接用imresize压缩整张图而不是裁剪标志区域,是因为掩膜面积最大的颜色基本对应主标志,整图缩放后标志仍然占主导。如果你的GUI里裁剪了区域,就把这段改成先裁剪再resize。

4.3 在GUI启动时加载模型与相对路径坑

模型加载放在按钮回调里是最差的实践,因为每点一次识别按钮都要读一次几十MB的mat文件。我习惯在GUI的OpeningFcn里一次性加载三个模型,存到handles.models:

function Traffic_Iden_OpeningFcn(hObject, eventdata, handles, varargin) handles.models = load_all_models(); handles.currentImgPath = ''; guidata(hObject, handles); end function models = load_all_models() models.red = load('rednet_cnn.mat', 'net'); models.blue = load('bluenet_cnn.mat', 'net'); models.yellow = load('yellownet_cnn.mat', 'net'); end

load_all_models返回的结构体里每个字段都是一个带net字段的结构体,所以上面predict时用models.red.net。这里要提醒另一个坑:load中的相对路径是相对于当前工作目录的,GUI双击运行时工作目录经常不在源码目录。保险做法是用mfilename('fullpath')先取到脚本绝对路径,再拼接模型文件名,这样无论从哪个目录启动都不会找不到文件。

注意:如果最终用Application Compiler打包成exe,需要把三个net_cnn.mat和三个precision.mat作为附加资源加入,否则运行时会报“找不到文件”。

5. 精度评估与调试技巧:从precision文件到多尺度投票

5.1 从redprecision.mat这类文件反推问题

redprecision.mat这类文件,答辩时老师一定会问“精度怎么算的”。我的习惯是保存成结构体,包含整体精度、每类精度和测试样本数三项。如果某类精度特别低,先别急着调网络,看该类样本数和图片质量。样本太少时精度低是正常的,优先补数据而不是加层。混淆矩阵里如果“禁止”类样本被大量分到“警示”类,多半是颜色分割阶段把红色的两个子类混在一起了,而不是CNN的问题。

5.2 常见误判与阈值调整

第一个常见误判:红色和深橙色在HSV中H值靠得很近。预处理时我会把红色H区间从两端各收窄0.01,同时把S下限提高到0.35,减少橙色背景的干扰。

第二个常见误判:识别结果置信度不高,但系统仍给出一个标签。默认classify会取分数最高的类别,哪怕它只有30%的概率。我一般在GUI展示时加一个置信度门槛,低于阈值显示“无法识别”:

[~, scores] = predict(net, I); [maxScore, idx] = max(scores); if maxScore < 0.6 resultLabel = '无法识别'; else resultLabel = classNames{idx}; end

阈值0.6不是拍脑袋定的,我通常先在验证集上画出置信度分布,选一个能把“正确且高置信”和“错误但高置信”分开的点。如果你发现很多正确样本分数也在0.6以下,就降到0.5,同时检查是不是数据增强过度了。

5.3 一个提高鲁棒性的小技巧:多尺度投票

最后一个调试技巧适合在答辩演示前用,成本很低但效果明显。交通标志在照片中大小不一,模型对训练时见过的尺度敏感,测试图一旦比训练图大很多或小很多,精度会掉。我通常会把测试图缩放到几个不同尺寸分别识别,然后投票决定最终类别。

function finalLabel = multi_scale_vote(I, net, classNames) scales = [0.8, 1.0, 1.2]; votes = zeros(length(classNames), 1); for k = 1:length(scales) resizedI = imresize(I, scales(k)); resizedI = imresize(resizedI, [64 64]); [~, scores] = predict(net, resizedI); [~, idx] = max(scores); votes(idx) = votes(idx) + 1; end [~, finalIdx] = max(votes); finalLabel = classNames(finalIdx); end

这段代码里imresize了两次,第一次按比例缩放模拟不同拍摄距离,第二次强制拉回网络输入尺寸。两次缩放叠加后,等效于在原图上做不同细节保留的重采样,起到了类似数据增强的作用。投票数太少时可能平票,我会在votes累加时同时累加scores,按总分打破平局。这个技巧对GUI代码的改动只有一行:把classify换成multi_scale_vote即可。

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

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

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

立即咨询