资讯动态

OpenCV+CNN车牌识别实战:从HSV定位到字符识别

发布时间:2026/10/4 1:34:45 来源:尧图企业网站定制
简介基于CNN与OpenCV的车牌识别项目完整资源面向计算机视觉方向的高校学生、毕业设计与课程设计场景。资源包含Python实现的车牌识别核心代码、1994张覆盖多种光照与场景的车辆/车牌样本图片、报告模板及答辩PPT等内容从OpenCV图像预处理、CNN特征提取到车牌号码输出均有配套工程支持远程调试训练适合快速构建可现场演示的毕设/课设系统。压缩包约202.74MB共2000个文件其中jpg图片为训练与测试数据集py文件包含模型训练和识别脚本docx为报告文档模板另含答辩PPT压缩包与说明文本目录结构清晰。已有1055人学习浏览资源热度可见。这套项目交付完整项目源码、数据集、报告框架与答辩材料结合CNN对光照、遮挡、畸变等干扰的鲁棒性可帮助理解传统OpenCV处理与深度学习融合的思路也可作为项目演示、论文撰写与后续算法优化的基础。1. 毕设季最常被问到的 CNNOpenCV 车牌识别究竟是哪一步省了你的事每年毕设开题和课设验收前后总有人拿着“车牌识别”来问——用 OpenCV 找车牌位置再用 CNN 认字符这个组合看着老却是几乎所有能顺利答辩的工科项目里最稳的一条线。它不追求端到端的花哨而是把“车牌在哪、字符是什么”拆成两个独立阶段每一段都能单独调、单独测、单独写进论文里不会出现“整个模型跑不通就全部推翻”的绝望局面。适合谁呢手里有摄像头或者手机拍的车牌图片想在两三周内跑出一个看得见、指标能写进结题报告的识别程序的人。无论你是正被毕设逼着的本科四年级还是课设只剩两周的大三学生这套方案都不需要昂贵的 GPU 训练环境CPU 就能把网络训完。2. 车牌识别的三段式链路定位、分割、识别各自的职责与选型理由一个完整车牌识别系统商用方案里可能直接上一体化检测模型但课程设计和毕业论文里最常见、也最好答辩的做法还是拆成三个环节先定位车牌区域再做字符分割最后做字符识别。这么拆的原因很实际前两段可以用成熟的 OpenCV 传统视觉方法完成到了最后一段才交给 CNN而 CNN 只需要做纯分类任务不用在整张图里找车牌网络规模可以压得很小CPU 上几分钟就能训练一轮。为什么不是端到端一把梭很多同学一开始会问“能不能直接 YOLO 检测车牌再识别”我的回答是能但代价很大。端到端方案要求你先标注几百张带车牌的完整图像训练一个检测头再叠加一个识别头任何一个环节数据集不够最后指标就很难看而且答辩时老师问“你这个框为什么定在这”解释起来是很痛苦的。三段式方案里每一段都是可解释的尤其适合毕设课设的评审场景。2.1 定位段为什么选 HSV 颜色空间而不是模板匹配或深度学习检测国内蓝底白字车牌占比最大所以最常见的定位做法就是按颜色找。直接基于 BGR 的 RGB 阈值在光照变化下极不稳定——同一块蓝色车牌在强光、阴影、夜间三种环境下的 RGB 数值能差出好几倍阈值根本没法写死。HSV 将色相、饱和度、明度三个分量拆开色相对光照强度不那么敏感因此更可靠。具体操作是先用cv2.cvtColor把 BGR 转 HSV再用cv2.inRange做蓝色掩膜。下面这组 HSV 阈值是我的常用起点注意 OpenCV 里 H 通道的范围是 0 到 180不是 0 到 360很多第一次做的人会在这里翻车通道最小值最大值说明H色相100124蓝色主体实测深蓝浅蓝都落在这个区间S饱和度90255低于 90 容易把灰色水泥墙和阴影带进来V明度80255太暗看不清车牌太亮则是强光过曝宁高勿低为什么不直接上 YOLO 检测车牌核心原因是数据成本。用 YOLO 需要手动标注车牌框标注一两百张图的时间足够把 HSV 方案调完并写进论文了。传统颜色定位在固定场景下准确率并不低做毕设绰绰有余。2.2 字符分割段水平投影和垂直投影的分工逻辑定位到车牌矩形之后切字符。常用的套路有两种一种是直接找轮廓按外接矩形逐个切另一种是投影法先做水平投影去掉上下边缘再做垂直投影找到每个字符的左右边界。我的经验是两种方法结合用而不是单选一种。直接findContours的问题在于车牌上的铆钉、边框、分隔符都是轮廓你很难写一个宽高条件把它们全部过滤干净。投影法更稳定逻辑也更好讲水平投影统计每一行的白色像素数白色像素多的行就是字符所在的高度范围这样能先把车牌上下边框切掉垂直投影统计每一列的白色像素数从第一列走到最后一列白色像素数量从无到有再到无就切出一个字符的区间。字符与字符之间的空隙是天然的“波谷”只要设定一个最小宽度和最大宽度就能把噪声块过滤掉。2.3 识别段CNN 在字符识别里到底学到了什么字符识别可选的方案很多模板匹配、SVM、CNN。模板匹配对字体变体几乎无能为力同一个“京”字在不同车牌上的笔画粗细、倾斜角度、曝光程度都不一样模板一换就认错SVM 需要你手动设计 HOG 之类的特征调特征又是个玄学过程。CNN 的好处是自动学特征你只需把字符图缩放到统一尺寸喂进去网络自己会学边缘、笔画、拐角这些低层特征再组合成字符类别。这里要特别强调一个认知在这个项目里 CNN 不是万能的检测器它只是一个分类器输入是已经切好的字符块输出是字符类别。常见做法是切出来的字符统一缩放到 32×32 或 32×64 的灰度图送入三层卷积加全连接的小网络。这个网络非常轻训练快的另一个原因是字符类别虽多汉字省份约 30 多个字母数字约 34 个但每类的特征差异其实非常明显——不是让你区分相似的人脸区分难度低一个量级。3. 从安装到定位切分OpenCVPython 的最小实现与参数说明这一章直接给能跑的代码。环境上我只依赖 OpenCV、NumPy识别部分再加 PyTorch避免一次装太多库把环境搞坏。3.1 环境安装版本匹配和 import 失败的常见原因Python 版本建议用 3.8 或 3.10这两个版本对 OpenCV 和 PyTorch 的兼容性都很好。不要用最新的 Python 3.12 去装老版本 OpenCV编译依赖经常会出问题。安装命令如下# 创建独立虚拟环境避免和系统 Python 打架 python -m venv .venv # Windows 激活 .venv\Scripts\activate # Linux/macOS 激活 source .venv/bin/activate # 安装核心依赖版本号固定是为了可复现 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install matplotlib # CPU 版 PyTorch足够训练本项目的字符识别网络 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu安装完先做一次自检在 Python 里执行import cv2如果报ModulenotFoundError: No module named cv2大概率是 pip 装的包名写错了。opencv-python 和 opencv-contrib-python 不要同时装两个包会互相覆盖同一份 cv2 扩展文件导致 import 时出现类似cv2.error: OpenCV(4.4.0)的诡异报错。我一般只装 opencv-python它的功能对车牌识别完全够用。3.2 车牌定位代码HSV 掩膜加轮廓筛选import cv2 import numpy as np def locate_plate(image): # 缩小图像减少计算量但 fx/fy 不宜小于 0.5否则车牌字会糊 img cv2.resize(image, None, fx0.8, fy0.8, interpolationcv2.INTER_CUBIC) # 高斯模糊去噪核必须是正奇数(5,5) 是性能与去噪的折中 blur cv2.GaussianBlur(img, (5, 5), 0) # 转 HSV注意 OpenCV 读入的是 BGR 通道顺序 hsv cv2.cvtColor(blur, cv2.COLOR_BGR2HSV) # 蓝色掩膜阈值来自 2.1 表格可按实际图片微调 mask cv2.inRange(hsv, (100, 90, 80), (124, 255, 255)) # 闭运算把断裂的笔画连成整块kernel 太大会把相邻车连着 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 找外轮廓RETR_EXTERNAL 只取最外层避免轮廓嵌套 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h cv2.boundingRect(cnt) # 蓝牌宽高比约 3:1 到 4.4:1太小是噪声太大是整片车身 if 2.5 w / h 4.5 and w 80 and h 20: return img[y:y h, x:x w] return None这里说明几个关键参数。宽高比取 2.5 到 4.5标准蓝牌是 440mm×140mm比例约 3.14加上拍摄角度轻微透视适当放宽到 4.5 能多召回。w 80和h 20这两个下限不是拍脑袋定的是按缩放后车牌的最小像素尺寸估算的如果你的测试图是大图这两个值要跟着放大。MORPH_CLOSE的核用 7×7太小断缝连不上太大容易把车牌和旁边的深色车衣连成一片这个在调试时看掩膜图最直观。3.3 字符分割代码水平投影加垂直投影def split_chars(plate_bgr): # 转灰度后再自适应二值化THRESH_BINARY_INV 让字符为白色 gray cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2) # 水平投影找出主要字符行的上下界去掉车牌框和铆钉 row_sum np.sum(binary 255, axis1) rows np.where(row_sum 10)[0] if len(rows) 0: return binary, [] top, bottom rows.min(), rows.max() binary binary[max(0, top - 2):bottom 3, :] # 垂直投影逐列统计白色像素波峰是字符波谷是间隙 col_sum np.sum(binary 255, axis0) boxes [] in_char False start 0 for i, v in enumerate(col_sum): if v 8 and not in_char: in_char True start i elif v 8 and in_char: in_char False w i - start # 字符宽度过滤太窄是噪点太宽说明两个字符粘连 if 8 w 70: boxes.append((start, i)) # 二次过滤和切分过宽的块按中点拆两半 final_boxes [] for x1, x2 in boxes: if x2 - x1 70: mid (x1 x2) // 2 final_boxes.append((x1, mid)) final_boxes.append((mid, x2)) else: final_boxes.append((x1, x2)) return binary, final_boxes这段代码里v 8的判定很关键垂直投影中车牌的铆钉区域在二值化后也会产生白点但数量通常不会超过 8 个这个阈值能把铆钉过滤掉。如果二值化后噪声特别重可以把 8 提高到 15代价是字符边缘比较细的时候可能被误断。8 w 70的宽度范围是按字符在整块车牌中约占 1/7 到 1/10 估算的分隔符“·”宽度更小会被自动丢弃。拆过宽字符时用中点拆分是最粗暴的做法更精细的做法是在中点附近找垂直投影的局部最小值再切字符有粘连时精度更高。4. 训练 CNN 字符识别模型数据集组织、网络结构与训练参数定位和分割解决之后剩下的核心问题就是字符识别。前面已经说明为什么选 CNN这一章直接讲怎么搭建一个能用于毕设的轻量网络以及训练时那些容易被忽视的参数。4.1 数据集怎么组织字符级目录命名和划分原则训练集不是整张车牌图而是切出来的单字符图。目录结构我习惯这样组织data/char_train/ ├── 京/ # 汉字省份 │ ├── 0001.png │ └── 0002.png ├── A/ # 字母 ├── B/ ├── 0/ # 数字 └── 1/每个字符一个文件夹文件夹名就是标签。PyTorch 的torchvision.datasets.ImageFolder会自动按目录名生成类别索引写训练代码时省掉自己读文件列表的麻烦。划分训练集和验证集时建议按“车辆”划分而不是按“文件”随机划分——同一个车牌的多张图不要同时出现在训练集和验证集里否则验证集指标会虚高这是我在实际项目里踩过的最深的坑之一。数据量方面每个字符类别至少收集 100 张比较稳。汉字省份字符是最难收集的常见做法是自己写脚本对已有图片做平移、旋转、缩放增强把 20 张扩展到 100 张。增强不能太过旋转超过 10 度就已经不像真实车牌字符了。4.2 网络结构三层卷积加全连接为什么不用 VGG 或 ResNet车牌字符识别不需要太深的网络因为字符类别内部的结构差异远小于 ImageNet 那种复杂图像。VGG 和 ResNet 在这个任务上不仅训练更慢而且在小数据集上更容易过拟合——你只有几百张训练图强行堆 16 层卷积的结果就是验证集损失不降反升。下面这个网络是我在多个项目里反复用过的版本参数量小CPU 上训练一个 epoch 只要几十秒import torch.nn as nn class PlateCharNet(nn.Module): def __init__(self, num_classes): super().__init__() # 三层卷积通道数按 32-64-128 递增 self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d((2, 2)), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 2 * 2, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x))两个细节解释一下。第一没有堆 BatchNorm因为训练 batch 很小时 BN 的统计量不稳定反而导致验证集精度波动。如果 batch 能开到 128 以上加 BN 没问题但课程设计常见的单机 CPU 训练场景不加更省心。第二全连接前用了AdaptiveAvgPool2d((2, 2))而不是MaxPool2d这样全连接层的输入尺寸固定输入图稍微不是 32×32 也能跑写代码时少一个维度换算的烦恼。4.3 训练参数学习率、batch、epoch 和数据增强的搭配import torch from torch.utils.data import DataLoader from torchvision import transforms, datasets transform transforms.Compose([ transforms.Resize((32, 32)), transforms.Grayscale(), transforms.RandomRotation(5), transforms.RandomAffine(0, translate(0.05, 0.05)), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) dataset datasets.ImageFolder(data/char_train, transformtransform) loader DataLoader(dataset, batch_size64, shuffleTrue, num_workers0) model PlateCharNet(num_classeslen(dataset.classes)) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size8, gamma0.5) criterion torch.nn.CrossEntropyLoss() for epoch in range(30): for images, labels in loader: out model(images) loss criterion(out, labels) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() print(fepoch {epoch 1}, loss {loss.item():.4f})为什么是这些参数而不是默认值lr1e-3是 Adam 在小型分类任务上的安全起始点调大容易前期震荡调小则收敛太慢。StepLR每 8 个 epoch 把学习率减半作用是让网络在后期用小步长把权重磨细而不是在大步长下反复横跳。batch_size64是 CPU 训练时的甜点区间——再大内存拷贝开销明显再小梯度噪声太大。RandomRotation(5)只转 5 度模拟摄像头安装角度带来的轻微倾斜超过 10 度的旋转会让网络学到错误的笔画形状。RandomAffine的平移量 0.05 模拟字符在车牌上的小幅度偏移。训练完成后保存模型权重只需要一行torch.save(model.state_dict(), plate_char.pth)。推理时同样加载这个文件不要整个模型做序列化保存换 Python 版本或换机器时pickle老模型经常崩只保存state_dict是最稳妥的。5. 避坑记录从 OpenCV 报错到识别率上不去我踩过的五个坑这个项目的坑集中在环境、定位、分割、训练和部署五个方面。每一条都是实际运行中会被绊住的问题按“现象到解决”写清楚。5.1 现象import cv2 报错或者打开摄像头时出现 cv2.error: OpenCV(4.4.0) 路径下的异常典型场景是pip install opencv-python成功后import cv2却抛出ModuleNotFoundError: No module named cv2更隐蔽的是能 import 但调用cv2.VideoCapture时出现 dll 加载失败。原因通常是两个一是opencv-python和opencv-contrib-python混装两个包都往 site-packages 里写了一份 cv2 目录版本互相覆盖二是 conda 和 pip 混合管理依赖同一个环境里有两套 OpenCV。解决方法是把两个包都卸载干净然后只装opencv-python如果是 conda 环境建议直接在 conda 环境里用pip install装 PyTorch不要 conda 和 pip 交叉混用。卸载命令是pip uninstall opencv-python opencv-contrib-python装好之后用python -c import cv2; print(cv2.__version__)验证。5.2 现象定位时把整片蓝色车身或蓝色天空框进来宽高比看起来也像车牌原因很好排查HSV 阈值里的 S 饱和度过低或者 V 明度范围太宽导致车身漆面、天空这种淡蓝区域也能通过掩膜。另一个隐蔽原因是闭运算核太大把车牌和旁边的蓝色障碍物连成了一个连通域。解决方法是先把 mask 图用imshow显示出来人工判断哪些区域是误检然后收窄 S 下限从 90 提到 120再叠加一个轮廓内部白色像素占比的判据——真实车牌内部会有大量白色字符占比通常超过 3%而车身漆面是纯色没有字符。代码上就是在boundingRect后加一句white_ratio np.mean(gray[y:yh, x:xw] 200)小于 0.03 的直接丢弃。5.3 现象字符分割切出来的块里带着铆钉、左右边框或“·”分隔符这个坑几乎人人都会踩。原因在于垂直投影只按列统计白色像素铆钉在投影图上也会形成一个小峰分隔符“·”则是一个比字符窄得多的峰。解决方法是双保险切之前先用水平投影把车牌上下边缘各多裁掉 2 到 3 个像素我在 3.3 代码里留了余量因为铆钉通常紧贴上下边缘切之后按矩形框的宽高比和面积过滤字符的宽高比大概在 0.4 到 0.8 之间面积不小于整个车牌面积除以 10小于这个阈值的块直接丢弃。如果你在调试时发现某个字符被切成了两半多半是二值化阈值太大把笔画中间掏空了把adaptiveThreshold的blockSize从 11 调到 15 往往能改善。5.4 现象CNN 训练时 loss 收敛到很低但实际测试识别率忽高忽低这是典型的“训练集和验证集分布不一致”。我见过很多同学把同一个车牌的 20 张图随机分配到训练集和验证集结果验证集里全是训练集的轻微变体指标虚高到 99%换一张真实拍摄的新图立刻跌到 70%。解决方法是按“车辆”分组划分数据一辆车的所有字符图要么全在训练集要么全在验证集不要混着放。另外汉字省份字符类别少且样本少福建“闽”、山西“晋”这类生僻字容易在增强后过拟合建议对汉字类别单独做更强的增强比如额外的RandomErasing。5.5 现象想远程调试训练但 SSH 端口映射总是连不上毕设课设里常见场景是一台笔记本带不动训练要用实验室或宿舍的台式机训练自己在寝室远程连。常见做法是训练机上启动 Jupyter Notebook 只监听本机地址通过 SSH 本地端口映射把服务转发出来命令如下# 训练机上启动 Jupyter注意 --no-browser 和指定端口 jupyter notebook --no-browser --port8888 # 本地电脑上建立 SSH 隧道将远端 8888 映射到本机 8888 ssh -L 8888:localhost:8888 your_useryour_machine_ip连不上的原因通常是三处训练机的防火墙没放行 22 端口路由器做了 NAT 但没有做端口转发还有最常见的是 SSH 服务没装。Linux 下执行sudo apt install openssh-server再systemctl enable ssh就能解决。需要注意把 SSH 服务暴露到公网有被暴力扫描的风险不要使用默认的 22 端口改为高位端口并设置强密码或使用密钥登录。局域网内调试不建议开端口转发直接连局域网 IP 更安全。6. 从能跑到能答辩验证指标怎么算、中间结果怎么留、演示怎么收尾先别急着把整套流程串起来单独验证每个阶段的指标比直接跑端到端有用得多。我一般会准备一个 50 张左右的车牌图片文件夹里面图片全部是不同场景下车尾或车头照片然后用三个数字评估车牌定位成功率定位框是否包含完整车牌、字符分割成功率切出的 7 个字符是否齐全、字符识别准确率每个字符是否都认对。端到端准确率是三个数字的乘积而不是最后一条命令输出的那个整串结果——这条经验帮我避免过无数次“模型明明识别率挺高但整个车牌却从头错到尾”的困惑。答辩演示时最实用的一个技巧是把中间结果用 matplotlib 拼成一张大图从原始图、HSV 掩膜、定位框、切分字符到最终识别结果按顺序排列保存到result/目录。老师问“你怎么做的”你直接把这六格图放出来比念 PPT 讲十页文字都清楚。评价指标也建议算混淆矩阵把易混的字符对找出来比如“0”和“O”、“8”和“B”这类能写进结论部分作为后续优化方向。最后说一个我个人的教训第一次做这个项目时我把所有图片放进训练集验证准确率 99%到了现场验收才发现摄像头实时画面识别率惨不忍睹。后来学乖了训练时固定留出 10% 的车牌数据做验证且验证集完全独立于训练集。这个习惯后来在所有深度学习项目里都救过我。如果你正在走这条路这套方案是值得投入的——它不炫技但每一步都能讲明白、查得到、改得动这恰恰是毕设课设最需要的。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑