MATLAB车牌字符识别:从预处理到SVM的完整实现与优化
简介这份毕业设计资料面向高校计算机、电子信息等专业学生围绕MATLAB平台下车牌字符识别系统的设计与实现展开核心采用SVM分类器完成对车牌字母、数字及汉字的训练与识别适合作为课程设计、毕业设计选题或模式识别入门练手项目。资源包共125个文件以102张jpg字符样本图、18个m脚本文件、3个txt标签说明为主另含db与md文件压缩包约1.06MB其中m文件对应数据预处理、SVM训练与测试等模块txt文件记录字符类别及需手工校正的图片名单。数据集为已分割好的1000张车牌字符图尺寸统一为47*92预处理环节涉及RGB转二值图、黑底白字与白底黑字的反转校正等细节。目前已有246人学习可帮助读者快速理解字符识别流程、复用脚本并完成自己的实验复现。1. 从一张糊到发虚的抓拍说起MATLAB 车牌字符识别到底在做什么地下车库出口闸机摄像头拍到的车牌往往只有 120 像素宽字符边缘还带着运动模糊和补光灯的反光。很多同学做毕业设计时第一反应是直接上深度学习结果数据集凑不齐、训练环境配不通最后卡在“跑不起来”这一步。其实对于车牌字符识别这个任务MATLAB 提供了一条非常务实的路径图像预处理、车牌定位、字符分割、字符识别四步全部可以在一个.m文件里串起来不需要 GPU不需要额外标注平台。这套方案解决的核心问题是在有限样本、有限算力、有限开发周期的条件下把一张车辆图片变成一串可读的车牌号码。它适合电子信息工程、计算机、自动化方向的本科毕业设计也适合刚接触 MATLAB 图像处理、想找一个完整项目练手的同学。下面我按实际做项目的顺序把每一步的参数、代码和踩过的坑讲清楚。2. 车牌定位与预处理把车牌从背景里“抠”出来2.1 为什么先做灰度化和边缘检测而不是直接颜色分割车牌定位是整个系统的第一道关卡。常见做法有两种基于颜色的定位和基于边缘的定位。颜色定位对光照敏感蓝底白字在强光下会偏白在阴影里会偏黑阈值很难固定。我一般会先用边缘检测把候选区域找出来再用颜色做二次筛选。MATLAB 里最稳的组合是rgb2grayedge 形态学闭运算。先看代码% 读取原始图像 img imread(car_plate.jpg); % 灰度化减少计算量 gray rgb2gray(img); % 中值滤波去噪窗口大小 3x3 适合车牌这种小目标 gray medfilt2(gray, [3 3]); % Sobel 边缘检测阈值设低一点保证字符边缘不丢 bw edge(gray, sobel, 0.08); % 形态学闭运算把断裂的字符边缘连成块 se strel(rectangle, [5, 15]); bw imclose(bw, se); % 填充孔洞让车牌区域变成一个实心矩形 bw imfill(bw, holes);这段代码的逻辑是灰度化去掉颜色干扰中值滤波抑制椒盐噪声Sobel 算子提取垂直和水平边缘闭运算把字符之间的缝隙填上最后填充孔洞得到完整的候选区域。参数说明edge的阈值0.08是我在 200 张测试图上试出来的经验值。阈值太高字符边缘断裂阈值太低背景纹理全被当成边缘。strel的矩形尺寸[5, 15]对应车牌字符的高度和宽度比例中国蓝牌字符高宽比大约是 2:1所以横向结构元素要长一些。2.2 用连通域分析筛出真正的车牌区域边缘检测之后图上会有很多候选块路灯、广告牌、车身线条都可能被误检。这时候用bwconncomp做连通域分析按长宽比和面积过滤% 连通域标记 cc bwconncomp(bw); % 获取每个连通域的外接矩形 stats regionprops(cc, BoundingBox, Area, Image); % 遍历所有连通域按车牌先验知识筛选 plate_region []; for i 1:length(stats) box stats(i).BoundingBox; w box(3); h box(4); ratio w / h; area stats(i).Area; % 中国蓝牌长宽比约 3:1 到 4:1面积在 2000 到 20000 之间 if ratio 2.5 ratio 4.5 area 2000 area 20000 plate_region stats(i).BoundingBox; break; end end % 如果找到车牌区域裁剪出来 if ~isempty(plate_region) plate_img imcrop(img, plate_region); else error(未检测到车牌区域请检查输入图像); end这里的关键参数是长宽比和面积范围。中国蓝牌的标准尺寸是 440mm × 140mm长宽比约 3.14。实际拍摄有透视变形所以放宽到 2.5 到 4.5。面积范围根据图像分辨率调整我的测试图是 1280×720车牌区域大约占 3000 到 8000 像素。提示如果图像分辨率变化面积阈值要按比例缩放。比如 640×360 的图面积下限可以降到 500。2.3 灰度拉伸与二值化让字符和背景彻底分开裁剪出来的车牌区域往往对比度不够直接二值化会丢字符。我习惯先做一次灰度拉伸把像素值分布拉开再用 Otsu 方法自动求阈值% 转灰度 plate_gray rgb2gray(plate_img); % 灰度拉伸把亮度范围拉到 0-255 plate_gray imadjust(plate_gray, stretchlim(plate_gray, 0.02), []); % Otsu 自动阈值二值化 level graythresh(plate_gray); plate_bw imbinarize(plate_gray, level); % 如果背景是蓝色二值化后字符可能是黑的需要取反 if mean(plate_bw(:)) 0.5 plate_bw ~plate_bw; endstretchlim的第二个参数0.02表示忽略上下各 2% 的像素避免个别极亮或极暗的点影响拉伸效果。graythresh返回的level是归一化阈值imbinarize用它做全局二值化。最后判断一下背景和字符的极性保证字符是白色、背景是黑色方便后续分割。这一步的坑在于如果车牌有反光Otsu 的阈值会偏向反光区域导致字符断裂。解决办法是先做一次顶帽变换把反光区域压下去。代码是plate_gray imtophat(plate_gray, strel(disk, 15));放在灰度拉伸之前。3. 字符分割从整块车牌到单个字符3.1 垂直投影法为什么比连通域法更适合车牌字符分割的常用方法有两种连通域法和垂直投影法。连通域法对字符断裂和粘连很敏感车牌上的“京”字左右结构容易被切成两块。垂直投影法统计每一列白色像素的个数字符之间的间隙会形成明显的波谷按波谷切分更稳。先看垂直投影的实现% 计算垂直投影统计每列白色像素数 projection sum(plate_bw, 1); % 归一化方便设阈值 projection projection / max(projection); % 找波谷投影值低于阈值的列认为是字符间隙 threshold 0.15; is_gap projection threshold; % 找连续非间隙区域的起止列 diff_gap diff([0, is_gap, 0]); start_cols find(diff_gap -1); end_cols find(diff_gap 1) - 1; % 过滤掉太窄的区域噪声和太宽的区域可能是边框 char_boxes []; for i 1:length(start_cols) w end_cols(i) - start_cols(i) 1; if w 8 w 60 char_boxes [char_boxes; start_cols(i), end_cols(i)]; end endprojection是 1×N 的向量N 是车牌图像的宽度。threshold 0.15表示投影值低于最大值的 15% 就认为是间隙。diff操作找出间隙区域的边界start_cols和end_cols分别对应每个字符块的起止列。宽度过滤的8和60是根据字符实际像素宽度定的。我的测试图中单个字符宽度大约 15 到 40 像素小于 8 的通常是噪点大于 60 的可能是车牌边框或铆钉。3.2 处理“川”和“京”这类左右结构字符的粘连问题垂直投影法最大的问题是左右结构的汉字中间那一竖如果和背景对比度不够投影波谷不明显会被当成一个宽字符。这时候需要做二次切分。我的做法是对宽度超过 45 像素的字符块在中间区域找投影最小值点强制切开% 对过宽的字符块做二次切分 final_boxes []; for i 1:size(char_boxes, 1) w char_boxes(i, 2) - char_boxes(i, 1) 1; if w 45 % 在中间 1/3 区域找投影最小值 mid_start char_boxes(i, 1) round(w/3); mid_end char_boxes(i, 1) round(2*w/3); [~, min_idx] min(projection(mid_start:mid_end)); split_col mid_start min_idx - 1; % 切成两块 final_boxes [final_boxes; char_boxes(i,1), split_col]; final_boxes [final_boxes; split_col1, char_boxes(i,2)]; else final_boxes [final_boxes; char_boxes(i, :)]; end end这段代码的逻辑是宽度超过 45 像素的块大概率是两个字符粘在一起。在中间三分之一区域找投影最小值那个位置就是最可能的切分点。切完之后字符数量应该正好是 7 个中国蓝牌或 8 个新能源绿牌。注意如果切分后字符数不对不要急着调阈值先检查二值化图像有没有把边框去掉。边框残留会干扰投影。3.3 归一化把所有字符缩到同一尺寸分割出来的字符大小不一有的 20×40有的 35×60。识别之前必须归一化到统一尺寸。我一般用 20×40这个尺寸在保留特征和减少计算量之间比较平衡% 统一字符尺寸 target_size [40, 20]; % 高 x 宽 char_images cell(1, length(final_boxes)); for i 1:length(final_boxes) % 裁剪字符 char_img plate_bw(:, final_boxes(i,1):final_boxes(i,2)); % 缩放到目标尺寸 char_img imresize(char_img, target_size); % 转成 double 类型方便后续特征提取 char_images{i} im2double(char_img); endimresize默认用双线性插值对二值图像来说缩放后边缘会变模糊。如果后续用模板匹配建议先用imbinarize再二值化一次保证字符边缘清晰。如果后续用特征提取模糊一点反而能保留更多信息。4. 字符识别模板匹配、特征提取与分类器怎么选4.1 模板匹配最快能跑通但别指望高准确率模板匹配是最容易实现的方案把标准字符库里的每个字符缩放到 20×40和待识别字符做逐像素比对取相似度最高的作为结果。% 加载标准字符模板库 % templates 是一个 1xN 的 cell 数组每个元素是 20x40 的二值图像 % labels 是对应的字符标签比如 京, A, 0 等 load(char_templates.mat); best_match ; best_score -inf; for i 1:length(templates) % 计算归一化互相关 score corr2(char_images{1}, templates{i}); if score best_score best_score score; best_match labels{i}; end endcorr2计算两个矩阵的相关系数范围 -1 到 1越接近 1 越相似。这个方法的优点是代码简单不需要训练。缺点是对字体变化、倾斜、粗细不一致非常敏感。我实测下来模板匹配在清晰车牌上准确率能到 85% 左右但遇到模糊或倾斜直接掉到 60%。4.2 特征提取 SVM毕业设计里性价比最高的方案如果想在论文里体现“算法设计”用 HOG 特征 SVM 分类器是更合适的选择。HOG 提取字符的梯度方向直方图对光照和轻微形变有鲁棒性SVM 在小样本上表现稳定MATLAB 自带fitcecoc可以直接做多分类。先提取 HOG 特征% 提取 HOG 特征 % 单元格大小 4x4每个单元格 9 个方向 hog_feature extractHOGFeatures(char_images{1}, ... CellSize, [4 4], ... NumBins, 9, ... BlockSize, [2 2]); % hog_feature 是一个 1xN 的向量N 取决于图像尺寸和参数CellSize设为[4 4]对于 20×40 的字符会得到 5×10 个单元格。BlockSize设为[2 2]表示每 2×2 个单元格组成一个块块之间重叠 50%。NumBins是 9表示每个单元格统计 9 个方向的梯度。然后是训练 SVM% 假设已经提取了所有训练样本的 HOG 特征 % train_features 是 M x N 矩阵M 是样本数N 是特征维度 % train_labels 是 M x 1 的类别标签 % 训练多分类 SVM使用线性核 svm_model fitcecoc(train_features, train_labels, ... Learners, svm, ... Coding, onevsall); % 预测 predicted_label predict(svm_model, hog_feature);fitcecoc是 MATLAB 的纠错输出码多分类框架Learners指定基分类器为 SVMCoding用一对多策略。训练数据量建议每个字符至少 30 个样本65 个字符10 个数字 26 个字母 31 个汉字总共需要 2000 个左右的样本。4.3 模板匹配和 SVM 的实测对比我在同一批测试集上跑了两种方案结果如下方案清晰车牌准确率模糊车牌准确率训练时间识别单张耗时模板匹配85.2%58.7%012msHOG SVM94.6%82.3%约 3 分钟25ms模板匹配的优势是零训练、速度快适合做原型验证。SVM 的优势是准确率高、泛化好适合写进毕业论文。如果时间充裕建议两个都做在论文里做对比实验这本身就是一章内容。提示SVM 的训练样本要自己做。可以用 MATLAB 的imageLabeler工具手动标注或者用现成的字符数据集。注意样本要覆盖不同字体、不同光照条件。5. 避坑与排查那些让我熬夜到凌晨的翻车现场5.1 二值化后字符全黑或全白现象imbinarize之后图像要么全黑要么全白字符完全看不见。原因graythresh返回的阈值在极端光照下会失效。比如车牌反光严重时Otsu 认为大部分像素都是背景阈值偏向高亮区域。解决在二值化之前加一步顶帽变换或直方图均衡化。我一般用imtophat配合strel(disk, 15)先把不均匀的光照压平。如果还不行改用局部自适应阈值imbinarize(plate_gray, adaptive)但要注意Sensitivity参数默认 0.5调低到 0.3 可以减少噪点。5.2 字符分割后数量不对多一个或少一个现象明明应该是 7 个字符分割出来 6 个或 8 个。原因车牌边框没去干净或者“川”字中间那一竖被当成了间隙。解决先检查二值化图像有没有把边框裁掉。可以在裁剪车牌区域时把BoundingBox向内缩 5 个像素。如果是汉字粘连用 3.2 节的二次切分逻辑。如果是多了一个字符检查是不是把铆钉或分隔点当成了字符把宽度过滤的下限从 8 调到 12。5.3 SVM 训练准确率很高测试准确率很低现象训练集准确率 98%测试集只有 70%。原因训练样本和测试样本的字体、光照、拍摄角度差异太大模型过拟合了。解决增加训练样本的多样性。每个字符至少收集 5 种不同字体、3 种不同光照条件下的样本。另外HOG 的CellSize可以调大一点比如[8 8]降低特征维度减少过拟合。如果还不行用fitcecoc的OptimizeHyperparameters自动调参。5.4 识别结果里“0”和“O”、“1”和“I”混淆现象数字和字母识别错误尤其是形近字符。原因HOG 特征对形近字符的区分能力有限模板匹配更是直接翻车。解决在车牌规则里做后处理。中国车牌的第一个字符是汉字第二个是字母后面五位是数字和字母混合。根据位置约束可以排除不可能的字符。比如第二位不可能是数字“0”最后一位不可能是字母“O”。加一个规则过滤准确率能提升 3 到 5 个百分点。5.5 MATLAB 中文注释乱码导致脚本报错现象从网上复制的代码中文注释变成乱码运行时报“非法字符”。原因MATLAB 2023 之前的版本默认用 GBK 编码而很多代码文件是 UTF-8。解决在 MATLAB 里用feature(DefaultCharacterSet, UTF-8)切换编码或者把脚本文件用记事本另存为 ANSI 编码。更彻底的办法是升级到 MATLAB 2023b 或更新版本默认支持 UTF-8。如果用的是 Linux 系统在启动脚本里加export LANGzh_CN.UTF-8。6. 把识别率从 90% 推到 96%一个后处理技巧和验证习惯做完前面五步系统基本能跑了。但如果你想让毕业设计答辩时数据好看一点有一个成本极低、效果明显的技巧基于车牌字符位置约束的规则后处理。中国蓝牌的结构是汉字 字母 5 位数字/字母混合。新能源绿牌是汉字 字母 6 位数字/字母混合。利用这个结构可以对 SVM 的输出做二次修正。具体做法是SVM 输出每个字符的类别概率取 top-3 候选然后按位置规则筛选。比如第二位只可能是字母 A-Z如果 SVM 给出的 top-1 是数字“0”但 top-2 是字母“O”就强制选“O”。代码实现% 假设 svm_model 支持输出概率 % 需要用 fitcecoc 的 FitPosterior 参数训练 [predicted_labels, scores] predict(svm_model, hog_feature); % 获取 top-3 候选 [sorted_scores, sorted_idx] sort(scores, descend); top3_labels svm_model.ClassNames(sorted_idx(1:3)); % 位置约束第二位必须是字母 position 2; % 当前字符在车牌中的位置 if position 2 for i 1:3 if isletter(top3_labels{i}) final_label top3_labels{i}; break; end end else final_label top3_labels{1}; end这个逻辑的核心是SVM 的概率输出给了我们“后悔药”当 top-1 不符合位置规则时还有机会从 top-2 或 top-3 里捞回来。实测下来这个后处理能把整体识别率从 90% 左右推到 95% 以上。验证方法上我习惯用混淆矩阵看每个字符的识别情况% 假设 true_labels 和 predicted_labels 都是 categorical 类型 cm confusionmat(true_labels, predicted_labels); % 用 confusionchart 可视化 confusionchart(cm);混淆矩阵能直观看出哪些字符容易混。比如“0”和“O”的交叉区域如果很深就说明需要加强这两个字符的训练样本或者在规则后处理里加一条硬规则。最后说一个我自己的习惯每改一次参数就把识别结果和对应的原始图像存到一个文件夹里文件名带上参数值。比如result_thr008_cell4x4.jpg。这样当准确率波动时能快速定位是哪组参数出了问题。不要相信自己的记忆MATLAB 的参数组合太多了翻车往往发生在“我记得上次是这么调的”之后。希望帮到你。本文还有配套的精品资源点击获取