简介:这份资源是面向计算机视觉初学者与毕业设计学生的车牌识别系统MATLAB完整实现,围绕图像预处理、模板匹配、字符分割与字符识别四个环节搭建可运行流程,帮助读者理解从原始车牌图像到输出完整车牌号的工程链路。压缩包共62个文件、约554KB,以45个bmp模板图、10个m脚本、5张jpg测试图及1个asv备份和1个xls数据表为主,模板图覆盖数字与多省份汉字,脚本对应读取、切割与识别各模块,便于按流程对照调试。目前已有252人学习下载。读者可据此掌握灰度化、二值化、边缘检测、连通成分分析等图像处理方法,并借助模板匹配与分类器思路完成数字和汉字识别,同时获得一套可运行、可修改的课程设计参考代码,用于理解模式识别核心原理并在此基础上做光照与角度适应性优化。
1. 从一张糊到看不清的抓拍图说起:车牌识别系统到底要解决什么
某高校实验室门口的道闸抓拍图,车牌区域只有 90×30 像素,还带着夜间补光的过曝和雨天的反光。A 同学拿着这张图问我:车牌识别 matlab 到底能不能做出来?我的回答是能,但前提是你别一上来就想着上深度学习。课程设计和毕设场景里,真正卡住人的不是算法不够先进,而是整条链路没打通——预处理没做对,二值化阈值一塌糊涂,字符分割切歪一个像素,最后模板匹配全盘皆输。这套方案面向两类人:一是要交课程设计或毕设、需要在 MATLAB 里跑通完整流程的学生;二是要出一份可交付技术方案的从业者,想拿它当原型验证的骨架。它解决的核心问题是:在有限分辨率和复杂光照下,把「定位—校正—分割—识别」四步串成一条稳定可复现的流水线,而不是堆一堆炫技函数。
2. 需求拆解与技术选型:为什么这套方案坚持用传统图像处理
2.1 先把功能需求写成可验收的条目
做方案最怕需求写成一句「实现车牌识别」。我一般会把需求拆成能打勾的条目,否则后面验收全是扯皮。这套系统的功能边界通常这样划:
| 编号 | 功能项 | 输入 | 输出 | 验收口径 |
|---|---|---|---|---|
| F1 | 图像读取与预处理 | 单张 JPG/PNG,分辨率不低于 640×480 | 灰度、去噪后的图像 | 肉眼可见噪声下降,车牌区域不糊 |
| F2 | 车牌定位 | 预处理图 | 车牌候选区域坐标 | 单张图定位框与真实车牌 IoU 大于 0.6 |
| F3 | 几何校正 | 候选区域 | 水平校正后的车牌图 | 倾斜角残差小于 3 度 |
| F4 | 字符分割 | 校正车牌图 | 7 个字符子图 | 分割数量正确率不低于 85% |
| F5 | 字符识别 | 字符子图 | 车牌字符串 | 单字符识别率不低于 90% |
| F6 | 结果输出 | 全流程 | 标注图 + 文本 | 界面或命令行可读 |
这张表看着朴素,但它是后面所有设计决策的依据。比如 F2 的 IoU 门槛决定了定位算法不能太激进,F4 的 85% 决定了分割阶段必须留容错,而不是追求一次切准。很多同学翻车就翻在没写这张表,做到一半发现「识别率」这个词根本没法量化。
2.2 技术选型:传统流程与深度学习的取舍
为什么这套方案不走深度学习?不是不会,是场景不匹配。课程设计和毕设的交付周期通常四到八周,标注数据要自己攒,训练环境还不一定给力。传统图像处理链路每一环都可解释、可调参、可写进论文的「算法设计」章节,答辩时老师问「你这个阈值怎么定的」你能答上来,这比甩一个黑匣子模型强得多。
具体选型上,预处理用中值滤波加直方图均衡,定位用颜色特征(蓝底白字车牌在 HSV 空间的色调区间)配合形态学闭运算,校正用 Radon 变换估计倾斜角,分割用垂直投影加连通域分析,识别用归一化后的模板匹配。这套组合的优点是每一步都有明确的数学依据,缺点是参数敏感——这正是后面要专门写避坑章节的原因。
提示:如果你的题目明确要求深度学习,这套流程依然值得先做一遍,用它生成定位和分割的训练样本,比手工标注快得多。
2.3 开发环境与目录约定
MATLAB 版本建议 R2018b 及以上,主要用到 Image Processing Toolbox。目录结构我习惯这样组织,避免所有 .m 文件堆在一个文件夹里:
% 项目根目录结构约定 % plate_recognition/ % main.m % 主入口,串起全流程 % +preprocess/ % 预处理模块(包目录) % +locate/ % 定位模块 % +segment/ % 分割模块 % +recognize/ % 识别模块 % data/templates/ % 字符模板库 % data/samples/ % 测试样本 % test/ % 批量测试脚本用包目录(+前缀)的好处是函数命名不打架,调用时写preprocess.denoise(img)一眼能看出属于哪个环节。主入口main.m只做流程编排,不写具体算法,这样换掉某一环的实现时不用动主文件。
3. 总体架构与核心模块实现:四段流水线怎么串起来
3.1 主流程编排:一个函数串起四个阶段
主入口的设计原则是「薄」。它只负责读图、按顺序调用四个模块、把中间结果存下来供调试。我一般写成这样:
function result = main(imagePath) % 主流程:读取 -> 预处理 -> 定位 -> 校正 -> 分割 -> 识别 raw = imread(imagePath); if size(raw, 3) == 3 gray = rgb2gray(raw); else gray = raw; end % 阶段一:预处理,输出增强后的灰度图 clean = preprocess.denoise(gray); clean = preprocess.enhance(clean); % 阶段二:定位,返回车牌区域的边界框 [x y w h] bbox = locate.byColor(raw, clean); if isempty(bbox) warning('未定位到车牌区域,请检查图像或调整颜色阈值'); result = struct('plate', '', 'bbox', []); return; end % 阶段三:裁剪并校正倾斜 plateImg = imcrop(clean, bbox); plateImg = locate.deskew(plateImg); % 阶段四:二值化后分割字符 bw = segment.binarize(plateImg); chars = segment.split(bw); % 阶段五:逐字符识别并拼接 plateStr = recognize.batchMatch(chars); result = struct('plate', plateStr, 'bbox', bbox, 'chars', chars); end这段代码的关键在参数传递方式:每个模块只接收自己需要的输入,返回结构化的输出。bbox用[x y w h]四元组而不是逻辑掩膜,是因为imcrop直接吃这个格式,省一次转换。result里保留chars字段,是为了调试时能单独看每个字符切得对不对——这个习惯救过我很多次,识别错了到底是分割的锅还是模板的锅,一看便知。
3.2 预处理与车牌定位:颜色特征加形态学的组合拳
定位是整条链路里最容易翻车的一环。纯边缘检测在复杂背景上会炸出一堆候选框,纯颜色阈值又容易被车身同色区域干扰。我的做法是双通道投票:颜色通道先粗筛,形态学再收窄。
function bbox = byColor(rgbImg, grayImg) % 蓝底车牌定位:HSV 空间色调筛选 + 形态学闭运算 hsv = rgb2hsv(rgbImg); H = hsv(:, :, 1); S = hsv(:, :, 2); V = hsv(:, :, 3); % 蓝色调大致落在 0.55~0.75,饱和度与亮度设下限滤掉灰白区域 blueMask = (H > 0.55) & (H < 0.75) & (S > 0.4) & (V > 0.3); % 闭运算连接车牌内部断裂区域,结构元素尺寸按图像宽度自适应 seSize = max(3, round(size(rgbImg, 2) / 200)); se = strel('rectangle', [seSize, seSize * 3]); closed = imclose(blueMask, se); % 连通域分析,按面积和长宽比筛选候选 stats = regionprops(closed, 'BoundingBox', 'Area', 'Extent'); bbox = []; bestScore = 0; for k = 1:numel(stats) w = stats(k).BoundingBox(3); h = stats(k).BoundingBox(4); ratio = w / h; % 国内蓝牌长宽比约 3.14,留出透视变形余量取 2.2~4.5 if ratio > 2.2 && ratio < 4.5 && stats(k).Area > 500 score = stats(k).Extent * stats(k).Area; if score > bestScore bestScore = score; bbox = stats(k).BoundingBox; end end end end这里有几个参数值得说清楚。色调区间 0.55~0.75 是蓝底车牌的经验范围,但不同相机的白平衡会让它整体偏移,所以实际调参时我建议先把测试图的 HSV 分量画出来看一眼,别照抄。结构元素的宽高比设成 1:3,是贴合车牌横向延伸的形态,闭运算之后车牌内部的字符间隙会被填平,形成一个完整矩形。长宽比筛选区间 2.2~4.5 比理论值宽,是因为抓拍图有透视变形,卡太死反而漏检。
注意:如果测试集里混有黄牌或绿牌,颜色通道要扩展成多分支,每个分支独立跑一遍再按得分排序,不要试图用一个阈值覆盖所有底色。
3.3 倾斜校正与字符分割:投影法为什么比连通域更稳
校正环节用 Radon 变换估计倾斜角,思路是把图像旋转一系列角度,找投影能量最集中的那个角度。分割环节我倾向垂直投影而不是纯连通域,原因是车牌字符之间有铆钉和边框干扰,连通域容易把「川」这种字切成两半,而投影法看的是整列的像素密度,对断裂更宽容。
function chars = split(bwPlate) % 垂直投影分割:先去掉上下边框,再按列投影找字符间隙 [h, w] = size(bwPlate); % 裁掉上下各 15% 的边框区域,避免边框被当成字符 bwPlate = bwPlate(round(h*0.15):round(h*0.85), :); colSum = sum(bwPlate, 1); % 投影值低于阈值的列视为字符间隙 gapThresh = max(colSum) * 0.08; isGap = colSum < gapThresh; % 找出连续非间隙区间作为字符候选 chars = {}; inChar = false; startCol = 1; for c = 1:w if ~isGap(c) && ~inChar inChar = true; startCol = c; elseif isGap(c) && inChar inChar = false; seg = bwPlate(:, startCol:c-1); % 宽度过窄的区间多半是噪声,直接丢弃 if size(seg, 2) > w * 0.02 chars{end+1} = seg; end end end end裁掉上下 15% 是个经验值,目的是把车牌边框和铆钉排除在投影统计之外。间隙阈值取最大投影值的 8%,这个比例太低会把字符内部笔画间隙也当成分隔,太高又会把相邻字符粘在一起。实际调试时我会把colSum画成曲线,肉眼确认波谷位置和字符边界对得上,再定阈值——这比盲调快得多。宽度过滤w * 0.02是剔除噪点的兜底,正常字符宽度远大于这个值。
3.4 字符识别:模板匹配的归一化细节决定成败
模板匹配听起来简单,但直接拿分割出的字符和模板做corr2会死得很惨,因为尺寸和笔画粗细都不一致。必须先归一化到统一尺寸,再做相关性计算。
function str = batchMatch(chars) % 批量模板匹配:统一归一化到 20x40 后计算相关系数 templates = loadTemplates(); % 返回 struct 数组,含 .img 和 .label str = ''; for i = 1:numel(chars) seg = imresize(chars{i}, [40, 20]); seg = imbinarize(seg); bestCorr = -1; bestLabel = '?'; for t = 1:numel(templates) tpl = imresize(templates(t).img, [40, 20]); tpl = imbinarize(tpl); c = corr2(double(seg), double(tpl)); if c > bestCorr bestCorr = c; bestLabel = templates(t).label; end end str = [str, bestLabel]; end end归一化尺寸 20×40 是权衡结果:太小丢失笔画细节,太大增加计算量且对分割误差更敏感。corr2之前统一二值化,是为了消除灰度差异带来的干扰。模板库的构建建议每个字符准备三到五张不同字体或轻微形变的样本,匹配时取最高分,比单模板鲁棒性明显好。这套识别在规整样本上单字符准确率能到 90% 以上,但遇到严重污损或遮挡会掉得厉害,这是传统方法的固有上限,方案里要如实写进局限性分析。
4. 避坑与常见问题排查:五个让流程跑不通的典型故障
4.1 定位框为空或框到车身
现象是byColor返回空,或者框住了一大片蓝色车身。原因通常是色调阈值和实际图像白平衡不匹配,或者闭运算结构元素过大把车身和车牌连成一片。解决办法是先单独输出blueMask看一眼,确认蓝色区域是否只覆盖车牌;结构元素的宽高比从 1:3 往 1:2 收,面积下限从 500 往上调,逐步收紧候选。
4.2 分割出的字符数不是 7 个
现象是切出 5 个或 9 个字符。原因多半是边框没裁干净,或者字符间粘连。解决时先把二值化结果和投影曲线并排画出来,看多出来的区间落在哪;如果是边框,加大上下裁剪比例;如果是粘连,把间隙阈值从 8% 提到 12% 左右再试。别一次改多个参数,改一个看一次结果。
4.3 识别结果全是同一个字符
现象是每个字符都匹配到同一个模板。原因通常是归一化后图像几乎全黑或全白,corr2对所有模板都返回接近的值。检查imbinarize前后的像素分布,确认二值化阈值没有把整张图判成背景。这类问题用imshowpair对比原图和二值图,一眼就能看出来。
4.4 批量测试时某几张图直接报错
现象是单张测试正常,批量跑挂在中途。原因多是图像尺寸不一致导致imresize参数越界,或者某张图是灰度图却走了 RGB 分支。解决是在主流程入口统一做通道判断和尺寸检查,把异常图单独记录到日志而不是让整个循环崩掉。
4.5 换了数据集识别率断崖下跌
现象是在自建样本上表现不错,换一批图就崩。原因是参数过拟合到某一批图像的光照和分辨率。解决办法是留出至少 20% 的样本不参与调参,专门做验证;调参时优先动那些物理意义明确的参数(长宽比区间、面积下限),少动纯经验阈值。
5. 验收与答辩:怎么证明这套系统真的能跑
验收这件事,我的习惯是提前把「证据」准备好,而不是答辩现场临时演示。具体做法分三层。第一层是功能验收,按第 2 章那张需求表逐条打勾,每条附一张截图:定位框画在原图上、分割出的 7 个字符排成一行、最终识别字符串显示在标题里。第二层是批量指标,用test/目录下的脚本跑至少 50 张样本,统计定位成功率、分割正确率、单字符识别率三个数,写成表格放进报告。第三层是失败案例分析,主动挑三到五张识别错的图,说明错在哪一环、为什么错、改进方向是什么——这一层最能体现你对自己系统的理解深度,答辩时老师往往就盯着这里问。
批量测试脚本可以这样写,把指标算出来直接打印:
% test/batchTest.m 批量评估脚本 files = dir('data/samples/*.jpg'); total = numel(files); locateOk = 0; segOk = 0; charTotal = 0; charRight = 0; for i = 1:total r = main(fullfile(files(i).folder, files(i).name)); if ~isempty(r.bbox), locateOk = locateOk + 1; end if numel(r.chars) == 7, segOk = segOk + 1; end % 真值从文件名读取,约定格式为 真值_序号.jpg truth = extractBefore(files(i).name, '_'); charTotal = charTotal + 7; if strlength(r.plate) == 7 charRight = charRight + sum(r.plate == truth); end end fprintf('定位成功率: %.1f%%\n', locateOk/total*100); fprintf('分割正确率: %.1f%%\n', segOk/total*100); fprintf('单字符识别率: %.1f%%\n', charRight/charTotal*100);这段脚本的关键是把真值编码进文件名,避免再维护一张标注表。extractBefore取第一个下划线之前的部分作为真值,约定好命名规则后批量评估就是全自动的。答辩时把这三个百分比和失败案例一起放,比空口说「效果不错」有说服力得多。
还有一个容易被忽略的点:演示环境要提前锁定。MATLAB 版本、工具箱是否齐全、样本图路径,答辩前一天完整跑一遍,别指望现场装工具箱。我吃过一次亏,演示机没装 Image Processing Toolbox,regionprops直接报错,场面相当尴尬。从那以后我每次交付前都强制走一遍「干净环境复现」流程,把依赖和路径问题在答辩前全部暴露掉。希望这套结构和验收方法能帮到你,把方案稳稳落地。
本文还有配套的精品资源,点击获取