首页 / 资讯中心 / 文章详情

Matlab车牌识别全链路拆解:预处理、定位、切割与模板匹配

Matlab车牌识别全链路拆解:预处理、定位、切割与模板匹配 ★ FEATURED ARTICLE
简介围绕Matlab车牌识别课程设计整理的一份docx报告面向信息科学、自动化等专业需要完成课程设计或图像处理入门实践的学生帮助梳理从车辆检测、图像采集到牌照定位、字符分割与号码输出的完整实现思路。文档共1个文件为单份docx压缩包约1.37MB内含设计目的、设计原理、详细步骤、结果分析、总结体会与参考文献等模块并附源代码可对照图像灰度化、开运算背景提取、阈值二值化、垂直投影分割及模板匹配或神经网络识别等环节理解算法流程。已有116人学习适合用于课程设计选题分析、报告结构参考、算法复现与答辩准备也能为不同光照、车牌质量等条件下的测试和识别率优化提供思路报告中的进度安排与教学要求也便于按周推进。1. 一张逆光车尾照暴露了这套 Matlab 车牌识别课设的全部短板去年帮人看一份课程设计对方发来一张地库出口拍的车尾照蓝底白字逆光车牌右半边糊成一团白。跑完他的 Matlab 车牌识别脚本输出结果是「京A·8B8Q1」——七个字符里错了三个。翻代码才发现二值化阈值是拿全图最大最小灰度做三分法算的样本库路径写死在 F 盘的 work 目录里字符切割的下标还硬编码成findmax-1:findmax5。这套东西本身不复杂链路就六步读图、灰度化、背景相减增强、二值化、Canny 边缘加形态学滤波、定位后投影分割再模板匹配。六七年前高校课设里这个题目出镜率极高现在拿来当图像处理的入门实战依然合适——因为它把「预处理怎么影响后面每一步」这件事暴露得特别彻底你调错一个结构元尺寸后面全盘皆输。适合刚上手 Matlab 图像工具箱、想搞明白一次完整识别链路怎么落地的人。2. 整体架构与运行环境六个环节串成一条从像素到字符串的链路2.1 三段式结构与六个处理环节从工程视角看这套系统按报告里的说法分三块车辆检测、图像采集、牌照识别。但课程设计实际交付的代码只做了第三块前两块是文字描述。真正跑起来的是下面这条链路每一环的输出都是下一环的输入中间任何一环崩掉后面就是垃圾进垃圾出环节输入核心函数输出图像预处理彩色原图rgb2gray、imopen、imsubtract增强后的灰度图二值化增强灰度图im2bw 自算阈值黑白二值图边缘与滤波二值图edge(...,canny)、imclose、imopen连通的车牌候选块车牌定位候选块bwlabel、regionprops、宽高比筛选车牌子图倾斜校正与切割车牌子图水平/垂直投影、fit、imresize7 张 22×14 字符图字符识别字符图与样本库逐张相减取最小误差车牌字符串这里面有个容易被忽略的设计取舍定位和分割全部依赖投影。水平投影用来找车牌的上下边框和字符行高垂直投影用来找每个字符的中心和宽度。投影法的好处是不需要训练、可解释性强坏处是对倾斜极其敏感——车牌歪三度投影的峰谷就开始糊所以链路里必须插一步倾斜校正这一步在很多简化版课设里被省掉了然后定位阶段就开始玄学。2.2 文件与目录约定原始交付是一个 docx 报告加一份脚本脚本是流水账式的从头到尾一个文件。样本库靠代码里imwrite自己往外写路径是写死的绝对路径。我把它整理成能复现的结构大概是这样plate_recognition/ ├── main.m # 主流程按环节顺序调用 ├── preprocess.m # 灰度化、背景估计、增强、二值化 ├── locate_plate.m # 边缘、形态学、连通域、宽高比筛选 ├── rectify_segment.m # 投影分析、倾斜校正、字符切割 ├── recognize.m # 与样本库匹配输出字符 ├── samples/ # 字符样本库按 0-9 A-Z 汉字 分目录 │ ├── 0/ 1/ ... 9/ │ ├── A/ B/ ... Z/ │ └── han/ # 汉字省份简称 ├── images/ # 待测原图 └── output/ # 中间结果图、识别日志样本库这一步值得单独说。报告里原始做法是「切割出来的字符图命名成 imageK.jpg 直接扔进同一个文件夹」靠文件名顺序去猜它是什么字符。这在课堂演示里能跑因为演示的图就是训练的那张。换成第二张图就完蛋——所以样本目录必须按字符分类一个字符一个子目录识别时遍历目录名当标签用。2.3 环境准备与最小可运行脚本Matlab 这边没有复杂依赖Image Processing Toolbox 是必须的bwlabel、strel、edge、imresize、fit都在里面。版本上要注意 2013b 之后imfeature已经被移除原始报告里的Feastats imfeature(L,basic)在新版本上会直接报未定义函数必须换成regionprops。这个坑后面第 5 章会展开。最小可运行主流程长这样% main.m —— 主流程骨架每一步都能单独打断点看图 clear; close all; clc; imgPath fullfile(pwd, images, car01.jpg); Scolor imread(imgPath); % 读入彩色原图 if size(Scolor,3) 3 Sgray rgb2gray(Scolor); % 统一转灰度避免后续维度判断 else Sgray Scolor; end % 大图先缩小控制结构元半径与车牌实际像素宽度的比例 if size(Sgray,2) 800 Sgray imresize(Sgray, 800/size(Sgray,2)); end Egray preprocess(Sgray); % 背景相减增强后的灰度图 bw binarize_plate(Egray); % 二值化 grd edge(bw, canny); % 边缘提取 % 形态学滤波先闭后开把字符连成块、把噪点抹掉 bg1 imclose(grd, strel(rectangle,[5,19])); bg3 imopen(bg1, strel(rectangle,[5,19])); bg2 imopen(bg3, strel(rectangle,[19,1])); [plateImg, ok] locate_plate(bg2, Sgray); % 定位ok 标记是否找到 if ~ok warning(未定位到车牌区域检查阈值和结构元尺寸); return; end chars rectify_segment(plateImg); % 校正 切割返回 cell result recognize(chars, fullfile(pwd,samples)); fprintf(识别结果%s\n, result);这段代码几个点size(Scolor,3)3的判断是防着有人直接拿灰度图测试导致后面对不上imresize到固定宽度是我加的原始代码没有但这一步直接决定了后面strel(disk,13)的半径是不是合理locate_plate返回ok而不是直接error是为了批量跑图时一张失败不至于中断整批。3. 预处理链路灰度化、背景相减、二值化和 Canny 边缘3.1 读图与灰度化彩色转灰度这一步是零成本但报告里给的理由不太准确——它说「彩色图像存储开销大、处理速度慢」真实原因更简单后面所有操作阈值、边缘、投影都是单通道运算彩色图你没法定义「一个像素的灰度值」也就没法做阈值分割。转灰度的公式rgb2gray用的是加权平均Scolor imread(3.jpg); % 待处理彩色图 Sgray rgb2gray(Scolor); % 加权平均0.2989R 0.5870G 0.1140B figure, imshow(Sgray), title(原始黑白图像);权重里 G 通道占比最高这是按人眼亮度感知来的。对蓝底白字的车牌来说这个加权有一个副作用蓝色背景被压得比较暗白色字符保持很亮两者灰度差被拉开对后面的阈值分割是有利的。如果你处理的是黄底黑字同样的权重下黄色背景偏亮、黑字偏暗对比依然够用但针对蓝牌的阈值就不适用了这是后面二值化那节要提的事。3.2 用开运算估背景再相减这一步是整个预处理里最有巧思的地方。常规做法是直接对灰度图做直方图均衡化或者imadjust拉对比度但这套代码走的是「估计背景再减掉」的路子s strel(disk, 13); % 圆盘结构元半径 13 像素 Bgray imopen(Sgray, s); % 开运算 先腐蚀后膨胀抹掉比结构元小的亮块 Egray imsubtract(Sgray, Bgray); % 原图减背景突出局部亮区域 figure, imshow(Bgray), title(背景图像); figure, imshow(Egray), title(增强黑白图像);开运算的物理含义是把图像里所有比结构元小的亮目标都吃掉只留下大面积的、结构元塞不进去的亮区。车牌上的字符笔画宽度通常十几个像素圆盘半径 13 能覆盖掉所以开运算之后字符被抹平剩下的就是「这一片区域整体的亮度分布」也就是背景。原图减去这个背景相当于做了局部自适应的高通滤波——车灯、路面反光这类大范围亮度渐变被减掉了字符这种局部高亮被保留下来。半径 13 不是随便给的。它必须大于字符笔画宽度、小于车牌区域尺度。图缩到 800 像素宽时蓝牌大概 200 到 300 像素宽字符笔画 8 到 15 像素13 是中间值。如果原图是 3000 像素宽的手机原片直接跑笔画可能 40 像素半径 13 完全盖不住字符会被当成背景留在Bgray里相减之后字符就没了这就是典型的比例失配翻车。稳妥做法是像我上面主流程那样先imresize到固定宽度再估背景。提示imsubtract遇到负值会截断到 0不产生环绕。如果你的减法是Sgray - Bgray用普通减号uint8 会下溢成 255图上会出现一片诡异的亮点。3.3 三分法取阈值的二值化im2bw需要一个 0 到 1 之间的归一化阈值原始代码的取法是拿增强图的最大最小灰度做三等分取靠上的那个分点fmax1 double(max(max(Egray))); % 增强图全局最大灰度 fmin1 double(min(min(Egray))); % 增强图全局最小灰度 level (fmax1 - (fmax1 - fmin1)/3) / 255; % 三分法偏亮的那条分界线 bw22 im2bw(Egray, level); % 按 level 二值化 bw2 double(bw22); % 转 double 方便后续运算 figure, imshow(bw2), title(图像二值化);把式子拆开看fmax1 - fmin1是灰度动态范围除以 3 得到区间高度的三分之一从最大值往下退一个台阶落在 2/3 处。取偏高的阈值意味着「只有足够亮的像素才判为白」这对背景相减后的增强图是合理的——增强之后字符是少数高亮像素背景接近 0阈值定高一点能把残余噪声挡掉。但这个取法的软肋非常明显它只看全局最值不看分布。如果图里有一块高光反光fmax1会被拉到 255阈值跟着抬到很高整车牌可能全被判成黑如果图整体偏暗fmin1很低动态范围被拉大阈值又被压低噪声全进来。所以实际调试时我一般会给一个兜底三分法算出来的level之外再算一个graythresh(Egray)Otsu两个值差太多就以 Otsu 为准或者直接跑两遍二值化看哪个定位成功。3.4 Canny 边缘与形态学开闭运算二值化拿到的是一个实心的亮块图接下来要把它变成「只保留轮廓」的线图用的是 Cannygrd edge(bw2, canny); % Canny 算子提取边缘 figure, imshow(grd), title(图像边缘提取); % 矩形结构元 [行 列] [5 19]横向拉长配合车牌扁长形状 bg1 imclose(grd, strel(rectangle,[5,19])); % 闭运算连断点、填小孔 bg3 imopen(bg1, strel(rectangle,[5,19])); % 开运算去细小噪点 bg2 imopen(bg3, strel(rectangle,[19,1])); % 竖向开运算切断上下方向的粘连strel(rectangle,[5,19])这个参数里行 5、列 19是一个横向扁长的矩形。为什么这么给车牌的字符在水平方向排列密集、字符之间有间隙字符内部笔画是竖向的。用横向 19 的闭运算能把同一行里相邻字符的边缘连成一条同时不会把上下不同的东西连起来再用竖向 1 的开运算把垂直方向上粘连的杂散边缘切断。这段的调参血泪经验在于结构元的尺寸必须和车牌在图像里实际占的像素尺寸挂钩。原图大、车牌小结构元给大了会把整个车牌和旁边车灯连成一块结构元给小了字符之间的边缘连不起来后面bwlabel会标出几十个碎片区域。我一般会先把图缩到车牌宽度 200 像素左右这样 [5,19] 这组参数能直接复用不用每换一张图就重调。4. 车牌定位、倾斜校正与字符切割从候选框到七个字符4.1 bwlabel 连通域标记与区域特征边缘经过形态学处理之后图上是一堆连通的白色块每个块可能是一个车牌、一个车灯、一段保险杠的反光。bwlabel的作用是给每个连通块编个号[L, num] bwlabel(bg2, 8); % 8 邻域连通返回标记矩阵和区域数 stats regionprops(L, BoundingBox, Area, Centroid); % 新版写法 Area [stats.Area]; BoundingBox [stats.BoundingBox]; % 每行是 [x y width height] RGB label2rgb(L, spring, k, shuffle); % 伪彩色显示便于肉眼核对 figure, imshow(RGB), title(图像彩色标记);bwlabel的第二个参数 8 表示 8 邻域——上下左右加四个对角都算相连。用 4 邻域的话斜向相连的笔画会被拆成两个区域车牌常常因此被切成两半用 8 邻域则会好一些但也会让相邻但不相干的块粘在一起。这个参数没有绝对对错取决于前面形态学处理得干不干净。regionprops的BoundingBox返回的是[x y width height]x 是列、y 是行顺序和矩阵索引(row, col)是反的这是新手最容易写错的地方——按(y, x)去取子图切出来的永远是转置的。4.2 长宽比先验筛选车牌拿到每个候选区域的宽高之后用一个非常朴素但有效的先验车牌是扁的。国内蓝牌标准尺寸 440mm×140mm宽高比约 3.14就算拍摄角度有透视畸变实际图像里的比值也基本落在 2.2 到 4.5 之间。candidateIdx 0; bestScore inf; for k 1:num bb stats(k).BoundingBox; w bb(3); h bb(4); if h 10 || w 40 % 过滤掉太小或太窄的碎片 continue; end ratio w / h; area stats(k).Area; % 打分宽高比越接近 3.14、面积越大越可能是车牌 score abs(ratio - 3.14) - area / 1e5; if ratio 2.0 ratio 5.0 score bestScore bestScore score; candidateIdx k; end end if candidateIdx 0 plateImg []; ok false; return; end bb stats(candidateIdx).BoundingBox; % 向外扩一点边避免把边框切掉 padX round(bb(3) * 0.05); padY round(bb(4) * 0.10); x1 max(1, round(bb(1)) - padX); y1 max(1, round(bb(2)) - padY); x2 min(size(grayImg,2), round(bb(1)bb(3)) padX); y2 min(size(grayImg,1), round(bb(2)bb(4)) padY); plateImg grayImg(y1:y2, x1:x2); ok true;打分函数就是「宽高比偏差减去归一化面积」两项量纲不同面积那项除以 1e5 让它不至于压过比值项。这是很糙的启发式但比报告里「谁更接近就选谁」的单指标筛选稳一点因为车牌往往是面积最大的符合比例的区域而车灯虽然也可能符合比例面积通常对不上。外扩 5% 到 10% 的边是必须的形态学操作会把车牌的细边框吃掉不外扩切割出来的子图上下边缘会贴着字符。4.3 水平投影峰谷分析与线性拟合求倾角定位出来的车牌子图通常是斜的。倾斜有两个来源拍摄角度带来的透视变形以及车牌本身安装不平。三度以内的倾斜肉眼几乎看不出来但对投影分析是致命的——字符的垂直投影峰会变宽、谷会变浅切出来的字符边界就飘了。sbw1 im2bw(plateImg, graythresh(plateImg)); % 车牌子图重新二值化 % 逐行扫描找每行第一个和最后一个白像素作为车牌上下边缘的采样点 [rows, cols] size(sbw1); xdata []; ydata []; for r 1:rows c find(sbw1(r,:), 1, first); if ~isempty(c) xdata(end1) c; ydata(end1) r; end end fresult fit(xdata, ydata, poly1); % 一次多项式拟合y p1*x p2 angle atan(fresult.p1) * 180 / pi; % 斜率转角度弧度换度 angle max(min(angle, 15), -15); % 限幅 ±15 度防止拟合跑飞 subcol imrotate(plateImg, angle, bilinear, crop); % 灰度图旋转 sbw imrotate(sbw1, angle, bilinear, crop); % 二值图同步旋转关键点在fit用的poly1也就是一阶多项式本质是直线拟合fresult.p1是斜率。atan把斜率转成弧度再乘180/pi转角度。imrotate的第三个参数bilinear是双线性插值比最近邻平滑但会让二值图出现中间灰度值所以旋转完还得再二值化一次。第四个参数crop表示输出尺寸和原图一致超出边界的部分被裁掉。这里我强烈建议加限幅。如果车牌区域混进了别的亮块拟合出来的直线可能斜 40 度imrotate一转车牌直接被转出画面后面全是空数组。±15 度是个安全区间真实场景里超过这个角度的车牌本身也很难识别了。4.4 去上下边框、垂直投影切字符校正之后开始做真正的字符切割。这一步分两小步先切掉车牌的上下边框拿到字符的准确行范围再在行范围内做垂直投影切出每个字符。histrow sum(sbw, 2); % 水平投影每行白像素数量 rowTop find(histrow max(histrow)*0.3, 1, first); % 字符区起始行 rowBot find(histrow max(histrow)*0.3, 1, last); % 字符区结束行 sbw2 sbw(rowTop:rowBot, :); % 裁掉上下边框后的车牌二值图 charH rowBot - rowTop 1; % 字符高度 histcol sum(sbw2, 1); % 垂直投影每列白像素数量 colThr mean(histcol) * 0.5; % 投影阈值低于此值认为是字符间隙 % 找出所有连续的白列区间 mask histcol colThr; d diff([0 mask 0]); starts find(d 1); % 每个字符段的起始列 ends find(d -1) - 1; % 每个字符段的结束列histrow max(histrow)*0.3这个 0.3 是经验系数车牌上下边框是横向贯通的白条投影值接近满行宽字符行虽然也有很高的峰但字符之间有间隙投影值不会满。用最大值的 30% 当门限既能找到字符的上下边界又能把边框切掉。垂直投影的阈值用均值的一半含义是「这一列的白像素数低于平均的一半就判定为字符间隙」。字符间隙在垂直方向通常只有几个像素宽投影值接近 0很容易被切出来。麻烦的地方在于汉字像「京」「粤」这种结构复杂的字内部笔画之间也会有投影低谷按固定阈值会被切成两半。所以切完必须做一次字符段合并相邻两段间距小于字符平均宽度的 30% 就合并。4.5 归一化到 22×14 并落盘样本切出来的字符图尺寸各不相同要和模板比对必须先归一化。原始代码用的是imresize(SegBw, [22 14])这个 22×14 的选择跟当时用的样本库尺寸绑定字符的宽高比大致是 1:1.57符合车牌字符的瘦长形状。segList {}; for k 1:length(starts) cleft max(1, starts(k) - 1); cright min(size(sbw2,2), ends(k) 1); seg sbw2(:, cleft:cright); % 先切出原始尺寸字符 segNorm imresize(seg, [22 14]); % 归一化到 22 行 14 列 segList{end1} segNorm; %#okSAGROW end % 只有确认字符类别时才写样本库别在生产识别里写 if isTraining label A; % 由人工指定 dirName fullfile(sampleRoot, label); if ~exist(dirName, dir), mkdir(dirName); end fname fullfile(dirName, sprintf(s_%03d.jpg, numel(dir(dirName))-1)); imwrite(segNorm, fname, jpg); end样本落盘这块原报告的做法有个隐患识别流程里顺手就imwrite把切出来的图存进样本库存的时候还没验证识别对不对等于把错例当正样本喂回去了。训练和识别必须分开走两条路径用isTraining开关控制。注意imresize默认用双线性插值对二值图会产生介于 0 和 1 之间的灰度值。后面做模板相减时这些灰度值会参与运算严格来说应该im2bw再二值化一次。5. 字符匹配识别与五类典型翻车排查5.1 模板匹配的识别闭环字符识别的逻辑非常简单粗暴把待识别字符和样本库里每一个同类尺寸的图做差算误差取误差最小的那张图对应的标签作为结果。function [label, score] match_char(charImg, sampleRoot) label ?; score inf; dirs dir(sampleRoot); for i 1:numel(dirs) if ~dirs(i).isdir || startsWith(dirs(i).name, .), continue; end curLabel dirs(i).name; % 目录名就是标签 files dir(fullfile(sampleRoot, curLabel, *.jpg)); for j 1:numel(files) tpl imread(fullfile(sampleRoot, curLabel, files(j).name)); if ~isequal(size(tpl), size(charImg)) tpl imresize(tpl, size(charImg)); % 尺寸对齐兜底 end d double(charImg(:)) - double(tpl(:)); err sum(abs(d)) / numel(d); % 平均绝对误差 if err score score err; label curLabel; end end end end误差度量用的是平均绝对误差除以像素总数是为了让不同尺寸的样本可以横向比较。也有人用欧氏距离sqrt(sum(d.^2))对边缘错位的惩罚更重实测在这套二值字符上差别不大。这个方案的上限在哪它本质上是像素级的最近邻对字体、笔画粗细、轻微形变完全没有容忍度。报告里说得很实在识别失败的那张图里G 被认成 B、K 认成 A、0 认成 8这三个错误全是「字形相近 样本单一」导致的。解决办法只有两个方向——扩样本每个字符收十几种字体和拍摄条件下的图或者换特征不比对像素比对投影直方图、网格特征这类统计量。5.2 神经网络方案的取舍报告里提到了另一条路用神经网络做识别思路是先用样本训练四个子网络汉字、字母、字母数字、数字再用训练好的权值对切出来的字符分类。在小规模字符集上这套方法的实际收益和代价要算清楚维度模板匹配BP 神经网络样本量需求每字符 1 到 5 张即可跑起来每字符至少几十张否则过拟合训练耗时无分钟级到小时级取决于网络规模对字体差异的容忍极差较好可解释性高能直接看误差低权值是个黑匣子调试难度低改阈值改样本就行高不收敛时几乎无从下手如果是课程设计、图个能演示模板匹配足够。如果真要上一点工程强度我的建议是不要一上来就上神经网络先用「投影特征 KNN」这种中间方案每个字符提取垂直投影直方图当特征向量用欧氏距离找最近邻。这个方案对字形差异的容忍度比像素比对高一个量级样本需求又比神经网络低得多调试时可解释性也好。5.3 五条踩坑记录现象、原因、解决这套代码从报告搬到能跑我踩过的坑按痛苦程度排一下。现象一运行到imfeature报「未定义函数或变量」。原因是imfeature在 MATLAB R2013b 就被移除了接替它的是regionprops但字段名和返回格式都变了——imfeature(L,basic)返回的是结构体数组字段叫Area、BoundingBoxregionprops(L,Area,BoundingBox)返回的也是结构体数组但需要显式列出属性名而且默认还带一个Centroid。解决就是把Feastats imfeature(L,basic)换成stats regionprops(L,BoundingBox,Area)取用时[stats.Area]数组化。如果代码里还用了Orientation这类属性regionprops里也能拿到但要显式写出来。现象二换一台机器跑识别结果全空样本目录找不到。原因是样本库路径写死成F:\MATLAB\work\sam\image1.jpg换机器 F 盘根本不存在imwrite静默失败或者imread报错被 try-catch 吞掉。解决是把路径全部改成fullfile(pwd, samples)这种相对路径用mfilename(fullpath)拿到脚本所在目录再往上拼并且在程序启动时用exist(sampleRoot,dir)检查一次不存在就mkdir并明确报错退出别让它静默跑完输出个空字符串。现象三大分辨率原图上定位失败二值图里车牌区域整块消失。原因是strel(disk,13)的半径远小于字符笔画宽度开运算没把字符抹掉imsubtract相减之后字符区域变成 0。现象很迷惑——预处理后的图看着「很干净」其实是把有用信息全减没了。解决办法在预处理前统一缩放把图缩到宽度 800 像素左右让车牌宽度稳定在 200 到 300 像素区间笔画宽度落到 10 像素上下半径 13 才合理。如果不想缩放那结构元半径得按车牌估计宽度 / 20动态算。现象四切割出来的字符数量不是 7有时 5 个有时 9 个。两个原因。一是垂直投影阈值固定成均值的一半遇到汉字这种笔画密集的字符字内部被切成两段遇到「1」这种窄字符投影值太低直接被判成间隙跳过。二是原始代码里for kfindmax-1:findmax5这种硬编码索引findmax一旦偏移切出来的就是连续 7 列而不是 7 个字符。解决办法是先按阈值切出所有候选段再做合并间距 平均字符宽度 30% 的合并和过滤宽度 平均宽度 40% 的丢弃最后校验段数不等于 7 就记录日志并把这一张扔进错例目录。现象五倾斜校正后车牌被转出画面后续全报错。原因是fit拟合用的采样点混进了车牌以外的白像素斜率算出来几十度imrotate(...,crop)一转车牌整个旋出边界输出全是黑的。解决办法是采集拟合点时只取车牌区域内行方向白像素数量在合理区间的行剔除首尾噪声行角度做 ±15 度限幅旋转后检查有效像素占比低于 5% 就判定校正失败直接返回原图不转。6. 二次开发把课程脚本改成能批量跑、能回捞错例的工具单张图跑通只是起点。真正拿这套代码做二次开发我建议按「批量入口 参数外置 错例回收」三件事改改完你才能知道算法的真实准确率是多少而不是靠挑一张好图自欺欺人。第一步把流水账脚本拆成函数参数收进一个 config 结构体所有魔数结构元尺寸、阈值系数、角度限幅都从 config 读。这样换数据集时不用改代码只改配置。function cfg default_config() cfg.resizeWidth 800; % 统一缩放宽度锁定笔画像素尺度 cfg.diskRadius 13; % 背景估计的圆盘半径 cfg.bgRatio 0.33; % 三分法阈值的位置系数 cfg.closeRect [5 19]; % 闭运算结构元 cfg.openRect [5 19]; % 开运算结构元 cfg.aspectRange [2.0 5.0]; % 车牌宽高比筛选区间 cfg.angleLimit 15; % 倾斜校正限幅度 cfg.expectChars 7; % 期望字符数 cfg.sampleRoot fullfile(fileparts(mfilename(fullpath)), samples); cfg.outputRoot fullfile(fileparts(mfilename(fullpath)), output); end第二步写批量入口遍历images/下所有图每张图 try-catch 包住失败的不中断把识别结果、耗时、是否命中期望字符数写进 CSVfiles dir(fullfile(images, *.jpg)); fid fopen(fullfile(output, result.csv), w); fprintf(fid, file,plate,chars,ok,elapsed\n); for i 1:numel(files) t0 tic; try [plate, n] run_once(fullfile(images, files(i).name), cfg); catch ME plate ERROR; n 0; fprintf(处理 %s 失败%s\n, files(i).name, ME.message); end ok (n cfg.expectChars) ~strcmp(plate, ERROR); fprintf(fid, %s,%s,%d,%d,%.2f\n, ... files(i).name, plate, n, ok, toc(t0)); end fclose(fid);第三步是错例回收这是最有价值的一环。凡是字符数不对、或者跟人工标注不符的把中间结果增强图、二值图、定位框、切割出的每张字符图全部存到output/fail/文件名/下。攒够二三十个错例问题会自己浮出来——你会发现大部分失败集中在几类夜间反光、车牌倾斜超过 15 度、字符被遮挡、样本库里缺某个汉字。改造方向上我列几个性价比从高到低的改造点原做法改法预期收益阈值选取全局最大最小三分法Otsu 或局部自适应adaptthresh光照不均时二值化稳定字符识别像素级模板相减垂直投影直方图特征 KNN对字体和轻微形变更鲁棒倾斜校正全行采样做直线拟合只取字符区行、加 RANSAC 去野点大角度倾斜不再被带跑偏样本库单字符一张图每字符 10 张以上不同字体/光照相近字符混淆率明显下降汉字处理和字母数字共用一套汉字单独一个模板库加笔画数过滤省份简称识别率提升有一件事要提醒先别急着动识别算法。我见过的绝大多数失败案例根因在预处理和定位不在识别。把中间结果图存下来逐张看二值化糊了就把阈值治好定位框偏了就把结构元治好这些改完准确率往往能上一大截比换神经网络省事得多。最后一次调这套代码是处理一批夜间地库拍的图怎么调准确率都卡在 60% 上不去。后来我把每张图的Egray和bw2都存下来看发现有一半图的二值化结果里车牌区域是一整块白——背景相减的时候字符已经被减掉了后面所有环节都在处理一块纯白。问题不在识别端在strel(disk,13)和那些根本没缩放过的大图。从那以后我定了条死规矩任何图像处理流程动手调参数之前必须先imshow一遍每一步的中间结果把链路当透明的看而不是当黑匣子。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站