
简介基于深度学习的高性能中文车牌识别毕设/课设项目包面向计算机类毕业生与课程设计学生提供一套从模型训练到系统部署的完整参考实现。项目综合运用Python、C覆盖图像预处理、CNN特征提取、字符识别、后处理等关键环节并涉及OCR、CTC损失函数与NMS优化适合动手实践深度学习落地流程。资源共179个文件以py、cpp、h源码为主包含MNN模型文件、Android工程、Gradle构建脚本、演示图片等压缩包整体约32.91MB。目前已有123人学习浏览。包内提供可直接安装的APK、SDK核心源码如hyper_lpr_sdk.cc、inference_helper_mnn.cpp以及识别引擎、检测架构实现同时包含数据集、训练/评估脚本和Dockerfile辅助环境配置目录结构清晰便于按模块阅读与二次开发可作为毕业设计选题论证、系统设计及功能实现的直接参考。1. 中文车牌识别毕设 zip 里最不该省的那几步如果你是在做毕设或课程作业拿到一份“基于深度学习高性能中文车牌识别”的项目压缩包第一反应多半是赶紧把训练脚本跑起来看损失曲线。但我见过太多人卡在这儿环境配好、训练跑完、测试准确率也有 95%一放到门口的实拍照片上就翻车。问题不在深度学习模型本身而在数据分布、字符解码和前后处理这几件看起来不起眼的事。这篇笔记就围绕中文车牌识别整条落地链路展开从模型选型、环境搭建、训练调参到推理部署讲清楚每一步怎么做、参数怎么设、坑在哪适合正拿这个方向做毕设或课程设计的同学直接照着复现。2. 原理先行汉字、绿牌和模糊帧为什么中文车牌更容易翻车2.1 车牌结构与识别难点不止是 7 个字符中国普通蓝牌是单行 7 个字符第 1 位是省份汉字第 2 位是发牌机关字母后 5 位是大写字母和数字混合。新能源绿牌则是 8 位第 2 位直接跟 D 或 FD 代表纯电、F 代表混动长度和蓝牌差一位。如果代码硬按“1 汉字 1 字母 5 位数字”解码绿牌几乎全错。除此之外还有黄底黑字的大型车车牌、白底黑字的警车车牌检测模型在训练时如果没见过这些颜色推理时也会漏检。字符集的复杂性是中文车牌区别于国外车牌的核心。省份汉字有三十多个而且字形相近的组合特别多比如“湘”和“鄂”、“渝”和“沪”、“赣”和“豫”在低分辨率和强反光下人眼都容易看错。字母侧也有坑官方车牌字符集刻意剔除了 I 和 O但不少公开数据集里还残留着这两个字符模型会把数字 1 误判成 I、把 0 误判成 O。数据分布偏差则是另一道坎公开数据集 CCPD 全部来自安徽合肥同一批摄像头样本基本都是“皖 A”开头拿它训练出的模型去识别外地车牌省份汉字这一项会出现明显下滑。所以真正的高性能方案必须在数据层补足省份和车牌类型的多样性。早期传统方案依赖边缘检测定位车牌、按固定宽度做字符分割、再用模板库匹配。这套方法在停车场出入口那种正视角、均匀光照的场景下还能用一旦遇到模糊、倾斜或夜间反光分割位置偏一点后面全盘皆错。深度学习方案不需要显式分割字符直接学习“从图像到字符串”的映射鲁棒性高出一个量级这也是这个方向在毕设和课程作业里经久不衰的原因。2.2 检测 识别两级架构为什么是主流基于深度学习的车牌识别行业落地的主流是“检测 识别”两级架构。第一步用目标检测模型YOLO、SSD 这类在整张图中框出车牌位置第二步把裁剪出来的车牌区域缩放到固定宽高用序列识别模型输出车牌字符串。两个模型各管一件事检测模型只回答“车牌框在哪”识别模型只回答“框里的字是什么”。两级架构最大的好处是解耦。两者可以分别准备训练数据、单独调参推理时还能在中间插入预处理步骤用透视变换把倾斜车牌转正再送入识别模型。这一步对识别准确率的影响经常比换更大的模型还明显。检索车牌识别方向论文时能看到不少端到端方案输入整张图直接输出车牌字符串公开数据集上指标很好看但复现成本高且对检测框质量极度敏感——一旦车辆密集或被遮挡整串输出就崩。毕设算力和时间都有限我更推荐两级架构每一步都能单独写进报告、单独画流程图答辩时也好讲。也有人会问为什么不直接拿通用 OCR 来识别车牌通用 OCR 对印刷体效果好但车牌是特殊字体加反光材质加小尺寸直接上 PaddleOCR 这类通用模型准确率通常到不了 90%。车牌识别必须定制字符字典和图像预处理专用模型才是正解。另一个常踩的误区是想自创网络结构来当“创新点”。车牌识别方向已经非常成熟自己设计的轻量网络几乎打不过 LPRNet 这类经过大量数据验证的结构。我一般建议把创新点放在数据增强策略、后处理纠错或部署优化上网络结构保持经典交付更快答辩也不容易被问倒。2.3 高性能的瓶颈速度与精度从哪抠“高性能”在这类项目里通常拆成两个指标识别准确率和推理耗时两者经常需要权衡。准确率侧字符识别的主流做法是 LPRNet 类轻量 CNN 加 CTC 损失。LPRNet 不依赖字符预分割用卷积把图像特征映射成序列概率CTCLoss 负责对齐不定长的字符序列对模糊、倾斜、带噪声的车牌鲁棒性比“先切割再逐个分类”好得多。速度侧真正的瓶颈往往不在模型计算量而在数据通道和前后处理。检测模型输入分辨率从 640 降到 320计算量直接降到原来的约四分之一视频流里每 2 到 3 帧只做一次检测、中间帧复用上一帧的框整条链路立刻接近实时。把模型导出成 TensorRT FP16 或 OpenVINO 又能再省一截时间。这些优化不影响原代码逻辑又是答辩时能讲清楚的高性能交付点我通常建议先做这三件事再去折腾模型结构。3. 把 zip 变成能跑的识别系统环境、数据与最小命令3.1 环境配置先让深度学习框架跑通这个方向的代码绝大多数是 Python 写的依赖集中在 PyTorch 和 ultralyticsYOLO 的训练库。配置顺序建议是先确认 NVIDIA 驱动再创建虚拟环境最后用 pip 装依赖。不要直接装在系统 Python 里否则后面换版本非常痛苦。conda 创建环境时 Python 版本选 3.10 最稳3.11 上有些老项目依赖没有预编译 wheel会临时编译浪费半小时。# 创建虚拟环境Python 3.10 兼容性最好 conda create -n plate python3.10 -y conda activate plate # 安装 PyTorchcu118 对应 CUDA 11.8cu121 对应 CUDA 12.1 # 先去 NVIDIA 官网确认驱动支持哪个 CUDA再选对应版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 --trusted-host download.pytorch.org # 安装 YOLO 训练库和图像处理依赖 pip install ultralytics opencv-python numpy pandas tqdm装完立刻验证 CUDA 是否可用命令是python -c import torch; print(torch.cuda.is_available())。如果输出 False说明驱动、CUDA 版本和 PyTorch 编译版本不匹配而不是代码有问题。我遇到过最浪费时间的情况是一个人调了两天模型效果最后发现 torch 装成了 CPU 版。nvidia-smi能显示驱动版本python -c import torch;print(torch.__version__)能看 torch 编译的 CUDA 版本两个一对比就知道问题出在哪。3.2 数据准备公开数据集怎么组织成 YOLO 格式毕设里最常用的公开数据集是 CCPD包含约 25 万张自然场景车牌图标注信息直接写在文件名里解析起来比读 json 还方便。但 CCPD 有个明显缺陷全部拍摄于安徽合肥车牌以“皖 A”为主、场景单一全是停车场出入口。如果项目要求识别全国车牌或应对复杂街道场景就要额外补充自采数据或合成数据。数据集目录建议按下面这样组织训练脚本、验证脚本、配置都基于这个结构。plate_data/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── plate.yamlYOLO 训练要求图片和标签文件名相同、后缀不同。把 CCPD 文件名里的坐标解析成 YOLO 格式 txt 的脚本如下# 解析 CCPD 文件名中的标注生成 YOLO 格式标签 import os def ccpd_filename_to_label(filename): # 示例: 2016-03-30-11-24-48.jpg?ccpd_bb268,194,317,232... parts filename.split() bbox_str parts[0].replace(ccpd_bb, ) x1, y1, x2, y2 map(int, bbox_str.split(,)) img_w, img_h 720, 1160 # CCPD 统一尺寸 x_center ((x1 x2) / 2) / img_w y_center ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h return f0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n # 按目录批量生成 labels for img in os.listdir(ccpd/images): if img.endswith(.jpg): label ccpd_filename_to_label(img) with open(fccpd/labels/{img.replace(.jpg, .txt)}, w) as f: f.write(label)脚本逻辑是先提取文件名里ccpd_bb后的坐标再做归一化。CCPD 图片尺寸固定 720×1160所以可以直接写死换成其他数据集时必须用cv2.imread读实际宽高。YOLO 格式要求坐标是相对值所以每一项都除了宽高。类别 id 默认是0表示单类车牌如果要把蓝牌和绿牌当两个类别这里要分别写0和1训练配置里的类别数也要同步改。划分训练集和验证集时有个典型错误随机打乱。同一辆车在 CCPD 连续多帧出现随机划分会让相同车辆同时出现在训练集和验证集评估结果虚高。应该按文件名排序后取前 80% 做训练、后 20% 做验证模拟真实泛化场景。3.3 训练检测模型最小命令与三个关键参数检测模型不需要从零预训练直接用 YOLOv8s 的公开权重做迁移学习参数量小、训练快适合毕设的算力环境。先写一个数据配置# plate.yaml定义训练集、验证集路径和类别 train: ./plate_data/images/train val: ./plate_data/images/val nc: 1 names: [license_plate]训练命令一行就能跑起来yolo detect train dataplate.yaml modelyolov8s.pt epochs100 imgsz640 batch16 patience10 workers8这里面三个必调的参数是imgsz、batch和patience。imgsz是输入分辨率640 精度好显存紧张或追求速度就降到 320batch受显卡显存限制8G 显存建议 16 以内patience是早停轮数10 表示连续 10 个 epoch 验证集没提升就自动停。workers8是为了让 CPU 多线程做数据加载避免 GPU 空等。训练过程中关注两个信号验证集 mAP50 是否还在上升训练 loss 和验证 loss 的差距是否过大。验证集 mAP50 到 95% 以上就够用了再往上对最终字符识别准确率帮助不大因为识别阶段有自己的纠错能力。ultralytics 默认开启 Mosaic 数据增强在 CCPD 这种背景高度一致的数据集上Mosaic 容易让模型学到背景套路建议在超参配置里把mosaic调低或关掉。3.4 识别模型训练LPRNet 的输入和字典检测模型负责找“车牌在哪”识别模型负责把“车牌区域”变成字符串。LPRNet 是中文车牌识别里的经典轻量结构输入是一张固定高度的宽幅图如 96×24输出是序列概率分布。训练前先定义字符字典# 字符字典省份汉字 大写字字母 数字 provinces 京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云陕甘青宁新 letters ABCDEFGHJKLMNPQRSTUVWXYZ digits 0123456789 # 注意车牌字母里没有 I 和 O避免和数字 1、0 混淆 charset provinces letters digits char_to_index {c: i for i, c in enumerate(charset)}字典是识别模型的“黑匣子”入口决定了模型能输出哪些字符。很多人训练时发现模型永远不输出某个省份的字十有八九是字典漏了这个字。字母表剔除了 I 和 O这是车牌规则决定的不是笔误。训练识别模型的损失函数用 CTCLossPyTorch 里直接调用import torch.nn as nn criterion nn.CTCLoss(blanklen(char_to_index), zero_infinityTrue) # log_probs: (T, N, C)T 是序列长度N 是 batchC 是字符类别数 # target_lengths: 每个样本真实字符数蓝牌 7绿牌 8 loss criterion(log_probs, targets, input_lengths, target_lengths)CTCLoss 的blank参数必须等于字符类别数表示空白符不能指向任何真实字符。zero_infinityTrue防止训练初期 log 值为 0 导致 loss 变成无穷大。训练时固定输入高度比如 24宽度可以变LPRNet 的卷积对宽度不敏感batch 内按最宽图 padding 即可。真实摄像头里车牌往往是斜的直接把倾斜的裁剪图扔给识别模型即使 LPRNet 能容忍一定倾斜效果也会打折。送入识别模型前用 OpenCV 透视变换矫正是最划算的预处理# 矫正把斜车牌转正 import cv2 import numpy as np def rectify_plate(plate_crop, corners): # corners 是检测到的车牌四个角点顺序为左上、右上、左下、右下 # 输出尺寸宽 96高 24 dst np.array([[0, 0], [96, 0], [0, 24], [96, 24]], dtypenp.float32) src np.array(corners, dtypenp.float32) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(plate_crop, M, (96, 24)) return warped如果检测模型只输出正矩形框可以先对框内区域做轮廓分析找出车牌的四个角点再做矫正。这一步在很多开源方案里是效果优化的关键动刀点。3.5 推理链路检测框到车牌号的一行输出训练完成后把检测和识别串起来。流程是读图 → 检测出车牌框 → 裁剪 → 透视矫正 → 送入 LPRNet → CTC 解码 → 后处理纠错。# inference.py完整识别流程 import cv2 import torch from ultralytics import YOLO detect_model YOLO(best_detect.pt) recog_model torch.jit.load(lprnet_jit.pt) # 用 TorchScript 加速推理 def decode_ctc(output, charset): # output shape: (T, C)每个时间步取最大概率类别 pred_ids output.argmax(dim1).tolist() result [] prev None for i, idx in enumerate(pred_ids): if idx prev: continue # 相邻重复字符合并 prev idx if idx len(charset): result.append(charset[idx]) return .join(result) def recognize(img_path): img cv2.imread(img_path) results detect_model(img)[0] for box in results.boxes.xyxy: x1, y1, x2, y2 map(int, box.tolist()) plate_crop img[y1:y2, x1:x2] warped rectify_plate(plate_crop, get_corners(plate_crop)) warped cv2.resize(warped, (96, 24)) warp_t torch.from_numpy(warped / 255.0).permute(2, 0, 1).unsqueeze(0).float() with torch.no_grad(): output recog_model(warp_t) return decode_ctc(output, charset)注意几个细节送入识别模型的图要先归一化到 0 到 1维度转成 C×H×W。检测结果results.boxes.conf是置信度车牌检测一般把阈值设在 0.5 左右太低会混入噪声框太高会漏掉模糊车牌。NMS 的 IoU 阈值通常设 0.45。torch.jit.load是加载 script 化模型的方式比直接调 eval 模式快但要求模型结构不带动态分支。4. 避坑与常见问题五条能救回一整周的血泪经验4.1 现象训练集 99%测试集 60%过拟合的典型表现现象训练集准确率接近 100%放到实拍图中立刻打回原形。原因检测模型把背景里某种固定模式当成了特征或者识别模型只记住了特定字体、特定光照下的字形。CCPD 图片全部来自同一批监控摄像头背景和角度高度一致模型很容易学会“背景套路”而不是“车牌特征”。解决第一增强数据对车牌图做随机亮度、对比度、透视变换。我常用的参数是亮度 ±20%、对比度 ±10%、透视 ±5°、旋转 ±10°这组范围不会破坏字符结构第二按拍摄场景切分训练集和验证集而不是随机切分第三识别模型只喂“裁剪后的车牌图”别把背景图直接送进去减少背景干扰。训练时同时盯训练 loss 和验证 loss两者差距持续拉大就是过拟合信号。4.2 现象绿牌识别率低得离谱字符数变了但模型不知道现象蓝牌识别率正常绿牌基本全错或者多一位少一位。原因绿牌是 8 位字符很多训练代码把输出序列长度固定成 7。更隐蔽的是绿牌第 2 位是 D/F 新能源标识不是传统发牌机关字母字符字典里要是没有 D 和 F解码必然错位。解决训练 CTC 时target_lengths按真实字符数传蓝牌 7、绿牌 8字典必须补上 D 和 F。推理时 CTC 解码本身支持变长输出不需要改网络结构。如果代码里用了全连接层把输出维度硬顶成 7那就要把结构改成序列输出。另外绿牌数据在标注阶段如果全按蓝牌 7 位标加再多样本也没用要先统一标注规则。4.3 现象GPU 利用率上不去训练慢是因为数据加载拖后腿现象用 GPU 训练但nvidia-smi里利用率只有 20%。原因读图、缩放、归一化都在 CPU 上做成了瓶颈。CCPD 图片是 720×1160 的 JPEG解压很慢GPU 在空等数据。解决先把 DataLoader 的num_workers调到 CPU 核心数减 1并开启pin_memoryTrue。ultralytics 训练命令里加workers8和cacheTrue能明显缓解。如果显存占满但利用率还是低说明单张图计算量太小而拷贝频繁调大 batch 就能解决。还有一个土办法把 JPEG 全部预转成 PNG 或内存映射格式磁盘占用高一些但训练速度立竿见影。4.4 现象识别结果多字漏字CTC 解码顺序反了现象车牌“皖A12345”被识别成“皖A123345”或“皖A1235”。原因CTC 解码时先做了相邻去重再取最大概率导致本应相邻的重复字符被错误合并或者blank索引设置到了字典内某个真实字符上解码结果混入空白符。解决顺序必须是先argmax拿到每个时间步的类别序列再合并相邻重复符号最后去掉空白符号。正确逻辑如下def decode_ctc(preds, charset): preds preds.argmax(dim-1).tolist() # 先取每个时间步的类别 out [] for i, idx in enumerate(preds): if i 0 and idx preds[i - 1]: continue # 合并相邻重复 if idx len(charset): continue # 跳过空白符号 out.append(charset[idx]) return .join(out)注意idx preds[i - 1]只在相邻位置才跳过非相邻的重复字符比如“皖A·55555”里的连续 5会完整保留。这个细节是我见过最频繁的翻车点。4.5 现象推理速度比预期慢很多图片尺寸和 batch 是元凶现象单张图要 200ms离实时差一大截。原因检测输入设成 640 没有降识别模型单独跑模型加载和预处理重复耗时没有用批处理或加速引擎。解决检测输入降到 416 或 320识别模型固定输入宽度用torch.cuda.synchronize()加time.time()分段计时看耗时到底是检测还是识别占掉。视频流场景每 2 到 3 帧跑一次检测中间帧复用上一帧检测结果车牌在视频里不会瞬移这个 trick 在监控场景里很常用。再进一步就是导出 TensorRT FP16 或 OpenVINO具体做法见第 5 章。5. 进阶玩法从能跑到好用只差这三步5.1 用 TensorRT 把推理速度再压一半这是“高性能”最容易立竿见影的一步。PyTorch 模型先导出成 ONNX再用 TensorRT 转成 FP16 engine命令如下# 导出 ONNXopset 版本建议 12 以上 yolo export modelbest_detect.pt formatonnx opset12 dynamicTrue # 用 trtexec 生成 FP16 engine trtexec --onnxbest_detect.onnx --saveEnginebest_detect_fp16.engine --fp16dynamicTrue会生成动态 batch 的 ONNXTensorRT 转换时可以指定最大 batch。FP16 精度损失在车牌识别任务上几乎看不出来但速度能提 40% 到 60%。如果项目明确要求 CPU 部署就不要碰 TensorRT改用 OpenVINO流程一样。5.2 自动化评估用一张表格说服答辩老师毕设最怕“演示成功、答辩被问死”。我一般会先跑一个批量评估脚本算三个指标字符级准确率、车牌级准确率、平均推理耗时。遍历测试集调用前面的recognize函数比对结果后统计correct_plate sum(1 for pred, gt in results if pred gt) total len(results) char_total sum(len(gt) for _, gt in results) char_correct sum( sum(1 for p, g in zip(pred, gt) if p g) for pred, gt in results if len(pred) len(gt) ) print(f车牌级准确率: {correct_plate / total:.2%}) print(f字符级准确率: {char_correct / char_total:.2%})这个脚本不花多少时间但能帮你定位模型到底在哪个省份、哪种光照下表现差答辩时也能拿出数字支撑而不是只说“效果还不错”。5.3 演示脚本置信度最高帧优先到了答辩或交作业那一步建议写一个支持摄像头实时识别、结果叠加在画面上的脚本。有一个我用过很多次的小技巧连续几帧识别结果不一致时取置信度最高那帧的结果不要取最新帧。车牌在视频里偶尔被行人挡住或反光糊住模型会跳字取置信度最高帧能大幅减少演示翻车。这套链路做完你会发现中文车牌识别项目里真正拉开复现效果差距的不是模型结构而是数据分布、解码逻辑和前处理细节。我自己的习惯是动手训练之前先花两小时把数据分布看清楚、把解码写对比盲目调参有用得多。希望这些经验能帮你少走几步弯路早点把这个项目的分数握在手里。本文还有配套的精品资源点击获取