
1. 从锚框到端到端为什么 YOLO 依旧是实时视频 AI 的底座1.1 选型变化YOLOv5 到 YOLO11我到底该追哪一代做实时视频 AI 的同学很多人的第一个检测模型都是 YOLO。这几年 YOLO 的版本号涨得比手机芯片还快从 YOLOv5 到 YOLOv8再到 YOLO11现在官方 Ultralytics 已经用年份命名了中间还夹杂着一堆社区变体。每次新版本发布讨论区总有人在问“要不要换”我的态度很明确先搞清楚自己场景的瓶颈在哪再决定跟不跟版本别为了换而换。YOLOv5 是过去几年工程化最成熟的一代生态完善部署资料遍地都是。YOLOv8 把 C2f 结构和 Anchor-Free 头做进了主干训练更稳小目标召回有提升。YOLO11 在分类、检测、分割、姿态这四条任务线上都有更新推理速度更快但说实话对大部分业务场景来说v8 到 v11 的精度提升没有质变真正的质变在部署方式和推理效率上。我自己在项目里常用的选型判断逻辑是这样的版本优势适合场景不建议的场景YOLOv5生态最全、板上钉钉的成熟快速落地、老设备兼容需要 SOTA 小目标精度YOLOv8结构均衡、训练稳定通用检测、分割、姿态多任务极高并发且资源紧张YOLO11推理更快、API 统一边缘设备、实时视频高帧率没有调优预算的存量项目YOLO-NAS / YOLOX 等各有特性特定竞赛、特殊精度需求需要长期维护的工程所以如果你现在要启动一个实时视频 AI 项目我建议直接选 YOLOv8 或 YOLO11。原因不光是精度更是 Ultralytics 这套 Python API 和导出链路的完整程度从训练到 ONNX 再到 TensorRT几乎是无缝的对做集成的人来说省掉的是最磨人的格式转换时间。1.2 损失函数和标注数据是怎么影响模型上限的很多做集成的同学容易忽略一个问题模型效果的上限其实在训练数据标注那一刻就被定死了。损失函数只是把标注里的信息逼出来。这里把 YOLO 常用的几个损失项拆开说。检测头里一般有三类损失Box Loss回归预测框和真实框的位置差异。YOLOv8 和 YOLO11 里用的多是 CIoU 或 WIoU 这一类 IoU 变体CIoU 会把中心点距离、宽高比都考虑进去比早期 YOLOv3 的 MSE 直接回归要稳很多。Class Loss分类损失一般用 BCE二分类交叉熵处理多标签问题。这里要注意YOLO 的分类头不用 Softmax因为一个目标理论上可以同时属于多个类别。DFLDistribution Focal Loss把边框坐标看成离散分布去学习是 v8 之后精度提升的一个关键点。对于实时视频项目损失函数层面最容易踩的坑是正负样本分配策略。YOLOv8 的 Positive Sample Assignment 是自适应的它会根据每个 GT 框自动决定哪些 anchor point 负责预测。但如果你自己的数据和 COCO 分布差异极大比如全是小目标默认分配策略可能不太够这时候就得改损失权重或者加数据增强。再说标注。我用过 KITTI 数据也转过自定义数据集最大的感受是标注质量对视频 AI 的影响比模型结构还大。框画偏了 5 个像素边界模糊遮挡目标没标——模型在训练时会把这些错误当成“标准答案”去拟合结果就是在视频流里出现时好时坏的抖动检测。所以我在带项目时有一个铁律训练集里每一张图都必须人工抽检重点关注三个问题类别标签是否错乱最常见的错法是把近似的类别标混遮挡严重的目标是否漏标小目标在压缩后的图像里是否还“看得见”压缩是一个很多人忽略的隐性杀手。摄像头码流经过 H.264/H.265 压缩后小目标可能只剩下几个像素的纹理标注的时候在原始帧上勉强能看见但模型推理时才真的看到的是解码后的图像。所以标注前最好用和线上推理完全相同的解码参数去抽取样本别用原始视频截图去标。1.3 实例分割和检测的边界什么时候该上 MaskYOLO 系列现在已经不止做框检测了。YOLOv8-seg 和 YOLO11-seg 都能做实例分割每个目标输出一个二值 Mask。但实际项目里我遇到太多人把分割当万能药什么问题都往分割上靠。先给个最简单的决策标准只需要定位“有没有、在哪、几个”→ 用检测别碰分割需要知道“目标的精确轮廓、面积、旋转角度”→ 用分割需要区分重叠目标、密集场景如货架上的商品、人群计数→ 分割的效果通常比检测跟踪好Mask 的代价比 Box 大得多。同样的输入分辨率下分割头的计算量可能是检测头的 2 到 3 倍。在实时视频 AI 里这意味着你原本能跑 30 FPS 的链路换成分割可能就掉到 15 FPS。我做过一个工厂场景的项目一开始用检测框统计工人是否进入危险区后来客户要求精确到“人身体的哪部分越线”。这时候才需要 Mask而且我用的是降采样之后的分割结果做边缘判定而不是对全帧做高分辨率分割。策略是先用检测框把目标区域裁出来只对裁剪区域做分割将 Mask 映射回原图坐标这样比全帧分割省一半以上的计算量精度损失微乎其微。至于 SAM2 和 YOLO 的关系我的看法是SAM2 是个通用分割大模型适合做交互式标注或者零样本分割不适合实时视频里每一帧都跑成本根本扛不住。实用的做法是用 SAM2 辅助标注生成精细 Mask再拿这些数据去训练一个轻量 YOLO-seg线上用 YOLO-seg 推理。2. 实时视频 AI 的整体基建一条视频帧的完整旅行2.1 从摄像头到浏览器一条链路里藏着的延迟黑洞做实时视频 AI最容易踩的坑是只盯着模型推理时间忽略了整条链路的耗时。模型从 30ms 优化到 15ms结果播放器端延迟还是 3 秒问题往往出在视频拉流、编码、传输和播放缓冲上。一条典型的实时视频 AI 链路长这样摄像头RTSP/GB28181/WebRTC 推流 → 流媒体接入层拉流、鉴权、协议转换 → 解码硬解 H.264/H.265 → NV12/RGBA → AI 前置处理缩放、归一化、通道转换、堆叠 Batch → 推理引擎TensorRT / OpenVINO / ONNX Runtime → 后处理NMS、过滤、置信度阈值、跟踪器关联 → 结果叠加在帧上绘制框/掩码/标签 → 编码H.264/H.265 硬编 → 分发WebRTC / 低延迟 HTTP-FLV / HLS → 播放器解码渲染每一步都有延迟但量级差别很大。模型推理通常只占 10%~20% 的端到端延迟真正的大头在播放缓冲和传输协议。举一个实测数据。同一个 1080p 场景我用三套方案对比延迟方案端到端延迟适用场景HLS常规切片 6s8~15 秒监控回放、直播点播不追求实时HTTP-FLV无缓冲1~3 秒Web 端低延迟直播兼容性最好WebRTC强制低延迟200~500ms实时交互、AI 结果即时展示如果你做的是“视频 AI 预警”希望在事件发生后 1 秒内把结果推给用户就必须走 WebRTC 或定制低延迟 FLV。这也是 SmartMediaKit 这类中间件在集成时要重点解决的事。2.2 AI 推理结果如何与视频帧绑定时间戳、帧号和跟踪器实时视频 AI 和单张图片 AI 最大的区别是你在每一帧上跑出来的检测结果必须能稳定关联到同一个物理目标。不然这个画面里框住的是人 A下一帧框住的可能是人 B。这个问题的核心有两点帧同步AI 推理是异步的处理速度不一定跟得上视频帧率。当推理结果返回时这一帧可能已经过去了怎么把结果映射回正确的帧目标关联需要跟踪器给每个目标分配全局 ID跨帧维持身份。我在 SmartMediaKit 的集成里帧同步这块采用的办法是在 AI 模块入口维护一个“帧队列”每一帧进来先登记frame_id和timestamp用 PTS解码时间戳别用系统时间当推理引擎完成时把结果按frame_id回填到对应帧再进入后续叠加和编码模块。这样即使推理延迟了十几毫秒结果也能精确落在正确帧上。跟踪器选型上ByteTrack 是目前性价比最高的选择它不需要 ReID 特征只靠检测框之间的 IoU 和运动信息做关联速度极快在遮挡不严重的场景下已经够用。如果场景里人群密集、遮挡严重再考虑 DeepSort 或 BoT-SORT。我这里有一个小经验跟踪器的输入不一定要全帧检测框可以对 ROI 区域单独加大检测密度。比如人形跟踪先把全图跑一遍小模型找出“可能有人”的区域然后对这些区域用更高分辨率的模型精检。这种级联方式在算力有限时非常实用。3. SmartMediaKit 的集成思路让 AI 成为媒体管线的一等公民3.1 SmartMediaKit 的核心设计把推理模块装进媒体流里而不是挂在外面先说清楚 SmartMediaKit 在我这个项目里的定位它是一个媒体处理工具包负责拉流、解码、编码、推流、WebRTC 分发这些脏活累活同时预留了 AI 推理的插入位置。为什么这么设计因为把 AI 全部收到媒体管线内部比“AI 服务 媒体服务分开跑”要优雅得多。早期我做视频 AI喜好是把检测服务做成一个独立 HTTP 接口媒体服务抽帧后 POST 过去再拿结果。这样做的坏处很明显每一帧都走网络协议栈序列化反序列化成本高延迟高且不稳定而且当视频源数量增多时HTTP 连接管理和超时重试能把集成方的代码搞成屎山。SmartMediaKit 的集成模式改为“嵌入式管线”媒体框架在解码出视频帧后不直接送编码器而是先经过一个AI Filter这个 Filter 在进程内调用推理引擎结果叠加后再送回媒体链路。这种方式省掉了进程间通信延迟极低而且可以精确控制帧丢弃策略——如果推理跟不上主动丢帧而不是阻塞管线。3.2 插件化注册与推理服务解耦设计 SmartMediaKit 的 AI 集成接口时我特意把它做成了插件化注册机制。每个 AI 能力检测、分割、分类都是一个独立插件通过统一的IAnalyzer接口注册到媒体框架中。// 简化的接口定义实际项目中会更多参数 class IAnalyzer { public: virtual ~IAnalyzer() default; virtual bool Initialize(const AnalyzerConfig config) 0; virtual bool ProcessFrame(const FrameData frame, std::vectorTargetResult* results) 0; virtual void Release() 0; };这样做的好处是业务方不需要懂媒体细节只需要实现ProcessFrame返回的目标结果统一用TargetResult结构体表达包含类别、置信度、检测框坐标、Mask 指针、跟踪 ID。媒体端拿到结果后统一做绘制和编码跟具体算法无关。推理服务解耦方面SmartMediaKit 里支持两种方式进程内推理适合单机单卡、延迟敏感的场景模型直接加载在同一个进程里零拷贝传递帧数据。远程推理网关适合多机扩展媒体节点把帧压缩后发到推理集群。这种情况下要格外注意网络延迟一般建议只在百兆内网或专线环境使用。我在实际项目里90% 的场景走进程内推理远程推理只在模型很大、GPU 资源需要池化的场景才用。3.3 推理结果回传与画面叠加的细节坑结果回传最容易被忽视的是坐标空间转换。推理模型通常在 640x640 或 1280x1280 的输入上跑但视频帧可能是 1920x1080叠加层还要考虑 Web 端画布的比例。千万别忘了 letterbox 之后坐标要逆变换回原图坐标这个坑我见过太多次了最后框全部画歪。另一个细节是结果数据的编码传输。如果是 WebRTC 直播场景推荐走两条通道视频通道把检测框、跟踪 ID、置信度直接渲染在帧上编码进视频流里。这样所有观看者都能看到画面不需要额外客户端逻辑。信令通道通过 WebSocket 把结构化 JSON 结果目标的类别、坐标、跟踪 ID、时间戳推给前端。前端可以做业务逻辑告警、统计、联动。我在集成时发现双通道比把数据塞进视频流 metadata 更稳妥因为 WebRTC 的 metadata 机制不同浏览器支持度不一致而 WebSocket 是普遍可用的。4. 训练数据准备的工程细节标注格式、KITTI 转换与数据校验4.1 KITTI 标注转 YOLO不只改个坐标那么简单KITTI 是自动驾驶领域用得最多的开源数据集之一但它的标注格式和 YOLO 需要的格式差异很大。很多第一次做训练的同学上网找转换脚本跑完发现类别对不上坐标算错甚至图都没对齐。KITTI 标签文件每行格式是class truncated occluded alpha bbox_x1 bbox_y1 bbox_x2 bbox_y2 dimensions_3d location_3d rotation_y其中 bbox 是绝对像素坐标左上角和右下角x 轴向右、y 轴向下。YOLO 格式需要的是class_id cx_ratio cy_ratio width_ratio height_ratio也就是目标中心点和宽高都除以图像宽高做归一化。转换时有两个易错的点class是字符串比如Car、Pedestrian必须先建立类别到class_id的映射表并保证和训练配置里的类别顺序一致。坐标要限制在[0,1]区间。KITTI 里有些框的x2/y2会超出图像边界转换后可能出现大于 1 的值YOLO 训练时会报错或者自动忽略。我一般会在转换脚本里做 clamp。import os def kitti_to_yolo(kitti_line, img_w, img_h, class_map): parts kitti_line.strip().split() class_name parts[0] assert class_name in class_map, funknown class: {class_name} x1, y1, x2, y2 map(float, parts[4:8]) x1 min(max(x1, 0), img_w) x2 min(max(x2, 0), img_w) y1 min(max(y1, 0), img_h) y2 min(max(y2, 0), img_h) cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h return f{class_map[class_name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n4.2 数据校验与划分别把脏数据喂进网络标注转完格式不代表训练一定能顺利。我每次都会写一个数据校验脚本检查这几个维度图像尺寸一致性训练时很多人用imgsz640如果原始标注是用 1920x1080 的图标的转出来的归一化坐标是没问题的但训练时的 letterbox 会改变有效区域小目标可能被压得更小。建议训练前统计所有目标的宽高分布如果小目标占比太高就得考虑加大imgsz或者用 SAHI 这类切图推理方案。空标注文件有些图像没有目标训练时会自动忽略但没有问题检测任务允许空图。类别均衡如果某一类样本只有几十个其他类几万个模型基本学不会这一类。可以给这类目标做过采样或复制粘贴增强。画分数据集时我习惯按“视频”而不是按“帧”划分。用同一个视频的相邻帧既做训练又做验证会造成严重的数据泄漏模型评测时精度虚高上线后立刻现原形。原则是同一个拍摄场景的所有帧只能出现在一个集合里。4.3 从零到一跑通自定义训练用 Ultralytics 训练自己的 YOLO 模型流程已经非常简单了。关键步骤是写准了数据集配置 YAML 文件# dataset.yaml path: /data/my_dataset train: images/train val: images/val nc: 3 names: [person, car, bicycle]然后训练命令就是一行yolo train modelyolo11s.pt datadataset.yaml imgsz640 epochs100 batch16 device0这里我给两个建议。第一pretrained权重一定要用哪怕是和自己任务差异很大的 COCO 预训练权重迁移学习的起点也远好于随机初始化。第二训练过程中盯住val/box_loss和val/cls_loss的趋势如果验证损失在某个 epoch 后开始回升说明过拟合了及时早停别傻乎乎跑完 300 个 epoch。5. Web 端实时视频 AI 的落地延迟优化与渲染叠加5.1 Web 端延迟到底从哪里来Web 端看视频 AI 结果要求的是“低延迟 稳定画面”。排查延迟问题时我建议按三段来分析采集到编码前摄像头采集延迟、解码延迟、AI 推理延迟。这部分通常在 100ms 以内可优化的空间不大。传输阶段协议选择是关键。HLS 因为切片和播放器缓冲延迟天然大WebRTC 走 UDP能控制在毫秒级。播放器缓冲这是 Web 端最大头的延迟来源。播放器为了抗网络抖动会默认缓冲 1-3 秒的数据。做实时视频 AI 的时候必须主动把缓冲调到最小宁愿画面偶尔卡顿也要保证延迟可控。5.2 WebCodecs、WebRTC 与 WASM 推理的取舍Web 端做实时视频 AI有几种技术路线视频流走 WebRTC推理结果走数据通道这是我最推荐的方案。视频质量完全依赖现有的实时传输链路前端拿到视频后只负责渲染检测结果通过 WebSocket 或 WebRTC DataChannel 传给浏览器绘制在 Canvas 叠加层上。前端自己解码帧再跑推理用 WebCodecs 解码视频帧然后丢给 TensorFlow.js 或 WebGPU 跑模型。这个方案的优点是端到端全在浏览器里隐私性好但性能和模型大小受浏览器限制一般只适合轻量级应用。用 WASM 跑YOLO模型把模型转换到 TensorFlow.js 或 ONNX Runtime Web在浏览器里跑检测。方便但速度慢一次推理可能耗时 200ms 以上不适合高帧率场景。性能对比大致如下路线帧率表现1080p 视频延迟开发成本后端推理 WebRTC 视频25~30 FPS300ms 左右中前端 WebCodecs WASM 推理5~15 FPS1s 左右高后端输出 MJPEG 前端抽帧实时性差2s 左右低5.3 帧同步与叠加层的实现细节前端渲染检测框时最关键的是视频帧和检测结果的时间戳对齐。如果后端在编码前就把框画进视频帧里前端不需要额外处理这是最简单可靠的方式。但如果采用双通道模式前端需要根据检测结果的timestamp和视频当前播放位置做匹配。我的做法是前端维护一个pendingResults队列存下最近 5 秒内的检测结果每渲染一帧时取出与当前currentTime最接近的结果绘制到 Canvas。为了避免绘制抖动可以加一个简单的平滑对同一个跟踪 ID 的框做指数移动平均。// 简化示意 const smoothBox (prevBox, newBox, alpha 0.3) { return { x: prevBox.x * (1 - alpha) newBox.x * alpha, y: prevBox.y * (1 - alpha) newBox.y * alpha, w: prevBox.w * (1 - alpha) newBox.w * alpha, h: prevBox.h * (1 - alpha) newBox.h * alpha, }; };alpha值别调太大不然框会“追”不上目标一般 0.2~0.4 之间效果比较好。6. 性能调优与部署避坑从 30 FPS 到稳定 7x24 小时6.1 推理加速选型TensorRT、OpenVINO 与 ONNX Runtime模型训练完只是万里长征第一步真正难的是让它稳定跑在 7x24 小时的视频处理进程里。推理加速层面我按部署环境给出选择建议NVIDIA GPU 环境首选 TensorRT。用onnx-tensorrt或 Ultralytics 自带的export formatengine导出FP16 精度通常能带来 2 到 3 倍加速掉点几乎可以忽略。如果对精度要求苛刻可以用 INT8 量化但需要校准数据集调起来很费时间。Intel CPU 环境OpenVINO 是合理选择。它对 Intel 平台的优化很到位而且支持动态输入形状方便处理不同分辨率的视频源。跨平台、轻量集成ONNX Runtime 是最中庸也最稳的CPU/GPU 都能跑部署最简单但性能通常不如前两者适合原型验证或低压力场景。我实测过一个 YOLOv8s 模型1080p 视频流输入 640x640推理后端平均单帧推理耗时备注PyTorch CPU120ms基本没法做实时ONNX Runtime CPU60ms勉强 15 FPSOpenVINO CPU35ms可到 28 FPSTensorRT FP16 GPU8ms轻松超过 30 FPS6.2 长时间运行的隐形杀手显存泄漏、推理队列堆积和进程抖动视频 AI 服务最大的特点是不间断运行而显存泄漏是这类服务最常见的崩溃原因。我的排查经验是如果发现进程运行 3~5 天后 GPU 显存占用逐渐上涨十有八九是以下三个原因之一TensorRT 的ICudaEngine和IExecutionContext创建了没释放。每次推理都会创建新的 context忘了释放就会持续泄漏。帧数据在 CPU 和 GPU 之间拷贝时内存没有归还给 CUDA 缓存。torch.cuda.empty_cache()只能释放 PyTorch 缓存对 TensorRT 不生效。推理结果队列只增不减。某些帧处理超时后异常路径里忘了 pop队列越积越长内存和显存双双上涨。我的解决方案很简单粗暴给推理模块加一个自愈机制。每处理 10000 帧统计一次显存峰值如果连续 5 次统计都超过首帧基线的 10%就主动重新加载模型引擎。虽然这有点“治标不治本”但在工程项目里稳定的重启策略比彻查所有泄漏路径更快落袋。另外模型推理速度必须留出 20% 的余量。比如算法每秒处理能力是 36 FPS部署时视频源帧率就控制在 30 FPS 以内超了宁可丢帧也别让队列堆积否则延迟会像滚雪球一样越来越大。6.3 部署时几个值得一试的细节优化最后分享几个我在实际部署中验证有效的小技巧开启 CUDA 的cudaMallocAsync减少显存碎片化降低内存峰值。多路视频流共享模型实例一个 TensorRT engine 可以同时给多路视频推理不用每个视频源单独加载一份模型显存节省明显。用torch.inference_mode()而不是torch.no_grad()前者会禁用 autograd 的所有记录机制在高并发推理时能挤出几个百分点的性能。给不同通道的视频分配不同的letterbox参数如果各视频源分辨率差异很大不要统一 resize 到 640可以先按长边等比缩放到不大于 640再 pad 到固定尺寸这样能省掉一部分无效计算。调试阶段可以先用单路视频跑通然后把多路视频并成 batch 推理batch_size4通常能比单路推理 4 次快 1.5 倍左右。但要注意 batch 里的帧时间戳不能乱结果回填时要按frame_id精确匹配。做实时视频 AI 这件事模型的精度和推理的速度只是冰山一角真正决定项目成败的往往是那些不起眼的工程细节——帧同步、格式转换、显存管理、延迟排查。我在这条路上踩过不少坑写出来希望能让后来的人少走几步弯路。以后有空我打算再写一篇单独聊多路视频调度和边缘设备部署的实战记录那个坑更多一次说不完。