ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MATLAB车牌识别完整实战:灰度化、边缘检测与模板匹配全流程解析

MATLAB车牌识别完整实战:灰度化、边缘检测与模板匹配全流程解析

简介:面向MATLAB图像处理学习者和车牌识别初学者的系统论文文档,以车牌识别方法研究为主线,详细覆盖车牌区域提取、字符切割与字符识别三个核心模块,并围绕研究背景、理论概况和系统程序设计展开说明。压缩包内仅包含1个doc文件,包体594KB,轻量实用,便于直接查看。论文详细给出了车牌区域提取的图像灰度化、Roberts边缘检测、灰度腐蚀、平滑处理和边界值计算步骤,字符切割与识别则涵盖去噪、膨胀腐蚀及归一化处理,并配有MATLAB代码和效果图;结合数学形态法与彩色信息完成车牌定位,采用垂直投影实现字符分割,同时分析了我国车牌汉字字母数字混合、格式多样、光照不均、背景复杂等现实难点,可帮助读者从图像输入到字符输出完整复现系统。目前已有314人学习,适合作为数字图像处理课程论文、实验复现或车牌识别入门参考。

1. 一个课程设计级的MATLAB车牌识别项目:论文与完整源码一次交付

翻过不少车牌识别项目,大多数要么只贴关键代码没有完整工程,要么论文和代码对不上号。这份资源把数字图像处理课程论文和整套可运行的MATLAB源码一起交付,从图像灰度化、Roberts边缘检测、形态学腐蚀闭运算,到蓝色像素投影定位、垂直投影字符切割、模板匹配字符识别,是一条完整的传统视觉处理链路,不依赖深度学习框架,也不吃显卡。适合两类人:正在做数字图像处理课程设计、需要论文和代码配套的学生;以及想用MATLAB快速验证经典图像处理流程的开发者。源码里保留了按行扫描统计蓝色像素、按面积比值决定膨胀还是腐蚀这类有年代感的写法,恰恰是理解形态学处理最好的教材。

2. 整体流程与算法选型:灰度化、Roberts边缘检测与形态学组合

2.1 一条完整的识别链路:从车辆图像到七个字符

先看源码主函数main从头到尾做了什么,这决定了你拿到代码后该按什么顺序去读。整个流程可以分成四个阶段。

第一阶段是图像预处理。用uigetfile弹出文件选择框读入一张jpg,转灰度后做imadjust线性灰度变换,再经过medfilt2中值滤波去掉噪声,最后调用edge函数用Roberts算子做边缘检测。这里有个值得注意的细节:代码里连续出现两次灰度图和直方图的figure输出,说明作者在调参阶段反复对比过灰度变换前后的效果。

第二阶段是车牌定位。定位思路不是深度学习目标检测那种端到端,而是传统视觉里的两步走:先用形态学腐蚀和闭运算把边缘图处理成若干候选连通域,再用bwareaopen按面积阈值删掉小于1000像素的小对象,最后用蓝色像素统计做水平和垂直方向的投影,找到车牌的上下左右边界。

第三阶段是字符分割。把定位出来的车牌区域单独保存成dw.jpg,重新读入后灰度化、二值化、中值滤波,再根据字符面积占车牌面积的比例决定膨胀还是腐蚀,之后用垂直投影扫描切出七个字符,分别保存为1.jpg到7.jpg。

第四阶段是字符识别。把每个字符归一化成40乘20像素,和字符模板库里的模板逐像素做差,统计差异像素数量,差异最小者即为识别结果。

这四段链路在设计上是经典的“预处理-定位-分割-识别”四件套,和论文目录完全对应。论文第三章先讲车牌区域提取,再讲字符切割,最后讲字符识别,代码的编排顺序和论文叙述顺序一致,拿来做课程设计答辩,讲起来很顺。我建议初次读代码时不要从第一行看到最后一行,而是按这条链路分别去读。因为主函数是顺序执行的,中间还有大量imwrite和imread来回读写磁盘,单纯按行读容易跟丢图像数据流的方向。按阶段读,每个阶段输入什么图像、输出什么图像,会清晰很多。

2.2 第一步预处理:RGB转灰度与Roberts边缘检测

打开主函数,前几十行基本是在做预处理,原文代码:

I1=rgb2gray(I); % RGB转灰度 I1=imadjust(I1,[0.3,0.7],[]);% 线性灰度变换,拉伸0.3到0.7灰度区间 I1=medfilt2(I1); % 中值滤波去噪,默认3x3窗口 I2=edge(I1,'roberts',0.25,'both'); % Roberts算子边缘检测,阈值0.25,两个方向都检测

这段代码每行都值得展开说。灰度化的原因很朴素:不管是蓝底白字还是黄底黑字,车牌底色和字符颜色反差大,转成灰度后对比度被保留,而后续边缘检测和形态学处理只需要处理单通道,计算量比RGB三通道小得多。imadjust的作用是拉伸灰度分布,代码里把输入区间设为0.3到0.7,意思是原始灰度值在0.3以下的部分映射到0,0.7以上的映射到1,中间做线性拉伸。这个参数的选择和图像曝光直接相关,不是通用的,后面避坑章节我会单独说。

medfilt2中值滤波是去掉椒盐噪声的经典手段,车牌图像里常见的污点、反光点经过中值滤波后会变成周围像素的中值,对后续边缘检测很友好。edge函数这里指定的是Roberts算子,这是一种基于对角方向差分的边缘检测算子,对45度和135度方向的边缘响应明显。阈值0.25意味着梯度幅值低于0.25的像素不认为是边缘,‘both’表示水平和垂直两个方向的差分都要计算。为什么选Roberts而不是Sobel或Canny?我的判断是:Roberts算子计算量最小,只有2x2的模板,对这个项目场景足够用,而且边缘检测后还要做腐蚀和闭运算,边缘的精细程度不是关键,关键是边缘位置的大致轮廓,所以用最轻量的算子就能满足需求,跑起来也快。

这一段预处理里其实藏着一个可调整的旋钮:imadjust的[0.3,0.7]区间直接影响后续边缘检测的输入质量。区间拉得过大会让暗部细节丢失,车牌字符边缘断裂;过小则对比度不足,背景噪声会被放大成边缘。你在跑通代码后可以试着把这两组参数改一改,观察对最终识别结果的影响,这是理解整个系统敏感度最快的实验。

2.3 形态学滤波路线:腐蚀、闭运算与面积筛选

边缘检测得到的图里,车牌区域的边缘和车身边缘、路面标线、树木阴影的边缘会混在一起。下一步用形态学操作滤除干扰。原文代码:

se=[1;1;1]; % 3x1的列向量结构元素 I3=imerode(I2,se); % 腐蚀:消除细小的边缘点 se=strel('rectangle',[20,20]); % 生成20x20矩形结构元素 I4=imclose(I3,se); % 闭运算:先膨胀后腐蚀,连接邻近区域 I5=bwareaopen(I4,1000); % 删除面积小于1000像素的连通域

这里结构元素的设计是有讲究的。第一次腐蚀用的是3x1的列向量,作用是消除水平方向的细小边缘段。因为车牌字符边缘在垂直方向有连续性,而背景噪声边缘往往是孤立点或短线,用列向量做腐蚀可以保留垂直纹理的同时抹掉水平短线。这里有一个容易踩的坑:腐蚀的结构元素形状和方向必须和目标特征匹配,如果你用3x3方形结构元素,会把车牌边缘本身也腐蚀掉一部分,反而降低定位准确率。我在二次开发时把结构元素换成过不同形状,实测下来对定位结果的影响比预想的大,这属于典型的“玄学调参”环节。

第二次用strel(‘rectangle’,[20,20])生成20x20的矩形结构元素做闭运算。闭运算是先膨胀后腐蚀,可以把距离小于结构元素尺寸的两个连通域连接成一个。这里20x20的矩形对应的是车牌字符笔画间距的量级,如果两个区域距离小于20像素,膨胀后就会连上,闭运算就起到了桥接作用。闭运算后还要用bwareaopen删除面积小于1000的对象。注意这里源码里实际跑的是1000,而论文正文里写的是2000,代码和论文不一致。我按代码里的1000跑,定位效果正常,说明这个阈值宽容度还不错,只要能把车标、反光点这类小面积对象滤掉就行。

这个阶段结束后,图像里通常只剩下一两个候选连通域。如果进入下一步蓝色像素投影时还有多个候选区域,说明前面腐蚀或面积阈值没调好,最常见的表现是车身贴纸、保险杠装饰条也被当成候选车牌。这属于定位阶段的典型问题,后面避坑章节我会给排查方法。

3. 核心模块拆解:蓝色像素投影定位与字符分割的实现

3.1 双方向投影法确定车牌边界

定位阶段的核心思路是:把二值图像分别向水平和垂直方向做投影,每一行或每一列统计白色像素数量,然后通过阈值判定车牌区域的边界。源码里具体实现是统计白色像素,但变量名取的是Blue_y,注释写的是“判断蓝色像素”。原因在于输入I5已经是二值图像,颜色信息早已丢失,这里统计的白色像素其实来自车牌边缘和字符本身,但作者的本意是定位蓝色车牌区域。读这种老代码要保持警惕:注释是参考,代码行为才是真相。

[y,x,z]=size(I5); % y是行数,x是列数 myI=double(I5); % 转成双精度再处理 Blue_y=zeros(y,1); % 每行白色像素计数器 for i=1:y for j=1:x if(myI(i,j,1)==1) % 二值图像中值为1即白色 Blue_y(i,1)=Blue_y(i,1)+1; end end end [temp MaxY]=max(Blue_y); % 找白色像素最多的行,作为垂直中心参考 PY1=MaxY; while ((Blue_y(PY1,1)>=5)&&(PY1>1)) PY1=PY1-1; % 向上扫描,直到找到白色像素少于5的行 end

边界扫描的逻辑是:先找到白色像素累加值最大的行MaxY,作为车牌的垂直中心参考线,然后从这个中心线出发,向上和向下扫描,只要当前行的白色像素数大于等于5,就继续往外扩展,直到遇到白色像素少于5的行,那就是车牌的上下边界。

这个阈值5的含义很直接:车牌的每一行至少要有一小段连续的白色像素才能算作车牌行。设置得过小会把噪声行计入,导致定位区域偏大;设置得过大则会把车牌上下边缘的行漏掉,导致字符被裁掉一部分。水平方向用同样的思路扫描,阈值设为3,因为车牌宽度方向的行数比高度方向的列数多,投影值天然偏大,所以做对称调整稍微放低一点。

边界确定后还有两行修正代码值得注意:

PY1=PY1-2; % 上边界向外扩2行 PX1=PX1-2; % 左边界向外扩2列 PX2=PX2+3; % 右边界向外扩3列 PY2=PY2+10; % 下边界向外扩10行

这些微调的数值来源于一个常识:边缘检测和形态学处理都会造成边界收缩,如果直接按投影边界裁剪,字符边缘很可能被切到。向外扩的目的就是补偿这个收缩量。实际跑代码时你会发现,不同的测试图片可能需要调这些补偿值,我一般是先按原值跑一遍,看裁剪后的图像里字符是否完整,再决定要不要加大补偿量。这套“先裁出来肉眼看”的调试方法,比在代码里反复试参数要直观得多。

裁剪出车牌区域后,代码把图像保存成dw.jpg,然后重新imread读入。这是为了把“定位”和“字符识别”两个阶段解耦。你在二次开发时可以保留这个习惯,中间结果落盘到本地,能帮你快速定位问题出在哪个阶段。

3.2 字符分割:垂直投影与粘连切断

字符分割阶段输入的是已经裁剪好的车牌二值图。源码的做法是先灰度化、imadjust、im2bw二值化、medfilt2滤波,然后根据字符面积占比做膨胀或腐蚀:

[m,n]=size(d); % m行数,n列数 if bwarea(d)/m/n>=0.365 % 字符面积占车牌面积比例过高 d=imerode(d,se); % 腐蚀:让字符变瘦 elseif bwarea(d)/m/n<=0.235 % 比例过低 d=imdilate(d,se); % 膨胀:让字符变粗 end

这里的逻辑非常有意思。字符面积占车牌面积的比例,理论上应该在0.235到0.365之间。如果占比超过0.365,说明字符笔画太粗或者噪声太多,需要腐蚀来瘦身;如果低于0.235,说明字符太细或图像太稀疏,需要膨胀来加粗。se用的是eye(2),即2x2的单位矩阵。这个自适应策略是为了应对不同图片中字符粗细不一致的问题,本质上是一个基于先验知识的归一化操作。比例阈值本身来自对车牌版式的大量统计,属于典型的经验值。

分割主循环用垂直投影方法:

s=sum(d); % 对每一列求白色像素总和,得到垂直投影 j=1; while j~=n while s(j)==0 j=j+1; % 跳过空白列 end k1=j; % 字符起始列 while s(j)~=0 && j<=n-1 j=j+1; % 字符结束列 end k2=j-1; if k2-k1>=round(n/6.5) % 列宽超过一个半字符,判定为粘连 [val,num]=min(sum(d(:,[k1+5:k2-5]))); % 段内找投影最小列 d(:,k1+num+5)=0; % 在该列切断 end end

垂直投影的原理是:二值图像每一列的白色像素数投影到x轴,字符区域投影值非零,字符间隙投影值为零。扫描白色段和空白段,每个白色段就是一个候选字符。这里有个粘连处理:如果一个白色段的宽度超过车牌宽度n除以6.5,也就是超过约一个半字符宽度,就认为这个段里包含了两个字符,需要在段内找投影值最小的列作为分割点,把两个粘连的字符切开。

切完之后的逻辑还有一段基于宽度的干扰滤除,wide<10就认为是左侧干扰并将该区域置零。这里10也是经验阈值,因为第一个字符如果是汉字,笔画较多,宽度不会太窄,过窄的白色段大概率是边框噪声。这些阈值在代码里的注释都以问号形式标注,说明原作者也是在不同图片上反复试出来的,不是严格的公式推导。读代码时看到这种“裸奔的魔数”,最好的处理方式不是直接删,而是把它提炼成常量,加上注释说明来源,方便下次调整。

3.3 qiege和getword子程序的设计思路

源码里有两个关键子程序被反复调用,一个是qiege,一个是getword。主函数里加上注释掉的部分,qiege至少被调用了三次。虽然这次交付的文档里没有直接给出这两个子程序的完整代码,但是从调用位置和参数行为可以推断出它们的职责。

qiege的作用是“切割”。主函数里第一次调用是在膨胀腐蚀处理后,紧接着是连续调用三次qiege。从调用时序看,qiege的功能大概率是移除图像最左侧的干扰列或最右侧的干扰列,让后续投影扫描的起点更干净。因为车牌二值图像经过形态学处理后,边缘可能残留一些细小的白色干扰,这些干扰位于图像边缘,不影响字符主体,但会影响投影扫描时的空白列判断。

getword的作用更明确,从命名看就是“获取单个字符”,也就是从图像中切出一个字符。主函数里这样调用:

[word2,d]=getword(d); [word3,d]=getword(d);

每调用一次,返回两个值,第一个是切割下来的字符图像,第二个是切除该字符后剩余图像。连续调用七次,依次得到七个字符。这种“切一个,返回剩余”的接口设计非常实用,第二次调用自动从上一个字符结束位置开始扫描,不需要在外部维护游标变量,代码逻辑干净很多。

由于文档里没有贴这两个子程序,你需要了解它们的实现才能做二次开发。我的经验是基于调用逻辑补写并不复杂:qiege实现一个while循环找到第一个非空列,把这一列之前的区域置零;getword就是垂直投影法切字符,再用imcrop截取字符区域。自己补写时要注意qiege和getword之间对“白色像素”的判断标准必须一致,否则会出现字符被多切或少切的情况。判断标准不一致这个问题,在我自己复刻这个流程时踩到过,后面避坑章节会细说原因。

4. 字符识别模块:模板匹配全流程与模板库组织

4.1 字符归一化与差异像素统计

字符识别阶段的核心是把每个字符和模板逐像素比较。首先要统一尺寸,源码用imresize把所有字符和模板缩放到40x20像素:

word1=imresize(word1,[40 20]); % 统一为40行20列 word2=imresize(word2,[40 20]);

这里40x20的选择是经典的字符归一化尺寸,宽度20、高度40,比例接近字符的实际宽高比。imresize默认使用双线性插值,对于二值图像来说这个选择没问题,字符笔画不会出现明显断裂。要注意的是,如果字符图像中有大量椒盐噪声,缩放后噪声也会被插值放大一些,所以imresize之前最好再做一次中值滤波。原代码在二值化后已经做过一次medfilt2,通常足够。

归一化后,代码把每个字符保存为1.jpg到7.jpg,然后逐一读入做匹配。这种落盘再读入的方式效率不高,但有一个隐藏的好处:方便你在分割阶段结束后停下检查每个字符的质量。如果有某个字符明显缺笔画或带干扰,可以直接打开对应的jpg文件,快速确认问题出在分割阶段还是后续处理阶段。实际项目里我不推荐频繁读写磁盘,但在这个课程设计级别的项目里,落盘检查本身就是调试手段。

4.2 识别主循环代码逐段拆解

模板匹配的主循环是理解整个识别模块的关键,我把核心段拆出来逐行解释:

liccode=char(['0':'9' 'A':'Z' '京辽桂陕苏渝浙']); % 字符代码表:数字0-9、字母A-Z、省份简称汉字 SubBw2=zeros(40,20); % 存放每个像素的差异值 l=1; for I=1:7 ii=int2str(I); t=imread([ii,'.jpg']); % 读入第I个待识别字符 SegBw2=imresize(t,[40 20],'nearest'); % 用最近邻插值缩放到40x20 if I==1 kmin=37; kmax=43; % 第一位:只匹配汉字 elseif I==2 kmin=11; kmax=36; % 第二位:只匹配字母A-Z else kmin=1; kmax=36; % 第三位起:匹配数字和字母 end for k2=kmin:kmax fname=strcat('字符模板\',liccode(k2),'.jpg'); SamBw2=imread(fname); % 读入模板 for i=1:40 for j=1:20 SubBw2(i,j)=SegBw2(i,j)-SamBw2(i,j); % 像素差 end end Dmax=0; for k1=1:40 for l1=1:20 if (SubBw2(k1,l1)>10 || SubBw2(k1,l1)<-10) Dmax=Dmax+1; % 差异明显的像素数 end end end Error(k2)=Dmax; % 记录与当前模板的差异数 end ... end

这段代码的信息量不小,分几层看。

先看字符集。liccode里只包含7个汉字:京、辽、桂、陕、苏、渝、浙。这意味着模板库只支持这7个省份简称的识别,其他省份的车牌会被识别错。这是课程设计级别项目的常见取舍,用有限的模板覆盖最常见的字符,把精力放在流程完整性上。

再看识别位置的约束。车牌第一位是汉字省份简称,第二位是字母发牌机关代号,第三位及以后是字母和数字混合。代码在循环里对第一个字符只匹配汉字模板(kmin=37到kmax=43),第二个字符只匹配字母(kmin=11到kmax=36),后面五个字符匹配数字和字母。这个约束大幅缩小了匹配范围,提高了准确率,也提升了匹配速度。这个“按位置限定字符集”的思路在工程上非常实用,即使换成深度学习识别,也建议沿用这个先验规则。

匹配的核心是两个嵌套循环。外层遍历模板,内层遍历像素,用字符图像逐像素减去模板像素。注意这里判断差异的阈值是10,但字符和模板都是二值图,像素值只可能是0或1,差值理论上只可能是-1、0、1。那为什么要用10做阈值?因为实际读入的jpg经过压缩和缩放,边缘像素可能存在不完全是纯黑或纯白的过渡值,差值的绝对值可能到10以上。这个阈值等于给匹配过程加了一层抗干扰容错,允许像素在小范围内浮动,不至于因为一个像素的微小偏差就把模板判错。

差异像素数最少的就是识别结果,代码里的实现是:

Error1=Error(kmin:kmax); MinError=min(Error1); % 找最小差异数 findc=find(Error1==MinError); % 找对应模板索引 Code(l)=liccode(findc(1)+kmin-1); % 取出字符

这里有个细节值得注意:findc(1)而不是直接取findc,是因为可能存在两个模板差异数相同的情况,此时取索引小的那个。如果你要处理匹配分数冲突,这里可以改成记录所有最小差异的模板,再结合上下文规则二次决策,比如第二位不能是数字这种规则约束。原代码把第二个字符限定在字母区间,本质上就是在用规则消解冲突。

4.3 模板库的命名规则与扩充方法

模板匹配依赖的模板文件放在“字符模板”文件夹里,文件名就是字符本身,比如京.jpg、A.jpg、0.jpg。识别时通过liccode里的索引拼出文件名,strcat(‘字符模板\’,liccode(k2),‘.jpg’)这种拼接方式决定了模板文件名必须和liccode里的字符完全一致,包括大小写和中文编码。

如果你要扩充模板库支持更多省份简称,只需要三步。第一步,在liccode字符串末尾追加新汉字,比如粤、鲁、川。第二步,把对应汉字的模板图片放进字符模板文件夹,文件名和汉字完全一致,jpg格式。第三步,调整汉字识别的索引区间,因为代码里kmin和kmax是硬编码的37和43,追加字符后需要重新推算偏移量。

我建议把模板索引改成动态计算,而不是硬编码。比如用strfind(liccode,'京')找到每个字符的起始索引,再根据固定偏移量算出区间。这样扩充分省简时不用改循环里的数字。这块改动虽然不大,但对后续维护价值很高,尤其是你想把识别范围扩到全国的时候。硬编码索引会让你陷入一个很尴尬的局面:每加一个字符,就要回头改十几处数字,改错一个就整个识别崩溃,还不容易排查。

实际扩充模板时还要注意一点:模板图片的字体、笔画粗细要和目标车牌尽量一致。用不同字体生成的模板,匹配时会明显降低准确率。我一般从同一批车牌样本里截取字符做模板,保证字体特征一致,而不是用系统字体去生成。

5. 避坑记录:跑通这个项目常见的五个问题

5.1 中文路径导致imread读取失败

现象:运行主函数时,在uigetfile里选了图片,代码执行到imread报错,提示文件名无效或路径不存在。

原因:在中文Windows系统下,如果文件路径包含中文字符目录,老版本MATLAB的imread函数在解析路径时会遇到编码问题。这个项目最初是在R2014到R2016版本之间调试的,那几年MATLAB的中文路径处理一直是老大难,源码里用uigetfile返回的路径如果落在“桌面”“图片”这些中文目录下,踩中概率极高。

解决:把整个源码目录和测试图片放到纯英文路径下,路径中不要出现任何中文字符。如果你坚持用uigetfile交互选文件,选完图片后建议加一行disp(filepath)打印路径,确认没有中文;或者直接用固定路径读图,把路径字符串硬编码成全英文。我自己习惯是建一个叫D:\plate_recog的目录,所有素材和源码都放里面。

5.2 imadjust参数对不同光照图片的敏感度

现象:换一张测试图片后,灰度图的对比度明显不对,车牌区域和背景糊在一起,定位出来的区域包含大片非车牌内容。

原因:imadjust的输入区间[0.3,0.7]是基于原始图片灰度分布的经验值,不同光照条件下拍摄的车牌,灰度分布范围差异很大。逆光、夜间补光、阴天漫反射几种场景的直方图形状完全不同,固定区间必然顾此失彼。

解决:把imadjust的参数改成自适应计算。用graythresh函数基于灰度直方图自动计算阈值,再把阈值转换成imadjust的输入区间;或者直接用im2double加直方图均衡化histeq替代。我改过一版,用histeq做全局对比度增强,对曝光不足的图片改善非常明显。改动只要两三行代码,但能明显提升对不同光照图片的适应能力。这个改动是性价比最高的一个优化点。

5.3 字符分割把汉字切成两半

现象:分割出的七个字符里,第一个汉字字符缺了一部分,或者被切成了两个块,后面的字母和数字反而都正常。

原因:汉字笔画密集,垂直投影时某些汉字内部存在整列空白,扫描逻辑会把空白列误判为字符间隙。比如“京”字中间的空隙、某些字体的“苏”字笔画间的空隙,在投影图上就是一个零值列。垂直投影法对这个问题的抵抗能力天然不足,因为投影只统计每列的像素和,失去了二维结构信息。

解决:在判定字符间隙时,不仅要看投影值是否为0,还要考虑空白列的宽度。汉字内部的空白列宽度通常较窄,而字符间的间隙宽度较宽。可以设置一个最小间隙宽度阈值,比如3列以下视为字符内部空白,不切断。我在二次开发时还用过另一种更稳的方案:先统计所有字符列宽的中位数,再根据中位数推断间隙宽度阈值,这个方法在字符宽度不均匀时更稳。

5.4 模板匹配把相近字符认错

现象:识别结果里出现“0”和“O”混淆、“B”和“8”混淆这类错误,而且往往集中在后五位。

原因:逐像素模板匹配对字符形变敏感,而车牌字体中0和O的轮廓几乎一致,8和B在归一化后差异像素也很少。差异像素数可能只差十几个,这时候阈值容错和缩放插值的微小偏差都会改变判定结果。

解决:在模板匹配结果中加入“第二候选”机制。当最小差异和次小差异相差很小时,不立即判定,而是结合车牌规则做人工规则修正。比如第二位字符只能是字母,所以如果候选里有0和O,直接排除0;后五位里字母和数字都可能出现,对于B和8的冲突,可以强制规定模板中“8”和“B”的差异像素数下限,低于下限直接判为字母。这些规则写起来不复杂,本质上是在模板匹配的决策层加一个后处理规则器,比盲目堆模板样本更有效。

5.5 较新版本MATLAB的行为差异导致运行报错

现象:在较新版本MATLAB里运行源码,edge函数有时不返回结果,im2bw提示建议改用新函数,或者strel创建结构元素时行为不一致导致形态学处理结果异常。

原因:MATLAB图像处理工具箱在新版本里对一批函数做了行为修正和废弃处理。im2bw在新版本中被标记为即将移除,edge的‘both’参数内部实现也做了调整,strel则新增了更多类型的支持。这些改动叠加起来,会让老代码在18版本之后的MATLAB上表现异常,有的报错,有的不报错但结果和旧版本不一致。

解决:先做一轮“翻译式改造”。im2bw用imbinarize替代,效果等价;edge建议改用Sobel算子或者显式指定阈值向量;strel建议用offsetstrel替代部分矩形结构元素,或者保持strel但确认版本支持。我跑的时候也踩过edge的坑,后来统一改成edge(I1,'sobel',0.15,'both'),识别率没有明显变化,问题就解决了。新版本本身不是问题,问题是你得先确认哪些旧函数已经被“温柔地废弃”了。

6. 二次开发:从模板匹配走向特征分类

6.1 给定位阶段加颜色先验

目前的蓝色统计定位依赖二值图,但不同省份车牌底色不同,蓝底白字、黄底黑字、绿底黑字都有。如果你要支持多底色车牌,在灰度化之前先用原彩色图做一次颜色掩膜,提取蓝色、黄色、绿色三种底色区域,再和边缘检测结果做交集运算,定位的准确率和鲁棒性都会明显提升。具体可以这样改:在灰度化之前用HSV颜色空间分割出目标底色区域,生成一个掩膜图像,然后把这个掩膜和边缘检测结果做与操作,再走原有的形态学流程。

6.2 把模板匹配替换成特征分类

如果对识别准确率不满意,优先改识别这一块。我的建议是不需要上深度学习,用传统的HOG特征加SVM分类器,几十张字符样本就能达到比模板匹配好得多的效果。关键点是保留现有的字符分割流程,只替换最后的识别环节,这样改动面最小,也最容易验证效果。实现时用extractHOGFeatures函数抽取每张字符图的特征,用fitcecoc训练多分类器,训练样本可以从现有模板库扩充,加上不同光照下的车牌截图,每类字符准备20到50张左右,效果就足够稳定。

6.3 评估的方法

每次改动后,准备一个包含几十张不同场景图片的测试集,跑完记录定位、分割、识别三个阶段各自的成功率。定位成功不等于分割成功,分割成功不等于识别成功,所以三阶段要分开统计。我自己每次改完代码,都会在main函数末尾加一小段统计代码,输出每个字符的匹配差异值和最终识别结果,这样能快速判断错误是出在分割缺笔画还是出在识别特征不足。从那以后我每次改这个项目,都强制把测试集完整跑一遍,对比分阶段成功率再决定下一步动哪里,这个习惯帮我少走了很多弯路,希望帮到你。

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

返回列表