资讯动态

中文车牌识别实战:从YOLO检测到LPRNet识别与系统部署

发布时间:2026/9/12 3:03:17 来源:尧图企业网站定制
简介面向计算机相关专业毕业设计及深度学习初学者的中文车牌识别与管理系统项目包。基于深度学习实现车牌检测、字符分割与识别并配有简洁美观的图形管理界面。压缩包共16个文件包含9个Python脚本模型训练、核心识别、界面及视频处理、2个H5模型权重文件、3个GIF效果展示、1个演示视频及1份使用说明整体大小约26.69MB。模型与代码已调试完成下载解压后按说明即可运行适合快速搭建完整毕设Demo或学习CNN/UNet在CV任务中的应用。目前已有274人学习使用可作为高分毕业设计参考也可在此基础上扩展夜间识别、新能源车牌等方向。文件按功能拆分清晰便于对照阅读和二次开发。1. 中文车牌识别真正的门槛不在模型在数据中文车牌识别和通用 OCR 看起来都是图里找字上手做一次就知道差别很大。车牌只有 7 个字符却涵盖 31 个省份汉字、24 个英文字母和 10 个数字加上蓝绿黄几种底色以及倾斜、反光和运动模糊传统模板匹配到了真实场景基本撑不住。深度学习把任务拆成检测和识别两步是目前车牌识别项目里最主流的技术路线。这篇从一个典型课题切入——基于深度学习的中文车牌识别与管理系统。别把注意力全押在完整代码上这类项目真正要花时间的是模型选型、训练数据来源以及识别结果如何落进管理系统。下文按常规工程顺序从选型讲到部署验证。2. 检测与识别分开做中文车牌识别的主干选型理由2.1 为什么检测选 YOLO 系而不是把识别做成端到端车牌识别在结构上有两条路线。第一条是端到端输入一张全景图模型直接输出车牌字符串中间不分检测和识别。论文里这条路线的指标往往很好看因为训练数据和推理图是同一分布端到端梯度回传也省掉了中间环节。但实际做管理系统时我一般不碰它——真实场景里车牌在画面中的尺度变化很大出入口照片里车牌可能只占整个画面的 2% 到 5%端到端模型要么在整图上漏检小目标要么放大特征图后引入大量背景干扰调起来非常被动。第二条路线是检测加识别两阶段。检测网络先用目标检测模型把车牌区域框出来再把框内图像裁剪后送给识别网络。两个模型各自独立训练、独立换版本比如检测从 YOLOv5s 换成 YOLOv8n识别网络完全不用动反过来识别网络从 LPRNet 换成 CRNN检测侧也不用重训。这个解耦特性在毕业设计和中小型系统里非常实用因为车牌检测的数据集相对好找识别数据集才是真正的瓶颈。检测模型的选择我一般推荐 YOLOv5s 或 YOLOv8n。v5s 生态成熟部署资料多v8n 在同样输入尺寸下推理略快但对 PyTorch 版本有要求。车牌这类小目标检测输入尺寸不要盲目用 640条件允许的话用 960 或 1280 训练对小车牌召回率提升明显代价是显存占用变大。锚框方面车牌的长宽比大约是 3:1 到 4:1默认锚框对这个比例覆盖不好训练前用 k-means 重算锚框能少调很多 epoch。提示两阶段方案不是对所有场景都最优。如果只识别固定的相机画面、车牌占据画面比例稳定端到端模型反而更省事。选型先看输入分布再谈模型结构。2.2 识别网络LPRNet 用 CTC省掉字符分割车牌区域裁剪出来后第二步是从一张宽高比 3:1 左右的小图里读出 7 个字符。传统做法是先把字符一个个切出来再做单字符分类这要求定位算法对字符间距、边缘粘连非常敏感稍微有点倾斜或反光就切错。深度学习领域里更省事的做法是序列识别加 CTC也就是不做显式字符分割直接对整张车牌图预测按时间步展开的字符概率序列再用 CTC 解码对齐。LPRNet 就是这类方案的代表。主干是一个轻量卷积网络后面不接 RNN直接输出按时间步展开的字符概率通过 CTC 损失训练。因为车牌字符是等宽的印刷体卷积特征本身就带有位置信息RNN 不是必须的。实际训练中 LPRNet 字符级准确率可以做到 99% 以上整牌准确率在干净数据上到 95% 左右真实抓拍数据上会掉到 85% 到 92%这个区间完全够管理系统用。CRNN 是另一条常见路线结构是 CNN 特征提取加双向 LSTM 再加 CTC。它比 LPRNet 多一个序列建模阶段对模糊字符的容错稍好代价是模型体积和推理时间增加。车牌识别这种字符长度固定、字符间距均匀的任务LPRNet 性价比更高如果识别对象还包括其他变长文字CRNN 更通用。选型参考对比项LPRNetCRNN端到端单模型字符分割不需要不需要不需要序列建模无靠卷积位置信息双向 LSTM视结构而定模型体积小几 MB中等大车牌场景准确率高略高依赖数据分布调试难度低中高2.3 字符集与标注规范65 分类是怎么来的中文车牌识别的字符集是个硬约束。车牌第一位是省份简称全国 31 个省份对应 31 个汉字第二位是发牌机关代号用英文字母表示后面是序号由字母和数字组成。实际类别集合是 31 个汉字、24 个英文字母I 和 O 不使用避免和数字 1、0 混淆、10 个数字一共 65 类。这是 65 类不是常见的 10 类标注时要特别注意类别 id 的一致性。检测模型标注用 YOLO 格式类别只有 1 类plate标注框是车牌的四个顶点坐标转成中心点加宽高。识别模型标注是一串字符串比如京A12345训练时映射成类别 id 序列再用 CTC 的机制处理序列对齐。另外新能源绿牌是 8 个字符比蓝牌多一位如果系统要同时处理两类车牌识别网络输出不要写死长度靠 CTC 的合并机制兼容 7 位和 8 位。拿到一个所谓完整代码的 zip 包先别看训练脚本先看数据集的目录结构。多数项目会提供原始图片和标注文件标注格式如果不统一后面所有脚本都要返工。我一般要求数据组织成下面这种格式一眼能检查标注对不对也方便做数据清洗dataset/ ├── 京A12345/ │ ├── 001.jpg │ ├── 002.jpg │ └── ... ├── 沪B88888/ │ └── ... └── ...目录名就是标签字符串目录下是裁剪好的车牌图。这个格式的好处是标注检查不需要打开任何标注工具文件管理器里扫一眼就知道字符有没有标错、图片有没有混入非车牌图。3. 训练可用的识别模型从数据增强到收敛判断3.1 最小可跑的 LPRNet 训练脚本先给一个能跑的 PyTorch 训练框架。数据按上一章说的方式组织dataset目录下每个子目录名就是车牌字符串子目录里是该车牌对应的裁剪图。这是车牌识别数据集最常见的组织方式也是对初学者最友好的一种。import os import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from PIL import Image import torchvision.transforms as T CHARS 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼 \ ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 char_to_idx {c: i for i, c in enumerate(CHARS)} class PlateDataset(Dataset): def __init__(self, root, height48, width168): self.paths, self.labels [], [] for label in os.listdir(root): for name in os.listdir(os.path.join(root, label)): self.paths.append(os.path.join(root, label, name)) self.labels.append(label) self.tf T.Compose([ T.Resize((height, width)), T.ToTensor(), T.Normalize([0.5], [0.5]) ]) def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) label [char_to_idx[c] for c in self.labels[idx]] return self.tf(img), torch.tensor(label, dtypetorch.long) class LPRNet(nn.Module): def __init__(self, num_classes): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d(2), ) self.fc nn.Conv2d(256, num_classes, 1) def forward(self, x): x self.features(x) # (B, C, H/8, W/8) x self.fc(x) # (B, C, H/8, W/8) x x.mean(dim2, keepdimTrue) # 合并高度方向 x x.squeeze(2).permute(2, 0, 1) # (T, B, C) return x def ctc_decode(preds): # preds: (T, B, C)每个时间步取最大概率类别再合并重复 preds preds.argmax(dim2) # (T, B) batch_size preds.size(1) results [] for b in range(batch_size): seq, prev, out preds[:, b].tolist(), -1, [] for t in seq: if t ! prev and t ! len(CHARS): # len(CHARS) 是 blank out.append(t) prev t results.append(.join(CHARS[i] for i in out)) return results这段代码有两个关键点。第一输入统一缩放到 168 乘 48这个 3.5:1 的比例接近真实车牌是识别效果和推理速度的折中不要随便改成正方形。第二ctc_decode里把类别总数len(CHARS)单独留出来作为 blank 的索引PyTorch 的 CTCLoss 默认blank0如果你的空白位放在索引 0字符映射就要整体后移一位否则解码全错。这个细节是初学者最容易踩的坑我习惯把 blank 放最后显式传给损失函数。训练循环本身不长但 CTC 的几个参数必须对齐def collate_fn(batch): imgs, labels zip(*batch) return torch.stack(imgs), list(labels) model LPRNet(num_classeslen(CHARS) 1) # 加 1 给 blank optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CTCLoss(blanklen(CHARS), zero_infinityTrue) dataloader DataLoader(PlateDataset(dataset), batch_size64, shuffleTrue, collate_fncollate_fn) for epoch in range(60): model.train() total_loss 0 for imgs, labels in dataloader: batch_size imgs.size(0) input_lengths torch.full((batch_size,), imgs.size(3) // 8, dtypetorch.long) target_lengths torch.tensor([len(l) for l in labels], dtypetorch.long) targets torch.cat(labels) preds model(imgs) # (T, B, C) loss criterion(preds, targets, input_lengths, target_lengths) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch}: loss {total_loss / len(dataloader):.4f})input_lengths是特征序列长度168 宽经过三次池化后是 21 个时间步所以用imgs.size(3) // 8计算target_lengths是每个样本的真实字符数targets要把 batch 内的标签拼成一个一维张量这就是 CTC 需要的扁平化目标。collate_fn是必须的因为 batch 内每个车牌字符串长度一样但组合不同默认的 collate 会把变长张量直接报错。优化器用 Adam、初始学习率 1e-3、批次 64显存不够就减到 32。训练 50 到 80 个 epoch 后字符准确率一般能达到 98% 以上。3.2 数据增强参数别把车牌增强没了车牌识别数据增强比通用分类任务更容易做过头。车牌纹理就是字符和底色增强过于激进字符边缘被模糊掉模型学到的是色块而不是字符。我常用的增强分三类几何、颜色和噪声。几何增强里随机旋转不要超过 15 度水平透视变换偏移量控制在宽度 10% 以内超过这个范围车牌形变就不像真实相机拍出来的。颜色增强里HSV 的 H 通道抖动控制在正负 5 度以内S 和 V 可以放宽到正负 20%因为真实场景色温和亮度变化很大。噪声增强里高斯噪声标准差建议 0.01 到 0.03再大字符边缘会被噪声淹没。import imgaug.augmenters as iaa aug iaa.Sequential([ iaa.Affine(rotate(-12, 12), shear(-8, 8)), iaa.AdditiveGaussianNoise(scale(0, 0.02 * 255)), iaa.MultiplyHueAndSaturation((0.95, 1.05), (0.8, 1.2)), iaa.LinearContrast((0.8, 1.2)), iaa.GaussianBlur(sigma(0.0, 0.8)) ])imgaug 的GaussianBlursigma 上限 0.8 是经验值超过这个值字符笔画被磨平模型会对模糊图像过拟合测试集反而掉点。shear控制在正负 8 度模拟车辆转弯时的车牌形变对蓝牌够用对大车双行车牌要再调小。如果数据集中没有雨雾样本可以叠加iaa.Rain或对比度降低但这类增强只建议在最后 20% 的 epoch 开启前期全开模型收敛会很慢。参数范围参考下面的表增强类型参数范围说明旋转±12 度超过后字符形变失真水平剪切±8 度模拟转弯形变高斯噪声σ0~0.02过大淹没字符边缘高斯模糊σ0~0.8经验上限色相偏移±5%车牌底色有约束另一个容易被忽略的点是不要对整张训练图做随机裁剪。识别网络的输入应该是完整车牌区域裁剪会把边缘字符切掉CTC 对字符缺失的容错很差。真实场景的车牌遮挡应该在检测模块用遮挡样本训练而不是在识别模块用裁剪模拟。3.3 收敛标准字符准确率和整牌准确率要分开算训练时只看 loss 容易误判。CTC loss 降到 0.1 左右视觉上可能还有 5% 的字符是错的。建议每个 epoch 后在验证集上同时算两个指标字符准确率识别对的字符数除以总字符数和整牌准确率7 个字符全部正确的车牌占比。字符准确率反映模型鲁棒性整牌准确率反映业务可用性管理系统做查询和统计时用的是后者。实际项目中这两个指标差距通常在 8 到 12 个百分点。字符准确率 98% 时单牌 7 个字符全部正确的概率约是 0.98 的 7 次方约 87%也就是每 100 辆车会识别错 13 辆左右。如果管理系统要求失配率低于 5%字符准确率要到 99.3% 以上这时候单纯堆数据不太够要考虑置信度过滤或多次识别投票这个在识别服务部分展开。4. 识别服务与管理系统的对接数据流和接口设计4.1 用 FastAPI 把识别能力封装成独立服务训练好的模型不该和业务代码耦合在一起。常见做法是把识别封装成独立 HTTP 服务管理系统通过接口调用模型更新不需要重新部署业务多个入口也能共用同一个识别服务。用 FastAPI 写一个最小实现from fastapi import FastAPI, UploadFile import torch from PIL import Image app FastAPI() def load_models(): detect torch.hub.load(ultralytics/yolov5, custom, pathweights/plate_det.pt) recog torch.load(weights/lprnet.pt, map_locationcpu) return detect, recog detect_model, recog_model load_models() app.post(/recognize) async def recognize(file: UploadFile): img Image.open(file.file).convert(RGB) dets detect_model(img, size960) results [] for box in dets.xyxy[0].cpu().numpy(): x1, y1, x2, y2 map(int, box[:4]) crop img.crop((x1, y1, x2, y2)) plate, conf recognize_plate(crop, recog_model) # 内部做 LPRNet 推理 results.append({plate: plate, conf: conf, bbox: [x1, y1, x2, y2]}) return {code: 0, data: results}接口设计有两点要注意。第一size960让检测模型在大图上推理对小目标召回率很重要但单张推理时间会从 20 毫秒涨到 60 毫秒左右车牌识别系统没有实时视频流要求的话完全可以接受。第二接口把置信度和检测框一并返回业务端统一做置信度过滤而不是每个业务系统各写一套判断逻辑。部署时用 gunicorn 加 uvicorn worker 启动4 个 worker 可以支撑每秒 20 次左右的识别请求。4.2 管理系统的表结构与查询设计管理系统常见需求是车辆进出记录、车牌查询、统计报表。表结构上最少要三张表车辆档案表、通行记录表和识别日志表。车辆档案表保存白名单和车主信息通行记录表保存每次识别事件识别日志表保存原始图片路径和模型输出用于事后排查和算法迭代。CREATE TABLE vehicle_registry ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(10) NOT NULL UNIQUE, owner_name VARCHAR(50), phone VARCHAR(20), vehicle_type TINYINT DEFAULT 0 COMMENT 0蓝牌 1绿牌 2黄牌, status TINYINT DEFAULT 1 COMMENT 1白名单 0黑名单 ); CREATE TABLE passage_record ( id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(10), direction TINYINT COMMENT 0入场 1出场, confidence DECIMAL(5, 4), image_path VARCHAR(255), happen_time DATETIME NOT NULL, INDEX idx_time (happen_time), INDEX idx_plate (plate_no) ); CREATE TABLE recog_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, raw_plate VARCHAR(10), corrected_plate VARCHAR(10), raw_conf DECIMAL(5, 4), final_conf DECIMAL(5, 4), log_time DATETIME );三个表的关键都在索引上。passage_record的happen_time建索引因为日常查询基本都带时间范围比如统计一天的进出车辆数plate_no建索引是为了按车牌查历史记录。recog_log表是个人习惯也是答辩时最值得展示的一张表被问到识别错了怎么办可以直接指出误识别样本的修正链路。管理端前端框架用 Vue3 或 React 都行关键是把查询条件和结果表格分开这里不展开。4.3 置信度三档策略和二次识别识别服务返回的置信度在真实场景里分布很宽。干净蓝牌照片通常 0.95 以上反光或污损的车牌掉到 0.7 到 0.9。管理系统一般分三档处理大于 0.95 直接采用0.85 到 0.95 进人工复核队列低于 0.85 判定识别失败走重拍或人工录入。阈值不要拍脑袋定建议用 500 张现场照片做阈值扫描画准确率随阈值变化的曲线取准确率开始掉头的点。二次识别是提升整牌准确率的低成本手段。第一次结果置信度不高时把车牌图分别做一次轻微旋转和一次对比度增强再送识别模型三次结果投票多数一致的就是最终输出。这个操作只多花 2 到 3 次推理时间误识别率能降 20% 到 40%。更重一点的做法是保留 CTC 解码时每个时间步的 top-2 概率对易混淆的字符对0/O、1/I、8/B做规则替换。5. 部署验证和演示避坑让系统在答辩现场不翻车5.1 导出 ONNX 的三个注意点部署到 CPU 时把模型导出为 ONNX 再推理速度提升明显。PyTorch 直接推理一张车牌图在 CPU 上约 15 到 30 毫秒ONNX Runtime 可以压到 8 到 15 毫秒。导出时注意三点固定输入尺寸不要用动态尺寸torch.onnx.export指定opset_version11以上否则部分算子不兼容导出后的输入输出名要和加载代码保持一致。验证导出是否正确的标准做法是拿同一张测试图分别跑 PyTorch 和 ONNX Runtime对比 argmax 序列。浮点精度差异允许概率值略有不同但 argmax 必须一致不一致优先检查图像归一化参数是否在导出链路里丢失。5.2 演示现场最容易翻车的三类输入第一类是倾斜超过 30 度的车牌。检测模型通常能框住但识别模型训练增强里的旋转范围只有正负 12 度30 度直接超出分布。演示前加一道校正根据检测框四点坐标做透视变换把车牌扶正再送识别。第二类是暗光和逆光图对比度极低识别模型容易把 6 看成 8。演示测试图尽量选真实抓拍风格别用高清图库图否则现场换成手机实拍效果落差很大。第三类是带边框标注的车牌素材YOLO 可能把边框也当目标过滤掉贴近图像边缘的检测框。5.3 留存测试集和启动自检系统验收不要只看训练时的验证集。单独留存 500 到 1000 张未参与训练的照片按正常光照、夜间、雨天、倾斜四类场景分别统计整牌准确率这组数据既是验收依据也是后续迭代基准。字符混淆矩阵值得细看找出模型常错的字符对针对性补样本比无脑加数据更有效。最后给一个具体技巧识别服务启动时加载完模型用一张固定测试图跑一遍自检输出与预期不符就直接拒绝启动。模型路径错误、ONNX 算子兼容问题都会被挡在门外。真实项目里昨天还能跑今天启动就报错太常见自检是成本最低的一道保险。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价