news 2026/9/11 3:23:56

C++与OpenCV实现车牌识别:从图像定位到CNN字符分类

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++与OpenCV实现车牌识别:从图像定位到CNN字符分类

简介:面向计算机视觉方向的高校毕业生和图像处理开发者,这份毕业设计资源以OpenCV与C++为基础,融入深度学习技术实现车牌识别全流程,覆盖车牌定位、字符分割与识别等核心模块。项目为个人原创毕业设计,评审得分95分以上,源码经过严格调试,可直接运行验证,可在此基础上进行二次开发。压缩包内包含完整项目源码与配套毕业设计论文,目录结构清晰,可帮助读者理解从图像预处理、特征提取到模型推断的完整工程实现,也为类似车牌识别或目标检测课题提供可复用的代码框架与写作参考。包体为ZIP格式,大小约6.61MB,轻量易下载,非常适合课程设计或课后自学。目前已有991人学习使用,适合需要快速落地毕业设计或入门OpenCV+C++深度学习应用的开发者参考借鉴。

1. 车牌识别毕设项目,一条C++与OpenCV串起的完整技术链路

毕业设计选车牌识别,等于把颜色空间转换、形态学运算、边缘检测、字符切分、卷积神经网络训练和C++工程部署一次性全过了一遍。表面上这是图像识别题目,拆开看实际是两套技术路线的拼接:OpenCV负责定位和字符分割,深度学习模型负责字符分类。用C++做载体而不是Python,除了性能更好之外,还在于工程链路更完整,从库依赖配置到模型文件加载,每一步都需要真正踩一遍,这正好是答辩和后续面试时能讲清楚的部分。下面按落地顺序展开:先搭好C++和OpenCV的环境,再做车牌定位与字符分割,接着训练一个轻量CNN模型,最后把模型接回C++识别循环,并给出论文实验设计和答辩演示可以直接用的方法。

2. 环境搭建与车牌定位,C++/OpenCV的核心工作流

2.1 版本与环境:选择OpenCV 4.x与Visual Studio的理由

写车牌识别项目时,我更推荐直接用OpenCV 4.5.2之后的版本。原因有两个:DNN模块在这一代已经能原生读取ONNX格式模型,训练好的网络可以直接在C++端跑前向推理,不需要额外引入TensorRT或ONNX Runtime;预编译包对新人友好,网上一搜opencv安装教程就有大量资料可以参考,省去自己编译的麻烦。如果对部署体积要求高,也可以选择源码编译,把不需要的模块裁掉,链接体积能缩小三分之一,但毕设阶段不建议在编译期给自己增加太多障碍。

Windows下最常见的配置组合是Visual Studio 2019/2022加OpenCV预编译包。流程是下载release包、配置环境变量、在项目属性里填入include和lib路径。Debug和Release模式下各要一份lib配置,且位数要和运行时dll一致。也有人用vscode配置c/c++环境,通过MinGW或MSVC插件控制编译参数,但调试体验不如VS顺手,断点查看图像中间结果还是VS方便。

这里有一个高频踩坑点:OpenCV调试版lib文件名带d后缀。Debug项目链接了release版lib,或者反过来,链接阶段就会报LNK2019,错误信息只指向几个无法解析的符号,很难往依赖不匹配上想。判断方法很简单,检查linker输入里Debug对应的是否是opencv_world460d.lib,Release对应opencv_world460.lib。

2.2 车牌定位:HSV颜色阈值加形态学闭运算的组合拳

定位阶段的目标是把车牌区域从背景里框出来。直接按BGR颜色特征筛,光照一变就全偏了;转HSV之后,色相H受光照影响小,配合饱和度和明度阈值能覆盖从白天到傍晚的大部分场景。车牌底色主要分蓝、黄、绿三类,毕设以蓝牌为主最省事。

下面这段代码完成蓝牌候选区域的初筛:

cv::Mat hsv, mask; cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV); cv::inRange(hsv, cv::Scalar(100, 80, 80), cv::Scalar(124, 255, 255), mask); cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(17, 7)); cv::morphologyEx(mask, mask, cv::MORPH_CLOSE, kernel);

cvtColor把BGR帧转换为HSV,inRange用低阈值和高阈值两个三元组卡出蓝色区域。注意OpenCV的H分量范围是0到180,蓝色分布在100到124这个区间。形态学处理用17×7的矩形结构元做闭运算,先膨胀再腐蚀,把字符间隙填起来,让原本断开的字母连成一个整体,下一步轮廓查找才能把整个车牌当作一个连通域处理。

紧接着用连通域加几何约束过滤候选:

std::vector<std::vector<cv::Point>> contours; cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); for (const auto& contour : contours) { double area = cv::contourArea(contour); if (area < 5000) continue; cv::Rect rect = cv::boundingRect(contour); double ratio = (double)rect.width / rect.height; if (ratio >= 2.0 && ratio <= 5.0) { cv::Mat plate = src(rect); // 到这里得到候选车牌区域,交给字符分割流程 } }

findContours输出的是点集合,boundingRect拿到轴对齐外接矩形。宽高比范围按车牌物理比例来设,普通蓝牌宽高比接近3.0到3.5,放宽到2.0到5.0是为了容纳新能源车牌和轻微透视形变。面积阈值5000假定输入图片尺寸在800×600以上,换更高分辨率的相机时这两个参数都要按比例调整。

调试阶段最常用的两个函数是cv::drawContourscv::fillPoly。前者把轮廓画回原图,肉眼确认每个候选框是否准确;后者用来生成掩码,比如手动遮掉某一区域再观察分割结果。把轮廓和矩形框画在原图上核对,比看log打印坐标直观得多。

2.3 Canny边缘二次确认与倾斜校正

颜色分割的弱点在白色车身和强反光场景下会被放大,大量非车牌蓝色区域会混进候选。所以实际项目中我在颜色初筛之外再跑一条Canny边缘检测,对两组结果取交集,能明显降低误检率。车牌边框、字符、铆钉都会形成密集边缘,车身蒙皮反而是大片平滑区,两者在边缘密度上差异显著。

cv::Mat gray, edges; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, gray, cv::Size(5, 5), 0); cv::Canny(gray, edges, 60, 180); cv::dilate(edges, edges, cv::Mat(), cv::Point(-1, -1), 3);

GaussianBlur用来去掉传感器噪声,Canny双阈值60和180控制边缘灵敏度。下限太低会把路面裂缝和阴影纹理都当成边缘,上限太高又容易把浅色字符滤掉。dilate膨胀3次是为了让断裂的边缘片段连起来,否则一个车牌轮廓会碎成几十段,后续根本筛不出来。

这里要单独说清楚cv::Rectcv::RotatedRect的区别。前者坐标轴对齐,适合粗筛;后者带旋转角,能输出四个顶点,适合做倾斜校正。车牌在画面中有明显旋转时,先用minAreaRect求最小外接矩形,拿到角度后反向旋转原图,再交给分割模块。C++里这个角度范围在-90度到0度之间,正负号方向和数学课本相反,新手极易写反,校正结果变成头朝下的车牌。

表格:定位阶段的主要参数

模块参数参考值调整提示
HSV颜色H下限/H上限100/124黄昏时把下限降到95
形态学结构元尺寸17×7约为字符宽度的1.2倍
轮廓筛选最小面积5000按输入分辨率线性缩放
Canny边缘双阈值60/180逆光时调低上限到40/140
倾斜校正有效角度范围-30°到30°超出范围视为无效候选

3. 字符分割与归一化,连接传统视觉和深度学习的那道桥

3.1 垂直投影法切分车牌字符

拿到校正后的车牌图像后,下一步是切成单个字符。最基本也最实用的是垂直投影法:逐列统计非零像素数量,字符列数值高,间隙列接近0,波谷就是分割点。

cv::Mat gray, binary; cv::cvtColor(plate, gray, cv::COLOR_BGR2GRAY); cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); cv::Mat column_sum; cv::reduce(binary, column_sum, 0, cv::REDUCE_SUM, CV_32S); std::vector<std::pair<int, int>> segments; bool in_segment = false; int seg_start = 0; for (int col = 0; col < column_sum.cols; ++col) { int sum = column_sum.at<int>(0, col); if (sum > 0 && !in_segment) { in_segment = true; seg_start = col; } else if (sum == 0 && in_segment) { in_segment = false; if (col - seg_start >= 8) { segments.emplace_back(seg_start, col); } } }

THRESH_OTSU根据直方图自动计算二值化阈值,比固定阈值抗光照变化。车牌字符本身是白色,背景偏深,所以用THRESH_BINARY_INV把字符像素置为255,再统计投影才有意义。reduce把二维矩阵折叠成一行,每列求和,得到的值直接反映该列有多少白色像素。

循环里的in_segment记录当前是否处在字符区间,sum为0时认为字符结束,宽度达到8像素才保留。这个宽度下限是过滤噪声的关键,铆钉、灰尘形成的短投影会被直接剔除。如果字符间隙里有小噪点,可以把下限提到10,代价是窄字符如数字1和字母I可能被误删,需要结合下一个条件判断。

这个方法的假设是字符互不相连且垂直对齐。边框没有完全清除时,白色边缘会和两端字符粘连,把“京”和边框识别成一个超宽区域。所以分割前必须先剥边框:对二值图做一次erode腐蚀掉最外圈像素,或者遍历第一行和最后一行把白色直接置黑。

3.2 字符归一化、字符表映射与数据增强

切出的字符宽高比例不一,数字1窄,汉字京宽,直接送进CNN会把尺寸信息弄乱。常见做法是先按比例缩放到统一尺寸,再贴到固定画布上:

cv::Mat pad = cv::Mat::zeros(32, 32, CV_8UC1); cv::resize(segment, segment, cv::Size(28, 28), 0, 0, cv::INTER_AREA); segment.copyTo(pad(cv::Rect(0, 0, 28, 28)));

不直接resize成32×32,是为了保留字符原始宽高比。硬拉伸会让“1”和“7”这类对形状敏感的字符产生误判,识别率掉得莫名其妙。INTER_AREA是缩小图像时抗锯齿效果最好的插值方式。

字符表映射在C++里一般用字符串数组初始化:

const char* kIndexToChar[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "京", "沪", "粤", "苏", "浙", "鲁", "川" };

注意字母表里去掉了I和O,因为这两个字符在视觉上和数字1、0几乎无法区分,这是车牌识别的通用做法。训练端和C++端必须共用同一份字符表,顺序一乱,模型输出全部错位。

数据增强方面,平移正负3像素、旋转正负5度、加高斯噪声是最常用也最难出错的三种方式。这三类增强模拟的是摄像头安装角度差异和夜间传感器噪点,幅度不要过大,过大会让字符本身变形。

表格:数据增强参数范围

增强方式参数范围模拟场景
平移±3像素摄像机安装抖动
旋转±5度车牌安装倾斜
缩放0.9到1.1倍车距变化
高斯噪声σ=0.01到0.03夜间低照度噪点

3.3 分割出问题的常见表现与容错策略

演示时最容易暴露的问题就是摄像头角度稍微一歪,字符分割全部乱套。有两个容错思路:第一,切出的字符段数不符合预期时自动调整参数重切;第二,某字符置信度太低时标记为待人工复核,不要硬输出结果。

段数判断直接看segments.size()。普通蓝牌是7个字符,新能源车牌8个。段数不够说明有字符粘连,把最小宽度阈值往上调,或者对二值图做一次腐蚀把连接处打断;段数多了说明背景噪声被当成了字符,调大最小宽度并增加面积过滤。

开发阶段每个中间步骤都应该通过cv::imwrite输出图像:定位框截图、二值图、字符切分图各存一份。光盯着最终准确率很难定位问题,把分割图逐张翻一遍,错误原因马上就能看出来。这个做法写进论文里可以当作中间过程可视化的素材,也能体现调试过程是完整的。

4. 基于CNN的车牌字符识别,从训练到C++推理

4.1 选轻量CNN做字符分类,不用目标检测的原因

字符已经从背景中抠出来了,剩下的任务就是分类:输入32×32灰度图,输出65个类别中的一个。这类任务用轻量CNN完全够用,准确率高,推理快。YOLO这类目标检测网络擅长定位和分类同时完成,这里替它做就显得笨重,训练难度高,推理还慢。轻量CNN用两层卷积加两个全连接层就能达到实用水平,参数量小,训练快,C++端部署没有任何额外负担。

算一笔开销:32×32灰度图,两层卷积各32和64个卷积核,单次前向推理在一颗i5处理器上大约2到3毫秒。视频连续识别时,耗时瓶颈通常不在模型推理,而在定位阶段的颜色转换和形态学操作上。

4.2 训练数据组织与标签映射

训练数据一般有两类来源:开源的中文车牌字符数据集,以及自己拍摄实景照片后用前面的分割流程自动切出的字符。两类混合效果最好。数量上每个类别不少于500张原始图,增强后不少于5000张,基本能训练出可用模型。

目录结构用分层式最省事:

dataset/ train/ 0/ 1/ A/ ... 京/ val/ 0/ 1/ ...

PyTorch的ImageFolder可以直接读这个结构,做推理时C++端维护同一个kIndexToChar顺序即可。训练集和验证集划分有个容易忽略的坑:不能用同一辆车的不同视频帧分别进训练和验证,模型会记住车牌整体而不是字符形状,验证分数虚高。正确做法是先按车辆分组再切分,保证验证集里的车辆完全没有出现在训练过程中。

4.3 用OpenCV DNN加载ONNX模型做前向推理

训练用PyTorch完成,训练好后导出为ONNX格式,C++端直接用OpenCV DNN模块读取,不需要引入PyTorch库和ONNX Runtime依赖,部署时只需OpenCV自带的dll。导出时固定好输入输出名,C++端调用才不会对不上。

cv::dnn::Net net = cv::dnn::readNetFromONNX("license_plate_cnn.onnx");

推理部分的实现:

cv::Mat blob = cv::dnn::blobFromImage(character_img, 1.0 / 255.0, cv::Size(32, 32), cv::Scalar(0), true); net.setInput(blob); cv::Mat output = net.forward(); double maxVal; cv::Point maxLoc; cv::minMaxLoc(output, nullptr, &maxVal, nullptr, &maxLoc); int classId = maxLoc.x;

blobFromImage最后一个参数true表示swapRB,因为训练样本通常是RGB格式,OpenCV读进来是BGR,通道顺序不一致会让模型识别率骤降。如果训练时直接用的BGR,这里传false。输出是1×65的矩阵,minMaxLoc在单行矩阵上直接找出最大值下标,maxLoc.x取到的就是类别索引。

表格:训练与推理参数对照

环节参数数值说明
训练输入尺寸32×32单通道灰度
训练初始学习率0.001Adam,30轮后衰减一次
训练batch size128视显存调整
训练类别数6531汉字加24字母加10数字
训练轮数50验证loss不降就提前停止
推理输入尺寸32×32必须与训练一致
推理swapRBtrue通道顺序保持一致

训练和推理看着是两头,但参数必须严格对齐:输入尺寸、通道顺序、字符表顺序,这三样任何一处不一致,最终整牌识别率都会明显下降。

5. 整牌输出与论文实验设计,从识别字符串到毕业设计答辩

5.1 识别结果串接与合法性校验

字符分类全部完成后,按横坐标排序,把类别名拼成字符串。串接之后必须加校验。车牌长度限定为7或8位,第二个字符必须是发牌机关字母而不是汉字,否则大概率是分割顺序错了。校验可以用正则表达式,也可以直接按省份简称和字母范围写判断逻辑。发现不合法就直接丢弃这一帧结果,不要硬输出错误车牌。后校验逻辑在论文里也是一处加分点,对应的是工程系统中的容错设计。

5.2 准确率统计口径与鲁棒性实验写法

写实验部分最容易犯的错误是把单字符准确率当作整牌准确率来写。单字符98%时,7个字符整牌全对的概率大约在87%左右,两者差一个数量级。实验表格要分两行呈现字符级准确率和整牌级准确率,并写明测试集总张数和场景来源。鲁棒性测试上,把测试集按光照、角度、遮挡三个维度各挑30张,分别统计识别率,比只给一个总准确率有说服力,也能说明算法在哪些条件下还存在短板。

5.3 答辩演示的环境准备

演示现场优先使用Release模式编译,把所有dll放在exe同目录,目标机器提前安装对应版本的Visual C++ Redistributable。程序里保留保存日志和逐帧输出中间结果的功能,实时摄像头效果不稳定时,直接切换本地图片目录批量识别,日志回放也能证明系统处理能力。不要在演示现场改代码,也不要依赖IDE启动环境。代码里给定位、分割、识别、后校验四个模块各留一个debug输出开关,后续改数据、补实验、写论文都会顺畅很多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 3:23:22

多Agent点对点通信:hermes peer协议解析与订单履约落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 3:23:19

YOLOv8小目标检测工程实践:安全锥识别与边缘部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 3:22:54

多Agent协作拓扑选型:四种模式对比与踩坑实践

1. 先说结论&#xff1a;多 Agent 不是团队越大越好 两三年前我第一次做多 Agent 项目&#xff0c;想法非常简单粗暴&#xff1a;任务复杂&#xff0c;那就多拆几个角色&#xff0c;角色不够再加人。最开始是 2 个&#xff0c;后来到 5 个&#xff0c;最高峰一次上线了 12 个 A…

作者头像 李华
网站建设 2026/9/11 3:21:57

Winform变身HTTP服务:C# 中通过 HttpListener 实现 Json 接口实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华