☰
MATLAB车牌识别系统实战:定位、分割与模板匹配
2026/10/27 11:37:15 网站建设 项目流程

简介:基于MATLAB的车牌识别系统设计资料包,面向数字图像处理课程设计、本科毕业设计及答辩场景,适合需要快速跑通车牌定位、字符分割与识别全流程的读者。压缩包共77个文件,整体仅2.11MB,包含53个bmp标准字符样本、19个jpg真实场景图、4个m源码文件与1份答辩PPT;bmp样本覆盖多省汉字及数字字母,jpg场景包括白底黑字等不同样式,m源码负责核心算法,可在MATLAB中直接运行调试。源码涉及图像灰度化、均值滤波、二值化、膨胀腐蚀、车牌区域提取及字符模板匹配等关键环节,可帮助理解从预处理到识别的完整链路。资源目录清晰,样本库与场景图分类存放,便于替换测试数据或扩展字符集。目前已有2528人学习下载,配套答辩PPT辅助成果展示,是兼顾算法实现与汇报输出的实用资料。

1. 基于MATLAB的车牌识别系统:从课程设计到能扛光照变化的识别流程

基于MATLAB的车牌识别系统在每年matlab图像处理大作业里几乎是固定嘉宾。你搜到的多数帖子都能跑出一张干净蓝牌,但很少有人告诉你:从大作业演示到能应对不同光照和角度的实拍车图,中间隔着的不是算法难度,而是定位和分割环节里一堆要手动磨的阈值。一套完整流程通常由预处理、车牌定位、字符分割、模板匹配识别四段组成,适合课程设计、本科毕设以及想快速验证视觉方案的工程师。这套方案的核心结论是:定位比识别更值钱,前面环节做扎实,识别其实只是收尾。

2. MATLAB车牌识别整体流程与预处理:灰度化、边缘检测和形态学的出场顺序

2.1 为什么先灰度化而不是先做边缘检测:颜色空间和通道选择

很多新手拿到车拍图第一反应是直接做边缘检测,结果会得到满屏树干、车窗和路沿的边缘,车牌淹没在噪声里。常见做法是先把彩色图转成灰度,再做边缘检测。原因很简单:车牌区域的特征是“字符密集、边缘纹理丰富”,灰度图足够表达这种纹理,而彩色信息在强光、阴影下极易漂移。蓝底白字车牌虽然可以用 HSV 颜色空间提取蓝色掩膜,但蓝色阈值对傍晚、路灯和车身蓝色漆非常敏感,灰度路线更稳。

% 读取图像并转灰度 img = imread('car_scene.jpg'); if size(img, 3) == 3 gray = rgb2gray(img); else gray = img; end % 对比度拉伸,减少光照波动 gray = imadjust(gray, stretchlim(gray, 0.01), [0 1]); % 中值滤波去噪,窗口不宜过大 gray = medfilt2(gray, [3 3]);

这段代码里 stretchlim(gray, 0.01) 取图像灰度直方图的 1% 和 99% 分位点做线性拉伸,效果是把偏暗或偏亮的画面拉回正常范围。中值滤波窗口选 3x3 而不是 5x5,是为了保留字符边缘的锐度,窗口越大边缘越糊,后续边缘检测反而吃亏。实测在逆光场景下,这步能明显提升车牌区域和车漆之间的对比度。如果你的输入是 ROI 已裁好的车牌图,可以跳过 medfilt2,直接进二值化。

2.2 边缘检测算子选择:Sobel 稳还是 Canny 好

预处理之后的核心操作是边缘检测。MATLAB 的 edge 函数内置多种算子,车牌识别场景里我一般优先用 Sobel,而不是很多人默认的 Canny。原因是 Canny 的双阈值会把字符内部细碎边缘也连起来,导致后续形态学闭运算把整个车牌糊成一块白斑;Sobel 只响应梯度较大的位置,对字符笔画这种“粗边缘”更友好。Canny 的优势在弱边缘检测,但车牌字符本身对比度高,不需要这种优势。

% Sobel 边缘检测,阈值取经验值 edges = edge(gray, 'sobel', 0.08); % 闭运算把字符边缘连成连通域 se = strel('rectangle', [15, 15]); closeImg = imclose(edges, se); % 填充内部空洞,去掉字符笔画之间的缝隙 closeImg = imfill(closeImg, 'holes');

edge(gray, 'sobel', 0.08) 的第三个参数是梯度阈值,低于该值的像素不算边缘。0.08 是从 800x600 左右的实拍图里调出来的,图像分辨率越高、光照越强,阈值可以往上抬到 0.1;夜间图建议降到 0.05。结构元素 15x15 是闭运算的核心参数,它决定“多近的边缘会被连成一块”。车牌字符高度在图中约占 20 到 30 像素时,15x15 能刚好把整行字符连成长条,又不会把旁边车身缝隙也并进来。这个参数没有标准答案,需要按你的图像分辨率等比缩放。

2.3 预处理参数速查:光照不均、噪声和分辨率怎么配

环节推荐参数作用失败表现
灰度拉伸stretchlim 0.01压住过曝和过暗低对比度→定位框偏大
中值滤波3x3去椒盐噪声窗口过大→字符边缘断开
Sobel 阈值0.05 ~ 0.1控制边缘密度过低→噪声边缘爆炸
闭运算结构元15x15 起调连通字符区域过小→车牌断成几块
空洞填充默认补字符间隙不填→连通域面积偏小

这些参数之间是联动的。我见过不少人把 Sobel 阈值调到 0.2 想压噪声,结果车牌边缘也一起被滤掉,后面 regionprops 根本找不到候选框。正确顺序是先固定分辨率,再调 Sobel 阈值,最后动结构元素尺寸。每次只改一个变量,否则定位翻车时你分不清是哪一步导致的。预处理这块最忌讳把 MATLAB 当黑匣子,每步都输出 imshow 看一眼中间结果,能省下大量排错时间。

3. 车牌定位的实现细节:边缘密度、形态学闭运算与连通域三阈值

3.1 车牌定位的三条路:边缘密度、颜色掩膜和深度学习检测

车牌定位有主流的三类做法。第一类是基于边缘密度加形态学筛选,速度快、依赖少,是 MATLAB 车牌识别系统里最常见的方案,也是本方案的主角。第二类是基于 HSV 颜色空间提取蓝色掩膜,再利用蓝色区域的外接矩形定位。它对蓝色车漆和夜晚路灯非常不友好,唯一优点是能直接排除红车干扰。第三类是用深度学习目标检测,比如在较新的 MATLAB 版本里通过 Deep Learning Toolbox 调 YOLO 系列模型,精度高但需要标注数据和 GPU,课程设计里性价比不高。

做传统方案时,我一般用边缘密度作为主筛条件。车牌区域在二值边缘图中表现为“横向纹理密集”的长条,而车漆、玻璃、路面的边缘要么太稀疏,要么方向杂乱。闭运算之后,车牌的字符和边框会连成一个或两个大块连通域,这是后续筛选的基础。需要说明的是,闭运算的矩形结构元方向要水平,尺寸要和车牌在画面中的比例匹配,这部分逃不掉人工调参,属于玄学之外的工程经验。

3.2 用 regionprops 筛候选车牌:宽高比、面积占比与填充率

形态学处理之后,画面里剩下的连通域可能还有十几个。此时用 regionprops 提取每个区域的几何属性,再用三组阈值做粗筛。三组阈值分别是宽高比、面积和填充率。蓝底白字车牌的宽高比一般在 2.5 到 4.5 之间,面积占整图的比例随拍摄距离变化,但不会低于某个下限,填充率反映的是“区域内边缘像素的密集程度”,车牌区域因为字符密布,填充率明显高于车身边缘。

stats = regionprops(closeImg, 'BoundingBox', 'Area', 'FilledArea'); candidates = []; for k = 1:length(stats) w = stats(k).BoundingBox(3); h = stats(k).BoundingBox(4); ratio = w / h; fillRatio = stats(k).FilledArea / stats(k).Area; % 车牌候选框常见约束 if ratio > 2.5 && ratio < 4.5 && fillRatio > 0.3 candidates(end+1, :) = stats(k).BoundingBox; %#ok<SAGROW> end end % 取面积最大的候选框 if ~isempty(candidates) [~, idx] = max(candidates(:,3) .* candidates(:,4)); plateBox = candidates(idx, :); end

区域填充率 fillRatio 是 FilledArea 和 Area 的比值,它比单纯用面积稳定。因为面积受拍摄距离影响大,而填充率描述的是这个连通域“内部实不实”。字符密集的车牌填充率通常在 0.3 以上,而一段孤立的保险杠边缘填充率只有 0.1 左右。最后取面积最大候选框的操作隐含了一个假设:车牌在画面里不会是占比最小的那个区域。如果你的测试图里车牌离得远,面积阈值就要下调。

3.3 车牌倾斜校正:用霍夫变换求主角度

定位得到的矩形框往往带一点倾斜角,不校正直接分割会导致字符边界切歪。常见做法是用霍夫变换检测矩形里的长直线,取最长的直线角度作为车牌倾斜角,再用 imrotate 做旋转。注意霍夫变换的输入应该用原始灰度图的边缘图,而不是形态学处理之后的连通域图,否则长直线会被闭运算吞掉。

% 从原图裁出车牌区域 plateGray = imcrop(gray, plateBox); % 重新对车牌区域做边缘检测 plateEdge = edge(plateGray, 'sobel', 0.08); % 霍夫变换检测直线 [H, theta, rho] = hough(plateEdge); peaks = houghpeaks(H, 5, 'Threshold', ceil(0.3 * max(H(:)))); lines = houghlines(plateEdge, theta, rho, peaks, 'FillGap', 20, 'MinLength', 40); % 取最长的直线,用其角度做旋转校正 if ~isempty(lines) angles = [lines.angle]; angle = mean(angles); plateGray = imrotate(plateGray, angle, 'bilinear', 'crop'); end

houghpeaks 的 Threshold 设为 max(H) 的 0.3 倍,可以滤掉短直线干扰。FillGap 20 表示同一直线上间距小于 20 像素的断点会被连接,MinLength 40 表示小于 40 像素的线段不参与统计。这段参数针对 800x600 图。车牌倾斜超过 10 度时,imrotate 的 crop 选项会裁掉边缘字符,此时建议改成 loose 再手动定位,或者干脆在分割前对每行字符单独做水平校正。

4. 字符分割与模板匹配识别:投影法切字和归一化相关打分

4.1 字符分割:垂直投影法的谷底判定与峰值阈值

车牌区域校正之后,下一步是把 7 个字符逐个切出来。最常见的做法是投影法:对二值车牌图做列求和,字符列因为白色像素多形成峰值,字符间隔处形成谷底,通过谷底位置切分字符。但这一步最容易踩坑的是二值化方式。直接 imbinarize 用全局阈值,在光照不均时会丢掉字符,所以更可靠的输入是自适应二值化。

% 自适应二值化,蓝底白字选择 dark 前景 bw = imbinarize(plateGray, 'adaptive', ... 'ForegroundPolarity', 'dark', 'Sensitivity', 0.5); % 去除上下边框干扰:只保留中间 80% 行 bw = bw(round(end*0.1):round(end*0.9), :); % 垂直投影,统计每列前景像素数 colProj = sum(~bw, 1); % 谷底判定:低于峰值 10% 的列视为字符间隔 threshold = max(colProj) * 0.1; validCol = colProj > threshold; startIdx = find(diff([0, validCol]) == 1); endIdx = find(diff([validCol, 0]) == -1);

ForegroundPolarity 设为 dark,是因为蓝底白字里字符是亮前景,但经过取反后字符像素在 ~bw 中表现为 1,所以投影前要对 bw 取反。Sensitivity 0.5 是自适应二值化的敏感度,调大会保留更多细节,调小则抗噪更强。字符断裂时优先提高 Sensitivity,字符粘连时降低它。切出来的每个区间还要用字符宽度做过滤,比如宽度小于中位数一半的区间直接丢弃,通常是铆钉或噪点。

4.2 模板匹配识别:归一化相关与候选排序

字符切片之后,常见做法是归一化尺寸到固定大小,然后和模板库逐张计算归一化相关系数。MATLAB 的 corr2 直接算两张图的皮尔逊相关系数,相比绝对差值和,它对照明变化不敏感,适合字符内有轻微阴影的场景。每个字符的输出结果是所有模板里的最高分,通常还会保留第二高分用于后续调试。

% 模板库 templates 为 cell 数组,charList 为对应字符 normSize = [40, 20]; scores = zeros(1, length(templates)); % 对每个字符切片归一化尺寸 charResized = imresize(segChar, normSize); for t = 1:length(templates) tmpl = imresize(templates{t}, normSize); scores(t) = corr2(double(charResized), double(tmpl)); end [sortedScores, idx] = sort(scores, 'descend'); bestChar = charList{idx(1)}; secondChar = charList{idx(2)}; % 当第一第二候选分差太小时,标记为低置信度 if sortedScores(1) - sortedScores(2) < 0.05 fprintf('低置信度字符: %s / %s\n', bestChar, secondChar); end

这里的关键参数是置信度门限 0.05。分差小于这个值时说明两个模板都像,后续宁可报警让人工复核,也不要硬给一个结果。模板尺寸统一到 40x20 是经验值,切出来的字符高度大约 30 像素,上采样到 40 并不会增加信息,只是让所有模板和切片处在同一尺度。注意 imresize 默认双线性插值即可,不要用 nearest,否则字符边缘会出现锯齿导致匹配分数波动。

4.3 模板库怎么来:用字体渲染生成训练样本

模板匹配的效果上限取决于模板库质量。拍照采集真实车牌字符样本是最准的路径,但需要几百张图才能覆盖各省简称、字母数字的粗细变化。更高效的常见做法是用字体渲染生成一套基础模板:用 insertText 把字符画到背景上,或直接通过 figure 绘制后用 getframe 截取。汉字字体用中文字体,字母数字用 Arial 加粗,这两类字体特征与实际车牌接近。

% 用 insertText 生成简单模板 charSet = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZ京津沪渝冀豫云辽黑湘皖新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼'; templateList = {}; for i = 1:length(charSet) c = charSet(i); img = insertText(zeros(60, 120, 'uint8'), [10, 10], c, ... 'FontSize', 40, 'TextColor', 'white', 'BoxOpacity', 0); tmpl = rgb2gray(img); tmpl = tmpl > 128; templateList{end+1} = tmpl; %#ok<SAGROW> end

生成模板时建议有意做几个变体:加粗版、细体版、轻微模糊版。实际拍到的车牌字符经过摄像头采样后会有一定模糊,模板太锐利反而匹配不上。注意车牌字符里没有字母 I 和 O,避免混淆 1 和 0。模板库大小为每个字符 2 到 3 个变体就够,堆太多变体反而会让低分模板抢到最高分。

5. MATLAB车牌识别避坑手册:从定位失败到字符断裂的 6 个高频问题

5.1 定位翻车:蓝色车漆、夜间光照和小目标

现象:形态学处理之后,蓝色车漆和车窗边缘形成一个面积很大的连通域,被误判成车牌,真正的车牌反而没进候选框。

原因:蓝色车身在灰度图里和蓝底车牌灰度接近,闭运算把车身边缘也连成了长条,叠加填充率约束也无法区分。

解决:在筛选条件里补充颜色先验,用 HSV 提取蓝色掩膜,只保留同时满足“蓝色像素占比高”和“宽高比合理”的连通域。实际操作是在灰度图定位基础上,对候选框区域计算蓝色像素比例,低于 5% 的直接丢弃。这个办法能同时压制蓝色货车、绿色树木等干扰。

现象:夜间照片里车牌区域过暗,Sobel 边缘图里车牌字符几乎不可见,定位框完全缺失。

原因:夜间车牌在车灯照射下容易过曝,或车牌本身处于阴影里,灰度拉伸无法恢复字符对比度。

解决:针对暗图单独走颜色定位分支,在 HSV 空间用较低的 V 阈值提取蓝色区域,再对候选区做局部直方图均衡化。stretchlim 对全局图处理不了这种局部过暗,需要 imlocalbrighten 或 adapthisteq 辅助。

现象:车牌在画面中占比小于 1%,闭运算后车牌区域被当成噪声直接抹掉。

原因:结构元素 15x15 是按近距离图调的,远距离车牌字符间距小,但整个车牌面积也小,闭运算后无法形成有效连通域。

解决:把输入图按照最长边缩放到固定尺度再做流程,比如最长边 1000 像素。这样远近图都能落到同一参数区间,比反复调结构元素尺寸省事。

5.2 分割断裂:铆钉、边框和投影谷底误判

现象:垂直投影切分时,字符被切出多段,比如“京A12345”切成了 10 个碎片;或铆钉噪声让第一个字符被拦腰切开。

原因:车牌上下边框没有完全清除,二值化后边框和字符连成一片,投影谷底被边框抬高,字符间隔无法形成明显谷底。

解决:先做水平投影定位字符行的上下边界,把上下边框精确裁掉,再做垂直投影。水平投影找到前景像素密度最高的一段连续行,这段就是字符行,其余行全部丢弃。

现象:字符“1”和“7”之间的间隔被投影谷底吞掉,两个字符粘成一个宽块。

原因:自适应二值化的 Sensitivity 调太高,字符边缘扩张,间隔处的前景像素没降到阈值以下。

解决:把 Sensitivity 从 0.5 下调到 0.3,然后观察二值化结果里字符间是否出现黑色缝隙。如果还粘连,就改用固定阈值 imbinarize(bw, graythresh(plateGray)) 做对照实验。

现象:投影切出的字符块宽窄不一,有的字符顶部被切掉一截。

原因:车牌倾斜校正角度算偏了,字符行仍有少量旋转,投影时字符顶部和底部不在同一列范围内。

解决:在切割前对二值图逐行动态校正,常见做法是用形态学提取字符行的上下轮廓,拟合两条包络线,把字符按包络线逐段拉平。这个步骤代码量不大,但对识别率提升非常明显。

5.3 识别混淆:模板风格不一致和低置信度硬选

现象:模板库里汉字用常规宋体生成,实际车牌汉字是特殊字体,匹配分数普遍偏低,甚至误识别成“津”这种字形相近的字。

原因:模板风格和真实字符差异大,corr2 对笔画粗细、起笔收笔的形状差异非常敏感。

解决:换用真实车牌字体生成模板,或者找现场图裁剪出字符样本补充进模板库。没有现成字体时,对基础模板做形态学腐蚀和膨胀各生成一套变体,也能缓解风格不匹配。

现象:某帧图识别结果在几个字符间跳变,比如“5”和“6”分差不稳,单帧识别率不高。

原因:单帧图像里字符本身模糊,模板匹配的分数差异被噪声放大。

解决:多帧投票。连续拍 3 到 5 帧,每个字符位置取出现次数最多的结果。这个办法能直接把识别率从 90% 推到 97% 以上,代价仅仅是延迟增加几百毫秒。

现象:调试时发现算法在 A 组图上表现好,换到 B 组图定位框乱跳。

原因:没有固定一套验证集,参数是针对记忆中的图片调的,属于典型的过拟合调试。

解决:把测试图按白天、夜间、逆光、倾斜四类分别建文件夹,每次调参跑全量测试集,记录定位成功率和识别准确率,用数字说话。

6. 把识别率再往上推:多帧投票、混淆矩阵与验证集划分

识别率卡在某个位置不上不下时,继续调 Sobel 阈值往往是浪费时间,更有效的是从验证和集成两个方向入手。多帧投票是最容易实施的技巧,它的思路是单帧识别会有随机性,但同一辆车的连续几帧字符不会突变,逐位取众数能把偶发抖动压掉。

% voteMatrix: 帧数 x 字符数 的字符矩阵 finalResult = ''; for col = 1:size(voteMatrix, 2) chars = voteMatrix(:, col); finalResult(end+1) = mode(chars); %#ok<AGROW> end

mode 是众数函数,少数服从多数。这个技巧要求车辆在画面里停留至少 3 帧,对停车场闸机这类场景天然适用,对单张抓拍则无效。

另一个高性价比手段是人工检查混淆矩阵。把自己测试集上识别错的字符整理成表,通常会发现错误集中在少数几对字符之间。

混淆对常见原因应对措施
0 和 O模板库里 O 太少去掉 O,车牌不含 O
1 和 I模板风格不统一统一去掉 I
7 和 T分割时顶部缺失修正倾斜校正
8 和 B下边缘模糊提高二值化敏感度

最后是验证集划分。我早期做这项大作业时习惯把 50 张图来回调参,最后在自认为不错的准确率上沾沾自喜,换一批现场图立刻现原形。后来养成的习惯是预留 20% 的图完全不参与调参,只在最终评估时跑一次。这个习惯帮我避开了大量自我感觉良好的翻车现场。整体思路就是让参数调整有反馈闭环,让识别结果有置信度出口,剩余的交给数据量说话。希望帮到你。

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

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

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

立即咨询