简介:基于MATLAB的车牌识别项目提供了一套完整的车牌自动识别源码实现,覆盖图像灰度化与去噪、车牌定位与分割、字符切割、特征提取与模板识别等全流程,适合计算机视觉初学者、MATLAB开发者以及需要完成车牌识别课程设计或毕业设计的学生参考学习。压缩包共69个文件,其中10个m脚本为算法主体,覆盖主程序、形态学处理、投影切割、字符识别等功能模块;44个bmp是各省简称及数字字母字符模板;14张jpg为不同场景下的车辆测试图片;整体仅4.51MB,目录结构清晰,便于直接运行和二次开发。该资源已有539人学习下载。通过研读源码,可掌握边缘检测、连通区域分析、垂直投影、模板匹配等关键技术,并能利用自带模板与图片验证识别效果,是快速入门MATLAB图像处理和模式识别的一份实用参考。
1. 基于 MATLAB 的车牌识别:从课程设计到能落地的图像处理方案
车牌识别(LPR,License Plate Recognition)是数字图像处理领域最经典的综合性实战项目,也几乎是每个 MATLAB 学习者绕不开的里程碑。它不是单个算法能搞定的任务,而是「图像预处理 → 车牌定位 → 字符分割 → 字符识别」四步流水线。这四步里每一步都有成熟的算法选型,也有大量值得记录的工程细节,尤其是光照不均、车牌倾斜、字符粘连这类真实场景下的常见翻车点。我有段时间在本地用模拟车牌的图片反复调参,走了不少弯路,也积累了可复现的完整流程。这篇笔记面向两类人:一是正在做课程设计、想跑通一个靠谱车牌的初学者,二是已经在用其他语言做识别、想用 MATLAB 快速验证算法效果的熟手。读完你会明白怎么从零搭起一条处理链,每步的算法为什么这样选,以及参数调到哪里才算「能用」——毕竟实验室里跑通一张图容易,换一批图还能稳定识别才是真本事。
2. 车牌定位:从灰度图到候选区域的常规打法
2.1 为什么先做灰度化和边缘检测:把彩色图降维的思路
拿到一张包含车牌的图片,第一步不是急着找车牌,而是把问题简化。彩色图含 RGB 三个通道,每个通道的纹理信息互相干扰,直接处理计算量也大。常见做法是先转成灰度图,再用边缘检测把图像里的「突变」位置标出来。车牌区域本身有两大特征:一是字符与背景之间对比度高,二是车牌边框和字符共同构成密集的垂直线段。这两点让车牌成为整张图里边缘密度最高的区域之一。
灰度化是整套流程的地基。MATLAB 里用 rgb2gray 就能完成,但有个容易被忽略的点:如果是模拟生成或夜间拍摄的图,灰度化之前可以考虑先做一次光照补偿,比如用直方图均衡化(histeq)把过暗或过曝的区域拉伸开。我踩过的坑是,在强逆光下直接做边缘检测,车牌区域反而因为对比度过低而「隐形」,先均衡化再走后续流程会稳很多。
边缘检测算子方面,Sobel、Prewitt、Canny 都有人用。我的习惯是优先用 Canny,它对弱边缘的连续性保持得更好,车牌的上下边框不容易断。但 Canny 的两个阈值(thresh 和 thresh*2)需要根据图片整体亮度微调,过于依赖默认参数换图就容易失效。这里贴一段最小可跑的定位前置代码:
img = imread('car.jpg'); gray = rgb2gray(img); grayEq = histeq(gray); % 光照补偿,弱光图强烈建议加 bwEdge = edge(grayEq, 'canny', [0.1 0.25]); % 双阈值,前小后大 figure, imshow(bwEdge);分段说明:第一步读图,第二步转灰度,第三步直方图均衡化,第四步 Canny 提取边缘。这里两个阈值不是随便写的,[0.1 0.25] 表示低阈值 0.1、高阈值 0.25,高阈值决定哪些边缘是「强边缘」必须保留,低阈值决定弱边缘能否连成闭合轮廓。换到对比度不同的图片时,低阈值往 0.08 调、高阈值往 0.2 调是止血操作;如果边缘断得厉害,多半是高阈值太高,降低它比换算子更见效。
2.2 形态学操作让车牌区域「连成一块」:闭运算与矩形结构元
边缘检测之后得到的是密集的线条,但这些线条还是散的,不能直接作为车牌候选区域。这时候需要形态学操作来「缝合」它们。闭运算(先膨胀后腐蚀)是首选,因为车牌区域的边缘之间缝隙小,闭运算能把缝隙填上,让整个车牌变成一个连通的矩形块。背景里的树干、路面标线等杂乱边缘,因为不满足这种「紧密排列」的形态特征,多数情况下不会被缝合到一起。
结构元素(structuring element)的形状和尺寸决定缝合效果。车牌是水平方向长、垂直方向短的矩形,所以结构元也用矩形,比如 strel('rectangle', [5, 19])——垂直方向 5 像素、水平方向 19 像素。这个比例不是拍脑袋定的,水平方向比你预期的字符间距略大,才能把相邻字符间的空隙填上;垂直方向设小一些,避免把车牌以外的高边缘区域也误并进来。
形态学做完后,下一步是找连通域并筛选。一个很实用的技巧是:先标记所有连通区域,再用「宽高比」过滤。标准车牌宽高比大约在 2.5 到 3.5 之间,加上拍摄角度的影响可以放宽到 2 到 4。面积也要限制,滤掉那些虽然形状像但实际很小的干扰块。下面是定位环节的代码:
se = strel('rectangle', [5, 19]); % 矩形结构元:高5宽19 bwClose = imclose(bwEdge, se); bwFill = imfill(bwClose, 'holes'); % 填充内部孔洞 stats = regionprops(bwFill, 'BoundingBox', 'Area'); for k = 1:length(stats) w = stats(k).BoundingBox(3); h = stats(k).BoundingBox(4); ratio = w / h; if ratio > 2 && ratio < 4 && stats(k).Area > 2000 plateBox = stats(k).BoundingBox; % 保存候选车牌框 end end plateImg = imcrop(img, plateBox);这段代码的思路是:闭运算把边缘缝成块,imfill 填掉车牌字符之间的孔洞,让整个区域更「实心」,然后用 regionprops 提取每个连通域的宽、高和面积。筛选条件里的宽高比 2~4 和面积阈值 2000,是我多次换图后调出来的经验值,前者相对通用,后者和图片分辨率强相关——如果你的图是 1920 宽的大图,这个面积阈值可能要翻几倍。具体使用中可以先画一幅所有连通域外接矩形的图,看看干扰块的面积分布,再决定阈值。
2.3 定位不准时优先查什么:边缘断点与结构元尺寸
定位环节的常见失败模式就几种。第一种是候选框把整块车身框了进去,通常是结构元水平尺寸设得过大,把车灯、车窗等区域也并进了同一连通域;第二种是车牌区域被漏掉,原因是 Canny 高阈值太高,车牌边框边缘断裂太严重;第三种是选出了多个候选框,说明过滤条件太宽,需要收紧宽高比范围。
调试这些问题的顺序有讲究。先看 bwEdge 的显示结果,确认边缘是否完整;再看 bwClose 的结果,确认车牌是否被缝合成了完整块;最后才调宽高比和面积阈值。从前往后逐层排查,能快速定位问题出在哪一级。很多人一上来就改面积参数,越改越乱,这是没理解每层操作的责任边界——边缘检测管「哪些线保留」,形态学管「线怎么连成块」,筛选规则管「哪些块是车牌」。
3. 字符分割:把车牌区域切成 7 个独立字符
3.1 投影法分割的原理与实现:垂直投影为什么够用
车牌定位完成后,得到的一般是一张只剩车牌区域的彩色或灰度图。接下来要把这块区域均匀地切成单个字符。国内标准车牌是 7 个字符(如「某A·12345」),但字符之间有间隔、首字符后面还有分隔符点,直接均分切割是不靠谱的。更稳的做法是利用字符排列的「投影」特性:把二值图按列累加像素值,字符所在的列区段会有明显的响应,字符间隔的列区段响应接近零,这就是垂直投影法的核心思想。
投影法之所以够用,本质上是因为车牌字符的排列是规整的,每个字符占据一个独立的列区间,互相之间的间隔足够产生「谷值」分隔信号。实现时先要把车牌区域转成二值图,这一步的阈值选择直接决定投影效果——我稍后会展开说。垂直投影的 MATLAB 实现很短,核心就是 sum 函数沿着列方向累加。
plateGray = rgb2gray(plateImg); bwPlate = imbinarize(plateGray); % 自动阈值二值化,后续可调 projCols = sum(bwPlate, 1); % 垂直投影:每列白像素数 isChar = projCols > 2; % 门限:大于2视为字符列 colRanges = find(diff([0, isChar, 0]) == 1); colEnds = find(diff([0, isChar, 0]) == -1) - 1; % colRanges 和 colEnds 一一对应,组成每个字符的左右边界投影数组 projCols 的长度等于图像宽度,值表示该列有多少个白色像素。isChar 做一次阈值判断,把「有字符」的列标成 1。diff 然后 find 的技巧是找到 0→1 和 1→0 的跳变位置,分别得到每个字符段的起点和终点。这里阈值「2」不是固定的,如果字符在二值化后变细,阈值要降到 1;如果图像噪声多,要提高到 3 或 4,否则会把噪声列也算成字符。
3.2 二值化阈值的选择:固定阈值与自适应阈值的取舍
二值化是分割环节最容易翻车的一步。用 imbinarize 的默认 Otsu 方法(全局阈值),在均匀光照下车牌图片表现不错,因为字符和背景的灰度分布是两个明显的峰。但真实拍摄场景经常有偏光、阴影或金属反光,全局阈值可能把部分字符的背景也二值化成白色,或者反过来把字符断开。这时候自适应阈值(local threshold)往往更可靠,它按局部邻域计算阈值,能适应同一张图里不同区域的亮度差异。
MATLAB 里自带的 adaptthresh 函数就是干这个的,核心参数是邻域大小 sensitivity。灵敏度设得越大,二值化后保留的细节越多,但也更容易引入噪点;设得太小,字符可能会缺笔画。我的经验值是 sensitivity 在 0.3 左右起步,根据显示的二值图微调。这里给出完整的分割后再筛选步骤:
T = adaptthresh(plateGray, 0.3, 'ForegroundPolarity', 'dark'); bwPlate = imbinarize(plateGray, T); bwPlate = ~bwPlate; % 车牌字符是深色,需要反色让字符为白色 bwPlate = bwareaopen(bwPlate, 50); % 去掉小噪点块这段代码的要点是 ForegroundPolarity 参数:它告诉算法前景(字符)是亮还是暗。国内蓝底白字车牌中,字符比背景暗的情况不常见,但新能源绿牌或者某些反光严重的场景里,极性设置错了会导致二值化结果完全颠倒,字符变成背景、背景变成字符。加 bwareaopen 是为了清除面积小于 50 像素的孤立噪点块,这些块通常来自车牌螺丝或边框残留。分割完成后,逐个把字符从原图上裁出来,统一缩放到相同尺寸,识别环节就能用统一的特征了。
3.3 分割结果不理想时的三个快速修正手段
分割最常见的问题有三种。一是字符粘连,多发生在字符间距因拍摄角度而压缩时,垂直投影的谷值不够低,两个字符被并成了一个。二是汉字断开,尤其是「某」这类笔画多的汉字,二值化后中间可能出现断裂,投影时被拆成了两块。三是分隔符被误判成字符,国内车牌的圆点或方点虽然小,但投影后会形成一个小响应区,容易被当成字符。
针对这三个问题,我一般会先做同一件事:显示投影曲线图和二值图,肉眼对齐确认问题原因。然后是修正手段——字符粘连时,尝试用形态学开运算把字符间的细连接断开;汉字断开时,反过来用闭运算把断裂的笔画连上;误判分隔符时,用一个简单的宽度阈值过滤:字符宽度与整体平均宽度差异超过 50% 的段直接丢弃。这些操作都是针对 bwPlate 的微调,代价小、见效快,比反复换二值化方法更省时间。经过这些处理,分割出的 7 个字符块就可以进入最后的识别环节了。
4. 字符识别:模板匹配与特征提取的实操对比
4.1 模板匹配的思路:为什么要统一尺寸和训练样本
分割出来的字符图,接下来要回答「这个字符是什么」。最朴素也最容易上手的方法是模板匹配:把每个待识别字符和预先存好的标准字符模板做相似度对比,取最相似的作为识别结果。实现模板匹配前,必须把待识别字符和模板缩放到同一尺寸,这是匹配准确率的基础。常见的缩放目标大小是 40×20 或 48×24,保持宽高比一致,不要压变形。
模板的来源通常有两种。一种是自己截取标准车牌字符图片,二值化后做成模板库;另一种是从公开的字体文件直接渲染生成。用字体生成的好处是量大、覆盖全,但要注意字体风格与车牌实际字体的差异。车牌字符的字体宽度、笔画粗细与否,对匹配结果影响很明显。这里给出模板匹配的简化版实现,逻辑清晰且容易扩展:
function label = matchChar(charImg, templates, labels) charRes = imresize(charImg, [24, 12]); % 统一尺寸:高24宽12 charVec = charRes(:); bestScore = -1; label = '?'; for i = 1:length(templates) tRes = imresize(templates{i}, [24, 12]); score = corr2(charVec, tRes(:)); % 皮尔逊相关系数 if score > bestScore bestScore = score; label = labels{i}; end end end这段代码的思路是:把待识别图与每个模板都缩放到相同尺寸,然后按列展开成向量,计算二者的相关系数。相关系数越接近 1,说明两张图的线性关系越强,匹配度越高。corr2 的好处是它对整体亮度和对比度变化不敏感,也就是说待识别字符的笔画粗细和模板略有差异时,匹配结果仍然相对稳定。坏处是对字符的形变很敏感,旋转超过 5 度就会明显掉分,所以前面分割时要把倾斜矫正做好。
4.2 特征提取:用网格特征代替全像素匹配,抗干扰更强
纯模板匹配依赖像素级比较,抗干扰能力有限。更实用的方案是先用特征提取把字符图像压缩成一个低维向量,再做分类。网格特征是其中最简单有效的手段:把归一化后的字符图划分成若干小格子,统计每个格子内的白像素密度,把这些密度值串成一个特征向量。这个特征向量的维度比原始像素少得多,且对微小位移和笔画粗细有一定容忍度。
网格划分的粒度影响识别效果。常见做法是 4×4 或 6×4 的网格,格子太少,特征区分度不足;格子太多,又回到了像素级比较的困境。我一般选择 6×4,也就是把字符图横向切 6 等份、纵向切 4 等份,得到 24 维特征。训练一个最近邻分类器,对中小规模的模板库来说速度和准确率都是可接受的。实际操作中把模板也做同样的网格化处理,待识别的字符和模板在同一个特征空间里做距离比较,会比全像素比较更稳定。贴出特征提取的核心代码:
function feat = extractGridFeature(img, rows, cols) imgRes = imresize(img, [rows*10, cols*10]); % 放大后分块更均匀 feat = zeros(rows, cols); h = size(imgRes, 1) / rows; w = size(imgRes, 2) / cols; for i = 1:rows for j = 1:cols block = imgRes(round((i-1)*h)+1 : round(i*h), ... round((j-1)*w)+1 : round(j*w)); feat(i, j) = mean(block(:)); % 每格白色像素占比 end end feat = feat(:); % 展平成列向量 end这段代码的可替换参数在于 rows 和 cols 的取值。把字符放大是技巧所在——直接在原图尺寸上分块,网格边界容易落在字符笔画边缘上,导致特征抖动。放大 10 倍后再分块,边界位置更稳定。mean(block(:)) 计算的是该格子里白像素的平均值,值域在 0 到 1 之间。特征向量的比较可以继续用相关系数,也可以用欧氏距离,在 MATLAB 里 knnsearch 一行就能实现。实践下来,网格特征在「同一字符不同字体」的场景下,比全像素匹配的错误率低一半左右。
4.3 识别结果验证:用多张图而不是一张图来评测
识别模型跑通后,最忌讳的是用一两张图验证就宣布「可用」。我习惯的做法是准备至少 20 张不同场景的车牌图:白天、傍晚、雨天、略微倾斜的角度都要覆盖。跑完后统计每个字符的识别率,而不是只看整张车牌对不对。整张对只不过说明运气好,单字符识别率才能反映算法短板。比如「0 和 O」「1 和 I」「B 和 8」这类容易混淆的字符,单独统计混淆对是很直观的反馈。
如果识别率在 90% 以下,不要急着换识别算法,先回头检查分割结果。很多识别错误其实是分割时把字符切歪了或者切断了——识别环节只是把前面的错误放大了。我经历过一次大面积误识别,排查后发现问题出在二值化阈值太高,字符「某」的笔画被切掉一块,模板匹配时和相似的字符产生了强相关。这类问题不看中间结果根本发现不了。识别率到 90% 以上后,才值得考虑更复杂的分类器换 CNN,或者增加模板数量。记住一个原则:流程中的每一步错误率是相乘的,分割 5% 的错误会让总正确率显著下降,先优化错误率最高的环节,收效最快。
5. 车牌识别避坑指南:四类常见问题的现象、原因、解决
做这个项目跑了一个多月,遇到的坑集中在四类:光照导致的前处理失效、倾斜车牌的定位失败、字符分割时的粘连与断裂、二值化极性搞反。下面逐条记录现象、根因和最终的处理策略。
坑 1:强逆光下车牌整体偏暗,定位框直接落到车灯上。现象:候选区域框住了车灯或车窗,车牌被忽略。原因:Canny 边缘检测对低对比度区域的响应很弱,车牌边缘没有形成足够的线密度,而车灯反而是高亮高对比区域。解决:先做直方图均衡化,再进边缘检测。具体做法是在 rgb2gray 后加 histeq,对比度拉伸会让车牌字符边缘重新「显形」。这是调参之外最值得优先做的事,效果往往是立竿见影的。
坑 2:车牌旋转超过 10 度,宽高比过滤把真车牌滤掉了。现象:字符是斜的,定位环节没有任何候选框通过过滤条件。原因:旋转使车牌的外接矩形宽高比变化,比如斜 15 度时水平宽度明显增大,比例超出 2~4 的范围。解决:先做倾斜矫正再做宽高比过滤。矫正方法不复杂——用 Hough 变换检测车牌边框的长直线,计算直线与水平方向的夹角,然后对车牌区域做旋转。实践中最常见的做法是:先粗定位到车牌区域,再做矫正,最后用更严格的宽高比二次筛掉误检区域。不矫正直接放宽过滤范围的方案不推荐,那是把其他干扰块一并放进来了。
坑 3:字符分割时「某」被切成两段。现象:项目中的「某」字(泛指一个常见汉字省称,替换含义)笔画密集,二值化后笔画之间出现细缝,垂直投影形成两个相邻的波峰,被分成两个字符。原因:汉字笔画密集区域的列投影值波动大,字符内部出现谷值,但谷值并未降到「无字符」的水平——问题出在二值化后笔画出现了断裂,而不是投影方向选错了。解决:使用闭运算把断口连上,然后重新投影。具体是 strel('line', 3, 90) 这样的垂直短线结构元做闭运算,注意控制长度,太长了会把字符之间的间隔也连起来。
坑 4:二值化后字符和背景的颜色关系反了,识别全乱。现象:二值图里车牌背景是白色、字符是黑色,后续投影和识别全失效。原因:拍摄时车牌类型不同(蓝底白字、黄底黑字、绿底白字),前景极性与 Otsu 的假设不一致;或者使用 adaptthresh 时没有指定 ForegroundPolarity,默认值不匹配当前字符颜色。解决:处理前明确车牌的字符极性,调用 adaptthresh 时显式传入 'ForegroundPolarity', 'dark' 或 'bright'。如果字符颜色在图片中不统一,比如反光导致白字变灰,就需要先做局部亮度更均匀的预处理。这个坑的特点是出错快、全错,但只要定义了字符极性并显示二值图确认一遍,就不会被它反复影响。
以上四坑基本覆盖了做车牌识别时前 80% 的翻车原因。大多数错误最终都能追溯到「输入没准备好」,也就是前处理没有适配图片的实际情况。先把中间过程可视化地检查一遍,再谈调识别网络,这是做这个项目最快的debug路径。
6. 验证与进阶:用批处理脚本做一次完整的端到端自检
项目做到能识别单张图还不够,我会建议你用一批图做完整的验证,同时把流程固化成一个批处理函数,为后续换算法留好接口。这个函数接收图片路径、返回识别结果字符,内部依次调用前文各部分。批处理跑完后计算识别率,再决定哪些环节值得深挖。
function result = recognizePlate(imgPath) img = imread(imgPath); % Step 1: 定位 gray = rgb2gray(img); grayEq = histeq(gray); bwEdge = edge(grayEq, 'canny', [0.1 0.25]); % ... 形态学、筛选、裁切 % Step 2: 分割 % ... 投影分割、二值化、去噪 % Step 3: 识别 % ... 模板匹配或特征 + 分类器 result = ['识别的车牌字符序列']; end这段框架代码的意图是结构化。你可以把每个 step 内的细节替换成自己调好的参数;如果之后要引入新的分割算法或识别模型,只需要改对应的 step 块,不影响整体流程。建议把每张图的中间结果保存到文件夹里,一次批处理跑完再逐个看,能很清楚地发现有问题的环节在哪里。
验证集的选择也很关键。不要只用干净的高清大图,那种图容易在实验室里「人人通过」。至少包含这样几类:有轻微倾斜的、背景有树荫或车身反光的、夜间弱光但肉眼可辨的、以及不同车牌颜色(蓝底、黄底、绿底)的样本。每类 5~10 张就足够暴露大多数问题。如果某类图识别率特别低,单独分析是在前处理、定位还是分割环节掉的链子。
经过这些验证,你会对流程的稳定性有更准确的判断。我自己做过一版识别,在实验室的干净图上正确率很高,但换到夜间场景掉到 60% 以下。后来发现解决问题的最短路径,不是上更复杂的模型,而是把前处理做到位:直方图均衡化、自适阈值、闭运算,这三步让夜间图的车牌区域「显形」程度提升了一大截。后面即便再用同样的模板匹配,识别率也能回到 90% 上下。这个经历让我学到一个习惯:遇到识别结果不好,先不要怪分类器,回去看中间图像,90% 的问题在特征提取之前就已经埋下了。希望这些方法对你有帮助,少走我当时走过的弯路。
本文还有配套的精品资源,点击获取