基于深度学习的车牌识别实战:CCPD/CRPD数据集与多任务模型搭建
简介基于深度学习与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, encodingutf-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, encodingutf-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 Lossplate_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(pretrainedTrue) backbone.fc nn.Identity() self.backbone backbone # 将 ResNet 输出的特征图按宽度方向展开成序列 self.lstm nn.LSTM(512, 256, bidirectionalTrue, batch_firstTrue) 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 放在索引 0CTC Loss 默认 blank 为 0这一点忘了设置会导致 loss 完全混乱。3.3 车牌区域预处理归一化高度别动颜色通道车牌输入到识别网络之前预处理细节会直接影响三个任务的准确率。我的做法是先把检测框裁剪出来再按高度归一化。import cv2 def preprocess_plate_crop(crop_img, target_h48, target_w168): 车牌区域预处理保持宽高比缩放后补边再归一化 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, dtypenp.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(blank0) 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-4Epochs60字符识别任务 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_pathplate_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_version13, ) 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 字典约束没加。后来我把验证脚本固定成三指标一起输出每次改动模型结构或训练参数都会先跑一遍验证脚本再做部署决定。希望这套流程对你也有帮助少走我走过的弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →