简介基于MATLAB的车牌识别系统完整项目面向图像处理初学者、高校课程设计者以及车辆视觉方向学习者重点解决正向拍摄车牌检测与字符识别问题。压缩包共67个文件以JPG图像和M源文件为主另有FIG图形界面与MD说明文档整体容量412KB。已有144人浏览学习。资源内提供可直接运行的GUI界面内置字符模板覆盖省份汉字、字母与数字并包含多张不同条件下的测试图片项目完整覆盖二值化、灰度化、膨胀操作、均值滤波等关键图像预处理环节每一步均保留处理后的效果示例图便于对照观察中间过程。借助源码与说明文档读者既能快速跑通识别流程也能深入分析各环节对车牌定位和字符分割的影响为算法调优、功能扩展或课程设计报告撰写提供切实参考。1. 车牌识别MATLAB的一条完整图像处理链路车牌识别在MATLAB里做出来听起来像一个老课程设计题目但它涉及的技术栈恰好是图像处理入门的完整链条灰度化、边缘检测、形态学运算、连通域分析、投影分割、模板匹配。工业现场的车牌识别早被专用相机和深度学习模型替代但用MATLAB自己搭一遍仍然有价值因为每个环节都能单独可视化定位不准时你能马上判断是哪一步的参数出了问题。本文按最常见的蓝色底白字车牌为例子把定位、分割、识别三段核心代码拆开顺带说明尺寸阈值、模板样本和前后处理这几个最容易翻车的点。适合正在做课程设计、毕业设计以及想快速验证图像处理算法的工程师。2. 车牌定位用MATLAB先把候选区域从背景里抠出来车牌定位是整个流程里最容易失败的一步。背景里有树荫、车灯、路面文字、反光条纹这些都会成为干扰。定位的思路概括起来就两条一是靠边缘梯度找矩形区域二是靠颜色特征找蓝色区域。两者不是互斥的我一般会先用边缘定位失败后再用HSV颜色分割兜底最后再按几何比例做一次确认。2.1 边缘定位和颜色定位怎么选边缘定位对光照变化相对稳定因为车牌字符和底色之间有稳定梯度车牌边框和车身之间也有明显灰度跳变。颜色定位在复杂背景中更有针对性但依赖白平衡和光照条件不同相机的蓝色色偏差异很大。两种方法的适用关系如下表方法适用场景常用MATLAB函数主要失败原因边缘Sobel背景纹理少、车牌清晰edge、strel、imclose、imfill边缘断裂车牌和车体连成一块灰度形态学车牌倾斜不严重regionprops、bwconncomp与白色车身粘连导致面积筛选失败HSV颜色分割复杂背景、多颜色场景rgb2hsv、逻辑阈值色相阈值不好定蓝色车漆也被选中深度学习检测倾斜、遮挡、夜间开源检测模型需要额外部署环境不适合纯MATLAB课设实际做的时候不需要一开始就上很多函数。先把最小能跑通的流程搭起来再针对失败图加分支。2.2 最小可运行的MATLAB定位脚本下面这段代码是边缘定位的核心部分能处理大多数白天场景下的蓝色车牌。假设原始图像在test.jpg中输出候选车牌图。% main_plate_locate.m clear; close all; img imread(test.jpg); % 读入原始彩色图 if size(img, 3) 1 gray img; % 如果已经是灰度图 else gray rgb2gray(img); % 三通道转灰度 end % 边缘定位用Sobel算子把车牌区域梯度拉出来 % 车牌字符和底色之间存在明显边缘所以阈值选0.15 edgeImg edge(gray, sobel, 0.15); % 磁盘结构元素半径影响牌照区域的连通程度 se strel(disk, 7); closeImg imclose(edgeImg, se); % 闭运算把字符边缘和边框断口连起来 fillImg imfill(closeImg, holes); % 填充内部空洞 % 8连通域提取 cc bwconncomp(fillImg, 8); stats regionprops(cc, BoundingBox, Area); % 按面积和宽高比筛选候选车牌区域 minArea 500; % 小图建议500~2000按图像尺寸缩放 allBox []; for i 1:length(stats) if stats(i).Area minArea continue; end bbox stats(i).BoundingBox; w bbox(3); h bbox(4); % 普通车牌宽高比约3:1到4:1给一点余量 if w h*2.5 w h*6 allBox(end1, :) bbox; %#okSAGROW end end % 简单去重这里保留面积最大的候选框 [~, idx] max(allBox(:,3) .* allBox(:,4)); plate imcrop(img, allBox(idx, :)); figure; imshow(plate); title(定位结果);逻辑说明edge(gray,sobel,0.15)返回二值边缘图比背景多的地方会形成连续的边缘簇。imclose的作用是把同一物体上的断口接起来否则字符“京A”这些笔画的边缘会散成碎片。imfill填充边缘围起来的区域让车牌变成一个闭合连通块。bwconncomp和regionprops负责找块、量面积。参数说明minArea要跟随图片尺寸变化。如果测试图是 1920x1080建议设为 2000 以上如果是 640x480500 就足够。宽高比范围建议从小到大调不要一上来就限定 3:1因为倾斜车牌在二维框里的视觉宽高比会变小。disk的半径 7 适合 640 宽度左右的图图像尺寸变大时要同步增大。2.3 复杂背景下用HSV蓝色分割兜底如果边缘定位返回空结果或框到了车窗我一般会换HSV颜色分割。HSV 比 RGB 更容易分离颜色本身和亮度尤其适合处理蓝色车漆、蓝色交通标志。% hsv_blue_plate.m imgHsv rgb2hsv(img); % 转换色相、饱和度、明度 % 蓝色色相大致在0.55~0.75之间饱和度要高避免把白色当成蓝色 blueMask (imgHsv(:,:,1) 0.55 imgHsv(:,:,1) 0.75) ... (imgHsv(:,:,2) 0.3) ... (imgHsv(:,:,3) 0.15); % 形态学清理 blueMask imopen(blueMask, strel(disk, 3)); blueMask imclose(blueMask, strel(disk, 15)); % 再走连通域筛选 cc2 bwconncomp(blueMask, 8); stats2 regionprops(cc2, BoundingBox, Area); % 之后仍按面积和宽高比筛选代码和2.2相同这里imopen先去掉孤立的蓝色噪点imclose把车牌区域内的字符空洞填上。HSV定位的失败点往往是色相上下限同一个蓝色在暖光和冷光下能差出 0.05 到 0.1。建议先把蓝色区域单独imshow出来再调整阈值不要盲改。3. 字符分割在二值化车牌上按投影切出每一个字符定位拿到的是包含车牌的矩形图下一步是把京A12345这样的字符一个个切出来。字符分割看起来简单但实际会遇到铆钉、车牌边框、汉字左右分离、粘连字符四种问题。常见做法是先二值化再去边框和铆钉最后用垂直投影找字符边界。3.1 车牌图像先做二值化与去边框定位出来的plate仍然是彩色首先要转灰度并做二值化。MATLAB 的imbinarize默认用Otsu阈值对车牌这种黑白分明的区域比较管用。% segment_plate_preprocess.m plateGray rgb2gray(plate); bw imbinarize(plateGray); % Otsu自动阈值字符为白色 % 去掉上下边框统计每一行的白色像素数量边框行通常特别密集 rowSum sum(bw, 2); rowMask rowSum max(rowSum) * 0.8; % 去掉明显高亮行 bw bw(rowMask, :); % 去掉左右边框每一列的白色数量如果突然接近整行高度可能就是边框 colSum sum(bw, 1); colMask colSum size(bw,1) * 0.8; bw bw(:, colMask);这段代码的问题在于对二值化非常敏感。如果车牌图像偏暗字符可能变成黑色背景白字这时需要bw ~bw翻转。另外imbinarize在灰度不均匀时会把阴影也切成白色解决办法是先做imtophat顶帽变换去除不均匀光照再用Otsu。3.2 垂直投影切分字符的MATLAB实现完成去边框后bw是一个黑底白字的二值矩阵。垂直投影就是统计每一列有多少白色像素字符所在的列白色像素多字符间隙列白色像素少依据这个就能切出每个字符。% segment_by_projection.m % bw 是去除边框后的二值化车牌字符为白色背景为黑色 projV sum(bw, 1); % 垂直投影每列白色像素总数 threshold max(projV) * 0.1; % 低于该阈值的列认为是字符间隙 inChar projV threshold; charCols {}; col 1; nCols length(inChar); while col nCols if inChar(col) startCol col; while col nCols inChar(col) col col 1; end if col - startCol 5 % 去掉噪声列 charCols{end1} [startCol, col-1]; %#okSAGROW end else col col 1; end end % 对每个区间进行纵向裁剪 for k 1:length(charCols) charImg bw(:, charCols{k}(1):charCols{k}(2)); % 进一步去掉上下空白 rowProj sum(charImg, 2); rows find(rowProj 0); if ~isempty(rows) charImg charImg(min(rows):max(rows), :); end % 统一尺寸到32x64后续识别阶段使用 charImg imresize(charImg, [64 32], bilinear); end逻辑说明projV是长度为图片宽度的向量每个元素表示对应列的白色像素总数。threshold取最大值的10%是为了抵抗少量噪声列。循环里维护一个滑动窗口遇到白色像素多就记录连续段起点直到碰到间隙列才结束。charCols保存了每个字符的左右边界之后按这些边界裁剪并去掉上下空白得到干净的字符图。参数说明10% 可能偏严或偏松如果字符粘连就把阈值提高到20%~30%如果字符被切断就降到5%。col - startCol 5是过滤噪声列图片宽度只有几十像素时可以降到2。3.3 汉字和数字的合并规则汉字“京”“沪”“苏”这类字左右结构明显投影时会被切成两段甚至三段。直接按连续段切会把“杭”变成“木”和“亢”。因此切割之后要按相邻段的距离和高度做合并。% merge_break_chars.m % charCols 是 3.2 得到的字符区间列表每个区间为 [left, right] mergeCols {}; i 1; while i length(charCols) cur charCols{i}; if i length(charCols) gap charCols{i1}(1) - cur(2); % 两段之间的空隙 % 如果空隙小于字符宽度的0.4倍则认为是一个字 if gap 0.4 * (cur(2) - cur(1) 1) % 还要判断高度是否接近避免把相邻两个独立字符合并 % 这里简化处理只合并间隙小于阈值的连续段 nxt charCols{i1}; mergeCols{end1} [cur(1), nxt(2)]; %#okSAGROW i i 2; continue; end end mergeCols{end1} cur; %#okSAGROW i i 1; end合并导致的新问题如果车牌字符间距本身就小比如“冀F”后面的“F”和数字“1”距离很近也容易被合并不开。所以合并时要额外看两个候选段的高度差如果高度差超过30%宁可不合并。总而言之分割是识别准确率的真正瓶颈字符切歪了模板匹配做得再漂亮也没用。4. 字符识别MATLAB模板匹配的落地写法与微调字符识别这块模板匹配算是最经典的做法。它的优点是不需要训练只要有一组标准字符模板就能跑缺点是字体变化、倾斜、噪声都会影响结果。对于车牌这种字体相对固定的场景足够应付课程设计和中小规模验证。4.1 模板匹配的数学选择MATLAB 里有两个容易混淆的函数corr2和normxcorr2。corr2直接计算两张图对应像素的相关系数要求两图尺寸一致normxcorr2是归一化互相关能处理平移偏移但计算慢。车牌字符经过了前一步的裁剪和缩放位置基本对齐所以corr2更实用。corr2对线性亮度变化不敏感比如字符变亮或变暗相关性不会剧烈下降。4.2 MATLAB模板匹配实现模板匹配的核心逻辑很直接把所有模板读进内存逐个和待识别字符求相关系数取最大的作为结果。% recognize_by_template.m % charImg 是归一化到 32x64 的字符图 % templateDir 下每个 .png 文件名为标签如 A.png, 京.png function label recognizeChar(charImg, templateDir) charImg im2double(charImg); charImg imresize(charImg, [64 32], bilinear); files dir(fullfile(templateDir, *.png)); bestScore -inf; label ; for i 1:numel(files) tpl imread(fullfile(templateDir, files(i).name)); tpl im2double(tpl); tpl imresize(tpl, [64 32], bilinear); score corr2(charImg, tpl); if score bestScore bestScore score; label files(i).name(1:end-4); % 去掉.png后缀 end end end逻辑说明files用dir读取模板文件夹下所有PNG文件文件名第一段作为标签。corr2会把两张图展开成一个向量计算皮尔逊相关系数结果越接近1说明越像。imresize统一尺寸是为了让模板和输入对齐。识别时如果所有模板分数都比较低低于0.3就应该放弃识别而不是直接给一个最大结果。模板文件本身要预处理干净。模板里如果带了边框或空白corr2会把边框的相关系数也计算进去导致“1”和“7”难以区分。建议模板生成时用和实际字符相同的二值化方式例如统一使用imbinarize加同样的归一化尺寸。4.3 两个很容易被忽略的识别细节第一个细节是裁剪字符边界。用imresize缩放前如果字符本身四周有空白缩放后空白会被放大模板匹配时空白区域占了很大比例区分度下降。第二个细节是只靠corr2区分相近字符不够需要辅助特征。例如“6”和“8”、“B”和“8”都很容易互相误判。辅助特征计算方式用途像素密度sum(bw(:)) / numel(bw)区分1和I、0和O字符宽高比字符实际边界盒的宽/高区分1和7汉字和数字孔洞数量regionprops(bw,EulerNumber)区分8和B、0和D实际使用中我会把模板匹配的相关系数和像素密度各算一个分最后用加权和判断。例如总得分 0.8 * corr2 0.2 * (1 - abs(density_tpl - density_char))。这样能明显减少数字0和字母O、数字1和字母I的误判。5. 车牌识别MATLAB的调参与验证别让识别率骗了你整条流程跑通后最忌讳的事情是拿三五张图看结果不错就说识别成功。我建议做两件事一是统计单字符混淆矩阵二是做批量参数扫描。5.1 用混淆矩阵看单字错误MATLAB 自带confusionmat但需要统计和机器学习工具箱。没有工具箱时可以用手动统计逻辑非常简单。% error_matrix.m % labels 和 preds 是两个长度相同的字符串元胞数组 % labels 为真实标签preds 为识别结果 allLabels unique([labels, preds]); % 取所有出现过的字符 errMat zeros(numel(allLabels)); for i 1:numel(labels) r find(strcmp(allLabels, labels{i})); c find(strcmp(allLabels, preds{i})); errMat(r, c) errMat(r, c) 1; end % 非对角线元素之和除以总数就是单字符错误率 singleCharErr (sum(errMat(:)) - trace(errMat)) / numel(labels); fprintf(single char error rate: %.2f\n, singleCharErr);这段代码的关键是errMat(r,c)表示把真实标签allLabels{r}错认成allLabels{c}的次数。和confusionmat的结果一样但不依赖工具箱。看混淆矩阵时不要只看总体准确率要找哪两个字符互相混淆最多然后针对那对字符加辅助特征。5.2 按批调参而不是单图调参定位阶段的形态学半径和边缘阈值不能靠印象调。我一般写一个循环脚本把disk半径从3到15按步长2扫描把edge阈值从0.05到0.3按步长0.05扫描每组合一次统计最终识别正确数量。这样跑一小时能替代手动试调一整天。批量调参时要注意定位失败、分割失败和识别失败要分开统计。常见做法是输出每一张图的中间结果到独立文件夹按名称编号。定位失败时再去专门优化定位不要在识别阶段浪费时间。5.3 如果想再往上走一步纯模板匹配的极限在标准车牌图像上大约是95%左右遇到倾斜、暗光、雨雾就会明显下降。如果你有深度学习基础可以考虑用开源车牌检测识别模型替代定位和识别两个模块MATLAB 通过importNetworkFromONNX加载ONNX模型预处理仍然沿用本文的颜色空间转换和投影逻辑。这也是一条平滑过渡路线不会一下子推翻前面所有代码。对于纯MATLAB课设来说先把边缘定位、HSV分割、垂直投影、模板匹配这四个模块各自封装成函数再写一个main.m串起来是性价比最高的做法。调参顺序建议从定位阶段的形态学半径开始因为定位错误会导致后面全错而且它最容易通过一行可视化代码观察变化。本文还有配套的精品资源点击获取