简介:面向高校理工科学生与Matlab图像处理初学者的车牌识别课程设计报告,围绕车辆检测、图像采集与牌照识别三大环节展开,系统梳理车牌定位、字符分割和字符识别的设计思路,并给出总体方案、详细步骤、结果分析、总结体会与参考文献,适合课程设计撰写、实验报告参考和入门项目复盘。压缩包共1个docx文件,约1.37MB,文档结合源码片段讲解灰度化、开运算背景估计、背景相减增强、阈值二值化、垂直投影分割以及模板匹配与神经网络识别等方法,还讨论光照、车牌质量、车速等因素对识别率的影响及优化方向。报告按设计目的、原理、详细步骤、结果与分析、总结、体会和参考文献组织章节,便于快速定位课程设计所需模块,也能帮助读者理解从图像预处理到号码输出的完整链路。已有116人学习,可作为Matlab车牌识别课题的结构模板与算法排错参考。
1. 从一张歪斜车牌照片说起:这套课程设计到底要交付什么
一份 MATLAB 车牌识别课程设计,最容易出问题的地方不是算法写不出来,而是结果图贴上去、老师一问参数就答不上来。这套方案要解决的正是这个问题:从一张倾斜、光照不均、字符边缘发虚的车辆照片开始,经过灰度化、背景减法增强、自适应阈值二值化、Canny 边缘、形态学开闭运算和长宽比筛选定位出车牌,再用最小二乘拟合算出倾斜角做旋转校正,靠水平与垂直投影的峰谷分析切出单个字符,最后缩放到 22×14 的模板尺寸,与自建样本库逐个比对输出车牌号。整条链路每一步都有中间图、有可调参数、有失败案例可复现,适合正在做图像处理方向课程设计或毕业设计的学生照着搭,也适合需要一套可运行基线方案的从业者拿去改。
2. 总体方案与技术路线:三段流水线、五个模块与工程目录
2.1 三段流水线:定位、分割、识别各自交付什么
完整的牌照识别系统在工程上分车辆检测、图像采集、牌照识别三部分,车辆检测负责在车到达时触发采集单元,采集单元拿到当前视频帧,牌照识别单元再对图像做处理并输出号码。课程设计受硬件条件限制,通常把前两段简化成“读一张本地静态图片”,真正要交作业的是第三段。但即便是第三段,也不能写成一坨从上跑到尾的脚本,要按交付物切成三段流水线。
第一段是车牌定位:输入一张彩色车辆图,输出车牌区域的灰度子图和二值子图。这一段结束的标志是你能用imshow单独显示出一块只含车牌、不带车灯和保险杠的图。第二段是字符分割:输入车牌子图,输出 7 个归一化到统一尺寸的二值小块,蓝牌是“省份汉字 + 字母 + 五位字母数字”的结构,切出来应该是 7 块。第三段是字符识别:输入这 7 个小块,输出字符代码,拼成字符串。
把链路切成三段的价值在调试阶段才体现出来。定位不准,后面切出来的字符必然是歪的;分割多切一块,识别结果整体错位;识别把 G 认成 B,前两段却是对的。如果全部写在一个脚本里,出了问题只能从头顺一遍,中间量全是临时变量,改一处不知道影响哪一处。我一般的做法是每段结束后强制figure显示一次中间结果,图上带title标注这一步的参数,答辩时这几张图本身就是证据。
2.2 识别器选型:模板匹配先跑通,神经网络留作对比
字符识别有两条主流路线。模板匹配的做法是把分割后的字符二值化、缩放到样本库模板的尺寸,逐个模板算差异,取差异最小的那个作为结果;神经网络的做法是先提取特征再训练分类器,或者直接把图像送进网络让网络自己学特征。两条路都能交作业,但落地顺序不一样。
模板匹配的优势是实现短、误差可量化、坏样本一眼看得出来。你切出来的字符和库里哪个模板差多少,是一个能打印出来的数字,答辩时可以说“这个 K 与模板的最小误差是 312,次小误差是 289,所以判错”。神经网络则需要样本量和训练时间,四个子网络(汉字、字母、字母数字、数字)分别训练,样本不够时过拟合,训练曲线好看但换一张图就崩。所以我的建议是先把模板匹配跑通,用同一套分割结果再喂网络做一组对比实验,答辩时手里有两张识别率表,比只有一张有说服力。
模板匹配的局限也要提前想好:对字体、笔画粗细、缩放比例敏感。样本库如果全是同一批图切出来的,干净得像实验室标本,换一批实拍图识别率会明显下滑。这一点在第 5 章会展开。
2.3 工程目录与主流程骨架
目录不用复杂,但要让别人打开就能跑。主脚本一个,功能函数按段拆,样本库单独一个文件夹,测试图按条件分类放。
% main.m —— 一次完整流程的入口,按三段流水线顺序调用 clear; close all; clc; % ---- 第一段:预处理与定位 ---- Scolor = imread('test/3.jpg'); % 原始彩色图像,建议先统一到 500x400 左右 Sgray = rgb2gray(Scolor); % 灰度化 se = strel('disk', 13); % 圆盘结构元素,尺度要跟图像分辨率匹配 Bgray = imopen(Sgray, se); % 开运算估计背景 Egray = imsubtract(Sgray, Bgray); % 原图减背景,压掉光照不均 fmax1 = double(max(Egray(:))); fmin1 = double(min(Egray(:))); level = (fmax1 - (fmax1 - fmin1)/3) / 255; % 三分法阈值,归一化到 0~1 bw2 = double(im2bw(Egray, level)); % 二值图 grd = edge(bw2, 'canny'); % 边缘提取 bg1 = imclose(grd, strel('rectangle', [5, 19])); % 横向闭运算,连上断口 bg3 = imopen(bg1, strel('rectangle', [5, 19])); % 去细小噪点 bg2 = imopen(bg3, strel('rectangle', [19, 1])); % 纵向开运算,去竖向干扰 [L, num] = bwlabel(bg2, 8); % 8 邻域连通域标记 stats = regionprops(L, 'BoundingBox', 'Area'); % 老版本写作 imfeature(L,'basic') box = cat(1, stats.BoundingBox); % 每行 [x y width height] ratio = box(:,3) ./ box(:,4); % 宽高比 idx = find(ratio > 2.2 & ratio < 4.0 & [stats.Area] > 800); % idx 里通常就是车牌候选区,取第一个或面积最大的那个 % ---- 第二段:校正与分割(见第 4 章)---- % ---- 第三段:模板匹配识别(见第 4.5 节)----这段骨架的每个变量名都和后面章节对得上,读者可以按注释把功能函数替换进去。三个参数最需要记住:disk的半径 13 决定了背景估计的平滑尺度,图像放大一倍,半径也要跟着放大;阈值系数里的/3决定了二值化偏向亮端还是暗端,改成/2阈值会上移、笔画更容易保留但噪点也更多;连通域筛选的宽高比区间 2.2 到 4.0 是按国内蓝牌 440×140 的比例留的余量,倾斜车牌投影后宽度会被压缩,区间不能卡太死。
3. 图像预处理与车牌定位:灰度化、背景减法到长宽比筛选
3.1 灰度化与背景估计:disk 结构元素怎么定
彩色图三个通道的信息量对定位几乎没用,反而拖慢处理速度,所以第一步统一转灰度。真正决定后续成败的是背景估计这一步:自然环境下背景复杂、光照不均,直接对灰度图做二值化,车灯、玻璃反光、路面高光都会变成大片白斑。
这里用的是形态学开运算估计背景的思路。开运算是先腐蚀后膨胀,效果是抹掉比结构元素小的亮细节,保留下来的就是大尺度的明暗趋势,也就是“背景”。原图减去背景,车牌字符这种局部对比度高的部分被放大,光照的整体漂移被抵消。
Sgray = rgb2gray(Scolor); % 三通道按亮度加权合成单通道 se = strel('disk', 13); % 半径 13 的圆盘结构元素 Bgray = imopen(Sgray, se); % 开运算:抹掉小于结构元素的亮细节 Egray = imsubtract(Sgray, Bgray); % 原图减背景,输出可能为负,MATLAB 会截断到 0imsubtract遇到负值会截断为 0,所以增强后的图整体偏暗是正常的,不要以为算错了。半径的选择有个简单判据:结构元素的直径要明显大于车牌字符的笔画宽度,又要明显小于图像短边的十分之一。500×400 的图用 13 比较稳,如果换成 1000×800,半径给到 25 左右,否则背景会跟着车牌一起被减掉,车牌区域反而变黑。
3.2 自适应阈值二值化:用 max/min 三分法算阈值
二值化的关键是阈值。固定阈值(比如 0.5)在实验室图上能跑,换一张阴天拍的图就全黑或者全白,这是课程设计里最常见的翻车点之一。这里用的是按图像自身灰度分布算阈值:取增强图的最大值和最小值,从最大值往最小值方向走三分之一的位置作为分割点。
fmax1 = double(max(Egray(:))); % 增强图最大灰度 fmin1 = double(min(Egray(:))); % 增强图最小灰度 level = (fmax1 - (fmax1 - fmin1)/3) / 255; % 三分法,归一化供 im2bw 使用 bw22 = im2bw(Egray, level); % 高于阈值置 1,低于置 0 bw2 = double(bw22); % 转 double 便于后续形态学运算为什么取三分之一而不是中点?因为这个阈值偏亮端,能把字符笔画这种相对亮的像素保留下来,同时把背景大面积的中低灰度压掉,保形性更好,不会把有用的形状信息丢掉。二值图的另外两个要求是:不产生额外空缺、边缘连续。如果二值化后字符断成虚线,说明阈值偏高,把/3改成/2.5往回调一点。
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])); % 纵向细长开运算:抹掉竖直的格栅线条闭运算用横向矩形,是因为车牌框左右两条竖边之间距离大、上下两条横边之间距离小,横向闭运算能把上下边连成一条完整的框线。第三步用[19,1]这种竖直细长的结构元素做开运算,专门针对散热格栅、雨刷这类竖向细线做定向清除,这是很实用的一招,比反复调阈值有效。
| 结构元素 | 形状与尺寸 | 作用 | 调参方向 |
|---|---|---|---|
strel('disk',13) | 圆盘,半径 13 | 估计背景 | 图像变大则半径同步放大 |
strel('rectangle',[5,19]) | 高 5、宽 19 横条 | 横向闭/开运算 | 车牌框断口大则加宽 |
strel('rectangle',[19,1]) | 高 19、宽 1 竖条 | 竖向定向开运算 | 格栅粗则加大高度 |
strel('rectangle',[5,1]) | 高 5、宽 1 竖条 | 去字符内竖向毛刺 | 视噪声强度微调 |
3.4 连通域标记与长宽比筛选
形态学处理完,图上剩下若干个白色连通块,其中一块是车牌。怎么让程序自己挑出来?用区域特征参数比较,核心是面积和宽高比两个先验知识。
[L, num] = bwlabel(bg2, 8); % 8 邻域标记,返回标签图 L 和连通域个数 stats = regionprops(L, 'BoundingBox', 'Area', 'Centroid'); box = cat(1, stats.BoundingBox); % 每行 [x y width height] ratio = box(:,3) ./ box(:,4); % 宽高比 area = [stats.Area]; cand = find(ratio > 2.2 & ratio < 4.0 & area > 800); % 通常还有一步:取面积最大的候选,或者按 Centroid 是否靠图像中部做二次筛选 RGB = label2rgb(L, 'spring', 'k', 'shuffle'); % 彩色显示,便于肉眼确认这里有一个绕不过去的版本兼容问题:原始报告里写的是imfeature(L,'basic'),这个函数在新版 MATLAB 中已经被移除,直接抄会报未定义函数。替换成regionprops即可,字段名也从BoundingBox、Area变成了用字符串指定。BoundingBox的四个分量依次是左上角 x、左上角 y、宽度、高度,算宽高比时分子分母别弄反,反了会导致筛选区间整个失效。
宽高比区间不能卡太窄。车牌在图像里有轻微倾斜时,外接矩形会变宽或变高,实测 2.2 到 4.0 的区间对倾斜 10 度以内的车牌都能覆盖。面积阈值 800 是按 500×400 的图像估的,换成高分辨率图要按比例放大,否则会把车牌连同背景一起筛掉。
4. 倾斜校正、字符分割与模板匹配:从歪斜车牌到七位字符
4.1 最小二乘拟合求倾斜角:poly1 拟合怎么做
车牌装歪、摄像机没端平,定位出来的车牌子图就是斜的。斜着的车牌做投影,字符的峰谷会糊成一片,分割必然失败。所以校正放在分割之前,这一步顺序不能反。
校正的思路是找车牌上边框(或下边框)那一排白点的坐标,用一次多项式拟合出一条直线,直线的斜率换算成角度,再把整幅图反向旋转回去。相机拍出来的框线在近处会有轻微畸变,用一次拟合而不是高次拟合,是为了避免把畸变也拟合进去,导致角度过校正。
% 以车牌二值子图 sbw1 为例,逐列扫描上边框第一个白点 [h, w] = size(sbw1); xdata = []; ydata = []; for c = 1:w r = find(sbw1(:, c) == 1, 1, 'first'); % 该列最上面的白点行号 if ~isempty(r) xdata(end+1) = c; %#ok<AGROW> ydata(end+1) = r; %#ok<AGROW> end end fresult = fit(xdata', ydata', 'poly1'); % poly1 表示一次拟合 y = p1*x + p2 angle = atan(fresult.p1) * 180 / pi; % 斜率转角度,弧度换度 subcol = imrotate(subcol1, angle, 'bilinear', 'crop'); % 灰度子图旋转 sbw = imrotate(sbw1, angle, 'bilinear', 'crop'); % 二值子图同角度旋转三个参数要说明。poly1是一次多项式,fresult.p1就是斜率,atan之后乘180/pi换成角度制,num2str(angle)打到图标题上,答辩时可以直接指着说“这张图倾斜 4.7 度”。imrotate的第二参数据说正角度是逆时针,实际方向与坐标系有关,如果旋转后车牌更歪了,把角度取负即可,这是很常见的符号问题。'bilinear'是双线性插值,比最近邻平滑,'crop'保证输出尺寸与原图一致,不加'crop'会得到一张变大的图,后面的行列索引全部要重算。
另外,参与拟合的点要去掉离群值。车牌左上角如果有反光白斑,最上面的白点会跳到图像顶部,拟合出的角度能差十几度。实用的做法是拟合一次,算残差,把残差超过两倍标准差的点剔除再拟合一次,两次结果通常就稳定了。
4.2 水平投影去上下边框、取字符高度
旋转校正后,重新算水平投影。水平投影就是每一行的白点数,写成sum(sbw, 2),得到一个列向量。车牌区域在水平方向上的投影会呈现明显的双峰结构:上方一条窄峰是上边框,下方一条窄峰是下边框,中间一大段高值区是字符区。峰谷分析要做的就是找到这三段的边界。
histrow = sum(sbw, 2); % 水平投影,每行白点数 histcol = sum(sbw, 1); % 垂直投影,每列白点数 % 归一化后按阈值切分:低于均值一半视为谷,高于均值为峰 avgRow = mean(histrow); markrow = find(histrow > avgRow * 0.5); % 粗略定位有内容的行区间 rowtop = markrow(1); % 字符区上边界 rowbot = markrow(end); % 字符区下边界 sbw2 = sbw(rowtop:rowbot, :); % 去上下边框后的子图 charH = rowbot - rowtop + 1; % 字符高度阈值取均值的 0.5 倍是个经验值,因为投影分布里字符区的行数远多于边框行数,均值被字符区拉高,取一半刚好把边框这种“矮峰”排除掉。如果车牌有污损,字符区中间可能出现凹陷,导致markrow断开成两段,这时候不能直接取首尾,要先做一次一维闭运算把断口连上,或者按“最长连续区间”来取。
去完上下边框,charH这个值很关键:它既是后面判断字符是否切全的依据,也是归一化时的参考高度。蓝牌字符实际高度约 90 像素,如果你的charH只有 30 出头,说明上下边框没去干净,字符被截了一半,后面识别必然是乱码。
4.3 垂直投影找字符中心与最大字符宽度
去完上下边框,做垂直投影找每个字符的左右边界。字符在垂直方向上的投影会在字符之间的间隙处取得局部最小值,这正是垂直投影法能分割字符的原理。做法是先找每个字符的“上升点”和“下降点”,算出宽度和中心位置,再算相邻中心的间距。
histcol = sum(sbw2, 1); % 去水平边框后的垂直投影 avgCol = mean(histcol); markcol = find(histcol > avgCol * 0.5); % 有内容的列 n1 = 7; % 蓝牌 7 个字符 markcol5 = zeros(1, n1); % 每个字符的中心 for k = 1:n1 up = markcol(k); % 第 k 个字符上升点 down = markcol(k+1) - 1; % 第 k 个字符下降点 markcol5(k) = up + (down - up) / 2; % 中心位置 end dist = diff(markcol5); % 相邻字符中心距 maxs = max(dist); % 最大值通常是第 2、3 字符之间的距离(分隔点) findmax = find(dist == maxs, 1); dist(findmax) = 0; % 剔除分隔点后 maxwidth = max(dist); % 剩下最大的就是最大字符宽度findmax这一步是这套方案里比较巧的地方。国内蓝牌第二个和第三个字符之间有一个圆点分隔符,间距明显大于其他字符间距,如果直接取最大值当字符宽度,会把分隔符的空白也算进去,导致每个字符切出来都偏宽、带进邻居的笔画。把最大间距抹掉再取次大值,maxwidth就接近真实字符宽度。这个细节在报告里值得单独写一段,答辩时也容易被问到。
4.4 切图并归一化到 22×14
有了每个字符的中心和统一的最大宽度,切图就是简单的矩形裁剪加边界保护。裁剪边界越界是必现的问题:第一个字符贴着左边缘,按中心减半宽算出来的左边界可能是负数,直接索引会报错。
l = 1; [m2, n2] = size(subcol); % subcol 为校正后的车牌灰度子图 for k = (findmax-1):(findmax+5) % 从分隔符前一个字符开始,共 7 个 cleft = round(markcol5(k) - maxwidth/2); cright = round(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, [22 14]); % 统一到 22 行 14 列 imwrite(SegBw2, sprintf('sam/image%d.jpg', k), 'jpg'); % 入样本库备选 l = l + 1; endimresize到 22×14 不是为了好看,是为了让待识别字符和样本库模板尺寸完全一致,模板匹配才能逐像素相减。这个尺寸的由来是字符实际高宽比约为 2:1,22×14 是一个在计算量和保真度之间折中的选择。尺寸定下来之后,样本库里的每一张模板都必须用同一个尺寸,混用不同尺寸会让误差计算完全失去意义。
4.5 模板匹配识别:逐字符比误差
识别环节的核心是把待识别字符和样本库里的每一张模板做逐像素相减,取绝对差之和最小的那张作为结果。样本库按字符类别分文件夹组织,文件名就是字符代码。
function [label, minErr] = matchChar(SegBw2, templateDir) files = dir(fullfile(templateDir, '*.jpg')); minErr = inf; label = '?'; target = double(SegBw2(:)); % 拉成列向量,避免维度不一致 for k = 1:numel(files) tpl = imread(fullfile(templateDir, files(k).name)); tpl = imresize(tpl, [22 14]); % 双保险,防止库里有尺寸不一致的图 tpl = double(tpl(:)); err = sum(abs(target - tpl)); % L1 距离,对孤立噪点不敏感 if err < minErr minErr = err; label = files(k).name(1); % 文件名首字符即字符代码 end end end用 L1 距离而不是欧氏距离,是因为二值图上的差异多是孤立的像素翻转,平方会把少数几个大差异放大,导致判断被个别噪点带偏。返回的minErr不要扔掉,它是判断结果可信度的依据:最小误差和次小误差差距很小时,说明两个模板都对得上,这个字符大概率会判错,第 5 章会讲怎么处理。
识别错误的典型案例在原始测试里已经出现过:G 被认成 B、K 被认成 A、0 被认成 8。这三对字符的共同点是笔画骨架接近,二值化后只差一两条短线。单纯靠换距离度量救不回来,得从样本库入手,同一字符放入不同字体粗细、轻度倾斜、轻微污损的多张模板,让“类内差异”覆盖掉这些形变,最小误差才会稳定落在正确类别上。
5. 避坑与常见问题排查:五个最容易翻车的点
5.1 定位框跑到车灯或保险杠上
现象是彩色标记图上最大面积区域不是车牌,而是车灯或者保险杠上的一条亮边,后续切出来的字符完全是噪声。
原因通常是结构元素尺寸与图像分辨率不匹配。strel('disk',13)是按 500×400 左右的图定的,图像换成 1200×900 后,背景估计的结构元素相对太小,车牌框在开运算里被当成细节抹掉了;另一个原因是宽高比区间卡得太死,倾斜车牌的外接矩形比例偏离 3.14,被筛选条件排除。
解决分两步。先把disk半径按图像短边除以 40 左右重新估一遍,再重新看背景图是否只保留了大尺度明暗。然后把宽高比区间放宽到 2.0 到 4.5,改用“面积最大 + 中心点靠近图像中部”两个条件联合筛选,比单纯卡比例稳。定位这步一定要把候选区的BoundingBox打印出来,四个数字对不上肉眼看到的位置,说明标记和原图之间有坐标偏移。
5.2 换一张图二值化结果全黑或全白
现象是同一套代码在自己的测试图上正常,换成同组另一张照片就整幅变黑,字符一个都看不见。
原因就是阈值。原始代码里用的是按图像自身 max/min 算的三分法阈值,虽然比固定阈值灵活,但如果图像整体偏暗,fmax1很小,算出来的level跟着下移,噪声被大量保留;如果图里有大面积高光,fmax1被拉到接近 255,阈值上移,字符笔画全被压成黑色。
解决的办法不是反复手调/3这个系数,而是先检查增强图Egray的直方图。直方图如果挤在左侧一小段,说明背景估计减过头了,把disk半径调小;如果直方图有两个明显分离的峰,说明图本身没问题,是阈值落点不对,把三分法换成大津法graythresh作为对照。答辩时拿两张不同光照的图分别跑,把两个阈值都记在报告里,比只报一个数字可信。
5.3 倾斜没校正,投影峰谷糊成一团
现象是垂直投影图上本应出现 7 个清晰凹谷,实际是一片起伏不明显的波浪,切出来的字符框互相重叠。
原因是校正顺序搞反了,先做了投影再做旋转,或者拟合用的点里混入了离群值,算出的角度是错的,旋转之后车牌更斜。倾斜车牌在垂直方向上的投影,字符之间的间隙被拉成斜线,局部最小值消失,垂直投影法直接失效。
解决的关键在拟合点的清洗。扫描上边框白点时,把每一列第一个白点的行号记下来,先拟合一次,算所有点的残差,剔除残差超过两倍标准差的点,再拟合第二次。同时检查imrotate的角度符号,旋转后图上车牌如果朝反方向倒,取负号即可。校正完必须重新算一次水平投影和垂直投影,用新的投影图确认峰谷清晰度,这一步不能省。
5.4 字符多切一块或少切一块,识别结果整体错位
现象是切出来的字符数量不是 7,或者切出来的图里带着相邻字符的半条笔画,识别结果整体右移一位。
原因有两个。一是maxwidth取值被分隔符污染,第二个字符和第三个字符之间的圆点间距被当成字符宽度,导致每个切割窗口都过宽;二是起始索引findmax-1算错,findmax是diff结果的索引,diff之后数组长度比中心点少 1,差一位就会整体偏移。
解决方法是把切出来的每一块都用subplot排成一行显示出来,肉眼数一遍。如果发现窗口过宽,检查剔除最大间距的逻辑是否生效;如果发现整体偏移,把findmax的取值打印出来,与手工数的分隔符位置对比。另外切割窗口的右边界建议用markcol5(k) + maxwidth/2 - 2,减 2 个像素是为了避开相邻字符的边缘,这个余量在小尺寸图上很关键。
5.5 换一批实拍图识别率断崖式下跌
现象是课程设计验收时用自己准备的图能到 90% 以上,老师随手拿手机拍的一张,识别结果全是乱码。
原因是样本库太“干净”。如果模板全部来自同一张测试图切割出来的字符,样本库实际上记住了那张图的字体粗细、边缘锐度、二值化残留,换一张光照不同、分辨率不同的图,待识别字符的像素分布与模板差得很远,最小误差也会很大。
解决办法是在建库阶段就引入差异。同一个字符至少放三张模板:正常一张、轻微倾斜一张、笔画偏细一张,来源尽量是不同图片。匹配时优先看最小误差与次小误差的比值,比值接近 1 就标记为“低置信度”,在最终输出里用问号占位而不是硬猜。这个处理在报告里写出来,比单纯报一个识别率数字更能体现工程思维。
6. 验收与答辩:把识别率、参数和失败案例摆到台面上
验收现场最怕的不是识别错,而是说不出为什么错。我一般会要求学生准备三样东西:一组按条件分类的测试图、一张参数记录表、一份失败案例说明。测试图至少分三类——正常光照、逆光或阴影、车牌轻微倾斜,每类不少于 3 张,文件名里带条件标签,跑完直接统计每类的识别率,而不是只报一个笼统的总数。
参数记录表按处理段列,把每一步的关键参数和取值写在表里,答辩时被问到“这个 13 是怎么来的”,能直接指到对应的行。
| 验收项 | 合格标准 | 举证方式 |
|---|---|---|
| 车牌定位 | 正常光照图定位正确率不低于 80% | 每张图的彩色标记图与BoundingBox数值 |
| 倾斜校正 | 倾斜 10 度以内的车牌校正后投影峰谷清晰 | 旋转前后的水平投影对比图,角度值打到标题上 |
| 字符分割 | 单张车牌稳定切出 7 个 22×14 子图 | 7 个子图排成一行显示 |
| 字符识别 | 正常光照图整牌识别正确率不低于 70% | 识别结果字符串与人工标注对照表 |
| 失败分析 | 至少 3 个错误案例,说明错在哪一段 | 逐段中间图 + 最小误差与次小误差数值 |
| 兼容性说明 | 说明本方案适用的车牌类型与调整方向 | 蓝牌、黑牌、黄牌的算法差异说明 |
答辩陈述有个好用的结构:先用一张倾斜车牌的原图开场,说这张图难在哪;然后按三段流水线走一遍,每段只讲一个关键参数和一个中间图;最后主动拿出失败案例,说明错在 G 与 B 的骨架太接近,以及下一步打算怎么通过扩充模板解决。主动暴露问题比被问出来强得多,老师追问的空间也小。
还有一个常被忽略的点:把minErr和次小误差的比值做成一张散点图,横轴是样本序号,纵轴是比值,正确识别的点聚在低比值区,错误识别的点散在高比值区,这张图能直观说明“系统知道自己什么时候不确定”。答辩时这张图往往比识别率数字更抓人。
从那以后我每次带人做这套东西,都强制先跑通一张图、把每段的中间图存成单独文件,再去调参数,绝不在一张图上同时改三个变量。希望帮到你。
本文还有配套的精品资源,点击获取