☰
基于深度学习的车牌识别实战:CCPD/CRPD数据集与多任务模型搭建
2026/10/2 8:34:35 网站建设 项目流程

简介:基于深度学习与CCPD、CRPD数据集构建的车牌识别综合项目,覆盖车辆检测、车牌检测识别、车牌颜色识别与车身颜色识别,整体识别准确率达98.5%。源码基于Python 3.8与PyTorch 1.8开发,支持国内多种车牌,测试图片存放于imgs文件夹,推理结果自动保存至result目录,并允许用户使用CPU或GPU自行训练和测试模型,适合计算机、人工智能、自动化等专业学生用于课程设计、毕业设计或项目立项演示。压缩包共154个文件,约39.03MB,主要包含43个Python脚本、19个PyTorch模型文件(pth/pt)、20个YAML配置、38张测试图片、7个Shell脚本及若干XML、TXT等标注与说明文件,目录结构清晰,便于按功能模块查阅。目前已有1181人学习下载。资源内含可直接运行的项目代码、预训练模型、数据集配置与训练脚本,还附带基础环境说明与结果验证流程,方便在此基础上二次开发,拓展更多识别功能。

1. 基于深度学习的车牌识别项目:它实际解决什么问题

晚上十点的停车场出口,灯光昏暗,车牌在镜头里带了一点倾斜和反光,传统 OpenCV 加模板匹配的做法在好天气能跑到九成,一碰到这种场景就翻车。这个项目要做的,是用 CCPD 和 CRPD 这两个国内真实场景数据集,训练一套深度学习车牌识别管线,让车牌号识别、车牌颜色识别、车身颜色识别一次全出来。CCPD 覆盖蓝牌和复杂场景,CRPD 补足新能源绿牌,两者合起来才是完整的国内车牌分布。适合做智慧停车、园区出入口、套牌车筛查的从业者,拿到源码包之后可以直接改造,而不是从零攒数据、调网络。

2. CCPD 与 CRPD 数据集:标签解析与训练集配比怎么做

先说一个结论:这个项目的数据层没有想象中省事。CCPD 和 CRPD 的标注格式不兼容,前者把部分标签藏在图片文件名里,后者用的是独立标签文件。如果不先做一个统一解析层,后面训练脚本会被两套文件格式拖死。我一般会先花半天把数据解析脚本写对,再碰模型。

2.1 CCPD 文件名里藏着标签:先学会把数字段解析出来

CCPD 的图片文件名很长,一串字符里包含了车牌文本、检测框坐标、角点坐标等字段。不同渠道下载到的 CCPD 文件名结构并不完全一致,直接按固定索引切分容易出错。我的做法是先提取文件名里所有连续数字,再通过可视化脚本人工确认哪一段是 bbox。

import os import re import cv2 def extract_digits_from_name(img_path: str) -> list: """从 CCPD 文件名中提取所有连续数字字段,供后续解析确认""" fname = os.path.basename(img_path) nums = re.findall(r"\d+", fname) return [int(n) for n in nums] def draw_bbox_from_nums(img_path: str, nums: list, candidate_idx: int) -> None: """ 把第 candidate_idx 组数字当作 x, y, w, h 画框。 参数 candidate_idx 对应 extract_digits_from_name 返回列表中的起始下标。 """ img = cv2.imread(img_path) h_img, w_img = img.shape[:2] x, y, w, h = nums[candidate_idx:candidate_idx + 4] # 如果坐标小于 1,说明本身就是归一化坐标,直接放大到原图尺寸 if max(x, y, w, h) <= 1: x, y, w, h = int(x * w_img), int(y * h_img), int(w * w_img), int(h * h_img) cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imwrite("check_bbox.jpg", img)

这段代码的逻辑是:把文件名里所有数字抓出来,然后拿其中一段作为 bbox 画到原图上。如果画出来的框正好包住车牌,这段坐标就是标注;如果框偏到车灯或保险杠上,就换下一个候选下标。注意 CCPD 是单目标数据集,每张图只有一辆车、一个车牌,所以候选框画出来只需要确认一个。这一步看着琐碎,但能避免后面训练时标签整体偏移的灾难。

2.2 把 CCPD 和 CRPD 统一成同一份 txt 清单

CRPD 的标签一般以独立文件存在,常见的有 XML 或 JSON 两种风格。各渠道下载的字段命名不完全一样,但最终都需要转成同一份训练清单。我推荐统一成四列 txt:一行一个目标,包含图片路径、车牌文本、车牌颜色、车身颜色、bbox 坐标、是否绿牌。先写一个简单的中间转换脚本:

import json import glob import os def parse_crpd_json(json_path: str): """解析 CRPD 标注文件,字段名按实际数据集调整即可""" with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) # 常见字段:plate_text, plate_color, car_color, bbox # 如果没有 car_color 字段,手动补一个默认值 record = { "plate_text": data.get("plate_text", ""), "plate_color": data.get("plate_color", "blue"), "car_color": data.get("car_color", "gray"), "bbox": data.get("bbox", [0, 0, 100, 100]), } return record def convert_to_unified_txt(json_dir: str, out_txt: str) -> None: """把一批 CRPD 标签统一写成四列 txt""" lines = [] for jp in glob.glob(os.path.join(json_dir, "*.json")): rec = parse_crpd_json(jp) img_path = jp.replace(".json", ".jpg") x, y, w, h = rec["bbox"] lines.append(f"{img_path} {rec['plate_text']} {rec['plate_color']} {rec['car_color']} {x} {y} {w} {h}") with open(out_txt, "w", encoding="utf-8") as f: f.write("\n".join(lines)) print(f"转换完成,共 {len(lines)} 条")

做这一步时要注意:CRPD 里车身颜色字段存在缺失的可能,没有就补一个统计占比最高的类别,别让标签空缺导致训练崩溃。统一成 txt 之后,训练脚本只需要读一份文件,不用每次都在两套格式之间切换。这里面最容易出问题的点是 bbox 坐标是否为归一化值,转换前先打印几条数据人工核对,再批量跑。

2.3 训练集配比:蓝牌、绿牌和恶劣场景的比例决定了模型上限

CCPD 以蓝牌为主,CRPD 主要补绿牌。训练时如果直接把两个数据集按原始数量混合,绿牌占比会非常低,模型在绿牌上的召回率会很难看。我一般用分层抽样的方式控制比例,先按车牌类型分组,再按场景分组。

import random def build_stratified_dataset(ccpd_files: list, crpd_files: list, blue_ratio: float = 0.75): """ 按蓝牌/绿牌比例做分层抽样。 blue_ratio 表示蓝牌在训练集中所占比例,绿牌由 CRPD 补充。 """ blue_files = [f for f in ccpd_files if "green" not in f] green_files = crpd_files # 按目标蓝牌比例计算各取多少张 total = len(blue_files) + len(green_files) target_blue = int(total * blue_ratio) target_green = total - target_blue sampled_blue = random.sample(blue_files, min(target_blue, len(blue_files))) sampled_green = random.sample(green_files, min(target_green, len(green_files))) return sampled_blue + sampled_green

这里 blue_ratio 设为 0.75 是因为真实路面场景蓝牌确实占多数,但模型不能因此就把绿牌当异常忽略。CRPD 图片数量如果不够,可以用复制、水平翻转和轻微旋转做数据增强补量。另外 CCPD 自带的 weather、blur、challenge 子集,每类抽 5% 到 10% 混进训练集,能让模型在雨雾和模糊场景下不至于直接失效。

提示:训练集里蓝绿牌比例与真实场景偏差太大,颜色识别和车牌检测都会翻车。配比之前先统计两边的数量,做到心里有数。

3. 车牌识别网络怎么搭:检测加多任务识别,一次输出三类信息

数据解析完之后,进入模型设计。这个项目的任务有三个:车牌检测、车牌字符识别、车牌颜色和车身颜色分类。常见做法不是端到端一个网络全干,而是两段式:第一步用 YOLO 检测车牌,第二步把车牌区域裁剪出来,送入一个多任务 CNN,同时完成字符识别和两种颜色分类。

3.1 为什么不选端到端方案

端到端方案把检测、字符识别、属性分类塞进一个网络,理论上更优雅,但实际工程里问题很多。第一个问题是标签格式要求高,端到端模型通常需要更精细的车牌角点标注;第二个问题是调试困难,字符识别错了你不知道是检测阶段错了还是识别阶段错了。两段式的好处是每个环节可以单独验证,检测模型输出车牌框,画出来肉眼检查,识别模型单独测准确率,模块边界清晰。

检测部分直接用 YOLOv5s 或 YOLOv8s 都是合理选择,输入分辨率 640,模型小、部署方便。唯一要注意的是检测模型训练时要加入 CCPD 的 challenge 子集,否则车牌倾斜角度大一点就可能漏检。

3.2 多任务识别网络:共享 Backbone、三个输出头

识别网络输入是裁剪出来的车牌区域,输出三样东西:车牌字符序列、车牌颜色类别、车身颜色类别。我用一个轻量 CNN 做 backbone,后面接 LSTM 加 CTC 做字符识别,再接两个全连接头做颜色分类。

import torch import torch.nn as nn import torchvision.models as models class PlateMultiTaskNet(nn.Module): """ 输入:车牌区域裁剪图,resize 到 (3, 48, 168) 输出:ctc_logits 给 CTC Loss;plate_color_logits、car_color_logits 给 CrossEntropy Loss """ def __init__(self, vocab_size: int, num_plate_colors: int = 6, num_car_colors: int = 9): super().__init__() backbone = models.resnet18(pretrained=True) backbone.fc = nn.Identity() self.backbone = backbone # 将 ResNet 输出的特征图按宽度方向展开成序列 self.lstm = nn.LSTM(512, 256, bidirectional=True, batch_first=True) self.ctc_fc = nn.Linear(512, vocab_size) # 颜色分类头 self.plate_color_fc = nn.Linear(512, num_plate_colors) self.car_color_fc = nn.Linear(512, num_car_colors) def forward(self, x): feat = self.backbone(x) # (batch, 512, 3, 6) b, c, h, w = feat.shape # 把高度方向作为序列长度,宽度作为 batch 内的独立帧 feat_seq = feat.permute(0, 3, 2, 1).reshape(b, w * h, c) lstm_out, _ = self.lstm(feat_seq) ctc_logits = self.ctc_fc(lstm_out) # 用全局池化后的特征做颜色分类 pooled = feat.mean(dim=[2, 3]) plate_logits = self.plate_color_fc(pooled) car_logits = self.car_color_fc(pooled) return ctc_logits, plate_logits, car_logits

这段代码的核心思路是把 ResNet 输出的特征图高度方向当作时间步,送入双向 LSTM,从而让 CTC 自己学会字符之间的时序关系,不需要提前切分单个字符。输入图高度设置为 48,宽度 168,比例接近真实车牌的长宽比。颜色分类头不依赖序列信息,直接对全局特征池化后分类。

CTC 的字符表很关键。车牌字符由汉字省份简称、英文字母、数字组成,总数大约 70 个左右。构造字典时要把 blank 放在索引 0,CTC Loss 默认 blank 为 0,这一点忘了设置会导致 loss 完全混乱。

3.3 车牌区域预处理:归一化高度,别动颜色通道

车牌输入到识别网络之前,预处理细节会直接影响三个任务的准确率。我的做法是先把检测框裁剪出来,再按高度归一化。

import cv2 def preprocess_plate_crop(crop_img, target_h=48, target_w=168): """车牌区域预处理:保持宽高比缩放后补边,再归一化""" h, w = crop_img.shape[:2] scale = target_h / h new_w = int(w * scale) resized = cv2.resize(crop_img, (new_w, target_h)) # 宽度不足 target_w 时右侧补灰边 if new_w < target_w: canvas = np.full((target_h, target_w, 3), 114, dtype=np.uint8) canvas[:, :new_w] = resized resized = canvas resized = resized.astype(np.float32) / 255.0 # BGR 转 RGB,并调整维度为 (3, 48, 168) resized = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB) return torch.from_numpy(resized).permute(2, 0, 1)

缩放时保持宽高比非常重要,直接把 200 像素宽的车牌压成 168 会导致字符横向变形。补灰边比拉伸更稳妥。注意不要做颜色增强,比如饱和度抖动、色调偏移这类增强,会直接把车牌颜色类别搞乱,蓝牌可能变成紫牌。

4. 训练参数与 Loss 配比:多任务场景下怎么调才不打架

多任务训练的难点不在单个任务,而在三个任务共享 backbone 后互相干扰。字符识别的梯度更新幅度大,颜色分类的梯度幅度小,权重配得不合适,模型训练十几轮后颜色分类还在原地踏步。

4.1 三个 Loss 各用各的,但权重按任务难度配

字符识别用 CTC Loss,两种颜色分类用 CrossEntropy Loss。总 loss 按权重相加:

ctc_criterion = nn.CTCLoss(blank=0) ce_criterion = nn.CrossEntropyLoss() ctc_loss = ctc_criterion(ctc_logits.permute(1, 0, 2), targets, target_lengths, input_lengths) plate_loss = ce_criterion(plate_logits, plate_labels) car_loss = ce_criterion(car_logits, car_labels) total_loss = ctc_loss * 1.0 + plate_loss * 0.5 + car_loss * 0.25

权重这样分配的原因有三点。CTC 负责的是最核心的车牌字符识别,而且序列任务收敛慢,需要最大权重;车牌颜色只有蓝、黄、绿、白、黑等少数几类,相对好学,给 0.5 足够;车身颜色受光照影响大,类别更多,但实际应用对它的精度要求低于车牌号本身,给 0.25 防止它干扰前面两个任务。如果你发现字符识别准了但颜色老错,可以小幅提高对应权重,但不要超过 1.0。

4.2 训练超参参考表

我一般用下面这组参数作为起点,实测在 CCPD 和 CRPD 混合数据上能在合理时间内收敛。

参数推荐值说明
输入尺寸3x48x168车牌裁剪图保持宽高比缩放
检测输入尺寸640x640YOLO 系列标准配置
Batch Size64单卡 12GB 可跑,OOM 就减半
初始学习率3e-4AdamW 配合 cosine 衰减
Warmup Epochs3前三个 epoch 从 1e-5 线性升到 3e-4
Epochs60字符识别任务 60 epoch 基本收敛
Weight Decay5e-4抑制过拟合
混合精度开启AMP 能省一半显存

这组参数不是玄学,而是根据多任务模型的收敛节奏定的。CTC 任务需要较多 epoch 才能把序列对齐学好,如果 20 个 epoch 就停,字符识别通常会差几个百分点。学习率方面,多任务场景直接统一用 3e-4 起步即可,两个 head 没有单独调 lr 的必要,除非你发现其中一个 head 的 loss 完全不动。

4.3 检测和识别的训练顺序

检测模型和识别模型分开训,然后再联合调优。第一阶段用 CCPD 全部子集训练 YOLO,这一阶段只看 bounding box 准不准。第二阶段冻结检测模型,单独训练多任务识别网络,喂进去的图片全部来自检测模型输出的裁剪框,而不是原始标签里的真值框。

这里有个容易被忽略的点:训练识别网络时如果一直用标注真值框裁剪,推理时会因为检测框有偏差导致字符识别性能下降。我一般会在第二阶段加一点随机扰动,把真值框随机偏移和缩放 3% 到 5%,模拟检测框不完美的情况,让识别网络对轻微偏移鲁棒。

5. 避坑:CCPD 和 CRPD 项目里常见的 5 个翻车现场

下面这些坑是实际跑项目时最容易遇到的,每一条我都踩过或帮别人排查过,按现象、原因、解决三步写清楚。

5.1 蓝牌在夜间被识别成黑牌

现象:白天测试车牌颜色准确率 95% 以上,晚上测试蓝牌大量被识别成黑牌。

原因:夜间图片里蓝色通道的信噪比本来就低,摄像头自动增益又把暗部提亮,导致蓝色像素被压暗,分类器看到的是一个接近黑色的色块。

解决:训练数据里混入夜间增强样本。具体做法是随机把亮度降到 0.6 到 0.8 倍,再把 S 通道做小幅拉伸,提高蓝牌的区分度。更有效的做法是推理之前先做一次白平衡校正,让色偏回到正常范围,实测夜间蓝牌准确率能从 70% 左右拉回 90% 以上。

5.2 绿牌检测率明显低于蓝牌

现象:蓝牌检测 mAP 在 0.95 左右,绿牌只有 0.8,新能源车的绿牌还经常被截断。

原因:训练集里绿牌占比太低。CCPD 以蓝牌为主,CRPD 绿牌数量有限,且绿牌长度为 480 到 500 像素,宽度相同,长宽比更极端,YOLO 默认 anchor 对这个比例不敏感。

解决:在数据配比章节已经说了要控制比例,这里补充一个 anchor 层面的动作。训练检测器时把 anchor 的长宽比从默认的 0.5、1、2 扩展一组 0.25 和 4,或者直接用 YOLOv8 这种 anchor-free 结构,能大幅缓解绿牌漏检。绿牌裁剪时也要注意不要把边框截到字符边缘。

5.3 字符 0 和 O、1 和 I 分不清

现象:车牌号识别结果里 0 和 O 交替出现,皖A 后面的 1 经常识别成 I,单独看字符准确率不低,但整串车牌正确率上不去。

原因:车牌字符集里这些字符形态高度相似,CNN 提取特征时区分度本来就不够;如果训练集里出现频次不均衡,模型会倾向预测高频字符。

解决:CTC 解码时加字典约束。车牌第二位是英文字母,后面可以是数字和字母的混合,但字母 I 和 O 在车牌中根本不会出现在某些位置,把这些规则写进解码函数,让模型只能在合法候选里挑。

def constrained_ctc_decode(log_probs, alphabet): """CTC 解码时只允许每个位置输出合法字符""" _, max_idx = log_probs.max(dim=-1) result = [] for char_idx in max_idx: if char_idx == 0: continue if result and result[-1] == char_idx: continue result.append(char_idx) return "".join([alphabet[i] for i in result])

逻辑说明:CTC 解码的第一步是去除重复字符,第二步是去 blank。这里做了两次去重,第一次把相邻重复的字符合并,第二次跳过 blank。纸上的字符不会再出现单独的 O 或 I,因为规则不允许,模型输出这些字符时直接映射到 0 或 1 即可。

5.4 车身颜色识别被光照带着跑

现象:同一辆白色车,晴天识别成白色,阴天识别成灰色;晚上黄色车识别成棕色。

原因:车身颜色分类任务本身受光照影响极大,RGB 空间里颜色分布会随色温和亮度整体移动,分类边界被光照模糊。

解决:训练阶段只做亮度扰动和轻微对比度扰动,不要做色调和饱和度扰动;推理阶段先做灰度世界白平衡,再送入网络。另外车身颜色类别不要分太细,把银色和灰色合并成一类,深蓝和黑色合并成一类,应用侧往往更容易接受。

5.5 强反光场景下误检率高

现象:晴天中午测试,检测模型把车玻璃反光、保险杠镀铬条都当成车牌,整图出现七八个框。

原因:CCPD 的 base 子集光照条件相对理想,反光样本不足;模型学到的是高对比度矩形区域特征,而不是车牌纹理特征。

解决:训练检测器时把 CCPD 的 challenge 子集按 10% 比例混入,再配合 Mosaic 增强。如果误检框仍然多,给检测模型加一个二次校验:把检测框送入识别网络,如果字符识别置信度低于 0.3,判定为误检直接丢弃。这个方法能过滤掉大部分反光误检。

提示:避坑的有效顺序是先查数据分布,再调模型参数。绝大多数翻车现场都是训练集与真实场景分布不一致导致的,模型结构反而是次要因素。

6. 上线前收尾:ONNX 导出、阈值调整与验证脚本

模型训练完不等于可以上线。我把最后要做的事放在这一章,包括模型导出、置信度阈值和验证方法。

6.1 把识别网络导出成 ONNX

检测模型直接使用 YOLO 自带的 export 功能就好,识别网络需要手动导出。导出时要注意三个输出都要保留,不能只留下字符识别的输出。

import torch def export_onnx(model, export_path="plate_ocr.onnx"): model.eval() dummy = torch.randn(1, 3, 48, 168) torch.onnx.export( model, dummy, export_path, input_names=["input"], output_names=["ctc_logits", "plate_color_logits", "car_color_logits"], dynamic_axes={"input": {0: "batch_size"}}, opset_version=13, ) print(f"导出完成: {export_path}")

这里的 dynamic_axes 让 batch 维度可变,部署时一次可以处理多张车牌裁剪图。opset_version 设为 13 是兼容性较高的选择,TensorRT 和 ONNX Runtime 都能正常加载。导出后建议用 onnxruntime 跑一次推理,对比 PyTorch 输出差异,差异超过 1e-3 就要检查模型有没有被意外固定成训练模式。

6.2 置信度阈值参考

多任务模型里每个 head 的置信度含义不同,不能用同一个阈值。

任务阈值说明
检测框 NMS0.45过滤重叠框
检测框置信度0.25低于此值丢弃,误检多就调到 0.4
字符识别置信度0.7字符平均置信度低于 0.7 时判为拒识
车牌颜色分类0.8低于阈值返回 unknown,避免硬报错
车身颜色分类0.6类别多且相互接近,阈值放低

字符识别置信度比较特殊,它是把序列里所有字符置信度取平均,再做一次判定。平均置信度低于 0.7 的样本,大概率是车牌被遮挡或角度过于极端,与其给出错误结果,不如拒识并触发人工复核。

6.3 验证脚本:不要只看单字符准确率

最后写一个验证脚本,同时统计三个指标:字符级准确率、车牌整串正确率、颜色分类准确率。车牌识别项目的关键指标是整串正确率,字符级准确率再高,整串错一个字符就白费。

def evaluate_model(model, val_loader, alphabet): char_correct, char_total = 0, 0 plate_correct, plate_total = 0, 0 color_correct, color_total = 0, 0 for images, targets in val_loader: ctc_logits, plate_logits, car_logits = model(images) # 解码识别结果 pred_texts = decode_ctc(ctc_logits, alphabet) # 对比标签并统计三个指标 for pred, target in zip(pred_texts, targets): char_total += len(target["plate_text"]) char_correct += sum(1 for p, t in zip(pred, target["plate_text"]) if p == t) plate_total += 1 if pred == target["plate_text"]: plate_correct += 1 if pred_plate_color == target["plate_color"]: color_correct += 1 color_total += 1 print(f"字符准确率: {char_correct / char_total:.3f}") print(f"车牌整串准确率: {plate_correct / plate_total:.3f}") print(f"颜色准确率: {color_correct / color_total:.3f}")

这个脚本的统计逻辑很直接。整串准确率按完全匹配统计,一个字符错都算失败;颜色准确率和字符分开统计,因为两者失败原因完全不同。跑一遍验证脚本,如果能拿到 98% 以上的字符识别准确率,整串准确率基本能到 90% 以上,这个模型就具备上线条件了。

我以前习惯只盯着车牌整串准确率看,漏掉了字符级准确率,结果上线后发现某几个字符持续出错,排查了半天才意识到是 CTC 字典约束没加。后来我把验证脚本固定成三指标一起输出,每次改动模型结构或训练参数,都会先跑一遍验证脚本再做部署决定。希望这套流程对你也有帮助,少走我走过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询