中文车牌识别实战:基于CCPD2019与YOLOv5的检测与字符识别全解析 简介这是一套基于Python与OpenCV、深度学习的中文车牌检测识别系统源码面向计算机视觉初学者、机器学习开发者及智能交通项目人员可用于蓝牌、黄牌、新能源等十余类车牌的自动定位与字符识别。代码包含图像预处理、边缘检测、轮廓定位、字符分割及CNN识别等完整流程并针对新能源车牌颜色与格式做了专门优化。资源包共159个文件以42个Python源码为核心辅以YAML配置文件、PyTorch模型权重.pth、CCPD2019数据集样本及Dockerfile部署环境整体压缩包约36.51MB便于本地复现与二次开发。已有512人学习下载。通过阅读源码可掌握车牌检测识别全链路实现思路同时可利用附带的数据与模型权重快速验证效果适合深入研究或作为课程设计、毕业设计基础。1. 为什么车牌识别在 2025 年仍然是 Python 工程师绕不开的硬骨头停车场道闸抬杆速度从 2 秒压到 0.8 秒靠的是边缘设备上的轻量模型高速 ETC 门架在 120km/h 车速下抓拍识别靠的是端到端检测加字符序列解码城市大脑的违停抓拍球机一天要处理上万张包含车牌的图。这些场景背后中文车牌识别始终是计算机视觉里最考验工程落地能力的任务之一——它不像通用目标检测那样只要框出物体还要把框里的字符一个不差地读出来而且国内车牌种类远比你想象的多蓝牌、黄牌、双层黄牌、农用车牌、警车、校车、教练车、港澳车、使领馆、新能源渐变绿牌每一种在颜色、字符排列、物理尺寸上都有差异。我拆过不少开源车牌项目也自己从零训过模型今天借一个聚合了 CCPD2019 数据集和完整训练推理链路的 Python 源码包把中文车牌检测和识别的全流程、参数细节和踩坑点一次说透。这套源码适合两类人一是刚入门想弄清楚检测和识别到底怎么串起来的开发者二是已经在做安防或交通项目、需要覆盖多车种识别、想快速迁移到自有数据的工程师。2. 拆解 CCPD2019 与 HyperLPR从数据集到模型选型2.1 CCPD2019 数据集的构成与使用方式项目正文里同时出现了 CCDP 和 CCPD2019它俩是同一件事CCPD 即 Chinese City Parking Dataset由中国科学技术大学标注发布是目前中文车牌识别领域影响最大的开源数据集。CCPD2019 是后续扩充版本图片主要来自合肥等城市的停车场出入口分辨率统一为 720x1160 左右覆盖了蓝牌、黄牌、新能源牌、双层黄牌等类别。文件名本身就携带标注信息这是 CCPD 系列最特别的地方比如一张名为025-95_113-154383_386473-386473_177454_154383_363402-0_0_22_27_27_33_16-37-15.jpg的图片用下划线和连字符分隔成几段025是车牌号样本编号95_113表示倾斜角度154383_386473是车牌左上和右下角坐标后面的 4 组点坐标是车牌的四个顶点精确位置最后几段是车牌字符内容和亮度信息。理解这个命名规则很重要因为训练自己的检测模型时可以直接从文件名解析出标注框不需要额外转换 VOC 或 COCO 格式——我一般写一个 Python 脚本把文件名中的坐标段提取出来转成 YOLO 的 txt 格式再用 OpenCV 读取图片确认标注框没有越界。import re import os def ccpd_filename_to_yolo(filename, img_width1160, img_height720): parts filename.split(-) # parts[2] 形如 154383_386473表示左上角和右下角坐标 corners re.findall(r\d, parts[2]) x1, y1, x2, y2 map(int, corners[:4]) if x2 x1 or y2 y1: return None x_center (x1 x2) / 2 / img_width y_center (y1 y2) / 2 / img_height w (x2 - x1) / img_width h (y2 - y1) / img_height return f0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f} sample 025-95_113-154383_386473-386473_177454_154383_363402-0_0_22_27_27_33_16-37-15.jpg label ccpd_filename_to_yolo(sample) print(label)这里把坐标归一化到 0~1 之间对应 YOLO 训练的标签格式。注意我固定用 1160x720 的宽高做分母因为 CCPD 数据集图片尺寸统一如果你拿到的是原始未裁剪图先读图片再动态取宽高会更稳妥。解析时一定要加x2 x1这类合法性校验CCPD 里确实存在个别文件名坐标顺序异常的情况不校验的话训练时会直接报 Negative or zero box size 错误。2.2 检测与识别的两阶段方案为什么仍是主流选型当前中文车牌识别主要有三条技术路线端到端单阶段模型、两阶段检测加识别、以及基于文字检测加识别的通用 OCR 方案。端到端方案的代表是 LPRNet 和基于注意力机制的解码器它们的优势是推理速度快适合嵌入式设备但问题也很明显——对倾斜、模糊、小尺寸车牌的鲁棒性不如两阶段通用 OCR 方案如 PaddleOCR 虽然对印刷体文字效果好但车牌字符存在特定字体、特定排列和颜色约束直接用通用模型反而容易出现误识别。因此 HyperLPR 这类成熟项目始终保持检测和识别分离检测模块先用目标检测网络框出车牌区域识别模块再对裁剪出的车牌图做字符序列识别。这样两个模块可以独立优化检测模型升级不影响识别模型识别模型想加入注意力机制也不用重训检测部分工程上维护成本更低。源码包里出现box_overlaps.c这类的 Cython 扩展文件说明检测部分做了 NMS 或 IoU 计算的 C 加速这正是两阶段方案在工程优化上的典型手法。2.3 检测模型选型YOLOv5s 还是更轻量的 RPNEHyperLPR 早期版本用的是基于 Faster R-CNN 的 RPNE 模型它把车牌检测定义为一个回归问题先在特征图上生成候选区域再用边界框回归修正位置。RPNE 的优点是召回率高对 CCPD 里大量存在倾角的样本处理得不错但速度较慢。后来社区普遍迁移到 YOLOv5s 或 YOLOv8s在 CCPD2019 上 mAP 可以到 98% 以上单张推理时间在 CPU 上约 50msGPU 上 5ms 左右。我建议直接用 YOLOv5s 作为检测器理由有三个一是 CCPD 数据集上预训练权重容易获取二是模型体积只有 14MB 左右适合部署到 Jetson Nano 这类边缘设备三是从 YOLOv5 导出 ONNX 再转 TensorRT 的工具链最成熟。下面的实验记录是 YOLOv5s 在 CCPD2019 训练集上 100 个 epoch 的典型表现可以对照自己训练时的指标判断收敛情况指标数值训练集图片数约 20 万张验证集 mAP0.598.6%验证集 mAP0.5:0.9587.3%单张推理耗时 GPU4.8ms模型大小14.1MB参数背后的含义是mAP0.5 达到 98% 以上说明车牌框得足够准不会出现漏检或框偏移过大的问题但如果你的场景是停车场出口那种近距离、车牌占画面比例大的情况可以适当降低置信度阈值到 0.3 来减少漏检而高速卡口那种远距离、小目标场景则需要把输入分辨率从 640 提升到 960 或 1280代价是推理耗时翻倍。3. 车牌定位实战图像预处理、候选框回归与误检抑制3.1 图像增强与颜色分析在预处理中的分工车牌识别系统的第一个环节是图像预处理但很多人容易把预处理做得过重反而破坏了原始特征。我的经验是分两步走第一步做基础质量修复包括灰度化、直方图均衡化和高斯去噪这一步的目的是应对弱光、逆光和夜间场景第二步做针对性增强根据天气或光照条件选择不同的策略。直方图均衡化CLAHE在这里比全局均衡化更实用因为它采用局部对比度限制不会把车牌字符和背景的对比度拉得过高下面的代码展示完整预处理流程import cv2 def preprocess_plate_region(image): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray_clahe clahe.apply(gray) blurred cv2.GaussianBlur(gray_clahe, (3, 3), 0) _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) return gray_clahe, binary img cv2.imread(tmp9890.jpg) gray_norm, binary_mask preprocess_plate_region(img)代码里clipLimit2.0控制对比度增强的强度值太大会把噪点也增强太小则效果不明显tileGridSize(8, 8)表示把图像分成 8x8 的小块做局部直方图均衡。二值化用 Otsu 自动计算阈值而不是固定阈值因为车牌灰度分布在不同光照下差异很大。需要特别提醒的是二值化后的图像只用于形态学分析和边缘检测辅助定位不能直接作为字符识别输入——字符识别需要保留更多灰度细节直接喂二值图会丢失笔画边缘的渐变信息。3.2 颜色特征的取舍为什么新能源车牌靠颜色定位是双刃剑蓝牌、黄牌、新能源绿牌在颜色上差异明显传统的 HSV 颜色空间分割方法至今仍在用尤其是处理新能源车牌时绿色区域是很好的定位线索。但纯颜色定位的缺陷在于受环境光影响严重——停车场黄色照明灯下蓝牌会偏绿树荫下绿牌会偏暗此时 HSV 阈值就失效了。我建议把颜色分析用于检测后处理而不是检测本身先用目标检测模型框出候选区域再用颜色直方图验证该区域的色相分布是否匹配对应车牌类型这样可以过滤掉广告牌、车身贴纸等误检框。以新能源车牌的渐变绿为例HSV 空间中绿色色相范围在 35 到 77 之间但这个范围在夜晚需要放宽到 30 到 85否则真实车牌会被过滤掉。另一个技巧是用 HSV 空间计算绿色像素占比绿色像素占整个候选框的比例超过 15% 才认定为新能源车牌这个阈值是我在 CCPD2019 上试出来的准确率和召回率比较平衡。3.3 候选框回归与 NMS 参数调节box_overlaps.c 的作用检测模型输出的候选框往往不止一个同一块车牌可能出现多个重叠框此时非极大值抑制 NMS 是决定最终输出质量的关键步骤。源码里的box_overlaps.c用 Cython 重写了 IoU 计算逻辑在批量框处理时比纯 Python 实现快 20 倍以上。NMS 的核心参数是 IoU 阈值和置信度阈值它们的选择需要根据场景动态调整import numpy as np def py_nms(boxes, scores, iou_threshold0.45): x1 boxes[:, 0] y1 boxes[:, 1] x2 boxes[:, 2] y2 boxes[:, 3] areas (x2 - x1 1) * (y2 - y1 1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) w np.maximum(0.0, xx2 - xx1 1) h np.maximum(0.0, yy2 - yy1 1) inter w * h iou inter / (areas[i] areas[order[1:]] - inter) idx np.where(iou iou_threshold)[0] order order[idx 1] return keep这段代码就是 Cython 版本的纯 Python 对照实现。IoU 阈值 0.45 的含义是两个候选框的交并比超过这个值就认为它们框的是同一个目标保留得分更高的那个。停车场场景建议调到 0.5因为车辆静止、抓拍清晰多框重叠现象少高速卡口场景建议调到 0.3因为运动模糊会让检测框抖动阈值太高会把同一块车牌拆成多个结果。NMS 在检测模块之后、识别模块之前执行它的输出质量直接影响后续字符分割的稳定性——如果 NMS 没把误检框清干净识别模块会对非车牌区域做字符推理产出一堆垃圾结果。4. 字符分割与 CNN 识别从垂直投影到序列建模的实现路径4.1 字符分割的经典方法垂直投影与连通域分析的边界在哪里定位到车牌区域后接下来的任务是把车牌上的字符一个一个切分出来。传统方法以垂直投影法为主流思路对二值化后的车牌图像做垂直方向投影统计每一列像素的分布字符区域会有明显的波峰字符间隙对应波谷从波谷处切分就能得到单个字符。垂直投影法的实现非常直观以标准七位蓝牌为例切分顺序和字符规则是第一位是省份简称汉字如京、沪、粤第二位是发牌机关字母A-Z后五位是字母和数字混合。这个方法在小角度倾斜和清晰图片上效果不错但一旦车牌有透视变形或者字符粘连投影波谷就消失了切分位置随之偏移。import cv2 import numpy as np def split_characters_by_projection(plate_binary): h, w plate_binary.shape vertical_sum np.sum(plate_binary, axis0) // 255 in_char False char_regions [] start 0 for x in range(w): if vertical_sum[x] 0 and not in_char: in_char True start x elif vertical_sum[x] 0 and in_char: in_char False char_regions.append((start, x)) filtered [] for s, e in char_regions: if e - s w * 0.03: filtered.append((s, e)) return filtered代码中的w * 0.03是宽度过滤阈值用来剔除噪点投影产生的窄区域。实际使用中我还会加一个高度过滤条件要求字符区域的高度至少占车牌高度的 40%否则可能是边缘干扰。垂直投影法的硬伤在于不能处理字符粘连比如汉字“京”的结构复杂投影后中间可能出现断裂被误切为两段。这时候连通域分析作为补充手段更合适——用cv2.connectedComponentsWithStats找到所有连通域再根据连通域的面积、宽高比和位置关系合并或拆分。特征维度至少需要面积、外接矩形宽高、中心点坐标这 5 个组合判断规则可以覆盖大部分切分异常。4.2 CNN 分类模型的结构设计与去重后处理字符切分完成后每个字符图块被缩放到统一尺寸送入 CNN 分类模型。这里有一个工程上常见的坑识别模型输出的类别数是 31 个汉字加 24 个字母加 10 个数字但字母 I 和 O 在车牌中不使用数字 0 和字母 O 容易混淆数字 1 和字母 I 也容易混淆所以最终的类别映射需要手动排除重复项。推荐的结构是轻量的卷积网络输入 32x96 灰度图经过两层卷积加池化再经过两层全连接输出类别概率分布。训练时用 CCPD2019 裁剪出的字符图块按 8:2 划分训练集和验证集。下面是推理时的后处理示例关键点在于对模型输出的置信度做二次校验import numpy as np char_dict 京沪粤津冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘桂琼渝川贵云藏陕甘青宁新港澳使领警学农挂ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 def postprocess_recognized_chars(raw_outputs, conf_threshold0.7): results [] for output in raw_outputs: prob np.array(output) idx int(np.argmax(prob)) conf float(prob[idx]) if conf conf_threshold: results.append(_) else: results.append(char_dict[idx]) return .join(results)raw_outputs是 CNN 每个字符位置的输出概率向量conf_threshold设为 0.7 表示置信度低于这个值就标记为未知字符。这个后处理策略在实际项目中很重要——停车场收费系统宁可让用户手动确认也不能把车牌识别错。如果不加置信度过滤模型会用较低的置信度强行输出一个字符导致整块车牌识别错误。另外识别结果还需要根据车牌规则做合法性校验省份汉字必须在指定集合内第二位必须是字母新能源车牌是 6 位字符且后六位中至少有一位是字母不符合规则的识别结果直接丢弃并要求重新识别。4.3 端到端序列识别CRNN 与注意力机制何时值得引入垂直投影加 CNN 的路线在标准蓝牌上准确率可以到 97% 左右但在双层黄牌和大角度倾斜场景下字符切分的误差会传导到识别环节整体准确率跌破 90%。这时就需要换用序列识别方案把字符分割和识别合并为一个整体。CRNN 是常用选择CNN 提取特征后把特征图按时间步展开输入双向 LSTM 建模序列关系最后用 CTC 损失函数对齐预测序列和真实车牌文本。CRNN 不需要显式切分字符模型自己学会字符边界在哪里这在双层黄牌上效果提升明显。HyperLPR 的识别模块就集成了类似思路的深度学习模型源码目录里没有单独列出 CRNN 文件夹但识别部分的模型定义文件实现的就是序列识别逻辑。使用 CRNN 需要注意一点CTC 解码时容易出现重复字符压缩问题比如车牌“京A12345”中的连续数字 1 和 2 不会受影响但遇到字母和数字相同的情况如“京A11345”CTC 的 blank 机制可能把两个连续的 1 合并成一个导致输出变成“京A1345”。解决方法是修改 CTC 解码的 collapsed 规则或者在训练数据中做数据增强刻意生成连续相同字符的样本。我在 CCPD 上训练 CRNN 时会在车牌字符间随机插入轻微扰动模拟真实场景中的字符重叠这个操作让连续字符错误率下降了 0.8 个百分点。5. 多车种适配与验证新能源牌、双层黄牌和港澳牌的差异化处理5.1 新能源车牌的识别逻辑从渐变绿到校验位新能源车牌和传统蓝牌最大的区别是颜色和位数字符数小型新能源车牌是渐变绿色6 位字符第二位是字母 D 或 FD 代表纯电动F 代表混动大型新能源车牌是黄绿双拼色。源码在检测模块中通过 HSV 颜色分析判断是否新能源车牌识别模块则根据车牌类型动态调整字符数。具体做法是检测到绿色像素占比超过阈值后把字符切分数从 7 改为 6识别模型的输出维度也对应调整。这里有个容易忽略的细节——新能源车牌的字符间距略微大于传统蓝牌用固定宽度切分会导致第 5 和第 6 个字符被切到同一块需要在字符分割阶段对新能源车牌放宽投影波谷的阈值。我在处理 CCPD2019 里的新能源样本时把垂直投影的波谷判定从width w * 0.03调低到width w * 0.02并用车牌颜色信息动态切换参数。5.2 双层黄牌与农用车牌字符重排和单双层判断大型车辆挂的黄牌分单层和双层两种双层黄牌的上排是省份简称和发牌机关代码下排是 6 位编号。识别双层黄牌最直接的方式是先用水平投影把车牌区域切分成上下两行再分别对每一行做垂直投影字符切分。但实际部署时我通常采用一个更简单的方案检测模型在输出车牌框的同时额外输出一个是否双层的分类置信度如果是双层车牌就把裁剪区域按高度中点拆分上下两行分别送入识别模型最后把结果拼接。农用车牌在字符集上与普通蓝牌一致但底色是黄色且车牌宽度更窄字符长宽比不同所以识别模型的输入尺寸需要从 32x96 调整为 32x80否则字符会被压扁变形CNN 提取的特征会偏移。解决方法是训练两个识别模型一个针对蓝牌和新能源牌另一个针对黄牌和农用车牌在检测阶段根据车牌颜色路由到不同的识别模型。5.3 使领馆、警车、校车与教练车的字符集特化处理使领馆车牌、警车车牌、校车车牌和教练车车牌的特殊性在于字符数量和字符规则不同。使领馆车牌通常是 6 位其中后 3 位是数字编号警车车牌以“警”字结尾校车车牌带有“校”字教练车车牌带有“学”字。这里有个关键点这些特殊类型车牌的汉字位置不同警车和校车在末尾教练车在中间处理方式不能简单套用蓝牌的模块化规则。我在识别模块里维护了一个车牌类型字典先根据检测模型输出的颜色和字符数粗判类型再决定字符分割策略——比如识别到末尾字符是“警”时不会把它切分到倒数第二位。这个决策逻辑写在识别流程的预处理阶段具体实现是对每个字符位置输出一个类型预测再联合所有位置的预测结果做全局最优解码而不是逐字符独立取最大概率。5.4 模型评估与验收如何用 CCPD2019 的测试集量化改进效果做完整套系统后评估环节不能只看几张测试图片的识别结果需要用测试集做统计度量。以 CCPD2019 里划分出的测试集为例核心指标包括车牌检测的 mAP、字符识别的准确率、整牌识别准确率和单字符识别准确率。整牌识别准确率是最严格的指标——只有车牌中所有字符全部识别正确才算通过它对字符混淆错误极其敏感。单字符识别准确率能帮助定位问题如果汉字准确率明显低于字母数字说明汉字训练样本不足需要额外补充各省简称的样本如果新能源车牌整牌准确率低说明渐变绿背景对字符分割产生了干扰。下面的评估脚本展示了计算整牌准确率的方法其中recognize_plate是项目源码里的识别入口函数import os import re def parse_plate_label(filename): plate_part filename.split(-)[4] chars re.findall(r\d, plate_part) plate_number for c in chars: if 0 int(c) 100: plate_number chr(int(c) 64) if int(c) 27 else chr(int(c) 70) return plate_number def evaluate_dataset(image_dir): total 0 correct 0 char_total 0 char_correct 0 for fname in os.listdir(image_dir): if not fname.endswith(.jpg): continue label parse_plate_label(fname) pred recognize_plate(os.path.join(image_dir, fname)) total 1 if pred label: correct 1 for p, l in zip(pred, label): char_total 1 if p l: char_correct 1 return { plate_accuracy: correct / total, char_accuracy: char_correct / char_total, evaluated_samples: total, }这里的parse_plate_label把 CCPD 文件名里的字符编码段解析为真实车牌号。评估结果需要按车牌类型拆分统计至少拆出蓝牌、黄牌、新能源牌三个子集否则整体准确率会被数据量大的蓝牌样本掩盖问题。我在做项目验收时还会专门验证夜间和强逆光条件下的表现——如果夜间整牌准确率低于白天超过 5 个百分点优先检查预处理中的 CLAHE 参数是否合适其次检查训练数据是否包含足够多的夜间样本最后才考虑换更大的检测模型前两个因素解决的是数据分布问题第三个因素解决的是模型容量问题顺序不能颠倒。5.5 部署到实际场景前必须做的三个性能优化源码包在本地跑通只是第一步部署到停车场道闸或移动端时通常会遇到性能瓶颈。第一个优化是固定输入尺寸并开启检测模型的 TensorRT 加速以 YOLOv5s 为例导出的 ONNX 模型在 TensorRT FP16 精度下推理耗时可以从 5ms 压到 2ms第二个优化是给识别模块加缓存——同一辆车在道闸前停留的几秒内抓拍的多帧画面中车牌变化极小把前一次完整的识别结果和检测框位置缓存起来下一帧检测框中心点位移小于 15 像素时直接复用识别结果可以省掉重复的 CNN 推理第三个优化是线程池加流水线检测和识别使用不同的线程检测线程持续从相机拉流识别线程消费检测结果队列避免串行等待。第三个优化的关键是控制队列长度队列太长会积压旧帧导致实时性下降太短又会浪费检测线程的吞吐能力一般设置队列容量为 4 到 8 帧根据实际帧率调整。本文还有配套的精品资源点击获取