ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Matlab的车牌识别系统实战:从图像预处理到字符识别全流程解析

基于Matlab的车牌识别系统实战:从图像预处理到字符识别全流程解析

前阵子花了几个周末,把基于Matlab的车牌识别系统从头到尾完整做了一遍。坦白说,车牌识别不算新鲜课题,网上各种开源方案也很多,但真轮到自己从零搭一遍时,才发现教程里轻描淡写的每一步,都藏着不少需要实测确认的细节。这篇文章记录的是我在图像处理这条完整流水线上的实现过程、参数调优逻辑和踩坑记录,从车牌区域定位、字符分割到最终识别,每一步我都会解释为什么要这样选,以及遇到问题该往哪个方向排查。

如果你正在写车牌识别的课程设计,或者刚开始接触图像处理、想找一个能串联知识点的完整实战项目,这篇文章应该能帮你少走不少弯路。如果你只是想要现成代码,那这里可能不太适合你;我想讲清楚的是代码背后的判断逻辑,以及那些在常规文档里根本不会写的实测经验。

1. 整个系统从零搭建前,先想清楚三件事

1.1 为什么在算法验证阶段选Matlab而不是直接上Python

很多人一听到图像处理就默认选Python加OpenCV,这没有对错之分,但要分场景。车牌识别这类算法验证项目,Matlab有个天然优势:矩阵运算和图像数据结构完全贴合,写起来几乎不用考虑底层内存布局。我实际对比过,同样一个边缘检测加形态学处理流程,在Matlab里从加载图片到看到中间结果,中间代码量能比在OpenCV里少三成左右,而且每个中间变量都能直接imshow,所见即所得,对排查问题太重要了。

当然,Matlab的短板也很明显:速度和部署能力不如C加加,生态也不如Python丰富。但如果是课程设计、算法验证、毕设前期的方案对比阶段,Matlab的调试体验在我个人看来是最好的选择。我用的运行环境就是普通的桌面版Matlab,主要依赖Image Processing Toolbox。整个工程组织方式不复杂:一个主脚本负责流程串联,三个功能函数分别对应定位、分割、识别三个环节,每个环节单独调参,不用为了试一个阈值把整条流水线从头跑一遍。

1.2 系统整体流程与模块职责划分

我最终确定的处理链路是:原始图像输入 → 图像预处理 → 车牌候选区域提取 → 精确定位与倾斜校正 → 字符分割 → 字符识别 → 结果输出。这个顺序不是随便定的,有两个设计取舍值得展开说。

首先是"先定位再分割"。很多人第一次做会想,能不能直接对全图做字符识别?答案是不行。车牌是一个高宽比接近3比1的矩形区域,内部字符排列规则,如果先拿到这个矩形框,后续分割会简单很多;反过来对全图做字符分割,图片里任何文字、数字、logo都会变成干扰源,识别结果会乱成一锅粥。

其次是定位环节用哪种特征。我初版用的是边缘检测加形态学闭运算,因为蓝底白字的车牌在灰度图里边缘特征非常明显;但遇到车身也是浅色且线条复杂的场景时,边缘法会产生大量非车牌候选区域。后来我把颜色特征加了进来,在HSV空间里筛选蓝色区域,两条路线并行再取交集,定位稳定性提升了一个档次。详细对比后面单独讲。

1.3 测试图片集与评价指标

没有一套像样的测试图片集,调参就是瞎试。我自己收集的测试集包含三类图片:白天顺光车辆照片、阴天或阴影下的车辆照片、夜间或光线不足的照片,大概一百来张。每张图片里车牌尺寸占整幅图像的比例都不同,这是有意为之,专门用来测定位算法在不同尺度下的鲁棒性。

评价指标我用了三个:定位成功率(候选框是否准确覆盖车牌区域)、字符分割正确率(每个字符是否完整切分且不粘连)、整牌识别准确率(七个字符全部正确的比例)。另外还统计单张图片的平均处理耗时。虽然Matlab原型不需要追求极致性能,但如果慢到每秒处理不了一张,后面做实时化改造会非常吃力。

2. 车牌定位:从复杂背景里"挖"出目标区域

2.1 边缘检测加形态学闭运算的经典路线

车牌定位是整个系统的基础,它要是偏了,后面分割和识别做得再精细也白搭。我最开始实现的经典路线是:彩色图转灰度 → 边缘检测 → 形态学闭运算 → 连通域筛选。这条路线每一步都有讲究。

读入图片后第一步是灰度化,直接调rgb2gray。为什么转灰度?因为车牌区域的字符和底色之间存在强烈的灰度跳变,这种跳变在灰度图里表现为密集的边缘响应,不依赖颜色信息就能捕捉到。灰度化之后我用imadjust做对比度拉伸,把灰度范围拉开,边缘响应会更集中。

接着是边缘检测,我用的是Sobel算子,灵敏度阈值设在0.2到0.3之间。阈值太低会把车身的纹理、地面的颗粒都检测成边缘;阈值太高又会把车牌字符的边缘滤掉。这个阈值非常依赖图片本身的对比度,所以我并没有写死,而是根据灰度化后图像的平均梯度动态调整。动态调阈值的方法很简单:先算出整幅图的梯度均值,再以均值加若干倍标准差作为阈值参数。

形态学操作这一环节是踩坑重灾区。闭运算的作用是把距离较近的边缘连接到一起,形成连通的区域。车牌上每个字符的边缘是离散的线段,它们之间的间距经过闭运算后应该被"填满",最终形成一个接近矩形的连通块。这里最关键的参数是结构元素的大小和形状,我用的结构元素是水平方向较长、垂直方向较短的自定义矩形:

se = strel('rectangle', [5, 15]); closed = imclose(edges, se);

水平方向要更长,是因为车牌是横向排列的字符,字符间距小,需要用更宽的结构元素把同一排字符的边缘连起来;垂直方向则不能太宽,否则会把上下两排的干扰边缘也粘到一起。这个结构元素的大小建议从[5, 15]起步,根据实际图片分辨率等比调整——图片分辨率高,字符边缘间距就大,结构元素也要相应放大。

处理完闭运算,图像里会出现若干个连通区域,真正的车牌区域只是其中之一。此时调用bwlabel给每个连通域编号,再用regionprops提取外接矩形框、面积、填充度属性。车牌区域的特征非常鲜明:外接矩形宽高比大致在2.5到4.5之间,面积不会太小,填充度(区域面积占外接矩形面积的比例)通常在0.3到0.7之间。按照这三个条件依次筛选,候选区域的数量能从上十个压缩到两三个。

2.2 颜色特征在HSV空间中的并行筛选方案

单靠边缘法在背景干净的场景下表现不错,但背景一复杂,车身线条、路边广告牌、建筑窗户都可能形成类似矩形的连通域。这时就要靠颜色特征来兜底。蓝底白字车牌的核心颜色是蓝色,但直接在RGB空间里筛蓝色并不稳妥,因为光照变化会让蓝色像素的RGB值大幅波动。更稳妥的做法是转到HSV空间,把色调、饱和度、亮度分开处理。

在Matlab里转HSV并生成蓝色掩膜很简单:

hsvImg = rgb2hsv(img); blueMask = (hsvImg(:, :, 1) > 0.5) & (hsvImg(:, :, 1) < 0.75) & ... (hsvImg(:, :, 2) > 0.3) & (hsvImg(:, :, 3) > 0.15);

H分量范围的选择最需要小心。HSV空间里色调用角度表示,蓝色大概落在0.5到0.75区间(归一化之后)。饱和度S太低会混入灰白像素,亮度V太低则会捞进来一堆暗部噪声。这三个条件的阈值我实际调了很久,核心原因在于不同环境下蓝色的呈现差异很大:晴天强光下蓝色偏淡,阴天偏灰,夜间甚至接近黑色。固定阈值并不理想,我后来采用的是先用最大类间方差法对亮度分量做自适应分割,把过暗或过亮的像素剔除,再在剩余区域里筛蓝色,效果比纯固定阈值稳得多。

把颜色掩膜和边缘法得到的连通域做交集后,效果立竿见影:红色、白色车身直接就被排除了。即便车身也是蓝色,由于车牌区域同时满足"蓝色加密集边缘加矩形比例"三个条件,依然能在多个候选里排在最前面。

2.3 定位模块的三种失败场景与修正方法

我整理一下实际运行中最常见的三种定位失败场景和对应修正思路,这部分算是纯经验总结。

第一种是车牌边缘在闭运算后没有连成闭合矩形,车牌被拆成上下两半。这通常不是结构元素不够大,而是边缘检测阈值太高,把字符内侧的边缘滤掉了。处理办法是调低边缘阈值,或者把闭运算结构元素再放大一圈。

第二种是候选框把车牌和旁边的车灯、保险杠连在一起。这种情况往往是闭运算结构元素太大,把车牌边缘和紧邻物体的边缘粘到了一起。此时应该缩小结构元素,同时增加一项筛选规则:候选区域宽高比是否在合理范围,如果超过4.5明显不合理,就需要对区域做二次分割。

第三种是夜间场景车牌区域整体曝光不足,边缘响应极弱。这种场景光靠边缘法几乎无解,我的做法是先对灰度图做局部自适应直方图均衡化,即adapthisteq,大幅提升局部对比度,再走边缘检测流程,效果比全局拉伸好很多。

定位这块我的体会是:不要指望一个固定阈值跑通所有图片,更不要一上来就上深度学习。先用简单规则把90%的场景搞定,留下10%难啃的,再有针对性地引入复杂策略,整个系统的可解释性也好。

3. 字符分割:把车牌拆成一排完整单体

3.1 二值化处理:阈值选择背后的逻辑

拿到车牌区域的彩色图之后,第一步是把它变成黑白分明的二值图。这里有一个容易忽略的细节:提取出的车牌区域不仅包含车牌本身,还包含周围少量车身或背景像素,所以不能直接拿原始灰度做全局二值化,要先做一次裁剪,把多余边缘尽量去掉。裁剪思路很简单:用regionprops拿到外接矩形框,再往外扩几个像素取子图。注意不能扩太多,否则背景又进来了,一般扩2到5个像素,具体看原图分辨率。

二值化阈值选择上,我推荐最大类间方差法,也就是灰度直方图双峰间的最佳分割点会自动算出来的那个思路,不需要人为指定,适应性比固定阈值好得多。但这类方法也不是万能的,车牌底色光照不均匀时,同一个区域内会一侧亮一侧暗,全局阈值会丢失暗侧的字符像素。

实测里最稳的方案是局部自适应阈值。Matlab没有直接封装好的自适应二值化函数,可以自己写:把车牌区域分成若干小块,对每个小块分别计算阈值,再做双线性插值得到平滑的阈值曲面。这个方法实现不复杂,效果却非常显著,尤其对付一半在阴影里一半在阳光下的车牌。如果觉得麻烦,折中方案是先用imadjust把车牌区域对比度拉到位,再跑全局阈值,简单场景下也够用。

3.2 水平投影与垂直投影的分割操作

二值化之后,字符分割的核心手段是投影法。原理不复杂:把二值图在水平方向做累加,得到每一行白色像素的数量,这个一维向量就是水平投影。车牌字符集中在某一高度范围内,水平投影会在这个范围内形成一个明显的波峰,波峰的上下边界就是字符的上下边界。

水平方向切好后,在字符区域内做垂直投影,即统计每一列白色像素的数量。字符之间是空白间隔,垂直投影图上会出现明显的"山谷",根据这些山谷位置就能确定每个字符的左右边界。

这一步的坑在于判断山谷是真正的字符间隔,还是字符内部的缺口。比如字母E中间那一横的空隙会让垂直投影出现一个小幅下凹,但不能把它当字符边界。我的处理方法是设置一个最小分裂宽度,当山谷宽度大于某个阈值(比如5到8个像素)才认为是字符间隔;同时限制字符的最小宽度,太小不当作有效字符。投影法代码写起来不复杂,思路类似下面这样:

verticalProj = sum(bwImage, 1); top = 1; inChar = false; for col = 1:length(verticalProj) if verticalProj(col) > 0 && ~inChar top = col; inChar = true; elseif verticalProj(col) == 0 && inChar bottom = col - 1; if bottom - top > minWidth charRects = [charRects; top, bottom]; end inChar = false; end end

这段代码的局限也很明显:只能处理字符之间有完整空白的理想情况。一旦车牌边框和铆钉与二值图粘连,垂直投影可能把边框误判成字符,或者在字符之间形成假粘连。所以分割前要把二值图清理一遍:去掉水平方向上超过正常字符宽度的长条形区域(通常是上下边框),再用连通域分析排除面积异常的小块(通常是铆钉)。

3.3 倾斜校正与铆钉干扰的实战处理

车牌拍摄时因为角度问题会产生倾斜,倾斜超过两三度,垂直投影分割结果就会不准确,字符也会被切歪。我采用的倾斜校正方法是Hough变换直线检测:在车牌区域二值图里检测长直线,找出最接近水平的边缘线,计算倾斜角度,再用imrotate把整个车牌区域旋转回来。

Matlab里Hough变换是封装好的:

[H, theta, rho] = hough(bwImg); peaks = houghpeaks(H, 5, 'Threshold', 0.3 * max(H(:))); lines = houghlines(bwImg, theta, rho, peaks);

从检测到的直线里挑选长度最大且角度接近0度的线段,将它的角度作为倾斜量。这里有个经验:不要直接用单条边缘的角度,因为二值图的毛刺会产生偏差,最好把检测到的所有合格直线的角度取中值,结果明显更稳健。

铆钉和螺丝是分割阶段的经典干扰项,它们位于字符上下方的边框区域,在二值图里表现为紧贴边缘的两个小块。处理常规做法是在水平投影确定上下边界后,把上下边界向内收缩3到5个像素,让字符区域避开边框和铆钉所在的行。如果个别铆钉恰好在字符区域内部,就靠连通域面积来排除:计算所有连通域面积,把明显小于字符平均面积的标记为噪声移除。

倾斜校正和铆钉清理结束后,分割模块输出7个整齐的字符子图。此时字符大小可能不一致,需要归一化到统一尺寸,我习惯缩放到48乘96像素,宽高比1比2,后面做模板匹配时尺寸一致才有可比性。

4. 字符识别:模板匹配的细节与准确率提升

4.1 模板库构建与归一化处理

字符识别我首选模板匹配,理由很直接:数据量小、实现简单、可解释性强。训练数据不需要成千上万张,只要把每个字符做出一套清晰的模板图,匹配效果就能达到可接受的水平。

模板库分三部分:省份简称汉字、英文字母、数字。其中字母需要去掉容易混淆的I和O,避免和数字1、0冲突,所以字母加数字一共34个字符。每个字符至少要准备一到两种接近车牌字体规范的样式。我在构建模板时,是从几张不同角度拍摄的清晰车牌里手动把字符裁出来,再归一化到和分割输出一致的尺寸,并把字符重心平移到模板中心。这一步对识别准确率的影响非常大。

这里要提醒一句:模板图的制作质量直接决定识别上限。我第一版直接用二值图当模板,效果很差,因为分割出的字符边缘有毛刺,和模板对不齐。后来我把模板也做了归一化、去噪和边缘平滑处理,匹配效果立刻提升。归一化不只是缩放尺寸,还必须保证字符在模板中居中,重心一致。我写了一个小函数,先把字符子图二值化,再计算白色像素的重心,把重心平移到模板中心,这样即使原始字符稍微偏左偏右,归一化后依然对齐。

4.2 匹配策略:为什么用归一化相关系数

模板匹配有多种度量方式,选错方法会导致结果千差万别。我在项目里用了归一化相关系数(NCC),衡量的是待测字符图像和模板图像之间的线性相关程度,取值在-1到1之间,越接近1说明越相似。NCC的一个大优势是对光照不敏感,因为它做了均值归一化,模板和待测图的整体亮度偏差不会影响匹配结果。

Matlab里可以用normxcorr2直接计算二维互相关,但输出的是响应图,需要自己找最大值坐标。如果只是做一对一匹配,更轻快的做法是把图像展成一维向量,直接算余弦相似度:

similarity = dot(testVec, tplVec) / (norm(testVec) * norm(tplVec));

把所有模板都算一遍相似度,取最大值对应的模板字符作为识别结果。这个做法简单直接,但性能上有一个明显瓶颈:每识别一个字符都要遍历所有模板。我做的优化是把模板向量的L2范数提前缓存,相似度计算时不再重复计算分母,只算一次点积,在批量测试上百张图片时能省下不少时间。

4.3 易混淆字符的规则后处理

模板匹配跑通之后,我在测试集上发现了几个典型的混淆问题。最容易出错的是数字0和字母O、数字1和字母I,我在构建模板时已经把字母O和I去掉了,所以数字0和1不再有字母对手,混淆概率明显下降。但新的问题来了:部分省份汉字字形非常接近,在小尺寸归一化之后很容易互相匹配错。解决这个问题不能只靠模板质量,还要引入车牌规则做后处理。

车牌字符有固定结构:第一个字符是省份简称汉字,第二个是发牌机关代码字母,后面是字母和数字混合。我在后处理阶段做了两件事:第一,强制第一个字符只匹配汉字模板集合;第二,后续位字符只匹配字母和数字模板集合。这样就把搜索空间压缩了,也排除了"汉字出现在第二位置"这种明显不可能的识别结果。

另一个后处理技巧是置信度判断。如果最高相似度和第二高相似度非常接近,说明识别信心不足,此时不要强行输出,而是标记为待人工确认。我设的规则是两者差距小于0.05时,输出第二个候选作为参考并打上低置信度标记。这个规则在批量测试时非常有用,能快速定位系统薄弱样本,针对性补充模板或调整预处理参数。

我用一个表格总结常见的字符混淆问题与处理手段,实测下来这几种策略能把整牌准确率提升五到八个百分点。

问题类型具体表现处理手段
数字与字母混淆0与O、1与I模板库去掉字母I、O
相近汉字结构相似的省份简称规则后处理限定首字符
相似度接近两个候选分数几乎一样低置信度标记,人工复核
字形变形车牌倾斜导致字符拉长倾斜校正、归一化重心对齐

5. 从调试到稳定:实测路上的疑难杂症与排查思路

5.1 蓝色车身干扰定位的完整排查链路

这是我调试过程中印象最深的一个问题。某张测试图片里是一辆深蓝色轿车,车牌是蓝底白字,定位模块输出的候选框竟然落在了车身上,而不是车牌区域。我一开始认为是颜色筛选阈值有问题,把车身蓝误认成了车牌蓝,但调低蓝色掩膜阈值后,车牌候选框依然不在正确位置。

排查思路要从输出倒推。我先可视化每一步中间结果:把蓝色掩膜叠加在原图上,发现车身区域几乎全部被标记为蓝色,而车牌区域由于拍摄角度和反光原因,蓝色像素反而不完整。这说明问题不在阈值,而在于我过度依赖颜色特征,把颜色掩膜当成了定位的主依据。

修正方案是调整两条路线的融合逻辑:颜色掩膜的作用不是直接生成候选区域,而是作为边缘连通域的"打分项"。具体做法是,先用边缘法生成所有连通域候选框,再计算每个候选框内蓝色像素占比,只有占比超过40%的候选框才被保留。这样一来,车身虽然是蓝色,但它的连通域形状不是车牌矩形,在宽高比筛选时就被淘汰了;车牌区域虽然蓝色像素不完整,但占比仍超过40%,且矩形特征吻合,最终被正确保留。

这个案例给我的启发是:多特征融合不能简单叠加,而是要搞清楚每个特征在决策链中的角色。边缘特征负责找形状,颜色特征负责验身份,分工明确之后系统稳定性大幅提升。

5.2 强光和夜间场景的预处理补救

测试集里有一批强光下拍摄的照片,车牌区域过曝,字符几乎看不清。直接跑标准流程,定位和识别双双失败。这类图片需要在预处理阶段做额外处理。

我的做法是:灰度化之后、进入定位流程之前,先对全图做局部自适应直方图均衡化,也就是adapthisteq。这个函数会把图像分成若干小区域,每个区域分别做直方图均衡,再用双线性插值消除块间边界。对局部过曝区域的恢复效果比全图直方图均衡好很多。

夜间场景是另一个极端:背景暗、车牌反光。此时单纯提高亮度会让噪声同步放大,处理顺序应该是:先用中值滤波去除椒盐噪声,再做自适应均衡化,最后做一次轻微高斯平滑。注意不要过度平滑,否则字符边缘被抹掉之后,定位环节的边缘检测就形同虚设了。类似思路也适用于雨天、雾天图片,核心都是在增强对比度和保留边缘细节之间找平衡。

5.3 性能瓶颈与后续扩展方向

Matlab原型跑完,单张图片整体耗时大约在0.8到1.2秒。其中字符识别模块占了将近一半时间,主要花在模板遍历上。如果想把系统改造得更快,有三个方向可以参考。

第一是缩小定位阶段的搜索范围。边缘检测是对全图做的,但实际车辆在画面中的位置通常有先验知识,比如中下方区域出现车牌的机率更大。固定摄像头场景下可以提前截取感兴趣区域再做检测,计算量直接降一截。

第二是用分类器替代模板匹配。模板匹配每识别一个字符都要遍历所有模板,计算量与模板数量成正比。如果换成KNN分类器,先离线提取所有模板的方向梯度直方图特征建立索引,在线识别时只提取待测字符特征并做最近邻搜索,速度可以提升一个量级。我在实验里验证过,KNN方案识别准确率略高,速度明显更快,代价是需要额外工具箱支持和稍长训练流程。

第三是数据扩增思路。如果后续想在更多复杂场景下提升准确率,可以人为制造训练样本:对模板做轻微的旋转、缩放、加噪声,生成大量变体,让模板库或分类器见过更多"长歪"的字符。这个做法在深度学习里叫数据增强,在传统方法里同样适用。

整个系统从定位、分割到识别,核心细节和调试经验到这里就梳理完了。这个项目的价值不在于多先进,而在于它把图像处理里最基础的知识点串成了一条完整流水线。如果你正卡在某一步,不妨按我上面的排查顺序走一遍:先看中间结果,再锁定问题环节,最后针对性调参数。这个过程本身就是图像处理最有意思的部分。最后再分享一个小技巧:任何环节出问题,第一步永远是可视化中间结果,眼见为实,靠猜是猜不出正确答案的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进