MATLAB车牌识别实现:从HSV颜色空间定位到模板匹配识别
简介:基于 MATLAB 的车牌识别系统源码包,面向高校图像处理与模式识别课程设计、毕业设计场景,实现从车牌图像到车牌号码的自动识别。资源覆盖预处理、模板匹配、字符分割、数字与汉字识别等完整流程,提供多个可直接运行的 .m 文件,适合学生对照学习算法的工程实现。
压缩包共 62 个文件,约 554KB。其中 10 个 .m 源文件分别承担读图、分割、识别等核心功能;45 个 .bmp 构成数字、字母及省份简称模板库,用于模板匹配与字符识别;5 个 .jpg 是测试车牌图像;另有 .asv 自动备份与 .xls 结果记录文件,方便调试与对比。已有 249 人学习下载。
通过阅读和运行源码,可理解 Canny 边缘检测、Hough 变换、连通成分分析、OCR 分类器等知识点,并观察不同光照和角度下的识别效果。对需要完成车牌识别课题的同学,这份源码包能提供完整的 MATLAB 工程骨架、字符模板集和测试样例,显著降低从零搭建环境的成本。
1. 车牌识别MATLAB实现:一套从定位到识别都能跑通的完整代码
做车牌识别这件事,最怕的不是算法论文看不懂,而是打开一个号称“全能”的代码包,跑起来全是报错,连第一步图片路径都改不利索。这套MATLAB实现的车牌识别系统,我拆过之后的第一感受是:它把“车牌定位→字符分割→字符识别”这条主线完整串起来了,用的不是花哨的深度学习方法,而是工业界和课程设计里最常见的颜色空间分割加模板匹配方案。它对环境的要求很低,不需要GPU,不依赖额外的深度学习工具箱,只要MATLAB基础环境就能跑。适合正要交课程设计、做毕业设计,或者想快速验证车牌识别思路的工程师。代码里每一步都有中间结果输出,参数调整能看到即时反馈,这一点比很多“黑匣子”式项目要友好得多。
2. 车牌定位与预处理:从颜色空间到形态学操作的选型逻辑
2.1 颜色空间选型:为什么多数方案停在RGB而实战常转HSV
车牌定位的第一步是把车牌区域从整张图片里找出来。常见做法有两种:一种是基于边缘检测,利用车牌字符和底色的强对比度找矩形框;另一种是基于颜色分割,利用车牌底色(国内蓝牌、绿牌、黄牌都有固定色调)直接筛区域。这套MATLAB实现选择的是颜色分割路线,但它没有直接拿RGB做阈值,而是先把图像转成HSV空间,这一步非常关键。
RGB三个通道对光照极其敏感,同样一块蓝色车牌,上午十点拍和傍晚六点拍,RGB数值可能差出整整一倍。HSV空间把色相(H)、饱和度(S)、明度(V)分开,其中H通道受光照影响最小,蓝色在H通道里稳定落在0.55到0.68这个区间(对应角度约198度到245度)。所以代码里的第一段核心逻辑就是这样:
hsvImg = rgb2hsv(I); % 输入RGB图像,转到HSV空间 h = hsvImg(:, :, 1); % 色相通道 s = hsvImg(:, :, 2); % 饱和度通道 v = hsvImg(:, :, 3); % 明度通道 % 蓝色车牌范围,H在0.55~0.68,S和V做宽松过滤 mask = (h >= 0.55) & (h <= 0.68) & (s >= 0.35) & (v >= 0.15);这段代码的意思是生成一个二值掩膜mask,像素满足条件记为1,不满足记为0。H的范围根据蓝色车牌的实际色调标定,S大于0.35是为了去掉偏灰白的浅蓝区域,比如天空和浅蓝色车身;V大于0.15是为了去掉接近黑色的暗部噪声。你看到的蓝色范围不是拍脑袋写的,而是统计了不同时段、不同角度下蓝色车牌的H值分布得到的。如果识别绿色新能源车牌,把H范围改成0.25到0.45,S下限可以放宽到0.3,其余逻辑不用动。
这里有个容易踩的误区:有人直接用H通道做单阈值分割,不约束S和V,结果把深蓝色的货车车厢、蓝色广告牌全圈了进来。代码在定位阶段故意把S和V加进来,宁可漏掉一些边缘模糊的车牌,也不引入大量干扰区域,因为后面还有形态学操作和连通域筛选来兜底。
2.2 形态学操作与连通域筛选:定位车牌区域的三步套路
拿到蓝色掩膜之后,图像里通常是一堆离散的蓝色像素块,车牌区域虽然明显,但边缘有很多毛刺,中间也可能因为字符颜色(白色)把掩膜切断。这时候直接找连通域会得到一堆碎片。代码在这里做了三步形态学处理,顺序有讲究:
% 第一步:先闭运算,把字符区域造成的空洞填上 se1 = strel('rectangle', [5, 3]); maskClosed = imclose(mask, se1); % 第二步:开运算,去掉细小的孤立噪声 se2 = strel('rectangle', [1, 5]); maskOpened = imopen(maskClosed, se2); % 第三步:连通域分析,按宽高比和面积筛选候选区 cc = bwconncomp(maskOpened); stats = regionprops(cc, 'BoundingBox', 'Area', 'AspectRatio');第一步用5×3的矩形结构元素做闭运算,矩形结构元素比圆形更适合车牌这种扁长形状,5行3列的设计思路是:横向跨度略小,纵向跨度略大,这样能把字符间的白色缝隙填平,但又不会把两个相邻车牌粘连在一起。第二步用1×5的结构元素做开运算,意思是在横向上做细长条的开操作,去掉那些横跨几个像素的细线噪声,这些噪声常来自车窗边缘的反光。
闭运算和开运算的先后顺序不能反,如果先开运算再去闭运算,噪声会被闭运算重新放大,导致最后候选框里混入大量非车牌区域。筛选逻辑也值得说:车牌的宽高比大约在3:1到4:1之间,面积不能太小,代码里一般会加一个条件,候选区域面积要占全图面积的0.5%到5%之间,低于这个范围的多半是远处的小噪声块,高于这个范围的多半是蓝色车身。如果车牌本身尺寸很大,超过5%的上限怎么办?实际处理中会把上限放宽到10%,或者改成先按面积排序,取最大的三个候选再做宽高比过滤。参数灵活一点,能适配更多场景。
2.3 定位参数的调整:摄像头角度变化时该改哪些阈值
定位这块的参数,我建议你按自己的图像来源先做一轮标定,别直接用默认值跑。代码里暴露出来的参数主要是三个:H的上下限、S下限、形态学结构元素尺寸。如果图像来自停车场固定机位,车牌角度正、光照均匀,H范围收窄到0.57到0.65就够用,S下限可以提到0.45,这样误检率最低。如果图像来自手持相机或行车记录仪,角度随时在变,车牌可能在画面里倾斜,这时候H范围放宽到0.52到0.70,S下限降到0.30,宁可多检几个候选框,让后面的分割阶段去排除。
倾斜车牌是定位阶段最考验细节的地方。代码里如果没有倾斜校正,直接用正矩形框裁剪出来的车牌,字符可能是歪的,分割和识别都会受影响。常见的处理办法是用regionprops输出候选框的角度信息,或者对裁剪出来的车牌区域做Radon变换检测倾斜角,再做一次仿射旋转。这套代码在定位阶段预留了角度输出的接口,如果你发现识别率上不去,先检查是不是倾斜校正没做。我一般会写一个调试片段,把每个候选框画在原图上,框的四个角点连起来,肉眼看一眼就知道倾斜校正有没有生效。
3. 字符分割与归一化:连通域分析之外的边界处理方法
3.1 投影法与连通域法怎么选:车牌字符粘连时的分支逻辑
定位出的车牌区域是一张包含车牌底板和字符的彩色小图,下一步要把七个字符一个个切出来。字符分割主流有两条路:垂直投影法和连通域分析法。这套MATLAB实现用的是垂直投影为主、连通域为辅的混合策略,这个选择是合理的。
垂直投影法的思路是把二值化后的车牌图像按列求和,每一列的白色像素个数形成一个波形,字符区域列方向上的像素多、波峰高,字符间隔的位置像素少、波谷低,按波谷切开就能得到字符边界。它的优点是对字符间距均匀的车牌特别稳定,缺点是一旦字符粘连(比如两个字符之间被噪声搭桥),波谷会被填平,切分位置就会偏。连通域法直接找每个字符的独立连通区域,对粘连问题更敏感,但对字符断裂情况反而容易把一个字符拆成两截。
这段代码里有一个巧妙的判断分支:先做垂直投影,统计投影波形的极值点数量,如果极值点数量接近7个,说明字符基本分开,直接用投影切分;如果极值点数量明显少于7个,说明存在粘连,自动切到连通域分析模式,把每个连通域的外接矩形提取出来,再按宽度聚类合并。这个分支逻辑有效避免了大面积翻车,代价是代码里多了一小段判断逻辑。
% 统计垂直投影 colSum = sum(bwImg, 1); % 找波谷位置,即投影值低于阈值的列 threshold = max(colSum) * 0.3; valleyIdx = find(colSum < threshold); % 根据波谷把字符区间切出来 splitPoints = [0, find(diff(valleyIdx) > 1), size(bwImg, 2)]; charBounds = arrayfun(@(k) [valleyIdx(splitPoints(k))+1, valleyIdx(splitPoints(k+1))], ... 1:length(splitPoints)-1, 'UniformOutput', false);这段代码里threshold设成最大投影值的30%,意思是只要某一列的白色像素数少于最大列的30%,就认为这里是字符间隙。30%这个经验值能扛住大部分光照不均的图,但如果字符笔画特别粗,或者车牌底色偏脏,间隙处的投影值可能超过30%,这时候建议把阈值降到15%。此外我用过一段改写版本:在切分前先二值化图像中只保留字符可能存在的区域,比如用行投影先把车牌上下边界收窄,再做列投影,这样投影波形更干净,波谷位置更准确。
3.2 去除铆钉和边框:分割前必须做的两个预处理
很多第一次做车牌识别的人会在分割阶段被铆钉和边框坑得很惨。车牌上固定用的铆钉是金属色,在灰度图上和白色字符的亮度很接近,二值化后铆钉会变成一个小白点;车牌外边框又是连续的矩形线框,在垂直投影上会形成两条非常高的波峰,直接导致投影波形里多出两个假字符,连字符数都数不对。
这段代码的预处理顺序是:先把车牌灰度图做一次中值滤波,窗口选3×3,作用是平滑掉铆钉的强高光;然后做边缘腐蚀,用结构元素把外边框的细线腐蚀掉;最后才是二值化。中值滤波不会伤害字符边缘,但会让铆钉的高光区域变小,配合形态学腐蚀基本能消除铆钉影响。外边框更麻烦,因为边框是连续的,普通腐蚀要花很多轮才能掉干净,所以代码用的是先定位上下边框位置、直接用行像素置零的办法,只在字符区域的上下边界保留原有灰度值。
另一个容易忽略的坑是:车牌底部的汉字区域往往有防伪花纹或小字,二值化后会在投影上产生额外噪声。代码里针对这个情况加了一个限定:分割前统计字符高度,只保留位于字符高度范围内的像素。具体做法是先按行投影找到字符区域的上下边界,然后把这个边界外的行全部置零,这样防伪花纹即使被二值化也不参与后续分割。
3.3 字符归一化:为什么统一到40×20而不是直接resize
字符切出来之后,尺寸差异很大——同一个车牌上,汉字大、字母数字小,而且字符的高度宽度比并不一样。识别阶段要拿这些字符和模板匹配,尺寸不统一根本没法比。这套代码归一化的目标尺寸是40×20(高度40像素,宽度20像素),注意它没有无脑resize,而是先做等比缩放到目标高度,再在左右两侧补零到目标宽度,这一点很关键。
为什么不直接拉伸到40×20?因为直接拉伸会改变字符的宽高比,比如一个本身接近正方形的“A”被拉成2:1的长条形,特征完全变形,模板匹配的相似度计算会被严重干扰。等比缩放配合补零,保持了字符的自然比例,宽度不足的部分用黑色背景填充。汉字和常规字符还有一点区别,汉字的笔画多、结构密,40×20的网格对汉字来说横向偏窄,所以在单独处理汉字字符时,代码会另存一种归一化方案:目标尺寸用32×32,等比缩放后补零,汉字模板按32×32保存。
归一化之后字符图像就可以直接和模板库做匹配了。我建议你在这里留一个可视化输出,把归一化前后的图像打印到一个figure里对比看看,确认缩放逻辑没有把字符裁掉边缘。这一步是后面模板匹配正确率的基础,很多识别失败的问题追根溯源都在归一化时把字符的一部分挤出了边界。
4. 字符识别:模板匹配为主、特征提取兜底的实现方案
4.1 字符库构建:把标准模板做成可增量更新的数据集
识别阶段的整个地基是字符模板库。代码里的模板不是写死在代码里的,而是放在一个templates目录下,按类别分子文件夹,比如字符“A”的模板文件就是A_1.png、A_2.png,同一个字符存多张不同字体、不同粗细的模板,匹配时取最高得分作为该字符的最终得分。这个设计思路非常实用,它把模板库和识别逻辑解耦了,后期想增加新字符、新字体样本,直接往文件夹里丢图片就能生效,不用重新编译。
模板的来源一般有两种:一种是从公开的标准车牌字符图片里截取,另一种是从实验数据里实际切出来的字符经过人工标注后保存。第二种来源的效果通常更好,因为模板和待识别字符的成像条件一致,都经历了同样的镜头畸变和光照退化。这套代码在第一次跑的时候会执行一个脚本,扫描templates目录下的所有图片,统一归一化到40×20或32×32,把数据存入一个MAT结构体里。模板库的命名规则建议直接这么干:字母数字用单字符命名,汉字用“省份简称拼音”加字符命名,跑注册脚本时自动重命名也可以。
这里有一个细节:模板里的字符必须和实际识别字符的底色一致。车牌字符是白色,模板也应该是白色字体、深色背景,如果混入了黑底白字和白底黑字两种模板,匹配时会出现系统性偏差。
4.2 模板匹配的评分策略:相关度与差异度的取舍
字符图像和模板图像做比较,最原始的方法是把矩阵相减,算差的绝对值和,但这个指标容易被图像整体亮度变化欺骗。两张同一字符的图像,一张亮度调低一半,像素差绝对值会整体上升,但是形状并没有变。代码里选择了归一化互相关系数(normxcorr2)作为主评分指标,它能衡量两幅图的形状相似度,而不是像素差的绝对大小。
% img是归一化后的待识别字符,template是模板图像 c = normxcorr2(img, template); % 取相关系数矩阵的最大值作为该模板得分 score = max(c(:));normxcorr2计算时会遍历所有可能的平移位置求相关系数,最大值天然带有对轻微位移和错位的容忍。语音识别里常用动态时间规整处理时序错位,图像识别里没有这种问题,但normxcorr2也做了一件类似的事:它不强制两张图的边缘严格对齐,只要形状轮廓走势一致,相关系数就会偏高。
不过normxcorr2的输出范围是-1到1,模板和字符完全一致时为1,完全反相时为-1。实际代码里score能稳定在0.75以上基本就能确定字符归属。如果几个候选模板的得分差距小于0.05,直接取最高分就容易误判。代码在这里加了第二个判据:对得分前两名的模板,再算一次不相似度(用归一化后图像的像素差值百分比),如果最高分和第二名的相关系数差得不多,但像素差值百分比差异明显,就改选像素差值更小的那个。这个双判据相当于在形状和像素两个层面做了综合表决,能压掉一小部分误判。
4.3 汉字识别单独处理:为什么“京”和“津”容易翻车
车牌第一个字符是汉字省份简称,这是识别最容易翻车的地方。汉字笔画多,结构复杂,像“京”“津”“冀”这几个字本身笔画数不同,但结构上有相似的横竖撇捺布局,归一化到32×32后细节信息有损,互相之间的相关系数差异会被压缩,导致误匹配率比字母数字高一个量级。字母数字总共34类,汉字有几十个类,类间相似度更高,识别难度天然就大。
代码的应对策略是给汉字单独走一条识别路径:汉字匹配时用的模板尺寸是32×32,匹配特征上也做了升级——不只保留二值化后的形状,还提取了字符的骨架特征。骨架提取的方法是bwmorph操作,反复细化连通区域直到单像素宽度,这样得到的骨架对笔画粗细不敏感,而笔画粗细恰恰是不同字体下同一个汉字差异最大的地方。汉字匹配时,相关系数只占60%权重,骨架相似度占40%,这样对字体的泛化能力更好。
还有一个实用的小技巧:车牌字符的语法规则本身就可以当纠错信号来用。国内车牌第一个字符必须是汉字省份简称,第二个字符必须是字母,第三到第七个字符是字母数字混合,但字母I和O一般不出现。代码在识别结果出来后加了一个过滤:如果第二到第七位识别出了I或O,强制改成对应的1或0;如果第一位识别出来的结果不在合法汉字简称集合里,就跳到次高分的候选。这个规则不是纯算法层面的,但能有效把识别率拉高几个百分点,成本几乎为零。
5. 车牌识别常见问题排查:5条实测踩坑记录
5.1 定位框漂移:蓝底车牌的亮度阈值怎么收敛
现象:定位阶段输出的候选框有时框住了车牌的左半截,有时框住了右半截,车牌本身明明完整在画面里,但分割阶段拿到的图像缺了一块。
原因:蓝色车牌的HSV掩膜在S和V通道的阈值设定太宽松,车身反光区域的像素被当成车牌,而真正的车牌区域边缘因为V值偏低被漏掉。
解决:把V的下限从0.15往上调,我这里最后收到0.22-0.25之间,同时把H的上限往回收一点。调完之后做一次全量测试,如果定位框开始抖动,说明阈值又太紧了,可以用二分法在0.18和0.30之间来回试,每次只改一个参数。
5.2 字符分割粘连:二值化后多出噪声块的处理
现象:分割结果里面多个字符被切成了一块,连通域分析得到的区域数量明显少于7个。
原因:车牌表面有污渍或者拍照时有强反光,二值化后这些区域和字符连成了一片。
解决:先检查二值化阈值来源。如果用的是全局Otsu阈值,改成局部自适应阈值,代码里用imbinarize的adaptive选项处理。如果仍然粘连,在二值化前对灰度图做一次顶帽变换,顶帽变换能把比周围环境亮的小尺度结构提取出来,而车牌字符正好是局部亮点,这会极大地削弱污渍和反光的影响。
5.3 模板匹配误识别:相似字形“0/O”“B/8”的区分
现象:字母O识别成数字0,B识别成8,D识别成0,这类错误出现在不同批次的图片里,有时一个车牌上同时错两个字符。
原因:模板匹配的相关系数对这类近似字形区分度不足,尤其是归一化后的低分辨率图像,O和0几乎只有细小的几何尺度差异。
解决:一方面在模板库里同时放大字号和普通字号的O与0模板,让匹配时有更多参考;另一方面引入车牌语法规则,前面提到的I/O过滤在这里能解决一部分,剩余的用“局部特征比对”兜底——把O和0模板的右侧局部区域单独摘出来做二次匹配,因为O的右侧曲线弧度和0的右侧竖线弧度在局部特征上差异更明显。
5.4 光照不均匀:全局阈值还是局部阈值
现象:同样的代码,白天停车场照片识别率很高,到了傍晚或者阴天场景,分割出的字符残缺严重,识别结果完全没有可读性。
原因:全局阈值处理光照不均匀的图像时,亮区的字符和背景对比度高,暗区的字符会被阈值当成背景直接滤掉。
解决:换到灰度图之后先做直方图均衡化,再使用自适应阈值。整套代码里二值化逻辑建议统一封装成一个函数,内部优先用局部阈值,全局阈值作为兜底,这样测试不同光照的图片时,不需要反复改主流程。
5.5 识别速度慢:三处耗时瓶颈的优化顺序
现象:单张图片跑完整个定位、分割、识别流程要接近一秒,连续处理几十张测试图的时候,等待感太强。
原因:normxcorr2是个计算量很大的操作,尤其模板库里有上百个模板时,每个字符要对所有模板做一次相关计算,这部分占掉了大半运行时间。
解决:先砍模板数量,把每个字符的模板从七八张裁到三四张,优先保留不同字体的代表性样本;再把normxcorr2的输入图像尺寸缩小,从40×20降到28×14,匹配速度提升非常明显;最后才是考虑并行化,但MATLAB的parfor在模板匹配这种任务里收益不稳定,如果前两步优化后速度能压到0.3秒内,就没必要上并行。
6. 从算法到可用工具:GUI封装与批量验证的落地技巧
6.1 用App Designer搭一个能选文件、调参、出结果的界面
算法全部跑通之后,把代码包成一个可交互的界面,价值会大很多。用MATLAB的App Designer写一个简单的车牌识别工具并不复杂:左侧放一个轴组件用来显示原图和中间结果,右侧放几个参数输入框(H上下限、S下限、二值化阈值模式),底部放一个“识别”按钮和一个输出文本框。按钮的回调函数里调用封装好的识别主函数,识别主函数接收一个图片路径和参数结构体,返回识别结果字符串和定位框坐标。界面上的参数和主函数之间通过结构体传参,不要用全局变量,否则后期改逻辑会很难维护。
GUI的调试有个小技巧:在识别主函数里加一个debug参数,为true时把每一步中间图像都以子图形式画出来,GUI模式下默认关掉。界面开发里最容易出问题的是图像显示时的坐标轴刻度残留,每次显示新图前先cla一下,再imshow,避免叠加显示引起误判。
6.2 批量图片回归测试:验证改参数没有弄坏旧样本
参数调优最大的风险是:改了一处阈值解决了A问题,结果B场景的识别率悄悄掉了。批量回归测试是唯一能压住这种问题的办法。代码里准备一个testData目录,里面放至少三组图片,分别是正常停车场场景、强光照场景、倾斜角度场景,每组50张左右,维护一个Excel文件记录每张图的正确识别结果。跑批量测试时,脚本读取这个Excel,和算法输出比对,最后输出一个识别率统计表。
% 遍历测试目录下的所有图片,统计识别率 imgFiles = dir(fullfile('testData', '*.jpg')); correctCount = 0; for k = 1:length(imgFiles) imgPath = fullfile(imgFiles(k).folder, imgFiles(k).name); result = recognizePlate(imgPath, params); % 返回识别字符串 trueLabel = getTrueLabel(imgFiles(k).name); % 从文件名读正确结果 if strcmp(result, trueLabel) correctCount = correctCount + 1; end end accuracy = correctCount / length(imgFiles); fprintf('整体识别率:%.2f%%\n', accuracy * 100);这段脚本的关键点是文件名里必须包含正确的车牌号,比如“sample_京A12345_01.jpg”,getTrueLabel函数从文件名中解析出真值。一旦改了定位阈值或分割参数,就跑一遍这个脚本,对比前后识别率变化。我习惯把每次参数改动和对应的识别率记在一个小备忘里,时间久了会发现哪些参数是全局敏感项,哪些只在特定场景下起作用。
6.3 把中间结果显示出来:调试灰度图、二值图、定位图的习惯
识别系统最怕的就是“结果错了但不知道错在哪一步”。调试阶段我把主函数内部每一步的结果都用一个subplot画出来,从原始图、HSV掩膜图、形态学处理后的候选区、字符分割图到最终的识别结果,一屏五张小图全部输出。这样一眼就能定位问题发生在定位、分割还是识别阶段——定位框歪了就看前两张图,字符切碎了就看分割图,别的都对识别错就查模板匹配。
这看起来是笨办法,但它的调试效率远高于在命令行里断点一步步跑。我用MATLAB处理这类图像问题时,习惯在每段核心算法后面写一个if debug条件,控制中间结果是否输出。这个习惯帮我解决过不少玄学问题,比如有一次整个流程跑起来偶尔正确偶尔失败,最后发现是某次循环里图像变量被循环变量意外覆盖了,中间结果图一对比立刻就暴露了。
从那以后,我每次拿到新的车牌识别资源,都会先花二十分钟把中间结果的输出结构摸清楚,再动手改参数。这种强制走一遍的调试流程,能让你在改三四个阈值之后依然保持清醒,知道每一行改动到底改的是什么。车牌识别这套MATLAB实现整体思路经典,参数可调空间大,适合作为基础框架去扩展。希望帮到你。
本文还有配套的精品资源,点击获取