☰
基于YOLOv5+CRNN的中文车牌识别系统搭建实战与避坑指南
2026/10/1 23:46:50 网站建设 项目流程

简介:这是一份基于YOLOv5与CRNN的中文车牌识别系统完整源码包,附带详细文档和全部项目资料,主要面向人工智能、计算机视觉及通信工程等专业的学生、教师和开发者,可解决复杂场景下车牌检测与中文汉字/字母识别问题,适用于毕业设计、课程设计、作业或项目初期演示。压缩包共92个文件,整体仅7.65MB,内容涵盖Python源码、YAML配置文件、训练好的pth/pt权重、多种测试图片以及说明文档,覆盖车牌检测、字符识别、颜色识别和GUI交互等完整功能模块。目前已有113人学习下载。项目内除训练、测试与推理脚本外,还提供数据配置、模型导出(torch2trt加速)等实用工具,配合清晰的目录结构和README,便于深入理解YOLOv5目标检测与CRNN序列识别协同工作的原理。所有代码经过运行验证,可直接部署或二次扩展,对需要快速搭建车牌识别应用的中高级学习者颇具参考价值。

1. 基于 YOLOv5 + CRNN 的中文车牌识别系统,为什么值得自己搭一套

基于 YOLOv5 + CRNN 的中文车牌识别系统,是近几年课程设计、毕业设计和车辆视觉项目里出现频率非常高的一个组合。它的做法很直白:先用 YOLOv5 在整张画面里定位车牌位置,再把裁剪出来的车牌图交给 CRNN,让它把“京A12345”这类字符串读出来。两段式最大的好处是检测和识别完全解耦,哪一环不准就单独换哪一环,不用把定位误差和字符识别误差揉在同一个模型里互相影响。

这套方案能落地到停车场出入口、校园车辆统计、园区闸机等场景,也能在拿到带源码、文档和训练资料的项目包后快速复现并做二次开发。下面这篇实战笔记会从原理拆到环境配置、数据集制作、训练命令,再讲我踩过的几个高频坑,最后给出一套能直接用的验证和部署方法。

2. 先定位再读字符:YOLOv5 与 CRNN 的中文车牌识别分工

2.1 车牌检测为什么用 YOLOv5 而不是传统 OpenCV

不少老项目还在用颜色阈值加轮廓提取的方法做车牌检测:先框出蓝色像素区域,再找外接矩形。固定角度的卡口机位下这套做法确实能用,但一旦换成停车场入口、路边临停、夜间补光不足或者车牌表面有泥点污渍,HSV 阈值就会把车身边缘、路牌甚至反光条一起当车牌框出来。更麻烦的是,传统方法很难自己适配不同省份不同底色的车牌,绿色新能源牌、白色警牌、黄色教练车牌都要重调一轮参数。

YOLOv5 属于 anchor 类单阶段目标检测器,主干用 CSPDarknet 做下采样,颈部用 PANet 做特征融合,对画面里小尺寸车牌有不错的召回。做“YOLOv5 训练自己的数据集”时,只需要把车牌标成一个矩形框,单一类别,标注成本远低于四点角点方案。推理时 YOLOv5 会在多个尺度输出候选框,再通过 NMS 去掉重叠框,最终留下高置信度检测结果。

车牌本身又是画面里很“显眼”的目标:大部分车牌底色和车身对比强,形状宽高比固定,所以推理时可以把置信度阈值设得偏高一些。我一般会把conf_thres设在 0.5 左右,iou_thres设在 0.45,这样停车场的远距离小目标不会因为阈值过高被丢掉,误检也会被压住。

2.2 CRNN 如何解决中文车牌的变长字符

中文车牌不是一组规整的等宽字符,而是由省份简称汉字、发牌机关字母、序号字母数字组成的混合序列。常见蓝牌是 7 位,新能源绿牌是 8 位,传统先切割后识别的流程一旦遇到字符粘连、二值化断笔、边框铆钉干扰,切分位置就会错位,后面的识别再强也没用。

CRNN 的处理方式是把整行车牌图当作一个序列来建模:卷积层负责提取字符纹理特征,双向 LSTM 负责建模字符之间的顺序依赖,最后用 CTC Loss 计算损失。CTC 的妙处是不需要知道每个字符的确切边界,只要给出“这一整行是什么文本”,模型自己会在时间步上寻找对齐关系,所以对不定长车牌识别特别合适。

真正让中文车牌识别难的不是模型结构,而是字符表。省份简称“京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新”这 31 个字里,不少和字母、数字的视觉特征接近,比如“京”和“示”容易混。字符表的顺序一旦固定,训练和解码就必须始终一致,这是后文踩坑部分会展开讲的重点。

2.3 整条流程:检测、裁剪、矫正、识别、后处理

一套可运行的 YOLOv5 + CRNN 车牌识别链路,通常由六步组成:输入图像、YOLOv5 检测车牌框、按框裁剪、对倾斜车牌做透视矫正、CRNN 识别字符、最后按车牌规则做后处理。

检测模型输出的轴对齐矩形框在多数正面场景够用,但车辆转弯或者相机装在侧面时,轴对齐框会把车身边缘和地面阴影裁进来,CRNN 会把这些背景纹理当成字符噪声。常见做法是拿检测框内部的二值化结果再做一次cv2.minAreaRect(),得到带角度的最小外接矩形,然后用仿射变换把车牌转正并统一缩放到固定宽高,比如高 32、宽 168 或 240。这一步对最终识别率的影响比我预想中更大。

后处理则是用已知规则给模型输出兜底:第一位必须是省份汉字,第二位必须是大写字母,其余位只能来自字母数字集合,长度必须是 7 或 8。规则不能写得太死,否则会把模型本来识别对的新能源 8 位车牌误杀。

3. 搭建可复现的车牌识别系统:环境配置、数据集制作与训练命令

3.1 conda 建环境还是直接 pip:YOLOv5 环境配置的取舍

很多源码包里的文档会把环境配置一笔带过,实际复现时大部分时间都耗在这。我的习惯是先用 conda 建独立环境,避免把系统 Python 装乱,也给后面切换不同 torch 版本留后悔药。

conda create -n plate python=3.8 -y conda activate plate # 先确认本机显卡驱动支持的 CUDA 版本,再选 torch;下面是常见组合 pip install torch==1.13.1 torchvision==0.14.1 # 进入 YOLOv5 源码目录后安装依赖 pip install -r requirements.txt

这段命令的前提是你已经把 YOLOv5 官方源码拉到了本地工作目录。python=3.8不是必须的,3.9、3.10 通常也能跑,但很多第三方依赖的老版本在 3.8 下最省事。PyTorch 版本不要随手装最新版,先跑nvidia-smi看驱动支持的 CUDA 版本,再去 PyTorch 官网选对应安装命令;如果只是为了先验证代码流程,CPU 版也能训练小数据集。

装完依赖后一定要做一次冒烟测试:在 YOLOv5 目录下用官方图片跑一次python detect.py。如果这一步就报torch.cuda.is_available()为 False,说明 torch 和驱动不匹配;如果 import 报缺少包,优先看requirements.txt里的版本是否和 Python 版本冲突,而不是盲目升级所有包。

3.2 车牌检测数据集:VOC 标注转 YOLO 格式

检测模型只需要做一件事:找车牌。所以数据标注时类别只有一个plate。常见标注工具 LabelImg 默认导出 VOC 格式的 XML,YOLOv5 训练需要的是归一化后的 txt 文件,转换脚本不难写。

import xml.etree.ElementTree as ET import os def voc2yolo(xml_path, out_dir, class_map): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_map: continue box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") xml_name = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, xml_name + '.txt'), 'w') as f: f.write('\n'.join(lines))

这段脚本把所有坐标统一除以图片宽高,转成 0 到 1 之间的相对值,这正是 YOLO 格式的要求。class_map在单类别项目里就是{'plate': 0}。转换完要抽查:打开一张原图,把 txt 里的坐标画成矩形,确认没有出现 x 中心点超出边界、宽高为 0 这类脏标签。数据分布上,近景车牌和远景车牌、白天和夜间、倾斜和端正都要有一定比例,检测模型才能在不同停车场场景下都稳定。

3.3 yolov5 训练自己的数据集:plate.yaml 与超参数设定

数据准备好后,先写一个数据集配置plate.yaml:

path: ./datasets/plate train: images/train val: images/val nc: 1 names: - plate

path是相对于你运行训练命令的工作目录来的,建议用绝对路径,避免在不同机器上复现时报找不到图片。nc是类别数,车牌检测只有一个类别。接下来训练命令可以这样写:

python train.py \ --data plate.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp data/hyps/hyp.scratch-low.yaml \ --patience 20

--weights yolov5s.pt表示加载 COCO 预训练权重。车牌检测属于中等目标,从预训练权重继续微调,比从头训练收敛快得多,mAP 也更高。--img 640是训练输入尺寸,车牌在停车场画面里通常不会太小,640 够用,也更好换到 CPU 推理。--batch 16要看显存,显存小的机器降到 8,并搭配更小模型yolov5n。--hyp控制数据增强和优化器超参数,hyp.scratch-low的增强强度更低,适合中小规模车牌数据集;如果数据集很大,可以换成hyp.scratch-high提高泛化。训练中期如果验证集的 mAP 连续 20 轮不升,--patience 20会自动早停,不用一直干等。

3.4 训练 CRNN 识别模型:字符表、网络结构与 CTC Loss

CRNN 这侧第一步是定义字符表。字符表不是随手写一串字符,它决定模型输出层的类别数量,也决定解码时的索引映射。

# 省份汉字:30 多个省级简称,注意别漏了“藏” provinces = "京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新" # 车牌字母不用 I、O,避免和数字 1、0 混淆 letters = "ABCDEFGHJKLMNPQRSTUVWXYZ" digits = "0123456789" charset = provinces + letters + digits char_to_idx = {ch: i for i, ch in enumerate(charset)} blank = len(charset) # CTC blank 放在最后一位

字符表的顺序一旦定下来,训练、验证、推理三个环节必须读同一份索引文件。不要每次运行都现场生成,建议把charset存成 JSON 或 Python 模块,否则很容易出现“训练时能用、部署时全乱码”的黑匣子问题。

网络结构可以按简化版 CRNN 来搭:

import torch.nn as nn class CRNN(nn.Module): def __init__(self, num_classes, nc=1): super().__init__() # 输入车牌灰度图:高 32,宽 168 或 240 self.cnn = nn.Sequential( nn.Conv2d(nc, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(), nn.Conv2d(256, 512, 3, padding=1), nn.BatchNorm2d(512), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(), ) # 卷积输出高为 2,把高度并到特征通道里,宽度方向作为时间步 self.lstm = nn.LSTM(512 * 2, 256, num_layers=2, bidirectional=True, batch_first=False) self.fc = nn.Linear(512, num_classes + 1) def forward(self, x): x = self.cnn(x) # B,512,2,W b, c, h, w = x.size() x = x.reshape(b, c * h, w) # B,1024,W x = x.permute(2, 0, 1) # W,B,1024,把宽度方向当作时间步 out, _ = self.lstm(x) # W,B,512 return self.fc(out) # W,B,num_classes+1

这个结构里,卷积部分把图像从高 32 一路压到高 2,再把高和通道合并,宽度方向变成 LSTM 的时间步。LSTM 双向输出拼成 512 维,全连接层输出维度是num_classes + 1,多出来的 1 就是 CTC blank。训练时损失函数直接用 PyTorch 的CTCLoss:

import torch loss_fn = torch.nn.CTCLoss(blank=blank, zero_infinity=True)

CTC 的input_lengths在 batch 内可能不一样,所以 DataLoader 的 collate 函数里要按最长序列做 padding,并记录每个样本的真实时间步长度。target_lengths则是每条车牌标注的字符数,比如“京A12345”就是 7。

3.5 串联识别接口:后处理与推理脚本

检测模型和识别模型分别训练好之后,剩下的就是把两条链路串到一起。推理主体可以这样写:

import cv2 import torch def recognize_plate(img_path, det_model, rec_model, transform, device='cuda'): img = cv2.imread(img_path) # 1. YOLOv5 检测车牌框 results = det_model(img, size=640) boxes = results.pandas().xyxy[0] if boxes.empty: return '' # 2. 取置信度最高的框,裁剪车牌区域 box = boxes.sort_values('confidence').iloc[-1] x1, y1, x2, y2 = int(box.x1), int(box.y1), int(box.x2), int(box.y2) plate_crop = img[y1:y2, x1:x2] # 3. 统一缩放到 CRNN 输入尺寸 plate_crop = cv2.resize(plate_crop, (168, 32)) # 4. 灰度、归一化,加 batch 维度 x = transform(plate_crop).unsqueeze(0).to(device) # 5. CRNN 推理,贪心解码取每步概率最大的字符 with torch.no_grad(): out = rec_model(x) # W,1,num_classes+1 pred = out.argmax(dim=-1).view(-1).tolist() # 6. 合并 CTC 重复字符,删除 blank res = [] prev = blank_index for idx in pred: if idx != blank_index: if idx != prev: res.append(charset[idx]) prev = idx else: prev = blank_index return ''.join(res)

这里的det_model指的是已经加载好的 YOLOv5 检测模型,加载方式可以直接复用官方源码里的DetectMultiBackend,或者按你改好的 detect.py 入口来,加载逻辑不是重点。transform用ToTensor()加归一化把图像转成模型输入,注意 CRNN 输入是单通道灰度图,所以变换前要把plate_crop转成灰度。

代码里的 CTC 解码顺序是:先看每个时间步拿到最大概率字符索引,再把连续重复字符合并,同时跳过 blank。很多人的坑是先把 blank 删掉再合并重复字符,结果原本连续的相同字符被错误地合并成两个,车牌识别这类短文本里这种错很致命。最后还可以加一道宽松规则校验:第一位必须是省份汉字,第二位必须是大写字母,整体长度 7 或 8,不满足就返回空字符串。

4. 中文车牌识别避坑:5 条高频踩坑记录与排查方式

4.1 环境配置翻车:CUDA、PyTorch、YOLOv5 版本连环报错

现象:按文档装完依赖,一跑train.py就发现torch.cuda.is_available()是 False,或者训练到一半弹出CUDA out of memory。有时候同一个源码包在别人机器上好好的,到自己这里就是各种 import 报错。

原因:PyTorch 安装时没有和本机 GPU 驱动匹配,最新版 torch 不一定带对应 CUDA 的运行时;另外就是显存本来不够,batch 又开得太大。还有一个常见来源是源码包里的requirements.txt版本顺序和你本机其他项目冲突。

解决:拿到任何车牌识别源码包,先不要急着一键pip install -r requirements.txt。先nvidia-smi看驱动支持的 CUDA 版本,再按 PyTorch 官方命令装对应版本。如果机器没有 NVIDIA GPU,直接装 CPU 版 torch,用img 320、batch 8、yolov5n先把流程走通,别在第一步就卡死。

4.2 检测框歪:车牌裁剪带背景,CRNN 识别率突然掉下来

现象:YOLOv5 单独跑,检测 mAP 很高,框也基本能套住车牌;但一旦把裁剪结果送进 CRNN,识别整牌准确率从 95% 掉到 70% 左右。把裁剪图存出来看,车牌在框里明显是斜的,四个角还带着车身和地面。

原因:YOLOv5 默认输出轴对齐矩形框。车辆转弯或相机安装角度偏斜时,轴对齐框并不能反映车牌的真实四边形,裁剪图里自然全是干扰背景。CRNN 对这类噪声很敏感,因为 LSTM 会把背景纹理也当成时间步特征。

解决:在检测框内部再做一次cv2.minAreaRect(),拿到最小外接旋转矩形后,用getRotationMatrix2D做仿射变换,或者用四点透视变换把车牌转正。血泪经验是:这一步省掉的十分钟,会在后面的字符识别上十倍还回来。

4.3 字符表对不上:中文普遍乱码,特别是省份汉字

现象:CRNN 训练时 loss 正常收敛,测试时凡是带“京”字的车牌全部识别成“示”,带“鲁”的识别成“兽”,但字母和数字基本不错。

原因:训练时用的字符表顺序和解码脚本里不一致,或者每次启动代码都重新生成字符表,顺序被dict的插入顺序带偏了。CTC 输出层的索引是完全跟着字符表走的,字符表偏一位,所有汉字输出都会集体漂移。

解决:把字符表写进一个独立文件,比如charset.json,训练脚本、验证脚本、推理脚本都从同一个文件读取。注意车牌字母表去掉 I、O,避免和数字 1、0 混淆;省份汉字表保证顺序固定且包含“藏”这类低频字。真遇到形近字错,优先检查是不是字符表错位,而不是急着换网络结构。

4.4 训练不收敛或收敛太慢:预训练权重比网络结构更关键

现象:YOLOv5 训了 50 轮,mAP@0.5 还是 0;CRNN 的 loss 在前面 10 轮一直抖动不下降。

原因:很多人把--weights参数省掉,让 YOLOv5 从随机权重开始训。车牌数据集通常只有几千张,从零训练一个 CSPDarknet 注定收敛慢。CRNN 这边则是学习率设得太大,batch 又小,BN 层统计不稳定。

解决:YOLOv5 训练自己的数据集时一定加载yolov5s.pt或yolov5n.pt预训练权重。hyp.scratch-low.yaml里的初始学习率lr0=0.01先不要动。CRNN 用 Adam 时,学习率从3e-4起步,发现 loss 不降就减半,不要反复调网络宽度。

4.5 后处理正则写得太死:正确车牌被自己写的规则丢掉

现象:模型输出“京A12345”很干净,但最终返回结果为空。调试后发现是后处理里用了^[京津冀][A-Z][A-Z0-9]{5}$,把 7 位蓝牌以外的结果全部过滤。

原因:正则只考虑了传统蓝牌,忘了新能源绿牌是 8 位,也忘了车牌上可能带圆点分割符或特殊用途车牌。规则比模型还严格时,等于自己给自己设置召回上限。

解决:后处理先过滤字符集,再按位校验:第一位必须在省份表,第二位必须是大写字母,剩余位必须来自字母数字集合,长度 7 或 8。宁可在最后用推荐结果兜底,也不要写一条把所有不确定输出都杀掉的“严格”正则。

5. 验收和进阶:延时时序、指标口径与部署提速

验证车牌识别系统时,不要只拿一两张漂亮的样例图出来说效果。我一般会录一段 10 秒的停车场视频,每隔 5 帧抽一帧,把检测框、识别字符画在帧上,同时统计单帧耗时。连续帧测试能暴露检测框抖动、同牌不同结果、某个角度下突然漏检这类单张图永远看不出的问题。

import cv2 import time cap = cv2.VideoCapture('road.mp4') tm = 0.0 frames = 0 while True: ret, frame = cap.read() if not ret: break t0 = time.time() plate = recognize_plate(frame, det_model, rec_model, transform) tm += time.time() - t0 frames += 1 cv2.putText(frame, plate, (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) cv2.imwrite(f'frames/{frames:05d}.jpg', frame) print(f'avg {tm / frames:.3f}s/frame')

验收指标我会分三档看:检测 mAP@0.5 用 YOLOv5 自带的val.py输出;识别整牌准确率统计完全相等字符,另外再单独统计字符级准确率,方便定位是汉字错还是字母数字错;单帧耗时从视频脚本里直接打印。我给自己的参考线是检测 mAP@0.5 不低于 0.95,整牌准确率不低于 0.97,中等 GPU 上单帧耗时 80ms 以内。夜间场景单独统计,不要把白天和夜间混在一起算一个数。

部署阶段有两个便宜且明显的提速点。第一个是把检测和识别模型各自导出成 ONNX,用 ONNX Runtime 推理,比原生 PyTorch 少一截调度开销;第二个是对支持 FP16 的显卡用 half 精度推理,显存占用和耗时都能降。导出 ONNX 后注意 YOLOv5 输出层的张量形状会发生转置,后处理里要按导出的格式调索引,这是另一个容易翻车的地方。

我养成的习惯是每次训练完先把中间结果落盘成图,检测框、矫正图、识别结果全部画出来肉眼过一遍,再谈精度指标。很多看起来像模型玄学的问题,最后都出在字符表顺序、裁剪边界或者后处理规则上。这套 YOLOv5 + CRNN 的中文车牌识别方向很值得做,先把全流程跑通,再回头抠数据和后处理,效果会比反复换网络结构来得明显。希望帮到你。

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

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

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

立即咨询