☰
Matlab车牌识别课设源码:定位矫正分割识别全流程
2026/10/28 13:45:48 网站建设 项目流程

简介:一份面向高校理工科学生与Matlab课程设计学习者的车牌识别课程设计报告,聚焦自然环境下车辆图像的车牌定位、字符分割与号码识别全流程,适合图像处理、模式识别入门及课程设计参考。压缩包仅含1个docx文档,约1.37MB,正文按课程设计目的、设计原理、详细设计步骤、结果分析、总结体会与参考文献组织,并附源代码。报告结合图像读取、灰度化、开运算背景估计、差分增强、阈值二值化、边缘提取与车牌定位,讲解垂直投影法字符切分及模板匹配、神经网络两类识别思路,同时讨论光照、牌照质量、多牌照等影响因素与优化方向。已有116人学习,可作为梳理设计流程、撰写报告和复现实验的参考。

1. 一张歪着 8 度的车牌,照出了课设代码的真实水位

某高校课程设计答辩现场,演示用的样板图跑得挺顺,换成一张手机随手拍的照片——车牌在画面右下角、倾斜七八度、旁边还有树影——定位框直接跑到车标上去了。这套基于 Matlab 的车牌识别课程设计报告配源代码,真正值钱的地方不是“能识别车牌”这句口号,而是它把定位、矫正、分割、识别拆成了十来个可以单独调试的步骤,每一步都吐中间图像。适合两类人:要交课设、需要一份能跑通又能讲清原理的新手;想拿一套最小可运行流水线做算法替换实验的熟手。纯 Matlab 实现,不依赖深度学习工具箱,装了 Image Processing Toolbox 就能开工。

2. 系统骨架与目录结构:一条从彩色图到车牌号的流水线

2.1 四段式链路里,代码只占后两段

报告开篇把完整系统拆成车辆检测、图像采集、牌照识别三块。车辆检测靠地感线圈或者视频虚拟线圈,检测到车压线就吐一个触发信号;采集单元收到信号抓一帧;牌照识别单元接图干活。这条链路在真实停车场系统里是标准配置,但落到课程设计,硬件部分只写在报告里,代码是从“已经拿到一张静态图”开始跑的。认清这一点很关键——不少人翻完源码找不到视频采集和串口触发的部分,以为文件漏了,其实是设计范围就到软件算法为止。

软件侧再往下拆,就是四步:预处理、车牌定位、字符分割、字符识别。预处理负责把彩色图变成对比度够用、噪声够少的二值图;定位负责在整幅画面里圈出一个矩形;分割负责把这个矩形切成 7 个独立字符;识别负责把 7 个 22×14 的小图变成 7 个字符。四步之间靠中间图像传递,任何一步的输出质量都会往后传,这也是后面排查问题时必须逐级看中间图的原因。我的习惯是每一步都figure, imshow一次,跑一遍能存下十几张图,比盯着最终结果猜要快得多。

2.2 目录结构与文件职责

源码包的组织方式比较朴素,主脚本一个文件从头跑到尾,测试图和样本库分目录放。按常见做法整理成下面这样,对照着看不会乱:

路径作用关键内容
main.m主脚本Step1~Step14 顺序执行,每步输出一张 figure
img/3.jpg测试图蓝底白字车牌,建议宽度压到 800 像素以内
sam/字符样本库image1.jpg~image7.jpg,统一 22×14 二值图
lib/char_map.mat字符映射表样本序号到字符的对应关系
output/中间结果各步骤截图,答辩时直接拿去讲

主脚本是顺序脚本,不是函数,所有变量都躺在工作区里。这种写法在课设里很常见,好处是断点随便打、变量随便看,坏处是变量名一旦重名就互相覆盖,比如maxhight在去上下边框那段被赋值两次,第二次是字符高度,第一次是行投影最大值,读代码时容易看串。真要改,第一步就是把每一段包成函数,这个放到最后一章讲。

样本库那块有个硬编码路径的坑:原文里imwrite的目标写成了F:\MATLAB\work\sam\imageK.jpg,换台机器、换个盘符直接报错。改成fullfile(pwd,'sam',...)就没事了。样本库的命名也有讲究,image1~image7只是占位,真正要落地得按字符建目录,比如sam/A/001.jpg、sam/0/003.jpg,识别时按目录名反查字符,比维护一张映射表省事。

2.3 中间变量链与参数约定

这套代码最值得抄的地方,是它的变量命名把数据流标得清清楚楚。把这条链记住,调试时就知道该看谁:

变量类型含义由谁产生
Scoloruint8 H×W×3原始彩色图imread
Sgrayuint8 H×W灰度图rgb2gray
Bgrayuint8 H×W形态学估计的背景imopen(Sgray,strel('disk',13))
Egrayuint8 H×W背景相减后的增强图imsubtract
bw2double 0/1二值图im2bw
grdlogicalCanny 边缘edge(...,'canny')
bg1/bg3/bg2logical闭运算、开运算、纵向开运算结果imclose/imopen
Ldouble连通域标签bwlabel
sbw1logical车牌二值子图(含边框)按包围盒裁切
subcol1uint8车牌灰度子图同上
sbwlogical旋转矫正后的二值子图imrotate
sbw2logical去上下边框后的字符带行投影裁切
SegBw2logical 22×14归一化后的单字符imresize

几个关键参数的取值逻辑得说清楚,不然换个分辨率就全崩:

  • strel('disk',13)的 13 是圆盘半径,经验上取字符笔画宽度的 1.5 倍左右。它的作用是把比圆盘小的亮细节(也就是字符)抹掉,剩下缓慢变化的背景。图像宽度从 640 变到 1600,这个值要跟着放大。
  • strel('rectangle',[5,19])是 5 行 19 列的横向长条。车牌“宽而扁”,横向闭运算能把断开的笔画连成整块,同尺寸开运算再去掉毛刺。后面的[19,1]是竖向长条,专门抹掉护栏、雨刷这类竖直细线。
  • level的系数 1/3 决定阈值落在2/3·max + 1/3·min的位置,比中点低,偏向保留前景。
  • [22 14]是归一化目标尺寸,来自当年一个字符点阵模板的约定,汉字和字母数字都往这个尺寸上压。

运行环境上,edge、strel、bwlabel、regionprops都属于 Image Processing Toolbox;原文用的fit(...,'poly1')属于 Curve Fitting Toolbox,没装就报错,换成polyfit一行搞定,这个坑后面单独说。

3. 预处理与车牌定位:灰度化、背景相减、二值化与 Canny 的那几刀

3.1 读图、灰度化与形态学背景估计

第一步没什么花活,读图、转灰度。要注意的是有些测试图本身就是灰度图,size(Scolor,3)会是 1,直接rgb2gray会报错,加个判断更稳:

%% Step1-2 读图与灰度化 clear; close all; clc; Scolor = imread('img/3.jpg'); % 原始彩色图,宽度建议先压到 800 以内 if size(Scolor,3) == 3 Sgray = rgb2gray(Scolor); % 三通道才转灰度 else Sgray = Scolor; % 灰度图直接用 end figure, imshow(Scolor), title('原始图像'); figure, imshow(Sgray), title('原始黑白图像');

灰度化不只是为了省内存。彩色图里 R/G/B 三个通道各带一份噪声,转成灰度相当于做了一次加权平均,后续做投影和连通域时波形会干净很多。这一步的输出Sgray会一路用到字符分割,别在中途覆盖它。

接下来是这套代码里比较巧的一招:用开运算估计背景,再和原图相减。

%% Step3-4 形态学背景估计与背景相减 s = strel('disk',13); % 圆盘半径约为字符笔画宽度的 1.5 倍 Bgray = imopen(Sgray,s); % 开运算抹掉比圆盘小的亮细节,留下背景 Egray = imsubtract(Sgray,Bgray); % 背景相减,压掉光照不均 figure, imshow(Bgray), title('背景图像'); figure, imshow(Egray), title('增强黑白图像');

imopen是先腐蚀后膨胀。腐蚀阶段,比圆盘小的亮区域(车牌字符、车标、反光点)被整块吃掉;膨胀阶段再把剩下的背景区域长回原来的边界。结果就是一张只有缓慢亮度变化的“背景图”。原图减背景图,光照不均造成的明暗渐变被抵消,字符相对背景的对比度被拉出来。这个思路和现在常用的顶帽变换(imtophat)是一回事,只是拆开写更直观。

参数上,disk的半径不能太大也不能太小。太大,字符被当成背景的一部分保留下来,相减后字符反而变暗;太小,背景估计不干净,相减后噪声一片。判断办法很简单:看Bgray那张图,如果车牌区域还能隐约看到字符轮廓,说明半径小了;如果整块车牌都被抹平,只剩一片灰,说明刚好。

3.2 阈值选取:为什么是 (max-(max-min)/3)/255

二值化这一步,原文没有用graythresh,而是手写了一个基于极值的阈值:

%% Step5 自适应阈值二值化 E = double(Egray); % 统一转 double,避免 uint8 相减截断 fmax1 = max(E(:)); fmin1 = min(E(:)); level = (fmax1 - (fmax1 - fmin1)/3) / 255; % 归一化到 0~1 bw2 = im2bw(E/255, level); % 新版本可写 imbinarize(E/255, level) bw2 = double(bw2); figure, imshow(bw2), title('图像二值化');

把公式展开看,level = (2/3·fmax1 + 1/3·fmin1)/255,阈值落在最大值往下三分之一的位置。背景相减之后,背景像素接近 0,字符像素是亮的高值,阈值取得偏低,效果是“宁可多留前景,也不要把笔画切断”。对车牌这种前景占比很小的图,这个偏向是对的:多留的噪声后面有形态学开运算兜底,笔画一旦断了就补不回来。

/255这一步容易被忽略。im2bw要求输入是 0~1 的 double,imbinarize对 uint8 输入会自动除以 255,但对 double 输入不会。如果Egray是 uint8,double(Egray)之后范围是 0~255,不归一化直接喂给im2bw,阈值和像素不在一个量纲上,出来的图不是全黑就是全白,这是新手最常翻的车。

提示:二值化后先figure, imshow(bw2)看一眼,前景(白)应该集中在车牌区域,背景接近纯黑。如果整张图大片发白,回头查level是不是算成了 0。

3.3 Canny 边缘与矩形结构元的开闭运算

拿到二值图之后做边缘检测,再靠形态学把边缘连成块:

%% Step6-7 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])); % 纵向开:抹掉竖直细线 figure, imshow(grd), title('图像边缘提取'); figure, imshow(bg1), title('图像闭运算[5,19]'); figure, imshow(bg3), title('图像开运算[5,19]'); figure, imshow(bg2), title('图像开运算[19,1]');

edge(...,'canny')内部做了高斯平滑、梯度计算、非极大值抑制和双阈值连接四件事,输出的边缘是单像素宽的细线。这些细线断断续续,直接做连通域标记会碎成上百个小块。imclose用 5×19 的横向长条做闭运算,横向膨胀把相邻的字符边缘粘起来,再腐蚀回原宽度,结果是车牌区域变成一个横向的长条块。

紧接着的imopen用同样尺寸做开运算,把粘合过程中带进来的零星噪点重新去掉。第三次imopen换成 19×1 的竖向长条,专门对付竖直线条——护栏、雨刷、车身上的接缝,这类东西在边缘图里是细长的竖线,横向结构元拿它没办法,竖向开运算一腐蚀就没了。

三组结构元的顺序不能乱:先横向闭(粘连)、再横向开(去噪)、最后纵向开(去竖线)。反过来先做竖向开运算,车牌边缘的横向笔画会先被削掉一截,后面再怎么闭也补不回来。

3.4 连通域标记与长宽比筛选

最后一步是给每个连通块打标签,算包围盒,按长宽比挑出车牌:

%% Step8 连通域标记 + 长宽比筛选 [L,num] = bwlabel(bg2,8); % 8 邻域连通标记 stats = regionprops(L,'BoundingBox','Area'); % 新版用 regionprops,imfeature 已废弃 ratioMin = 2.2; ratioMax = 4.2; % 蓝牌 440:140≈3.14,两侧留余量 cand = []; for k = 1:num bb = stats(k).BoundingBox; % [x y w h],x 是列、y 是行 if bb(4) < 8 || bb(3) < 20, continue; end % 太小直接丢 r = bb(3) / bb(4); if r > ratioMin && r < ratioMax cand(end+1,:) = [k, bb, stats(k).Area]; %#ok<AGROW> end end if isempty(cand) error('没有候选区域,回头检查二值化和形态学参数'); end [~,idx] = max(cand(:,6)); % 面积最大的候选当车牌 plateBox = cand(idx,2:5);

bwlabel的第二个参数 8 表示 8 邻域,斜对角相连也算同一块;用 4 邻域的话,倾斜车牌的边缘容易断成几段。regionprops返回的BoundingBox是[x y width height],注意 x 对应列、y 对应行,裁图时别写反成Sgray(x:x+w, y:y+h),那是另一个经典翻车点。

长宽比区间 2.2~4.2 是围绕 3.14 上下放宽的结果。收得太紧,倾斜或者透视变形的车牌会被漏掉;放得太宽,车标、进气格栅、广告牌都能混进来。面积最大这条规则在这里很管用,因为车牌通常是画面里最大的矩形候选,但遇到多车场景就不行了,得改成“取面积中位数以上的所有候选,再按位置排序”。

4. 倾斜矫正与字符分割:投影峰谷、poly1 拟合与 22×14 归一化

4.1 水平投影峰谷分析定位上下边框

定位拿到的是包围盒,直接裁出来的子图还带着车牌自身的边框(白线或黑边)。先去上下边框,靠的是水平投影:

%% Step9 裁子图并做双向投影 x = max(1, round(plateBox(1))); y = max(1, round(plateBox(2))); w = min(round(plateBox(3)), size(Sgray,2) - x + 1); h = min(round(plateBox(4)), size(Sgray,1) - y + 1); subcol1 = Sgray(y:y+h-1, x:x+w-1); % 车牌灰度子图 sbw1 = bw2(y:y+h-1, x:x+w-1); % 车牌二值子图 histrow = sum(sbw1,2); % 水平投影:每一行的前景像素数 histcol = sum(sbw1,1); % 垂直投影:每一列的前景像素数 figure, subplot(2,1,1), bar(histcol), title('垂直投影(含边框)'); subplot(2,1,2), bar(histrow), title('水平投影(含边框)');

sum(sbw1,2)里的 2 表示沿第 2 维(列方向)求和,得到列向量,长度等于行数;sum(sbw1,1)沿行方向求和,得到行向量。这个约定记反了,画出来的波形方向就是横的,峰谷分析全乱。

水平投影图上,车牌上边框和下边框各是一个又高又窄的峰,中间字符带是一片高度中等的连续区域。找峰的两个边界,就得到了字符带的起止行:

%% Step10 用水平投影去掉上下边框 rowProfile = sum(sbw,2); thrRow = mean(rowProfile(rowProfile > 0)) * 0.5; % 均值一半作为经验阈值 rows = find(rowProfile > thrRow); rowtop = rows(1); rowbot = rows(end); sbw2 = sbw(rowtop:rowbot, :); charH = rowbot - rowtop + 1; % 字符高度

阈值取“非零行的均值的一半”而不是全局均值,是为了避开上下边框那两个大峰把均值抬高。这一步的输出charH很关键,后面判断字符切得对不对,就看切出来的每块高度是不是接近它。

4.2 poly1 线性拟合求倾斜角与 imrotate 矫正

车牌倾斜会让垂直投影的峰谷变糊,字符之间的间隙分不开,所以得先转正。原文的做法是取车牌上边缘的一批点做一次线性拟合,斜率转角度:

%% Step11 上边缘点线性拟合,求倾斜角 [rowList, colList] = find(sbw1); % 所有前景点的行列坐标 topRow = min(rowList); band = rowList <= topRow + 3; % 取最上面 3 行作为上边缘带 xdata = colList(band); ydata = rowList(band); p = polyfit(xdata, ydata, 1); % 一次拟合,等价于 fit(...,'poly1') angle = atan(p(1)) * 180 / pi; % 斜率转角度(度) if abs(angle) > 15 warning('拟合角 %.1f 度超出合理范围,跳过矫正', angle); angle = 0; end subcol = imrotate(subcol1, angle, 'bilinear', 'crop'); % 灰度子图旋转 sbw = imrotate(sbw1, angle, 'bilinear', 'crop'); % 二值子图同步旋转

polyfit(x,y,1)返回[斜率, 截距],斜率就是dy/dx。图像坐标系里 y 轴朝下,所以正斜率对应顺时针倾斜,imrotate的正角度是逆时针旋转,两者符号相反,实际效果正好抵消。如果发现转完更歪了,把angle取个负号试试。

这里把原文的fit(...,'poly1')换成了polyfit,原因是fit属于 Curve Fitting Toolbox,很多人机器上没装这个工具箱,跑到这一步直接报 undefined function。polyfit是 MATLAB 基础函数,结果完全一样,没有必要为了一个一次拟合多装一个工具箱。

imrotate的第三个参数'bilinear'是双线性插值,比默认的最近邻平滑,代价是二值图的边缘会多出一圈灰度过渡,sbw旋转后建议再imbinarize一次保持二值。第四个参数'crop'让输出保持和输入一样的画布尺寸,不裁不扩——不加这个参数,旋转后图像会变大,后面所有基于原坐标的计算全部失效。

角度超过 ±15 度直接放弃矫正,是个很实用的保护。上边缘那 3 行里如果混进了车牌的金属反光或者贴纸,拟合出来的斜率能到三四十度,硬转过去字符全糊。

4.3 垂直投影切字符:中心间距与最大宽度

转正之后重做垂直投影,找每个字符的中心列。这段是全文最绕的地方,拆开看就三件事:找连续前景段、算中心、按最大字宽切。

%% Step12 垂直投影找字符段,定位中心列 colProfile = sum(sbw2,1); thrC = mean(colProfile(colProfile > 0)) * 0.4; % 只统计非零列,避开空白区 mask = colProfile > thrC; d = diff([0 mask 0]); % 前后补零,方便找边界 starts = find(d == 1); % 每段的起始列 ends = find(d == -1) - 1; % 每段的结束列 centers = round((starts + ends) / 2); widths = ends - starts + 1; gap = diff(centers); [~, gi] = max(gap); % 最大间隔 = 第2、3字符之间的点 if gi < 2 || gi > numel(centers) - 5 error('字符段数量异常(%d 段),检查阈值或边框是否去净', numel(centers)); end maxWidth = round(median(widths)); % 用中位数抗异常宽窄

diff([0 mask 0])是个常用技巧:在原序列前后各补一个 0,再做差分,上升沿(d==1)就是每段的起点,下降沿(d==-1)就是每段的终点后一位,减 1 得到终点。这样一次就能把所有连续段的边界找出来,不用写循环。

中国车牌 7 位,第 2 位和第 3 位之间有一个圆点分隔符,所以centers里最大的那个间隔gi一定落在圆点两侧。拿它当基准点,往左推一位是第 1 个字符,往右推五位是第 7 个字符:

%% Step13 按基准点向两侧切 7 个字符,归一化到 22×14 n2 = size(sbw2,2); segs = zeros(22, 14, 7, 'uint8'); for k = 1:7 c = centers(gi - 2 + k); % k=1 时取到 gi-1,即第 1 字符 cleft = max(1, c - floor(maxWidth/2)); cright = min(n2, cleft + maxWidth - 1); if cright - cleft + 1 < maxWidth % 贴边时向左补齐 cleft = max(1, cright - maxWidth + 1); end seg = sbw2(:, cleft:cright); segs(:,:,k) = imresize(seg, [22 14]); % 各向异性缩放到 22×14 end

gi - 2 + k这个偏移量是整段最容易写错的地方:gi是第 2、3 字符之间那个间隔的下标,centers(gi)是第 2 个字符的中心,所以第 1 个字符中心是centers(gi-1),通式就是centers(gi-2+k)。写成gi-1+k会让 7 个字符整体右移一位,切出来第一个是第 2 字符,最后一个是空白,识别结果永远是 6 个字符加一个乱码。

imresize(seg,[22 14])是各向异性缩放,不管原来多宽多窄都压成 22 行 14 列。这么做的代价是汉字被横向压扁、字母被纵向拉长,直接导致后面 G 和 B、0 和 8 分不清。改良办法在最后一章讲。

4.4 样本库落盘与命名规范

切完的字符要存下来当模板,原文用的是绝对路径:

%% Step14 字符落盘到样本库 libDir = fullfile(pwd, 'sam'); % 相对路径,换机器不用改 if ~exist(libDir, 'dir'), mkdir(libDir); end for k = 1:7 fname = fullfile(libDir, sprintf('image%d.jpg', k)); imwrite(segs(:,:,k), fname, 'jpg'); end

fullfile会自动处理路径分隔符,Windows 和 Linux 都能跑。存 jpg 是有损压缩,22×14 的小图压完边缘会糊,建议改成 png。命名上image1~image7只能表示“这一张图的第几位”,没法表示“这是字符 A”,所以sam目录只适合当临时输出。真正的模板库要按字符分类,一个字符一个子目录,每个子目录塞 10~20 个不同字体、不同光照的样本,识别时遍历目录名反查字符。

5. 避坑与排查:定位跑偏、切字断裂、模板误识的现场记录

5.1 定位类问题:框到车标、二值图翻车

坑一:定位框落在车标或进气格栅上,或者干脆框住整台车。

现象很直观,plateBox画出来的红框位置不对,但代码没报错,后面照样切图、照样识别,输出一串乱码。

原因是结构元尺寸和图像分辨率不匹配。strel('rectangle',[5,19])是按 640×480 这个量级调的,换成 2000 像素宽的照片,字符笔画粗了一倍多,5×19 的闭运算连不成整块,车牌区域的边缘碎成好几段,连通域面积反而比车标小;再加上长宽比阈值放到 4.2,车标那种接近 3:1 的矩形很容易被选中。

解决办法是进主流程前统一把图缩到固定宽度,Scolor = imresize(Scolor, [NaN 800]),然后结构元尺寸按k = W/800等比缩放,strel('rectangle', round([5 19]*k))。长宽比再收紧到 2.2~4.2,并且加一条“候选高度占图像高度 5%~25%”的约束,车标和整台车这两种极端尺寸都能被滤掉。

坑二:二值图全黑或全白,Canny 什么都检测不到。

bw2显示出来一片纯色,grd是空的,bwlabel返回num=0,后面全部报错。

根因在量纲。Egray是 uint8,imsubtract对负值做截断归零,背景相减本该产生的一批负值全被抹平;紧接着level除以 255 归一化到 0~1,而E还是 0~255,阈值和像素差了两个数量级,im2bw要么全判前景要么全判背景。图本身对比度低的时候,fmax1和fmin1还可能相等,level直接算成 0。

改法是把整条链统一到 double 0~1:E = double(Egray)/255;,fmax1 = max(E(:)); fmin1 = min(E(:));,然后bw2 = imbinarize(E, level);,阈值算之前加一句assert(fmax1 > fmin1, '图像无动态范围')。对比度确实低的图,换成graythresh(E)或者adaptthresh(E)自适应阈值,比手写极值公式稳。

5.2 分割类问题:函数弃用、字符粘连

坑三:跑到连通域那一步报Undefined function 'imfeature'。

老代码里写的是Feastats = imfeature(L,'basic'),在 R2013b 之后的版本里这个函数已经被移除,直接报未定义。

imfeature的替代品是regionprops,字段名从BoundingBox、Area这些直接取,写法是stats = regionprops(L,'BoundingBox','Area','Centroid')。注意返回的是结构体数组,取第 k 个要写stats(k).BoundingBox,不能写成stats.BoundingBox(k)。另外bwlabel在新版本里也建议换成bwlabeln或者bwconncomp,后者返回连通域结构体,处理大量小块时更快。

坑四:字符粘连,7 个字符切成 6 块或者 8 块。

切出来的segs里,某两块粘在一起,或者第 1 位汉字被从中间劈开,识别结果少一位、多一位。

两个原因叠在一起:一是左右边框没去干净,投影两端各有一段连续高值,被当成字符段;二是垂直投影阈值用了全局均值,汉字左右两部分笔画之间的那一列前景像素少,低于阈值,被误判成字符间隙。另外车牌倾斜没矫正到位时,垂直投影的谷底会被抹平,相邻字符的投影连成一片。

解决分三步:先按列投影把左右两端连续超过 3 列的高值区裁掉,去掉边框;阈值改成只统计非零列的均值再乘 0.4,让汉字内部的低投影列保留下来;切完用widths的中位数做一次过滤,宽度小于中位数一半的段并到相邻段里。

5.3 识别类问题:字形混淆与样本库脏数据

坑五:G 识别成 B,0 识别成 8,K 识别成 A。

原文报告里自己也承认了这个失败案例。Y 方向上看,G 和 B 的差别在右下角那一小段横钩,0 和 8 的差别在中间那个交叉点,K 和 A 的差别在左侧竖线和右斜线的连接方式。这些差别在 22×14 的网格里只占两三个像素,imresize的各向异性拉伸又把它们压得更扁,模板相减的误差自然分不开。

改法有四条,按投入产出排序:第一,缩放时保持宽高比,先imresize到高度 22、宽度按比例算,再把结果padarray到 14 列,避免字形被压扁;第二,每个字符的模板从 1 个扩到 10~20 个,覆盖不同字体、不同光照;第三,匹配准则从绝对差平方和改成归一化相关系数corr2,对整体亮度不敏感;第四,加二次判别特征,比如字符的宽高比、竖直笔画数、上下半区的前景占比,模板匹配得分接近时用这几个特征投票。

样本库脏数据是另一类隐蔽问题。切图时如果混进了一张带边框的图,或者某张图旋转角度不对,它作为模板会把所有同类字符往错误方向拉。建库时最好写个脚本把每个样本imshow一遍,人工过一眼,比事后调参数省事得多。

6. 二次开发:把课程设计改成能复用的识别 Demo

课设代码最大的问题是所有逻辑挤在一个脚本里,改一个参数要往上翻两百行。我一般做的第一件事是拆函数:preprocess(img)、locatePlate(bw)、rectifyAndSplit(sub, sbw)、recognize(segs)四个入口,主脚本只剩四行调用,中间结果全部返回结构体。拆完之后,想单独测分割就只传一张裁好的车牌图进去,不用每次从imread跑起,调试时间能砍掉一大半。

参数外置是第二件事。把disk半径、结构元尺寸、长宽比区间、投影阈值系数、归一化尺寸全塞进一个config.m,返回一个 struct,函数内部用cfg.diskR这种写法取。这样换一批图只改配置文件,不动算法代码。更进一步可以按图像宽度自动推:cfg.diskR = round(13 * W / 800),分辨率变化就不用管了。

识别器替换是收益最大的一块。模板匹配适合教学演示,工程上一般换成 BP 网络或者 SVM。换成 BP 的做法不复杂,输入层 22×14=308 个节点,隐层 40~60 个,输出层按类别数(汉字、字母、数字分开训三个子网),用切好的样本库当训练集,train一下就行。原来的模板匹配留着当兜底,两个结果不一致时输出置信度低的那个,人工复核。下面这张表是我整理过的改造清单,按优先级排:

改造点改动量收益
脚本拆函数 + 结构体传参半天单步调试,改动不牵连
参数外置 + 按分辨率自适应半天换图不用改代码
样本库按字符分类,每类 10+ 样本一天识别率提升最明显
匹配准则换corr2+ 二次特征一天混淆字对明显减少
加批量评测脚本,统计逐字符准确率半天改动有没有效果一目了然

批量评测这块值得单独说。很多人改完参数,拿原来那张样板图跑一遍觉得挺好就收工,换张图又崩。正确做法是准备 30~50 张不同光照、不同角度、不同背景的图,写个循环跑完,输出三列:定位是否成功、分割字符数是否等于 7、逐字符识别是否正确。三个指标分开统计,才能知道问题出在哪一环——定位成功率 95% 但分割正确率只有 60%,那说明问题在投影阈值,跟识别器没关系。

最后提一个我踩过的坑:imrotate矫正之后,sbw和subcol必须同步旋转,角度、插值方式、'crop'参数三样都得一致。有次只转了灰度图忘了转二值图,投影是在没矫正的图上做的,切出来的字符整体斜着,识别全军覆没,查了两个小时才发现是漏了一行。从那以后我每次改旋转参数,都把两张图并排subplot出来强制看一眼,确认字符是正的才往下走。希望帮到你。

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

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

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

立即咨询