简介:这份Matlab车牌识别课程设计报告面向电子信息、自动化及计算机相关专业学生,服务于图像处理与模式识别课程设计、答辩准备和入门实践。内容围绕车牌定位、字符分割与字符识别三条主线展开,串联原始图像读取、灰度化、背景估计与增强、二值化、候选区域筛选、垂直投影分割,以及模板匹配和人工神经网络识别等方法,并补充系统测试、光照与车牌质量影响、技术挑战及优化思路。包内共1个docx文件,约1.37MB,以完整设计报告为主体,正文含设计目的、原理、详细步骤、结果分析、总结体会与参考文献,并穿插Matlab实现代码与流程说明。已有116人学习,读者可据此对照任务书组织报告章节、复现关键处理环节、补充实验分析与测试结论,也可参考其中的算法脉络和文档结构完善课程设计或答辩材料。
1. 从一张换掉就崩的脚本说起
见过太多做课程设计的同学,把两百行 Matlab 全塞进一个main.m,跑通一张图就截图交差,换张图马上报错——因为车牌位置、阈值、保存路径全写死了。这份基于 Matlab 的车牌识别课程设计报告附源代码,真正值得留下来的不是报告排版的工整,而是里面那条完整的图像处理流水线:读彩色图、灰度化、形态学估背景、背景差分增强、自适应阈值二值化、Canny 边缘、开闭运算、连通域标记配长宽比筛选、水平投影校平、垂直投影切字、22×14 归一化、模板库比对输出。报告正文按 2.1 到 2.12 一节一段代码地拆开写,适合刚上手数字图像处理、想拿一套能跑通的完整流程改造成自己作品的人,也适合已经会调 API、但没系统走过一遍「车牌从像素到字符串」全链路的开发者。下面按能跑、能读、能改的顺序往下拆。
2. 工程全景:十二段代码怎么串成一条流水线
2.1 报告结构与代码文件的对应关系
这份资源的物理形态是「课程设计报告 docx + 配套源码」,报告正文的第三章「详细设计步骤」是主体,从 2.1 到 2.12 一共十二个小节,每一节的节奏高度统一:先一段 Matlab 代码,再一段文字解释这段代码在干什么,最后贴一张figure出来的结果图。这种写法对交付报告很友好,但对应的代码是散在文档里的片段,直接复制粘贴是跑不起来的,得先按执行顺序拼回一个脚本。
按照报告里出现的顺序,实际执行链路是这样的:imread读原图 →rgb2gray转灰度 →strel('disk',13)加imopen估背景 →imsubtract差分 → 极值算阈值加im2bw二值化 →edge(...,'canny')提边缘 → 三次imclose/imopen形态学滤波 →bwlabel标记加imfeature取特征 → 长宽比筛选出车牌候选 → 垂直投影和水平投影做峰谷分析 →fit(...,'poly1')拟合算倾角 →imrotate校平 → 二次投影去上下边框 → 垂直投影找七个字符中心 →imresize归一化加imwrite存样本 → 与样本库逐张比对输出结果。
常见做法是把这套东西整理成三个文件:main.m只负责调度和图窗展示,plate_locate.m把预处理到车牌子图这一大段封成函数,plate_segment.m负责切字和识别。原报告的写法是全部平铺在一个文件里,好处是变量在 workspace 里都是可见的,调试时随便figure一下就能看中间结果,这对刚接触图像处理的读者反而是优点——你能看见每一步到底变成了什么样。
| 报告小节 | 干什么 | 关键函数 | 产物 |
|---|---|---|---|
| 2.1 | 读入彩色原图 | imread、imshow | Scolor |
| 2.2 | 灰度化 | rgb2gray | Sgray |
| 2.3 | 形态学开运算估背景 | strel、imopen | Bgray |
| 2.4 | 背景差分增强 | imsubtract | Egray |
| 2.5 | 阈值二值化 | im2bw | bw2 |
| 2.6 | 边缘提取 | edge(canny) | grd |
| 2.7 | 开闭运算滤波 | imclose、imopen | bg2 |
| 2.8 | 连通域标记与筛选 | bwlabel、imfeature | L、Area |
| 2.9–2.10 | 投影分析与倾角校正 | sum、fit、imrotate | sbw、angle |
| 2.11 | 去边框与字符分割 | sum、imresize | SegBw2 |
| 2.12 | 模板匹配识别 | 矩阵相减取误差 | 车牌字符串 |
2.2 关键参数与它们各自影响哪一段
这套算法最劝退的地方不是代码量,而是参数多。同一个13,放在 640×480 的图上刚好合适,换成 1920×1080 就完全失效,因为结构元的物理尺度变了。把参数和它影响的环节对上号,调试的时候才知道该动谁。
| 参数 | 出现位置 | 报告中的值 | 影响环节 | 调大的后果 |
|---|---|---|---|---|
| 圆盘结构元半径 | 2.3 背景估计 | 13 | 背景亮度场的平滑尺度 | 车牌整体被吸进背景,差分后车牌消失 |
| 阈值系数 | 2.5 二值化 | 1/3 | 前景占比 | 前景变少,字符笔画断裂 |
| Canny 参数 | 2.6 边缘 | 默认 | 边缘密度 | — |
| 闭运算结构元 | 2.7 横向连通 | [5,19] | 字符横向粘连 | 车牌和车身连成一片 |
| 开运算结构元 | 2.7 纵向去噪 | [19,1] | 竖直线条抑制 | 车牌上下边框被切掉,字符也被切 |
| 长宽比区间 | 2.8 区域筛选 | 约 2.2–4.2 | 候选车牌数量 | 候选变少,漏检 |
| 拟合阶数 | 2.10 倾角 | poly1 | 旋转角精度 | — |
| 归一化尺寸 | 2.11 字符 | [22 14] | 模板尺寸 | 必须和样本库一致,否则没法比 |
国内蓝牌的标准尺寸是 440mm×140mm,宽高比约 3.14,所以筛选区间定在 3 附近是合理的。但如果你的图是手机竖拍后裁过的,或者在透视角度很大的场景下拍的,实际投影出来的宽高比会偏离这个值,区间就得放宽。
2.3 环境准备与跑通第一张图
MATLAB 版本上,报告里的imfeature是 6.x 时代的老函数,7.x 之后被regionprops取代,fit则需要 Curve Fitting Toolbox。这两点决定了你大概率不能原样复制粘贴,必须做两处替换,后面避坑那一节会细说。
跑通的最小路径是这样的:装好 MATLAB 和 Image Processing Toolbox,把配套源码里的.m文件和解码后的图片放到同一个目录,把imread('3.jpg')里的文件名改成你实际有的图,运行。第一次跑建议不要直接上复杂的实拍图,先用一张光照均匀、车牌占画面比例较大、车牌基本水平的图,把整条链路走通,确认每一步的figure都能弹出来且有合理内容。
% 跑通前的三处必改 Scolor = imread('3.jpg'); % ① 换成自己目录下真实存在的图片名 fname = strcat('F:\MATLAB\work\sam\image', int2str(k), '.jpg'); % ② 换成存在的目录 Feastats = imfeature(L,'basic'); % ③ 新版改为 regionprops(L,'Area','BoundingBox')第 ① 处不改会直接报「文件不存在」;第 ② 处不改会在imwrite那一步报「无法写入文件」,而且这个报错出现在循环里,前面的定位、分割、显示全都正常,很容易误判成识别算法出了问题;第 ③ 处不改会报Undefined function 'imfeature',一眼就能看出来,反而是最好处理的。
跑通之后不要急着换图,先做一件事:把每个中间变量imwrite存成 png 落盘。Sgray、Bgray、Egray、bw2、grd、bg1、bg3、bg2全存一遍,一共八个文件。后面换图失败的时候,把新旧两组图并排一放,哪一步开始不一样,问题就在哪一步,比在代码里反复disp快得多。这是我自己踩过坑之后养成的习惯,图像处理的黑匣子属性太强,不落盘就只能靠猜。
3. 预处理与车牌定位:从彩色像素到候选区域
3.1 灰度化与形态学背景估计
彩色图三通道在存储和运算上都是三倍开销,而车牌定位本质上是找局部高对比区域,颜色信息在定位阶段用处不大,所以第一步统一转灰度。rgb2gray用的是 0.2989R + 0.5870G + 0.1140B 的加权,比简单取平均更符合人眼亮度感知,转完之后字符和底牌的亮度差保留得更好。
接下来这一步是整个流程里比较巧的地方:不是直接对灰度图做二值化,而是先用一个大结构元做形态学开运算,得到一张模糊的「背景亮度场」。开运算是先腐蚀后膨胀,腐蚀会把比结构元小的亮细节(字符笔画、车标、反光点)全部抹掉,膨胀再把剩下的亮区恢复回原来的边界,结果就是一张只有车身材质和光照渐变、没有细节的图。
% Step1 读原始彩色图 Scolor = imread('3.jpg'); % Step2 灰度化,后续所有算子都在这张单通道图上跑 Sgray = rgb2gray(Scolor); % Step3 半径 13 的圆盘结构元做开运算:抹掉比它小的亮细节,剩下低频亮度场 se = strel('disk', 13); Bgray = imopen(Sgray, se); figure, imshow(Bgray), title('背景图像');半径13是这个算法的核心尺度参数。它要满足两个条件:明显大于字符笔画的像素宽度(640×480 下笔画大概 3–5 像素),这样才能把字符从背景里抹掉;同时明显小于车牌的像素宽度(同分辨率下车牌宽度大概 100–150 像素),这样车牌整体才不会被当成「细节」一起吃掉。取 13 正好落在中间。如果你把图统一缩放到 640 宽,这个值可以直接用;如果保持原图 1920 宽,笔画和车牌的像素尺度都放大了三倍,半径也该跟着放大到 40 左右,否则背景图里会残留字符轮廓,差分之后就变成噪点。
3.2 背景差分增强与自适应阈值二值化
有了背景图,下一步就是把原图和背景相减。车牌区域的字符是高频强对比,减完之后数值很大;车身平缓区域减完接近 0;比背景暗的区域会被截断到 0(imsubtract是饱和减法,不做负数回绕)。这一步做完,车牌区域就变成了整张图里最亮的几块之一。
% Step4 原图减去背景亮度场,把局部高对比区域抬出来 Egray = imsubtract(Sgray, Bgray); figure, imshow(Egray), title('增强黑白图像'); % Step5 用图像自身的动态范围推一个相对阈值,而不是写死 128 fmax1 = double(max(Egray(:))); % 差分图最大值 fmin1 = double(min(Egray(:))); % 差分图最小值 level = (fmax1 - (fmax1 - fmin1)/3) / 255; % 取动态范围上 2/3 处,归一到 [0,1] bw22 = im2bw(Egray, level); % 转二值 bw2 = double(bw22); % 转 double 供后续 edge 使用level这个写法是纯经验公式,含义是把「最大值往下退三分之一动态范围」的位置当分界线,等价于把最亮的那三分之一像素判成前景。之所以不用graythresh(Otsu),是因为差分之后的直方图并不是干净的双峰分布——大面积车身是 0,车牌是一小撮高值,中间还有各种反光和纹理,Otsu 求出来的阈值经常被车身区域拉偏。报告里这个写法的好处是它跟着每张图自身的动态范围走,不会因为图整体偏暗就全黑。
但它也有明确的失效条件:如果图里根本找不到车牌(比如车牌被完全遮挡),fmax1就是某个噪点的值,阈值会算得非常离谱,二值化结果是一片雪花。工程上我一般会加一道保险,判断fmax1 - fmin1是否过小,过小就直接判定为「未检测到车牌」返回,不要往下走。
3.3 Canny 边缘提取与双方向形态学滤波
二值图拿到之后提边缘。Canny 相比 Sobel、Prewitt 的优势在于它做了非极大值抑制和双阈值连接,出来的边缘是单像素宽的细线,不会因为梯度响应强就糊成一片粗边。这对车牌定位很重要,因为后面要靠形态学把字符连成一个实心块,边缘太粗会连成一片无法区分。
% Step6 Canny 提边缘,得到单像素宽的车牌字符轮廓 grd = edge(bw2, 'canny'); % Step7 三次形态学操作,把散落的字符轮廓拼成车牌大小的实心块 bg1 = imclose(grd, strel('rectangle', [5, 19])); % 横向闭:连上同一行相邻字符 bg3 = imopen(bg1, strel('rectangle', [5, 19])); % 开:去掉小块噪点 bg2 = imopen(bg3, strel('rectangle', [19, 1])); % 纵向开:压掉竖直细长干扰三次操作的分工要看清。第一次是闭运算,结构元[5,19]表示 5 行高、19 列宽的横向长条,闭运算先膨胀后腐蚀,膨胀时这个横条会把水平方向上距离小于 19 像素的两个字符连起来,七个字符连成一整块;第二次用同样尺寸做开运算,把面积小于结构元的孤立噪点抹掉;第三次换成[19,1]的竖长条做开运算,专门对付垂直方向上的细长条——比如车牌上下边框被边缘检测提出来之后形成的两条横线,或者车身缝隙。
这里有个容易被忽略的点:strel的参数是[行, 列]不是[宽, 高]。写成[19,5]意思完全不同,会变成竖长条,横向连不上字符。我第一次看这段代码的时候在这个地方愣了半天,因为直觉上「横向结构元」应该先写宽度。
3.4 连通域标记与长宽比筛选
形态学处理完之后,图上剩下若干块白色区域,其中一块是车牌,其余可能是车灯、格栅、贴纸、后视镜。bwlabel做八连通标记,给每块区域一个整数编号,然后用regionprops一次性取出每块区域的面积和外接矩形。
% Step8 八连通标记 + 提取几何特征 [L, num] = bwlabel(bg2, 8); stats = regionprops(L, 'Area', 'BoundingBox'); % 新版替代 imfeature Area = [stats.Area]; Bbox = cat(1, stats.BoundingBox); % 每行 [x y w h] % Step9 用长宽比 + 面积双条件筛出车牌候选 cand = []; for k = 1:num w = Bbox(k,3); h = Bbox(k,4); if w < 30 || h < 10, continue; end % 太小的直接丢 r = w / h; % 蓝牌标准 440/140 ≈ 3.14 if r > 2.2 && r < 4.2 && Area(k) > 400 cand(end+1) = k; end endBoundingBox的顺序是[x, y, width, height],x 是列方向、y 是行方向,宽高比要用w/h而不是h/w。这个顺序在老版本imfeature里也是一样的,所以从旧代码迁移过来时不用担心。
筛选条件里,长宽比区间2.2–4.2覆盖了绝大多数正面或小角度拍摄的蓝牌;面积阈值400是为了滤掉那些刚好长宽比也接近 3 的小噪点,这个数字跟图像分辨率直接相关,换分辨率要重算。如果候选不止一个,报告的思路是取宽高比最接近 3.14 的那个,或者按面积取最大的——正面拍摄时面积最大的通常就是车牌,倾斜角度大时按比值更稳。
3.5 水平投影峰谷分析与拟合旋转校正
切出车牌子图之后,还有一个必须解决的问题:车牌可能带几度到十几度的倾斜。倾斜会让垂直投影的字符间隙变得模糊,切字就会错位。报告的做法是先对子图逐行求和得到水平投影,在投影曲线上找车牌上边框和下边框的位置,再拿这些边界点做一阶线性拟合,斜率的反正切就是倾角。
% Step10 水平投影:逐行求 1 的个数,得到一条反映行方向亮度的曲线 histrow = sum(sbw1, 2); % 列向量,元素 i 是第 i 行白点数 histcol = sum(sbw1, 1); % 行向量,元素 j 是第 j 列白点数 % Step11 对车牌上边缘的点做一阶拟合,斜率换算成角度 % xdata/ydata 为上边缘每列第一个白点的行号与列号 fresult = fit(xdata', ydata', 'poly1'); % 需要 Curve Fitting Toolbox angle = atan(fresult.p1) * 180 / pi; % 弧度转角,p1 即斜率 % Step12 反向旋转把车牌摆正,crop 保证输出尺寸不变 subcol = imrotate(subcol1, angle, 'bilinear', 'crop'); sbw = imrotate(sbw1, angle, 'bilinear', 'crop');fit返回的是cfit对象,p1是poly1模型的一次项系数,也就是直线斜率。角度算出后要过一遍符号:MATLAB 的图像坐标系 y 轴朝下,如果拟合出来车牌是「左低右高」,算出的角度符号和imrotate需要的旋转方向可能相反,转完会更歪。判断方法很简单,转完肉眼看一眼,歪了就angle = -angle。
imrotate的第三个参数'bilinear'是双线性插值,比默认的最近邻平滑,字符边缘不会出现锯齿台阶;第四个参数'crop'让输出图保持和输入一样的尺寸,不加的话 MATLAB 会自动扩边把旋转后的整幅图都装下,尺寸一变后面的坐标全乱。这两个参数都不是可选项。
4. 字符分割与识别:从车牌子图到七段字符串
4.1 去上下边框与字符高度估计
校平之后的车牌子图还是带边框的,上下两条横线和左右两条竖线如果不去掉,投影曲线两端会被撑起两个高峰,字符的峰谷就淹在里面了。去边框靠的还是水平投影:边框在投影上表现为两端各一个又高又窄的峰,字符区域在中间是一段连续的起伏。
% Step13 水平投影找字符带的上下界 histrow = sum(sbw, 2); % 每行白点数 markrow = find(histrow > mean(histrow)); % 高于均值的行视为有效行 % 取连续段中最长的一段作为字符带,两端即上边框下沿与下边框上沿 maxhight = rowbot - rowtop + 1; % 字符带高度 sbw2 = sbw(rowtop:rowbot, :); % 裁掉上下边框后的子图用「高于均值」当判据是个粗糙但有效的做法,因为字符行的白点数明显多于纯边框行和空白行。mean(histrow)这个阈值在有噪声的情况下会偏低,导致边框行也被算进来;更稳的写法是取0.5 * max(histrow)或者用中位数,值得在改造时换掉。
maxhight拿到之后不要只当个中间变量,它有两个实际用途:一是后面切字符时校验每个字符的高度是否合理,差太多说明切歪了;二是作为尺度参照,判断这张车牌是不是太小、压根不该进识别环节。
4.2 垂直投影找字符中心与最大字符宽度
去完上下边框,剩下的核心工作是把七个字符的位置定出来。垂直投影(逐列求白点数)在字符笔画的位置形成高柱,在字符间隙的位置形成低谷,找谷底就能找到分割点。但直接找谷底不稳,因为字符内部也有间隙(比如「8」的中间),报告用的是「先找下降点、再推中心、再用中心距推最大宽度」的迂回路线。
% Step14 对垂直投影做峰谷分析,拿到每个字符的上升点与下降点 histcol = sum(sbw2, 1); % markcol 存下降点,markcol1 存上升点(具体由阈值扫描得到) n1 = numel(markcol) - 1; % Step15 由上升点/下降点推每个字符的中心列坐标 for k = 1:n1 markcol3(k) = markcol(k+1) - markcol1(k+1); % 下降点 markcol4(k) = markcol3(k) - markcol(k); % 字符宽度 markcol5(k) = markcol3(k) - double(uint16(markcol4(k)/2)); % 字符中心 end % Step16 中心距最大的一处 = 汉字与字母的分界;去掉它后剩下的最大值 = 最大字符宽度 markcol6 = diff(markcol5); maxs = max(markcol6); findmax = find(markcol6 == maxs); markcol6(findmax) = 0; maxwidth = max(markcol6);这段逻辑里最巧妙的是findmax的用法。国内蓝牌第一位是汉字(省简称),第二位是字母,汉字和字母之间的间距明显大于后面字母数字之间的间距,所以「中心距最大的那一处」几乎必然是第 1、2 个字符的分界。拿到这个分界点findmax,往前往后各取几个中心位置,就能把七个字符全定位出来,不用去猜字符个数。
markcol4(k) = markcol3(k) - markcol(k)这行算的是单个字符宽度,配uint16取半是为了中心位置落在整数列上。diff(markcol5)得到相邻中心的间距序列,findmax是最大间距的索引。
4.3 切割、归一化与样本库落盘
有了七个中心位置和统一的最大宽度,切割就变成简单的矩形裁剪加缩放。每个字符按「中心 ± 最大宽度/2」裁出来,然后统一缩放到 22 行 14 列——这个尺寸来自报告里样本库的规格,所有模板必须和待识别字符完全一致,否则逐像素相减没有意义。
% Step17 按中心位置切出七个字符,统一归一化后落盘建库 l = 1; [m2, n2] = size(subcol); for k = findmax-1 : findmax+5 % 从汉字到第 7 位,共 7 个 cleft = markcol5(k) - maxwidth/2; cright = 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]); % 必须与模板尺寸一致 imwrite(SegBw2, fullfile(sampleDir, ['image' int2str(k) '.jpg']), 'jpg'); l = l + 1; end越界钳制这两段不能省。车牌最左和最右的字符经常贴着边缘,cleft算出来是负数或者cright超出列数,MATLAB 会直接报索引越界。钳制之后宽度可能不足maxwidth,但imresize会强行拉到 22×14,影响可控。
imwrite那行的路径F:\MATLAB\work\sam是典型的写死路径,换台机器就崩。工程上的做法是先if ~exist(sampleDir,'dir'), mkdir(sampleDir); end,再用fullfile拼。样本库的命名也要规范,image1.jpg到image7.jpg只表示位置,不表示字符是什么,识别的时候得靠另一张对照表或者干脆把文件名改成字符本身,比如京.jpg、A.jpg、8.jpg,省掉一层映射。
4.4 模板匹配打分与结果输出
识别环节在报告里用的是最朴素的模板匹配:把待识别字符的 22×14 二值矩阵和样本库里的每个模板逐像素做差,绝对值求和,误差最小的那个就是识别结果。这个方法的优点是零训练、可解释、错了一眼就能看出为什么错;缺点是对字体、缩放、断裂极其敏感。
% Step18 模板匹配:逐张相减取绝对误差,最小者为识别结果 bestErr = inf; bestLabel = '?'; for i = 1:numel(samples) d = sum(abs(double(charImg(:)) - double(samples{i}(:)))); if d < bestErr bestErr = d; bestLabel = labels{i}; end end fprintf('第 %d 位识别结果:%s(误差 %d)\n', pos, bestLabel, bestErr);误差值bestErr一定要打印出来,这是判断结果可不可信的唯一依据。实践里误差明显高于同批其他字符的那一位,基本就是错的——报告里提到那个失败案例把 G 认成 B、K 认成 A、0 认成 8,都是形状高度相似的字符对,误差往往只差几十个像素。给误差加一个上限,超过就输出「?」而不是强行给一个错答案,比什么都认出来更有用。
样本库的规模决定了上限。报告里只用了极少数模板,换个字体、换个省份简称就废。做个能用的版本,至少每个汉字、每个字母、每个数字各准备 3–5 个不同来源的样本,误差取最小值而不是平均值。
5. 避坑与常见问题:六条跑不通时的排查记录
5.1 报错Undefined function 'imfeature'
现象:把报告里的代码原样复制进新版 MATLAB,运行到区域特征那一步直接中断,报函数未定义。
原因:imfeature是 MATLAB 6.x Image Processing Toolbox 里的函数,从 7.x 开始被regionprops取代并移除,报告写作年代较早,代码没跟着更新。
解决:imfeature(L,'basic')整体替换为regionprops(L,'Area','BoundingBox','Centroid'),取字段的方式也从Feastats.Area改成[stats.Area]这种逗号列表拼接。字段名本身没变,改完逻辑完全一致。
5.2 报错Undefined function 'fit'或提示需要许可证
现象:算车牌倾角那一行报错,或者弹窗提示 Curve Fitting Toolbox 未授权。
原因:fit属于 Curve Fitting Toolbox,不是基础 MATLAB 和 Image Processing Toolbox 自带的,很多精简安装或学生版没装。
解决:换polyfit。p = polyfit(xdata, ydata, 1)返回[斜率, 截距],斜率就是p(1),后面对atan的调用完全不用改。polyfit是基础函数,任何版本都有。这也是我后来写图像处理代码的默认选择——能不用工具箱就不用,交作业和移植都省事。
5.3 换一张图,二值化结果全白或全黑
现象:原图能跑,换一张阴天拍的或者逆光的,bw2出来是一片纯白,后面所有步骤连锁失效。
原因:level = (fmax1-(fmax1-fmin1)/3)/255依赖图像自身的动态范围。低对比度的图fmax1-fmin1很小,算出来的level逼近 0,于是几乎所有像素都判为前景;反过来高对比度图里如果有个强反光点,fmax1被拉到极值,level抬高,前景变少,字符断裂。
解决:加一层保护——先判断fmax1 - fmin1是否小于某个下限(比如 20),小于就说明这张图里根本没有可用的车牌对比度,直接返回失败而不是硬算。另外可以试graythresh(Egray)做兜底,两个阈值各跑一遍,取前景面积落在合理区间(比如占全图 5%–30%)的那个。这个「按前景占比反推阈值是否合理」的检查,是我后来在所有二值化环节都会加的。
5.4imwrite报无法写入,但前面所有显示都正常
现象:定位、分割、figure全部正常弹窗,唯独最后存样本库那一步报错,程序中断。
原因:路径F:\MATLAB\work\sam\image写死在代码里,当前机器上没有这个盘和目录,或者没有写权限。
解决:改成sampleDir = fullfile(pwd,'samples'); if ~exist(sampleDir,'dir'), mkdir(sampleDir); end,然后imwrite(..., fullfile(sampleDir, ['image' int2str(k) '.jpg']))。顺便说一句,这个报错之所以容易被误判,是因为它出现在循环末尾,前面的subplot已经把所有图都画出来了,视觉上「程序跑成功了」,实际只是没存下来。
5.5 字符切出七个但整体错位,最后一位是车牌边框
现象:分割出来的图里前六位看着正常,第七位是一条竖线或者空白,识别结果稳定错一位。
原因:findmax定位的是汉字与字母的最大中心距,但如果垂直投影的阈值没调好,最右侧的边框被误当成一个字符峰,中心位置整体右移一格;或者车牌只有六位、第七位本来就空。
解决:先校验findmax的合理性,它应该在 1 到 3 之间(汉字和字母的分界不会出现在很后面);再校验切出来的每个字符宽度是否都在0.6*maxwidth到1.4*maxwidth之间,超出范围的位置直接标记为无效而不是强行识别。加完这两道校验,错位问题基本能拦住。
5.6 换了白底黑字或黄底黑字的车牌,识别全乱
现象:蓝底白字的图跑得很好,换成其他颜色的车牌,定位出来的区域是反的,字符全是黑的底白的字。
原因:整套算法的极性假设是「亮字符 + 暗底牌」,二值化之后前景是 1。白底黑字刚好相反,im2bw出来前景背景翻转,后面的投影、切割全部失效。
解决:报告本身也提到了这一点——对白底黑字需要在定位之后加一步sbw = imcomplement(sbw)把 0/1 翻转回来,字符分割算法可以原样复用。判断要不要翻转,看二值图里前景像素占比,超过一半就说明极性反了。黄底黑字则更麻烦一些,rgb2gray之后黄底和黑字的亮度差不像蓝白那么极端,阈值那一步要单独调。
6. 二次开发:把它改成能反复验证的小工具
原版是「一个脚本跑一张图,输出七张字符小图和一个车牌号」,这个形态用来交报告足够,但想真正拿它练手或者做毕设,得先做三件事。
第一件事是切片成函数。把main.m里的平铺代码拆成locatePlate(img)返回车牌子图加倾角、segmentChars(sbw)返回七个 22×14 矩阵、recognize(chars, samples)返回字符串和每位误差,主脚本只负责遍历目录、调用、汇总。这么改的好处是每一段都能独立喂输入测,定位错了不用管切字,切字错了不用管识别——不然一次跑完只知道「结果错了」,不知道错在哪一段。
第二件事是换掉写死的样本库。把samples/下的文件命名成字符本身,比如京_01.jpg、A_03.jpg,加载时用dir加extractBetween解析出标签,存成一个struct数组。然后写一个小脚本,把已知答案的车牌图批量跑一遍,统计「定位命中率、切字正确率、字符准确率」三个数,每次改参数都跑一遍看曲线往哪走。这一步没有捷径,我试过只靠肉眼看单张图调参,结果是调 A 图好 B 图坏,来回横跳。
第三件事是给识别加兜底。模板匹配的误差分布是判断结果可信度的唯一信息,可以统计每类字符在样本库内部的平均类内距离,识别时如果某一位的最近邻误差超过类内平均距离的 1.5 倍,就输出?而不是硬给答案。整牌七位里有两位以上是?,就把这张图判为拒识,转人工。拒识率这个指标在做课程设计时通常没人看,但它恰恰是真实场景里最重要的一个数。
想再往前一步,可以把模板匹配换成「HOG 特征 + 最近邻」,或者干脆用ocr函数做对照实验,看看手工模板和通用 OCR 在你这批图上的差距,两者的错误模式往往互补,拿来写分析很有说服力。还有一个常被忽略的优化点:strel('disk',13)的半径不该是常数,应该按车牌高度的估计值动态算,比如半径 = round(0.1 * 车牌高度),这样换分辨率就不用重新调参。
从那以后我每次做图像处理相关的东西,都强制自己先把中间结果全部落盘、再写校验条件、最后才看最终输出,宁可多写二十行if也不让程序在错误的状态下继续往下跑。图像处理这行的错都是静默传播的,前一步偏一点,后面全偏,但不报错。希望这些能帮你在跑这份课程设计时少走几个弯路。
本文还有配套的精品资源,点击获取