简介:面向Matlab课程设计与车牌识别入门实践的课程设计报告,适合信息类、自动化及相关专业学生参考选题、撰写报告与复现实验。文档围绕车辆检测、图像采集、牌照识别三部分展开,串起图像灰度化、背景增强、二值化、车牌定位、垂直投影字符分割、模板匹配与神经网络字符识别等流程,并给出总体方案、详细设计步骤、结果分析、测试验证、技术挑战与优化思路,附源代码便于对照实现。资源包共1个docx文件,约1.37MB,结构按设计目的、原理、步骤、结果、总结、体会和参考文献组织,可直接借鉴目录框架与实验记录方式。已有116人学习,适合需要完成课程设计、整理答辩材料或补充Matlab图像处理实践案例的读者。
1. 从一块倾斜 8 度的车牌说起:这套课程设计真正要交付的东西
做 Matlab 车牌识别课程设计的人,十个里有八个卡在同一个位置:代码跑通了,命令行也蹦出了一个车牌号,可导师随口一句「disk(13) 的 13 怎么来的」「Canny 之后为什么还要再开一次运算」,就答不上来。这套方案要解决的不是「能不能跑」,而是从需求到验收整条链路能不能讲清楚——图像预处理怎么定参数、车牌定位为什么用长宽比而不是颜色、字符分割的投影峰谷怎么读、识别错了往哪里查。适合正在赶课程设计报告、做毕业设计,或者要交一份完整图像处理方案的人,照着这个结构写,报告的逻辑链是闭合的,答辩时每一步都有出处。
2. 总体方案设计:三个环节与九步处理链的职责划分
一套完整的牌照识别系统在硬件上包含车辆检测、图像采集、牌照识别三段:触发设备检测到车辆到达,摄像与照明设备配合采集当前画面,处理机上的软件负责把车牌号码算出来。这三段里,课程设计能交付、也必须交付的只有最后一段的软件部分。很多报告在这一步就跑偏了,花两页篇幅写地感线圈和补光灯选型,结果被问「你做的到底是哪一部分」时反而说不清。先把边界划出来,后面的模块划分才有意义。
2.1 系统边界:只交付「牌照识别」这一段软件
车辆检测和图像采集在课程设计里通常是给定前提——输入就是一张已经拍好的静态图片,或者一段已经录好的视频。软件部分要做的事情可以拆成三件:从整幅图像里把车牌所在的矩形区域框出来,把框里的字符一个一个切开,再把切开的字符翻译成具体的字母数字。这三件事对应定位、分割、识别三个环节,环节之间是串行的,前一个环节的输出质量直接决定后一个环节的上限。
把边界说清楚还有个好处:报告里的「设计内容及要求」和「详细设计步骤」能对得上。要求里写的是「定位出牌照位置,再将牌照中的字符分割出来进行识别,然后组成牌照号码输出」,那详细设计就应该是这三段的展开,而不是从摄像头型号开始讲起。我一般会建议在报告第二节开头用一段话把输入输出写死:输入是一张 JPG 静态图,输出是一个字符串加一张标注了定位框的可视化图,中间过程全部可复现。
2.2 九步处理链:每一步的输入、输出和失败表现
从一张彩色原图到一个车牌号,整条链路上真正干活的是九个步骤。把这九步列成表,写在报告里,答辩时被问到「你的流程是什么」直接翻到这一页,比口头描述清楚得多。表里的「失败表现」一列尤其关键,它是后面排查章节的索引。
| 步骤 | 输入 | 输出 | 关键函数 | 失败时的典型表现 |
|---|---|---|---|---|
| 1 读图 | JPG 文件 | 三维彩色矩阵 | imread | 路径错、图太大导致内存紧张 |
| 2 灰度化 | 彩色矩阵 | 二维灰度矩阵 | rgb2gray | 蓝底车牌对比度不足 |
| 3 背景估计 | 灰度图 | 平滑背景图 | strel / imopen | 半径过小,背景里还留着车牌 |
| 4 增强 | 灰度图、背景图 | 顶帽图 | imsubtract | 全图接近全黑,看不到字符 |
| 5 二值化 | 增强图 | 二值图 | im2bw | 全白或全黑,阈值越界 |
| 6 边缘检测 | 二值图 | 边缘图 | edge(...,'canny') | 边缘太碎,连不成块 |
| 7 形态学滤波 | 边缘图 | 连通块图 | imclose / imopen | 车牌断成两半或和背景粘连 |
| 8 区域筛选 | 连通块 | 车牌子图 | bwlabel / regionprops | 框到车灯、栅格或整幅图 |
| 9 校正分割识别 | 车牌子图 | 车牌号字符串 | fit / imrotate / imresize | 字符粘连、相似字误判 |
这张表还有个隐藏用途:报告里的「详细设计步骤」章节可以直接按行展开,每一行写一小节,结构天然是平的,不会出现某一节特别长、某一节只有两句话的情况。
2.3 技术选型:为什么是模板匹配而不是神经网络
字符识别这一段有两条路,一条是模板匹配,一条是神经网络。报告里两种都提到了,神经网络还细分了「先提特征再训练分类器」和「整图输入由网络自己提特征」两种做法。但课程设计最终落地的是模板匹配,理由要写明白,否则答辩时容易被追问「为什么不用深度学习」。
模板匹配的优势在可解释性和数据成本。整个识别过程就是「把待识别字符缩放到 22 行 14 列,和样本库里的每一张图逐像素相减,取误差最小的那张的名字」,中间没有任何黑匣子,误差是多少、第二名差多少,全都能打印出来。神经网络则要求每类字符有足够多的样本,训练轮数、学习率、隐层节点数都要调,调完了还得解释为什么这么调。两周的课程设计周期里,模板匹配能保证跑通,神经网络很容易停在「训练不收敛」上。
代价也要承认:模板匹配对字体、字号、倾斜、污损都敏感,样本库稍微不全就会误判。报告的结果分析里写得很实在——某张图把 G 认成了 B、K 认成了 A、0 认成了 8,这三对字符在低分辨率二值图里字形高度接近,是模板匹配的经典弱点。把这个弱点写进「设计体会」,比藏着不说要加分。
2.4 两周怎么排:把验收答辩倒推成四个节点
进度安排给的是第 19 周熟悉环境与选题分析、第 20 周编程实现与验收答辩。真按这个粒度执行,第二天就会乱。我一般会把它拆成四个可检查的节点:第一天把 MATLAB 环境和 Image Processing Toolbox 跑通,能读图能显示;第三天把预处理到二值化这一段调通,做到任意换一张图都能看到清晰的车牌候选区;第五天打通定位到分割,能切出七个字符子图并存盘;第七天接上模板库,输出完整车牌号并整理结果对比图。每个节点都有可见的产出物,卡住了也知道卡在哪一段。
参数最好一开始就集中管理,别散落在各个脚本里。用一个结构体把所有「魔数」收拢,答辩时被问到某个数字,能一句话说清它的物理含义和调整方向。
% cfg.m —— 把所有经验参数集中,避免散落在脚本各处 cfg.se_open_radius = 13; % 背景估计开运算圆盘半径,约为车牌字符高度的 1/3 cfg.close_rect = [5 19]; % 横向闭运算结构元,用于把断开的字符边缘连成块 cfg.open_rect1 = [5 19]; % 同尺寸开运算,抹掉孤立噪点 cfg.open_rect2 = [19 1]; % 纵向开运算,压制垂直方向的细长干扰 cfg.aspect_range = [2.6 4.0]; % 车牌长宽比容差区间,标准蓝牌约 3.14 cfg.area_ratio_min = 0.01; % 候选区域面积占全图比例下限 cfg.char_size = [22 14]; % 字符归一化尺寸,行×列结构元尺寸写成[行 列],[5 19]是一个 5 行 19 列的横向长条,[19 1]是一个 19 行 1 列的纵向长条,方向搞反了整个定位就废了,这是踩过才记得住的地方。
3. 图像预处理与车牌定位:顶帽变换、Canny 边缘与长宽比筛选
自然环境下拍到的车辆图像,背景复杂、光照不均,车牌区域在整个画面里可能只占百分之几。直接对原图做边缘检测,得到的是密密麻麻的纹理,车牌淹没在里面。预处理这一段的任务就是把「车牌字符比周围亮」这个先验变成可以计算的灰度差,让后续的二值化和边缘检测有的放矢。整条定位链路是:灰度化 → 开运算估背景 → 相减增强 → 阈值二值化 → Canny 边缘 → 形态学滤波 → 连通域筛选。
3.1 灰度化与背景估计:disk(13) 里的 13 是什么
彩色图转灰度是常规操作,三个通道加权成一个通道,存储和处理开销都降下来,而且车牌识别真正依赖的是亮度分布而不是颜色。真正的设计点在下一步:用一个大结构元对灰度图做开运算,得到一张「把车牌抹掉了」的背景图。
Scolor = imread('3.jpg'); % 读入待处理彩色图 Sgray = rgb2gray(Scolor); % 三通道加权转灰度,保留亮度分布 s = strel('disk',13); % 半径 13 的圆盘结构元 Bgray = imopen(Sgray,s); % 开运算:先腐蚀后膨胀,比圆盘窄的结构全部被抹平 Egray = imsubtract(Sgray,Bgray); % 原图减背景,字符区域被显著提亮开运算的物理效果是「消除比结构元小的亮区域」。车牌上的字符笔画宽度通常只有几个像素,圆盘半径取到 13,足以把整块车牌连同字符一起抹掉,剩下的就是平滑的车身和背景。半径太小,背景图里还残留着车牌轮廓,相减之后车牌区域反而被削弱;半径太大,背景估计过度平滑,车身反光之类的缓变区域会被错误提亮。
imsubtract(Sgray,Bgray)这一步在图像处理里有个正式名字叫顶帽变换,专门用来提取比背景亮的细节。MATLAB 里可以直接写imtophat(Sgray,strel('disk',13)),结果完全一致。报告里如果能把这个术语点出来,说明不是照抄代码,而是知道每一步在数学上对应什么操作。
3.2 三分之一阈值:这个经验值从哪来
增强之后的图像,背景接近 0,字符是高亮区,理论上随便取个中间值就能二值化。实际图像有噪声,固定阈值在不同光照下表现差异很大,所以设计里用了一个跟着图像动态范围走的自适应阈值。
fmax1 = double(max(max(Egray))); % 增强图的最大灰度值 fmin1 = double(min(min(Egray))); % 增强图的最小灰度值 level = (fmax1-(fmax1-fmin1)/3)/255; % 取 2/3 分位作为阈值,并归一化到 [0,1] bw22 = im2bw(Egray, level); % 按阈值二值化 bw2 = double(bw22); % 转 double,供 edge 使用把公式展开就是(2*fmax1 + fmin1) / 3 / 255,意思是阈值从最大值出发,往最小值方向走整个动态范围的三分之一。往高值偏的原因很直接:顶帽之后背景理论上接近 0,但总会有残留噪声,阈值定低了这些噪声全部变成白点,二值图会变成一片雪花。往高值偏可以牺牲一部分暗笔画,换来干净的前景。
三分之一定律在不同光照下不一定最优,逆光或者夜间补光的图,字符和背景的反差变小,这个固定比例会失效。更稳的做法是用 Otsu 方法自动求阈值,level = graythresh(Egray)一行搞定,报告里可以作为改进方向写一段对比。
注意:
im2bw的第二个参数必须在 0 到 1 之间,忘记除以 255 是新手最常见的报错来源,现象是二值图全白或者报「阈值超出范围」。
3.3 Canny 边缘与两次形态学滤波:结构元的尺寸怎么定
二值图里的字符仍然是分离的笔画,直接做连通域标记会得到几十个碎块。Canny 算子先提取边缘,再用形态学把断开的边缘连成整体,这一步是定位成败的关键。
grd = edge(bw2,'canny'); % Canny 双阈值边缘检测 bg1 = imclose(grd, strel('rectangle',[5 19])); % 横向闭运算:填补字符之间的横向间断 bg3 = imopen(bg1, strel('rectangle',[5 19])); % 同尺寸开运算:抹掉零散噪点 bg2 = imopen(bg3, strel('rectangle',[19 1])); % 纵向开运算:去掉垂直方向的细长干扰闭运算先膨胀后腐蚀,作用是填小孔、连近邻;开运算先腐蚀后膨胀,作用是去孤点、断细线。三个操作的顺序不能换:先闭把车牌内部的字符边缘连成一整块,再开把这块之外的零碎噪声清掉,最后用纵向结构元把散热格栅、保险杠这类竖向细长条压掉。
结构元的尺寸来自对车牌的粗略度量。一张 640×480 的图里,车牌宽度大概 120 到 160 像素,字符间距几个像素,所以横向结构元取 19,足够跨越字符间隙又不会把相邻的车灯连进来。这个数字换分辨率就得跟着改,不是万能常量。血泪经验是:换了一张不同尺寸的图,第一件事就是按图宽等比例缩放结构元,否则定位会莫名其妙地失效。
3.4 连通域标记与长宽比筛选:车牌凭什么被挑出来
形态学处理完,图上剩下若干个白色连通块,车牌是其中一个。筛选依据是形状特征:车牌是矩形,宽度约为高度的三倍,面积在整个画面里占一定比例但不是最大。
[L,num] = bwlabel(bg2,8); % 8 邻域连通域标记 stats = regionprops(L,'Area','BoundingBox'); % 提取面积与外接矩形 Area = [stats.Area]; BBox = cat(1,stats.BoundingBox); % 每行 [x y width height] ratio = BBox(:,3)./BBox(:,4); % 宽高比 idx = find(ratio > cfg.aspect_range(1) & ... ratio < cfg.aspect_range(2) & ... Area > cfg.area_ratio_min*numel(bg2));标准蓝牌的外形尺寸是 440×140 毫米,宽高比约 3.14。拍摄角度带来的透视畸变会让这个比值上下浮动,所以筛选用区间不用定值,2.6 到 4.0 之间比较稳妥。面积下限用来排除小图标和噪点,上限一般不加,因为车牌通常不会是画面里最大的连通块——加上限反而容易漏掉近景大车牌。
这里有个版本兼容的坑要提前说:老版本 MATLAB 用的是imfeature(L,'basic'),这个函数在较新的版本里已经被移除,照着老代码敲会直接报未定义函数。换成regionprops之后返回的是结构体数组,字段名大小写不敏感但拼写要对,BoundingBox里四个数的顺序是[x y w h],不是[x1 y1 x2 y2],用错顺序裁出来的子图位置会整体偏移。
4. 倾斜校正、字符分割与模板匹配:把一块车牌拆成七个 22×14 标准子图
定位之后得到的是一块包含车牌的矩形子图,里面还有上下边框、左右边框、可能还有几度的倾斜。分割要把这块图切成七个独立的字符,每个字符再归一化成统一尺寸送进识别环节。这一段的算法核心是投影分析:把二维图像的白色像素按行或按列累加成一维曲线,曲线的峰对应字符密集区,谷对应字符间隙和边框,找到谷的位置就找到了切割线。
4.1 水平投影峰谷分析:先找到车牌的上下边框
histcol1 = sum(sbw1); % 按列求和 → 垂直投影,反映每一列的白色像素数量 histrow = sum(sbw1'); % 按行求和 → 水平投影,反映每一行的白色像素数量sbw1是定位得到的二值子图。转置之后再按列求和,等价于对原图按行求和。水平投影曲线上,车牌上边框和下边框会形成两个陡峭的峰,中间是一段相对平缓、波动较小的字符区。峰的位置对应边框的行号,峰与峰之间就是真正要保留的字符高度范围。
读这条曲线的方法是先求最大值和均值,用它们构造阈值,再从左往右扫描找第一个上升点和最后一个下降点。上升点之前是边框和背景,下降点之后也是。这一步不需要精确到像素,误差几个行无所谓,后面还有垂直投影兜底。
4.2 线性拟合求倾角与 imrotate 校正
车牌倾斜会让投影的峰谷变得模糊,严重时字符之间连成一片,分割直接失败。校正的思路是取车牌上边缘或下边缘的一串白点,拟合一条直线,算出直线与水平轴的夹角,再把整幅图反向旋转回去。
fresult = fit(xdata',ydata','poly1'); % 一次多项式最小二乘拟合,等价于拟合直线 p1 = fresult.p1; % 斜率 angle = atan(p1)*180/pi; % 斜率转角度,弧度制转角度制 subcol = imrotate(subcol1, angle, 'bilinear', 'crop'); % 灰度子图旋转 sbw = imrotate(sbw1, angle, 'bilinear', 'crop'); % 二值子图同角度旋转'bilinear'指定双线性插值,旋转后新像素由周围四个点加权得到,比最近邻插值平滑;'crop'让输出保持和输入一样的尺寸,不裁掉边角也不补黑边,代价是旋转后四周可能被切掉一点。对车牌这种本身就填满画面的子图,crop更合适。
fit函数属于 Curve Fitting Toolbox,没装这个工具箱的机器上会报错。替代方案是用polyfit,同样是多项式拟合,返回系数向量,取第一个系数就是斜率:
p = polyfit(xdata, ydata, 1); % 一次拟合,p(1) 是斜率,p(2) 是截距 angle = atan(p(1))*180/pi; % 转成角度两条路的结果一致,报告里写哪一条都行,但要在代码注释里说明依赖,否则换台机器跑不起来会很难解释。旋转角度一般落在正负 10 度以内,超过这个范围说明拟合用的点取错了,多半是把字符内部的边缘也当成边框点混进去了。
4.3 去上下边框:拿到字符的实际高度
旋转之后重新计算水平投影,找投影最大值所在的位置作为字符区中心,再向前后扫描确定字符的顶行和底行。
maxhight = max(markrow2); % 投影峰值的宽度,对应字符高度 findc = find(markrow2 == maxhight); % 峰值所在位置 rowtop = markrow(findc); % 字符区顶行 rowbot = markrow(findc+1) - markrow1(findc+1);% 字符区底行 sbw2 = sbw(rowtop:rowbot, :); % 裁掉上下边框后的子图 maxhight = rowbot - rowtop + 1; % 字符实际高度markrow、markrow1、markrow2是峰谷分析得到的上升点、下降点和峰宽数组,由前面那套「求最小值 → 取阈值 → 算谷宽 → 找峰中心」的流程生成。裁完之后,车牌子图里只剩下字符和一左一右两个边框,高度信息已经拿到,接下来交给垂直投影。
4.4 垂直投影找字符中心并切割
垂直投影曲线上,每个字符形成一段高平台,字符之间形成低谷。难点在于车牌第一个字符是汉字,汉字与后面的字母之间有一段明显更宽的间隙,这个间隙是所有间隔里最大的,可以作为定位的锚点。
for k = 1:n1 markcol3(k) = markcol(k+1) - markcol1(k+1); % 字符下降点 markcol4(k) = markcol3(k) - markcol(k); % 字符宽度 markcol5(k) = markcol3(k) - double(uint16(markcol4(k)/2)); % 字符中心位置 end markcol6 = diff(markcol5); % 相邻字符中心距 maxs = max(markcol6); % 最大值必出现在汉字与第 2 字符之间 findmax = find(markcol6 == maxs); % 记录这个位置作为锚点 markcol6(findmax) = 0; % 剔除锚点间隔后重新取最大 maxwidth = max(markcol6); % 作为最大字符宽度findmax是汉字后面的那个间隔的索引,从findmax-1开始往后数 7 个位置,正好覆盖汉字加六位字母数字。切割时以每个字符中心为基准,向左右各扩展半个最大字符宽度,再对越界情况做保护:
l = 1; for k = findmax-1 : findmax+5 % 共 7 个字符 cleft = markcol5(k) - maxwidth/2; cright = markcol5(k) + maxwidth/2 - 2; if cleft < 1 % 左边界保护 cleft = 1; cright = maxwidth; end if cright > n2 % 右边界保护 cright = n2; cleft = n2 - maxwidth; end SegBw1 = sbw(rowtop:rowbot, cleft:cright); SegBw2 = imresize(SegBw1, cfg.char_size); % 归一化到 22 行 14 列 imwrite(SegBw2, sprintf('sample/image%d.jpg', k), 'jpg'); % 存入样本库 l = l + 1; endimresize到 22×14 这个尺寸不是随便定的,它要在保留字形特征和压缩数据量之间取平衡。尺寸太大,模板库膨胀,匹配变慢;尺寸太小,B 和 8、G 和 6 这类字形接近的字符会被压成几乎一样,误判率直线上升。原文用的路径是绝对路径,换机器就失效,建议改成相对路径sample/并把目录建在脚本同级。
4.5 模板匹配识别:误差最小的那张图就是答案
识别环节把每个 22×14 的待测子图和样本库里的所有模板逐一比对,用逐像素绝对差之和作为距离度量,取距离最小的模板名作为识别结果。
best = inf; bestName = ''; files = dir(fullfile(sampleDir,'*.jpg')); for i = 1:numel(files) T = imread(fullfile(sampleDir, files(i).name)); T = im2double(imresize(T, cfg.char_size)); % 统一尺寸与数据类型 D = im2double(SegBw2); d = sum(abs(D(:) - T(:))); % 逐像素绝对差之和 if d < best best = d; bestName = files(i).name; end end用D(:)和T(:)把矩阵拉成列向量再相减,是为了兼容老版本 MATLAB;新版本可以直接写sum(abs(D-T),'all'),代码更短但要求 R2018b 以上。这个细节写进注释,换环境时不至于卡住。
样本库的命名要能反查字符,image1.jpg这种命名在调试阶段就是灾难——你看到识别结果输出image37根本不知道它代表哪个字符。建议改成char_A_01.jpg、char_0_03.jpg这种带标注的名字,识别时直接解析文件名即可。样本库的覆盖度决定识别率上限,同一个字符最好有常规、偏亮、偏暗、轻微倾斜几个版本,每个字符三到五张,七个字符位置共七十来张,工作量不小但对最终准确率影响最直接。
5. 常见问题排查:五类最容易翻车的现象、原因与处理
调试这一段花的时间通常比写代码多得多。下面五类是这类项目里出现频率最高的问题,每一条都按「看到什么现象 → 为什么会这样 → 怎么改」的顺序写,遇到问题时可以当检查表用。
5.1 定位阶段的三类翻车
现象一:定位框框住了整幅图,或者框在完全无关的角落。原因多半出在二值化阈值上。level计算出来如果接近 0 或者接近 1,二值图会变成全白或全黑,连通域标记自然找不到合理的候选。先imshow(bw22)看一眼,如果全白,说明阈值偏低,把公式里的三分之一改成二分之一试试;如果全黑,说明阈值偏高,往低调整。另一个常见原因是图像本身对比度太低,顶帽增强后字符依然不明显,可以在灰度化之后加一步imadjust做对比度拉伸。
现象二:框到了车灯、散热格栅或者保险杠上的装饰条。这些区域的宽高比有时也接近 3,光靠长宽比筛不掉。解决办法是加面积约束和位置约束:车牌通常位于画面中下部,可以按BoundingBox的纵坐标做一次过滤;面积上给一个上下限,太小的是噪点,太大的是车身部件。多个候选同时满足条件时,取面积最接近经验值的那个,而不是简单取第一个。
现象三:换一张图整个流程就崩,前一张明明好好的。这是参数硬编码的典型症状。disk(13)、[5 19]、[19 1]这些数字都是针对特定分辨率调出来的,换一张 1280×720 的图,结构元相对变小,闭运算连不上字符,开运算又抹不掉噪声。正确的做法是把结构元尺寸和图像宽度挂钩,比如r = round(size(Sgray,2)/50),按比例缩放。血泪经验是:调试阶段每换一张图,先把所有结构元尺寸按图宽重算一遍,再谈其他。
5.2 分割与识别阶段的两类翻车
现象四:切出来的字符粘连在一起,或者一个字符被切成两半。粘连通常发生在汉字和第一位字母之间,因为汉字笔画密、间隙小,垂直投影的谷不够深。可以在投影曲线上做平滑再找谷,或者直接把maxwidth调小一点强制分开。切碎则相反,多半是字符内部的笔画间隙被误判成了字符间隙,投影阈值定得太高。把histcol画出来看一眼曲线的形状,峰谷关系一目了然,比盲调参数快得多。字符本身倾斜没校正干净也会导致这个问题,回头检查angle是不是接近 0。
现象五:整体识别率还行,但总有那么几个字反复认错。这套设计里最典型的是 G 被认成 B、K 被认成 A、0 被认成 8。根子在 22×14 的分辨率下,这几对字符的差异只剩下几个像素——G 和 B 的区别是右下角那一小段横线,K 和 A 的区别是中间那一竖,0 和 8 的区别是中间那一横。归一化时如果插值方式选得不好,这几个像素直接被抹平。改进方向有三条:把归一化尺寸提到 32×16 保留更多细节;针对易混字符单独建子库,匹配时先做粗分类再在子库内细匹配;引入简单的结构特征(比如中间是否有横线、右侧是否有闭环)作为二次判据。报告里把这三对字符的混淆矩阵列出来,比笼统写「识别率 90%」有说服力得多。
另外还有一类容易被忽略的问题:样本库里的图和待测图的二值化方式不一致。样本是用imresize缩放后的二值图,待测字符如果经过了不同的阈值处理,两者的白像素比例会差很多,逐像素相减时误差整体偏大。统一在im2double之后做匹配,并且保证样本和待测走完全相同的预处理路径,这一条不难做到,但漏掉的人不少。
6. 验收与答辩:用十分钟证明这套系统真的跑通了
验收现场最尴尬的不是识别错,是演示到一半程序报错,或者换一张图就出不来结果。我一般会准备一个固定流程,十分钟之内把该证明的都证明掉。
先跑通一张标准图,从读图到输出车牌号一气呵成,中间不停顿。然后把关键中间结果按顺序展示:灰度图、顶帽增强图、二值图、边缘图、形态学滤波后的连通块、定位框、七个字符子图、识别结果。这一串图摆出来,哪怕最后识别错了一个字,也能证明每一段都在按设计工作。
再准备两到三张「失败案例」,主动展示并解释原因。比如那张把 G 认成 B 的图,指出是因为 22×14 分辨率下字形差异被压缩,然后说明改进方向。主动暴露问题比被追问出来强得多,导师更看重的是知不知道为什么错。
答辩高频问题基本集中在参数来源上,提前准备好答案:
| 可能的提问 | 回答要点 |
|---|---|
| disk(13) 为什么是 13 | 约为字符笔画宽度的数倍、车牌高度的三分之一,作用是抹掉整块车牌估背景 |
| 阈值为什么取三分之一 | 顶帽后背景接近 0,阈值往高值偏可压制残留噪声,属于经验值,可用 Otsu 替代 |
| 为什么用长宽比不用颜色 | 蓝牌黄牌白牌都有,颜色不通用;长宽比是几何不变量,透视下仍在容差内 |
| 结构元方向为什么不同 | 横向长条连字符,纵向长条压栅格,方向由车牌是扁矩形这个先验决定 |
| 为什么不用神经网络 | 样本量不足、训练不可控,模板匹配可解释、误差可打印 |
最后留一个改进方向收尾,比如多车牌场景、夜间低照度、汉字识别单独建模。答辩不是要求做出完美系统,是要求讲清楚做了什么、为什么这么做、哪里还不够。
从那以后我每次带这类课程设计,都强制要求先把中间结果全部可视化存盘,再谈识别率。因为出问题时能一眼看到是哪一步断的,比在命令行里盲猜快十倍。希望帮到你。
本文还有配套的精品资源,点击获取