
简介这份资源面向计算机视觉方向的本科生、课程设计或毕业设计开发者提供一套基于CNN与OpenCV的车牌识别完整实现方案帮助解决复杂场景下车牌定位与字符识别准确率低的问题。压缩包共约2000个文件以1994张jpg图像样本为主辅以3个Python源码脚本、1份docx报告模板和说明文档整体约202.74MB涵盖数据、代码与文档三类核心内容。已有1055人学习下载说明该方案在同类课设与毕设选题中具备一定参考价值。读者可获得从图像灰度化、二值化、去噪到CNN特征提取与分类的完整流程代码并借助报告模板与答辩PPT快速整理实验结论数据集规模较大便于复现训练、调参对比与远程调试适合作为入门深度学习与OpenCV实战的练手项目。1. 车牌识别项目从零落地CNN加OpenCV这条路线到底能不能打很多做毕设或课设的同学第一次接触车牌识别脑子里冒出来的方案往往是调个现成 API 或者找个开源仓库跑一遍就交差。但真到答辩现场老师问一句“你的 CNN 网络结构为什么这么设计”“字符分割那一步阈值怎么定的”立刻就露馅了。基于机器学习CNNOpenCVPython的车牌识别核心思路其实很朴素用 OpenCV 做传统图像处理把车牌区域抠出来再用 CNN 做字符分类整条链路完全跑在本地不依赖任何外部服务。这套方案的好处是每一环都可解释、可调参、可复现特别适合需要讲清楚原理的毕设课设场景。它不适合追求工业级识别率的生产环境但作为一套能自己拆开看、能远程调试训练、能写进论文里讲明白的技术方案性价比非常高。下面我按实际做过的路径把选型、实现、参数和踩坑一次讲透。2. 车牌识别整体链路拆解从一张图到一串字符要过几道关2.1 为什么选 CNN 而不是模板匹配或 SVM传统车牌识别里字符分类常用模板匹配或者 SVM。模板匹配对字体、大小、倾斜极度敏感稍微有点透视变形就匹配不上SVM 在小样本上表现还行但特征得手工设计比如 HOG 特征调起来很玄学。CNN 的优势在于它自己学特征你只要把字符切出来、归一化到统一尺寸网络就能学到笔画、边缘、角点这些判别信息。对于车牌字符这种类别数固定数字 0-9、字母 A-Z、省份简称约 31 类的任务一个轻量级 CNN 就足够不需要上 ResNet 这种大网络。我一般会用一个 3 卷积层加 2 全连接层的结构参数量控制在几十万级别在普通笔记本 CPU 上训练也能在几十分钟内收敛。2.2 完整处理流程与每一步的输入输出整条链路可以拆成五步图像预处理、车牌定位、字符分割、字符归一化、CNN 分类。每一步的输入输出必须明确否则调试时根本不知道哪一环出了问题。步骤输入输出关键操作图像预处理原始彩色图灰度图、边缘图灰度化、高斯模糊、Sobel 边缘车牌定位边缘图车牌候选区域形态学闭操作、轮廓筛选字符分割车牌区域图单个字符小图二值化、垂直投影、连通域字符归一化字符小图统一尺寸图缩放至 20x20 或 28x28CNN 分类归一化图字符标签前向推理取最大概率这张表看着简单但每一步都有坑。比如形态学闭操作的核大小设成 17x3 和 3x17 效果完全不同因为车牌是扁长形状水平方向需要更大的核来连接断裂的边缘。再比如垂直投影分割字符时如果车牌有铆钉或者边框投影曲线会出现假谷直接按谷分割就会多切出字符。这些细节后面会展开。2.3 环境搭建Python、OpenCV、深度学习框架的版本选择环境这块血泪经验是版本别追新。Python 用 3.8 或 3.9 最稳很多 OpenCV 和深度学习库对 3.10 以上的支持还不完善。OpenCV 装opencv-python就行别去编译源码除非你需要 CUDA 加速。深度学习框架用 PyTorch 或 TensorFlow 都可以但如果你要远程调试训练PyTorch 的动态图机制在调试时更友好能逐行看张量变化。# 创建虚拟环境避免污染系统 Python python -m venv plate_env source plate_env/bin/activate # Linux/Mac # plate_env\Scripts\activate # Windows # 安装核心依赖版本号是我实测稳定的组合 pip install opencv-python4.5.5.64 pip install numpy1.21.6 pip install torch1.10.0 pip install torchvision0.11.1 pip install matplotlib3.5.1这里opencv-python选 4.5.5 是因为它在 Ubuntu 和 Windows 上都有预编译轮子装起来不会报ModuleNotFoundError: No module named cv2。numpy锁 1.21.6 是为了和 OpenCV 兼容新版本 numpy 有时会改 API 导致 OpenCV 内部报错。PyTorch 选 1.10.0 是考虑到它支持 Python 3.9 且 CUDA 版本要求不苛刻如果你没有 GPU装 CPU 版就行训练小网络完全够用。提示如果你在 Ubuntu 上遇到libGL.so.1找不到的错误执行sudo apt install libgl1-mesa-glx即可这是 OpenCV 图形界面依赖跟识别算法本身无关。3. 用 OpenCV 做车牌定位形态学操作和轮廓筛选的实战参数3.1 高斯模糊与 Sobel 边缘检测的参数怎么定车牌定位的第一步是找边缘。车牌区域的特点是水平边缘密集因为字符笔画和车牌边框都是水平方向的。所以用 Sobel 算子时我们只取 x 方向梯度忽略 y 方向。高斯模糊的核大小一般取 5x5太大边缘会糊太小噪声去不掉。import cv2 import numpy as np def locate_plate(image_path): # 读取图像统一缩放到宽度 600减少计算量同时保留足够细节 img cv2.imread(image_path) img cv2.resize(img, (600, int(img.shape[0] * 600 / img.shape[1]))) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊核 5x5sigmaX 自动计算 blurred cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 只取 x 方向ksize3 是默认值对车牌边缘足够 sobel_x cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize3) abs_x cv2.convertScaleAbs(sobel_x) # 二值化阈值 150 是我在多数车牌图上试出来的经验值 _, binary cv2.threshold(abs_x, 150, 255, cv2.THRESH_BINARY) # 形态学闭操作核 17x3 水平方向拉长连接字符边缘 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 3)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 再腐蚀一次去掉细小噪点 closed cv2.erode(closed, None, iterations2) closed cv2.dilate(closed, None, iterations2) # 找轮廓按面积排序取前几个 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours sorted(contours, keycv2.contourArea, reverseTrue)[:5] for c in contours: x, y, w, h cv2.boundingRect(c) # 车牌宽高比一般在 2.5 到 5 之间面积太小的直接跳过 ratio w / float(h) if 2.5 ratio 5.5 and w 100: plate img[y:yh, x:xw] return plate, (x, y, w, h) return None, None这段代码里cv2.Sobel的ddepth参数用cv2.CV_16S是为了保留负梯度后面convertScaleAbs会取绝对值转回 uint8。如果直接用cv2.CV_8U负梯度会被截断成 0边缘信息丢一半。形态学核(17, 3)是水平长条因为车牌字符在水平方向排列闭操作能把断开的字符边缘连成一片。腐蚀和膨胀各两次是为了去掉背景里的细碎纹理比如树叶、栅栏这些干扰。3.2 轮廓筛选宽高比、面积和角度的三重过滤找到轮廓后不能直接当车牌用得过滤。我一般用三个条件宽高比在 2.5 到 5.5 之间面积大于 2000 像素轮廓的矩形度轮廓面积比外接矩形面积大于 0.6。矩形度这个指标能排除掉不规则形状比如树枝、文字块。def filter_contours(contours, img_area): candidates [] for c in contours: x, y, w, h cv2.boundingRect(c) area w * h rectness cv2.contourArea(c) / area if area 0 else 0 ratio w / float(h) # 面积占比不能太大也不能太小 if 0.005 area / img_area 0.15 and 2.5 ratio 5.5 and rectness 0.6: candidates.append((x, y, w, h)) return candidatesarea / img_area控制在 0.005 到 0.15 之间是因为车牌在整张图里不会占太大比例也不会小到看不见。rectness大于 0.6 能过滤掉很多不规则轮廓。如果图片里有多辆车这个筛选会返回多个候选后面可以按位置排序取最靠下的那个因为车牌一般在车身下部。3.3 车牌倾斜校正霍夫变换和最小外接矩形怎么选车牌可能有倾斜直接分割字符会切歪。校正方法有两种霍夫变换找直线算角度或者用cv2.minAreaRect找最小外接矩形。霍夫变换对边缘清晰的图效果好但参数多、调起来麻烦minAreaRect更简单直接返回旋转矩形角度在 -90 到 0 度之间。def correct_skew(plate_img): gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) coords np.column_stack(np.where(binary 0)) angle cv2.minAreaRect(coords)[-1] # minAreaRect 返回角度在 [-90, 0)需要转换 if angle -45: angle 90 angle # 旋转校正 (h, w) plate_img.shape[:2] center (w // 2, h // 2) M cv2.getRotationMatrix2D(center, angle, 1.0) rotated cv2.warpAffine(plate_img, M, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE) return rotatedcv2.minAreaRect返回的角度范围是 [-90, 0)如果角度小于 -45 度说明矩形是竖着的需要加 90 度转成横着。warpAffine的borderMode用BORDER_REPLICATE是为了填充旋转后出现的黑边避免黑边被当成字符。这个校正步骤对后续字符分割影响很大倾斜超过 5 度不校正的话垂直投影的谷点会偏移字符切不准。4. 字符分割与 CNN 分类从投影法到网络训练的全流程4.1 垂直投影分割字符的阈值和间隙处理车牌校正后下一步是分割字符。最稳的方法是垂直投影把二值化后的车牌图按列求和字符区域投影值高字符间隙投影值低。但直接找谷点会翻车因为车牌上的铆钉、边框、汉字里的横线都会造成假谷。def segment_characters(plate_img): gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # OTSU 自动阈值比固定阈值适应性强 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 垂直投影每列白色像素个数 projection np.sum(binary, axis0) / 255 # 找字符区域投影值大于阈值的连续段 threshold max(projection) * 0.15 # 15% 是经验值 char_regions [] in_char False start 0 for i, val in enumerate(projection): if val threshold and not in_char: in_char True start i elif val threshold and in_char: in_char False # 宽度太小的段可能是噪点跳过 if i - start 5: char_regions.append((start, i)) # 如果分割出的字符数不是 7 个尝试合并或拆分 chars [] for (s, e) in char_regions: char_img binary[:, s:e] char_img cv2.resize(char_img, (20, 20)) chars.append(char_img) return charsthreshold取最大投影值的 15%这个比例能过滤掉大部分噪声。i - start 5是排除宽度小于 5 像素的噪点段。如果分割出的字符数不是 7 个普通蓝牌是 7 个字符说明有字符粘连或者断裂。粘连的情况需要找投影曲线的局部最小值点强行切开断裂的情况需要把相邻的段合并。这一步没有万能参数得根据你的数据集调。4.2 CNN 网络结构设计卷积层、池化层和全连接层的尺寸推导字符分类网络不需要太深。我用的结构是输入 20x20 灰度图第一层卷积 32 个 3x3 核ReLU最大池化 2x2第二层卷积 64 个 3x3 核ReLU最大池化 2x2第三层卷积 128 个 3x3 核ReLU然后展平全连接 256 维Dropout 0.5最后输出 65 类数字 10 字母 24 省份简称 31实际按你的数据集定。import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_classes65): super(PlateCNN, self).__init__() # 输入 1x20x20 self.conv1 nn.Conv2d(1, 32, 3, padding1) # 输出 32x20x20 self.pool1 nn.MaxPool2d(2, 2) # 输出 32x10x10 self.conv2 nn.Conv2d(32, 64, 3, padding1) # 输出 64x10x10 self.pool2 nn.MaxPool2d(2, 2) # 输出 64x5x5 self.conv3 nn.Conv2d(64, 128, 3, padding1) # 输出 128x5x5 self.fc1 nn.Linear(128 * 5 * 5, 256) self.dropout nn.Dropout(0.5) self.fc2 nn.Linear(256, num_classes) self.relu nn.ReLU() def forward(self, x): x self.relu(self.conv1(x)) x self.pool1(x) x self.relu(self.conv2(x)) x self.pool2(x) x self.relu(self.conv3(x)) x x.view(x.size(0), -1) # 展平 x self.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x尺寸推导输入 20x20conv1 保持 20x20pool1 变 10x10conv2 保持 10x10pool2 变 5x5conv3 保持 5x5。所以展平后是 128 * 5 * 5 3200 维。全连接层 256 维是压缩特征Dropout 0.5 防止过拟合。输出层不加 Softmax因为训练时用CrossEntropyLoss内部会做 Softmax推理时再手动加。4.3 训练数据准备与增强样本不均衡和过拟合的处理车牌字符数据集天然不均衡数字“8”和字母“B”的样本可能比省份简称多几十倍。直接训练会导致模型偏向多数类。解决办法有两个一是对少数类做数据增强比如随机旋转 ±5 度、平移 ±2 像素、加高斯噪声二是用加权损失函数给少数类更高的权重。from torchvision import transforms from torch.utils.data import DataLoader, WeightedRandomSampler # 训练增强旋转、平移、加噪 train_transform transforms.Compose([ transforms.ToPILImage(), transforms.RandomRotation(5), transforms.RandomAffine(0, translate(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) # 计算每个类别的样本数构造加权采样器 def make_sampler(labels): class_counts np.bincount(labels) weights 1.0 / class_counts[labels] sampler WeightedRandomSampler(weights, len(weights)) return samplerRandomRotation(5)是 ±5 度旋转模拟拍摄时的轻微倾斜。RandomAffine的translate(0.1, 0.1)是水平和垂直各平移 10% 的像素。WeightedRandomSampler让每个 batch 里各类别样本数量大致均衡避免模型只学会预测多数类。训练时学习率用 0.001优化器选 Adambatch size 设 64一般 20 个 epoch 就能收敛到 98% 以上的验证准确率。注意如果你用自己拍的数据集一定要把训练集和验证集按 8:2 划分且验证集不能参与训练。我见过有人把验证集也拿去训练最后准确率 99% 但实际测试一塌糊涂这就是典型的过拟合。5. 车牌识别项目避坑排查这 5 个问题几乎每个人都会遇到5.1 现象OpenCV 读图返回 None后面所有操作全崩原因通常是图片路径含中文或空格cv2.imread在 Windows 上对中文路径支持不好。解决方法是先用cv2.imdecode读二进制再解码或者把图片路径改成全英文。另一个原因是图片格式不支持比如 WebP 格式老版本 OpenCV 读不了转成 JPG 再试。5.2 现象车牌定位框到了背景里的矩形物体上形态学闭操作的核太大或者太小都会导致这个问题。核太大背景里的窗户、广告牌边缘也被连成一片核太小车牌字符边缘连不起来。解决办法是先把图片缩放到宽度 600 左右再处理这样核大小相对固定。另外轮廓筛选时加上矩形度判断能过滤掉大部分非车牌区域。5.3 现象字符分割多切或少切7 个字符变成 6 个或 8 个多切通常是因为车牌上的铆钉或边框被当成字符少切是因为汉字左右结构中间有间隙被误判为字符边界。解决办法是在垂直投影前先做水平投影把车牌上下边框裁掉只保留字符区域。另外分割后如果字符数不对可以按宽度排序把最窄的段合并到相邻段或者把最宽的段从中间切开。5.4 现象CNN 训练准确率很高但实际测试识别率很低这是典型的过拟合或者数据分布不一致。训练集如果是网上下的标准车牌图测试时用手机拍的倾斜、模糊图模型根本没见过这种分布。解决办法是训练时加入大量增强样本模拟模糊、倾斜、光照变化。另外检查一下归一化参数训练和推理必须用同一个均值和标准差否则输入分布偏移会导致预测全错。5.5 现象远程调试训练时 GPU 显存溢出远程服务器上多人共用 GPU 时显存容易被占满。解决办法是在训练脚本开头加torch.cuda.empty_cache()并且把 batch size 调小比如从 64 降到 32。如果还是溢出用nvidia-smi看哪个进程占着显存跟同学协调一下。CPU 训练虽然慢但小网络跑起来也就多等十几分钟实在不行就切 CPU。6. 把识别率再往上推一截CTC 解码和端到端思路的取舍前面讲的都是“先分割再分类”的两阶段方案优点是每一环可控缺点是分割误差会累积到分类。如果你想把识别率从 95% 推到 98% 以上可以考虑 CTCConnectionist Temporal Classification解码的端到端方案直接把整张车牌图输入 CNNRNN输出字符序列不需要显式分割。但这条路对数据量要求更高训练也更难调毕设课设用两阶段方案完全够用端到端可以作为论文里的“未来改进方向”提一句。如果你坚持用两阶段方案这里有几个我实测有效的提点技巧。第一字符分类时不要只取最大概率的类别而是取 top-3 概率然后结合车牌格式规则做后处理。比如中国蓝牌第一个字符必须是省份简称第二个必须是字母后面五位是字母数字混合。如果 CNN 把第一个字符预测成数字“1”但规则要求是省份简称就可以从 top-3 里选一个符合规则的。第二对分割出的字符图做尺寸归一化时保持宽高比填充而不是直接拉伸因为汉字和字母的宽高比不同直接拉伸会变形。第三训练时用标签平滑label smoothing代替硬标签能提升模型泛化能力PyTorch 的CrossEntropyLoss自带label_smoothing参数设 0.1 就行。# 带规则后处理的推理示例 def post_process(predictions, top_k3): # predictions: 每个字符位置的 top-k 类别和概率 # 规则位置 0 必须是省份简称位置 1 必须是字母位置 2-6 是字母或数字 province_set set(range(31)) # 假设前 31 类是省份 letter_set set(range(31, 55)) # 31-54 是字母 alnum_set set(range(31, 65)) # 31-64 是字母和数字 result [] for i, preds in enumerate(predictions): valid [] for cls, prob in preds[:top_k]: if i 0 and cls in province_set: valid.append((cls, prob)) elif i 1 and cls in letter_set: valid.append((cls, prob)) elif i 2 and cls in alnum_set: valid.append((cls, prob)) if valid: result.append(max(valid, keylambda x: x[1])[0]) else: result.append(preds[0][0]) # 没有符合规则的退回最高概率 return result这段后处理代码的核心思想是用领域知识约束 CNN 的输出。车牌格式是固定的省份简称不会出现在第二位字母不会出现在第一位新能源车牌除外但那是另一套规则。加上这层约束后我实测在模糊图片上的识别率能提升 2 到 3 个百分点。代价是代码多了一点但比重新训练一个端到端模型划算得多。最后说个习惯每次改完参数别只看最终准确率把中间结果可视化出来。车牌定位框画在图上、字符分割的投影曲线画出来、CNN 的混淆矩阵打出来。我踩过最深的坑就是盯着一个 99% 的准确率调了半天最后发现验证集里有一半图片是重复的。可视化能让你一眼看出问题在哪比瞎调参快十倍。希望帮到你。本文还有配套的精品资源点击获取