简介:以 MATLAB 为开发工具的车牌识别完整实现文档,面向图像处理初学者、智能交通方向学生及课程设计开发者,围绕车牌自动识别这一典型模式识别任务,梳理从算法原理到程序实现的完整脉络,涵盖图像预处理、边缘检测、车牌定位、字符分割与字符识别五个核心环节。整份文档为单一 doc 文件,压缩包约 1.33MB,篇幅紧凑,便于本地查阅与二次整理。内容结合开运算提取背景、灰度图与背景相减增强、最佳阈值二值化、边缘提取及垂直投影法分割字符等步骤展开,并对比模板匹配与人工神经网络两类识别思路,讨论车牌污损、倾斜、反光、光照不均等影响识别率的问题及改进方向。文档已有 877 人学习,可作 MATLAB 图像处理练手项目、课程作业参考或算法复现的起步材料,读者能据此搭建可运行的车牌识别流程,再按需调整阈值、定位与识别策略。
1. 一套能跑通的 MATLAB 车牌识别,卡点从来不在识别那一步
很多人第一次做基于 MATLAB 的车牌识别,会把精力全押在最后的字符识别上,结果发现前面的定位差几个像素,后面怎么调模板都没用。这套完整版的实现路线是:彩色图读入 → 灰度化 → 形态学开运算估背景 → 灰度差分增强 → 最佳阈值二值化 → Canny 边缘检测 → 矩形结构元素开闭运算 → bwlabel 连通域标记 → 宽高比筛车牌 → 线性拟合求倾角 → imrotate 矫正 → 水平/垂直投影去边框 → 单字符切割 → 模板匹配输出号码。整条链路只依赖 MATLAB 基础工具箱加图像处理工具箱,不碰深度学习框架,适合课程设计、图像处理大作业,也适合想真正搞懂传统 OCR 流程的人。下面按模块拆开,每个函数都给出可直接粘进.m文件的代码和参数含义。
2. 灰度化与形态学背景估计:把车牌从复杂背景里抬出来
2.1 从 imread 到 rgb2gray:先把三通道压成一通道
彩色图每个像素占 3 个字节,后续所有形态学、投影、边缘操作都要在单通道上算。不转灰度直接处理,strel和edge会直接报维度不匹配,这是一上手就撞的墙。
clear; close all; Scolor = imread('3.jpg'); % 读入彩色图像,返回 H×W×3 的 uint8 矩阵 Sgray = rgb2gray(Scolor); % 按 0.2989R + 0.5870G + 0.1140B 加权成单通道 figure, imshow(Sgray), title('原始黑白图像');rgb2gray不是简单取平均,它用的亮度权重对人眼敏感度做了补偿,绿通道占比最高。如果你的图读进来已经是单通道,这行可以直接跳过,但建议加一句if size(Scolor,3)==3判断,避免换素材时炸掉。
2.2strel('disk',13)与imopen:背景为什么用开运算估
形态学开运算的定义是先腐蚀后膨胀,效果是抹掉比结构元素小的亮目标,只保留大尺度的低频成分。字符笔画相对于整张图属于“小目标”,用一个半径 13 的圆盘去开运算,字符被吃掉,剩下的就是缓慢变化的背景光照。
s = strel('disk', 13); % 圆盘结构元素,半径 13 像素 Bgray = imopen(Sgray, s); % 开运算:先腐蚀再膨胀,估出背景 figure, imshow(Bgray), title('背景图像');这里半径的选择直接决定成败。它必须大于字符笔画宽度,才能把字符完整吞掉;但又不能大到把整块车牌区域平滑进去。800×600 的图,13 是个能用的起点;如果输入图只有 320×240,这个值要降到 8 左右。反过来,用strel('rectangle',[15,15])也行,但矩形元素在倾斜车牌上会带方向性残留,圆盘各向同性更稳。
2.3imsubtract做差分增强:负值截断恰好是我们要的
灰度图减去背景图,字符区域因为比背景暗,减完仍是正数;背景区域减完接近 0。这一步本质是高通滤波。
Egray = imsubtract(Sgray, Bgray); % 逐元素相减,负值自动截到 0 figure, imshow(Egray), title('增强黑白图像');imsubtract和imabsdiff的区别要讲清楚:imabsdiff取绝对值,把背景里比字符亮的部分也翻成正值,反而引入噪声;imsubtract只保留“比背景暗”的成分,正对车牌字符是深色这个先验。
| 参数 | 作用位置 | 取值偏小 | 取值偏大 |
|---|---|---|---|
| disk 半径 | 背景估计尺度 | 字符残留,差分图带纹理 | 车牌被一起抹平,差分图接近全黑 |
| 图像短边长度 | 决定半径基准 | 建议取 short/50 起步 | 超过 short/20 基本不可用 |
| 是否转 double | 影响减法精度 | uint8 相减会截断,必须转 | double 内存翻 8 倍,大图注意 |
提示:如果换素材后发现差分图一片灰,先别动后面的阈值,回头把 disk 半径调小一半再看。
3. 自适应阈值二值化与 Canny 边缘检测的门限取值
3.1 阈值取最大最小值的三分之一,这个公式在做什么
原实现里的阈值不是graythresh算出来的,而是用图像动态范围硬推:
fmax1 = double(max(max(Egray))); % 增强图的最大灰度 fmin1 = double(min(min(Egray))); % 增强图的最小灰度 level = (fmax1 - (fmax1 - fmin1)/3) / 255; % 归一化到 [0,1] 的阈值 bw22 = im2bw(Egray, level); % 二值化 bw2 = double(bw22); % 转 double,供 edge 使用 figure, imshow(bw2), title('图像二值化');(fmax1-fmin1)/3是从最大值往下退三分之一量程,含义是保住动态范围上部约 67% 的像素为前景,宁可多留字符笔画也不漏。这个策略偏保守,好处是细笔画不会被误杀,代价是噪点也会一起保留,所以后面必须靠形态学擦干净。
如果想换成自动阈值,一行就够:level = graythresh(Egray);。它用的是 Otsu 类间方差最大化,对双峰直方图的图更准,但车牌区域占比很小时容易把阈值压低,字符被切碎。我的做法是两个都跑一遍,看二值图的连通性再定。
3.2 Canny 边缘检测:对二值图和对灰度图做,结果差在哪
grd = edge(bw2, 'canny'); % 对二值图像做 Canny figure, imshow(grd), title('图像边缘提取');edge的 Canny 实现内部会先做高斯平滑,再算梯度幅值和方向,然后做非极大值抑制,最后用双阈值连接边缘。对已经二值化的bw2做,等于在 0/255 的跳变上找边缘,得到的是字符和车牌的轮廓线,线宽会比较均匀。
更稳的常见做法是对增强后的灰度图做,并显式给双阈值:
grd = edge(Egray, 'canny', [0.08 0.20], 1.4);[0.08 0.20]是低、高两个阈值,低于低阈值的梯度直接丢,高于高阈值的直接判为边缘,介于两者之间的只有连着强边缘才保留。1.4是高斯核标准差,值越大越平滑、边缘越少。经验上:噪声重就调大 sigma,字符断裂就调低双阈值下限。
注意:Canny 输出是 logical 类型,后面做形态学之前不用转 double,
strel系列函数能直接吃 logical。
4. 矩形结构元素开闭运算与连通域宽高比定位车牌
4.1 闭运算 [5,19] 与开运算 [5,19]:结构元素尺寸怎么定
边缘图里字符之间的缝隙是断开的,闭运算负责把它们连成一条条横向块。
bg1 = imclose(grd, strel('rectangle',[5,19])); % 先闭:连断缝 figure, imshow(bg1), title('图像闭运算[5,19]'); bg3 = imopen(bg1, strel('rectangle',[5,19])); % 再开:去孤立噪点 figure, imshow(bg3), title('图像开运算[5,19]'); bg2 = imopen(bg3, strel('rectangle',[19,1])); % 竖长条开运算 figure, imshow(bg2), title('图像开运算[19,1]');[5,19]的含义是高度 5 像素、宽度 19 像素的矩形。宽度取 19 是因为汉字字符之间的间隙在这个量级,取 5 的高度是为了不在垂直方向过度粘连。第三次[19,1]是个纵向长条,作用是敲掉高度超过 19 像素、宽度只有 1 像素的连通域——车牌是横向长条,竖直细长的基本是背景里的栏杆、雨刷残留。
| 结构元素 | 形状尺寸 | 主要干掉什么 | 调大后果 |
|---|---|---|---|
| strel('rectangle',[5,19]) 闭 | 横向短矩形 | 字符间裂缝 | 相邻字符糊成一块,无法分割 |
| strel('rectangle',[5,19]) 开 | 横向短矩形 | 零星噪点 | 车牌边缘被啃掉 |
| strel('rectangle',[19,1]) 开 | 竖向长条 | 纵向干扰连通域 | 可能连车牌自身一起删掉 |
4.2 bwlabel + regionprops 提取面积与 BoundingBox
[L, num] = bwlabel(bg2, 8); % 8 邻域连通域标记,L 是同尺寸标签矩阵 stats = regionprops(L, 'Area', 'BoundingBox'); Area = [stats.Area]; % 每个连通域的像素数 Boxes = vertcat(stats.BoundingBox); % 每行 [x y width height]这里有个版本坑必须提:老资料里写的imfeature(L,'basic')在 R2006a 之后已被移除,继续用会直接Undefined function 'imfeature',把这一行换成regionprops就行。返回的BoundingBox是[左上角x, 左上角y, 宽, 高],注意 y 是行、x 是列,和矩阵索引反着来,混了就会截错位置。
4.3 宽高比筛选与车牌子图截取
imgArea = numel(bg2); % 全图像素数 idx = find(Area > 0.004 * imgArea); % 先按面积过滤 ratio = Boxes(idx,3) ./ Boxes(idx,4); % 宽/高 keep = idx(ratio > 2.0 & ratio < 4.5); % 中国蓝牌约 440×140 ≈ 3.14 if isempty(keep), error('未找到符合宽高比的候选区域'); end [~, best] = max(Area(keep)); sel = keep(best); plateBox = round(Boxes(sel,:)); sbw1 = bw2(plateBox(2):plateBox(2)+plateBox(4)-1, ... plateBox(1):plateBox(1)+plateBox(3)-1);面积下限设在全图 0.4% 是排除小噪点,宽高比区间 2.0~4.5 是覆盖蓝牌、黄牌和新能源绿牌(绿牌略窄,约 2.9)。多候选时用max(Area)挑最大的那个,因为车牌字符块连通后通常面积最突出。截取时plateBox(2)是行起点,plateBox(1)是列起点,写反了拿到的是别的位置。
5. 线性拟合倾角矫正与投影法字符分割
5.1 水平投影定位字符上下边界
sum(sbw)对列求和得到垂直投影,sum(sbw')对行求和得到水平投影。字符区域在这两条曲线上表现为连续的高峰段。
histcol = sum(sbw); % 垂直投影(列方向累加) histrow = sum(sbw'); % 水平投影(行方向累加) figure, subplot(2,1,1), bar(histcol); title('垂直投影(含边框)'); subplot(2,1,2), bar(histrow); title('水平投影(含边框)');上下边框在水平投影上表现为两个高而窄的峰,字符主体是中间那段宽峰。求histrow的均值作为基准,低于均值的谷底就是边框与字符的分界。
thr = mean(histrow); markrow = histrow > thr; markrow1 = diff([0 markrow 0]); % 上升沿/下降沿检测 markrow2 = histrow .* markrow; maxhight = max(markrow2); findc = find(markrow2 == maxhight); rowtop = findc(1) - floor(maxhight/2); % 峰中心往上推半高 rowbot = rowtop + maxhight; sbw2 = sbw(rowtop:rowbot, :); % 去上下边框后的车牌子图diff([0 markrow 0])是标准的跳变点提取,+1是上升沿,-1是下降沿,配合find就能拿到每一段的起止行号。这里markrow用均值做门限是最省事的写法,换光照复杂的图可以改成mean(histrow) + 0.3*std(histrow)。
5.2 线性拟合求倾角与 imrotate 插值方式选择
车牌倾斜时投影峰会糊在一起,必须先矫正。做法是取车牌上下边缘中灰度值为 1 的点,拟合一条直线,取斜率转角度。
[row, col] = find(sbw1); % 取出所有前景点的行列坐标 p = polyfit(col, row, 1); % 一次多项式拟合,p(1) 是斜率 angle = atan(p(1)) * 180 / pi; % 弧度转角度 fprintf('车牌倾角: %.2f 度\n', angle); subcol = imrotate(subcol1, angle, 'bilinear', 'crop'); sbw = imrotate(sbw1, angle, 'bilinear', 'crop');polyfit替代了原文的fit,好处是不依赖 Curve Fitting Toolbox,装了基础 MATLAB 就能跑。'bilinear'是双线性插值,比默认的最近邻平滑,字符笔画不会出现锯齿台阶;'crop'保证旋转后输出尺寸不变,否则后面按行列号截取会全部错位。角度范围建议加一句限幅angle = max(min(angle,15),-15),超过 15 度的候选点多半是拟合到了背景噪声。
5.3 垂直投影的峰谷分析切出单个字符
histcol2 = sum(sbw2); % 去水平边框后的垂直投影 thc = 0.15 * max(histcol2); % 谷底判定阈值 isChar = histcol2 > thc; d = diff([0 isChar 0]); starts = find(d == 1); % 每个字符的左边界 ends = find(d == -1) - 1; % 每个字符的右边界 chars = cell(1, numel(starts)); for k = 1:numel(starts) seg = sbw2(:, starts(k):ends(k)); seg = imresize(seg, [20 10]); % 统一到 20×10,供模板比对 chars{k} = seg; end阈值0.15*max是经验值,取太高会把笔画细的“1”“I”整列丢掉,取太低会把字符内部间隙也当成字符边界。切完后每个seg就是一帧单字符二值图,送进识别环节。
6. 模板匹配识别的归一化策略与低识别率排查
6.1 归一化尺寸与 L1 距离匹配
模板匹配的核心不是距离公式多花哨,而是尺寸必须对齐。分割出来的字符高度取决于原图分辨率,模板库是固定的,两边不统一,相减出来的差值全是尺寸误差。
tplDir = 'chars'; % 模板目录:0-9、A-Z、各省简称 files = dir(fullfile(tplDir, '*.jpg')); tpl = zeros(20*10, numel(files)); % 每列存一个展平的模板 labels = cell(1, numel(files)); for k = 1:numel(files) im = imread(fullfile(tplDir, files(k).name)); im = imresize(im2bw(im), [20 10]); % 尺寸对齐到 20×10 tpl(:,k) = double(im(:)); % 展平成 200 维列向量 [~, labels{k}] = fileparts(files(k).name); % 文件名即字符标签 end result = ''; for k = 1:numel(chars) v = double(chars{k}(:)); % 待识别字符同样展平 diff = sum(abs(tpl - v), 1); % L1 距离,逐模板 [~, mi] = min(diff); result = [result labels{mi}]; end disp(['识别结果:' result]);tpl - v利用了 MATLAB 的隐式扩展,200×N 的模板矩阵减去 200×1 的列向量,自动对每一列做差,省掉一层循环。L1 距离比平方差对二值图更合适,因为它不会被个别粗笔画像素放大。
模板库建议按“省份简称 / 数字 / 字母”分三个目录,识别时先判断字符在车牌中的位置索引,位置 1 只在省份目录里搜,位置 2 起在字母数字目录里搜。这一步能把候选集从六十多个压到三十出头,识别率提升比调距离公式明显得多。
6.2 识别率上不去的排查顺序
按出现频率从高到低排:第一查分割,把chars里每个字符imshow出来,看有没有把“8”从中间切开或者两个字符黏在一起,这是最常见的原因;第二查归一化,用imresize时有没有加'nearest'参数,双线性插值会把二值图插成灰度图,隐式转 double 后距离计算全偏;第三查模板库,模板本身如果是扫描件缩放的,跟现场图笔画粗细差太多,建议模板和待测图用同一个二值化阈值处理;第四才轮到距离公式,可以在 L1 基础上叠加一个笔画宽度的权重项,但收益通常只有一两个百分点。
手工验证时给流程加一个断点保存,把中间结果全存盘,比一次性跑完再回头猜要快得多:
save('debug.mat', 'Sgray', 'Egray', 'bw2', 'grd', 'bg2', 'sbw1', 'sbw2', 'chars');调参时只改一个变量重跑,从chars里的分割结果往前推,能定位到具体是哪一步把信息吃掉了。
本文还有配套的精品资源,点击获取