☰
基于MATLAB的车牌识别系统:从图像预处理到字符识别完整实现
2026/10/5 13:58:24 网站建设 项目流程

做车牌识别项目,十个人里有八个会先想到MATLAB,原因很简单:图像处理工具箱成熟,几行代码就能完成滤波、边缘检测这些底层操作,可以把精力集中在算法逻辑本身。这套“基于MATLAB的车牌识别(含子程序)”整体结构分四层:主程序负责调用流程,子程序分别处理图像预处理、车牌定位、字符分割和字符识别。整套代码在Matlab R2020b以上版本均能直接运行,只依赖Image Processing Toolbox,不需要额外装深度学习工具箱,对绝大多数学生和嵌入式入门工程师来说是门槛最低的一套方案。

如果你的目标是毕业设计、课程大作业,或者想快速跑通一套完整的OCR流程,这套结构非常适合。它天然拆分成独立子程序,任何一个环节都能单独调试,也方便替换成你自己的想法,比如把模板匹配换成BP神经网络,或者版CNN,只需要改一个子程序的内部实现,其余代码不用动。

1. 系统整体设计与识别流程

1.1 车牌识别的核心链路

车牌识别这件事,听起来高大上,拆开看就是四步:图像预处理、车牌定位、字符分割、字符识别。整套系统按这个链路组织成四个子程序,主程序只负责按顺序调用并显示中间结果。

先说说为什么这样拆。把功能拆成子程序,不是单纯为了代码好看。车牌识别最大的痛点是场景变化太多:光照不一样、车牌角度不一样、图片清晰度不一样。如果你把所有逻辑写在一个脚本里,一旦定位效果不好,你要在几百行代码里找原因。拆成子程序以后,每个模块独立测试,比如定位子程序输入是原始图像,输出是一个裁剪好的车牌区域,你单独拿十张不同的图测这个函数,定位不准就只调这个函数,效果好了再往下走。

整个识别流程描述如下:

  1. 读取原始图像,转成灰度图,做中值滤波去噪
  2. 用边缘检测和形态学闭运算,把车牌区域从背景中提取出来
  3. 对定位到的车牌区域做倾斜校正、二值化,然后按投影法把每个字符切分出来
  4. 归一化每个字符的尺寸,用模板库做匹配,输出最终车牌号码

这个流程是传统图像处理路线的经典组合,虽然现在的深度学习方法端到端识别已经很强了,但对于学习图像处理基础和算法原理来说,这套流程仍然是最好的入门路径。每一步的中间结果都可视化输出,你能清楚看见图像在每一层发生了什么变化。

1.2 为什么用MATLAB实现而不是直接用现成库

如果你只是想要一个能用的车牌识别结果,OpenCV加一个现成的模型半小时就能搞定,没必要用MATLAB。但这个项目的目的不是“用”,而是“懂”。MATLAB在图像处理教学和算法验证上的优势,是它的中间过程可视化能力极强。你可以在每个子程序里随时暂停,看当前图像长什么样、阈值取多少合理、某个形态学操作改变了什么。

另外,MATLAB的矩阵操作让图像处理代码极度紧凑。比如rgb2gray一句转换灰度,edge一句做边缘检测,bwareaopen一句去掉小面积连通域。同样的功能用C++写,光图像读取和内存管理就得几十行。对于验证算法思路来说,MATLAB的调试成本是最低的。

客观来说,MATLAB的缺点也很明显:运行速度比C++慢,部署不方便,商业授权贵。所以这套代码定位是教学验证和毕设原型,如果你想上生产环境,后续可以把算法移植到C++或者Python,但算法原理和参数是你在这套代码里调出来的,这个积累是通用的。

2. 核心子程序设计:定位、分割、识别

2.1 车牌定位子程序:从整幅图中找车牌

车牌定位是整个系统里最影响成败的一步。车牌没找对,后面做得再好都白费。常用的方法是用车牌区域的边缘特征和颜色特征结合。

这里采用的方案是:先把图像转灰度,做中值滤波去除噪声,然后用Sobel算子做边缘检测。因为车牌区域字符密集,会产生大量边缘,加上车牌边框也是明显的矩形边缘,所以车牌区域在边缘图上呈现为一个高密度高亮的矩形块。再用imclose做形态学闭运算,把字符间的空隙连接起来,形成连通域,通过计算每个连通域的长宽比例和面积,筛掉明显不是车牌的区域。

核心代码如下:

function plateImg = locatePlate(grayImg) % 车牌定位子程序 % 输入:灰度图像 % 输出:裁剪并校正后的车牌图像 % 中值滤波去除噪声 denoisedImg = medfilt2(grayImg, [3 3]); % Sobel边缘检测 edgeImg = edge(denoisedImg, 'sobel'); % 形态学闭运算连接字符区域 se = strel('rectangle', [17, 3]); closeImg = imclose(edgeImg, se); % 填充空洞,增强区域连通性 filledImg = imfill(closeImg, 'holes'); % 提取连通域并筛选候选区域 cc = bwlabel(filledImg); stats = regionprops(cc, 'BoundingBox', 'Area'); % 车牌长宽比一般在2.5到4.5之间 % 面积也需大于一定阈值 candidate = []; for i = 1:length(stats) w = stats(i).BoundingBox(3); h = stats(i).BoundingBox(4); area = stats(i).Area; ratio = w / h; if ratio > 2.5 && ratio < 4.5 && area > 1000 candidate = floor(stats(i).BoundingBox); break; end end if isempty(candidate) error('未定位到车牌区域,请调整滤波参数或边缘检测阈值'); else plateImg = imcrop(grayImg, candidate); end end

这段代码里的几个参数值得细说。Sobel边缘检测比Canny更适合这里,因为Canny产生的边缘太细碎,闭运算连接后容易形成很多小碎块,干扰候选区域筛选。结构元素se选择[17, 3],意思是横向17像素、纵向3像素的矩形,刚好够把字符边缘连成块,又不会把相邻字符块合并得太大。实际调试时可以根据图像分辨率按比例缩放,图像越大会话,结构元素尺寸也要相应放大。

2.2 字符分割子程序:投影法切割

定位到车牌区域以后,第二步是字符分割。车牌的每个字符之间有一段空白间隔,这个特征让投影法成为最常用的分割方法。原理很简单:把二值化后的车牌图像在垂直方向做投影,也就是逐列统计白色像素个数,字符区域的投影值高,字符间隔处投影值为零或接近零。找到这些“波谷”位置,就能把字符一个个切开。

实际处理时,分割前要做两个关键操作。一是倾斜校正,车牌拍摄角度稍有偏斜,垂直投影的波谷就会被字符边缘污染,导致切割位置偏移。这里用Hough变换检测车牌上下边框的直线倾斜角度,然后把图像旋转回来。二是二值化,这里用Otsu自适应阈值,因为不同环境下车牌图像的灰度分布差异很大,固定阈值完全不靠谱。

function chars = segmentChars(plateImg) % 字符分割子程序 % 输入:定位后的车牌灰度图 % 输出:切割好的字符图像cell数组 % 二值化 level = graythresh(plateImg); bwImg = imbinarize(plateImg, level); % 去除上下边框干扰:统计水平投影,去掉上下边缘区域 horiProj = sum(bwImg, 2); rowIdx = find(horiProj > size(bwImg, 2) * 0.5); if ~isempty(rowIdx) bwImg(rowIdx(1):rowIdx(end), :) = 1; bwImg = ~bwImg; % 车牌字符为白底黑字,反转 end % 垂直投影 vertProj = sum(bwImg, 1); % 找字符区域的边界:投影值由0转正处为起点,由正转0处为终点 inChar = false; charBounds = []; for col = 1:length(vertProj) if vertProj(col) > 0 && ~inChar startCol = col; inChar = true; elseif (vertProj(col) == 0 || col == length(vertProj)) && inChar endCol = col; inChar = false; if endCol - startCol > 5 % 排除单个噪点 charBounds = [charBounds; startCol, endCol]; end end end % 依宽度排序去除异常区域 % 实际车牌字符宽度接近,可筛选掉过窄和过宽的区间 chars = cell(1, size(charBounds, 1)); for i = 1:size(charBounds, 1) chars{i} = bwImg(:, charBounds(i, 1):charBounds(i, 2)); end end

字符分割有一个细节容易忽略:车牌上有边框和铆钉,如果不去除,投影时会多出几个“伪字符”。上面代码通过水平投影把包含大面积横向连续白色像素的行先统一处理掉,这一步能在很大程度上消除边框干扰。铆钉位置通常和字符在同一行,面积小,通过设定endCol - startCol > 5就能把宽度不足的噪声区域过滤掉。

2.3 字符识别子程序:模板匹配法

字符识别是整个流程的最后一步,这里用模板匹配实现。思路是先准备一套0到9和A到Z的字符模板,把分割出来的字符和每个模板做相似度计算,选择最相似的那个作为识别结果。

模板匹配的核心是归一化,分割出来的字符尺寸不一样,必须统一缩放到和模板一样的尺寸,再计算相似度。常用的相似度度量有绝对差平均(Mean Absolute Difference)和归一化互相关(Normalized Cross-Correlation)。推荐用归一化互相关,因为对照明显更敏感,抗噪能力强一点。

function result = recognizeChar(charImg, templateSet) % 字符识别子程序 % 输入:分割后的字符图像,模板库 % 输出:识别字符 % 统一缩放到42*24 charImg = imresize(charImg, [42, 24]); charImg = charImg(:); bestScore = -1; bestChar = ''; for i = 1:length(templateSet) tpl = templateSet(i).img; tpl = tpl(:); % 归一化互相关 score = corr2(double(charImg), double(tpl)); if score > bestScore bestScore = score; bestChar = templateSet(i).char; end end result = bestChar; end

这里模板库的结构体templateSet每个元素包含两个字段:char是字符本身,img是对应的二值化模板图像。制作模板时需要注意,取样的图片最好用和你实际摄像头同类型的车牌图像,字体越接近,识别率越高。模板数量一般35个左右(数字10个,字母24个,排除I和O的歧义情况),省字特殊汉字如“京”“苏”“鲁”等需要单独加。

3. 主程序调用流程与运行效果

3.1 完整主程序与子程序接口约定

主程序的核心工作是把定位、分割、识别三个子程序串起来,并在每个节点显示中间结果,便于观察算法表现。

% ============ 主程序:车牌识别系统调用示例 ============ clear; close all; clc; % 读取图像 img = imread('car.jpg'); % 步骤1:灰度化 + 定位子程序 if size(img, 3) == 3 grayImg = rgb2gray(img); else grayImg = img; end plateImg = locatePlate(grayImg); % 步骤2:字符分割子程序 chars = segmentChars(plateImg); % 步骤3:加载模板库并逐个识别 load('template_set.mat'); % 变量名为templateSet plateResult = ''; for i = 1:length(chars) c = recognizeChar(chars{i}, templateSet); plateResult = [plateResult, c]; end % 显示结果 figure; subplot(2, 2, 1); imshow(img); title('原始图像'); subplot(2, 2, 2); imshow(plateImg); title('定位到的车牌'); subplot(2, 2, 3); imshow(cell2mat(imresize(chars{1}, [42, 24]))); title('分割效果示意'); subplot(2, 2, 4); text(0.3, 0.5, plateResult, 'FontSize', 24); title('识别结果'); disp(['识别结果: ', plateResult]);

子程序之间通过两个隐式约定解耦。第一,locatePlate的输出必须是灰度图,有彩色信息在里面会干扰后续二值化,所以主程序在调用前强制转灰度。第二,segmentChars输出的chars是一个cell数组,每个元素是一张二值化后的字符图,recognizeChar对输入图像是二值还是灰度并不敏感,因为内部会通过imbinarize再处理一次,不过建议在分割阶段就输出二值图,减少后续转换环节。

这种接口设计的好处非常明显:你想换掉识别算法,不需要动前两个子程序,只要把recognizeChar内部的模板匹配改成神经网络推理即可。同理,你想换定位策略,比如用颜色分割法定位蓝底车牌,也只需要改locatePlate一个函数。

3.2 运行效果与参数调优观察

以一张典型的高清小区入口监控图为例,图像尺寸1920x1080,车牌区域约占图幅的4%左右,整套流程在MATLAB里的运行时间约为2到3秒,其中定位子程序最耗时,因为要对全图做边缘检测和连通域分析。识别结果在“车牌区域清晰、无强反光、车牌无大幅倾斜”的条件下能跑到95%以上的准确率。

实际调试时,我最常动的三个参数是:定位中的连通域面积阈值、分割中的Otsu二值化结果、以及形态学结构元素尺寸。图像分辨率越高,最小面积阈值和结构元素尺寸要等比放大。比如原图是640x480时area > 1000效果不错,换到1920x1080时往往需要调到area > 4000以上。

还有一个调试思路:不要直接把参数改到一个固定值,而是把中间结果显示出来观察。比如定位不出来,就单独显示边缘检测图、闭运算图、连通域标记图,看车牌到底在哪一步消失了。这样定位问题基本几分钟就能完成。

4. 踩坑记录与常见问题排查

4.1 常见问题速查表

问题现象可能原因解决方案
定位不到车牌区域边缘检测阈值过高/结构元素太小调低edge阈值,放大结构元素尺寸,检查车牌在图中占比
定位到多个候选区域背景中有大量规则矩形物增加长宽比和面积约束条件,增加颜色特征校验
字符分割多切或少切车牌子边框未去除干净/图像倾斜增强水平投影去边框处理,先做倾斜校正
识别出的字符错乱模板库样本与实际字体风格不一致重新收集与测试图同字体风格的模板图像
整个流程崩溃报错子程序输入输出维度不匹配逐一检查imresize和imcrop后图像维度,接口处加断言判断

第一行的定位不到问题,是最多人卡住的地方。很多人直接把边缘检测图的阈值设为默认值0.2,结果图像细节少或光照暗时完全检测不到车牌边缘。建议先用edge(denoisedImg, 'sobel');后用imshow观察边缘图,如果车牌区域在边缘图上还没有明显的密集白点,说明你的滤波太狠,或者阈值设置不当。

4.2 提升鲁棒性的三个实践技巧

首先,车牌识别系统的性能上限不是由识别算法决定,而是由定位的鲁棒性决定。定位准了,后面的分割和识别都是顺水推舟的事。想让定位更稳定,可以在形态学处理前增加一个全局Otsu阈值化的步骤,把亮度比较暗的车牌从背景中剥出来再和边缘图做交集,双通道验证,能大幅减少定位失败的概率。

其次,二值化的时机很重要。传统流程是定位后在车牌区域内二值化,但如果你用的是蓝色车牌,在灰度图上字符和底色的灰度差往往不大,二值化很容易把字符和底色混在一起。更好的做法是在定位前用颜色特征提取蓝色区域作为候选,然后转灰度,再二值化。这样一来,即使图像在逆光环境下整体偏暗,蓝色区域依然是稳定的。

第三,字符分割后别忘了对每个字符做尺寸归一化。模板匹配对尺寸非常敏感,我在调试时遇到过一个案例:数字“1”和字母“I”最容易被混淆,就是因为“1”的宽度远小于模板而“I”的宽度接近模板,匹配分接近后就会错判。解决方案是识别前统一缩放高度,但宽度保持原始比例,再根据字符宽度的先验知识排除明显不合理的结果。

4.3 从传统方法升级到深度学习的思路

这套代码虽然基于传统图像处理,但它给深度学习方案留下了清晰的扩展空间。字符识别子程序recognizeChar是一个非常干净的函数级接口,输入一张字符图,输出一个字符。你已经把分割做完了,剩下的就是把模板匹配换成已经训练好的分类器。在MATLAB里可以用fitcecoc训练一个多分类SVM,或者用Deep Learning Toolbox里的trainNetwork跑到简单的CNN,输入输出接口完全不用改。

同样,端到端的深度学习车牌识别模型在公开数据集上已经能拿到99%以上的准确率,但代价是你需要大量标注数据和GPU资源。对这个毕设或者课程设计来说,用传统方法做一遍,理解图像处理每一步的物理意义,然后在此基础上再对比深度学习方案,你会真正明白有哪些问题传统方法解决不了,以及深度学习是怎么解决这些问题的。这种“先通传统,再学深度”的路线,对个人成长的价值比直接调用一个现成API高得多。

5. 扩展:从单张图片到实时视频流

5.1 视频流处理的帧间隔优化

很多做完图片版车牌识别的人,下一步就想把它接到摄像头或视频文件上。这里有个现实问题:MATLAB直接对视频流做全帧识别,每帧2到3秒的速度完全无法做到实时。实际工程中常用的策略是抽帧处理,典型做法是每秒只需要对2到3帧做完整识别,中间帧只检测车牌是否还在画面里。

在MATLAB里用VideoReader读取视频文件很简单,但注意readFrame读取的是压缩后的帧,颜色空间和静态图片可能有细微差异,建议从第一帧建立背景模型后,后续每帧先计算帧差,只有帧差超过阈值才做全流程识别。监控场景下大多数时间画面是静止的,这个优化能把平均CPU占用降一个数量级。

5.2 识别结果后处理:提升连续帧稳定性

单帧识别偶尔会出错,但连续帧不会在同一位置连续错。工程上可以做一个滑动窗口投票:连续识别5帧,每个字符位置选出现次数最多的结果作为最终输出。比如某字符5帧结果分别是“3”、“8”、“3”、“3”、“3”,那最终结果就是“3”。

这个方法朴素但非常有效,尤其适合进出口闸机、停车场道闸这类车辆静止或慢速通过的场景。注意投票帧数不要太多,否则车辆快速通过时,帧间车牌位置变化太大,字符分割的结果反而不稳定。5到7帧是一个比较平衡的经验值。

5.3 项目中进一步扩展的可能方向

这套代码的下一步扩展方向可以参考以下几个方面。

一是增加车牌颜色识别子程序,把蓝牌、黄牌、绿牌(新能源)和白色车牌区分开。这一步在定位子程序之前完成,根据像素颜色分布直接圈定候选区域,能显著提升定位的鲁棒性,也能为后续的车辆分类提供输入。

二是增加字符模板库的覆盖范围。中国车牌包含汉字、字母和数字,汉字和字母的字体风格有细微差别,模板匹配如果不管字体风格差异,识别率会掉得厉害。一个更稳妥的方案是对每个字符保存多套模板,匹配时取最高得分。

三是将识别结果与车牌归属地信息结合。比如汉字部分识别以后,可以根据前2位字符判断归属地,作为车辆行程管理和区域统计的输入。严格来说这一步已经不涉及图像算法了,更多是业务应用层的设计,但在毕设里加上,会显得整个系统完整度更高。

6. 实操心得与调试建议

整套项目做下来,我的感受是一个字:磨。跟参数较劲的时间远多于写代码的时间,但这也是最有收获的部分。

印象最深的一次是调试定位子程序,连续三天定位准确率只有60%。刚开始怀疑是边缘检测阈值问题,调了几天没起色。后来把每一步中间结果都打出来看,才发现问题是输入的测试图尺寸跨度太大,有高清的相机图片,也有低分辨率的监控截图。用同一套面积阈值,大图上的车牌面积轻松超过阈值,小图上的车牌面积连阈值的一半都不到。后来改成先归一化图像尺寸,再统一参数,准确率一下就上去了。这让我意识到,参数不是越调到某个固定值越好,而是要根据输入特性做自适应匹配。

如果你也在做类似项目,我的建议是:先把代码从头到尾跑通,再用10张不同场景的图去测,统计失败案例,从失败案例里找共性原因。不要一开始就追求极高的识别率,先把整个流程打通,再逐步提高每个环节的鲁棒性。车牌识别这个项目最大价值在于,它几乎是图像处理知识体系的一个微缩样本,滤波、边缘检测、形态学、投影、特征匹配全用上了,每一块知识都能找到直观的落地点。把这个项目认真啃下来,你再来回看图像处理的课本,会觉得所有概念都变成了实实在在的像素操作。

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

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

立即咨询