yolov8车牌识别实战:12种中文车牌类型分类与部署全解析
简介基于YOLOv8的车牌识别算法包覆盖12种中文车牌类型从单行蓝牌、黄牌到新能源绿牌再到警用、教练、武警、使馆、港澳粤Z牌、双层车牌及民航牌兼顾颜色、结构及专用场景差异覆盖面较广。压缩包内共224个文件总大小38.55MB包括149个Python源码、48个YAML配置、PyTorch模型权重pt/pth、测试图片及Shell工具脚本等结构清晰便于定位训练、推理或部署相关模块。已有348人浏览学习适合作为智能交通、停车场管理相关课程设计或毕业设计的基础项目。直接运行detect_plate.py即可体验端到端识别过程附带的多样车牌样张可验证不同拍摄条件下的效果同时YAML与权重文件也为进一步微调或迁移学习提供了良好起点。1. 从检测到分类yolov8 车牌识别真正难的不是框停车场闸机前绿牌新能源车停了三秒还没抬杆后台日志显示检测框一直在跳置信度在 0.45 上下徘徊——这种场景做车牌识别的人都不陌生。基于 yolov8 的车牌识别算法核心工作分两段先用目标检测把车牌从画面里找出来再对框内的内容做分类或字符识别。标题里的「12 种中文车牌类型」才是真正的分水岭因为中国车牌的类型区分不只在底色还在字符排列、位数和边框特征蓝牌是 7 位绿牌是 8 位且第二位是字母 D 或 F挂车黄牌是 8 位港澳入出境车牌是黑底白字且带「港」「澳」字样。yolov8 负责的是前一段——把各种底色、各种角度、各种光照下的车牌稳定检出并分类出类型为后面的字符识别OCR提供干净的输入。这篇文章把模型选型、数据标注、训练参数、部署推理和类型分类的坑一次讲透适合正在做毕业设计、停车场项目或安防系统选型的人直接参考。2. yolov8 网络结构与 12 类车牌分类的映射逻辑2.1 为什么用 yolov8 而不是 yolo5 或传统图像处理车牌识别领域之前的主流方案是「颜色分割 形态学 模板匹配」或者 yolo5 LPRNet。传统方案对光照和倾斜极度敏感yolo5 的 anchor-based 机制在检测小目标和细长目标时召回率不够稳。yolov8 是 anchor-free 的直接预测目标中心点到边框四边的距离省掉了聚类 anchor 这一步。这个特性对车牌识别很重要车牌在画面中经常是细长矩形尤其是侧方停车场景下宽高比能到 3:1 以上。anchor-based 模型需要在训练前对数据集做 K-means 聚类得到合适的 anchor 尺寸一旦拍摄距离变化大聚类结果就不够用了。yolov8 的解耦头Decoupled Head把分类和回归分支分开每个分支有自己的损失权重训练时互不干扰。C2f 模块Cross Stage Partial with 2 convolutions and n bottlenecks在保证梯度流通的同时减少了参数量GTX 1660 Ti 这种 6GB 显存的卡也能跑起来。12 种车牌类型分类这件事可以在 yolov8 的检测头里直接做——类别数设为 12也可以拆成「检测 分类」两段——检测只输出 1 类车牌另外用 ResNet 之类的分类器判断类型。两种方案各有场景但多数项目我会建议直接在检测头里做多类理由在 2.3 里展开。2.2 12 种车牌类别的标注定义与类别 ID 设计先把 12 类定义清楚这一步决定了后续所有工作的准确性。不同省份、不同时期的地方牌照写法有差异但作为通用车牌识别算法一般按「底色 字符结构 使用场景」三个维度划分类别ID类型名称典型特征字符位数常见场景0蓝牌蓝底白字7位小型汽车最常见1黄牌黄底黑字7位大型车/8位挂车大型汽车、挂车2绿牌绿底黑字8位第二位为D或F新能源车D纯电/F混动3黑牌黑底白字7位涉外车辆、港澳入出境4白牌白底黑字6~7位警车、军车5教练车黄底黑字带「学」字7位驾校车辆6农用车绿底白字或黄底黑字7位农机、低速载货汽车7使馆车黑底白字带「使」字7位外国驻华使馆8临时牌纸质白底黑字或棕底白字7位临时上路车辆9挂车黄底黑字最后一位为「挂」8位半挂车、全挂车10摩托车黄底黑字或蓝底白字7位二轮摩托车11其他特殊用途车牌不定拖拉机、轮式机械等注意这里的 12 类划分不是国标原文而是工程实现中为了训练收敛和实际场景覆盖而做的「合并同类项」。比如军牌里还分白底红字和绿底白字但样本量太少时不必单独开类归入「白牌」或「其他」即可。类别 ID 的设计有学问——训练时不建议把「蓝牌」和「绿牌」的 ID 设得差距过大或过近。yolov8 默认用交叉熵损失做分类类别间的独立性假设意味着它不考虑「蓝牌和绿牌长得有点像」这种语义关系。如果样本量不均衡模型会倾向把难分样本硬归到样本量大的类。因此标注阶段要设一个规则同一张图里的多块车牌都要标不能只标最清晰的那块。很多项目训出来精确率看着不错一上真实场景就崩就是因为训练集里把模糊车牌全跳过了。2.3 检测头直接分类 vs 检测 分类器两段式直接在 yolov8 检测头里输出 12 类好处是单模型端到端部署简单、推理时间短单帧一次前向搞定检测和类型判断。坏处是类型判断的精度受检测框质量影响——如果车牌边缘没框全分类特征特别是底色和边框就看不全。两段式方案yolov8 只检测车牌后面挂一个轻量分类器的好处是检测框稍微偏一点也能通过分类器修正坏处是推理链路变长而且多一个模型就要多做一次前向、多一份显存或内存开销。我的选择标准是看部署平台如果是 RK3588、Jetson Orin Nano 这类边缘盒子单模型 12 类直接出省事省算力如果是服务器 GPU 部署且精度要求极高再把类型分类拆出去优化。后面第 4 章的代码按单模型方案给出。3. 训练 yolov8 车牌识别模型从数据集到损失函数曲线3.1 数据来源、整理与 CCPD 之外的补充思路公开数据集里最常用的是中科大的 CCPD 数据集Chinese City Parking Dataset包含 30 万张以上自然场景车牌图覆盖蓝牌、绿牌、黄牌带详细的标注信息。但 CCPD 有一个明显短板场景集中在安徽合肥的停车场角度、光照、背景多样性不够。而且 CCPD 的标注格式是 JSON字段含义比较绕需要转成 YOLO 格式。更稳妥的做法是「开源数据 自采数据」混合开源部分CCPD 按需抽 5~10 万张并做去重同一辆车连续帧不要重复训。自采部分在停车场出入口、路侧、加油站等位置采集视频按帧抽图。合成数据用 3D 渲染或图像合成工具生成不同角度、不同光照、不同模糊程度的车牌特别是绿牌的 D/F 第二位特征要多做合成增强。标注工具用 LabelImg 或 X-AnyLabeling 都行导出 YOLO 格式。标注规范要提前写清楚检测框紧贴车牌边缘不要包含车牌边框以外的车身部分倾斜车牌按最小外接矩形标注但 yolov8 是水平框检测器倾斜角过大的车牌对检测来说天然困难这就引出 3.2 的角度增强。3.2 数据增强与倾斜车牌处理的三个关键参数from ultralytics import YOLO model YOLO(yolov8s.pt) model.train( dataplate.yaml, epochs200, imgsz640, batch16, optimizerSGD, lr00.01, lrf0.01, momentum0.937, weight_decay0.0005, warmup_epochs3, warmup_momentum0.8, cos_lrTrue, hsv_h0.03, hsv_s0.6, hsv_v0.5, degrees15, translate0.1, scale0.4, shear5.0, perspective0.0002, flipud0.0, fliplr0.5, mosaic1.0, mixup0.3, )这段代码里对车牌场景最有影响的参数是degrees旋转、perspective透视、fliplr水平翻转这三个。degrees15允许训练时把图片随机旋转正负 15 度。超过这个值会导致车牌长宽比变形太严重模型学到的是「斜矩形」而不是「车牌」。侧方停车场景下真实倾斜角通常在 10~30 度训练时用 15 度、推理时靠检测框的泛化能力去覆盖实测够用。如果你想让模型更抗斜可以逐步加到 25但一定要配合shear错切一起调单独加大degrees会让损失函数震荡。perspective0.0002模拟拍摄视角变化带来的近大远小。车牌识别最典型的失败场景是车头正对摄像头时车牌是一个正矩形但车从侧面经过时变成梯形。透视增强让模型见过各种形变的车牌代价是训练时间变长。fliplr0.5水平翻转增强这个对车牌识别有特殊的双刃剑效应。翻转后「京A12345」会变成「54321A京」字符位置语义被破坏了。但对类型分类影响不大——蓝牌翻过来还是蓝牌。所以如果 12 类分类精度优先fliplr建议降到 0.3如果检测召回优先保持 0.5 不变。hsv_h0.03要特别注意不能开大。车牌的底色是分类的核心特征蓝牌的 HSV 色相范围集中在 200~230 度之间hsv_h超过 0.05 会把蓝牌增强成紫色甚至红色模型分类就直接学歪了。3.3 模型结构选择n/s/m/l 怎么选与 GPU 显存估算yolov8 官方提供 nnano、ssmall、mmedium、llarge、xxlarge五个尺寸。车牌识别不是小目标检测车牌在画面中通常占 100x30 像素以上不需要为了小目标能力上 l 或 x。参考显存和帧率数据模型参数量输入尺寸 640x640 的 GFLOPs最低显存建议GTX 1660 Ti 实测帧率batch1yolov8n3.2M8.72GB60~80 FPSyolov8s11.2M28.64GB40~55 FPSyolov8m25.9M78.96GB20~30 FPSyolov8l43.7M165.210GB10~15 FPSGTX 1660 Ti6GB跑yolov8s是甜点选择精度比 nano 高一个档次显存刚好放得下 batch16 的训练推理帧率能到 40 以上。Jetson Orin Nano 部署建议用yolov8n或把yolov8s转 INT8 量化。RK3588 的 NPU 对 INT8 支持好但前提是模型结构里不要有算子不兼容的模块第 5 章会展开说。yolov8 的 C2f 模块相比 yolo5 的 C3 模块多了一次梯度分流split 后分别经过 Bottleneck 再 concat理论上梯度流更丰富、特征表达能力更强。但代价是模型文件大了 10%~15%加载速度和前向速度会略降。对车牌场景来说C2f 带来的精度提升是实打实的尤其对绿牌上 D/F 字母这种细节特征的提取有帮助。3.4 损失函数曲线怎么看判断模型是否欠拟合或过拟合训练时打开results.csv或直接用tensorboard看损失曲线。关键看三张图train/box_loss、train/cls_loss、val/cls_loss。# 训练结束后快速画出损失曲线 import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/train/results.csv) # 实际列名可能带空格先打印列名确认 df.columns [c.strip() for c in df.columns] plt.figure(figsize(12, 4)) plt.subplot(1, 3, 1) plt.plot(df[epoch], df[train/box_loss], labeltrain box loss) plt.plot(df[epoch], df[val/box_loss], labelval box loss) plt.xlabel(epoch) plt.ylabel(box loss) plt.legend() plt.subplot(1, 3, 2) plt.plot(df[epoch], df[train/cls_loss], labeltrain cls loss) plt.plot(df[epoch], df[val/cls_loss], labelval cls loss) plt.xlabel(epoch) plt.ylabel(cls loss) plt.legend() plt.subplot(1, 3, 3) plt.plot(df[epoch], df[metrics/precision(B)], labelprecision) plt.plot(df[epoch], df[metrics/recall(B)], labelrecall) plt.xlabel(epoch) plt.ylabel(score) plt.legend() plt.tight_layout() plt.savefig(loss_curves.png, dpi150)画完曲线后按这三个标准判断train loss 持续下降但 val loss 拐点后上升过拟合。车牌数据集样本量足够大时不太常见但如果自采数据只有几千张就容易出现。对策加大mixup到 0.5提前epochs到 150 左右开启早停。train 和 val 的 cls loss 都居高不下0.1类型分类学不动。大概率是类别样本不均衡黄牌样本是蓝牌的 1/50模型对黄牌的梯度贡献微乎其微。对策计算每个类别的样本数按倒数加权设置cls_loss的类别权重或者用过采样把少数类复制几轮。box loss 收敛但 cls loss 波动剧烈检测框已经稳定了但类型判断不稳定。重点检查标注——有没有把「教练车黄牌」和「普通黄牌」混标了这两个外观几乎一样区别只在有没有「学」字人的肉眼都容易看错。推荐的训练策略是先用yolov8s加上 mosaic1.0 跑 200 轮观察损失曲线如果 150 轮后 cls loss 还在平稳下降说明数据量不够可以加载 best.pt 继续训 100 轮把mosaic关掉mosaic0.0只用小角度旋转和 HSV 微调做最后精修。这个「先强增强、后弱增强」的两段式策略在车牌场景下比单阶段从头训效果稳定 3~5 个百分点。4. 推理部署与 12 类车牌的前后处理链路4.1 ONNX 导出与 CPU/GPU 推理基准训练完的best.pt是 PyTorch 权重不能直接上生产。最常见的落地路径是导出 ONNX再根据部署平台转成 TensorRTNVIDIA GPU、OpenVINOIntel CPU、RKNN瑞芯微 NPU或 TensorFlow Lite手机端。# 导出 ONNX并同时输出 NMS 后的结果end2end 模式在 yolov8 中可用 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) model.export( formatonnx, imgsz640, opset12, dynamicFalse, simplifyTrue, halfTrue, )导出后建议用onnxruntime或onnxsim验证一遍输出张量的形状。yolov8 的 ONNX 输出是一个 1x84x8400 的张量以 COCO 80 类为例其中 8400 是三个尺度特征图80x80 40x40 20x20展平后的候选框总数84 4边框 80类别概率。车牌 12 类场景下输出变成了 1x16x8400。这个形状和 yolo5 的端到端输出一样但注意 8400 个候选中绝大多数是背景需要 NMS 过滤。import onnxruntime as ort import numpy as np import cv2 session ort.InferenceSession(best.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider]) input_name session.get_inputs()[0].name def preprocess(img, size640): h, w img.shape[:2] r min(size / h, size / w) resize_w, resize_h int(round(w * r)), int(round(h * r)) resized cv2.resize(img, (resize_w, resize_h), interpolationcv2.INTER_LINEAR) canvas np.full((size, size, 3), 114, dtypenp.uint8) x_offset (size - resize_w) // 2 y_offset (size - resize_h) // 2 canvas[y_offset:y_offset resize_h, x_offset:x_offset resize_w] resized blob canvas[:, :, ::-1].transpose(2, 0, 1) blob blob.astype(np.float32) / 255.0 return blob[None], r, x_offset, y_offset img cv2.imread(car.jpg) blob, r, x_off, y_off preprocess(img) outputs session.run(None, {input_name: blob})[0] # shape: (1, 16, 8400) # 后续要对 outputs 做 NMS并把坐标按 r 和 offset 还原回原图preprocess 里做了 letterbox保持宽高比缩放 填充推理后必须把归一化坐标映射回原图。r是缩放比例x_off/y_off是填充偏移映射公式为ori_x (pred_x - x_off) / r ori_y (pred_y - y_off) / r这一步忘了做的话检测框会整体偏移——实际项目里很多「模型不准」的反馈最后都查出来是坐标没还原。4.2 车牌定位成功后的二次精细化为什么需要矫正yolov8 输出的水平检测框已经把车牌定位住了但 12 类分类可以用「框内图像」再做一次校验。真实场景下摄像头安装角度和车牌本身倾斜会导致检测框内的车牌是平行四边形或梯形直接拿去 OCR 会失败。常见做法是在类型分类之后、字符识别之前加一个四角点回归网络或传统边缘检测做透视矫正。如果不用额外模型可以用 OpenCV 的minAreaRect找车牌区域的旋转外接矩形再按角度做仿射变换import cv2 import numpy as np # 假设 yolo 输出检测框后的结果: box [x1, y1, x2, y2, conf, cls] # 在框内找绿色/蓝色的轮廓 def align_plate_crop(img, box): x1, y1, x2, y2 [int(v) for v in box[:4]] crop img[y1:y2, x1:x2] hsv cv2.cvtColor(crop, cv2.COLOR_BGR2HSV) # 蓝牌色相范围 mask cv2.inRange(hsv, (100, 80, 80), (130, 255, 255)) # 绿牌色相范围新能源是绿底黑字 mask_green cv2.inRange(hsv, (35, 80, 80), (85, 255, 255)) mask cv2.bitwise_or(mask, mask_green) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return crop contour max(contours, keycv2.contourArea) rect cv2.minAreaRect(contour) angle rect[2] if angle 45: angle angle - 90 matrix cv2.getRotationMatrix2D((crop.shape[1] / 2, crop.shape[0] / 2), angle, 1.0) rotated cv2.warpAffine(crop, matrix, (crop.shape[1], crop.shape[0])) return rotated这个方法的局限是依赖 HSV 找色对光照剧烈变化逆光、夜间会失效。更稳的替代方案是把四角点回归任务直接并入 yolov8——用 yolo 的检测头输出 4 个关键点坐标仿照 yolov8-pose 的结构但训练数据需要额外标注四角点。这是进阶优化第一次做车牌项目建议先用 HSV 矫正把管线跑通再考虑关键点方案。4.3 端到端部署时的 NMS 参数和置信度阈值推理时conf_thres建议设 0.35~0.45iou_thres设 0.5~0.6。车牌目标大、特征明显阈值太保守会漏检——特别是绿牌在阴暗环境下对比度低置信度普遍比蓝牌低 5~10 个百分点。如果你发现绿牌老是检不出来试着把conf_thres降到 0.3然后靠 NMS 后的面积过滤去掉误检。类别置信度还有一个后处理技巧12 类分类结果里如果「蓝牌」和「绿牌」两个类别的置信度都超过 0.4 且差值小于 0.1就标记为「待人工复核」不直接做决定。这比强行设定一个分类阈值要可靠得多。5. 识别结果的时序稳定性让算法对同一辆车只识别一次的工程手段5.1 单帧识别的问题闪烁、跳变和重复计数停车场场景中最影响体验的问题是「一帧识别到、下一帧没识别到、再下一帧又识别到」——原因是车辆运动模糊、车牌反光、摄像头自动曝光切换都会让某几帧的检测置信度跌破阈值。如果直接按帧触发后续逻辑比如抬杆、计费、入库就会产生重复记录或误触发。yolov8 本身没有任何时序记忆能力单帧输入输出结构决定了它「每帧都是第一次见这张图」。要做时序稳定必须在检测外面包一层「轨迹管理」逻辑常见做法是 IOU 匹配 状态机。5.2 基于 IOU 的跨帧匹配与车牌类型投票机制设计一个简单的PlateTracker类维护一个字典保存「当前正在跟踪的车牌」的状态检测框坐标、类别置信度、连续命中帧数、丢失帧数、累计类别投票结果。每帧检测完后把当前帧的检测框和已有轨迹做 IOU 匹配匹配成功就更新轨迹匹配失败就新建轨迹。超过连续 3 帧没匹配上丢失帧数 3就删除轨迹。class PlateTracker: def __init__(self, iou_threshold0.4, max_miss_frames3, vote_threshold5): self.iou_threshold iou_threshold self.max_miss_frames max_miss_frames self.vote_threshold vote_threshold self.tracks {} # track_id - {box: [...], votes: {...}, miss: int, hit: int} self.next_id 0 def iou(self, a, b): x1 max(a[0], b[0]); y1 max(a[1], b[1]) x2 min(a[2], b[2]); y2 min(a[3], b[3]) inter max(0, x2 - x1) * max(0, y2 - y1) area_a (a[2] - a[0]) * (a[3] - a[1]) area_b (b[2] - b[0]) * (b[3] - b[1]) return inter / (area_a area_b - inter 1e-6) def update(self, detections): # detections: list of [x1, y1, x2, y2, conf, cls] assigned [False] * len(detections) for track_id, track in list(self.tracks.items()): best_iou, best_idx 0, -1 for i, det in enumerate(detections): if assigned[i]: continue score self.iou(track[box], det[:4]) if score best_iou: best_iou, best_idx score, i if best_iou self.iou_threshold and best_idx ! -1: det detections[best_idx] track[box] det[:4] track[hit] 1 track[miss] 0 # 类别投票累积每帧的分类结果 cls_id int(det[5]) track[votes][cls_id] track[votes].get(cls_id, 0) 1 assigned[best_idx] True else: track[miss] 1 # 新建轨迹 for i, det in enumerate(detections): if not assigned[i]: self.tracks[self.next_id] { box: det[:4], votes: {int(det[5]): 1}, hit: 1, miss: 0, } self.next_id 1 # 清理长期丢失的轨迹 for track_id in list(self.tracks.keys()): if self.tracks[track_id][miss] self.max_miss_frames: del self.tracks[track_id] # 返回稳定结果命中帧数达到阈值且类别票数最高 results [] for track_id, track in self.tracks.items(): if track[hit] self.vote_threshold: best_cls max(track[votes], keytrack[votes].get) best_votes track[votes][best_cls] total_votes sum(track[votes].values()) if best_votes / total_votes 0.7: results.append({ box: track[box], cls: best_cls, hit: track[hit], votes: total_votes, }) return resultsvote_threshold5的含义是「同一辆车至少连续 5 帧被识别到才认为是有效结果」。这看起来增加了延迟以 30 FPS 的输入算约 200 毫秒但换来的是把单帧误检几乎完全过滤掉。miss_frames3允许短暂遮挡比如行人走过后轨迹不丢。提示投票机制解决的是「类型分类不稳定」的场景。如果 12 类里蓝牌和绿牌总是被投出不同结果说明检测框里包含了太多车身背景HSV 特征被污染了。回到第 3 章检查标注质量别在投票上硬堆阈值。5.3 边缘设备部署RK3588 和 Jetson Orin Nano 的适配差异RK3588 的 NPU6 TOPS INT8跑yolov8n转换后的 RKNN 模型实测单帧 640 输入能做到 25~40ms。转换链路是 PyTorch → ONNX → RKNN关键操作是rknn.config里把target_platform设为rk3588quantized_dtype设为w8a8。要注意节制如果模型里用了SiLU之外的激活函数或者自定义算子RKNN-Toolkit2 可能报错优先选择官方已经支持的结构C2f 模块在最新的 RKNN-Toolkit2 版本中是支持的。Jetson Orin Nano 的路线是 ONNX → TensorRT 的trtexec转 engine支持 FP16 和 INT8需要校准数据集。TensorRT 的 batch 固定后性能提升明显但动态尺寸--minShapes/--maxShapes会引入额外延迟固定 640 输入最稳。运行yolov8nFP16 的推理时间约 8~12msINT8 可以到 5ms 以内。两种边缘设备上都建议部署时开启硬件解码RK3588 的 MPP、Jetson 的 V4L2 NVDEC把 JPEG 解码从 CPU 挪到硬件单元。否则摄像头输入 1080p 时解码开销可能比模型推理还高。IPC 的 RTSP 流拉取用 FFmpeg 子进程 队列别在推理线程里直接做网络 I/O否则一丢包整套流程就卡住了。5.4 一个可直接落地的完整推理脚本骨架import cv2 import numpy as np import onnxruntime as ort from collections import deque class PlateRecognizer: def __init__(self, onnx_path, conf_thres0.35, iou_thres0.5): self.session ort.InferenceSession(onnx_path, providers[CPUExecutionProvider]) self.conf_thres conf_thres self.iou_thres iou_thres self.input_name self.session.get_inputs()[0].name self.input_size self.session.get_inputs()[0].shape[2] # 640 def detect(self, frame): blob, r, x_off, y_off preprocess(frame, self.input_size) preds self.session.run(None, {self.input_name: blob})[0][0] # preds shape: (16, 8400)转置后按置信度过滤 preds preds.T boxes, scores, cls_ids [], [], [] for p in preds: conf p[4:].max() if conf self.conf_thres: continue cls_id int(p[4:].argmax()) bx1, by1, bx2, by2 p[:4] # yolov8 直接输出 xyxy 坐标无需再解算 boxes.append([bx1, by1, bx2, by2]) scores.append(float(conf)) cls_ids.append(cls_id) indices cv2.dnn.NMSBoxes(boxes, scores, self.conf_thres, self.iou_thres) results [] for i in indices.flatten(): x1, y1, x2, y2 boxes[i] # 映射回原图 x1, x2 (x1 - x_off) / r, (x2 - x_off) / r y1, y2 (y1 - y_off) / r, (y2 - y_off) / r results.append([x1, y1, x2, y2, scores[i], cls_ids[i]]) return results def recognize_video(self, video_source): cap cv2.VideoCapture(video_source) tracker PlateTracker() while cap.isOpened(): ret, frame cap.read() if not ret: break dets self.detect(frame) stable tracker.update(dets) for res in stable: plate_type TYPE_NAMES[res[cls]] x1, y1, x2, y2 [int(v) for v in res[box]] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, plate_type, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow(plate, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这个骨架把「单帧检测」和「时序稳定」拆成两个独立模块PlateRecognizer.detect()只做图像推理PlateTracker.update()只做跨帧决策。这两个模块可以分别优化——比如在 GPU 服务器上把detect()换成 TensorRT engine而PlateTracker不用改一行代码。注意NMSBoxes需要 OpenCV 4.1否则换成onnxruntime里的自定义 NMS 或轻量实现。识别到稳定车牌后下一步是把车牌区域裁剪下来送进 OCR 引擎做字符识别。yolov8 只负责「找到车牌和判断类型」字符识别可以用 PaddleOCR 的 PP-OCRv4直接喂整张图或者轻量 CNN LSTM CTC 的 LPRNet只需要车牌区域图更适合边缘设备。类型分类结果可以作为 OCR 的前置知识——比如类型是「绿牌」OCR 的字符集就锁定为「数字 D/F 字母 省份简称」能显著提升识别置信度和速度。时序稳定这块还有一个更彻底的做法把 5~10 帧的检测结果做加权融合不只是投票而是把多帧的检测框坐标做平滑EMA类型概率做平均。这能进一步消除抖动但对实时性要求高的场景比如车速 60km/h 通过卡口帧间车牌位移可能超过 1/3 帧宽IOU 匹配会失败这时候要引入卡尔曼滤波预测下一帧位置。车牌识别项目做 100 个有 90 个的真正难点不在模型选型而在这种工程边界——模型结构用 yolo 系的现成方案就行时间和踩坑都花在数据质量、阈值适配和时序处理的取舍上。本文还有配套的精品资源点击获取