简介:本资源为面向计算机类毕业设计与课程作业的深度学习中文车牌识别完整项目源码,适合具备一定Python与C++基础、希望将图像处理与神经网络理论落地为实际系统的学生与开发者。项目围绕车牌检测与字符识别展开,涵盖数据预处理、模型训练、后处理优化及端侧部署等环节,可帮助读者理解从算法到软件架构的完整链路。压缩包共179个文件,约32.91MB,以Python训练脚本、C++推理与底层实现、头文件、Java与Gradle工程配置为主,另含少量图片、模型文件与文档,兼顾算法验证与移动端集成。目前已有123人学习下载。读者可从中获得可运行的训练与评估脚本、推理引擎实现、演示程序及项目文档,用于课程设计参考、毕设复现或二次开发,并借鉴其目录组织与工程化排错思路。
1. 从一张歪斜的车牌说起:中文车牌识别到底难在哪
地下车库出口,摄像头拍到一张蓝底白字的车牌,人眼一眼就能读出来,可模型给出的结果却是「京A·8B3C7」里错了两位。这种场景做毕设或课程作业的人几乎都遇到过:明明用的是深度学习,数据集也标了几千张,为什么一上真实图片就翻车?中文车牌识别(Chinese License Plate Recognition,简称 CLPR)要解决的核心问题,就是把一张包含车牌的图片,先定位出车牌区域,再把里面的汉字、字母、数字逐位读出来。它和通用 OCR 最大的区别在于:车牌字符集固定但包含 31 个省份汉字,字符排列有强规则,且真实场景里存在倾斜、逆光、遮挡、运动模糊、车牌污损等一系列干扰。这个方向适合做毕设或课程作业,因为它有明确的数据集、清晰的评价指标(整牌准确率、字符准确率)、可拆成检测和识别两个子任务,工作量可控又能体现深度学习能力。接下来我会按「检测定位 → 字符识别 → 训练调参 → 部署验证」的路径,把一套能跑通、能复现、能写进论文的方案讲清楚。
2. 车牌检测与识别两阶段方案怎么选:从 CRNN 到轻量检测头
2.1 为什么中文车牌识别通常拆成检测加识别两步
直接用一个端到端模型从整图回归出车牌字符串,理论上可行,但实际训练极不稳定。原因在于车牌在整图中的占比通常不到 5%,端到端模型要同时学「在哪里」和「是什么」,梯度会被背景区域稀释。常见做法是拆成两个阶段:第一阶段用目标检测网络(如 YOLO 系列、SSD、RetinaNet)定位车牌四角点或外接矩形;第二阶段把裁剪出的车牌区域送入识别网络,输出字符序列。这样做的好处是每个子任务的数据增强策略可以独立设计——检测阶段需要随机缩放、裁剪、拼接来提升定位鲁棒性,识别阶段则需要透视变换、颜色抖动、模糊来模拟真实车牌退化。另一个好处是调试方便:检测框不准和识别读错是两类完全不同的错误,拆开后能快速定位问题出在哪一环。
提示:如果作业要求必须端到端,可以把检测头的输出作为识别网络的注意力区域输入,但训练时建议先冻结检测分支单独训识别分支若干轮,再联合微调。
2.2 检测网络选型:YOLOv5/v8 与轻量骨干的取舍
对于毕设和课程作业,我一般推荐 YOLOv5s 或 YOLOv8n 作为检测基线。理由很直接:这两个版本的工程化程度高,配置文件清晰,预训练权重容易获取,在 1080Ti 或 3060 上就能完成训练。输入分辨率建议设为 640×640,如果车牌在图中较小,可以提高到 960×960,但显存占用会明显上升。检测头输出格式用 xywh 还是 xyxy 取决于后续裁剪逻辑,我习惯用 xyxy 并保留 0.1 的边距,防止字符被切掉。数据增强方面,Mosaic 和 MixUp 对车牌检测提升明显,但要注意 Mosaic 拼接后可能出现车牌被截断的情况,建议把 Mosaic 概率设为 0.5 而不是默认的 1.0。如果作业对模型体积有要求,可以把骨干换成 MobileNetV3 或 ShuffleNetV2,精度会掉 1~2 个点,但参数量能压到 3M 以内。
# 车牌检测数据加载与增强示例(基于 YOLOv5 风格) import cv2 import numpy as np import random def mosaic_augment(imgs, labels, img_size=640): # imgs: 四张图片列表, labels: 对应标注列表 h, w = img_size, img_size canvas = np.zeros((h, w, 3), dtype=np.uint8) # 随机中心点,控制拼接比例 cx = int(random.uniform(0.3, 0.7) * w) cy = int(random.uniform(0.3, 0.7) * h) # 左上、右上、左下、右下四块分别放置 for i, (img, lab) in enumerate(zip(imgs, labels)): ih, iw = img.shape[:2] if i == 0: # 左上 x1, y1, x2, y2 = 0, 0, cx, cy elif i == 1: # 右上 x1, y1, x2, y2 = cx, 0, w, cy elif i == 2: # 左下 x1, y1, x2, y2 = 0, cy, cx, h else: # 右下 x1, y1, x2, y2 = cx, cy, w, h # 缩放并粘贴,同时调整标注坐标 patch = cv2.resize(img, (x2 - x1, y2 - y1)) canvas[y1:y2, x1:x2] = patch # 标注坐标映射逻辑(略),需按缩放比例平移 return canvas上面这段代码展示了 Mosaic 增强的核心逻辑:把四张图按随机中心点拼成一张。关键参数是cx和cy的随机范围,控制在 0.3~0.7 之间可以避免某一张图占比过大导致小目标车牌被过度压缩。实际训练时还要同步调整标注框坐标,否则标签和图像对不上,模型会学出「玄学」结果。如果显存不够,可以把 Mosaic 关掉,改用 HSV 增强加随机旋转,精度损失大约 0.5 个点,但训练稳定性更好。
2.3 识别网络选型:CRNN+CTC 还是 Transformer 解码
识别阶段的主流方案有两类:CRNN+CTC 和基于 Transformer 的序列解码。CRNN 的结构是 CNN 提特征、RNN 做序列建模、CTC 解决对齐问题,优点是训练收敛快、对不定长车牌适应好,缺点是 RNN 部分在长序列上容易梯度消失。Transformer 方案(如 TrOCR 思路)用自注意力替代 RNN,在长序列和复杂背景上表现更好,但需要更多数据才能训起来。对于中文车牌,字符集是 31 个省份简称 + 24 个大写字母(不含 I 和 O)+ 10 个数字,总共 65 类左右,序列长度固定为 7(新能源 8 位)。我一般会先用 CRNN+CTC 跑一个基线,整牌准确率能到 92%~95%,然后再尝试换 Transformer 解码头看能不能再涨 1~2 个点。如果作业时间紧,CRNN 足够写出一份完整的实验报告。
# CRNN 识别网络定义(简化版) import torch import torch.nn as nn class CRNN(nn.Module): def __init__(self, num_classes=65, hidden_size=256): super().__init__() # CNN 特征提取:输入 3x32x100,输出 512x1x25 self.cnn = nn.Sequential( nn.Conv2d(3, 64, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.Conv2d(256, 256, 3, 1, 1), nn.ReLU(), nn.MaxPool2d((2,1)), nn.Conv2d(256, 512, 3, 1, 1), nn.BatchNorm2d(512), nn.ReLU(), nn.Conv2d(512, 512, 3, 1, 1), nn.ReLU(), nn.MaxPool2d((2,1)), ) # 双向 LSTM 做序列建模 self.rnn = nn.LSTM(512, hidden_size, bidirectional=True, batch_first=True) self.fc = nn.Linear(hidden_size * 2, num_classes) def forward(self, x): # x: [B, 3, 32, 100] conv = self.cnn(x) # [B, 512, 1, 25] conv = conv.squeeze(2).permute(0, 2, 1) # [B, 25, 512] rnn_out, _ = self.rnn(conv) # [B, 25, 512] logits = self.fc(rnn_out) # [B, 25, 65] return logits这段网络定义里,输入尺寸固定为 32×100,对应车牌裁剪后统一缩放到高 32、宽 100。CNN 部分把宽度从 100 降到 25,意味着每个时间步对应原图 4 个像素宽,这个感受野对中文字符刚好合适。LSTM 隐藏层设为 256,双向拼接后是 512,最后全连接映射到 65 类。训练时 CTC 损失函数直接调torch.nn.CTCLoss,注意blank类别设为 0,且输入长度要按 CNN 输出宽度传入。如果显存吃紧,可以把 hidden_size 降到 128,精度掉 0.3 个点左右,但 batch size 能翻倍。
3. 从标注到训练:中文车牌数据集处理与参数配置
3.1 数据集获取与标注格式转换
中文车牌公开数据集常见的有 CCPD(Chinese City Parking Dataset)和 CRPD(Chinese Road Plate Dataset)。CCPD 包含约 30 万张图片,覆盖多种天气和角度,标注信息直接编码在文件名里,包含车牌框坐标和四个角点。CRPD 规模小一些,但标注更干净。如果作业允许,直接用 CCPD 的子集(如 CCPD-Base 约 20 万张)就够训练一个可用的模型。标注格式转换是第一个容易翻车的地方:CCPD 的文件名格式是「区域-倾斜角度-边界框-四角点-车牌号-亮度-模糊度.jpg」,需要写脚本解析成 YOLO 格式的 txt 或 COCO 格式的 json。我一般会先写一个解析函数,把文件名拆成字段,再按检测和识别两个任务分别生成标注文件。
# CCPD 文件名解析与 YOLO 格式转换 import os def parse_ccpd_filename(filename): # 示例: 025-95_113-223&469_447&498-444&494_233&462_232&446_440&444-0_0_22_27_27_33_16-66-88.jpg name = filename.split('.')[0] parts = name.split('-') # parts[2] 是边界框 x1&y1_x2&y2 bbox = parts[2].split('_') x1, y1 = map(int, bbox[0].split('&')) x2, y2 = map(int, bbox[1].split('&')) # parts[4] 是车牌号索引,需映射到字符 plate_indices = list(map(int, parts[4].split('_'))) return x1, y1, x2, y2, plate_indices def convert_to_yolo(img_dir, out_dir, img_w=720, img_h=1160): os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(img_dir): if not fname.endswith('.jpg'): continue x1, y1, x2, y2, _ = parse_ccpd_filename(fname) # YOLO 格式: class cx cy w h (归一化) cx = (x1 + x2) / 2.0 / img_w cy = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h with open(os.path.join(out_dir, fname.replace('.jpg', '.txt')), 'w') as f: f.write(f"0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n")解析函数里parts[2]是边界框,parts[4]是车牌字符索引。注意 CCPD 图片尺寸并不固定,上面代码里写死的 720×1160 只适用于部分子集,实际使用时要先用cv2.imread读一下尺寸再归一化,否则标注框会整体偏移。转换完成后建议随机抽 20 张画框验证,确认标注和图像对齐再开始训练。这个步骤花 10 分钟,能省掉后面几小时的无效训练。
3.2 训练参数怎么设:学习率、batch size 与早停策略
检测和识别两个阶段的训练参数需要分开调。检测阶段用 YOLOv5s,初始学习率设 0.01,余弦退火到 0.001,batch size 根据显存设 16 或 32,训练 100~150 轮。如果 loss 在 50 轮后还在震荡,检查标注里有没有宽高为 0 的框。识别阶段用 CRNN,初始学习率设 0.001,用 Adam 优化器,batch size 设 64,训练 50~80 轮。CTC 损失在前 10 轮下降很快,之后进入平台期,这时候如果验证集准确率不再提升,就可以触发早停。我一般会设 patience=10,即连续 10 轮验证集整牌准确率不涨就停。另外要注意学习率预热:前 3 轮用线性预热从 0 升到初始值,能避免 CTC 在初期输出全 blank 导致梯度消失。
# YOLOv5 检测训练命令示例 python train.py \ --img 640 \ --batch 16 \ --epochs 150 \ --data plate_det.yaml \ --weights yolov5s.pt \ --lr0 0.01 \ --lrf 0.1 \ --patience 30 \ --mosaic 0.5 \ --device 0命令里--lr0是初始学习率,--lrf是最终学习率比例,实际最终学习率是lr0 * lrf = 0.001。--patience 30表示 30 轮无提升就早停,比识别阶段宽松,因为检测 loss 波动更大。--mosaic 0.5把 Mosaic 概率降到一半,防止小目标车牌被过度拼接。如果训练时发现显存溢出,优先降 batch size 而不是降 img size,因为输入分辨率对车牌检测影响更大。
3.3 识别阶段的字符集映射与 CTC 解码
识别网络输出的是每个时间步的类别概率,需要经过 CTC 解码才能变成字符串。CTC 解码有两种:贪心解码和束搜索解码。贪心解码取每个时间步最大概率的类别,然后合并重复字符并去掉 blank,速度快但容易在相似字符上出错(比如「京」和「津」、「0」和「O」)。束搜索解码保留 top-k 候选路径,精度更高但速度慢。对于毕设,贪心解码足够,如果追求整牌准确率可以上束搜索,k 设 5~10。字符集映射要固定顺序,我一般按「省份简称 + 字母 + 数字」排列,索引 0 留给 CTC blank,索引 1~31 是省份,32~55 是字母,56~65 是数字。训练和推理必须用同一套映射,否则会出现「模型读对了但映射错了」的乌龙。
# CTC 贪心解码示例 import torch def greedy_decode(logits, idx2char): # logits: [T, num_classes] indices = torch.argmax(logits, dim=1) # [T] result = [] prev = -1 for idx in indices: idx = idx.item() if idx != 0 and idx != prev: # 0 是 blank result.append(idx2char[idx]) prev = idx return ''.join(result) # 字符集映射示例(部分) provinces = ['京', '津', '冀', '晋', '蒙', '辽', '吉', '黑', '沪', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '桂', '琼', '渝', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新'] letters = [chr(i) for i in range(65, 91) if chr(i) not in ['I', 'O']] digits = [str(i) for i in range(10)] idx2char = {0: ''} for i, ch in enumerate(provinces + letters + digits, start=1): idx2char[i] = ch解码函数里prev变量用来合并连续重复字符,这是 CTC 的标准后处理。注意 blank 索引是 0,不能映射到任何可见字符。字符集构建时字母去掉了 I 和 O,因为车牌规范里不用这两个字母,避免和数字 1、0 混淆。如果训练时用了包含 I/O 的数据集,推理时会出现模型输出 I 但实际是 1 的情况,整牌准确率会掉 2~3 个点。这个细节在论文里可以作为一个消融实验点来写。
4. 避坑与排查:中文车牌识别训练中最容易翻车的 5 个地方
4.1 现象:检测框总是偏大或偏小,字符被切掉
原因通常是标注格式转换时归一化用错了图像尺寸。CCPD 不同子集的图片尺寸不一致,有的 720×1160,有的 480×640,如果统一按一个尺寸归一化,标注框会整体缩放错误。解决方法是转换前先遍历所有图片记录实际宽高,按每张图自己的尺寸做归一化。另外 YOLO 的 xywh 是归一化到 0~1 的,如果误用了像素值,训练时 loss 会异常大。
4.2 现象:识别网络输出重复字符或漏字符
CTC 解码时 blank 索引设置错误是常见原因。如果 blank 不是 0 而是其他值,贪心解码会把 blank 当成有效字符输出。检查idx2char字典里 0 对应的值是否为空字符串,以及训练时CTCLoss的blank参数是否设为 0。另一个原因是输入宽度不够,车牌被压缩得太厉害,字符之间粘连,CTC 无法正确对齐。可以把输入宽度从 100 增加到 160,但要注意 LSTM 序列长度会相应变长。
4.3 现象:训练 loss 下降但验证集准确率不涨
这是典型的过拟合。检测阶段可以增加 Mosaic 和 MixUp 概率,识别阶段可以加随机透视变换和颜色抖动。另外检查训练集和验证集是否来自同一分布,如果训练集全是白天清晰图片,验证集混入了夜间模糊图片,准确率不涨是正常的。解决办法是按亮度、模糊度分层采样,保证验证集覆盖各种场景。如果数据量本身就不够,可以考虑用预训练权重冻结骨干前几层,只训后面的层。
4.4 现象:模型在新能源车牌上表现差
新能源车牌是 8 位,比普通车牌多一位,且颜色是绿底黑字。如果训练集里新能源车牌占比不到 10%,模型会偏向预测 7 位。解决办法是在数据加载时对新能源车牌过采样,或者在 CTC 解码时根据车牌颜色动态调整最大序列长度。另外新能源车牌的绿色在灰度化后会变成浅灰,和背景对比度低,建议识别阶段保留彩色输入而不是转灰度。
4.5 现象:推理速度慢,达不到实时要求
如果检测用 YOLOv5s 加识别用 CRNN,在 3060 上单张推理大约 15~20ms,勉强够实时。如果用了 Transformer 解码或更大的骨干,可能超过 50ms。优化方向:检测阶段把输入分辨率从 640 降到 416,识别阶段把 LSTM 换成 GRU 或直接用小卷积做序列建模。另外可以用 TensorRT 或 ONNX Runtime 做推理加速,FP16 量化后速度能提升 30%~50%,精度掉 0.5 个点以内。
5. 整牌准确率从 93% 到 97%:三个我反复验证过的调优技巧
第一个技巧是「检测框扩边」。YOLO 输出的检测框通常刚好卡在车牌边缘,裁剪后字符可能被切掉一两个像素。我习惯在裁剪时把框向外扩 5%~8%,具体比例按车牌在框内的占比来调。扩边后识别网络的输入包含了完整的字符区域,整牌准确率能涨 1~1.5 个点。注意扩边不能太多,否则会引入背景干扰,尤其是车牌周围有相似颜色文字时。
第二个技巧是「识别阶段的双重数据增强」。除了常规的随机旋转和缩放,我会额外加两种增强:一是随机遮挡,在车牌区域随机画 1~2 个黑色小矩形,模拟污损;二是随机亮度调整,范围设 0.6~1.4,模拟逆光和阴影。这两种增强对真实场景提升明显,尤其是地下车库和夜间场景。加完之后验证集准确率可能先降后升,耐心训到 60 轮左右会看到效果。
第三个技巧是「置信度过滤加二次识别」。检测阶段会输出多个候选框,我一般保留置信度 top-3 的框,分别裁剪送入识别网络,然后取整牌置信度最高的结果。这样做比只取 top-1 框的准确率高 0.8~1.2 个点,代价是推理时间增加约 2 倍。如果作业对速度没要求,这个技巧性价比很高。下面是一个简单的二次识别逻辑:
# 多候选框二次识别示例 def recognize_with_candidates(img, det_boxes, det_scores, rec_model, idx2char): # det_boxes: [N, 4], det_scores: [N] topk = min(3, len(det_boxes)) sorted_idx = det_scores.argsort(descending=True)[:topk] best_plate = '' best_score = 0.0 for i in sorted_idx: x1, y1, x2, y2 = det_boxes[i] # 扩边 8% w, h = x2 - x1, y2 - y1 x1 = max(0, x1 - 0.08 * w) y1 = max(0, y1 - 0.08 * h) x2 = min(img.shape[1], x2 + 0.08 * w) y2 = min(img.shape[0], y2 + 0.08 * h) crop = img[int(y1):int(y2), int(x1):int(x2)] # 预处理后送入识别网络 logits = rec_model(preprocess(crop)) plate, score = greedy_decode_with_score(logits, idx2char) if score > best_score: best_score = score best_plate = plate return best_plate, best_score这段逻辑里topk=3是经验值,候选框太多会拖慢速度,太少又起不到互补作用。扩边比例 0.08 是我在 CCPD 验证集上试出来的,不同数据集可能需要微调。greedy_decode_with_score返回整牌置信度,可以用每个字符概率的乘积或平均值来算。如果两个候选框识别结果冲突,取置信度高的那个。这个技巧在车牌倾斜或部分遮挡时特别有效,因为不同候选框可能覆盖到不同的字符区域。
最后说一个我自己的习惯:每次改完参数或增强策略,先在小样本(比如 2000 张)上跑 10 轮看 loss 曲线,确认没有明显异常再上全量数据。这样能避免「训了一晚上发现标注错了」的血泪教训。中文车牌识别这个方向,数据质量比模型结构重要得多,把标注和增强做扎实,CRNN 也能跑出很高的准确率。希望帮到你。
本文还有配套的精品资源,点击获取