ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于YOLOv5+CRNN的中文车牌识别系统搭建实战与避坑指南

基于YOLOv5+CRNN的中文车牌识别系统搭建实战与避坑指南 简介这是一份基于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 建环境还是直接 pipYOLOv5 环境配置的取舍很多源码包里的文档会把环境配置一笔带过实际复现时大部分时间都耗在这。我的习惯是先用 conda 建独立环境避免把系统 Python 装乱也给后面切换不同 torch 版本留后悔药。conda create -n plate python3.8 -y conda activate plate # 先确认本机显卡驱动支持的 CUDA 版本再选 torch下面是常见组合 pip install torch1.13.1 torchvision0.14.1 # 进入 YOLOv5 源码目录后安装依赖 pip install -r requirements.txt这段命令的前提是你已经把 YOLOv5 官方源码拉到了本地工作目录。python3.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 格式的 XMLYOLOv5 训练需要的是归一化后的 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.yamlpath: ./datasets/plate train: images/train val: images/val nc: 1 names: - platepath是相对于你运行训练命令的工作目录来的建议用绝对路径避免在不同机器上复现时报找不到图片。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 LossCRNN 这侧第一步是定义字符表。字符表不是随手写一串字符它决定模型输出层的类别数量也决定解码时的索引映射。# 省份汉字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, nc1): super().__init__() # 输入车牌灰度图高 32宽 168 或 240 self.cnn nn.Sequential( nn.Conv2d(nc, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(256, 256, 3, padding1), nn.ReLU(), nn.Conv2d(256, 512, 3, padding1), nn.BatchNorm2d(512), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(512, 512, 3, padding1), nn.ReLU(), ) # 卷积输出高为 2把高度并到特征通道里宽度方向作为时间步 self.lstm nn.LSTM(512 * 2, 256, num_layers2, bidirectionalTrue, batch_firstFalse) 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_classes1这个结构里卷积部分把图像从高 32 一路压到高 2再把高和通道合并宽度方向变成 LSTM 的时间步。LSTM 双向输出拼成 512 维全连接层输出维度是num_classes 1多出来的 1 就是 CTC blank。训练时损失函数直接用 PyTorch 的CTCLossimport torch loss_fn torch.nn.CTCLoss(blankblank, zero_infinityTrue)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, devicecuda): img cv2.imread(img_path) # 1. YOLOv5 检测车牌框 results det_model(img, size640) 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_classes1 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 轮mAP0.5 还是 0CRNN 的 loss 在前面 10 轮一直抖动不下降。原因很多人把--weights参数省掉让 YOLOv5 从随机权重开始训。车牌数据集通常只有几千张从零训练一个 CSPDarknet 注定收敛慢。CRNN 这边则是学习率设得太大batch 又小BN 层统计不稳定。解决YOLOv5 训练自己的数据集时一定加载yolov5s.pt或yolov5n.pt预训练权重。hyp.scratch-low.yaml里的初始学习率lr00.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(fframes/{frames:05d}.jpg, frame) print(favg {tm / frames:.3f}s/frame)验收指标我会分三档看检测 mAP0.5 用 YOLOv5 自带的val.py输出识别整牌准确率统计完全相等字符另外再单独统计字符级准确率方便定位是汉字错还是字母数字错单帧耗时从视频脚本里直接打印。我给自己的参考线是检测 mAP0.5 不低于 0.95整牌准确率不低于 0.97中等 GPU 上单帧耗时 80ms 以内。夜间场景单独统计不要把白天和夜间混在一起算一个数。部署阶段有两个便宜且明显的提速点。第一个是把检测和识别模型各自导出成 ONNX用 ONNX Runtime 推理比原生 PyTorch 少一截调度开销第二个是对支持 FP16 的显卡用 half 精度推理显存占用和耗时都能降。导出 ONNX 后注意 YOLOv5 输出层的张量形状会发生转置后处理里要按导出的格式调索引这是另一个容易翻车的地方。我养成的习惯是每次训练完先把中间结果落盘成图检测框、矫正图、识别结果全部画出来肉眼过一遍再谈精度指标。很多看起来像模型玄学的问题最后都出在字符表顺序、裁剪边界或者后处理规则上。这套 YOLOv5 CRNN 的中文车牌识别方向很值得做先把全流程跑通再回头抠数据和后处理效果会比反复换网络结构来得明显。希望帮到你。本文还有配套的精品资源点击获取
返回列表