
做车牌识别这个方向也有一段时间了从最开始用 OpenCV 传统方法抠字符到后来上手深度学习方案踩过的坑不少。今天这篇就从一个完整项目的角度把“中文车牌检测识别系统”从技术选型、环境搭建、数据集处理到模型训练、检测识别串联、常见问题排查整个流程都捋一遍。项目核心就三样Python、YOLOv5、CRNN。这套组合在车牌识别这个场景里非常经典检测负责找到车牌在哪里识别负责把车牌上的字读出来清晰明确、可替换性强也是目前工程上落地最多的路线之一。这个系统能做什么简单说就是给它一张图片或一段视频流它能自动框出所有车牌的位置并输出完整的车牌字符串比如“京A12345”或者新能源车牌“粤BD12345”。适合谁参考想入门目标检测和 OCR 的同学或者公司要做停车场、门禁、高速收费等场景车牌识别的开发人员。文章里会把我验证过、能跑通的步骤和参数都写明白也会把容易翻车的地方指出来照着做能少走不少弯路。1. 为什么是YOLOv5CRNN技术选型拆解1.1 车牌识别的两条技术路线做车牌识别业界大致有两条路线一种是端到端输入整张图直接输出车牌号和位置网络需要同时学习定位和识别两个任务对数据质量要求很高标注成本也大另一种是两阶段先检测车牌区域再把裁剪后的车牌图交给识别模型两个模型独立训练、独立调优。后者更简单可控目前大部分商用系统也是这么做的。从工程角度看两阶段的优势非常明显检测出问题可以单独优化检测识别不准可以单独优化识别不会互相拖累。而且车牌检测这个任务本身就比较简单——目标类别单一、形状固定YOLOv5完全能胜任车牌识别本质上是一个序列文本识别任务CRNN又是这个领域的经典方案。两者都是成熟技术生态好、资料多踩坑成本低。1.2 中文车牌的难点不是“检测”而是“识别”检测车牌位置对现在的目标检测算法来说难度不大。真正的难点在于识别环节中文车牌第一个字符是汉字省份简称后面是字母和数字的组合一个字符错了整个结果就不可用。所以字符集设计、数据增广、模型对相似字的区分能力都是做中文车牌识别必须重点关注的地方。另外国内车牌有蓝牌、绿牌新能源、黄牌、白牌等字符数量也有差异常规蓝牌是 7 位新能源绿牌是 8 位。CRNN 的 CTC 机制支持不定长序列输出天然适配这种“位数可能变化”的情况这也是我选 CRNN 而不是固定长度分类模型的原因之一。如果换成 7 分类的模型绿牌就直接废了还得改结构。注意很多新人在做识别时容易忽略“字符集污染”问题。中文车牌字符集包含 31 个省份简称汉字、24 个大写字母通常去掉 I 和 O避免和 1、0 混淆、10 个数字如果字符集定义得不准后面训练全白费。2. 系统整体架构设计与工作流程2.1 数据流转的完整链路整个系统的数据链路可以分成四段每一步都有自己独立的输入输出方便单独测试原始图像或视频帧进入 YOLOv5 检测模型得到车牌边界框坐标和置信度根据边界框裁剪出车牌区域对裁出的图片做预处理透视校正、尺寸缩放、对比度增强等预处理后的车牌图片输入 CRNN 识别模型输出字符序列将字符序列映射为真实车牌字符串同时把检测框画回原图生成最终结果。这套链路每一步都可以独立 debug。比如识别结果不对我可以先把第二步裁剪出来的图直接 dump 到硬盘上人眼检查看是裁剪问题还是识别模型问题不用瞎猜。2.2 模块解耦带来的维护优势我在实际项目里强烈建议把检测和识别作为两个独立服务来维护而不是写死在同一个函数里。原因有三个检测模型和识别模型的生命周期不同。检测模型可能几个月才需要重新训练一次识别模型可能因为新省份数据加入一两周就要迭代混在一起不利于单独上线。其次是性能优化方便检测和识别分别做量化、剪枝可以单独压测耗时瓶颈。再者是容错性更好如果识别服务需要重启检测还能继续输出框图不会整个链路瘫痪。3. 环境准备从Python到模型依赖3.1 Python版本与虚拟环境配置YOLOv5 官方项目对 Python 版本要求是 3.7 到 3.10我建议直接用 Python 3.8 或 3.10太低或太高都会遇到依赖库没有对应 wheel 包的问题。CRNN 相关代码比较老一些开源实现甚至只支持到 Python 3.6建议直接挑选维护较好的版本或者自己适当改造。创建独立的 Python 环境这是避免“依赖地狱”的最有效手段。用 conda 的话conda create -n plate python3.8 conda activate plate如果用 venvpython -m venv plate_env source plate_env/bin/activate # Windows下用 plate_env\Scripts\activate千万不要图省事直接在全局环境装一堆包尤其是 PyTorch 和 NumPy版本冲突能把人搞到崩溃。3.2 PyTorch与YOLOv5依赖安装PyTorch 的安装需要根据你的显卡和 CUDA 版本选择命令。如果是有 NVIDIA 显卡的机器建议先确认驱动支持的最高 CUDA 版本再去 PyTorch 官网选对应版本安装。我这里以常见的 CUDA 11.8 为例pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118如果没有 GPU或者暂时先在本机调试安装 CPU 版本也可以跑通流程只是训练和推理慢不少pip install torch torchvision然后克隆 YOLOv5 仓库并安装依赖git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txtCRNN 的依赖相对简单主要就是 torch、torchvision、opencv-python、numpy 这些YOLOv5 的环境装好之后CRNN 基本不需要额外装太多东西。3.3 环境搭建踩坑记录第一Windows 下最容易遇到 CUDA 不可用的问题大概率是 PyTorch 装的是 CPU 版本或者 CUDA 驱动版本偏低。第二NumPy 版本过高会导致某些老代码报错比如np.int被移除遇到时可以把 NumPy 降到 1.24.3 以下。第三数据集路径里不要出现中文和空格别问我怎么知道的YOLOv5 训练时读取标注文件经常因为这个直接崩。提示如果是在 Linux 服务器上跑建议用 conda 而不是 pip 全局装权限问题少虚拟环境切换也快。4. 车牌检测YOLOv5的数据集与训练4.1 车牌数据从哪来训练车牌检测模型数据来源主要有两个方向一是公开数据集二是有条件的话自采数据。公开中文车牌数据集里比较常用的是 CCPD中国城市停车场数据集包含约 20 万张真实场景车牌图片涵盖不同天气、角度、光照条件标注信息也相对完整。不过 CCPD 的图片大多来自安徽合肥省份简称集中在“皖”字头上直接用来训练检测模型问题不大但如果用来做识别模型训练字符分布会严重失衡后面还得靠补充数据来均衡。自采数据的思路其实也简单找个人流车流适中的停车场或路边用手机或监控摄像头录几段视频抽帧后筛选出包含清晰车牌的图片再用标注工具框出来。这样采到的数据往往更贴近你实际部署的场景。4.2 标注与数据增强要点标注工具我用过 labelImg 和 X-AnyLabeling都支持导出 YOLO 格式的 txt 标注文件。车牌作为单一类别标注时只需要框出车牌区域但有几个细节必须注意标注框要紧贴车牌边缘不要留太多背景否则模型会把多余的背景也学进去。标注框不要切掉边缘字符宁可在字符外侧留 2~3 像素的边也不能裁掉一部分。24 位色深的车牌、反光严重的车牌单独筛选出来重点标注几批。YOLOv5 训练时自带 Mosaic 数据增强、颜色抖动、随机翻转等策略默认开启对提升泛化能力很有帮助。我自己的习惯是Mosaic 保持开启但随机翻转关掉或者减小概率因为车牌上的文字方向是固定的翻转会让模型学到“反着的车牌也能检测”这种错误先验。4.3 训练关键参数详解训练前需要准备一个数据集配置文件YOLOv5 中用 yaml 表示train: datasets/plates/train.txt val: datasets/plates/val.txt nc: 1 names: [plate]然后启动训练python train.py --data plate.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100几个关键参数的选取逻辑说一下--img训练分辨率640 是默认值也是速度和精度的折中。如果你的场景里车牌很小、距离远可以尝试 1280但显存占用和推理耗时都会上升。--batch大小取决于显存爆显存就调小不用死守默认值。--weights用官方预训练权重做迁移学习比从零训练收敛快很多效果也更好。epochs我建议 100 到 300 之间如果数据量小几百张图训练 300 轮也不慢但要注意过拟合。训练过程中重点观察验证集的 mAP0.5在提升不明显时可以提前停掉最后选择best.pt而不是最后一个权重。4.4 检测效果评估车牌检测的评估指标最常用的是 mAP0.5但还有一个指标很重要召回率。停车场场景里漏检一辆车的车牌比误检一个非车牌区域严重得多——漏检意味着整个流程断掉误检还可以靠后处理过滤掉。我在实际调优时会把置信度阈值调低一些比如 0.3让模型尽量多召回候选框再用宽高比、面积等规则过滤明显不对的结果。检测模型训练到 mAP0.5 在 0.95 以上基本就够用了。如果发现某个角度的车牌频繁漏检直接加对应角度的数据比调参更有效。5. 车牌识别CRNN的字符流处理5.1 CRNN为什么适合车牌CRNN 的全称是 Convolutional Recurrent Neural Network核心思想是用 CNN 提取图像特征把特征图按宽度方向切分成一个序列再用 RNN通常是双向 LSTM建模序列上下文最后用 CTC Loss 对齐输出字符序列。整个过程不需要对车牌做字符级分割原始的车牌图直接进去字符串直接出来省去了传统方法里最麻烦的字符切分步骤。相比纯 CNN 分类模型CRNN 的优势在于能处理“不定长”输入。常规蓝牌 7 位、新能源绿牌 8 位网络结构不用改训练数据里两种车牌都放进去就行。相比 Transformer 类 OCR 方案CRNN 轻量许多部署成本低在车牌这种字符排列规整、背景不会太复杂的场景里完全够用。5.2 中文字符集与标签编码车牌识别的字符集设计是整个项目里最基础但也最容易出错的环节。中文车牌字符集一般包含31 个省份简称汉字京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新24 个大写字母通常去掉 I 和 O因为它们和数字 1、0 太像10 个数字0-9把字符按固定顺序做成一个字典。我习惯把“0-9”放在最前面接着是“A-Z”最后是汉字方便和训练代码中的索引对应。特别提醒一下字符集的顺序一旦确定并开始训练就不要轻易改动否则模型的输出映射就会错位等于白训一遍。CTC 训练时需要把真值标签转换成索引序列长度不固定最后补一个空格符作为 CTC blank。常见实现会直接操作 PyTorch 的ctc_loss内部自动处理 blank不需要手动在标签中间插入空格。5.3 训练中的关键策略与参数CRNN 输入图片的宽高比很重要。由于车牌是横向长条字符是横向排列的网络输入高度通常设为 32宽度根据车牌实际宽高比缩放。比如 440x140 的车牌图往 32 高度缩放后宽度大约在 96~128 之间。输入太长会拖慢训练和推理速度太短则字符特征会被压缩到看不清。数据增广是识别精度提升的关键。我在训练时常用这些增广方式随机亮度、对比度调整模拟不同光照随机模糊模拟运动模糊随机透视变换模拟拍摄角度随机添加椒盐噪声和条纹模拟传感器噪声。每次增广的不只是图片标签不变但模型见过的形态会丰富很多。优化器我用过 Adam 也用过 SGD。Adam 收敛快适合初期快速看效果SGD 配合 warmup cosine 衰减最终精度往往更高。初始学习率设置 1e-3 左右batch size 64 到 128 之间训练到验证集 loss 不再下降时把学习率降一个数量级再训几十轮。6. 检测与识别的串联从车牌图像到最终文本6.1 检测框的过滤与去重检测模型输出的是所有候选框后处理阶段需要做两件事置信度过滤和 NMS 去重。YOLOv5 推理时已经内置了 NMS你可以通过conf_thres和iou_thres控制阈值。实际场景里我希望尽量少漏检所以conf_thres设置在 0.25 左右。置信度低不代表框一定不对很多模糊的小车牌置信度就是不高。如果是视频流场景同一辆车会在连续多帧中反复出现直接每帧都识别会非常冗余也容易造成识别结果抖动。一般做法是加上目标跟踪或者用简单的“检测框 IoU 时间序列匹配”来给同一辆车做去重确认车牌号后持续输出结果即可。6.2 车牌图像的预处理检测框是轴对齐的矩形但实际车牌可能因为拍摄角度产生透视变形。这时候直接裁剪出来的图是平行四边形直接送进 CRNN 效果会打折扣。处理方法有两种一是简单粗暴直接缩放到固定尺寸送进网络对轻微倾斜有效二是做透视校正先在做完检测后在裁剪区域内用边缘检测或角点检测找到车牌的四个角点再做透视变换拉正。后一种更稳但需要额外处理。我的经验是轻微倾斜10 度以内直接缩放问题不大CRNN 有足够鲁棒性。大角度场景下透视校正收益明显值得做。校正之后再做直方图均衡化能一定程度提升夜间图片的识别效果增强对比度后字符边缘更清晰识别自然更准。6.3 检测识别集成示例以 Python 代码为例核心流程大概是这样的import cv2 import torch from models.experimental import attempt_load from utils.general import non_max_suppression # 检测模型 det_model attempt_load(weights/plate_det.pt, map_locationcpu) det_model.eval() # 识别模型 rec_model CRNN(num_classeslen(char_dict)) rec_model.load_state_dict(torch.load(weights/plate_crnn.pth, map_locationcpu)) rec_model.eval() def predict(img): # YOLOv5检测 dets det_model(img)[0] dets non_max_suppression(dets, conf_thres0.25, iou_thres0.5)[0] result [] for *xyxy, conf, cls in dets: x1, y1, x2, y2 [int(v) for v in xyxy] plate img[y1:y2, x1:x2] # 预处理 CRNN识别 text crnn_infer(rec_model, plate) result.append(([x1, y1, x2, y2], text, float(conf))) return result集成时建议先把检测结果画到图上保存一份再输出识别文本方便肉眼核对。实际项目中我还会加一个接口层把识别结果序列化成 JSON 返回方便网页或 App 调用{plates: [{box: [100, 200, 340, 260], text: 京A12345, confidence: 0.93}]}7. 性能优化与部署经验7.1 精度和速度的平衡策略YOLOv5 提供 n/s/m/l/x 不同规模的模型从轻到重推理速度递减、精度递增。车牌检测这个任务相对简单不需要太大的模型。在我的实测环境里YOLOv5s 和 YOLOv5n 在精度上差距不大但 n 的推理速度快不少尤其是在嵌入式设备上差距非常明显。CRNN 也可以做减法。主干网络可以换轻量版本比如用 MobileNetV3 替换 VGG16两层双向 LSTM 可以保留一层隐藏层维度从 256 降到 128。这样的轻量 CRNN 在 CPU 上可以跑到极快而准确率只掉一两个点。如果没有极端性能要求或部署在 GPU 上建议保留完整模型省心。模型组合推理速度参考适用场景YOLOv5n 轻量CRNNCPU 可实时嵌入式、低功耗设备YOLOv5s 标准CRNNGPU 毫秒级常规服务器部署YOLOv5m/x 大CRNN显卡要求高高精度要求、离线批处理注意上表中的速度只是相对参考和硬件关系很大具体部署时还是要以自己机器的实测为准。7.2 ONNX导出与嵌入式部署如果在服务器上做 Python 推理直接用 PyTorch 没毛病。但如果要部署到摄像头一体机、嵌入式板子或者需要跨语言调用一般把模型转成 ONNX 或 TensorRT。转 ONNX 用 YOLOv5 自带的脚本就行python export.py --weights best.pt --include onnx之后可以用 ONNXRuntime 做 CPU 推理速度比纯 PyTorch 快不少。如果用的是 NVIDIA Jetson 系列可以继续转 TensorRT进一步提速。嵌入式部署还有一条路是用 RKNN 工具链部署到瑞芯微的平台很多车牌一体机方案就是在这种芯片上跑的稳定性也不错。7.3 实测常见性能瓶颈性能瓶颈常见在图像解码环节尤其是视频流。如果每帧都是 1080p 的 JPEG 解码解码时间可能比两个模型推理加起来还长。建议在推理前先压缩或裁剪到合适尺寸比如检测输入只需要 640x640就别把整个 4K 画面直接送进去。另一个瓶颈是视频流场景里的循环调度。检测和识别串行执行整条链路时间就是两者之和。如果时间差太多可以做多进程流水线检测进程持续出框识别进程消费框并行处理吞吐量能明显提升。这个优化做起来不难但对系统整体响应速度帮助非常大。8. 常见问题与排查技巧实录8.1 检测不到车牌怎么办这种问题优先级最高因为检测不到后面全完。先看是不是光照太暗或车牌反光这类需要加数据或做预处理。其次调低置信度阈值看低置信度结果里是否包含车牌框如果包含说明模型其实有检测能力只是不够自信再回去补充对应场景的数据增强。最后检查输入分辨率小目标在低分辨率下就是容易被漏掉适当提升--img到 960 或 1280 会有改善。8.2 车牌识别结果错字频发识别错字最常见的有两类形近汉字混淆比如“鄂”和“郭”、“赣”和“颠”字母数字混淆比如 Z 和 2、O 和 0。汉字混淆要靠增加各省数据量和针对性增广来缓解也可以在字符集层面把不可能出现在同一位置的字符做后校验。字母数字混淆上把 I、O 从字符集去掉是最直接的办法车牌规则本身就不允许这两个字母出现。识别结果里最后一位漏识别也很常见大概率是检测框裁剪太齐把最边缘字符的残影切掉了一部分。解决方案是在检测框基础上往外扩 5%~10% 再做裁剪让 CRNN 看到完整字符。8.3 运行时的环境与显存问题“CUDA out of memory”是出现频率最高的报错。解决方案按优先级排调小 batch size、降低输入分辨率、关闭梯度计算model.eval() torch.no_grad()。如果还是爆显存考虑换更小的模型。还有一类问题是依赖版本不匹配。比如 YOLOv5 新版需要较新的 torchvision但 CRNN 老代码可能依赖旧版两者放一个环境里很容易冲突。我的建议是分两个虚拟环境分别维护检测和识别之间通过文件或消息传递互不污染。虽然麻烦一点但确实能避免连环暴雷。写在最后的实操心得做这个项目到现在我最大的感受是模型结构只占三分数据质量占七分。车牌识别看起来很“深度学习”实际上大量的工作都花在数据采集、清洗、标注、增广上。你花三天时间整理出一份干净的数据集远比花三天时间调模型的收益高。最后分享一个小技巧训练完识别模型后批量跑一遍验证集把识别结果的裁剪图全部保存下来按“正确”和“错误”分目录人工扫一遍。很多 bug 光看指标看不出来把图铺开一眼就能发现问题比如某个省份的车牌全部错、某一类光照下识别率暴跌。先用肉眼把这些问题解决了再回到指标上做精细调优这个顺序我屡试不爽。