☰
MATLAB车牌识别实战:边缘检测定位与模板匹配字符识别
2026/10/17 18:43:46 网站建设 项目流程

简介:这套资源是基于MATLAB实现的车牌识别完整源码包,主要面向计算机视觉初学者、图像处理课程学生以及毕业设计人员。项目围绕车牌识别全流程组织,包含图像灰度化与二值化、中值滤波去噪、Canny边缘检测、霍夫变换定位、连通域分析、垂直投影字符切割、特征提取与分类识别等关键代码模块,并配有主程序main.m及形态学、倾斜校正、裁剪、投影、字符识别等独立脚本。压缩包共69个文件,含44个bmp字符模板、14个jpg测试图片、10个m源码文件及1个说明txt,整体大小4.51MB,目录区分“字符模板”“测试图片”“源代码”等模块,便于按步骤对照学习。已有539人浏览学习。这套代码覆盖面完整,读者既可以整体运行查看识别效果,也可以分步调试理解每个算法的MATLAB实现方法,还能利用模板库自行扩展不同省份字符。对完成课程设计或入门图像处理与模式识别均有较强参考价值。

1. 基于MATLAB的车牌识别:识别率瓶颈在定位,不在字符

基于MATLAB的车牌识别听起来是被做烂的题目,但真拿一张路边随手拍的照片跑一遍,你会发现多数开源Demo连车牌在哪都找不准。我拆这个资源包时印象最深的一点是:整个流程中定位环节占了近一半的代码量,字符识别反而是最短的一段。这套实现做的事情很直接——输入一张包含车牌的普通图像,输出车牌字符串,覆盖灰度化、边缘检测、形态学定位、字符分割、模板匹配一条完整链路。它适合正在做图像处理课程设计、毕设选了这个方向、以及想不依赖深度学习框架快速搭一个可解释识别基线的读者。跑通之后你再替换成深度学习识别器,前面的定位分割模块仍能直接复用。

2. 车牌定位:Sobel边缘与形态学闭运算的双重筛选

车牌定位是整个识别流程里最值得细看的模块,因为后续分割和识别的误差基本都在这里开始累积。一个直觉是“先把彩色图转成二值图再找矩形”,实际做一遍就会发现户外光照、车身反光、路牌干扰都会让简单二值化直接翻车。这里把定位分成两层筛选:先用边缘检测提取车牌边框特征,再用形态学闭运算把散碎边缘连成候选块,最后用几何约束过滤非车牌区域。

2.1 灰度化与Sobel边缘检测:为什么不用Canny

大多数输入图像是彩色照片。直接对彩色图做形态学处理有两个问题:三个颜色分量的噪声会叠加,亮度差异对结果的影响没有灰度图直观。因此第一步永远是灰度化,把后续所有操作的输入维度降低。

% 读取原图 img = imread('plate_sample.jpg'); % 彩色转灰度 gray = rgb2gray(img); % 水平方向的Sobel边缘检测,灵敏度阈值0.15 bwEdge = edge(gray, 'sobel', 0.15);

三个步骤分别对应读取、降维、提取边缘。这里用edge(gray, 'sobel', 0.15),第三个参数是灵敏度阈值:值越大,被判定为边缘的像素越少;值越小,边缘越密集。阈值0.15是一个经验起点,实际调试时按0.05的步长从0.1扫到0.3,再用可视化叠加确认效果,会比死记一个阈值可靠得多。

为什么选Sobel而不是Canny?车牌上下边框的水平边缘非常连续,Sobel对水平梯度的响应强、边缘线条粗、断裂少,后续闭运算更容易缝合;Canny的滞后双阈值对参数更敏感,户外环境下容易把车身纹理、树叶阴影也判定成边缘,产生大量碎片。边缘图里的碎片越多,闭运算越容易把干扰区域和车牌区域合并在一起,定位框随之整体偏移。

一个容易被忽略的细节是:Sobel默认提取整幅图像所有方向的边缘,实际车牌区域主要依赖水平边框,所以也可以用fspecial自定义一个水平方向的Sobel核来实现定向提取。这会让边缘图更干净,代价是丢失垂直边框信息,折中做法是先用默认方向提取,让形态学闭运算去做连接。

2.2 形态学闭运算与孔洞填充:把散点连成候选块

边缘图是一堆散碎的短线段,车牌区域的长边框和字符纹理被道路标线、车身装饰条打断是常事。形态学闭运算就是干这个的:先膨胀再腐蚀。膨胀把相邻边缘点缝合起来,腐蚀再把扩张出去的多余毛刺收回来,整体面积变化不大,但内部连通性大幅提升。

% 定义矩形结构元素:宽45、高15 se = strel('rectangle', [45, 15]); % 闭运算 bwClose = imclose(bwEdge, se); % 填充内部孔洞,让车牌区域变为实心块 bwFill = imfill(bwClose, 'holes');

结构元素尺寸是闭运算的唯一参数,也是定位精度最敏感的参数。宽45、高15是按常规车牌尺寸推算的:车牌物理尺寸约440mm×140mm,宽高比约3.1:1,字符间隙在宽度缩放到约600像素时大约为15像素左右。结构元素太窄,字符之间的缝隙缝合不上,车牌区域会断成两块;太宽,路牌、车灯、车身装饰条会被误并为同一个连通块。如果不确定尺寸,常见做法是对多个尺寸各做一次闭运算,把所有候选区域都交给下一层几何筛选去淘汰,而不是在一开始就赌一个参数。

孔洞填充在闭运算之后,针对的是车牌内部字符与字符之间的空隙。填充之后车牌区域从“空心矩形框”变成“实心矩形块”,这就是后续 regionprops 能正确计算长宽比的前提。如果跳过这一步,计算出的区域面积会明显小于实际车牌,面积下限阈值就不好设。

2.3 连通域筛选:用长宽比和面积过滤非车牌区域

闭运算结束后,图中通常还残留多个连通块。接下来用几何属性做第二层筛选。车牌是一个宽高比稳定的矩形,这两个约束能排除掉大部分干扰区域。

% 标记连通域 labeled = bwlabel(bwFill); % 提取每个连通域的包围框与面积 stats = regionprops(labeled, 'BoundingBox', 'Area'); for k = 1:numel(stats) w = stats(k).BoundingBox(3); h = stats(k).BoundingBox(4); ratio = w / h; area = stats(k).Area; % 保留宽高比2.5~5.5、面积大于2000的候选块 if ratio > 2.5 && ratio < 5.5 && area > 2000 candidate = stats(k).BoundingBox; % 从原图中裁出候选区域交给后续分割 plate_region = imcrop(gray, candidate); end end

宽高比范围为什么给到2.5到5.5而不是严格的3.1?因为车辆停放角度、拍摄视角都会改变车牌在画面中的投影比例,车头正对镜头时接近3.1,斜向拍摄时可能被压扁到2.4,侧向拍摄时可能拉长到4以上。范围太窄会把倾斜拍摄的车牌过滤掉,范围太宽会混入大量长条形干扰物。面积阈值2000同样不是通用值,它依赖图像分辨率。这里建议按“字符密集程度”来辅助判断:车牌区域内部边缘像素密度远高于周边,可以在筛选结果里再加一条边缘密度条件。

面积筛选之后还有一个可选的第三层校验:候选区内部字符纹理的方差。车牌的纹理特征是字符与底色交替出现,灰度直方图呈现双峰结构,而车身面板、路牌是平滑区块。用std2计算候选区灰度方差,能过滤掉一部分外形像车牌、内部却是空白的干扰物。加上这一层之后,定位的误判率通常能再降一截。

3. 字符分割:垂直投影与连通域校验,少一个都翻车

定位裁出的车牌区域包含一串字符,字符分割的任务是把它们逐个切出来。常见做法是垂直投影法:统计每一列的黑色像素数量,字符之间存在空隙,空隙处的像素数会形成波谷,从这个谷底切开。但直接套投影法在汉字车牌上必然踩坑,因为汉字内部笔画密集,投影波谷比字母数字复杂得多。

3.1 垂直投影法:按列像素密度切分字符

先对车牌区域做二值化,再统计每一列中前景像素的数量。字符区内列像素值高,字符间隙处列像素值接近零,投影曲线会呈现连续的高峰—低谷形态。波谷位置就是候选分割点。

% 车牌区域二值化:灰度大于0.6置为1 plate_bw = im2bw(plate_region, 0.6); % 统计每一列的前景像素数(假设前景为黑色像素) proj = sum(plate_bw == 0, 1); % 找到投影值为零的列位置 zero_cols = find(proj == 0);

这里的im2bw(plate_region, 0.6)是固定阈值二值化,0.6表示灰度归一化到0~1区间后的分割阈值。如果车牌图像亮度均匀,0.6左右效果不错;如果光照不均,固定阈值会让部分字符区域变白、背景变黑,投影曲线失真。更稳的替代方案是graythresh自动计算Otsu阈值,或者用imtophat做顶帽变换先校正亮度不均。

投影值计算中,plate_bw == 0表示取黑色像素,这是基于“白底黑字”车牌的二值化约定。二值化方向搞反了,投影曲线就会变成倒置形态,这是新手最常犯的错误之一。

3.2 连通域分析做二次校验:两个经典分割错误

投影法对“京”“鲁”这类结构复杂的汉字非常不友好。汉字内部笔画密集,投影到列方向时,笔画之间常常没有清晰的零值空隙,导致一个汉字被误判成两个字符。另一种情况恰好相反:数字“1”宽度窄、面积小,在投影图中呈现的波峰不明显,容易被当作噪声丢弃。

% 对二值图做连通域标记 cc = bwconncomp(plate_bw == 0); stats_char = regionprops(cc, 'BoundingBox', 'Area'); % 按包围框左边界排序 boxes = reshape({stats_char.BoundingBox}, [], 1); positions = cellfun(@(b) b(1), boxes); [~, order] = sort(positions); sorted_boxes = boxes(order);

这段代码的作用是:把二值图中所有连通的字符块找出来,按水平位置从左到右排序,再按位置和宽度依次排列成字符串顺序。连通域方式天然规避了“汉字被投影切碎”的问题——汉字笔画虽然密集,但在二维空间里仍然是连通的,整体会形成一个大的连通块。需要补的校验条件有两个:连通块的高度要达到车牌区域高度的50%以上,宽度不能小于整体宽度的5%,否则判定为噪声或边框残留。

一个常见的隐藏坑是二值化后车牌边框和字符粘连。如果车牌边缘框没有被完全去掉,边框会与第一个字符或最后一个字符形成一个连通块,导致分割结果出现“7个字符加上一条边框”。解决方式是在连通域分析之前,用imclearborder清除与图像边界相连的对象,或者在投影分割前先对车牌区域做一次腐蚀操作,把边框与字符的连接点断开。

3.3 尺寸归一化:分割完必须做的统一化处理

切割出的字符宽度、高度各不相同,模板匹配要求输入尺寸一致,因此每一张字符图都要归一化到统一尺寸。常见做法是缩放到48×24像素,这个尺寸能保留足够的笔画细节,又不会让匹配计算量过大。

% 将单个字符图像归一化为48×24 char_norm = imresize(char_img, [48, 24], 'bilinear'); % 统一转成二值图,消除灰度差异 char_bw = im2bw(char_norm, 0.5);

尺寸归一化有两个细节。第一,缩放算法的选择:双线性插值计算平滑,适合字符这种笔画型图像;最近邻插值会保留锯齿,对笔画细节不利。第二,二值化阈值统一使用0.5,是为了让模板匹配阶段只比较笔画的形状特征,而不是比较灰度深浅。如果字符图像本身是白底黑字,统一二值化之后再送入匹配。对于含有边框噪声的字符块,可以在归一化后加一步开运算,把细小的毛刺去掉。

4. 字符识别:模板匹配池设计与拒识阈值怎么定

字符识别这层在这套MATLAB实现里用的是模板匹配。模板匹配的思路非常朴素:把分割出的字符与模板库中的每一个模板做相似度计算,取相似度最高者作为识别结果。朴素不代表简单,模板库怎么构建、匹配算法怎么选、阈值怎么设,直接决定识别的上限。

4.1 构建模板库:样本来源与命名规范

模板库的质量决定匹配识别的质量,这是整个识别模块最容易被低估的部分。模板来源有两个方向:一是从同一拍摄设备采集的清晰车牌中手动切割字符,二是对已有的字符图像做几何变换生成多尺度变体。

% 模板文件夹结构示意 % templates/ % hans/ Wangzi/ shuzi/... % 读取模板 template_dir = 'templates/'; files = dir(fullfile(template_dir, '*.bmp')); for i = 1:numel(files) [~, name, ~] = fileparts(files(i).name); template_img = imread(fullfile(template_dir, files(i).name)); template_bw = im2bw(template_img, 0.5); templates.(name) = template_bw; end

命名规范建议采用“字符名称_编号”的格式,比如“京_01.bmp”“A_03.bmp”。这样在匹配结束后可以直接从变量字段名提取识别结果,避免维护额外的映射表。每个字符的模板建议准备5到10个变体样本,覆盖不同光照、不同粗细的印刷体字形。同一个字符不同模板之间,匹配分数取最高值。

4.2 汉字的特殊处理:第一字符单独匹配,不混池

车牌字符串的排列规则是“汉字+字母+数字”,其中第一个字符固定为汉字。如果把汉字和字母数字混在同一个模板池里匹配,宽度差异和结构差异会造成分数偏向,识别率反而不如分组匹配。

% 识别第一个字符:只用汉字模板池 char1_score = zeros(1, numel(han_templates)); for k = 1:numel(han_templates) char1_score(k) = match_score(char_bw_1, han_templates{k}); end [~, idx1] = max(char1_score); char1 = han_labels{idx1}; % 识别后续字符:字母+数字模板池 for j = 2:7 % ... 同上逻辑,从 alnum_templates 中选最高分 end

这里的关键是分池匹配。第一个字符只与汉字模板比较,后六个字符只与字母数字模板比较。汉字和字母数字在字形结构上差异显著,混池匹配偶尔会出现“某个字母恰好比某个汉字更相似”的误判,分池后这个概率被彻底排除。

汉字识别还有一个进阶技巧:对候选前三名做局部区域二次比对。汉字中“鲁”和“辽”、“沪”和“护”字形高度接近,整体匹配分数可能只差零点几;此时截取字符的右下角区域做局部匹配,局部差异会被放大,能有效区分相似字形。

4.3 匹配分数与拒识阈值:识别不出时要敢于不认

模板匹配的输出是一组相似度分数,但分数本身没有绝对意义。同一字符在不同光照条件下提取出的二值图可能差很多,导致最高分仅有0.5;与此对应,一个噪声块也可能和某个模板意外相似到0.7。因此拒识阈值要按“最低可接受分数”反推。

% 将最佳匹配分数转为置信度并判断阈值 best_scores = zeros(1, 7); % ... 在匹配循环中填充 best_scores if best_scores(j) < 0.6 result_char{j} = '?'; % 拒识 else result_char{j} = best_label; end

阈值0.6是底线,实际项目里通过一批测试集的分数分布来标定:收集300张车牌图片,统计所有正确匹配的分数,取最低值向下偏移0.05作为拒识阈值。识别不出时输出“?”,不要强行给一个低置信的答案,这样才能保证整体识别结果的可靠度。

5. 常见问题排查:五个翻车点与对应修改方案

这里整理五条拆解和调试这套MATLAB车牌识别资源时最常遇到的踩坑记录。每一条都是现象、原因、解决三个层次,对应到具体代码行和参数。

5.1 定位框总是切掉车牌的一部分

现象:输出候选框时,车牌左边或右边被切掉,分割出来的字符数量不对。原因:形态学闭运算的结构元素尺寸偏小,车牌边缘与字符区域之间的缝隙没有被完全缝合,整个车牌被拆成多个连通块,筛选时只留下了面积最大的那一块。解决:把strel('rectangle', [45, 15])的宽度加到60甚至80,重新做闭运算和区域筛选;或者对多个尺寸做闭运算后合并候选区域。

5.2 二值化之后车牌反白,投影曲线全部倒置

现象:分割阶段投影图中的波峰波谷与预期完全相反,字符区域的数据反而接近零。原因:二值化约定方向与sum(plate_bw == 0, 1)不一致,原本是黑底白字的车牌转换成了白底黑字,统计黑色像素时统计到的是背景。解决:先查看plate_bw中字符区域的值是多少,要么把二值化取反plate_bw = ~plate_bw,要么把统计条件改为sum(plate_bw == 1, 1),保证统计的是字符像素。

5.3 分割出来只有四个字符,中间的字母丢了

现象:车牌印刷清晰,但分割结果缺少“·”分隔符后面的字母或数字,连通域分析也没有找到。原因:字符宽度过窄,被当作噪声过滤;或者字符与边框粘连,开运算处理时整个字符被腐蚀掉。解决:检查连通域筛选的宽度阈值,把最小宽度从5%降到3%以下;对二值化后的车牌区域用imdilate轻微膨胀一次,把断裂笔画连上,再做连通域分析。

5.4 汉字识别结果全错,字母数字基本对

现象:前六个字符识别正常,但第一个汉字永远不对。原因:模板库中汉字样本与测试图像字体差异太大,或者汉字模板在归一化时保持了原始尺寸,没有统一缩放到48×24。解决:汉字模板统一用imresize(汉字, [48, 24], 'bilinear')做归一化;同时保证模板和分割字符使用相同的二值化阈值0.5;如果字体差异确实太大,补充该字体的模板样本。

5.5 数字8经常被识别成3,字母G和6混淆

现象:单独跑每个字符的匹配分数都超过0.7,但结果就是错。原因:模板匹配对笔画粗细和闭合程度敏感,字符“8”和“3”在二值化后轮廓相似,尤其当字符笔画偏细时,“8”的下半环容易被腐蚀成开放结构。解决:在匹配前对字符做统一细化处理,把图像线宽归一化到固定粗细;或者改用归一化互相关系数normxcorr2替代简单的像素差匹配,减少灰度绝对差异的影响。

6. 脚本化批量验证:把识别率从“玄学”变成可统计

单张图片跑出结果只能叫“能运行”,不能叫“可用”。我一般会把识别流程封装成一个函数,再写一个批量脚本跑整个测试集,用统计数字判断识别率到底是多少。封装函数的意义在于:所有参数集中管理,调参只改一处,不用每次打开主脚本翻半天。

function result = plate_recognition(imgPath) % 统一封装:输入图像路径,输出识别结果 % result.success: 是否识别成功(1/0) % result.plate: 识别出的车牌字符串 % result.confidence: 7个字符置信度的均值 img = imread(imgPath); % ... 定位、分割、识别流程,返回 result end

批量验证脚本遍历测试集目录,对每张图调用函数,并把每张图的识别结果、置信度、是否成功写入表格。这里刻意保留成功和失败样本的完整记录,不能只统计成功率数字,不然永远不知道失败样本长什么样。

files = dir('testset/*.jpg'); results = table(); for i = 1:numel(files) [~, name, ~] = fileparts(files(i).name); r = plate_recognition(fullfile(files(i).folder, files(i).name)); results = [results; {name, r.plate, r.success, r.confidence}]; end % 统计整体成功率 success_rate = sum(results.Var3) / height(results); disp(['识别成功率: ', num2str(success_rate)]);

跑完一轮之后,我会把所有失败样本的关键参数记录成一致性表格:是哪一步失败的(定位、分割、识别)、该步当时的阈值参数、失败原因描述。参数自检的价值在这个环节才真正体现出来——如果三张失败图都是定位阶段被长宽比筛掉的,那就是宽高比范围的问题;如果都是识别置信度低,那就是模板库覆盖不足。从那以后,我每次拿到一组新测试集,都会强制自己走一遍这张自检表,再谈识别率。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询