☰
基于MATLAB的多色车牌识别系统设计与GUI实现
2026/10/20 7:44:00 网站建设 项目流程

如果你翻过不少车牌识别相关的资料,大概率见过这种套路:先转灰度、再二值化、然后找边缘、最后模板匹配。这套流程跑固定的蓝底白字样例通常没问题,可一旦换成黄底黑字的教练车、绿底白字的新能源号牌,或者光照不太好的图片,整个流程说崩就崩。我做这个基于MATLAB的多色车牌识别系统带GUI界面,目标很明确:用一套代码兼容蓝、黄、绿、白多色车牌,并且所有操作都能在一个图形界面里完成,从打开图片到输出识别结果,每一步都看得见、可复现。

这个系统适合谁用?两类人。一类是正在做图像处理课设或毕设的同学,需要完整可跑的框架和能解释的原理;另一类是刚接触机器视觉的开发者,想了解车牌定位、字符分割、模板匹配这一整套经典流程,又不想一开始就上重型深度学习框架。MATLAB 在这类场景下优势很明显:矩阵运算天然契合图像处理,工具箱里各种函数拿来即用,GUI 界面拖拽就能搭出来,调试和可视化的成本都很低。

1. 项目背景与整体设计思路

1.1 多色车牌识别的难点在哪

车牌识别看似是一个简单的 OCR 任务,真正动手才发现坑非常多。最典型的问题就是颜色多样性。传统方法如果针对蓝色车牌做阈值分割,遇到黄色车牌时,原有的颜色判断逻辑就会失效,因为黄色车牌的字符是黑色的,底色的明度和饱和度跟蓝色差别很大。新能源绿色车牌的出现更让情况变得复杂,它的字符是白色,而且车牌底色本身也有浅绿和深绿之分。

还有光照问题。白天强光下车牌容易反光,傍晚光线不足时颜色饱和度下降,夜间路灯下色温偏暖,这些变化都会让像素值发生偏移。如果直接用 RGB 空间里的固定数值去判断颜色,几乎必然出错。所以多色车牌识别系统的第一个关键点,不是“怎么识别字符”,而是“怎么稳定地把车牌区域从复杂背景里找出来,并且可靠判断它属于哪一种颜色类型”。

另一个难点是字符分割。车牌上包含汉字、字母和数字,汉字本身有左右结构、上下结构,如果只用简单的垂直投影切割,很容易把一个“京”字或者“苏”字切成好几块,最后识别结果完全错乱。字符之间的间距、铆钉、边框干扰也会让分割结果不稳定。这也是我做这个项目时花费时间最多的部分,后面我会专门说处理思路。

1.2 系统流程与工具选型

整个系统的处理流程可以概括为一条链路:

原始图像输入 → 颜色空间转换 → 车牌区域定位 → 底色类型判别 → 图像预处理 → 字符分割 → 字符归一化 → 模板匹配 → 结果输出与 GUI 展示。

这个流程是经典的“传统图像处理 + 模式识别”路线,没有用深度学习。选择这条路线有几个原因。第一,车牌识别属于特定场景,字符集有限,模板匹配足够解决大部分问题;第二,传统方法每一步都可以在 GUI 里可视化中间结果,方便调试和答辩展示;第三,对硬件要求低,普通电脑跑起来毫无压力。

工具选型方面,我用了 MATLAB 的 Image Processing Toolbox,GUI 部分用的是 App Designer。相比老的 GUIDE,App Designer 更现代,组件维护起来也舒服,而且回调函数的结构更清晰。识别部分我用的是模板匹配,配合归一化互相关计算相似度。这里要说明一下:我并不是说模板匹配比其他方法更高级,而是它在“特定环境 + 有限字符集 + 可控光照”的毕设或实验场景下,是最稳妥、最容易解释的方案。

2. 基于HSV色彩空间的车牌定位与多色判别

2.1 颜色空间转换与阈值设计

很多初学者一上来就用 RGB 分量做颜色判断,比如“蓝色就是 B 通道大于 150,R 通道小于 100”。这在理想条件下确实可以,但 RGB 对光照变化极其敏感,稍微加一点阴影,同一个颜色的 RGB 值就变了。我实测下来,白天和晚上拍同一块蓝色车牌,B 通道的数值能相差 60 以上,直接用 RGB 阈值非常不稳定。

所以我把颜色判断放在 HSV 空间完成。HSV 分别表示色相、饱和度、明度,它把“颜色种类”和“亮暗程度”分开,抗光照干扰的能力远强于 RGB。MATLAB 里用rgb2hsv一行就能转换,得到 H、S、V 三个通道,范围都在 0 到 1 之间。注意 H 通道是个环形值,0 和 1 都是红色,所以在设置区间时不能简单用大于小于号,要单独处理红色范围。车牌里虽然很少有红底车牌,但红色字符和红色车标是常见干扰源,这个细节需要留意。

我用的颜色阈值大致如下,这些数值在我的测试集上表现稳定,你可以作为起点再微调:

车牌类型H 范围S 范围V 范围
蓝色0.55 ~ 0.70大于 0.35大于 0.25
黄色0.08 ~ 0.18大于 0.30大于 0.30
绿色0.25 ~ 0.45大于 0.25大于 0.30
白色0 ~ 1小于 0.15大于 0.55

白色车牌的识别比较特殊,因为它的背景基本没有颜色信息,单纯靠色相无法区分,所以我把“低饱和度 + 高亮度”作为白色的判据。实际项目中白色车牌遇到不多,但警车、救护车这类场景可能出现,保留这个分支成本不高,而且能体现“多色”的完整性。

2.2 形态学处理与候选区域筛选

颜色阈值分割之后,得到的是一个二值掩膜图像,白色表示像素颜色落在目标区间内,黑色表示不在。这时候掩膜通常是散乱的,因为车身、路牌、广告牌、其他车辆的贴纸都可能符合颜色条件。直接在这个掩膜上找车牌是不可靠的,必须做形态学处理和连通域筛选。

形态学处理主要用了膨胀和闭运算。车牌区域应该是连成一片的色块,但实际分割时由于反光、泥污、字符遮挡等因素,掩膜内部会出现空洞和断裂。用imclose对掩膜做一次闭运算,可以填充小的空洞,把断开的区域连接起来;再用strel定义一个合适的结构元素,比如 5×5 或者 8×8 的正方形,尺寸太小连接效果不明显,尺寸太大又容易把不相干的区域糊到一起,需要反复试。

处理完形态学之后,用bwlabel标定连通域,再用regionprops提取每个区域的属性。我会重点看面积、宽高比、填充率这三个指标。中国车牌的标准宽高比大约是 3.14,实际拍摄角度不同会有透视形变,所以我将宽高比范围放宽到 2.0 到 4.8。面积方面,设定一个绝对值下限,比如整个图像的千分之一,以及一个相对比值,避免把远处的微小色块当车牌。填充率是区域面积与外接矩形面积的比值,车牌区域比较规整,填充率通常较高,而杂乱的连通域填充率偏低。

筛选之后如果还剩多个候选区域,我会用“宽高比接近 3.14 的程度 + 车牌底色像素占比”两个条件做综合评分,排第一的作为最终车牌区域。这样处理之后,大部分场景下都能准确定位。在下一次迭代时,我还会加一步倾斜校正,因为拍摄角度造成的倾斜会让后面的字符分割出错。

2.3 车牌底色判定与方向校正

车牌区域确定下来之后,紧接着要做的事是判断这块区域到底是什么颜色类型。这个判断结果不仅用于显示,更重要的是决定后续二值化策略。蓝底白字和黄底黑字是完全不同的处理方向:蓝底车牌要做“提取白色字符”,黄底车牌要做“提取黑色字符”,如果不区分底色直接做一个固定阈值二值化,黄底车牌的字符可能直接消失。

我的做法是在车牌区域内再次统计 HSV 像素分布,记录满足蓝色、黄色、绿色、白色条件的像素数量,哪个颜色占比最高,就把车牌判定为对应类型。这里需要注意,统计区域应该排除字符和边框干扰,否则黄色车牌上黑色的汉字也会被算进背景分布里。一个更稳的方案是先用形态学腐蚀去除字符区域,或者只统计车牌区域中间部分,我一般取车牌区域中心 60% 的矩形区域做颜色统计,效果比全区域统计好很多。

方向校正方面,我用了最小外接矩形加边缘拟合的方式。regionprops里的Orientation属性可以给出区域主轴的方向角,如果倾斜角度超过三度,我就用imrotate将车牌转正。方向校正这个步骤如果省略,后面字符分割的垂直投影会变得很不规则,识别率直接下降。所以我的建议是宁可在定位阶段多两步运算,也别把倾斜的车牌直接送进分割模块。

3. 车牌字符分割与识别实现

3.1 投影法字符分割

车牌区域的字符分割,最常用的就是投影法。先用预处理把车牌图像转成灰度图,再用二值化得到黑白图像。接下来做垂直投影,也就是统计每一列里字符像素的数量,字符之间的间隔区域投影值接近零,字符所在区域投影值明显偏高,通过寻找投影中的连段就能把字符切出来。

这个方法原理简单,但实际操作有三个容易踩坑的地方。第一,车牌边框会参与投影,边框和字符连在一起的话,第一个字符会被错误拉宽;第二,铆钉或者车牌上的污渍会产生伪投影峰;第三,汉字因为笔画密集,水平方向上的连续性很强,如果二值化效果不好,汉字和旁边字符之间没有明显的零值间隙,就会切不开。

我的解决办法是:在投影之前先把边框切掉。怎么切?对二值图像做水平投影,车牌上下边界处会出现明显的两条高值带,分别对应上下边框,找到这些高值带后直接裁剪掉第一行和最后几行。左右边框同理,可以通过垂直投影先粗略去除左右两侧可能存在的边框带。切掉边框之后再做正式的垂直投影分割,稳定性会好很多。分割时还需要预设字符数量,普通燃油车牌是 7 个字符,新能源车牌是 8 个字符,如果分割结果不符合数量预期,就说明前面的预处理有问题,应该返工而不是硬着头皮往下走。

3.2 模板库构建与匹配识别

字符分割完成之后,每个字符图块的大小不一样,直接拿原始尺寸去匹配模板非常吃亏。所以需要做尺寸归一化。我统一将字符处理成 40×20 像素,保持宽度和高度的比例关系,使用双线性插值做缩放。归一化之后把所有字符转成二值图,并替换背景为纯黑、字符为纯白,这样匹配时不受背景灰度差异影响。

模板库的构建是这个系统里比较花时间的一步。我的字符集会包括数字 0 到 9,英文字母 A 到 Z,以及各个省份的简称汉字和车牌中可能出现的汉字。需要注意,模板不能只做一份“标准字体”,因为实际车牌上的字体有粗细差异,我在实验中发现最好每个字符准备两到三份变体:一份常规版,一份加粗版,一份轻微模糊版。这样匹配的时候覆盖面更宽,识别率更高。

匹配算法我用了归一化互相关。MATLAB 里可以直接调用normxcorr2,它对光照差异和灰度偏移不敏感,返回的峰值在 0 到 1 之间,越接近 1 表示越相似。每个待识别字符与模板库中所有模板逐个计算相似度,取最高分对应的字符作为识别结果。如果最高分低于某个阈值,我会判定为“未识别”,而不是强行给一个错误的答案。这个“拒识”机制很重要,因为与其输出一个错误字符让整个车牌号看起来合理一半,不如明确提示这块字符识别失败,用户重新调整图片或光照参数,效果反而更好。

3.3 常见字符误判与后处理

模板匹配方法的一个先天弱点是某些字符长相接近,比如数字 0 和字母 O、数字 1 和字母 I、字母 B 和数字 8、字母 Z 和数字 2。如果完全依赖模板得分,很容易出现来回横跳的情况。我的处理方式分两层。

第一层是规则层面的排除。中国车牌字符位置有固定逻辑:第一个字符是省份简称汉字,第二个字符是发证机关字母,后面才是数字和字母的混合。所以如果第一个字符识别出来是数字或字母,可以直接判定为错误;第二位必须是字母;第四位到第七位允许字母或数字。利用这些规则,可以在匹配结果中加入约束,比如第一位和第三位强制不参与字母 O 的候选,从而大幅减少误判。

第二层是上下文修正。把 7 个字符的识别结果分别取出后,再做一个整体合理性检查。比如同一个字符如果连续出现 E 和 F 这样在结构上非常接近的候选,同时相邻字符分数都不高,可以触发一次重匹配,把模板库中的模糊版本模板放在更高优先级。我实测这种后处理能把整体识别率提升五到八个百分点,代价仅仅是多花几十毫秒的计算时间,非常划算。

4. GUI界面设计与交互逻辑

4.1 界面布局设计

GUI 界面的最终效果,很大程度上取决于布局是否合理。我用 App Designer 搭了一个三栏式布局。最左侧是一个大坐标轴,用来显示原始图像和中间处理结果;中间放三个按钮,分别是“打开图像”“开始识别”“清空重置”;右侧是一块结果显示区域,包括识别出的车牌号码、车牌颜色类型、处理耗时,还有一句识别置信度提示。

这里有一个容易被忽略的设计细节:中间结果可视化非常重要。我在界面上加了一个下拉框,可以切换显示原始图、HSV 分割图、车牌定位图、字符分割结果图和最终识别结果图。这个设计对答辩和调试都非常友好,因为别人能看到整个处理逻辑,而不是只看到一个输入和一个输出。我自己调试的时候也依赖这个功能,比如某一步出错了,切换一下图像就能直观发现问题。

组件命名方面,我建议从一开始就按照功能模块来命名,而不是用 App Designer 默认的Button_2、EditField_3这种。虽然默认名也能用,但回调函数一多,你会发现自己都不知道哪个组件对应哪个功能。我会把按钮命名为OpenImageButton、RecognizeButton、ResetButton,坐标轴命名为OriginalAxes、ProcessAxes,这样代码可读性好很多。

4.2 回调函数与数据流动

App Designer 里每个组件的事件触发后都会调用回调函数。核心逻辑写在RecognizeButtonPushed回调里。它的处理顺序是:获取当前坐标轴里的图像数据 → 调用车牌定位函数 → 调用颜色判别函数 → 调用字符分割识别函数 → 更新右侧结果显示和下拉框中间图像。

在数据传递方面,我一直用的是 App 的属性存储方式,也就是在类的properties部分定义一些公共变量,比如originalImg、plateImg、plateType、charCells。每次开启新图片时先把这些属性清空,处理过程中逐步填充。这个做法比用global全局变量安全得多,因为global在多个回调并发调用时很容易出现数据串扰,而且变量生命周期不好控制,调试起来非常痛苦。

文件读取这里有一个我踩过很多次的坑:MATLAB 默认不识别带中文路径的图片,读入后可能是空的或者直接报错。所以我在“打开图像”按钮的回调里,会附加一段路径检查逻辑,如果检测到中文或空格路径,会弹窗提示用户把图片复制到英文路径下再操作。虽然不是特别优雅,但比用户莫名其妙报错要好得多。

4.3 结果显示与保存

识别结果不仅要显示字符串,还要给用户一个直观的反馈。我会在界面上用大号文本显示识别出的车牌,比如“A12345”这样的形式;同时用彩色标签显示车牌底色类型,蓝色就显示蓝色标签,黄色就显示黄色标签。这个效果的实现很简单,就是修改标签控件的背景色属性,但用户体验提升很明显。

我还加了一个“导出结果”的小功能,点击后把识别信息写入一个文本文件,同时把车牌定位图和字符分割图保存成图片文件。这个功能在做批量测试时尤其有用。我每次跑完一批测试图片,直接用导出结果生成一个清单,一目了然地看出哪张图识别成功、哪张图挂了。如果只靠眼睛一个一个看,几十张图下去眼睛就花了。

5. 完整工程结构与核心代码框架

5.1 工程文件组织

这个项目不是单文件程序,我建议把所有代码模块化。我的工程目录结构如下:

  • App/存放 GUI 主程序文件,包括.mlapp文件
  • Functions/存放核心处理函数,包括定位、分割、识别等
  • Templates/存放模板图片库,按字符分类放在子文件夹里
  • TestImages/存放测试图片,覆盖蓝、黄、绿等多种场景
  • Results/存放识别结果和导出文件

模块化带来的最大好处是容易排查问题。比如识别率突然下降,你可以单独调用识别函数对一张字符图测试,而不用每次打开整个 GUI。我强烈建议不要把所有代码都堆在按钮回调里,否则后期加功能时会非常痛苦。

5.2 主流程代码片段

下面这个代码片段展示了核心流程的调用方式,实际项目里参数需要根据你的图像做微调:

function results = detectPlatePipeline(img) % 输入:RGB图像 % 输出:车牌号码、车牌类型、置信度等 hsvImg = rgb2hsv(img); % 1. 分别提取各颜色掩膜 blueMask = createColorMask(hsvImg, 'blue'); yellowMask = createColorMask(hsvImg, 'yellow'); greenMask = createColorMask(hsvImg, 'green'); % 2. 形态学处理并筛选候选区域 plateBox = findPlateRegion(blueMask | yellowMask | greenMask); if isempty(plateBox) results = struct('plateNumber', '', 'plateType', 'none'); return; end % 3. 裁剪车牌并判断颜色类型 plateImg = imcrop(img, plateBox); plateType = judgePlateType(plateImg); % 4. 增强、二值化、字符分割 binPlate = preprocessPlate(plateImg, plateType); charCells = segmentChars(binPlate); % 5. 模板匹配识别 plateNumber = ''; for i = 1:length(charCells) charImg = normalizeChar(charCells{i}); [char, score] = matchTemplate(charImg); plateNumber = [plateNumber char]; end results = struct('plateNumber', plateNumber, ... 'plateType', plateType, ... 'confidence', score); end

createColorMask里实际做的事情就是上一节说的 HSV 阈值判断和形态学处理。segmentChars里包含边框切割、投影分割、字符数量校验三个环节。matchTemplate则是遍历模板目录、计算归一化互相关系数、选最大值。

5.3 关键参数与运行环境

我的开发环境是较新版本的 MATLAB,镜像处理工具箱版本不太重要,但最好保证比较新。normxcorr2这个函数在图像处理工具箱里,没有这个工具箱的话项目跑不起来,这一点要在发布给别人时提前说明。

运行速度方面,一张 800×600 的图片从定位到识别完成,一般在 1 秒到 2 秒之间。主要时间消耗在模板匹配环节,因为每个字符要和上百个模板做相关计算。如果你觉得慢,可以先把模板的尺寸统一缩小,再把模板库做索引分类,比如第一位字符只匹配汉字模板,第二位只匹配字母模板,后面的字符在数字和字母之间匹配,速度能提升不少。效果上不会差太多,因为车牌字符位置本身是有规律的。

6. 常见问题与调试心得

6.1 定位不准:光照、阴影与边缘干扰

定位不准是所有人第一次跑通流程时都会遇到的问题,表现有两种:一是把整块车身颜色当成车牌,二是把路牌、广告牌上的文字区域当成车牌。第一种情况通常是因为车身颜色和车牌颜色太接近,比如蓝色车辆配上蓝色车牌,形态学膨胀之后两者粘连,连通域面积变得特别大。针对这个,我会在筛选连通域时用“宽高比 + 填充率”双重限制,并且限制车牌区域的面积不能超过整个图像面积的三分之一,直接排除这种大色块干扰。

第二种情况多见于路牌和广告牌,它们可能和车牌一样是蓝色底色、白色文字,宽高比也恰好落在区间内。这种情况光靠形态学很难区分,我的办法是检查区域内部的结构特征:车牌内部字符区域更规整,字符之间存在比较均匀的间隔,而路牌上的文字排版往往更松散。不过这个判断逻辑比较复杂,实际项目中我更推荐检查字符分割后的字符数量是否合理,如果 7 个字符的节奏对不上,就回溯到候选区域列表里选下一个候选框。这种“先多候选、后验证”的思路比一次性选定一个区域稳妥很多。

6.2 字符粘连与汉字分割失败

汉字分割失败是非常常见的问题。比如“苏”字左右结构比较松散,垂直投影时中间会出现非常浅的谷值,程序可能错误地把它切成两个字。我在处理时做了两件事。一是预设字符宽度范围,根据车牌总宽度和字符数量估算每个字符的理论宽度,如果某个分割出来的字符宽度不到理论宽度的一半,就判定这次分割无效并合并相邻区域。二是对第一个字符单独处理,不参与通用投影分割,而是直接用连通域分析提取第一个汉字的完整轮廓。

6.3 识别率低与模板匹配的改进

模板匹配的识别率上不去,很多情况下不是匹配算法本身的问题,而是模板和待识别字符之间的差异太大。车牌照在某些角度下字符会发生透视变形,模板却是一个正面的标准字符,匹配得分当然不高。我的做法是在模板库里加入不同粗细、不同模糊程度的变体,对每个字符准备三个版本,匹配时取最高分。如果还不够,可以试试对每个字符做一次轻微的透视校正再匹配,但这个方法计算量更大,我只在测试集里用过,实际 GUI 里没有默认开启。

6.4 GUI 打包与跨机器运行问题

GUI 做完了有些人想打包成可执行文件发给别人用。这里有个常见的坑:打包的时候如果没把模板文件夹一起打进去,别人运行时会报错说找不到模板文件。更隐蔽的问题是,打包后的程序依赖的是编译时刻的快照,如果对方机器上没有安装图像处理工具箱,程序会直接运行失败。解决方法是打包时使用 MATLAB Compiler 的“运行时打包”模式,模板文件夹会被嵌入到资源目录,但模板搜索路径要自定义,不能用相对路径硬编码。我建议打包之前在模板路径上用fileparts(mfilename_fullpath())动态定位,而不是写死一个路径,否则换一台电脑路径就失效了。

调试整个系统时,我最常用的方法是在命令行单独执行每一个函数,加断点看中间变量。GUI 代码里很难直接看到中间图像,所以模块化的好处在这里体现得最明显。我也用了一组大约几十张的测试图片,覆盖了蓝、黄、绿、白四类车牌和正常光照、逆光、倾斜等场景,每次改完代码就跑一遍全量测试,把失败图片单独存到一个文件夹里,逐个看是哪一步出了问题。这个方法很笨,但效率很高。

我个人最后再分享一个经验:刚开始做的时候不要急着上 GUI,先把底层识别流程写成纯函数跑通,确认识别效果稳定了,再花一天时间把 GUI 外壳包上去。这样排查问题时的复杂度会低很多,否则一边调识别算法一边调界面布局,两边都在出错,定位问题会非常痛苦。

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

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

立即咨询