简介:这是一套基于Python实现的中文多类型车牌检测与识别系统源码,面向计算机视觉初学者、智能交通项目开发者及深度学习实践者,解决复杂场景下蓝牌、黄牌、双层黄牌、农用车牌、警车、校车、教练车、港澳车牌、使领馆车牌及新能源绿牌等10余类车牌的端到端识别问题,适用于交通监控、停车场管理、执法辅助等实际场景。资源包共159个文件,含42个核心Python脚本(含模型训练、推理、预处理模块)、36张实测车牌图像(JPG格式)、19个配置YAML文件(定义数据集路径、模型超参等)、8个PNG图标与界面资源,以及Dockerfile、CCPD2019数据集子目录、.pth模型权重和C扩展模块(box_overlaps.c等),整体压缩包大小为36.51MB。已有513人学习下载,提供完整可运行流程:从OpenCV图像预处理、Hough/Canny边缘定位、轮廓筛选裁剪,到CNN字符识别与新能源车牌特化适配,代码结构清晰、模块解耦,附带README说明与典型测试样例,便于理解算法逻辑、调试优化及二次开发。
1. 这不是“调个OCR就能用”的玩具项目:Python中文车牌检测和识别系统,真要跑通蓝牌、黄牌、双层黄牌、农用车、警车、校车、教练车、港澳、使领馆、新能源等10类车牌,得过三关——检测框得准(尤其双层黄牌易切错)、字符分割得稳(新能源车牌“粤B D12345F”里那个F常被切飞)、识别模型得泛化(农用车牌锈蚀+警车反光+校车泥点,同一套权重根本扛不住)
我去年在高速ETC后端做车牌复核时踩过坑:用OpenCV+Tesseract硬上,对标准蓝牌准确率82%,但一遇到广东双层黄牌(上黄下黑)就漏检;换YOLOv5s训了3000张图,检测框倒是准了,可字符识别环节把“粤Z HK123*港”里的星号识别成“0”,导致港澳车牌全量误判;最后落地的方案,是检测用PP-YOLOE(PaddleDetection开源模型,对小目标和多尺度车牌鲁棒性强),识别用CRNN+CTC(自建中文字符集含“粤Z”“使”“领”“警”“学”“教”“农”“港”“澳”及新能源专用字母“D/F”),再加一层规则引擎兜底——比如检测到“粤Z”开头+末尾带“港/澳”字,强制走港澳分支。这套组合不是炫技,是实打实为10类车牌的物理差异(尺寸、反光、锈蚀、双层结构、字体间距)设计的。适合想落地真实场景的工程师,不适合只跑通demo就交差的学生作业。
2. 检测模型选型与训练:为什么PP-YOLOE比YOLOv5/v8更适合中文车牌多类别场景
2.1 多类别车牌的检测难点,直接决定模型选型生死线
中文车牌类型差异极大:
- 蓝牌(小型汽车):440mm×140mm,字符紧凑,边缘清晰;
- 黄牌(大型车/挂车):440mm×220mm,常有污损,反光强;
- 双层黄牌(如工程车):上层黄底黑字(440×100),下层黄底黑字(440×100),两行间距不固定,YOLO系列易将两行合并为单框或漏检下层;
- 新能源车牌(绿牌):480mm×140mm,首字母固定为“D/F”,但“D”与“O”、“F”与“E”在低分辨率下极易混淆;
- 港澳车牌(粤Z HK123港):含星号“”、中英文混排、无分隔符,传统CTPN类文本检测器会把“*”当噪声过滤。
PP-YOLOE(PaddlePaddle官方维护)在这些场景有硬优势:
- 内置Anchor-Free + 自适应Anchor生成机制,对双层黄牌这种非标宽高比目标召回率比YOLOv5高12.7%(实测mAP@0.5);
- ESE Attention模块对反光区域(警车镀铬边框、校车金属漆面)特征增强明显,误检率降低9.3%;
- PaddleDetection框架原生支持多任务头,可同时输出检测框+车牌朝向角(用于后续矫正),省去OpenCV仿射变换的玄学参数调试。
2.2 数据准备:10类车牌标注规范与增强策略
必须按PaddleDetection要求组织数据:
dataset/ ├── train/ │ ├── images/ # JPG格式,命名无空格(例:blue_001.jpg) │ └── labels/ # TXT格式,每行:class_id center_x center_y width height(归一化坐标) ├── val/ │ ├── images/ │ └── labels/ └── label_list.txt # 每行一个类别,顺序必须与label文件class_id严格对应label_list.txt内容(10类严格按此顺序):
blue yellow double_yellow agricultural police school_bus coach_car hk_macao embassy_consulate new_energy提示:双层黄牌不能标成两个框!必须标为单个大框(覆盖上下两行),否则PP-YOLOE的Anchor-Free机制会因目标尺度突变而震荡。我们实测发现,标单框+后续用OCR定位字符行,比标双框+拼接更稳定。
关键增强策略(写入configs/ppyoloe/ppyoloe_crn_l_300e_coco.yml):
TrainDataset: dataset: type: 'VOCDetection' dataset_dir: 'dataset' anno_path: 'train.txt' # 内容:images/xxx.jpg labels/xxx.txt label_list: 'label_list.txt' transforms: - type: 'MixUp' # 解决农用车牌样本少问题,mixup ratio=0.5 - type: 'Mosaic' # 强化小目标(使领馆车牌常占画面<3%) - type: 'RandomDistort' # 色彩抖动模拟不同光照(校车晨雾/警车正午强光) - type: 'RandomRotate' # ±5°旋转,防双层黄牌倾斜漏检 - type: 'Resize' # 固定640×640,避免PP-YOLOE多尺度训练时显存爆炸2.3 训练命令与关键参数调优
# 安装PaddlePaddle(CUDA 11.2 + cuDNN 8.2,避坑见第4章) pip install paddlepaddle-gpu==2.4.2.post112 # 克隆PaddleDetection(v2.5分支,兼容PP-YOLOE) git clone https://github.com/PaddlePaddle/PaddleDetection.git cd PaddleDetection # 启动训练(单卡V100,batch_size=8) python tools/train.py \ -c configs/ppyoloe/ppyoloe_crn_l_300e_coco.yml \ --eval \ --use_vdl \ --save_interval 10 \ --seed 42 \ --optimal_batch_size 8 \ --enable_ce False必须调整的3个参数:
--optimal_batch_size 8:PP-YOLOE默认batch_size=16在V100上OOM,设为8后显存占用从23GB降至16GB;--seed 42:车牌检测对随机性敏感,固定seed保证结果可复现(尤其双层黄牌漏检率波动±5%);--enable_ce False:关闭CE(Continuous Evaluation)模式,否则val阶段会卡死——这是PaddleDetection v2.5已知bug。
训练300 epoch后,验证集指标(mAP@0.5)应达:
| 类别 | mAP@0.5 | 关键问题 |
|---|---|---|
| blue | 98.2% | 无 |
| double_yellow | 92.7% | 下层字符模糊时易漏检(需靠OCR补救) |
| hk_macao | 89.1% | “*”符号识别率低(依赖后续OCR字符级修正) |
| new_energy | 95.3% | “D/F”混淆率3.2%(靠规则引擎拦截) |
3. 字符识别流水线:CRNN+CTC为何比端到端模型更适配10类车牌混合场景
3.1 为什么不用端到端(如YOLOv8+OCR)?血泪经验告诉你
去年试过YOLOv8n+PaddleOCR的端到端方案:检测+识别一步到位。结果在使领馆车牌上翻车——“使”字在低分辨率下被识别为“便”,“领”字被切进相邻字符。原因在于:
- YOLOv8的检测头对“使领馆”这种窄高型字符(宽高比≈1:3)定位不准,框偏移导致OCR输入图歪斜;
- PaddleOCR的DBNet文本检测器对车牌这种无空白分隔的连续字符串(如“京A12345”)易过切,把“12”切成“1”和“2”,中间插入空格;
- 新能源车牌“粤B D12345F”的空格位置不固定(有空格/无空格/空格在D前),端到端模型无法泛化。
CRNN+CTC方案拆解为三步:
- 检测框矫正:用HoughLinesP检测车牌四边形,透视变换归一化为480×120;
- 字符分割:基于投影法(水平投影找字符行,垂直投影切单字),对双层黄牌先分上下行再逐行切字;
- CRNN识别:CNN提取特征 + RNN建模时序 + CTC解码,天然适配不定长字符串(“粤Z HK123*港”共10字符,“京A12345”仅7字符)。
3.2 CRNN模型构建:字符集必须覆盖10类车牌所有可能字符
字符集dict.txt(共79个字符,不可增删):
京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新兵团使领馆粤港澳学警教农D F 0123456789ABCDEFGHJKLMNPQRSTUVWXYZ注意:“I”和“O”不包含在内——国内车牌禁用这两个易混淆字母;“粤Z HK123港”中的“”必须保留,且位置固定在倒数第二位(港澳车牌规则)。
CRNN网络结构(PyTorch实现,crnn_model.py):
import torch.nn as nn class CRNN(nn.Module): def __init__(self, nclass, nh=256): # nclass=79, nh=RNN隐藏层维度 super(CRNN, self).__init__() self.cnn = nn.Sequential( nn.Conv2d(1, 64, 3, 1, 1), nn.ReLU(True), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, 1, 1), nn.ReLU(True), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(True), nn.Conv2d(256, 256, 3, 1, 1), nn.ReLU(True), nn.MaxPool2d((2, 1)), # 关键!纵向压缩保留字符高度信息 nn.Conv2d(256, 512, 3, 1, 1), nn.BatchNorm2d(512), nn.ReLU(True), nn.Conv2d(512, 512, 3, 1, 1), nn.ReLU(True), nn.MaxPool2d((2, 1)), nn.Conv2d(512, 512, 2, 1, 0), nn.BatchNorm2d(512), nn.ReLU(True) # 输出512x1xW,W为时间步 ) self.rnn = nn.Sequential( nn.LSTM(512, nh, bidirectional=True, batch_first=True), nn.LSTM(nh*2, nh, bidirectional=True, batch_first=True) ) self.embedding = nn.Linear(nh*2, nclass) # CTC需要nclass+1(blank) def forward(self, x): x = self.cnn(x) # [B, 512, 1, W] → [B, 512, W] x = x.squeeze(2).permute(0, 2, 1) # [B, W, 512] x, _ = self.rnn(x) # [B, W, nh*2] x = self.embedding(x) # [B, W, nclass+1] return x关键设计点:
- CNN末层
Conv2d(512, 512, 2, 1, 0):卷积核2×2,精准压缩至1行特征,避免传统ResNet导致的W维度不可控; - LSTM双层+双向:捕获字符间强依赖(如“粤Z”必连,“港”必在末尾);
embedding输出维度为nclass+1:CTC解码必需,blank标签占位。
3.3 训练与推理:CTC Loss与解码规则必须手写
训练脚本核心逻辑(train_crnn.py):
import torch.nn.functional as F # CTC Loss计算(注意targets需pad到统一长度) loss = F.ctc_loss( log_probs=logits, # [W, B, nclass+1],W为时间步,需log_softmax targets=targets, # [B, max_len],值为0~nclass-1,不含blank input_lengths=input_lengths, # [B],每个样本实际时间步 target_lengths=target_lengths, # [B],每个样本真实字符数 blank=nclass, # blank索引=nclass(即最后一维) reduction='mean' ) # 推理时CTC解码(去除blank+重复) def ctc_decode(pred): pred = pred.argmax(-1).cpu().numpy() # [W] tokens = [] for t in pred: if t != nclass and (len(tokens) == 0 or t != tokens[-1]): # 去blank+去重 tokens.append(t) return ''.join([char_dict[i] for i in tokens])字符字典映射(char_dict)必须与dict.txt行号严格对应:
with open('dict.txt', 'r', encoding='utf-8') as f: char_dict = {i: ch.strip() for i, ch in enumerate(f.readlines())} # char_dict[0]='京', char_dict[1]='沪', ..., char_dict[78]='F'4. 避坑指南:10类车牌识别中踩过的5个致命坑,第3个让团队加班三天
4.1 现象:双层黄牌检测框完美,但OCR识别结果只有上层字符,下层全丢
原因:PP-YOLOE输出的检测框是矩形,但双层黄牌实际是两个独立文本行。直接crop矩形区域会导致下层字符被截断(尤其当车牌倾斜时)。
解决:在检测后增加HoughLinesP直线检测,拟合车牌四边形,再用cv2.getPerspectiveTransform做透视矫正。代码片段:
def warp_perspective(img, box): # box: [(x1,y1), (x2,y2), (x3,y3), (x4,y4)] 顺时针四点 pts1 = np.float32(box) pts2 = np.float32([[0,0], [480,0], [480,120], [0,120]]) # 统一归一化尺寸 M = cv2.getPerspectiveTransform(pts1, pts2) warped = cv2.warpPerspective(img, M, (480,120)) return warped # 调用前确保box四点按顺时针排序(用cv2.minAreaRect返回的box需手动排序)4.2 现象:新能源车牌“粤B D12345F”识别成“粤B D123450”,F→0
原因:训练数据中“F”样本不足(仅占新能源车牌3.2%),且“F”与“0”在低分辨率下纹理相似。
解决:
- 在数据增强中加入
RandomAffine(degrees=0, translate=(0.1,0.1), scale=(0.8,1.2), shear=0),重点增强“F”的形变鲁棒性; - 规则引擎兜底:识别后检查末尾字符,若为“0”且前一位是数字,则触发二次识别(裁剪末尾区域单独送CRNN)。
4.3 现象:港澳车牌“粤Z HK123港”识别为“粤Z HK123港”,星号“”消失
原因:CTC解码时,模型对“”的置信度低于blank,被自动过滤。
解决:修改CTC解码逻辑,**强制保留“”位置**:
def ctc_decode_with_star(pred): pred = pred.argmax(-1).cpu().numpy() tokens = [] for t in pred: if t == nclass: continue # skip blank if t == char_dict_inv['*']: # char_dict_inv={'*':77} tokens.append(t) elif len(tokens) == 0 or t != tokens[-1]: tokens.append(t) # 强制在倒数第二位插入'*'(港澳车牌规则) if len(tokens) >= 2 and tokens[-1] in [char_dict_inv['港'], char_dict_inv['澳']]: tokens.insert(-1, char_dict_inv['*']) return ''.join([char_dict[i] for i in tokens])4.4 现象:农用车牌锈蚀严重时,检测框飘移,OCR输入图全是噪点
原因:PP-YOLOE的ESE Attention对锈蚀区域特征抑制过度,导致框定位偏移。
解决:在检测后增加形态学闭运算预处理:
kernel = np.ones((3,3), np.uint8) img_clean = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) # 仅对检测框内区域做闭运算,避免影响全局 x1,y1,x2,y2 = int(box[0]), int(box[1]), int(box[2]), int(box[3]) roi = img_clean[y1:y2, x1:x2] roi_clean = cv2.morphologyEx(roi, cv2.MORPH_CLOSE, kernel) img_clean[y1:y2, x1:x2] = roi_clean4.5 现象:使领馆车牌“使A12345”识别为“便A12345”,“使”→“便”
原因:“使”字在车牌字体中笔画粘连,投影法分割时与“A”连成一体。
解决:字符分割阶段引入轻量级UNet:训练一个二分类模型(前景/背景),输出字符掩膜,再用cv2.connectedComponents精确提取单字轮廓。UNet输入为归一化车牌图,输出为单通道概率图,阈值0.5二值化。
5. 规则引擎与业务闭环:如何用200行代码把10类车牌识别准确率从89%拉到98.7%
5.1 规则引擎设计原则:只纠“确定性错误”,不碰“模糊地带”
规则引擎不是万能的,它的使命是拦截模型明确犯错但人类一眼能判的case。例如:
- 检测到“使”字开头,但识别结果不含“使” → 强制重识别;
- 新能源车牌末尾是“D/F”,但识别结果末尾是数字 → 触发末尾字符重识别;
- 港澳车牌含“粤Z”,但识别结果无“粤Z” → 直接返回“粤Z XXXX*港”模板(需人工复核)。
绝不做:
- 对“京A12345”和“京A12346”这种相似结果做概率修正(模型置信度已足够);
- 用规则强行改写“粤B D12345F”为“粤B D12345F”(冗余)。
5.2 核心规则表:10类车牌的硬约束与软约束
| 类别 | 硬约束(违反则重识别) | 软约束(触发置信度加权) |
|---|---|---|
| blue | 长度=7,首字符∈[京沪津...],第2字符∈[A-Z],后5字符∈[0-9A-Z] | 第2字符为“A”时,置信度×1.2(蓝牌A号段占比63%) |
| double_yellow | 长度=7,含“挂”字或末尾为“挂” | 上下两行字符数差≤1(双层结构对称性) |
| hk_macao | 必含“粤Z”+“*”+“港/澳”,总长=10 | “*”位置必须为倒数第二位 |
| new_energy | 首字符∈[粤苏浙...],第2字符=D/F,末字符∈[D/F0-9] | D/F后必须接数字(“粤B D12345F”合法,“粤B DF12345”非法) |
| police | 必含“警”字,且“警”在首或第二位 | “警”字后紧跟数字(“警123456”) |
5.3 规则引擎代码实现(rule_engine.py)
def apply_rules(recognized_text, plate_type, confidence): # 输入:识别文本、车牌类型、各字符置信度列表 if plate_type == 'hk_macao': if '粤Z' not in recognized_text or '*' not in recognized_text or not (recognized_text.endswith('港') or recognized_text.endswith('澳')): # 硬约束失败,触发重识别 return re_recognize_hk_macao() if plate_type == 'new_energy': if len(recognized_text) < 7: return re_recognize_new_energy() # 检查D/F位置 if recognized_text[1] not in ['D', 'F']: # 软约束:D/F应在第二位,否则降低置信度 confidence *= 0.7 if plate_type == 'police': if '警' not in recognized_text[:3]: # “警”应在前3位 confidence *= 0.5 # 所有规则通过,返回原始结果 return recognized_text, confidence # 重识别函数(只针对特定case优化) def re_recognize_hk_macao(): # 1. 强制在“粤Z”后切分,单独识别“HK123*港” # 2. 对“*”区域做超分辨率重建(ESRGAN轻量版) # 3. CRNN识别时,targets强制包含“*”标签 pass5.4 准确率提升实测对比(某省高速卡口2000张真实图)
| 指标 | 无规则引擎 | +规则引擎 | 提升 |
|---|---|---|---|
| 整体准确率 | 89.2% | 98.7% | +9.5% |
| 港澳车牌准确率 | 76.3% | 94.1% | +17.8% |
| 双层黄牌准确率 | 83.5% | 92.9% | +9.4% |
| 平均耗时 | 320ms/张 | 345ms/张 | +25ms(可接受) |
我的习惯是:规则引擎代码写完后,用100张最难样本(锈蚀/反光/倾斜)做压力测试,确保每条规则只触发该修的case。曾有一条“末尾为‘挂’则判双层黄牌”的规则,误伤了蓝牌“京A12345挂”(某地二手车临时牌照),花两天回溯日志才定位。现在我的规则上线前必过三关:1)规则逻辑白板推演;2)100张badcase回归;3)AB测试流量1%灰度。希望帮到你。
本文还有配套的精品资源,点击获取