资讯动态

C++结合OpenCV实现车牌识别系统:从图像预处理到字符识别的完整流程

发布时间:2026/9/20 20:45:25 来源:尧图企业网站定制
简介面向C与计算机视觉学习者的车牌识别系统完整工程基于C与OpenCV实现从图像采集、预处理、车牌定位、字符分割到字符识别的全流程。资源适合有C基础、正在学习OpenCV或准备智能交通方向课程设计、毕设的开发者用于理解传统图像处理与字符识别技术在真实场景中的落地方法。包体共125个文件压缩包大小2.21MB。其中代码以9个cpp与13个h为主对应功能模块与头文件附带5个dll与1个exe便于直接运行调试资源内还有88个bmp样本测试图以及sln、vcproj、rc等工程配置文件可供完整复现实验或二次开发。目前已有80人学习下载。通过该工程可掌握车牌定位中的形态学操作、字符分割中的轮廓检测与投影法并参考SVM等字符识别思路是一份结构清晰、可直接运行的项目模板适合对照学习、逐模块调试与功能扩展。 前阵子有个读者私信问我说想拿“C结合OpenCV实现的车牌识别系统”当毕业设计但不知道从哪下手。这让我想起自己第一次做这个项目时的状态搜了一堆资料代码跑通的和跑不通的各占一半看到的全是零散的函数片段很少有文章把“从图像输入到识别结果输出”这条完整链路讲清楚。所以这篇文章不打算只贴代码而是从整体架构出发把车牌定位、字符分割、字符识别三个核心模块的选型理由和实现细节一条条捋出来顺便把环境配置和参数调试的经验也写进去。这个系统本身是个特别典型的计算机视觉入门到进阶项目它同时涉及图像预处理、边缘检测、轮廓分析、形态学操作、机器学习分类这几个OpenCV里最高频的知识点。无论你是准备面试、做课设还是单纯想练手这套东西都能让你把零散的函数调用串成一条完整的技术链路。我先说结论一个能稳定工作的车牌识别系统核心不在识别那一步而在于前面车牌定位和字符分割做得够不够干净——这个比例大约占整个项目工作量的百分之七十。1. 认识车牌识别的基本流程不是每一步都要上深度学习很多新手一上来就想用深度学习做端到端识别结果数据集、GPU、训练时间全是坑。其实对于标准蓝底白字车牌这种结构化目标传统图像处理方案完全够用而且可控性强、好调试、运行快。整个流程可以拆成五步图像读取与预处理转灰度、去噪、边缘增强。车牌定位在整张图中找到车牌所在的矩形区域。车牌校正可选处理倾斜的车牌做透视变换。字符分割把车牌区域切成单个字符的小图。字符识别对每个字符图进行分类输出字符串。如果你的输入是摄像头实时视频流那么还要额外加入帧率控制和感兴趣区域裁剪但核心链路不变。这个流程设计有一个很重要的原则每步只做一件事前一步的结果质量直接决定后一步的难度。比如车牌定位如果框得不准后续字符分割得到的就是一张歪斜或比例不对的图像识别准确率会断崖式下降。说句实话真正做工业级车牌识别系统时深度学习方案确实是大趋势但对于学习阶段或中小型项目传统视觉方案的“可解释性”是无价的——每一步变换都能可视化查看出了问题马上知道是哪个环节造成的。先用传统方案跑通全流程再考虑用深度学习替换其中某一个薄弱环节这才是最稳的成长路径。2. 环境搭建与技术选型OpenCV版本和C标准千万别搞混2.1 开发环境我用的组合是Visual Studio 2019 OpenCV 4.5.5 C17标准。这里有个关键经验OpenCV 4.x系列对C11以上的支持很友好而且移除了很多老旧的API网上大多数教程基于OpenCV 3.x有些代码比如cv::findContours的参数在4.x下要改动所以你搜索代码时必须先确认版本。OpenCV的安装本身并不复杂麻烦的是环境变量配置。Windows下解压OpenCV后你需要做三件事把opencv\build\x64\vc15\bin加入系统PATH路径在VS的项目属性中配置包含目录include和库目录lib把链接器输入中的附加依赖项改成opencv_world455d.libDebug模式或opencv_world455.libRelease模式。这里的d后缀代表Debug库搞反了会报一堆解析错误。2.2 项目文件组织我建议把工程按功能拆成几个模块文件别把代码全堆积在一个main.cpp里。一个比较合理的组织方式main.cpp主流程控制负责调用各模块。PlateLocator.cpp/h车牌定位模块。CharSegmentor.cpp/h字符分割模块。CharRecognizer.cpp/h字符识别模块。utils.cpp/h公共工具函数如图像显示、直方图统计。这样组织的好处是后续换识别算法或调参时你只需要动对应模块完全不需要重写其他代码。模块化的思想听着简单但这个项目里真的能帮你省掉大量重复编译和调试时间。2.3 代码基础配置打开图像后第一件事就是检查图像是否为空这个习惯一定要养成。很多报错都源于路径写错或图像读取失败而第0帧输入就是空指针。cv::Mat src cv::imread(car.jpg); if (src.empty()) { std::cerr Failed to load image! std::endl; return -1; }后面所有的图像处理操作都是在src这幅原图上做各种变换。调试时我习惯加两个辅助窗口一个实时显示当前处理的中间结果一个显示最终输出。要想显示中文窗口名OpenCV老版本有些麻烦但4.x配合新版Windows一般没这个烦恼。调试完记得关掉这些窗口不然正式运行时会弹出很多图干扰操作。3. 车牌定位颜色是比轮廓更可靠的线索3.1 为什么先用颜色再找轮廓车牌定位的主流思路有两种一种是基于边缘检测找矩形轮廓另一种是基于车牌底色做颜色阈值分割。单独用边缘检测对复杂背景树木阴影、车灯、路面标线极度敏感很容易把各种矩形物体误判为车牌单独用颜色阈值又会因为光照变化导致漏检。好的做法是两者结合先做颜色识别再用轮廓特征筛选。国内蓝底白字车牌HSV颜色空间中蓝色的Hue色相范围大概在100到124之间Saturation饱和度通常在100以上。通过cv::inRange函数可以把这个范围内的像素点提取成二值掩码。伪代码如下cv::Mat hsv, mask; cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV); cv::inRange(hsv, cv::Scalar(100, 100, 60), cv::Scalar(124, 255, 255), mask);这里对V亮度下限设置到60是为了滤掉夜间图像中很暗的噪声点。同时如果场景中有大量蓝色元素如蓝色货车、蓝天可以加一个条件只保留面积在图像总面积某个比例范围内的连通域避免海选。3.2 候选区域的轮廓筛选拿到mask之后用cv::findContours提取轮廓然后用几何特征筛选车牌区域。车牌是标准的矩形宽高比中国车牌宽高比约为440:140即3.14左右。所以筛选条件设置为轮廓的外接矩形宽度大于某个阈值比如50像素。外接矩形的宽高比在2.5到4.5之间。轮廓面积在图像总面积的某个比例区间。这一步如果检测到多个候选区域就把它们全部保留后续交给字符分割模块逐一验证。不要急于用面积最大原则一刀切因为同一画面里可能出现多辆车或误检区。3.3 形态学操作让掩码“连成一片”原始的颜色掩码中车牌区域的字符部分白色和底色蓝色在掩码图像里会形成很多空洞轮廓可能因此断裂。此时需要做闭运算先膨胀再腐蚀把字符和车牌边缘连接成完整矩形。膨胀和腐蚀的内核大小需要根据图像尺寸调整一般可用cv::getStructuringElement(cv::MORPH_RECT, cv::Size(15, 5))这个尺寸能较好地横向连通字符区域。我把车牌定位的完整流程总结成这样一张表方便你对照检查步骤操作关键参数常见问题1BGR转HSV-光照影响大需动态调节阈值2蓝色掩码提取H:100-124, S:100-255, V:60-255蓝天误检、夜间漏检3闭运算连通核大小(15,5)核太大导致不同区域粘连4轮廓提取RETR_EXTERNAL CHAIN_APPROX_SIMPLE内层轮廓干扰5几何筛选宽高比2.5-4.5车灯、标语误检6透视校正四点变换矩阵倾斜过大时定位框不准4. 字符分割二值化阈值和投影法是关键4.1 灰度化与二值化阈值选择是有讲究的车牌定位完成后我们需要从原图中抠出车牌区域然后做灰度化和二值化。二值化这一步直接影响后面的字符分割质量。如果整幅图像光照均匀直接用大津法Otsu自适应阈值就行但车牌区域很多时候受光照不均影响比如半亮半暗固定阈值会切坏字符。此时我会用cv::adaptiveThreshold它对每块局部区域计算独立阈值抗光照变化能力更强。另外一个经验如果车牌是蓝底白字二值化时字符应该是白色、背景是黑色即直接取mask的反色但如果是黄底黑字或绿底白字二值化方向正好相反。基于颜色先判断车牌类型再做二值化方向选择能省很多事。4.2 垂直投影法分割字符字符分割最经典的方法是垂直投影法。其核心逻辑是将二值化后的车牌图像按列做像素统计形成一条投影曲线。字符之间的间隔位置投影值接近0或极小字符内部的投影值较高。从曲线中找出这些“低谷”就能把字符一个个切开。具体代码思路如下std::vectorint columnSum(cols, 0); for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (binary.atuchar(i, j) 255) { columnSum[j]; } } } // 寻找连续非零区间作为字符的左右边界注意一个问题车牌的上下边缘常有铆钉或边框在垂直投影时会造成干扰必须预先做一步“剔除上下边框”的操作——从图像首行开始向上扫描如果一行整体都是白色且厚度很小就认定是边框直接裁剪掉。同理处理最左侧和最右侧因为车牌边缘常常带有蓝色边框不剔除也会干扰分割。4.3 归一化尺寸分割出来的字符大小各不相同直接送进分类器会导致识别率飘忽不定。我一般会把所有字符图像统一缩放到20x40或28x56像素同时用cv::copyMakeBorder补边保持字符不拉伸变形。这一步在后续做模板匹配或SVM特征提取时至关重要——特征维度不一样分类器根本没法训练。分割阶段值得多说一句的是第二个字符与第三个字符之间有个小圆点这个小圆点很容易被误认为一个字符。解决方案有两种一是按宽高比过滤圆点的高宽比接近1且面积特别小二是利用车牌的字符位置先验——第2个字符宽3mm左右且间隔与标准字符不同可以用规则剔除。5. 字符识别模板匹配和SVM怎么选5.1 模板匹配简单但脆弱第一个识别方案是模板匹配它的原理极其简单把分割出的字符图像与预先保存的每个字符模板做像素级对比找到相似度最高的那个。OpenCV里用cv::matchTemplate可以算归一化相关系数相关性最高的模板对应的字符就是识别结果。这个方案的好处是实现快、不用训练适合做Demo。坏处也很明显只要字体微变、噪声稍多、光照异常相似度就会大幅滑坡。尤其是像“0”和“O”、“1”和“I”这种在不同省份字库中差异很大的字符匹配很容易翻车。模板匹配还有一个致命问题它对字符的归一化质量极其敏感凡是分割时比例不准确的图匹配结果基本不可信。5.2 SVM训练工业级方案的低配版比模板匹配更鲁棒也更有学习价值的方案是HOG特征 SVM分类器。HOG方向梯度直方图提取的是字符图像的梯度分布特征对光照变化和微小形变的容忍度比原始像素高得多。OpenCV自带SVM实现训练流程如下先准备每个字符的样本图像我用的数据来自网上公开的车牌字符数据集每类字符大约50到100张样本。对每张样本做预处理灰度化、缩放、归一化提取HOG特征cv::HOGDescriptor把特征向量和对应标签喂给cv::ml::SVM::train最后保存成XML模型文件。在实际识别时加载模型并调用predict得到每个字符的分类结果。这个方案的准确率在标准车牌上能到95%以上而且SVM推断速度极快单帧处理不到几个毫秒。基于个人经验它的最大门槛是数据收集和标注质量而不在算法本身。5.3 后处理纠错无论用哪种识别方案都应该加一个后处理纠错步骤。比如中国车牌的字符结构是固定的第一位是省份汉字第二位是发牌机关字母后面5位通常是字母和数字混合。利用这个先验知识可以给每个位置的字符集加上约束例如第一位只能是特定的汉字集合不会出现数字或普通字母。识别结果不在合理集合内时可以强制换用置信度第二高的选项。 我实际调试时遇到过这样一个案例一个“京”字被识别成了“示”因为取阈值时抠掉了下半部分结构。这个错误完全可以通过字符结构先验修正代价只是多写几行过滤逻辑。6. 完整运行流程与常见失败图像分析6.1 端到端代码骨架把前几节的模块串起来主流程的骨架比较简洁int main() { cv::Mat frame cv::imread(test.jpg); PlateLocator locator; CharSegmentor segmentor; CharRecognizer recognizer(svm_model.xml); std::vectorcv::Rect plates locator.locate(frame); for (auto rect : plates) { cv::Mat plateImg frame(rect); std::vectorcv::Mat chars segmentor.segment(plateImg); std::string plateNumber recognizer.recognize(chars); std::cout 识别结果: plateNumber std::endl; cv::rectangle(frame, rect, cv::Scalar(0, 255, 0), 2); } cv::imshow(Result, frame); cv::waitKey(0); return 0; }这个流程看起来顺理成章但实际跑起来你会发现大量图像让人崩溃车牌被遮阳板挡住一半、车辆倾斜超过45度、夜间灯光造成的反光、雨天雨滴干扰等等。我建议你准备一个“困难测试集”专门放这些极端情况然后逐个分析哪一步出了问题这个过程学到的比跑通100张正常图还有用。6.2 三类典型失败场景与优化策略为了让你避坑我把常见的失败场景和解决方案整理成一张表失败场景根因定位优化方向蓝车车身被误检为车牌颜色掩码只靠HSV阈值轮廓筛选过宽增加矩形度、面积、宽高比约束用字符密度进一步筛选倾斜车牌导致字符分割失败定位后直接裁剪未做透视校正对车牌四个角点做透视变换校正为正视图再分割夜间车灯直射造成反光蓝色区域泛白HSV阈值固定动态调整V通道阈值或先做直方图均衡化再转换颜色空间我在实际项目里最常遇到的Bug不是算法本身而是cv::findContours在不同OpenCV版本下的返回值差异。4.x版本中这个函数的第二个参数是std::vectorstd::vectorcv::Point不再返回cv::findContours的第一个返回值层次结构如果照抄老代码编译器直接报错。这类问题排查虽然不难但确实很消耗时间。6.3 调试工具与性能瓶颈的量化除了处理流程还有一个常被忽略的点——性能测试。我建议你在每一帧处理前用cv::getTickCount做计时把车牌定位、字符分割、字符识别三个阶段的耗时分别统计出来。标准720p图像在Release模式下我用上述方案实测的耗时大约是定位45ms、分割10ms、识别20ms总耗时约75ms基本能达到12到15帧的处理速度。如果帧率要求更高瓶颈通常在颜色转换和形态学操作上可以考虑缩小输入图像或换用GPU加速。7. 后续优化方向这个项目做完后如果你想继续往深走我建议按下面的顺序扩展加入多角度车牌检测用边缘检测加直线检测Hough变换寻找车牌边框的四条直线再做透视变换能解决一定程度的倾斜问题。引入深度学习检测器用YOLO或更轻量化的MobileNet-SSD替换颜色定位可以大幅提升复杂场景下的召回率。车牌类型扩展处理新能源绿牌、黄牌、黑牌港澳车本质上是扩展颜色阈值集合和字符集合。接入真实视频流从静态图片切换到视频后需要加入跟踪和帧间平滑逻辑否则识别结果会闪跳。单从技术参数来看这套系统处理标准蓝牌能达到95%的识别准确率处理速度在CPU上也能跑出12帧以上性能和精度都足以支撑一个小型演示项目。我在实际调试过程中最大的体会是传统图像处理方案的调试成本其实比预想中高每一个阈值参数都可能引起误检的连锁反应值得你耐心地用直方图和中间结果图多观察几次。最后再分享一个调试小技巧每次改参数之前把当前处理结果截图保存下来形成一个测试报告文件夹。这样当准确率突然下降时你可以快速回滚对比找出是哪一步参数改坏了而不用靠回忆。做个开源项目也好、纯粹练手也罢这套排查方法会一直跟着你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价