
简介面向数字图像处理课程设计的MATLAB车牌识别完整实现文档适合高校学生完成课设作业或入门车牌识别项目时参考。文档系统讲解从图像预处理、车牌定位、字符分割到模板匹配识别的完整流程并附源代码对imerode腐蚀、imdilate膨胀、strel结构元素、edge边缘检测等关键函数的用法与参数做了详细说明读者可照此思路独立搭建系统。内容涵盖MATLAB R2011a调试环境、Windows兼容性及硬件配置要求并给出从车辆图像采集到字符输出的整体设计框架。包内共1个doc文件整体约2.37MB以课程设计报告形式完整呈现设计目标、总体方案、功能模块与核心代码便于直接阅读和参照修改。目前已有3203人学习下载适合需要快速理解车牌识别原理并在此基础上二次开发或撰写课程设计报告的读者。1. 车牌识别课程设计MATLAB实现从定位到识别的最短实战路径车牌识别是数字图像处理课程设计里出现频率最高的题目之一用MATLAB实现一套完整流程通常包含车牌定位、字符分割、字符识别三大步。很多同学从网上找一份附源代码的课程设计文档复制到MATLAB里一跑白天样例图能出结果换个拍摄角度或者光线差一点就直接崩连定位框都画不对。这篇文章不讲虚的直接给一条能复现的工程路径用边缘检测加形态学做定位用垂直投影做字符切分用模板匹配做识别每一步的代码都带参数说明坑也一并列清楚。适合正在做课程设计的学生也适合想快速理解传统机器视觉基础流程的从业者。2. 车牌定位边缘检测加形态学处理先谈思路再给可运行函数2.1 为什么课程设计优先走边缘形态学而不是深度学习车牌识别的工业方案现在大多走深度学习检测但课程设计这个场景不太一样时间窗口短训练数据少评分的重点往往落在对数字图像处理基础方法的理解上。边缘加形态学的定位思路是教科书级别的经典路线Sobel算子对车牌的竖向边缘响应很稳定闭运算能把离散的字符边缘连成一个完整矩形块再按面积和宽高比筛出候选区。这套方案不需要训练不依赖GPU在MATLAB里十几行就能跑起来。深度学习方法在课程设计里反而容易翻车要么数据集只有几十张图训练出来的模型泛化能力很差要么被追问训练细节时答不上来。传统方法每一步都有明确的中间结果可以可视化定位失败时能精确到是边缘检测没出效果还是形态学参数不对排错路径清楚得多。所以我的建议是课程设计老老实实走传统图像处理路线等答辩完之后想扩展再聊深度学习。另外要注意一个细节定位是整个识别链路的地基。分割和识别做得再好定位框偏了后面全是白搭。课程设计里最常见的失败就是定位框只框住半个车牌或把旁边车灯切进来所以这一章先把定位做稳。2.2 locatePlate函数sobel边缘、闭运算、区域筛选的最小实现下面这个是能直接放进脚本里跑的最小定位函数流程是灰度化、中值滤波、边缘检测、闭运算、填充孔洞、连通域筛选。function [plateImg, plateBox] locatePlate(imgPath) % 基于边缘和形态学的车牌定位 % 输入: 图像路径 输出: 候选车牌图与矩形框[x,y,w,h] I imread(imgPath); if size(I, 3) 3 I rgb2gray(I); % 彩色图先转灰度 end Ig medfilt2(I, [3 3]); % 中值滤波去掉椒盐噪声 edgeMap edge(Ig, sobel, horizontal); % 水平边缘检测 se strel(rectangle, [17 3]); % 竖直17、水平3的矩形结构元 closeImg imclose(edgeMap, se); % 闭运算连接断裂字符边缘 fillImg imfill(closeImg, holes); % 填充孔洞让候选区变成实心块 cc bwlabel(fillImg); stats regionprops(cc, Area, BoundingBox); plateBox []; for k 1:numel(stats) ratio stats(k).BoundingBox(3) / stats(k).BoundingBox(4); if stats(k).Area 400 ratio 2 ratio 5 plateBox stats(k).BoundingBox; break; end end if isempty(plateBox) error(未定位到车牌区域尝试调整se大小或改用HSV特征); end plateImg imcrop(I, plateBox); end参数说明中值滤波用 3×3 窗口只去掉孤立噪声点不会过度模糊字符边缘。窗口再大的话容易把细笔画抹掉定位框会缩小。Sobel 检测方向选 horizontal车牌字符边缘以横向长条为主竖直边缘反而会引入车灯、保险杠等干扰。结构元[17 3]是这套方案的核心参数。17 是竖直方向用来连接同一列字符被噪声打断的笔画3 是水平方向避免把相邻字符黏连成一个整体。面积阈值 400 和宽高比 2~5 来自车牌的先验尺寸。中国车牌约 440×140宽高比在 3 左右。如果你处理的是其他规格车牌这两个值要按比例改。跑完这段代码建议把 fillImg 用 imshow 显示出来看一眼。如果候选区域一片散点说明闭运算不够结构元尺寸要加大如果多个区域连成一大片说明水平方向连得太狠把 3 改成 2 再试。2.3 备选定位方案HSV蓝色提取与多尺度se参数调整灰度边缘定位在光线均匀的场景下表现不错但遇到车牌颜色偏暗、字符对比度低的情况Sobel 响应会变弱。这时我一般切到 HSV 色彩空间靠车牌底色的蓝色特征做第二路定位。HSV 把色调、饱和度、亮度分开蓝色车牌的 H 值集中在 0.55 到 0.70 之间不受光照强弱影响太多。function blueMask extractBlueRegion(img) % 从HSV空间提取蓝色车牌候选区 hsv rgb2hsv(img); H hsv(:, :, 1); S hsv(:, :, 2); V hsv(:, :, 3); blueMask (H 0.55 H 0.70) (S 0.25) (V 0.15); blueMask bwareaopen(blueMask, 200); % 去掉小于200像素的零散区域 se2 strel(rectangle, [15 15]); blueMask imclose(blueMask, se2); % 合并车牌内部因字符造成的空洞 endHSV 定位和灰度边缘定位不应该互相替代而是互补关系。我常用的策略是先跑灰度边缘定位如果返回的 plateBox 宽高比不在 2.2~4.5 区间就自动切换到 HSV 通道重新提取候选区。这样做的好处是夜间或逆光场景下有兜底不至于整个程序直接报错退出。关于 se 参数的调试有个比较笨但有效的习惯准备三张代表性图片分别是白天顺光、夜间闪光、阴天散射光跑定位时把中间结果 dump 出来对比。如果三张图片的中间结果差异大说明参数鲁棒性不够而不是某张图的问题。多尺度结构元也可以做进代码里用一个 for 循环试几组 se 尺寸取所有框里面积最大的那个作为最终候选。3. 字符分割垂直投影切出7个字符的算法与参数细节3.1 预处理二值化与统一字符极性定位拿到的是车牌区域的灰度图分割前必须先二值化。这一步的坑在于二值化阈值不能写死。灰度值 0.5 的固定阈值在白天顺光时可能有不错效果但黄昏或夜间拍摄时整个图像灰度下移字符和背景会被一起压成黑色。更可靠的做法是用 graythresh 函数自动计算 Otsu 阈值。Otsu 方法根据灰度直方图的类间方差自动求阈值对光照变化的适应能力比固定阈值强得多。二值化之后还需要统一字符极性。蓝底白字车牌在二值化后可能出现两种情况字符为白色、背景为黑色或反过来。模板匹配阶段要求字符始终是白色否则匹配距离完全失去意义。判断极性最直接的方法是统计整张图白色像素占比超过 0.5 就取反。车牌字符面积通常占车牌区域的 40% 左右低于一半所以白色像素占比超过 50% 大概率是背景变白了。另一个预处理细节是去除边框。二值化后的车牌边缘经常残留白色边框线垂直投影时边框会造成整列高响应干扰分割起始位置。常见做法是对二值图做一次腐蚀让边框变细后再做投影或者直接检查投影图的左右两端是否有持续高响应列有的话就把这一列到字符第一峰之间的区域削掉。3.2 splitChars函数投影法实现与区间合并规则垂直投影法的原理很直白对二值图统计每一列白色像素的数量字符区域会有明显峰值字符间隙的投影值接近零按列投影把连续非零区段切出来即可。下面是实现代码。function chars splitChars(bw) % 输入: 二值化车牌图(输出为白字黑底) % 输出: 元胞数组每个元素是单个字符的01矩阵 if mean(bw(:)) 0.5 bw ~bw; % 统一为白字黑底 end proj sum(bw, 1); % 垂直投影每列白色像素总数 startIdx find(proj 0); % 所有有内容的列 jump find(diff(startIdx) 1); % 断点位置 limits []; if isempty(jump) limits [startIdx(1) startIdx(end)]; else limits [startIdx(1) startIdx(jump(1))]; for i 1:length(jump)-1 limits [limits; startIdx(jump(i)1) startIdx(jump(i1))]; %#okAGROW end limits [limits; startIdx(jump(end)1) startIdx(end)]; end minW round(size(bw, 2) * 0.03); % 过窄区域视为噪声 chars {}; for i 1:size(limits, 1) w limits(i, 2) - limits(i, 1) 1; if w minW chars{end1} bw(:, limits(i, 1):limits(i, 2)); %#okAGROW end end end这段代码里很容易出错的地方是断点合并逻辑。startIdx 存的是投影值大于零的列号diff 大于 1 表示列号不连续说明字符之间有空白间隙。jump 存的不是间隙本身而是断点前最后一个内容列在 startIdx 里的下标。构造 limits 时起始列要取 startIdx(1)结束列要取 jump 对应的那个内容列下一段的起始列取 jump 后一位。这个逻辑理不顺的话分割结果会错位建议先用一个简单的二值矩阵在命令行里手动推演一遍。minW 的设定也值得解释。车牌字符宽度大致是车牌总宽度的 10%~14%而第二位和第三位之间的小圆点宽度只有总宽度的 1%~2%。把 minW 设为总宽度的 3%可以稳定滤掉小圆点和边框残留不会误伤真正的字符。如果车牌字符偏细这个阈值可以降到 2%但不要低于 1.5%否则噪声段会混进来。3.3 分割结果修正字符计数校验与间隔异常处理splitChars 跑完后应该得到 7 个字符区域汉字省份简称加字母加五位数字。实际中常见两种异常切出 8 个以上区域说明小圆点或噪声没有被滤干净切出 5~6 个区域说明有两个字符粘连了。粘连的主要原因是二值化后字符笔画之间有桥接或者拍摄角度倾斜导致相邻字符在垂直方向上的投影区域重叠。字符粘连的修正在课程设计里最实用的方案是检查字符宽度总和与实际位置。如果某个区域的宽度明显超过平均字符宽度的 1.5 倍就对这一列区域内部再做一次垂直投影把峰值之间的间隙切出来。这个思路很少在一份现成代码里直接提供但答辩时被问粘连字符怎么处理时能答出来分数会有明显差别。上下出界也是常见问题。定位框如果稍微上移或下移字符会被截掉三分之一垂直投影依然有峰值但分割出的字符形状不完整影响后面模板匹配。可以在投影前对二值图做一次行方向扫描检查最上和最下的非零行如果字符顶到图像边界说明定位框偏了不建议在分割阶段硬救回 2.2 调整定位函数更高效。4. 字符识别模板匹配的工作方式与模板库组织4.1 归一化为什么尺寸统一是匹配的前提模板匹配的原理是把待识别字符与每个候选模板逐像素比较差异最小的模板类别作为识别结果。但直接比较的前提是两张图尺寸一致而且宽高比例尽量保持一致。如果字符原图是 30×60模板是 24×48直接 reszie 成统一尺寸可能导致数字 1 被拉宽成方块或者字母 B 和数字 8 形状趋同。正确的做法是保持宽高比的归一化。先固定目标高度宽度按原比例缩放然后把缩放结果贴到统一大小的画布上两侧多余部分用白色背景填充。这样字符的外形比例不会被破坏数字 1 依然瘦长数字 0 依然接近正方形。这个预处理步骤决定了匹配的准确性值得花十行代码认真写。function normChar normalizeChar(charImg) % 保持宽高比填充式归一化到 64x32 画布 [h, w] size(charImg); targetH 64; targetW 32; scale targetH / h; nw round(w * scale); resized imresize(charImg, [targetH nw]); normChar zeros(targetH, targetW); startCol max(1, floor((targetW - nw) / 2)); normChar(:, startCol:startColnw-1) resized; end目标尺寸选 64×32 是经验值主要考量是信息量够用且计算量小。尺寸再大对识别率提升不明显反而增加模板匹配时间。startCol 的计算保证字符在画布中水平居中避免因位置偏移产生额外距离误差。4.2 matchChar函数欧氏距离匹配的核心逻辑二值化后的图像适合用欧氏距离衡量差异度。把字符矩阵和模板矩阵都转成一维向量逐元素求差值的平方和值越小表示越相似。要注意的是匹配前两边都必须是二值图模板库里存的也应该是处理过的二值矩阵不要拿灰度图直接做欧氏距离光照差异会淹没真实的字形差异。function result matchChar(charImg, templateSet) % 输入: 单个字符二值图模板库 % 输出: 识别结果与匹配分数 charImg normalizeChar(charImg); bestName ; bestScore 1e10; for k 1:numel(templateSet) t normalizeChar(templateSet(k).img); diff sum((double(charImg(:)) - double(t(:))) .^ 2); if diff bestScore bestScore diff; bestName templateSet(k).name; end end result struct(char, bestName, score, bestScore); end这段代码把归一化放在匹配循环里每次都要对模板重新缩放。模板库规模不大时影响可忽略字符模板就几十个但如果后续扩展到省份汉字、字母、数字全量组合超过 80 个字符建议在加载模板时就提前归一化并存储避免循环里重复计算。bestScore 初始化的值设得足够大1e10保证第一个模板必然成为初始最优解。如果识别结果分数明显偏高比如欧氏距离大于 30% 像素总数说明字符可能被污染这时与其硬猜一个字符不如输出不确定后续可以用其他策略纠正。4.3 模板集怎么组织匹配失败先改哪个参数模板库在课程设计中有两种常见组织方式。第一种是从训练图片中逐字符切割、人工标注建成优点是和拍摄环境匹配度高缺点是容易过拟合到训练集的字体风格。第二种是用系统字体直接渲染字符生成模板这种方法不依赖真实拍摄数据生成速度快但和场景内车牌的印刷字体有差异。我通常建议用真实切割的字符做基础库再补充几个不同字重的字体模板同一字符多个模板参与匹配取分数最低的那个作为最终距离。模板库用 MAT 文件存储比散落的图片文件更好管理。一个 templates.mat 里存一个结构体数组每个元素包含 name 和 img 两个字段。加载一次整个库进入工作区匹配函数里直接遍历不需要反复读磁盘文件速度提升明显。匹配失败时先检查以下顺序一归一化后字符是否保持原有宽高比二模板库中该字符的字体风格是否与待识别字符差异过大三待识别字符边界是否包含噪声或残留边框。如果形近字之间互相混淆比如 0 和 D、8 和 B优先增加同一字符的不同模板数量而不是修改距离度量。欧氏距离本身没有方向区分能力增加模板是投入产出比最高的做法。5. 车牌识别MATLAB实现避坑五个典型踩坑记录5.1 固定阈值二值化导致夜间图片全黑现象白天样例图识别正常换成夜间或黄昏拍摄的照片分割前二值化后字符几乎全部消失投影曲线变成一条低矮平线。原因代码里直接用了im2bw(I, 0.5)的固定阈值。夜间接电图像整体灰度偏低字符和背景都被压到 0.5 以下二值化把字符和背景统一变成了黑色。解决换成 graythresh 自动阈值或者先做直方图均衡化增强对比度再二值化。我一般在定位前做一次adapthisteq限制对比度自适应直方图均衡化对夜间场景的改善非常明显但注意参数不要调得太过否则会出现光晕伪影。5.2 定位框切歪了形态学参数一改就崩现象locatePlate 返回的矩形框只框住车牌左侧一半或者把车牌上方一段车身纹理切进来后续分割出来的字符缺胳膊少腿。原因边缘检测后字符边缘没有连成完整块imclose 的结构元尺寸对当前图片不匹配。车牌窄结构元大会把多个字粘成一片结构元小又连不上断笔。解决把结构元尺寸从[17 3]改成[15 3]或[20 3]多试几组并配合 imshow 观察 closeImg 的中间结果。另一个思路是把定位框先用 regionprops 的 boundingbox 取出来再做一步外扩 5% 的框扩大弥补形态学边缘回缩带来的截断问题。5.3 第二位和第三位之间的小圆点被当成字符现象分割结果输出 8 个区域多出来的一个只有几个像素宽后续模板匹配把圆点识别成数字或字母。原因垂直投影对小圆点同样会产生一个窄峰值区段minW 若设成固定值比如 5 像素小圆点宽度可能刚好超过该阈值。解决minW 用相对值round(size(bw, 2) * 0.03)而不是绝对像素。同时判断字符数量如果超过 7 个删掉最窄的那个区域后再继续识别。圆点位置基本固定在第二位和第三位之间还可以直接用经验位置过滤。5.4 0和D这类形近字的识别混淆现象车牌中的数字 0 识别成 D数字 8 识别成 B其他字符都正确整体识别率却上不去。原因字符归一化时直接 imresize 到 64×32 会把原本接近正方形的 0 压扁与字母 D 的轮廓更接近。如果模板库中 D 的模板笔画较粗欧氏距离会被字形外的像素面积主导。解决归一化改成 4.1 节中的保持宽高比填充方式不要强制拉伸成统一宽高。另外给 D 和 0、B 和 8 这类形近对分别多准备两三个不同笔画的模板同类多个模板取最小值能明显减少交叉误判。5.5 换一台设备结果就变参数显式化与中文路径现象代码在自己的电脑上跑得好好的拷到实验室另一台机器后有些图片定位结果完全变了甚至直接报错说图片读取失败。原因除开 MATLAB 版本差异最大的坑是路径里有中文字符。imread 对不同编码环境下的中文路径处理不一致返回空矩阵后续所有操作都崩。另一个原因是代码里用了edge(Ig, sobel)这种写法不同版本默认参数有细微差别。解决所有文件路径用纯英文命名代码里用fullfile拼接路径。所有函数调用都把参数显式写出来比如edge(Ig, sobel, horizontal, 0.15)结构元尺寸、面积阈值都定义成脚本开头的变量不要散落在代码中间。这个习惯在图像处理项目里能省下大量排错时间。6. 把课程设计做成一组可验证数据批量测试与置信度输出6.1 批量脚本验证识别率课程设计验收时很多同学只会跑两张样例图被问整体识别率多少就答不上来。准备一个批量脚本把 20 到 30 张带有真值标注的图片放进一个文件夹按文件名规则标注车牌内容比如京AB12345.jpg脚本自动循环处理并统计完全识别正确的张数比例。这比一张一张跑图说服力强得多。function evaluateAll(imgDir, templateSet) files dir(fullfile(imgDir, *.jpg)); correct 0; total 0; for i 1:numel(files) imgPath fullfile(imgDir, files(i).name); [plateImg, ~] locatePlate(imgPath); bw imbinarize(plateImg, graythresh(plateImg)); if mean(bw(:)) 0.5, bw ~bw; end chars splitChars(bw); result ; for j 1:numel(chars) r matchChar(chars{j}, templateSet); result [result r.char]; %#okAGROW end groundTruth lower(files(i).name(1:end-4)); if strcmp(lower(result), groundTruth) correct correct 1; end total total 1; end fprintf(识别率: %.2f%%\n, correct / total * 100); end脚本跑出的识别率不仅是一个数字更是一个调试工具。哪张图识别错了就把这张图单独拿出来打印每一环节的中间结果定位、二值化、分割、匹配一步一步看是哪一层出了问题。大部分情况下问题出在二值化或分割而不是模板库。6.2 置信度输出与倾斜校正加分项matchChar 返回的 score 是原始欧氏距离不同字符对的绝对差距没有统一尺度。可以把 score 归一化到 0~10 表示完全一致1 表示完全不相关设定一个阈值比如 0.35超过就输出不确定。这样做的价值在于系统不会信誓旦旦地把一个模糊字符硬猜成某个数字答辩时也能展示你对识别置信度的理解。倾斜校正是另一个加分项。拍摄视角不正时字符列方向会有几度倾斜垂直投影的峰谷会变得不明显。常见的做法是检测牌照区域的最小外接矩形计算旋转角度后用 imrotate 做一次旋转校正。这个步骤不用做得太复杂试过在 8 度以内的倾斜场景下加入校正后分割准确率能提升十几个百分点。但注意旋转后的图像边缘会出现黑色填充需要重新定位一次车牌区域再做分割否则反而引入新噪声。我自己的课程设计当初也翻过车最后发现问题全出在二值化阈值和路径命名上调完这两处识别率一下子从六成跳到九成。这个方向最大的价值不在代码本身而是让你把整个图像处理流程的每个环节都亲手调了一遍。答辩时被问某一层的参数为什么这么设你能从中间结果图讲清楚来龙去脉比代码跑通本身重要得多。希望这篇笔记帮你把这条路径一次走通少走我已经踩过的那些弯路。本文还有配套的精品资源点击获取