简介一份面向计算机类毕业设计或课程作业的深度学习实践项目聚焦中文车牌识别场景涵盖从数据集预处理、模型训练到应用部署的完整流程。项目代码以Python为主并通过C实现底层图像处理与SDK调用适合需要快速搭建识别系统的高年级本科生或研究生参考。压缩包共179个文件、约32.91MB包含大量头文件、Python源码、C源文件、车牌图像样本、XML配置、MNN模型文件及训练/评估脚本兼顾算法实验与工程落地。资源内还提供Android APK、Java/Gradle工程、Dockerfile等部署相关内容便于跨平台验证。已有122人学习下载对于想掌握深度学习车牌识别、了解移动端部署与SDK封装方式的读者是一份结构清晰、可直接运行调试的完整项目模板。1. 中文车牌识别项目的正确打开方式这不是一个黑匣子而是一条可复现的流水线拿到“毕设课程作业_基于深度学习高性能中文车牌识别.zip”这个压缩包时你大概率正处于两个阶段之一一类是真的要交毕设或课程大作业手里只有一份代码和一堆不知道能不能跑起来的数据另一类是刚把深度学习的理论课学完想找一个能落地、能演示、能写进简历的项目。这个标题指向的东西很简单输入一张路边停车或监控抓拍的图片模型给出车牌在画面里的位置再把车牌区域里的字符识别出来输出一串类似“京A1B2C3”的文本。它看起来只是目标检测加 OCR 的组合但真正上手就会发现难点集中在中文汉字上的表现、蓝牌和新能源绿牌之间的差异以及最后能不能在 GPU 上跑出实时性能。这篇文章我会从解压 ZIP 开始一步步带你跑通训练、调参、加速和验收并把最容易翻车的细节摊开讲。2. 拆开 ZIP 包解压、伪加密和项目结构读懂2.1 先用命令把 ZIP 变成可以上手的目录unzip、7z 与伪加密的判断拿到这个压缩包后第一步自然是解压。如果你在 Linux 服务器上做深度学习环境配置最常见的方式就是命令行。Windows 用户虽然可以用右键菜单里的“压缩为 zip”对应的解压功能但服务器上通常没有图形界面所以命令行操作是躲不掉的# 先看一下压缩包里的内容避免解压出一堆不知名的目录 unzip -l 毕设\课程作业_基于深度学习高性能中文车牌识别.zip | head -30 # 正常解压到当前目录 unzip 毕设\课程作业_基于深度学习高性能中文车牌识别.zip # 如果你更习惯 7-Zip也可以用 7z 命令兼容性更好 7z x 毕设\课程作业_基于深度学习高性能中文车牌识别.zip参数说明unzip -l只列出压缩包内文件清单不解压适合先确认里面是完整项目还是散文件7z x会保留压缩包内的目录层级解压后的文件结构一目了然。这里有一个很多人遇到过的怪事解压到一半提示需要密码或者解压出来的文件打不开。这背后很可能不是真的加密而是 zip 伪加密。所谓伪加密是指压缩包文件头的加密标志位被改成了 1工具就以为它加密了但实际数据流并没有被加密处理。判断方法很简单用7z l archive.zip查看条目属性如果显示某些文件带加密标记但用空密码或随便输一个密码都能解开那基本就是伪加密。遇到这种文件不需要找所谓的 zip 密码移除工具去暴力破解直接修改文件头标志位或用 7-Zip 重新压缩一遍就能解决真正的 AES 加密 ZIP 只能靠字典撞那才需要专门工具。动手之前先花两分钟确认是不是伪加密能省掉大半天的折腾。2.2 先读 README 和目录结构检测模型、识别模型、数据预处理怎么分工解压之后不要急着跑训练脚本先花十分钟把目录结构看一遍。常见的中文车牌识别项目通常不是单一模型而是一个检测加识别的两段式流水线目录一般长这样lpr_project/ ├── README.md ├── requirements.txt ├── data/ │ └── ccpd/ │ ├── images/ │ └── annotations/ ├── detect/ │ ├── models/ │ ├── train.py │ └── config.yaml ├── rec/ │ ├── datasets.py │ ├── lprnet.py │ └── train.py ├── tools/ │ ├── split_data.py │ └── visualize.py ├── export.py └── inference.py这个结构里detect目录下的模型负责第一步从一张包含车辆、行人、道路的复杂街景图里找出“哪里是车牌”。它通常是一个目标检测网络输出车牌的边界框坐标。rec目录下的模型负责第二步把裁剪出来的车牌小图变成一串文本字符。这是一个不定长序列识别任务常见的做法是 LPRNet 或 CRNN 配合 CTC Loss。两段式方案的工程优势很明显检测和识别可以独立训练、独立替换检测模型换成 YOLOv8 不用动识别代码识别模型换成 Transformer 结构也不影响检测流程。对毕设来说这种模块化设计还能在答辩时清晰地讲出“我的系统由哪几个部分组成、每个部分承担什么职责”。另外data/ccpd下面常见的 CCPD 数据集本身就包含了一张图里多辆车、多块车牌的情况如果用一个端到端模型一口气输出所有字符训练难度会大很多两段式反而更稳。3. 数据准备CCPD 数据集的正确打开方式与预处理参数3.1 先看清标注格式CCPD 里没有 xml坐标都写在文件名里很多第一次接触车牌识别的同学会习惯性地去data/annotations里找 Pascal VOC 风格的 xml 文件或 COCO 风格的 json结果发现什么也没有。CCPDChinese City Parking Dataset是一个非常出名的开源中文车牌数据集它的所有标注信息——包括车牌四个角点坐标、车牌字符内容、车牌亮度等——都直接编码在图片文件名里。文件名长这样025-95_113-154383_386473-386473_307500_154385_88139-30_24_15_32_29_34-78-78.jpg要想把这个数据集用起来第一步就是写一个解析文件名的脚本。下面的代码可以把文件名里的关键信息提取出来显示车牌边界框和字符内容import re import cv2 def parse_ccpd_filename(filename: str): 解析 CCPD 数据集文件名。 返回: 车牌边界框四角点、车牌字符标签(未映射汉字)、倾斜角。 name filename.split(.)[0] parts name.split(-) # parts[2] 是边界框的两个对角点: 154383_386473 corners parts[2].split(_) pt1 [int(x) for x in corners[0].split()] pt2 [int(x) for x in corners[1].split()] # parts[3] 是四个角点坐标: 386473_307500_154385_88139 four_points parts[3].split(_) pts [list(map(int, p.split())) for p in four_points] # parts[4] 是字符标签30_24_15_32_29_34 (6个字符的类别编号) char_label parts[4].split(_) return { bbox: [pt1, pt2], four_points: pts, char_label: char_label, brightness: int(parts[5]) if len(parts) 5 else None, } if __name__ __main__: img cv2.imread(025-95_113-154383_386473.jpg) info parse_ccdp_filename(025-95_113-154383_386473.jpg.split(/)[-1]) print(info)逻辑说明文件名第一段025是车牌相对于整图的水平偏移比例第二段95_113是倾斜程度和车牌亮度信息第三段和第四段是定位信息第四段之后的数字串才是真正用于识别模型训练的字符类别编号。这里有个容易被忽略的细节字符类别编号里0和1分别代表车牌第一个汉字和第二个字母实际序列顺序是从左到右以_分隔的解析时千万别调换顺序。另外CCPD 里的车牌存在倾斜如果你直接cv2.imread后按bbox裁剪框里会带不少背景识别模型的输入最好按四个角点做一个透视矫正这一点直接影响到后续字符识别的准确率。3.2 预处理管线归一化、尺寸、Mosaic 增强和标签同步数据解析完下一步是确定预处理管线。这里要明白一个原则训练时对图片做的每一种变换推理时都必须能复现。很多新手训练时效果还行一上生产就崩十有八九是预处理不一致。常见做法是检测模型用 640×640 的输入识别模型用高度固定的 32×94 的灰度图两套预处理要分开写在配置里import cv2 import numpy as np def letterbox(img, size640): 保持宽高比的缩放和填充检测模型常用。 h, w img.shape[:2] r min(size / h, size / w) new_w, new_h int(w * r), int(h * r) resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) canvas np.full((size, size, 3), 114, dtypenp.uint8) dw, dh (size - new_w) // 2, (size - new_h) // 2 canvas[dh:dh new_h, dw:dw new_w] resized return canvas def rec_preprocess(img): 识别模型输入灰度化、固定高度、归一化到[-1, 1]区间。 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) h, w gray.shape[:2] target_h 32 target_w int(w * target_h / h) resized cv2.resize(gray, (target_w, target_h), interpolationcv2.INTER_CUBIC) # 归一化时注意原始像素区间是[0,255] normalized (resized.astype(np.float32) / 127.5) - 1.0 return normalized参数说明letterbox里的填充值114是一个经验值YOLO 系列官方实现在训练时用这个灰度值填充和用 0 填充效果差异不大但推理时保持一致能避免边界框偏移。rec_preprocess里目标高度32是 LPRNet 常用的输入高度宽度不固定是因为 CTC Loss 天然支持变宽输入但实际训练时最好把宽度也固定到一个范围内比如 94否则 batch 内张量形状不一致处理起来很麻烦。数据增强部分Mosaic 是目前检测模型提升小目标性能的常用手段把四张车牌图拼接成一张再训练。但要注意车牌本身就属于长宽比极端的目标Mosaic 拼接后如果对标签坐标处理不严谨很容易把车牌的标注框算错增强没效果不说反而把基础精度拉低。这块的逻辑是先保证单图的增强管线正确再加入 Mosaic每次改动后都跑一次验证集看指标变化不要一把梭。4. 训练与高性能推理把“高性能”三个字落到实处4.1 模型选型与训练参数YOLO 系做检测CTC 做识别“高性能”这个词在不同的标题下含义不同这里主要指两方面识别准确率高推理速度快。检测部分用 YOLOv5 或 YOLOv8 是当前的主流选择识别部分多数项目采用 LPRNet 或 CRNN。LPRNet 的核心设计是“CNN CTC”不依赖 RNN因此推理时对输入宽度不敏感部署起来也简单。训练时几个关键参数值得认真对待参数推荐值说明输入尺寸detect: 640×640 / rec: 32×94检测图太小时小目标车牌会丢失识别图高度固定后宽度按比例缩放batch sizedetect: 16~32 / rec: 128显存不够时优先减 batch别动输入尺寸初始学习率detect: 0.01 / rec: 0.001识别模型参数少学习率太大会震荡epochdetect: 100~200 / rec: 60~100车牌识别数据量不大过早停止反而更好weight_decay5e-4防止全连接层过拟合warmup3 epoch让训练初期更平稳尤其在使用预训练权重时优化器SGD momentum 0.937Adam 收敛快但最终精度通常不如 SGD训练命令本身并不复杂以识别模型为例# 在深度学习环境配置完成后启动识模型训练 python rec/train.py \ --data data/ccpd \ --batch-size 128 \ --lr 0.001 \ --epochs 80 \ --input-h 32 \ --input-w 94 \ --gpu 0参数说明--input-w 94的设定和 CCPD 数据集里车牌裁剪后的平均宽高比有关蓝牌是 440×140 的比例缩放到高度 32 后宽度大约就是 94。如果你加了透视矫正实际裁剪出来的图比例会更规整这个参数可以保持不变。这里要留意一个训练技巧识别模型的汉字部分各省简称样本量分布很不均匀像“京”“苏”出现频率高“藏”“青”出现频率低。如果不做任何处理模型会在高频字符上表现良好低频字符上表现拉胯。解决思路有两个一个是对低频字符做过采样另一个是在损失函数里给不同字符加权后者实现起来更省事直接改CrossEntropyLoss的weight参数就行。4.2 高性能部署从 PyTorch 到 ONNX 再到 TensorRT 的加速链路模型训练好以后如果只是调用model.forward()做推理离“高性能”还很远。PyTorch 的推理速度和 C 部署的 TensorRT 引擎不在一个数量级毕设答辩时能现场演示实时识别靠的往往是最后这一步加速。整个过程分两步走先导出 ONNX再构建 TensorRT 引擎。# 第一步导出 ONNX 格式opset 版本不要太高11~13 之间兼容性最好 python export.py --weights rec/best.pt --dynamic --simplify --opset 12 # 第二步用 trtexec 构建 FP16 推理引擎 /usr/src/tensorrt/bin/trtexec \ --onnxrec/best.onnx \ --saveEnginerec/best_fp16.engine \ --fp16 \ --minShapesinput:1x1x32x94 \ --optShapesinput:8x1x32x94 \ --maxShapesinput:16x1x32x94逻辑说明export.py导出 ONNX 时开启--dynamic是为了保留动态 batch 能力但trtexec构建引擎时给了三个形状约束其中minShapes是推理时最小的输入optShapes是性能最优的输入maxShapes是显存允许的上限。车牌识别模型输入通常只有 1 个通道形状是N×1×H×W和检测模型的N×3×H×W不同别搞混。--fp16开启半精度推理对车牌识别任务来说精度损失几乎可以忽略但显存占用和延迟能下降近一半。如果追求极致性能还可以用 INT8 量化但 INT8 需要额外的校准数据集而且车牌字符识别对细节很敏感量化后可能在某些低频汉字上翻车。我的建议是 FP16 作为默认选择INT8 留到确认精度可接受后再尝试。另外rec/best.pt是训练产出的权重文件导出前必须先跑一次验证集确认精度正常否则导出一个坏模型的 ONNX后面排查起来非常折磨。5. 避坑记录车牌识别常见的 5 个坑与排查步骤5.1 现象省份简称和汉字总是识别错尤其是“湘”“鄂”“川”混淆原因中文字符类别多但训练样本分布严重不均而且部分汉字字形相近模型提取的特征区分度不够。解决先统计训练集里每个字符的样本数量再用np.bincount可视化确认哪些字符样本极少。处理手段是给损失函数加weight或者对低频字符的图片做更强的随机扰动包括旋转、模糊、光照变化让模型不过度依赖局部纹理。5.2 现象训练到一半 loss 突然变成 nan或者从 2.4 直接炸到 1e5原因学习率过大加上模型中存在数值不稳定的层尤其在使用混合精度训练时梯度溢出是常见元凶。解决第一步先把torch.cuda.amp.GradScaler打开让梯度动态缩放第二步把初始学习率直接除以 10 再训练一轮观察 loss 曲线是否平滑下降。如果模型加载了预训练权重还需要确认warmup阶段是否生效GitHub 上常见的 YOLO 系实现里warmup_epochs设成 3 是安全线。5.3 现象PyTorch 测试没问题导出 ONNX 后识别结果变差原因ONNX 导出过程中算子被融合或替换部分实现存在精度差异更常见的原因是导出时没有把预处理操作归一化的均值方差同步进去推理时喂给模型的数据分布和训练时不一致。解决导出 ONNX 前把预处理写成模型的一部分比如在forward里做x (x / 255.0 - 0.5) / 0.5这样导出后输入只需要原始的0~255像素图即可。另外用onnxruntime跑一遍导出的模型对比 PyTorch 的推理结果逐层排查在哪一层开始出现偏差。5.4 现象检测模型在一张普通街景图上框出十几个区域大部分不是车牌原因NMS 阈值设置过松或者没有对候选框的宽高比做限制。车牌的宽高比相对固定蓝牌约 3:1绿牌约 2:1而检测模型在训练时可能见过各种形状的误检样本。解决把 NMS 的 IoU 阈值从默认的 0.45 调到 0.3同时在后处理阶段过滤掉宽高比小于 1.5 或大于 4.5 的框。这个约束不消耗额外算力但对输出质量的提升非常明显。5.5 现象训练时DataLoader的num_workers设成 8 后显存没满但内存先爆了原因PyTorch 的DataLoader每个 worker 都会复制一份数据预处理相关的对象如果你的预处理里包含大尺寸numpy数组或模型变换内存会成倍增长。解决num_workers设成 4 通常足够同时把persistent_workersTrue打开减少每个 epoch 启动 worker 的开销。如果内存还是不够退一步把预加载的数据改成__getitem__里实时读取牺牲一点速度换稳定性这在教学级显卡上尤其值得重视。6. 验收与演示两个指标和一个 demo让答辩现场一眼看到性能模型训练完不要用一两个随机样例拍脑袋而是要给出让别人信服的数据。这里有两套指标必须分开算字符级准确率和车牌级准确率。字符级准确率是把每一帧识别出的错误字符都算进去车牌级准确率则要求整块车牌的所有字符全部正确才计为一次成功。很多项目演示时会刻意展示车牌级准确率但如果你把它拆开看字符级准确率可能只有 90% 出头。正确做法是把两个指标都列出来配上根据置信度筛选后的曲线def evaluate_accuracy(pred_strs, gt_strs): 计算字符级和车牌级准确率。 total_chars 0 correct_chars 0 correct_plates 0 for pred, gt in zip(pred_strs, gt_strs): # 字符级逐字符比较长度不同按最长长度算 if len(pred) len(gt): correct_chars sum(p g for p, g in zip(pred, gt)) total_chars max(len(pred), len(gt)) # 车牌级完全相等才算对 if pred gt: correct_plates 1 char_acc correct_chars / total_chars plate_acc correct_plates / len(gt_strs) return char_acc, plate_acc逻辑说明这段代码里有一个大多数人会忽略的细节——预测字符串和真实标签长度不一致时直接跳过会导致指标虚高。所以在字符级计算时按较长的长度补位来算才能真实反映模型在多字或少字问题上的表现。另一个能够在答辩现场加分的做法是写一个 Gradio demo让评委直接拖入一张图片就能看到框和识别结果。核心代码大约 30 行调gr.Interface把inference函数包装一下既省去命令行操作的疏离感又能直观展示检测框和识别文本。我自己的经验是答辩时最容易暴露问题的是低光照和倾斜车牌的现场测试所以在验收前一定要准备 5 张自己拍的、不在训练集里的照片提前确认这些场景下的表现避免现场被问住。最后想说这类项目能不能真正做扎实关键不在于用了多新的模型而在于你对自己的系统边界有多清楚——知道它在什么条件下表现好、什么条件下会崩这比背下任何一篇论文都更让人信服。希望帮到你。本文还有配套的精品资源点击获取