多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MATLAB车牌识别实战:从图像预处理到字符识别的完整流程

MATLAB车牌识别实战:从图像预处理到字符识别的完整流程 如果你手头只有一张车辆图片想用MATLAB把车牌号自动认出来其实就是在走一条固定流程图像预处理、车牌定位、字符分割、字符识别。我之前在实验室做过完整的MATLAB车牌识别方案代码按功能拆成了多个子程序维护和复用都很方便这次把整个思路、代码框架和调试经验整理出来希望能帮到正在做课程设计、毕业设计或者单纯想入门MATLAB图像处理的同学。这个方案只用MATLAB基础功能和图像处理工具箱不需要额外安装深度学习框架训练时间短、硬件要求低应对常见的蓝底白字车牌已经足够。我用的是经典的“传统图像处理”路线没有上神经网络。原因很简单在数据集规模不大、识别目标单一的情况下模板匹配加形态学处理完全够用而且每一步都能看到中间结果调起参来心里有底。下面按模块拆开讲。1. 整体流程和方案设计先想清楚再动手车牌识别看着是个单一需求真正拆开之后其实是一连串独立的图像处理任务。动手写代码之前一定要先把流水线理清楚否则后面会被各种边界情况打得措手不及。1.1 为什么这个项目特别适合用MATLAB实现MATLAB处理这类图像问题的优势在于三点。第一是矩阵运算天然友好图像本身就是矩阵灰度化、二值化、边缘检测这些操作都对应现成函数不用自己造轮子。第二是调试体验好脚本里任意一行代码执行完马上能imshow看中间结果这对形态学处理、阈值选择这种需要反复试参的场景太重要了。第三是函数封装简单把定位、分割、识别各写成一个.m文件主程序里几行就能调用这就是标题里“含子程序”的核心价值。有人可能会问现在深度学习这么火为什么不直接用YOLO或者CNN我承认深度模型在复杂场景下泛化能力更强但代价是标注数据、训练时间和显卡资源。很多课程设计的时间就一两个月用手头几百张图去训一个可以稳定工作的检测模型难度远高于传统方法。反过来传统方法对“固定机位、干净背景、标准车牌”的场景处理得又快又稳而且代码逻辑透明写论文也好解释。我建议先把这个方案吃透再去碰深度学习也不迟。1.2 车牌识别的完整流水线拆解整个识别过程可以分成四段每一段都有明确输入输出。图像预处理读入彩色图像转灰度、增强对比度、去噪声目的是让车牌区域更突出。车牌定位在预处理后的图像里找到车牌所在矩形区域输出候选区域的坐标和裁剪后的子图。字符分割把车牌子图按列方向切割得到单个字符的小图同时去掉边框和铆钉干扰。字符识别对每个字符小图做特征提取与模板库比较输出最终车牌字符串。这四个阶段是串行关系前一步的质量直接决定后一步的效果。我在实际测试中最大的感受是一半以上的识别失败都出在定位不够准或分割不干净真正字符识别翻车的反而少。所以后面我会把重心放在前两步。1.3 子程序模块怎么划分“子程序”在MATLAB里一般指独立函数文件或脚本文件。我建议按功能划分而不是按流程阶段简单粗暴切分。我的文件结构大概是这样的main_plate_recognition.m主脚本负责读图、调用子程序、显示结果。locatePlate.m定位子程序输入灰度图输出车牌区域图像和坐标框。segmentChars.m分割子程序输入车牌子图输出字符图元胞数组。recognizeChars.m识别子程序输入字符图输出识别后的字符串。loadTemplate.m模板加载子程序从文件夹读取字符模板到内存。每个函数只做一件事输入输出定义清楚这样任何一个环节想换成别的算法只要保持接口不变其他代码不用动。比如想把模板匹配换成SVM分类器只需修改recognizeChars.m内部实现主程序一行都不用改。这就是模块化的价值。2. 图像预处理与车牌定位决定成败的前半程车牌定位是整条流程里最容易出问题的环节。拍到的车辆可能在画面中间也可能在角落光照可能均匀也可能有阴影背景可能干净也可能有一排栏杆、树叶。定位算法的鲁棒性决定了后面所有步骤能否顺利进行。2.1 灰度化、增强与边缘检测原始图像一般是RGB三通道彩色图第一步要转成灰度。MATLAB里一句rgb2gray(img)就行内部按亮度加权Y 0.299R 0.587G 0.114B。这个权重是有讲究的绿色分量占比最高因为人眼对绿色最敏感转灰度后更符合视觉感受。灰度图出来后我通常会先做一次直方图均衡化用histeq增强对比度。尤其拍摄时背光或阴天车牌灰蒙蒙的直方图均衡化能让车牌字符和底色的边界更锐利。但要注意如果图片本身对比度很好这一步可省过度增强反而会让边缘变粗。定位的核心先找边缘。车牌区域文字密集在垂直方向上会产生大量高频边缘这是定位的重要线索。我用的是Sobel算子提取垂直边缘edgeImg edge(grayImg, sobel, vertical);为什么只看垂直方向因为车牌字符的竖笔画比较密集水平笔画虽然也有但更容易受到车灯、车窗轮廓干扰。经过实测“vertical”方向在多数场景下效果优于完整Sobel或Canny后者细节太多了后续形态学处理不好收。edge函数自带的阈值是自适应计算的但对某些图片会得到太多噪音。我更推荐自己设定阈值比如edge(grayImg, sobel, 0.15, vertical)这个值不是拍脑袋定的需要看边缘图里车牌轮廓是否连续。调参方法我放在第四章统一讲。2.2 形态学处理把车牌区域“找出来”得到边缘图后车牌区域通常是一块由字符边缘组成的“点阵簇”需要通过闭运算把邻近边缘连到一起再通过开运算去掉小的孤立区域。我常用的结构元素是一个横向矩形se strel(rectangle, [5, 17]); morphImg imclose(edgeImg, se); morphImg imopen(morphImg, strel(rectangle, [3, 3]));这里有两个参数值得解释。闭运算的矩形高度取5、宽度取17是因为车牌的宽高比大约为3比1字符横向分布用宽矩形可以把同一行里的字符边缘连成片同时又不会把上下行的干扰连进来。如果图片分辨率更高比如从高清相机拍出来的图这个尺寸要同比放大我一般按图像宽度除以100的整数倍来估算。开运算的核取了很小的3×3作用不是分割大块而是去掉边缘检测产生的散点噪声。开运算结构元素如果太大会把车牌边缘本身也抹掉所以宁小勿大。闭运算后图像里会出现若干连通区域接下来用regionprops找候选区regions regionprops(morphImg, BoundingBox, Area);然后按面积和长宽比筛选。这一步非常关键我会在下一小节详细说明。2.3 长宽比筛选和颜色校验标准蓝色车牌的宽高比是440毫米比140毫米约3.14比1但实际截出来的区域包含白色边框比例会稍微变化大概在2.5到5.5之间都算合理。再用面积约束过滤掉太小的噪点块for k 1:length(regions) w regions(k).BoundingBox(3); h regions(k).BoundingBox(4); ratio w / h; area regions(k).Area; if ratio 2.4 ratio 5.8 area 1000 candidateBox regions(k).BoundingBox; break; end end注意这个面积阈值不是绝对的我建议改成相对阈值比如area imageWidth * imageHeight * 0.005这样图像分辨率变了也不用频繁改写。光靠长宽比还有可能误检到车身贴纸、前挡风玻璃区域。这时候可以再加一个颜色校验原始彩色图里车牌区域的蓝色均值应该明显高于周围。我在代码里把候选区域转回HSV或直接用RGB蓝色通道判断。更简单的做法是计算候选区域蓝色像素比例blueMask colorImg(:,:,3) 120 colorImg(:,:,3) - colorImg(:,:,2) 30; blueRatio sum(blueMask(:)) / numel(blueMask(:));如果blueRatio低于某个阈值比如0.25就认为不是车牌区域。这个参数在不同光照下需要微调实际拍摄时偏色严重的话最好先做白平衡或颜色校正否则颜色校验反而会把真车牌误杀。3. 字符分割与识别把号码一个个读出来定位到车牌区块后接下来要做的工作类似“把一整行字拆成单个汉字”再到“每个字是啥”两步。字符分割对后续识别准确率影响极大这个环节我会多讲一些处理和判断的细节。3.1 垂直投影法分离字符字符分割最常见的方法是垂直投影把二值化后的车牌子图按列求和得到每个像素列上白色像素的数量。字符区域会有较高投影值字符之间的空隙投影值接近0。根据这个峰谷分布就能切出每个字符。先要做一个前处理对得到的车牌子图转灰度再用阈值imbinarize做二值化。因为车牌底色是蓝色阈值分割时如果直接对灰度图做全局阈值容易把亮斑和暗斑搞混。我建议先对蓝色通道做处理比如用colorImg(:,:,3) - colorImg(:,:,2)得到蓝黄色差图再取阈值。蓝色车牌的蓝色通道明显大于红色和绿色通道这样能很好地把白色字符和蓝色底分离开。二值化之后我还会做一次中值滤波去掉边框和铆钉产生的孤立噪点binPlate medfilt2(binPlate, [3, 3]);注意二值化后白色字符是前景还是背景取决于你的阈值方向。我习惯让字符为1、底色为0这样投影求和得到的就是字符区域的“厚度”。垂直投影的代码很简洁proj sum(binPlate, 1);然后找到投影值从0变成正数的起点和从正数变成0的终点一对一对地记下来每个区间就是一个候选字符。正常情况下第二个字符和第三个字符之间会有一个小圆点间隔投影图上是一段近距离的低谷这部分在切割时要小心不能把点也当成独立字符。实际切出来的字符宽度不会完全一致我通常再做一个归一化判断如果某个候选区间的宽度超过平均宽度的1.5倍说明这个区域可能包含了两个字符需要二次切分如果宽度小于平均宽度的0.4倍多半是噪声或边框残留直接丢掉。3.2 模板匹配识别的原理与实现字符分割完成后每个字符图都要送去识别。模板匹配的思路很朴素把字符图和模板库里每个模板做相似度计算相似度最高的模板字符就是识别结果。这里需要先准备一套标准的字符模板包括省份简称汉字、大写字母和数字。模板来源可以是自己用程序切出来的干净字符图也可以是网上公开的标准模板集。每个模板在加载时都要缩放到统一尺寸比如40×20像素并归一化成同样的二值模式。匹配时最常用的是计算归一化互相关系数MATLAB里可以直接用corr2corr corr2(charImg, templateImg);遍历所有模板取最大corr值对应的编号即可。这个方法虽然简单但要注意字符粗细、位置偏移都会显著影响相关系数。为了让匹配更鲁棒我会对模板做一点小偏移测试比如分别把模板向左、右、上、下平移1到2个像素各算一次相关系数取最大值。这样做能在一定程度上容忍分割时产生的微小偏移。3.3 子程序的接口设计与参数传递虽然这个项目规模不大但我还是坚持用函数封装。每个函数入口和出口尽量简单明确避免互相干扰。比如字符分割子程序的定义function [charCells, plateBin] segmentChars(plateImg)入口是车牌彩色子图出口是分割后的字符图元胞数组和用于调试的二值图。在函数内部所有中间变量都是局部的不污染工作区。有人喜欢用全局变量传递参数但全局变量容易让程序越改越乱我强烈建议养成“接口传参”的习惯。子程序里的参数有些需要手动调整比如二值化阈值、形态学结构元素尺寸。我一般把它们集中定义在函数开头的参数区并加上注释方便后面对着不同的测试集快速修改。function [charCells, plateBin] segmentChars(plateImg) % 参数区 binThreshold 0.6; % 蓝黄色差二值化阈值 medfiltSize [3, 3]; % 中值滤波窗口 minCharWidthRatio 0.4; % 最小字符宽度比例 maxCharWidthRatio 1.5; % 最大字符宽度比例 % ... 中间处理 ... end这种写法还有个好处你可以在主脚本里用输入参数或配置文件统一覆盖这些默认值不用每个函数都改一遍。4. 完整代码框架与关键参数调整很多人拿到代码后直接运行结果发现前面那辆车识别不对第一反应是“代码有问题”其实多半是参数不适配当前图像。第四章我就把整个项目的主干代码和参数调试方法摊开来讲。4.1 主程序和子程序文件结构假设你已经把locatePlate.m、segmentChars.m、recognizeChars.m、loadTemplate.m全部放在当前路径下主程序可以这样写%% 主程序车牌识别 clc; clear; close all; % 读取图像 img imread(car.jpg); % 调用定位子程序 [plateImg, plateBox] locatePlate(img); % 调用分割子程序 [charCells, plateBin] segmentChars(plateImg); % 调用识别子程序 result recognizeChars(charCells, templates); % 显示结果 figure; imshow(img); rectangle(Position, plateBox, EdgeColor, r, LineWidth, 2); title([识别结果: , result]);其中templates可以由loadTemplate.m加载templates loadTemplate(./template);loadTemplate.m内部用dir列出模板文件夹下所有图片按文件名读取并统一缩放成40×20返回一个结构体数组。整个工程就是一个主脚本加四个函数文件拷贝到任何一台装有MATLAB的电脑上都能直接用。对应毕设论文里也很容易画模块结构图。4.2 关键参数怎么选我把整个流程中最影响效果的参数整理成了表格方便对照排查。参数影响环节推荐起点调整方向边缘检测阈值边缘图噪点多少0.15背景复杂时调高车牌不明显时调低闭运算结构元素宽度边缘区域连通程度图像宽度/100连通域太碎就调大粘连过多就调小候选区最小面积过滤非车牌区域图像面积*0.005误检增多时调大车牌长宽比范围定位筛选2.4~5.8斜视拍摄时适当放宽蓝黄色差二值化阈值字符分割0.6光照不足时降低偏色严重时调整颜色系数模板尺寸匹配精度与速度40×20分割后字符实际比例偏差大时全组调整这些数值没有一个是一劳永逸的都需要根据你自己的图片集微调。我建议做一个循环脚本批量读取几十张测试图每次只改一个参数统计识别率用最笨但最有效的网格搜索方式找最优值。比如边缘阈值从0.1到0.3每0.02试一次跑完就能画出识别率曲线。4.3 从识别失败反推参数问题当某张图识别错误时不要急着改代码先搞清楚是哪个环节出的错。一个很实用的调试方式是在主程序里添加多个figure把中间结果全部显示出来图1原图 定位框图2边缘图图3闭运算后的二值图图4分割出来的字符图用montage展示图5模板匹配相似度排序看一遍中间图像问题基本就定位到了。比如原图定位框歪了说明定位阶段有问题定位框正常但分割的字符图连在一起说明分割参数要改分割正常但识别结果错误说明模板库或匹配策略有问题。这个方法比盲改参数高效一个数量级。5. 常见问题、调试顺序与性能优化这个项目做完后我遇到过不少奇怪的情况很多都是不同场景下的典型问题。这里挑几个高频的坑和对应的排查思路分享出来能省你很多时间。5.1 识别失败的高频原因根据我测过的图片最容易翻车的几种场景如下。车牌区域反光阳光直射下车牌字符发白蓝底变浅直接用固定阈值二值化会丢失字符轮廓。解决办法是在二值化前做高光抑制或者用自适应阈值adaptthresh。车牌歪斜拍照角度不正车牌在图像里是梯形直接垂直投影切出来的字符宽度差异很大。先做倾斜校正用霍夫变换检测车牌上下边缘再旋转到水平。新能源绿牌字符还是白色但底色是渐变绿用蓝黄色差图定位会失灵。需要额外判断绿色通道特征把颜色判断逻辑扩展成“蓝色或绿色”。汉字识别率低汉字结构复杂模板比较少的时候容易出现混淆尤其是字母“O”和数字“0”、字母“D”和数字“0”。这个没有特别好的传统办法只能扩充模板样本或者对相似字符做二次判定。5.2 调试顺序和可视化技巧我自己的调试顺序是先用“绝对干净”的合成测试图跑通全流程比如在PPT里打一个白底黑字的假车牌截图存成图片全部识别正确后再换真实照片逐步增加难度。这样做能保证框架本身没有bug后续只是参数适配问题。另外在开发阶段尽量把可视化语句都保留只在最终版本里注释掉。因为每次改完参数你都需要重新观察是变好了还是变坏了。没有可视化就只能盯着最终字符串猜问题效率低到崩溃。5.3 运算速度优化与批处理传统方法速度其实非常快单张图在普通笔记本上运行大概0.1到0.3秒。如果要做批量测试可以在主程序外面套一个循环files dir(./testImages/*.jpg); for i 1:length(files) img imread(fullfile(files(i).folder, files(i).name)); result recognizePlate(img); fprintf(%s - %s\n, files(i).name, result); end如果发现速度太慢90%的情况是模板匹配那里模板数量太多或模板图太大。可以先把所有模板二值化并缩放到统一尺寸存储在内存里不要每次识别都重新读图。另一个优化点是避免在循环里调用imclose这种大核形态学操作必要的话先降采样原图再做定位因为车牌定位不需要全分辨率缩小到宽600像素左右完全够用。最后再分享一个我养成的习惯所有子程序都要写成可以独立运行的形式即使没有主程序也能用一小段测试数据调通。这样做的好处是等项目做完拿去给别人演示时不会出现一个函数报错导致整个主程序崩溃的尴尬情况。这套车牌识别方案没有特别高深的理论但每一步都踩得实。如果你正在做类似项目我建议先把整体流程跑通再逐步优化定位和分割的鲁棒性你会发现MATLAB的图像处理能力比想象中顺手得多。
返回列表