
从 YOLO 演进到实时视频 AISmartMediaKit 的集成思路与技术实践接触目标检测的这些年我眼看着 YOLO 从实验室里的玩具变成生产环境中的主力。很多团队一开始是在图片上跑 YOLO跑通一张图就开心半天但一旦需要处理视频流、对接业务系统、部署到边缘设备才发现“能检测”和“能落地”之间隔着一整条流水线。我自己踩过不少坑也在 RK3588、x86 服务器、树莓派上反复折腾过部署方案最后沉淀下来一套相对成熟的集成思路也就是这套围绕 SmartMediaKit 构建的实时视频 AI 管线。这篇文章不是 YOLO 原理课而是一个从“单帧检测”走向“实时视频 AI”的工程实践记录适合那些已经能跑通 YOLO 训练、正准备把模型塞进视频业务里的开发者。先说清楚 SmartMediaKit 在这个架构里的角色。它不是一个替代 YOLO 的检测算法也不是一个花哨的 GUI 工具而是把视频接入、解码、推理调度、后处理、结果分发串起来的中间层。你可以把 YOLO 看成一辆性能不错的引擎把 SmartMediaKit 看成整车底盘和传动系统——没有后者引擎再猛也只能在台架上空转。这篇文章会从整体设计讲到核心代码再讲到边缘部署和问题排查全程围绕“实时视频 AI”这条主线展开。1. 从“单帧检测器”到“视频 AI 引擎”SmartMediaKit 的定位与整体思路1.1 YOLO 真正解决的是什么问题YOLOYou Only Look Once的核心贡献在于把目标检测定义成一个端到端的回归问题。早期的两阶段检测器先做区域提议再分类精度不错但速度太慢难以满足视频场景的帧率要求。YOLO 把整张图划分成网格每个网格负责预测目标中心点落在自己区域内的检测框、类别和置信度一次前向推理就能拿到所有目标信息。这种设计天然适合视频流——每一帧都独立检测不用跨帧缓存复杂状态。但这里有个容易被忽略的事实YOLO 从头到尾都是一个单帧检测器。它接收的输入是一张静止图像输出也是针对这张图像的检测结果。所谓“视频检测”本质上就是把视频抽帧逐帧送入 YOLO再把结果按时间顺序拼起来。如果你只是把视频帧丢给 YOLO 然后拿到结果就完事那只是一个“用 YOLO 处理视频”的小工具还称不上“实时视频 AI”。真正的视频 AI 需要考虑帧率匹配、检测结果的时间平滑、目标消失与重现的跟踪逻辑、告警事件的去重与聚合以及如何在资源有限的情况下保证推理吞吐。1.2 为什么单独使用 YOLO 无法构成实时视频 AI我见过不少团队一开始的架构很简单OpenCV 读取视频帧预处理后送进 YOLO拿到结果画框显示。这个 Demo 在本地视频上跑得挺好一换到网络摄像头就崩了——解码跟不上、推理占用过高、画面延迟越来越严重最后进程直接卡死。问题出在哪出在缺少对整条数据链路的管理。实时视频 AI 至少包含以下环节视频接入处理 RTSP、RTMP、GB28181、本地文件等多种协议还要处理网络抖动导致的断流重连。解码视频编码格式复杂H.264/H.265解码器的性能直接决定你能同时处理多少路流。帧率控制摄像头可能 25fps但推理可能只能跑到 12fps需要决策是丢帧还是降采样。推理调用 YOLO 模型进行前向计算这里涉及模型格式、精度、加速库的选择。后处理NMS非极大值抑制、置信度过滤、类别映射还有针对业务场景的规则判断比如吸烟检测需要看手部区域是否有烟头。结果分发检测结果要推送到告警平台、数据库、WebSocket 或者消息队列供上层业务消费。监控与恢复推理线程崩溃、显存泄漏、解码器卡死这些都要有守护机制。这些环节如果全部用手写胶水代码拼起来每一路流都要处理一遍代码量爆炸且无法复用。SmartMediaKit 的思路就是把这条链路抽象成可编排的流水线组件让开发者只需要关心“业务逻辑”和“模型本身”而不是每次都在解码和线程管理上重复造轮子。1.3 SmartMediaKit 的模块划分与集成思路我搭建 SmartMediaKit 时按照职责边界把系统拆成了几个核心模块模块职责关键点SourceManager视频源接入与生命周期管理支持 RTSP / RTMP / 本地文件 / 网络摄像头DecodePipeline硬件解码与软解码调度优先硬解回退软解处理 B 帧/PTSFramePool帧缓冲池控制内存占用逐出策略保证不堆积InferenceEngine模型加载、推理执行支持 ONNX / TensorRT / RKNN动态 batchPostProcessorNMS、过滤、业务规则可插拔规则链事件聚合与去重EventDispatcher结果分发WebSocket / Kafka / HTTP 回调异步非阻塞这七个模块合在一起就构成了一条可运行的实时视频分析管线。集成 YOLO 时最核心的是 InferenceEngine 与 PostProcessor 这两个模块因为模型本身并不关心视频协议和线程模型它只关心输入张量。而 SmartMediaKit 做了一件关键的事把视频帧统一包装成带时间戳的推理请求让 YOLO 模型完全感知不到自己处理的是视频而非单张图片。2. 核心环节拆解推理流水线中的决定成败的细节2.1 数据输入帧率控制与缓存策略视频流输入是整个链路的起点也是问题的高发区。RTSP 流本身有网络延迟和抖动如果解码器跟不上帧缓冲区就会堆积延迟指数级上升。我在 SmartMediaKit 里实现了一个可配置的帧池默认容量是 2 秒的帧数比如 25fps 就放 50 帧超过容量后采取“丢弃最旧帧”的策略保证推理端拿到的始终是较新的帧。帧率适配是一个值得展开讲的问题。假设摄像头输出 25fps模型推理只能处理 10fps你该怎么办粗暴的方案是每帧都送到推理节点让推理积压更好的方案是在 SourceManager 层做步长抽帧或者根据推理时延动态调整抽帧间隔。我实测下来简单按固定步长抽帧比如每 2 帧取 1 帧在多数监控场景已经够用因为画面内容变化通常没有摄像头标称帧率那么快。注意抽帧不等于降低检测精度。在目标移动极快的场景如高速出入口不要盲目抽帧否则会漏掉关键目标。此时优先考虑提高推理吞吐而不是降低输入帧率。2.2 推理后端选择PyTorch、ONNX、TensorRT 的取舍同一个 YOLO 模型可以用不同的推理后端运行速度差异可能是倍级的。我的经验是分阶段选型开发调试用 PyTorch集成测试用 ONNX Runtime正式部署按硬件选 TensorRT 或 RKNN。PyTorch 直接加载 .pt 权重最方便但动态图和 Python 运行时开销大在视频流高并发场景下容易出现 GIL 竞争和显存抖动。ONNX Runtime 是从 PyTorch 到生产环境的过渡带导出 ONNX 时需要注意动态维度配置我习惯把 batch 维度和宽高维度都设成动态否则模型只能接收固定分辨率输入。TensorRT 在支持 CUDA 的 x86 平台上是性能天花板尤其是 FP16 精度下吞吐能翻一倍多但转换过程中的算子兼容问题需要花时间排雷。RK3588 这类边缘设备走的是另一条路需要用 RKNN-Toolkit 把 ONNX 模型转换成 RKNN 格式。这个转换过程踩过不少坑后面专门讲这里先强调一条原则针对具体硬件做推理后端选型不要迷信“通用万能”的部署方案。2.3 后处理从原始输出到业务告警的距离很多人把 YOLO 的后处理简单理解为“跑一次 NMS”但在实时视频 AI 场景这远远不够。YOLO 模型的原始输出是一个巨大的张量包含大量低置信度框后处理需要完成以下步骤阈值过滤只保留置信度高于阈值的检测框。NMS 去重多个重叠框合并成一个IoU 阈值通常取 0.45 左右。类别映射把模型输出的 class id 映射到业务名称。坐标换算把归一化坐标变回原始画面的像素坐标。业务规则判定比如区域入侵检测需要判断目标中心点是否在多边形区域内吸烟检测需要判断烟盒区域与嘴部区域是否靠近、是否持续出现。第 5 步最容易被忽视。我一开始做烟火检测时只做普通 NMS 就上报告警结果每几秒就触发一次误报——因为画面里一闪而过的塑料袋也被当成目标。后来我在 PostProcessor 里增加了“连续多帧确认 告警冷却时间”机制同一目标至少连续 3 帧出现在告警区域才触发事件事件发生后 5 秒内不重复上报。这个策略直接砍掉了 80% 的误报。2.4 性能瓶颈解码、推理、编码之间的调度关系实时视频 AI 是 IO 密集 计算密集的混合体性能瓶颈往往不在推理本身而在解码和预处理。我在 x86 服务器上测试时发现用 OpenCV 的cv2.VideoCapture读 RTSP 流CPU 自动软解 1080p H.264 视频可能占到 40% 左右的 CPU 资源留给推理的算力所剩无几。解决办法是启用硬解Intel 平台用 VA-APINVIDIA 平台用 NVDECRK3588 用 MPP。SmartMediaKit 中的 DecodePipeline 专门处理了这个问题设计上有几条经验解码线程和推理线程分离用有界队列解耦避免解码卡顿直接阻塞推理。每路流一个解码器实例不要多路流共享一个解码器否则会产生 PTS 错乱。分辨率缩放放在推理前而非推理后YOLO 输入尺寸统一缩放到模型要求的大小但画框要映射回原始分辨率避免直接在小图上标注。3. 从数据集到模型高质量模型是一切实时应用的起点3.1 标注工具与数据集管理YOLO 模型的效果天花板由数据集质量决定这是无论换什么网络结构都无法突破的。标注工作最忌讳“差不多就行”一个边界框偏了几个像素对大目标影响不大但对于小目标检测比如监控画面中的人脸、烟火点可能是致命误检。我和团队现在常用的标注工具是 X-AnyLabeling支持自动预标注可以用已有模型先跑一遍生成粗标签人工在此基础上修正效率能提升两三倍。管理上所有标注数据统一放在一个目录结构下dataset/ images/ train/ val/ labels/ train/ val/ data.yamldata.yaml里声明类别列表和训练验证集路径这是 YOLO 系训练工具的通用约定。注意标注类别 ID 必须从 0 开始连续编号中间缺一个数字都会导致训练时类别映射错乱。3.2 格式转换KITTI 标注转 YOLO 的实战业界常用数据集格式不少KITTI 用于自动驾驶目标检测COCO 用于通用物体检测路径规划类的还有各种自定义文本格式。做工程集成时格式转换是躲不过的。KITTI 标注是单个 TXT 文件里写入类别、框坐标、截断、遮挡等信息转换到 YOLO 格式需要提取关键字段并做归一化。写转换脚本的核心逻辑是把 KITTI 的 box 坐标左上角 x、左上角 y、右下角 x、右下角 y换算成 YOLO 的 center_x、center_y、width、height并除以图像宽高。这里有个常见的坑KITTI 图像和标签中的坐标可能经过缩放导致转换后框偏移。转换前务必确认原图尺寸必要时先恢复原始分辨率再做归一化。我在实际脚本里还会加一个“可视化校验”步骤把转换后的 YOLO 标签画回图片上人工抽查几个样本这一步能拦截 90% 的坐标错位问题。COCO 格式转 YOLO 同样常见思路是利用 COCO 的 annotations 字段中的 bbox 数组它本身就是 [x, y, width, height] 格式相对简单但仍需注意宽高是否为负数、坐标是否超出图像边界等数据质量问题。3.3 训练参数建议batch size、学习率与数据增强训练 YOLO 自己的数据集参数设置直接影响模型收敛效果。分享几个我实测后的参数组合输入分辨率640x640 是通用选择资源充足时可以上 1280 提升小目标检测能力但推理速度会下降明显。batch size单卡训练时尽量开大但受显存限制。一般 8 或 16 比较均衡混合精度训练可以再翻倍。学习率初始学习率推荐 0.01 或按 batch size 线性缩放。我习惯配合 warmup前 3 个 epoch 线性上升到目标学习率能避免初期 loss 爆炸。数据增强Mosaic 增强对密集小目标效果显著但训练到后期建议逐步关闭否则会抑制模型对真实尺度的学习。epoch 数小数据集几百张图从 100 轮起步看验证集 mAP 不再提升就早停。3.4 什么情况下需要考虑改进模型结构YOLO 版本迭代很快从 v5、v8、v9、v10 到 v11 各有侧重。我在实际项目里不会盲目追新版本而是根据任务特性选型通用目标检测YOLOv8 / YOLOv11 是稳妥起点前者的工程生态成熟后者的 C3k2 模块和注意力机制有提升。姿态估计YOLOv8-pose 可以直接输出人体关键点做吸烟、跌倒检测很合适。实例分割YOLOv9-seg / YOLOv11-seg 支持像素级分割适合施工区域安全帽检测这类精细任务。边缘部署YOLOv5 的 ONNX / RKNN 转换链路最成熟如果追求轻量化可以考虑 YOLO 系列的 nano 版本或是 NanoDet、RTMDet 等替代方案。至于“改进模型”我建议先想清楚要解决什么问题。是精度不够、小目标漏检还是推理太慢如果是精度问题优先增加数据、调超参而不是改网络结构。只有当你对数据增强和调参已经尽力、仍然不满意时才考虑引入注意力机制如 CBAM、更换 Neck 结构如 BiFPN或做多模态融合。我做过的“YOLO 多模态融合”实践是在红外和可见光双路输入时把特征在 Backbone 层做早期融合效果确实优于单模态但模型体积和推理开销也同步上升部署时要仔细权衡。4. 实操全流程把 YOLO 接入 SmartMediaKit 并跑通实时视频分析4.1 环境准备与依赖安装以一台 Ubuntu 20.04 的 x86 服务器为例需要安装以下基础组件Python 3.8建议 3.10 或 3.11CUDA 11.8 cuDNN 8.6如果走 NVIDIA 推理ONNX Runtime GPU 版或 TensorRT 8.x视频处理相关FFmpeg 4.x、OpenCV 4.x源码编译开启 GStreamer 支持更好Python 依赖numpy、opencv-python、ultralytics、fastapi、websockets依赖安装的坑主要在 OpenCV 上。PyPI 上默认的opencv-python不带 FFmpeg 支持读取 RTSP 时可能报错或者异常卡顿。我的经验是直接用apt install python3-opencv搭配系统 FFmpeg或者从源码编译开启 FFmpeg 选项这样 RTSP 解码的稳定性好得多。4.2 对视频流执行推理的核心代码SmartMediaKit 的 InferenceEngine 层我封装成了一个统一的推理类。核心思路是无论底层是 PyTorch、ONNX Runtime 还是 TensorRT对外都暴露同一个infer(frame_batch) - detections接口。这样业务层不关心模型具体跑在哪个后端切换推理后端时只需要改配置。下面是一个基于 ONNX Runtime 的推理节点简化实现实际项目里我加上了 TensorRT 分支和 RKNN 分支但结构保持一致import cv2 import numpy as np import onnxruntime as ort class YOLOInference: def __init__(self, onnx_path, input_size640, conf_thres0.25, iou_thres0.45): self.input_size input_size self.conf_thres conf_thres self.iou_thres iou_thres so ort.SessionOptions() so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL self.session ort.InferenceSession(onnx_path, so, providers[CUDAExecutionProvider, CPUExecutionProvider]) self.input_name self.session.get_inputs()[0].name def preprocess(self, frame): img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img, ratio, (pad_w, pad_h) self.letterbox(img, new_shape(self.input_size, self.input_size)) img img.transpose(2, 0, 1).astype(np.float32) / 255.0 img np.expand_dims(img, axis0) return img, ratio, pad_w, pad_h def letterbox(self, img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw (new_shape[1] - new_unpad[0]) / 2 dh (new_shape[0] - new_unpad[1]) / 2 if shape[::-1] ! new_unpad: img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img, r, dw, dh def postprocess(self, output, ratio, pad_w, pad_h, orig_shape): # output shape: [1, 84, 8400] for YOLOv8, need transpose predictions output[0].transpose((0, 2, 1)) # [1, 8400, 84] boxes [] for pred in predictions[0]: class_scores pred[4:] class_id np.argmax(class_scores) confidence class_scores[class_id] if confidence self.conf_thres: continue cx, cy, w, h pred[:4] x1 (cx - w / 2 - pad_w) / ratio y1 (cy - h / 2 - pad_h) / ratio x2 (cx w / 2 - pad_w) / ratio y2 (cy h / 2 - pad_h) / ratio x1 max(0, min(x1, orig_shape[1])) y1 max(0, min(y1, orig_shape[0])) x2 max(0, min(x2, orig_shape[1])) y2 max(0, min(y2, orig_shape[0])) boxes.append([x1, y1, x2, y2, confidence, class_id]) # NMS keep self.nms(boxes, self.iou_thres) return [boxes[i] for i in keep] def nms(self, boxes, iou_thres): if not boxes: return [] boxes np.array(boxes) x1, y1, x2, y2, scores boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3], boxes[:, 4] areas (x2 - x1) * (y2 - y1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) if order.size 1: break xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) inter np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1) iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou iou_thres)[0] order order[inds 1] return keep def infer(self, frame): orig_shape frame.shape[:2] img, ratio, pad_w, pad_h self.preprocess(frame) output self.session.run(None, {self.input_name: img}) detections self.postprocess(output, ratio, pad_w, pad_h, orig_shape) return detections这个类就是 SmartMediaKit 中推理节点的核心。letterbox是 YOLO 系标准的等比缩放补边操作后处理里面的坐标换算绕过了很多教程里的错误做法——直接用缩放后的坐标画框导致框偏移。这里的ratio和pad_w/pad_h是同步传回去的保证标注坐标始终回回到原图尺度。在 PostProcessor 中我还会对detections做业务层加工。比如做区域入侵检测时会预先输入一个多边形列表然后判断检测框中心点是否在多边形内并且要维护一个“目标 ID 表”来实现“连续 N 帧命中才算事件”的确认逻辑。这一步往往才是业务方真正关心的功能。class RegionIntrusionRule: def __init__(self, polygon, min_frames3, cooldown5.0): self.polygon polygon # [[x1,y1],[x2,y2],...] self.min_frames min_frames self.cooldown cooldown self._tracker {} self._last_alarm_times {} def check(self, detections, timestamp): triggered_targets [] for det in detections: class_id int(det[5]) cx (det[0] det[2]) / 2 cy (det[1] det[3]) / 2 if self._in_polygon(cx, cy): key (class_id, round(cx // 20), round(cy // 20)) self._tracker[key] self._tracker.get(key, 0) 1 if self._tracker[key] self.min_frames: if timestamp - self._last_alarm_times.get(key, 0) self.cooldown: triggered_targets.append(det) self._last_alarm_times[key] timestamp else: self._tracker[key] 0 return triggered_targets这种min_frames加cooldown的组合是我在生产环境里调出误报率最低、漏报率可接受的一套方案。直接把检测结果上抛的业务系统上线第一周就会被告警轰炸到关停。4.3 边缘设备部署RK3588 场景的专项适配RK3588 是边缘部署的常见芯片8 核 ARM NPU 6 TOPS 算力。把 YOLO 模型跑在这个平台上最大的工作量在模型转换。我用 RKNN-Toolkit2 的经验是转换前置操作先把 PyTorch 模型导出为 ONNX尽量用 opset 12避免过高版本导致 RKNN 不支持。量化RK3588 上的 NPU 通常优先走 INT8 量化量化需要准备一张校准数据集建议选 100~200 张有代表性的图覆盖各种光照和目标形态。输出格式部分 RKNN 模型转换后输出格式和原始 YOLO 不完全一致需要在后处理层做适配。比如 YOLOv8 的 ONNX 本来就带了后处理部分导出时可以选择不带后处理的原始输出交给 RKNN 的 Python/C 接口做 NMS。输入格式RKNN 推理默认输入是 NHWC 布局而 PyTorch 习惯是 NCHW转换后推理前必须正确转换字节顺序否则检测结果直接错乱。我实际在 RK3588 上跑 YOLOv8n 的经验数据是输入 640x640INT8 量化后单路推理大约 30ms加上前后处理跑实时 25fps 的输入流没问题可以同时接 4 路摄像头。4.4 事件回调与告警输出推理结果只有发给业务系统才会产生价值。SmartMediaKit 的 EventDispatcher 组件统一处理事件分发我常用的订阅方式有两种WebSocket 推送适合实时展示面板前端拿到检测结果后直接在画面上叠加框。心跳机制要单独做否则断线后客户端完全无感知。HTTP 回调适合对接告警平台。发送 JSON 数据包包含事件 ID、时间戳、摄像头 ID、检测框坐标、置信度、截图的 Base64 编码。回调失败是最常见的生产问题之一。业务接口可能超时或拒绝如果回调线程是阻塞式的整个事件队列会越积越多。我的做法是引入独立的回调 Worker失败后重试 3 次仍失败则写入本地磁盘队列供运维侧拉取补推。class EventDispatcher: def __init__(self, webhook_urlNone, max_retry3): self.webhook_url webhook_url self.max_retry max_retry self._queue queue.Queue(maxsize1000) self._worker threading.Thread(targetself._run, daemonTrue) self._worker.start() def publish(self, event): try: self._queue.put_nowait(event) except queue.Full: # 队列满时丢弃非关键事件或者落盘 logger.warning(event queue full, drop event) def _run(self): while True: event self._queue.get() for attempt in range(self.max_retry): try: requests.post(self.webhook_url, jsonevent, timeout2) break except Exception: time.sleep(0.5 * (attempt 1))这段代码看起来简单但把“异步化”“重试”“熔断”三个关键点都覆盖了。生产环境里千万别在推理线程里直接发 HTTP 请求一个慢接口就能拖垮整条视频链路。5. 常见问题排查与调优实录5.1 帧率上不去问题到底在哪遇到帧率低我一般按以下顺序排查解码是否成为瓶颈观察 CPU 使用率和解码线程的队列积压。如果解码线程积压持续增长说明解码跟不上优先开启硬解。预处理是否拖慢多次cv2.resize和颜色转换很耗时。用profile找到耗时函数必要时把图像缩放从 Python 层移到 C 扩展或 GPU 上。推理后端是否生效确认 ONNX Runtime 真的使用了 CUDA provider而不是静默回退到 CPU。打印session.get_providers()查看一下。后处理是否过度如果单帧检测到 200 个目标纯 Python 的 NMS 循环会吃掉大量时间。可以用向量化的 numpy 实现或者用 C 编排的 NMS 算子。我在一次客户现场排查时发现对方“已经用了 TensorRT”但实际推理耗时高达 120ms。最后定位到问题他们加载的是 FP32 引擎而不是 FP16。换成 FP16 后推理时间直接降到 35ms。这个案例让我养成了检查推理引擎精度的习惯。5.2 检测框乱跳与漏检是模型问题还是后处理问题检测框在视频流里前后帧抖动可能是两个原因单帧检测本身的随机性阈值临界的目标容易被丢弃导致同一目标时而出现时而消失。没有做时间平滑即使目标一直在画面里检测框也在小幅漂移直接画在视频上会显得非常不稳定。我的处理方案是将检测结果接入一个轻量级的轨迹平滑模块不一定要上 ByteTrack 或 SORT 这类完整追踪器简单用“最近邻匹配 指数移动平均”就能消除 80% 的视觉抖动。核心代码并不复杂为每个检测框记录历史坐标然后用平滑系数更新当前输出坐标。漏检则大多与输入和模型有关。首先检查是否开启了合理的置信度阈值我默认用 0.25业务上想减少漏报可以降到 0.15但误报会略升。其次检查图像缩放方式如果直接用cv2.resize拉伸到模型输入尺寸目标比例失真会影响检测换成letterbox基本可以解决。5.3 显存和内存泄漏排查长时间跑视频分析最怕的就是内存/显存一路上涨最后进程被系统杀掉。这类问题通常来自OpenCV Mat 对象未释放Python 的cv2对象有 GC 管理但如果你在循环里保留了帧引用还是会造成内存增长。图模式切换PyTorch 中每次推理都重新计算图会导致显存碎片化尽量用torch.no_grad()和torch.inference_mode()并把模型切换到 eval 模式。ONNX Runtime 的 IO 绑定如果每帧调用都新建输入输出张量会造成显存碎片。建议用预分配的 OrtValue 做 IO Binding。排查工具方面我通常用nvidia-smi实时查看显存、用psutil记录进程内存曲线。进一步可以开启torch.cuda.memory_summary()打印显存分配详情找到泄漏的具体张量。5.4 多路视频并发的性能分配同时处理多路视频时不能简单地为每路流启一个进程。更好的做法是采用共享推理引擎的方式多路视频帧统一进入一个较大的 batch 进行推理。这充分利用了 GPU 并行能力避免了多路推理时频繁切换带来的开销。SmartMediaKit 中实现了一个动态 Batch 机制思路是多个 SourceManager 线程把自己的帧投递到 BatchingQueue。BatchingQueue 每隔 8ms可配收集一次队列中的视频帧凑成一个 batch 送入推理引擎。推理完成后按 batch 索引拆回各路流的检测结果。这个方案在 8 路视频场景下实测GPU 利用率可以从单路推理的 30% 提升到 80% 以上可以说是多路场景下性价比最高的优化手段。需要注意的是如果各路流的输入分辨率不一致动态 Batch 需要预处理成统一尺寸否则无法拼成一个张量。在做这套实时视频 AI 集成的过程中我最大的体会是YOLO 模型本身只占整个系统的一小部分真正的工程量在视频链路、并发调度、事件管理和部署适配这些“看不见”的地方。如果你也在搭类似的平台我建议先别急着堆功能把视频接入、推理抽象、结果分发这三层骨架搭稳再往里面填模型和业务规则。最后再分享一个小技巧所有实时视频分析系统上线之前一定要做 7x24 小时的稳定性测试很多人死在长时间运行的隐性泄漏和线程异常上而不是死在模型精度上。把日志和监控指标留好后续调优会轻松非常多。