简介:面向MATLAB图像处理课程设计、计算机视觉入门学习者与初级项目实践者的车牌识别项目,采用模板匹配完成车牌定位与字符识别,可用于智能停车、交通管理等场景的教学演示与课程作业复现。包内围绕主流程调度、车牌预处理与分割、单字符切割、模板匹配识别及GUI交互配置等模块组织,以.m脚本和.fig界面文件为主;文件总数显示为0,类型明细暂缺,压缩包约156KB。已有2047人学习,说明其作为模板匹配教学案例具有一定参考度。读者可据此梳理从图像预处理、车牌分割到字符逐个匹配的完整链路,理解相似度比较与最佳匹配选取的实现思路,并借助GUI完成图片上传与结果查看,适合作为课程设计原型和算法入门拆解材料。
1. 从一张地下车库抓拍图说起:模板匹配凭什么还能用在车牌识别上
一张地下车库的抓拍图,车灯过曝、车牌轻微倾斜、背景里还横着半块广告牌,这种图丢进任何一条识别链路都会让人心里打鼓。基于 MATLAB 模板匹配的车牌识别,恰恰是为这种“不想上模型、又要有可解释结果”的场景准备的:把车牌从背景里框出来,切成单个字符,再拿预先做好的字符模板逐个比对打分,输出置信度最高的一类。它不需要训练,不需要显卡,一套脚本加一个模板库就能跑;代价是模板库要自己攒,光照和倾斜一变就得回头调参数。适合做课程设计、产线小工具、算法基线,以及那些需要对每个识别结果说清楚“为什么是它”的场合。下面按定位、分割、建库、匹配、排查、验证六步走一遍。
2. 车牌定位:颜色阈值和形态学闭运算的先后顺序
2.1 蓝底车牌的 HSV 阈值怎么定
定位这一步决定了后面所有环节的上限。车牌框歪了、缺了一角,字符分割再精巧也救不回来。工程上最稳的起手式不是深度学习,而是颜色加形状:国内常见蓝底白字车牌,底色在 HSV 空间里有相当集中的分布,先把蓝色区域抠出来,再用宽高比筛一遍,绝大多数干净场景就能拿到候选框。
不用 RGB 而用 HSV,理由很实际。RGB 三个通道都随光照强度一起变,同一块蓝底在正午和地库里三通道数值能差出一倍;HSV 把色调 H 单独拎出来,亮度变化主要落在 V 上,H 相对稳定。蓝底车牌的 H 在 MATLAB 的rgb2hsv输出里乘 360 之后,通常落在 200 到 250 之间,饱和度 S 不会太低,明度 V 在正常曝光下也不会贴地。
% 读图并转到 HSV 空间,H 换算成 0-360 度便于设阈值 I = imread('car_01.jpg'); hsv = rgb2hsv(I); H = hsv(:,:,1) * 360; S = hsv(:,:,2); V = hsv(:,:,3); % 蓝色底色掩膜:H 卡区间,S 和 V 设下限过滤灰白区域 mask = (H > 200 & H < 250) & (S > 0.35) & (V > 0.20); % 先开运算去掉零散噪点,再闭运算把字符和边框连成整块 mask = imopen(mask, strel('rectangle', [3 3])); mask = imclose(mask, strel('rectangle', [7 21])); mask = imfill(mask, 'holes');这段代码里三个参数最值得动手调。S > 0.35是饱和度下限,定得太低,灰色路面和白色车体也会被拉进来;定得太高,阴天或逆光下蓝底褪色,车牌又漏掉。V > 0.20管的是暗部,夜间补光过曝的图 V 会顶到 1,但底色反而偏白,这时单靠颜色会失效,得靠下一节的边缘通道兜底。闭运算的[7 21]是竖向 7、横向 21 的长条结构元,因为车牌上的字符是横向排布的,横向多连一点才能把“X 1 2 3 4 5”连成一个连通域;这个尺寸跟你的图像分辨率强相关,图越小结构元要同比缩小。
提示:阈值不要照抄。拿二十张自己场景的图,把每张的 H、S、V 在车牌区域取中位数打印出来,看分布的重叠区在哪,再定区间,比任何教程给的数字都准。
2.2 边缘通道与形态学闭运算的候选框筛选
颜色通道在逆光、夜间、车牌脏污时经常整块丢失,所以我一般同时跑一条边缘通道,两条结果取并集再做筛选。边缘通道的思路是:车牌区域字符密集,Sobel 或 Canny 响应强,用横向长条的结构元做闭运算,能把字符之间的边缘连成一个矩形块,而路面、车身这种大面积平滑区域几乎不产生响应。
% 灰度化后做 Sobel 边缘,横向结构元闭运算连成块 gray = rgb2gray(I); bwE = edge(gray, 'sobel', 0.12); bwE = imclose(bwE, strel('rectangle', [5 25])); bwE = imfill(bwE, 'holes'); bwE = bwareaopen(bwE, 400); % 两路掩膜合并,交给连通域筛选 cand = mask | bwE; cand = imclose(cand, strel('rectangle', [5 15])); cand = bwareaopen(cand, 600);edge的第三个参数是阈值,0.12 是我在 720p 抓拍图上常用的起点,噪声大的图往上抬到 0.18,纹理弱的图降到 0.08。bwareaopen的 400 是面积下限,用来干掉路灯、反光条这类小块响应。两路合并之后再做一次轻量闭运算,是为了把颜色通道和边缘通道各自漏掉的一半拼起来。
拿到候选掩膜,下一步是按几何约束筛框。regionprops的BoundingBox返回[x y w h],直接算宽高比和面积占比:
stats = regionprops(cand, 'BoundingBox', 'Area', 'Extent'); boxes = cat(1, stats.BoundingBox); areas = cat(1, stats.Area); ratio = boxes(:,3) ./ boxes(:,4); % 宽高比 ext = cat(1, stats.Extent); % 连通域填充率 keep = ratio > 2.2 & ratio < 4.8 & ... % 车牌宽高比经验区间 areas > 800 & areas < 30000 & ...% 面积上下限 ext > 0.45; % 太稀疏的框不要 boxes = boxes(keep, :);宽高比 2.2 到 4.8 覆盖了单层蓝牌和部分新能源牌的形变区间;面积上下限要按你的图像分辨率换算,我给的数字对应 1280×720 左右的图。Extent是连通域面积与外接矩形面积之比,车牌框里字符和底色填得比较满,这个值通常在 0.6 以上,而树枝、栏杆这类细长目标填充率很低,用它能把误检压下去一截。
2.3 定位框外扩、裁剪与尺寸归一
筛完之后如果还剩多个候选,按面积从大到小排,取第一个,或者取宽高比最接近 3.14 的那个。真实车牌框经常贴得太紧,字符的上下边缘被切掉一两个像素,所以裁剪时统一向外扩 5 到 8 个像素,再做边界保护。
% 取面积最大的候选框,向外扩 6 像素后裁剪 [~, idx] = max(areas(keep)); b = boxes(idx, :); pad = 6; x1 = max(1, round(b(1)) - pad); y1 = max(1, round(b(2)) - pad); x2 = min(size(I,2), round(b(1) + b(3)) + pad); y2 = min(size(I,1), round(b(2) + b(4)) + pad); plate = I(y1:y2, x1:x2); % 统一到固定宽度,后续分割参数才好写死 plate = imresize(plate, [80 240]);外扩 6 像素是个折中:扩太少,字符边缘缺失会让二值化后的笔画断开;扩太多,车牌边框和车身会被带进来,投影法分割时多出两三个假波峰。统一缩放到 80×240 也不是随便定的,它让后面所有分割阈值、结构元尺寸、模板尺寸都变成常量,换一张分辨率不同的图不用重调一遍。这一步的代价是引入了插值模糊,imresize默认双三次,对二值化影响不大,但如果你的原图本身就很糊,建议改成'bilinear'减少振铃。
到这里定位链路就闭环了:HSV 掩膜和边缘掩膜各自出候选,合并后按宽高比、面积、填充率筛,最后外扩裁剪加缩放。这套参数在我手上的干净抓拍图上召回率能到九成以上,剩下的失败基本集中在夜间过曝和车牌严重污损两类,那属于另一条技术路线要解决的问题了。
3. 字符分割:垂直投影为什么会在第二刀翻车
3.1 灰度化、去噪与二值化的三个阈值
分割的目标是把 80×240 的车牌条切成 7 个字符块(省份汉字加 6 位字母数字),每个块单独归一化后送进匹配器。这一步最常用的方法是垂直投影:字符是亮的、底色是暗的,把二值图按列求和,字符所在列的和值高,字符间隙的和值低,找波谷就找到了切分点。
前提是二值图得干净。蓝底白字车牌灰度化之后,白字灰度在 200 以上,蓝底在 80 到 120 之间,理论上 Otsu 就能分开。实际翻车最多的是两种情况:一是车牌表面有反光,白字和反光斑连成一片;二是边框没去干净,左右两条竖边在投影图上形成两个巨大的假峰,把真正的字符峰淹掉。
% 灰度化 + 对比度拉伸 + 中值滤波压噪 g = rgb2gray(plate); g = imadjust(g, stretchlim(g, 0.02), []); g = medfilt2(g, [3 3]); % Otsu 全局阈值,白字为前景 bw = imbinarize(g, 'global'); % 去掉贴边的细长连通域,多数是车牌边框残留 bw = imclearborder(bw); bw = bwareaopen(bw, 30);stretchlim的 0.02 是百分位裁剪,把最暗和最亮的 2% 像素截掉再做拉伸,能明显改善逆光图的对比度。medfilt2用 3×3 窗口,窗口再大就把细笔画磨没了。imclearborder专门对付贴边连通域,车牌左右边框通常和图像边界相连,这一步能清掉大半;代价是如果某个字符恰好贴边,也会被一起清掉,所以裁剪阶段的外扩要留够。
注意:如果整张图的光照很不均匀,比如一半在阴影里一半在阳光下,全局 Otsu 会切出半张黑半张白的二值图。这时换成
imbinarize(g,'adaptive','ForegroundPolarity','light','Sensitivity',0.45),用局部窗口算阈值,代价是噪点变多,后面要补一次开运算。
3.2 垂直投影的波谷判定与宽度过滤
二值图干净之后,投影就很好做了。按列求和得到一条曲线,字符区域的列和值高,间隙的列和值接近零。判定切分点不能简单用“和值大于零”,因为噪点会让间隙列也有零星响应,所以设一个相对阈值:最大列和值的 15% 到 25%。
% 垂直投影,按最大值的比例设分割阈值 proj = sum(bw, 1); thr = 0.18 * max(proj); isChar = proj > thr; % 差分找上升沿和下降沿,得到每段的起止列 d = diff([0 isChar 0]); starts = find(d == 1); ends = find(d == -1) - 1; widths = ends - starts + 1; % 按字符宽度过滤:太窄是噪点,太宽是两个字粘在一起 valid = widths > 6 & widths < 30; starts = starts(valid); ends = ends(valid);0.18这个系数决定了切分的松紧。调高,字符内部的细笔画会被当成间隙切断,一个“8”切成两半;调低,两个相邻字符之间的窄缝被填平,变成一块。字符宽度 6 到 30 像素对应 240 宽的车牌条,正常单字符宽度在 18 到 26 之间,汉字略宽。如果过滤后发现有效段超过 7 个,说明有噪点被当成字符,按宽度从大到小取前 7 个;少于 7 个,说明有粘连,得回头检查二值化。
粘连是投影法最典型的翻车点。两个字符中间那列如果因为模糊或污渍导致和值没有降到阈值以下,就会被合并成一段,宽度直接翻倍。工程上的补救办法有两种:一是对超宽段做二次投影,把阈值再抬高 10% 重切;二是对超宽段直接按宽度均分,虽然粗暴,但对固定字体的车牌意外地有效。
% 对超宽段做二次切分:抬高阈值重投影,失败则均分 for k = 1:numel(starts) w = ends(k) - starts(k) + 1; if w > 30 sub = bw(:, starts(k):ends(k)); p2 = sum(sub, 1); t2 = 0.30 * max(p2); cut = find(p2 < t2, 1, 'first'); if isempty(cut) cut = round(w / 2); % 找不到谷底就均分 end % 用 cut 把这一段拆成两段,写回 starts/ends end end这段二次切分的逻辑不复杂,重点在于“先抬阈值找谷底,找不到再均分”这个兜底顺序。抬阈值能把大部分粘连切开,均分只在前者失败时兜底,不至于把本来正常的一段硬切成两半。
3.3 倾斜校正与字符归一化到 40×20
车牌在抓拍图里几乎不可能完全水平,倾斜 3 到 5 度很常见。倾斜对定位影响不大,对分割是致命的:投影的波谷会随着倾斜变宽变浅,本来清晰的一条缝变成一片缓坡,阈值法切不准位置,字符块里会带上邻居的半条笔画。
校正在二值图上做,用 Radon 变换扫角度。把二值图在 -8 到 8 度之间做投影,投影能量最集中的角度就是车牌长边的垂直方向,据此反推倾斜角。
% 在 ±8 度内做 Radon 扫描,取能量最集中的角度 theta = -8:0.25:8; R = radon(bw, theta); energy = sum(R .^ 2, 1); [~, mi] = max(energy); tilt = theta(mi); % 反向旋转校正,crop 保证输出尺寸不变 if abs(tilt) > 0.5 bw = imrotate(bw, tilt, 'bilinear', 'crop'); end这里的正负号跟图像坐标系有关,第一次用务必拿一张已知倾斜方向的图校准一次,别直接照抄。'crop'参数保证旋转后尺寸不变,避免后续按固定坐标裁剪时越界。校正完再跑一遍 3.2 的投影,切分准确率会明显不一样,这是我自己踩过坑之后固定下来的顺序:先校正,再投影,不要颠倒。
切出来的每个字符块尺寸不一,送进模板匹配之前必须归一化。统一到 40 高、20 宽,用双三次插值,同时做一次居中:把字符的外接矩形缩放到刚好填满 36×18,再贴到 40×20 的画布中央,四周留 2 像素白边。
% 单个字符块归一化:紧致裁剪后缩放并居中到 40x20 function ch = norm_char(bwImg) [r, c] = find(bwImg); if isempty(r), ch = false(40, 20); return; end crop = bwImg(min(r):max(r), min(c):max(c)); crop = imresize(crop, [36 18]); ch = false(40, 20); ch(3:38, 2:19) = crop; end居中这一步看起来多余,实际上直接决定了匹配分数可不可比。不居中的话,同一个字符在不同图里可能偏左偏右,corr2算出来的相关系数会凭空掉一截,阈值就没法统一设了。缩放比例用[36 18]而不是直接[40 20],是为了留边,防止旋转或插值把笔画推到画布外面被截断。
4. 模板库与匹配打分:corr2、SSD 和多模板投票怎么选
4.1 模板库的字符集、样本数和切割规范
模板匹配的上限由模板库决定,这句话怎么强调都不过分。字符集先定清楚:数字 0 到 9 共 10 类,字母去掉 I 和 O(容易和 1、0 混淆)共 24 类,两者合计 34 类;省份简称汉字单独一套,31 类。如果项目只要求识别后 6 位,汉字那套可以先不做,能省掉一大半工作量,因为汉字笔画复杂,模板匹配的准确率天然比字母数字低一截。
每类准备多少张模板?我的经验是每个字符 15 到 30 张,覆盖不同光照、不同字体粗细、轻微形变和 ±3 度的残余倾斜。来源就是你自己场景里的真实车牌,切割、归一化到 40×20,人工标注文件名。不要拿电脑字体渲染出来的图当模板,渲染图和实拍图在笔画边缘的模糊程度、反光分布上差得太远,用渲染模板去匹配实拍字符,分数分布会整体偏移一大截。
模板文件命名要能直接解析出类别,比如tpl_A_003.png,加载时按类别分组存进 cell 数组:
% 按文件名前缀加载模板,分组成 cell:classes{k} 存第 k 类的所有模板 files = dir(fullfile('templates', '*.png')); labels = strings(numel(files), 1); for k = 1:numel(files) [~, name] = fileparts(files(k).name); labels(k) = extractBetween(name, "tpl_", "_"); end uniq = unique(labels); classes = cell(numel(uniq), 1); for k = 1:numel(uniq) idx = find(labels == uniq(k)); classes{k} = cellfun(@(f) imread(fullfile('templates', f)), ... {files(idx).name}, 'UniformOutput', false); end存成 cell 的嵌套结构而不是三维数组,是因为每类模板数量不一样,三维数组得补齐,浪费内存还容易在索引时出错。加载完之后建议打印一下每类的模板数,少于 10 张的类别要补样本,否则那一类的匹配分数会明显偏低,形成系统性误判。
4.2 归一化互相关与 SSD 的取舍
单个字符和单张模板都是 40×20 的二值图,比对方式无非两类:逐像素差值的平方和(SSD),和零均值归一化相关系数(corr2)。两者的差别在于对亮度变化的敏感度。
SSD 直接算对应像素差,二值图下等价于汉明距离,对笔画粗细、边缘偏移非常敏感。字符稍微粗一点,SSD 就涨一大截,所以它适合模板和待测样本来自同一批设备、同一套预处理参数的场景。corr2先各自减去均值再算相关系数,对整体亮度偏移不敏感,笔画粗细变化的影响也被摊薄,代价是相似字符之间的区分度会下降,比如“8”和“B”的相关系数经常只差 0.02。
% 单个字符与某一类所有模板逐一比对,返回该类的最高分 function s = score_class(ch, tpls, mode) s = -inf; for k = 1:numel(tpls) t = logical(tpls{k}); if ~isequal(size(ch), size(t)) t = imresize(t, size(ch)); % 尺寸兜底,正常不该走到这 end switch mode case 'corr' v = corr2(double(ch), double(t)); case 'ssd' v = -sum((double(ch(:)) - double(t(:))).^2); % 取负,越大越好 end s = max(s, v); end endmode参数留着切换,实际项目里我会先用corr跑一遍看混淆矩阵,如果相似字符对(8/B、0/D、5/S、2/Z)的误判占比超过两成,再切到ssd对比,选整体准确率高的那个。尺寸兜底那行是防御性代码,正常流程里所有模板和字符都归一化到 40×20,走不到;但模板库手工维护,难免混进一张尺寸不对的图,corr2遇到尺寸不一致会直接报错,加一行兜底能省掉排查时间。
4.3 多模板打分、Top-K 候选与拒识阈值
一类多模板的合并策略很简单:取最大值。同一个字符在不同光照下的样子差异很大,但只要模板库覆盖到了,总有一张能对上,取最大就是把“最像的那张”作为该类的得分。34 类各算一个最高分,排序取第一,就是识别结果。
光有结果不够,还得有置信度。我一般同时输出 Top-3 和第一、第二名的分差:
% 对所有类别打分,排序输出 Top-3 和首两名分差 scores = zeros(numel(classes), 1); for k = 1:numel(classes) scores(k) = score_class(ch, classes{k}, 'corr'); end [sorted, order] = sort(scores, 'descend'); top3 = order(1:3); margin = sorted(1) - sorted(2); % 分差,越小越可疑margin是这套流程里最有用的一个数。第一名和第二名分差小于 0.05 时,基本可以判定为相似字符混淆,这时与其硬给一个结果,不如标记为“待复核”,交给人工或者第二级特征(笔画数、孔洞数)再判一次。拒识阈值定在多少合适?拿你的验证集跑一遍,把分差和是否正确的关系画出来,找一个既能把错误挡掉、又不会拒掉太多正确样本的点,通常落在 0.04 到 0.08 之间。
多模板带来的一个副作用是计算量。34 类、每类 20 张模板,一个字符要算 680 次corr2,7 个字符就是 4760 次,corr2内部是双层循环,MATLAB 里跑起来并不快。优化办法是把模板展平成矩阵,一次性做矩阵乘法算相关系数,能把耗时压掉一个数量级;但那是后话,先跑通再优化,别一上来就为了性能把代码写得没法调试。
5. 排查:模板匹配车牌识别最常见的五个翻车点
5.1 定位与二值化阶段的两个坑
第一个坑:夜间和逆光图整块定位丢失。现象是regionprops筛完之后boxes为空,代码在取最大值那行报索引越界。原因是 HSV 阈值里的V > 0.20和S > 0.35在过曝图里同时失效——补光灯把蓝底照得发白,S 掉到 0.1 以下,颜色通道整个空掉;边缘通道那边因为过曝,字符和底色的梯度也被压平,Sobel 响应弱到过不了阈值。解决办法不是把阈值往下调,那样误检会爆炸,而是给边缘通道单独降门槛:把edge的阈值从 0.12 降到 0.06,同时把闭运算结构元从[5 25]放大到[7 31],靠形态学把断掉的边缘重新连起来。更稳的做法是对图像先做一次imadjust或 CLAHE 局部增强再进边缘通道,但会增加一道预处理,看你的实时性要求。
第二个坑:二值化后车牌边框残留,投影多出两个假峰。现象是分割出来的有效段有 9 到 10 个,左右两边各多一块窄条,宽度在 6 到 10 像素之间,刚好卡在宽度过滤的下限附近没被筛掉。原因是imclearborder只能清掉与图像边界相连的连通域,车牌裁剪时外扩了 6 像素,边框被包在图像内部,imclearborder管不着。解决分两步:一是在二值化之后加一次形态学开运算,结构元用strel('rectangle',[2 2]),把细边框磨掉,注意窗口不能大,大了字符笔画也断;二是在宽度过滤里把下限从 6 提到 10,边框条的宽度通常不超过 8,正常字符最窄也有 12 以上,这个区分度是够的。
提示:这两个坑的共同点是“参数在干净图上没问题,换一批图就崩”。所以调参时一定要拿至少三类图(白天、夜间、逆光)各十张一起跑,只在单类图上调到最优,上线必翻车。
5.2 分割与匹配阶段的三个坑
第三个坑:corr2报“矩阵尺寸不一致”。现象是跑批量测试时中途崩掉,报错指向corr2那一行。原因是有张模板图的尺寸不是 40×20——手工切割模板时,imresize的目标尺寸写错,或者某张图保存时被别的工具重新编码过。这类问题最烦的地方是它只在特定图片上触发,跑前几十张都正常,很容易误以为是数据问题。解决是在加载模板时加一道断言,尺寸不对直接打印文件名并跳过,而不是等到匹配阶段才崩:
% 加载时校验尺寸,不合格的模板打印文件名后剔除 for k = 1:numel(classes) ok = cellfun(@(t) isequal(size(t), [40 20]), classes{k}); if any(~ok) fprintf('尺寸异常模板:第 %d 类,共 %d 张\n', k, sum(~ok)); classes{k} = classes{k}(ok); end end第四个坑:相似字符系统性混淆,8 认成 B、0 认成 D。现象是混淆矩阵里这两对占错误总数的六成以上,而且分差普遍很小。原因是二值化之后,字母和数字的拓扑结构高度相似,corr2又是全局相关,区分不了局部差异。解决办法有两个层次:轻量的是加结构特征做二次判决,统计字符的孔洞数(bweuler或regionprops的EulerNumber)和笔画端点,8 有两个孔洞、B 也是两个,这一招对 8/B 无效,但对 0/D 有效(0 一个孔洞、D 也是一个,还是无效);真正有效的是扩模板库,专门针对易混字符补样本,把不同字体、不同粗细的 8 和 B 都收进去,让类内方差覆盖住类间差异,同时把拒识阈值调紧,分差小于 0.06 的直接标待复核。
第五个坑:倾斜导致字符切分整体错位一格。现象是识别结果整体右移或左移一位,比如真实是“X12345”,输出成“12345X”或者“XX1234”。原因是没做倾斜校正,或者校正角度算反了符号,投影波谷位置整体偏移,第一个字符块里混进了边框,最后一个字符被挤出有效宽度。判断方法很直接:把分割结果的可视化图打出来看,如果每个字符块里都带着邻居的半条边,就是错位。解决就是回到 3.3 的 Radon 校正,并且用一张已知倾斜 3 度的合成图验证符号方向,别靠猜。
6. 把“看着还行”变成可测量:验证集、混淆矩阵与拒识阈值
调通流程之后,最危险的状态是“随手拿几张图跑,看起来都对”。模板匹配的参数多,阈值之间互相耦合,凭肉眼判断很容易把过拟合当成调好了。我现在的习惯是先固定一个验证集再动手调参:200 到 300 张图,覆盖白天、夜间、逆光、倾斜、污损五类,每张人工标注车牌框位置和真实字符序列,存成 CSV。指标分四级统计,缺一级都会漏掉问题:
| 指标 | 计算方式 | 我一般能到的水平 | 低于这个值先查什么 |
|---|---|---|---|
| 定位命中率 | 预测框与标注框 IoU > 0.5 记为命中 | 88% 到 94% | 夜间样本的颜色阈值 |
| 分割正确率 | 切出 7 段且边界与标注偏差 < 3 像素 | 85% 到 92% | 倾斜校正和宽度过滤 |
| 单字符准确率 | 逐字符比对,含汉字 | 92% 到 97% | 相似字符的模板覆盖 |
| 整牌准确率 | 7 位全对才算对 | 75% 到 88% | 前三级里最弱的那一环 |
整牌准确率永远明显低于单字符准确率,这是乘法效应:7 位里每位 95%,整牌就只有 70%。所以优化的时候不要盯着单字符准确率,要看整牌,并且优先修最弱的那一环。定位命中率不到 90% 时去调匹配阈值,纯属白费力气。
混淆矩阵用confusionmat直接算,重点看两件事:对角线以外最大的那格是哪两个类在混,以及被拒识的样本里正确结果排第二的比例。前者决定要不要补模板,后者决定拒识阈值是松了还是紧了。
% 统计混淆矩阵与拒识率,gt 和 pred 都是 categorical cm = confusionmat(gt, pred); acc = sum(diag(cm)) / sum(cm(:)); % 按分差阈值扫描拒识率与错误率的关系 for t = 0.02:0.01:0.10 rejected = margin < t; errRate = sum(~rejected & ~correct) / sum(~rejected); rejRate = sum(rejected) / numel(rejected); fprintf('阈值 %.2f:拒识率 %.3f,剩余错误率 %.3f\n', t, rejRate, errRate); end这段扫描跑完,你会看到一条典型的权衡曲线:阈值从 0.02 往上抬,拒识率线性涨,剩余错误率快速下降然后趋缓。拐点通常在 0.05 到 0.07 之间,取拐点稍右一点的位置,既能把大部分相似字符混淆挡下来,又不会拒掉太多正常样本。这个阈值跟你的模板库质量强相关,模板补得越全,可以放得越松。
最后说一个我自己交过学费的地方。早期做这套东西的时候,我把模板库和测试集从同一批图里切,准确率刷到 96%,换一批新图直接掉到 71%。后来固定了“模板图绝不进测试集”的规矩,哪怕某一类模板不够,也去补新图而不是从测试集里挪。模板匹配这套方法本身不复杂,难的是把数据边界守干净。希望帮到你。
本文还有配套的精品资源,点击获取