MATLAB车牌识别课程设计全流程:定位、分割、识别与避坑参数表

发布时间:2026/11/2 8:11:48
MATLAB车牌识别课程设计全流程:定位、分割、识别与避坑参数表

简介:《基于Matlab车牌识别课程设计报告(附源代码)》面向电子信息、自动化及计算机相关专业学生与课程设计指导教师,围绕车牌定位、字符分割与字符识别三大核心环节,提供一份可参考的完整设计文档。压缩包共1个docx文件,约1.37MB,正文含设计目的、总体方案、详细实现步骤、结果分析、总结体会与参考文献,并附Matlab源代码及关键处理片段。内容具体展开图像灰度化、背景估计与增强、二值化阈值选取、基于垂直投影的字符切分,以及模板匹配和人工神经网络两类识别思路,同时涉及不同光照、车牌质量与拍摄速度下的测试和识别率优化讨论。已有116人学习,适合需要完成课程设计、快速搭建Matlab实验流程或对照报告整理算法与代码的读者。

1. 一块车牌跑通全流程:这套 MATLAB 课程设计报告值不值得照着做

课程设计季一到,问得最多的就是那套 MATLAB 车牌识别代码现在还能不能跑。我手上这份课程设计报告加源代码,走的是最朴素也最耐查的路线:灰度化、开运算背景建模、背景差分、阈值二值化、Canny 边缘、形态学开闭运算、长宽比筛候选框,再靠投影切字符、模板库做匹配。答辩最容易追问的定位、分割、识别三段,它都留了中间图,能讲清每一步为什么这么干,而不是只交一个能跑的结果。适合正在做图像处理课程设计、想照着复现一遍流程的人,也适合带课的老师拿去当示例改。代码是旧版 MATLAB 写法,直接丢进新版本会翻车,后面把版本适配和参数标定的坑一并讲透。

2. 车牌定位链路拆解:从灰度化到候选框筛出唯一区域

这一章把定位段拆成四步:灰度与背景估计、背景差分与二值化、边缘与形态学、连通域筛选。每一步都给了可运行的片段,代码按 R2016b 之后的语法整理,旧函数替换的地方单独标注。跑通这四步,后面字符分割才有稳定的输入,否则投影图全是毛刺,再怎么调参数也是白费。

2.1 灰度化与形态学背景估计:strel('disk',13)到底在干什么

自然场景里车牌的最大干扰不是噪声,是光照不均。车顶反光、树荫、地库顶灯,都会让同一块车牌上半部分和下半部分的灰度差出几十级。这份代码没有上来就全局阈值,而是先用开运算估一个“背景照明图”,再把它减掉。开运算的本质是先腐蚀后膨胀,结构元尺寸大于字符笔画宽度时,笔画被腐蚀没了,剩下的就是缓慢变化的背景。这一步在照明均匀的图上增益不明显,但在逆光或局部阴影下,差分后的字符对比度能拉出一大截,后面二值化才不至于把整块车牌吞掉。

%% 2.1 灰度化 + 形态学背景估计 + 背景差分 clear; close all; clc; imgPath = fullfile(pwd, 'plate_samples', '3.jpg'); % 相对路径,换机器不用改 Scolor = imread(imgPath); if size(Scolor, 3) == 3 Sgray = rgb2gray(Scolor); % 彩色转灰度,加快后续运算 else Sgray = Scolor; % 已经是灰度图就直接用 end seBack = strel('disk', 13); % 背景估计结构元,半径略大于字符笔画宽度 Bgray = imopen(Sgray, seBack); % 开运算:抹掉细笔画,保留照明背景 Egray = imsubtract(Sgray, Bgray); % 背景差分:字符被拉出来,噪声同时被放大 figure; subplot(1,3,1); imshow(Sgray); title('原始灰度'); subplot(1,3,2); imshow(Bgray); title('背景估计'); subplot(1,3,3); imshow(Egray, []); title('差分增强');

strel('disk',13)里的 13 是半径,不是直径。它对标定最敏感:原图缩到 640 宽还沿用 13,背景估计会把车牌边框一起抹平,差分图里车牌区域反而变暗;原图放到 1920 宽还用 13,笔画残留,背景图上能看到一排排字符的残影。我一般按图像宽度取,800 宽左右的图用 8 到 13 比较稳,先看背景图,笔画干净了再往下走。imsubtract遇到负值会截断到 0,所以差分结果整体偏暗是正常的,别急着调亮度。

2.2 阈值落在动态范围的上三分之一:一行level的连锁反应

fmax1 = double(max(Egray(:))); % 用 (:) 展平,别写成 max(max()) fmin1 = double(min(Egray(:))); level = (fmax1 - (fmax1 - fmin1)/3) / 255; % 阈值放在动态范围上 1/3 处 bw = im2bw(Egray, level); % im2bw 的阈值参数是 0~1

这行level等价于把阈值放在离最大值三分之一动态范围的位置,再归一化到 0 到 1 给im2bw用。偏保守,倾向多留白像素,好处是字符不容易断,坏处是白车身、地面高光、车道线都会被一起留下。所以这一步不是“调准”,是“调到一个后面能靠形态学收拾掉的程度”。如果二值图里车牌字符已经粘连成一块,把系数从 1/3 改到 1/4 试,阈值往上抬一点;如果字符大面积断裂,改到 1/2。别小看这零点几的差别,一张逆光图能差出整块车牌的连通性。

注意:im2bw的第二个参数必须落在 0 到 1 之间。有人把level再乘 255 传进去,MATLAB 不报错但结果是全黑或全白,排查半天以为是图像问题。

2.3 Canny 边缘与各向异性结构元:横向连条、纵向剥高

grd = edge(bw, 'canny'); % 二值图上做 Canny,双阈值自动估计 bg1 = imclose(grd, strel('rectangle', [5 19])); % 闭运算:横向把一排字符连成条 bg3 = imopen (bg1, strel('rectangle', [5 19])); % 开运算:擦掉细碎噪点 bg2 = imopen (bg3, strel('rectangle', [19 1])); % 纵向开运算:车牌子区域太高就剥掉 figure; subplot(2,2,1); imshow(grd); title('Canny 边缘'); subplot(2,2,2); imshow(bg1); title('闭运算 [5 19]'); subplot(2,2,3); imshow(bg3); title('开运算 [5 19]'); subplot(2,2,4); imshow(bg2); title('纵向开运算 [19 1]');

结构元尺寸是这套流程里第二个敏感参数。[5 19]的意思是竖向 5 像素、横向 19 像素,为什么要各向异性?车牌是扁的,字符之间的间隙是竖向细缝,横向是长条,用一个方形结构元要么连不上条,要么把整幅图连成一片。第一次闭运算把字符横向连成候选条,第二次开运算把粘连的小点擦掉,最后一次[19 1]用竖向长条把“高度超标”的连通域削掉——车牌高的宽比摆在那,竖直方向太高的区域基本是护栏、立柱或者保险杠。

这里的尺寸和分辨率强绑定。800 宽图上[5 19]表现不错,1920 宽图上就得放大到[12 46]左右。判断标准看中间图:闭运算后车牌区域应该是一条完整的、边缘略毛的长条;如果字符还是一颗颗分开的,横向尺寸不够;如果整块图糊成一团,横向尺寸过大。

2.4bwlabel与长宽比筛选:把候选压到唯一一个

[L, num] = bwlabel(bg2, 8); % 8 连通,斜向粘连也能标到一起 stats = regionprops(L, 'Area', 'BoundingBox'); % 旧报告的 imfeature 已废弃 if isempty(stats) error('没有找到任何连通域,回到 2.2 检查二值化'); end area = [stats.Area]; bbox = cat(1, stats.BoundingBox); % [x y w h] 左上角坐标加宽高 ratio = bbox(:,3) ./ bbox(:,4); % 宽高比 keep = find(area > 400 & ratio > 2.2 & ratio < 4.5); if isempty(keep) error('长宽比筛完没有候选,放宽 ratio 区间或降低面积下限'); end [~, idx] = max(area(keep)); % 多个候选时取面积最大的 plateBox = bbox(keep(idx), :); plateImg = imcrop(Scolor, plateBox); % 从原彩图裁,保留颜色信息

中国单行车牌的字符区域宽高比大致在 3 比 1 附近,含边框会比这个宽一点。区间取 2.2 到 4.5 是留了倾斜和透视的余量,太窄会漏掉斜拍的牌,太宽会把保险杠上的长条一起收进来。面积下限 400 是 800 宽图的经验值,分辨率翻倍后要乘四。取面积最大的候选是个偷懒但有效的策略,多车牌场景下会失效,那种情况得按位置排序再逐个识别。

到这一步,plateImg就是后面所有分割和识别的输入。建议每张中间图都imwrite存盘,答辩时拼成一张流程对比图,比讲十句原理都直观。

3. 字符分割:水平投影定行、垂直投影切字的两轮校正

定位只解决“车牌在哪”,分割解决“第几个字在哪”。这一段的输入是定位后的车牌二值子图,输出是一组统一尺寸的字符小图。报告里的顺序是:先做倾斜校正,再用水平投影去掉上下边框锁定字符行,最后用垂直投影切出七个字符并归一化到 22 行 14 列。顺序不能乱,倾斜没校正就直接投影,峰谷会糊成一片,阈值怎么调都切不准。

3.1 倾斜校正:小角度扫描比直接拟合更稳

%% 3.1 角度粗估:小角度扫描,取水平投影方差最大的角度 bestAngle = 0; bestScore = -inf; for a = -8:1:8 tmp = imrotate(sbw0, a, 'bilinear', 'crop'); % sbw0 为定位后的车牌二值子图 hp = sum(tmp, 2); % 每行白像素数 score = var(hp); % 投影越尖锐,说明行越对齐 if score > bestScore bestScore = score; bestAngle = a; end end sbw = imrotate(sbw0, bestAngle, 'bilinear', 'crop');

报告里用线性拟合求上下边缘的斜率再算角度,思路没错,但对边缘提取的质量很敏感——边缘断成几段时拟合出来的斜率能偏好几度。我一般先跑一遍 -8 到 8 度的一度步长扫描,用水平投影的方差当评分,方差最大的角度就是让字符行最“平”的角度。这招在倾斜小于 15 度时很稳,代价是九到十七次旋转,单张图耗时可以忽略。扫描出的整数角度再交给拟合做精修,精度和鲁棒性都能兼顾。

imrotate的正角度是逆时针,'crop'参数保持输出尺寸不变,边缘会补黑。如果校正后车牌反而更歪,多半是拟合时取点顺序反了导致角度符号相反,先用扫描法兜底再回头查拟合。

3.2 水平投影去上下边框:把字符行夹出来

hp = sum(sbw, 2); % 水平投影:每行白像素数 hp = hp / max(hp + eps); % 归一化,消除分辨率差异 hpS = movmean(hp, 3); % 三行滑动平均,压掉单行噪声 th = mean(hpS) * 0.6; % 经验阈值,字符行显著高于边框行 rowFlag = hpS > th; d = diff([0; rowFlag; 0]); % 找连续段的起止 rowStart = find(d == 1); rowEnd = find(d == -1) - 1; [~, mainIdx] = max(rowEnd - rowStart); % 最长的连续段就是字符主体 rowTop = rowStart(mainIdx); rowBot = rowEnd(mainIdx); sbwChar = sbw(rowTop:rowBot, :);

车牌二值子图的上边和下边各有一条水平边框,投影上表现为两个窄峰,中间隔一段低谷;字符行的峰又宽又高,占车牌高度的六到八成。用“最长连续段”挑字符主体,比报告里“取最大峰再找相邻峰”更抗噪。阈值系数 0.6 是经验值,光照均匀的图 0.5 到 0.7 都能用,背景残留多的图要往上提。

这一步失败最典型的表现是:切出来的字符上下被削掉一截,或者带进一条黑边。前者是rowBot取小了,把阈值系数降到 0.5 再试;后者是rowTop取大了,检查一下闭运算的竖向尺寸是不是过大,把边框和字符连到了一起。

3.3 垂直投影切字符与 22×14 归一化

vp = sum(sbwChar, 1); % 垂直投影:每列白像素数 vp = vp / max(vp + eps); vS = movmean(vp, 3); isGap = vS < 0.15; % 谷区判定,0.15 是经验阈值 d = diff([0, isGap, 0]); gapStart = find(d == 1); gapEnd = find(d == -1) - 1; % 两个谷区之间的高投影段就是一个字符 charStart = [1, gapEnd(1:end-1) + 1]; charEnd = [gapStart(2:end) - 1, size(sbwChar, 2)]; segs = cell(1, numel(charStart)); for k = 1:numel(charStart) cleft = max(1, charStart(k)); cright = min(size(sbwChar, 2), charEnd(k)); seg = sbwChar(:, cleft:cright); segs{k} = imresize(seg, [22 14]); % 统一到 22 行 14 列,供模板匹配 end

垂直投影切字符有两个先天问题。第一,汉字和后面的字母之间间隔大,字母之间间隔小,用同一个谷区阈值会导致汉字被切成两半或者字母被合并。第二,车牌上的铆钉、污渍会在投影里造出假谷,多切出一个碎片。实际处理时加两条约束:切出来的字符宽度和平均宽度的偏差不超过 40%,总字符数不满足 7 个就按宽度排序取前七个。这不是万能的,但能挡掉大部分低级错误。

归一化到 22×14 是报告里的设定,我建议改成 32×16。22×14 一共才 308 个像素,0 和 8、G 和 6 的差异被压得几乎看不见,后面模板匹配误判大多出在这里。尺寸加一倍,匹配耗时会涨,但对一张图七个字符的量级完全感觉不到。

3.4 样本库落盘:命名规则决定后面的识别上限

outDir = fullfile(pwd, 'sam', 'trained'); if ~exist(outDir, 'dir') mkdir(outDir); end for k = 1:numel(segs) fname = fullfile(outDir, sprintf('char_%02d.jpg', k)); imwrite(segs{k}, fname, 'jpg'); end

报告里用image1.jpg到image7.jpg这种纯序号命名,识别时靠循环下标去对字符表,程序能跑,可一旦中间某一刀切歪,后面所有字符全部错位,而且很难查。我习惯在文件名里带标签,比如A_01.jpg、0_03.jpg,或者另存一个labels.mat存字符数组。多花十行代码,排查时能省一晚上。落盘还有个好处:样本库可以离线增广,旋转一两度、加一点高斯噪声,同一类字符扩出三到五份,模板匹配的容错会明显变好。

4. 字符识别两条路:模板匹配够快,神经网络够泛

分割之后是识别。报告里写了两条路:模板匹配和 BP 神经网络。前者是把分割出来的字符缩放后与字符库逐像素比对,取误差最小的当结果;后者是提取特征训练网络,或者直接把图像送进网络。对课程设计这个体量,模板匹配当天就能出结果,神经网络光样本采集就可能卡一周。但模板匹配的上限很低,光照、字体、污损一变就掉点,两条路的取舍要提前想清楚。

4.1 模板匹配:平均绝对误差加拒识阈值

%% 4.1 单字符模板匹配 % tmpl : N×(22*14) 矩阵,每行是一个展平的模板 % labels : 长度 N 的 cell,对应每个模板的字符 segVec = double(seg(:))'; % 1×308 行向量 err = mean(abs(tmpl - repmat(segVec, size(tmpl,1), 1)), 2); [minErr, idx] = min(err); if minErr > 0.32 predChar = '?'; % 拒识,宁可返回问号也不硬猜 else predChar = labels{idx}; end

误差度量用平均绝对误差而不是欧氏距离,原因是欧氏距离对个别离群像素过于敏感,车牌字符上的一个污点就能把距离拉偏一个数量级。0.32这个拒识阈值不是拍脑袋来的,拿一批测试图把正确识别的误差和错误识别的误差分别统计出来,中间会有一段明显的间隔,阈值放在间隔中点附近。没有拒识机制的模板匹配最坑的地方是“永远给你一个答案”,错的也给你,答辩演示时翻车就翻在这。

匹配前还有个容易漏的细节:分割出来的字符是 logical 类型,模板如果是 double 类型,abs运算前必须先统一,否则差值计算会出莫名其妙的结果。上面代码里两边都double了,别省这一步。

4.2 样本库怎么建才不拖后腿

字符集一共 65 类左右:31 个省简称汉字、24 个字母(去掉 I 和 O)、10 个数字。每类至少五份样本,就是三百多张图,手工一张张切不现实。可行的做法是拿一批已定位好的车牌,跑一遍批量分割自动落盘,再人工把切错的挑出来删掉,剩下的按标签改名。这个过程枯燥,但样本库的质量直接决定识别率的上限,报告里那句“数字库的建立很重要”是实话。

样本库的另一个坑是尺寸一致性。imresize到 22×14 之后,同一字符在不同光照下二值化出来的笔画粗细能差一两个像素,模板取平均会把笔画糊掉。我的做法是每类保留三份模板:正常光照、偏亮、偏暗,匹配时取三份里的最小误差。模板数量翻三倍,匹配耗时还是毫秒级,但跨光照的稳定性提升明显。

4.3 换成 BP 神经网络:特征维度和样本量的现实差距

报告里的方案是四个子网络分别识别汉字、字母、字母数字、数字,这种拆分思路在工程上是对的,能缩小每张网络的输出维度。但课程设计环境下真正卡人的不是网络结构,是样本量。308 维输入、40 个隐层节点、65 类输出,每类没有二三十个样本根本训不出稳定的权值,而手工采集三百个样本的工作量已经超过写代码本身。

如果确实想走这条路,建议把输入从原始像素换成投影特征:字符的水平和垂直投影各取 16 个 bin,加上宽高比和笔画密度,一共三十多维,样本需求量能降一个量级,训练也快。代价是特征工程要自己做,投影 bin 的划分方式得试几轮。对答辩来说,模板匹配能讲清楚误差度量和拒识逻辑,比硬套一个训练不收敛的网络更有说服力。

5. 避坑与排查:五个把识别率拉回及格线的现场记录

这一章的五个问题,是我照着报告复现时真实撞上的,每一个都能让流程在某个环节静默失败——不报错,但结果就是不对。按“现象、原因、解决”记下来,照着对一遍能省不少时间。

5.1 现象:imfeature报未定义函数

原因:imfeature在 R2013a 之后就被移除了,报告成文时间早于这个版本,代码本身没错,是版本不兼容。

解决:换成regionprops,注意返回的是结构体数组而不是矩阵,取字段要写[stats.Area]、cat(1, stats.BoundingBox)。另外regionprops的返回顺序和bwlabel的标签一一对应,中间如果做过删除连通域的操作,索引会错位,要么重新标记,要么保留原标签数组做映射。

5.2 现象:定位框把整个车头都框进来了

原因:闭运算结构元[5 19]是按某张特定分辨率的图调出来的,换一张宽度翻倍的图,19 像素的横向结构元只够连半个字符,候选区域碎成一片,面积最大的那块反而是保险杠。

解决:进流程前先统一缩放,把图imresize到宽度 800,所有结构元尺寸按这个基准调;或者按round(w/800)的比例动态缩放结构元。缩放会损失一点细节,但换来的是参数可复用,批量跑图时不用一张张改。

5.3 现象:倾斜校正后车牌歪得更厉害

原因:拟合直线的取点顺序和图像坐标系方向相反,算出来的角度符号是反的,imrotate正角度逆时针,结果越转越歪。

解决:别信拟合,先用 -8 到 8 度的一度步长扫描,取水平投影方差最大的角度,这个判据和符号无关。扫描出来的角度和拟合结果差两度以上,说明边缘点取错了,回去查边缘提取那一环。

5.4 现象:0 识别成 8,G 识别成 B

原因:归一化尺寸 22×14 太小,0 和 8 的内圈差异只剩几个像素,二值化后几乎一样;G 和 B 的右侧竖笔在缩放时被插值糊掉。报告里自己也记录了 G 误识为 B、K 误识为 A、0 误识为 8,这不是偶然。

解决:归一化尺寸提到 32×16,每类字符保留多份模板按最小误差匹配,再加拒识阈值兜底。如果这三招都上了还错,说明样本库里这一类字符的样本本身就偏少,去补样本,别在算法上继续折腾。

5.5 现象:imwrite报目录不存在,换台电脑就崩

原因:保存路径写死成F:\MATLAB\work\sam\image%d.jpg,换台电脑没有 F 盘,直接报错退出。

解决:全部换成fullfile(pwd, ...)拼相对路径,写之前用exist(outDir, 'dir')判断、mkdir补建。路径问题看起来低级,但它是“在我电脑上能跑”类事故的头号来源,交付前一定查一遍所有硬编码路径。

6. 从能跑到能交付:一套验收集和一张参数速查表

单张图跑通不算数,验收集至少要覆盖三种光照:正面顺光、逆光、夜间或地库。每类三到五张,分辨率统一到同一宽度,跑完把中间图和最终识别结果拼成一张对比图存盘。这么做有两个目的:答辩时说明算法在不同条件下的边界,自己排查时也能一眼看出是哪一步失效。参数速查表按 800 宽图给基准值,其他分辨率按比例缩放。

参数建议值(800 宽基准)失效表现调整方向
背景开运算半径8~13小了残笔画,大了吃边框看背景图,笔画抹净为止
二值化阈值位置动态范围上 1/3偏小白斑多,偏大字符断±0.05 微调
闭运算结构元[5 19]连不上条或糊成一片横向尺寸按字高比例
纵向开运算结构元[19 1]竖高干扰剥不掉按车牌高宽比推算
候选面积下限400 像素小框漏检按分辨率平方缩放
宽高比区间2.2~4.5窄了漏斜牌,宽了进保险杠单行车牌实测
字符归一化尺寸32×16 优先0/8、G/B 混淆不超过原尺寸一倍
拒识阈值平均绝对误差 0.32低了误识,高了全返问号按测试集统计间隔取中点

批量验证用一个循环把验收集跑一遍,结果打到命令行,比一张张点开看快得多:

%% 6.1 验收集批量跑,结果落表 imgList = {'front_01.jpg', 'backlight_02.jpg', 'garage_03.jpg'}; resCell = cell(numel(imgList), 2); for i = 1:numel(imgList) resCell{i,1} = imgList{i}; resCell{i,2} = runPlatePipeline(fullfile(pwd, 'plate_samples', imgList{i})); fprintf('%-18s -> %s\n', resCell{i,1}, resCell{i,2}); end

runPlatePipeline是把前面定位、分割、识别各步封成的函数,返回七位字符串。封装的时候记得给每一步留一个debug开关,打开就把中间图imwrite到debug/目录下,关掉就只返回结果。验收集里只要有一张识别不对,打开开关重跑这一张,中间图按顺序排开,问题卡在哪一步一目了然。

从那以后我每次交付图像处理类的课程设计代码,都强制先跑一遍三光照验收集,把中间图拼成一张长图塞进附录,再对着参数表逐项核一遍硬编码。血泪经验是:算法本身很少翻车,翻车的都是路径、版本、分辨率这类看着不起眼的细节。把这份报告当骨架,先把中间图跑出来,再按参数表一项项压,能省掉不少试错。希望帮到你。

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