简介:一份基于MATLAB实现车牌识别(LPR)系统的完整资源包,面向计算机视觉初学者、MATLAB开发者及交通监控、停车场管理等场景的算法研究者。内容覆盖图像预处理、车牌定位、特征提取、字符分割与识别全流程,重点演示了edge边缘检测、regionprops连通域分析、svmtrain与神经网络模型训练等关键方法,并配有实际车牌图像、演示视频及可直接运行的.m脚本,便于对照学习与二次开发。资源共22个文件,以8个MATLAB脚本为核心,辅以10张车牌样本图片、2个演示视频及1个png说明图,整体压缩包约42.71MB,结构清晰,适合从零搭建车牌识别系统时参考。当前已有373人学习下载,资源内还提供了一组car plate相关文件,可帮助读者快速验证预处理、定位到字符识别的完整链路,并针对不同光照、倾斜等场景进行参数调优。
1. 车牌识别 matlab 方案:从图像采集到字符输出的完整链路
车牌识别在 MATLAB 里做,难点从来不是调用哪个函数,而是把「定位、分割、识别」这条链路按顺序打通,每一步的参数都和实际图像强相关。同样是蓝底白字的民用车牌,光照、倾斜、遮挡、分辨率一变,固定阈值就会失效,所以成熟的 MATLAB 车牌识别工程,本质上是「图像预处理 + 形态学分析 + 字符切割 + 模板/分类器匹配」的组合,而不是某个单一算法通吃。这套方案的好处是调试直观,每一步都能用imshow实时看中间结果,坏处是参数多、需要有一套自己的调参方法论。本文面向能用 MATLAB 读写图片、知道图像滤波基本概念的人,目标是给你一套在小批量本地图片上能跑出可用准确率的流程。
2. 车牌定位与预处理:颜色特征与形态学操作的配合
2.1 为什么不能只靠灰度图做定位
车牌识别 matlab 入门教程里最常见的错误,是把 RGB 图直接转灰度、二值化、找轮廓。这么做在单一背景下可能成立,但换到实拍场景,车身的白色、路面标线、路牌文字都会成为干扰。真实工程里优先用颜色特征圈定候选区域,因为中国民用车牌有固定的蓝底白字,HSV 空间中蓝色色调集中在 H 分量的 100° 到 124° 附近,用这个特征做掩膜比灰度阈值稳定得多。
另一个原因在于「车牌区域」的几何特征:它宽高比大约为 3.14:1(440mm×140mm),面积在整张图中占比有限。颜色特征能快速把候选区缩小到几个矩形,再用几何过滤剔除误检。这个思路对后续字符分割有直接帮助——定位不准,后面所有步骤都是空中楼阁。
2.2 RGB 到 HSV 的转换与蓝色掩膜提取
% 读取图像 img = imread('car_plate.jpg'); % RGB 转 HSV hsvImg = rgb2hsv(img); % 提取蓝色通道掩膜:H 在 [0.55, 0.68],S 不低于 0.35 mask = (hsvImg(:, :, 1) >= 0.55) & (hsvImg(:, :, 1) <= 0.68) & (hsvImg(:, :, 2) >= 0.35); % 形态学闭运算,连接断裂的车牌区域 se = strel('rectangle', [15, 15]); maskClosed = imclose(mask, se); % 连通域分析,获取候选区域属性 cc = bwconncomp(maskClosed); stats = regionprops(cc, 'BoundingBox', 'Area', 'FilledArea');这段代码里rgb2hsv把 RGB 图像转换到 HSV 色彩空间,H 表示色相、S 表示饱和度、V 表示明度。hsvImg(:, :, 1)取出色相通道做区间判断,设置 H 为 0.55 到 0.68 是因为在 MATLAB 中 HSV 各分量均归一化到 [0, 1],蓝色对应的实际角度 198° 除以 360° 约等于 0.55。饱和度下限 0.35 用来排除偏灰的蓝色区域。
imclose做闭运算,先用 15×15 的矩形结构元素膨胀再腐蚀,目的是把车牌字符与蓝色底板之间的空隙填上——因为字符是白色的,二值化后它会把蓝色底板断开。结构元素尺寸由车牌在图像中的像素宽度决定,图片分辨率为 1920×1080 时,15 到 20 的尺寸比较适合;如果分辨率低,需要相应缩小。
bwconncomp找出所有连通区域,regionprops返回每个区域的包围盒和面积信息。这里用FilledArea而不是Area,是因为一个连通区域内可能有小空洞,填充后的面积更真实反映物理尺寸。
2.3 几何特征过滤与倾斜校正
% 筛选候选区域:宽高比约 2.5~4.0,面积足够大 candidates = []; for k = 1:length(stats) bb = stats(k).BoundingBox; ratio = bb(3) / bb(4); % 宽 / 高 area = stats(k).FilledArea; if ratio >= 2.2 && ratio <= 4.2 && area > 500 candidates = [candidates; bb]; end end宽高比范围设 2.2 到 4.2 是留出余量的做法,标准车牌是 3.14,但实拍中镜头畸变会让它偏移到 2.5 或 3.8。面积下限 500 像素是一个经验值,前提是车牌在画面中不是远小点。如果实际图片中车牌很小,这个下限需要降到 100 左右。
倾斜校正常用radon变换检测车牌长轴的角度,或者用最小外接矩形minAreaRect的思路——在 MATLAB 里对应的函数是regionprops的'Orientation'属性。得到角度后,用imrotate反向旋转,再裁剪出正的车牌区域。
% 假设 idx 是选定的候选区域索引 angle = stats(idx).Orientation; % 单位:度 rotated = imrotate(img, -angle, 'bilinear', 'crop'); % 裁剪车牌区域 plateBB = stats(idx).BoundingBox; plate = imcrop(rotated, [plateBB(1), plateBB(2), plateBB(3)*1.1, plateBB(4)*1.6]);注意裁剪时宽度乘 1.1、高度乘 1.6,这是把车牌上下边缘的铆钉、边框余量一起裁进来。后面字符分割时,这些边框是要去掉的。
提示:
imrotate的'crop'选项会让旋转后的图像尺寸与原图一致,缺点是四个角会被裁剪掉一部分。如果车牌正好在图像边缘,建议改用'loose'并手动去除多余黑边。
3. 字符分割的确定性算法:投影法与连通域分析组合
3.1 车牌区域二值化的阈值选择
% 转为灰度图 grayPlate = rgb2gray(plate); % 中值滤波去噪,3×3 窗口保留边缘的同时去除椒盐噪声 filtered = medfilt2(grayPlate, [3, 3]); % 自适应阈值二值化,局部邻域 31×31 bwPlate = imbinarize(filtered, 'adaptive', 'Sensitivity', 0.45);固定阈值 Otsu(graythresh)在均匀光照下没问题,但车牌表面经常有反光或阴影,全局阈值会把半个字符吃掉。自适应阈值按局部邻域计算阈值,对光照渐变有更好的容忍度。'Sensitivity'参数控制阈值偏置,0.45 的含义是让更多像素被判为前景(白色),因为字符笔画本身较细,提高敏感度能减少笔画断裂。
如果二值化结果是「黑底白字」,需要取反让字符为白色、背景为黑色,因为后面连通域分析时习惯性认为面积小的连通区域是字符:
% 统计前后景占比,若白色占大多数则取反 if mean(bwPlate(:)) > 0.5 bwPlate = ~bwPlate; end3.2 垂直投影分割法:计算逐列白像素数
% 垂直投影 proj = sum(bwPlate, 1); % 每一列的白像素总数 % 找到投影值为 0 的列作为字符间隔 gapCols = find(proj == 0);垂直投影的基本原理是:字符列的白像素数多,字符间隙的列白像素数为 0。相邻的 0 值列之间构成一个字符区间。实际处理中会有噪声导致投影值在间隙处不为 0,此时需要设一个阈值而不是直接用 0。
% 低通滤波平滑投影曲线 projSmooth = conv(double(proj), ones(1, 5)/5, 'same'); % 阈值设为最大投影值的 2% thresh = max(projSmooth) * 0.02; gapCols = find(projSmooth < thresh); % 将连续 gap 列合并为一个分割点conv做滑动平均,窗口为 5 个像素,把离散噪声抹平。阈值设 2% 是因为有些字符边缘会有毛刺,导致间隙列仍有一两个白像素。
3.3 连通域分析做二次分割:解决汉字断裂问题
垂直投影法最怕的情况有两种。第一,汉字结构复杂,比如「京」「苏」这类字,中间可能完全断开成为左右部分,投影表面上是多个字符;第二,字符粘连,投影值在所有列都不为 0,投影法直接失效。
综合做法是先跑连通域,用字符的高宽比过滤出候选字符块,再结合投影法修正。字符的典型高宽比约在 0.5 到 1.2 之间(汉字偏方,数字和字母偏窄),按照上面的高度就是plateBB(4)*1.6的裁剪后图像,字符高度约占车牌高的 70% 到 80%。
% 连通域分析 ccChar = bwconncomp(bwPlate); statsChar = regionprops(ccChar, 'BoundingBox', 'Area', 'Centroid'); % 按 x 坐标排序 boxes = reshape([statsChar.BoundingBox], 4, [])'; boxes = sortrows(boxes, 1); % 过滤:宽高比在 0.3~1.5,面积占比适中 charBoxes = []; for k = 1:size(boxes, 1) w = boxes(k, 3); h = boxes(k, 4); ratio = w / h; area = w * h; if ratio >= 0.25 && ratio <= 1.6 && area > size(bwPlate, 1) * size(bwPlate, 2) * 0.005 charBoxes = [charBoxes; boxes(k, :)]; end end过滤阈值中面积下限取整图的 0.5%,这是对「小噪声连通域不算字符」的经验界定。比率范围扩大是为了容纳数字「1」和汉字「川」两种极端宽度的笔画特征。
提示:连通域分割后,标准 7 位车牌会得到 7 个字符块,但省会简称「粤」「京」等常会被拆成 2 到 3 块。此时要结合投影法把相邻且纵向重叠的块合并成一个字符。
4. 字符识别:模板匹配与分类器的工程取舍
4.1 字符归一化:去除车牌边框干扰
% 去边框:上下左右各裁剪掉 3% 的边距 [h, w] = size(bwPlate); marginX = round(w * 0.03); marginY = round(h * 0.08); bwClean = bwPlate(marginY+1:end-marginY, marginX+1:end-marginX);车牌边框在二值化后会和字符粘连,需要先裁边再去连通域。竖直方向裁 8% 是因为车牌上下边框更粗,水平方向只裁 3%。
标准化时统一缩放到 40×20 像素,同时保持笔画粗细一致。imresize默认双线性插值,但二值图用最近邻更合适,不然会出现灰色边缘:
charNorm = imresize(charImg, [40, 20], 'nearest');4.2 模板匹配的数学逻辑与距离度量
% 加载模板库:50 张(31 省份简称 + 24 字母 + 10 数字),统一 40×20 templateSet = load('charTemplates.mat'); % 结构体含 chars 和 labels bestMatch = ''; minDist = Inf; for t = 1:length(templateSet.labels) tpl = templateSet.chars(:, :, t); % 度量 1:异或距离(汉明距离) xorDist = sum(xor(charNorm(:), tpl(:))); % 度量 2:归一化互相关 corrVal = corr2(double(charNorm), double(tpl)); % 组合评分:距离越小越好,相关越大越好 score = xorDist - 50 * corrVal; if score < minDist minDist = score; bestMatch = templateSet.labels(t); end end异或距离统计的是两个二值图像对应像素不同的数量,取值 0 到 800(40×20)。互相关corr2对轻微形变更鲁棒,但也会对笔画粗细差异过敏。把两个指标组合成权重评分,经验上异或距离为主、互相关为微调。
模板库的来源有两类:一是自己用上面的流程对 50 张已标注车牌做分割后构建,二是用程序生成标准字体再叠加噪声做仿真模板。前者准确率高但工作量在标注,后者精度稍低但可以大量生成。
4.3 基于统计特征的识别:避免大网络、小样本陷阱
如果样本量太少,训练 CNN 会过拟合,一个替代方案是提取方向梯度直方图(HOG)特征 + SVM。MATLAB 里可以用extractHOGFeatures提取特征,但一个值得警惕的点是该特征对笔画粗细和噪声较敏感,需要在提取前对字符做骨架化(bwmorph的'thin'选项)来减弱笔画粗细差异。
% 骨架化后再提取 HOG charSkel = bwmorph(charNorm, 'thin', Inf); features = extractHOGFeatures(charSkel, 'CellSize', [8, 8]);字符识别中 0(零)与 O(字母)、1(一)与 I(字母)是高频误判对。模板匹配阶段容易把「O」判成「0」,因为它们的二值图几乎一致。常见处理方式是后验修正:如果识别结果是字母 O 且车牌位置是第四位(数字位),强制改为 0。
4.4 训练集构建与交叉验证的实操框架
% 构建特征矩阵 featureMatrix = zeros(numSamples, numFeatures); labels = strings(numSamples, 1); for i = 1:numSamples img = imread(sprintf('train_chars/%03d.png', i)); % ... 预处理、归一化、骨架化 ... featureMatrix(i, :) = extractHOGFeatures(imgSkel, 'CellSize', [8, 8]); labels(i) = readLabelFile(i); % 从文件名读取标签 end % 训练多分类 SVM(一对一策略) mdl = fitcecoc(featureMatrix, labels, 'Learners', 'svm'); % 交叉验证 cvmdl = crossval(mdl, 'KFold', 5); loss = kfoldLoss(cvmdl); fprintf('5-fold 交叉验证准确率: %.2f%%\n', (1 - loss) * 100);样本量少于 500 时,用crossval做 K 折验证看泛化误差,不要只看训练集准确率。fitcecoc内部自动做一对一的多分类,每个二分类器都是线性 SVM,训练速度快,适合字符识别这种类别数 65 个左右的中等规模问题。
5. 定位失败、字符粘连、识别混淆的排查路径
5.1 定位环节的 4 个必查可视化点
% 可视化检查清单 figure; subplot(2, 2, 1); imshow(mask); title('蓝色掩膜'); subplot(2, 2, 2); imshow(maskClosed); title('闭运算后'); subplot(2, 2, 3); imshow(plate); title('裁剪出的车牌'); subplot(2, 2, 4); imshow(bwPlate); title('二值化后的字符');每个子图对应一个可调参数:掩膜出现大块蓝色噪声时收紧 H 区间或 S 下限;闭运算后车牌区域仍然是碎块时增大结构元素;裁剪出的车牌倾斜明显时检查Orientation是否因为连通域不完整而产生偏差;二值化后字符断笔画时降低'Sensitivity'。
5.2 字符粘连的启发式拆分
粘连的主要成因是二值化阈值偏高,把间隙也判成了白像素。如果字符间粘连较窄,可以在投影平滑曲线中找局部最小值代替绝对 0 值:
% 找到投影曲线的全部局部极小值 [~, locs] = findpeaks(-projSmooth, 'MinPeakProminence', max(projSmooth)*0.1); % locs 中相邻距离大于 5 像素的才是有效分隔点 splitPoints = locs(diff([0; locs(:)]) > 5);findpeaks需要负号传入才能搜索波谷。MinPeakProminence设 10% 的意义是:一个真正的字符间隙,其投影下降幅度应显著大于普通噪声波动。
如果粘连宽度超过 8 个像素,启发式拆分不可靠。此时不要硬拆,考虑重新拍图或换角度。工程上没有任何字符分割算法能在宽粘连下稳定工作,这属于输入质量问题,不是算法问题。
5.3 模板匹配误判的后处理策略
利用车牌本身的结构约束:第 1 位是省份简称(汉字),第 2 位是发牌机关字母,第 3 到 7 位可能混合数字和字母,但第 5 到 7 位在大多数城市是数字。把先验规则编码进后处理:
recognized = '京A12345'; % 假设识别结果 % 规则 1:第 5~7 位必须数字化 for pos = 5:7 if ~isstrprop(recognized(pos), 'digit') % 将字母映射到最相似的数字 recognized(pos) = letterToDigit(recognized(pos)); end endletterToDigit的映射表需要手动建立,例如 B→8、S→5、O→0、Z→2。这个规则的有效性取决于目标城市是否在末位使用字母(上海、广州的号段规则不同),要根据实际数据调整。
6. 识别准确率验证的闭环方法
6.1 用混淆矩阵定位高误判字符类别
% 假设有真实标签和识别结果 actual = load('actualLabels.mat'); % 字符串数组,与预测一一对应 predicted = load('predictedLabels.mat'); % 求混淆矩阵 classes = unique(actual); [C, order] = confusionmat(actual, predicted, 'Order', classes); % 显示每个字符的准确率 accPerClass = diag(C) ./ sum(C, 2); for i = 1:length(order) fprintf('%s: %.2f%%\n', order(i), accPerClass(i) * 100); end % 找到最低的三个 [~, idx] = sort(accPerClass); fprintf('最容易混淆的字符: %s, %s, %s\n', order(idx(1)), order(idx(2)), order(idx(3)));confusionmat的返回矩阵第 i 行表示真实类别为第 i 类的样本分别被预测成了哪些类。对角线占比过低就说明该字符特征提取不够有区分度。常见结果里「0」和「O」互混、「B」和「8」互混,如果这些类别恰好占比高,优先补充模板或样本。
6.2 光照变化的批量压力测试
% 对同一张车牌图做多组亮度变换,验证识别鲁棒性 baseImg = imread('test_plates/001.jpg'); brightnessFactors = [0.4, 0.7, 1.0, 1.3, 1.6]; for i = 1:length(brightnessFactors) adjusted = imadjust(baseImg, [], [], brightnessFactors(i)); result = plateRecognitionPipeline(adjusted); % 你自己的主函数 fprintf('亮度系数 %.1f -> %s\n', brightnessFactors(i), result); endimadjust的第四个参数大于 1 时让图像整体变亮,小于 1 变暗。这个测试能快速判断你的预处理对光照有多敏感。如果 0.4 和 1.6 全挂、1.0 全对,说明自适应二值化的参数还需要放宽。
6.3 减少回归错误的 Git 化实验习惯
把每次调整写成一个独立脚本,输出目录按「日期 + 参数摘要」命名:
experiments/ 20260612_hsv_055_068_s15/ 20260613_adaptive_sens_045/每个实验目录里保存四样东西:运行脚本、复制出来的中间图像、准确率统计文件、参数改动说明。下次调参时直接对比两目录的准确率,比在同一个脚本里反复改注释要可靠得多。
最后一笔,车牌识别的精度天花板不在识别算法,而在定位和二值化——这两步决定了交给分类器的输入质量。用imshow把每个中间步骤可视化出来,永远比直接看最终准确率更能定位问题。
本文还有配套的精品资源,点击获取