从零搭建 YOLO 视频推理服务:双模推理 + 实时流 + 目标追踪,一个服务全搞定

14 阅读5分钟

一个服务搞定视频检测、实时流推送和目标追踪

做视频 AI 的同行都知道,推理服务这个事,单图检测容易,视频处理就麻烦了。

上传视频等半天出结果算一个场景,接摄像头 RTSP 流 7×24 小时实时推又是另一个场景。市面上开源的方案大多只解决了其中一个,而且很多时候跑通容易,跑稳难——流断了怎么办?多客户端同时看怎么搞?结果怎么优雅地推出去?

这个项目把这两个场景搓到了一个服务里,顺便把目标追踪、断流重连、Docker 部署这些生产环境的刚需也一并搞定了。如果你也有类似的视频推理需求,这个项目或许能帮到你。

项目地址:github.com/VaneBlien/y…


能干什么

一句话概括:支持两种模式的 YOLO 视频推理服务。 你可以选择:

模式输入输出场景
验证模式上传 MP4/AVI/MOV 视频文件JSON 检测结果,轮询获取离线分析、批量处理
生产模式接入 RTSP/RTMP 视频流WebSocket 实时推送实时监控、安防、工业检测

再配上一个 ByteTrack 目标追踪,同一个目标在连续帧里能分配相同的 track_id,你就可以知道"这个人在画面里移动了 3 秒"。


效果展示:

屏幕截图 2026-05-05 042827.png


架构设计

聊代码之前,先把架构讲清楚。

                    ┌──────────────────────────────┐
                    │         Nginx (反向代理)       │
                    └──────────────┬───────────────┘
                                   │
                    ┌──────────────▼───────────────┐
                    │       FastAPI 应用层          │
                    │  ┌─────────────────────────┐  │
                    │  │  文件上传接口 (REST)     │  │
                    │  │  流管理接口 (REST)       │  │
                    │  │  WebSocket 端点          │  │
                    │  └─────────────────────────┘  │
                    └──────────────┬───────────────┘
                                   │
              ┌────────────────────┼────────────────────┐
              │                    │                    │
    ┌─────────▼────────┐  ┌───────▼────────┐  ┌───────▼────────┐
    │  Celery Worker   │  │  Stream Manager │  │     Redis      │
    │  (文件异步推理)   │  │  (流常驻推理)   │  │  状态/队列/缓存 │
    └─────────┬────────┘  └───────┬────────┘  └────────────────┘
              │                    │
              └──────────┬─────────┘
                         │
              ┌──────────▼──────────┐
              │    YOLO 推理引擎     │
              │  (单例,统一推理接口) │
              └─────────────────────┘

核心思路就一条:两种模式通过同一个推理引擎单例来完成检测,但走不同的调度链路。 文件推理走 Celery 异步队列,流推理走 asyncio 协程 + WebSocket,互不干扰。


几个值得展开的技术点

1. YOLO 推理引擎:线程安全单例

模型就那么大,加载一次就够了。所以推理引擎做成线程安全的单例模式:

class InferenceEngine:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls, model_name=None):
        with cls._lock:
            if cls._instance is None:
                cls._instance = super().__new__(cls)
                cls._instance._initialized = False
            return cls._instance

    def __init__(self, model_name=None):
        if self._initialized:
            return
        self.model = YOLO(f"{model_name}.pt")
        self.model.to(self.device)
        self._initialized = True

无论是文件推理的 Celery Worker,还是流推理的 Stream Manager,都通过 get_engine() 拿到同一个引擎实例,不重复加载模型。

而且这个引擎集成了 ByteTrack 追踪器。调用 engine.infer(frame, enable_tracking=True, fps=25) 就会自动给检测结果加上 track_id

2. 文件异步推理:Celery + 进度回调

上传视频之后,接口立刻返回一个 task_id,实际推理在 Celery Worker 里进行:

@celery_app.task(bind=True, name="video_detect")
def video_detect_task(self, task_id: str, file_path: str, config: dict):
    cap = cv2.VideoCapture(file_path)
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    
    for frame_idx in range(total_frames):
        ret, frame = cap.read()
        detections = engine.infer(frame, enable_tracking=True, fps=fps)
        
        # 每处理 5% 更新一次进度
        if frame_idx % (total_frames // 20) == 0:
            self.update_state(
                state="PROCESSING",
                meta={"progress": progress, "processed_frames": frame_idx}
            )
    
    return {"status": "completed", "detections": all_detections, "summary": {...}}

前端或者调用方通过 GET /api/v1/video/result/{task_id} 轮询进度,处理中返回进度百分比,完成后返回完整 JSON。

3. 流推理:asyncio 协程 + WebSocket 广播

启动一个流后,Stream Manager 会创建一个常驻 asyncio 任务,持续拉流、推理、广播:

async def _run_stream(self, task: StreamTask):
    engine = get_engine()
    
    while task.status == StreamStatus.CONNECTED:
        ret, frame = cap.read()
        detections = engine.infer(frame, enable_tracking=True)
        
        # 广播给所有连接的 WebSocket 客户端
        await self._broadcast_detections(task, frame_count, detections)
        await asyncio.sleep(0)

一个流可以有多个 WebSocket 客户端同时订阅,各自可以设置自己的过滤条件——比如客户端 A 只需要 person,客户端 B 只需要 car,互不影响。

4. 断流重连:状态机 + 指数退避

生产环境 RTSP 流断掉是家常便饭。这里实现了一个重连状态机:

CONNECTED ──断流检测──→ RECONNECTING ──成功──→ CONNECTED
                           │
                           │失败(未达上限)
                           └──→ 等待退避 ──→ 重试
                           │
                           │失败(达上限)
                           └──→ FAILED

退避时间是 2s → 4s → 8s → 16s → 30s,最多重试 5 次。断流期间新客户端连上来会收到 stream_reconnecting 状态,重连成功后再推送 stream_reconnected

5. WebSocket 协议设计

服务端推送的消息分三类:高频的检测结果帧、低频的流状态通知、以及错误消息。客户端也可以主动发消息来控制订阅:

// 客户端发送:调整过滤条件
{"type": "subscribe", "options": {"min_confidence": 0.7, "target_classes": ["person"]}}

// 服务端推送:检测结果
{"type": "detection", "stream_id": "stream-xxx", "frame_id": 1523, "detections": [...]}

// 服务端推送:状态变更
{"type": "status", "event": "stream_reconnected", "message": "重连成功"}

性能参考

模型单帧推理耗时25fps 流(跳帧=3)建议最大并发流
YOLOv8n~10ms8.3 帧/秒~8 路
YOLOv8s~20ms~6 帧/秒~4 路
YOLOv8m~40ms~4 帧/秒~2 路

测试环境:NVIDIA T4,GPU 推理。CPU 下放个 1~2 路也能跑。


本地跑起来

# 克隆
git clone https://github.com/VaneBlien/yolo-video-service.git
cd yolo-video-service

# 虚拟环境 + 依赖
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt

# 启动 Redis
docker run -d --name redis -p 6379:6379 redis:7-alpine

# 终端1: FastAPI
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

# 终端2: Celery Worker
celery -A app.tasks.celery_worker.celery_app worker --loglevel=info --pool=solo

浏览器打开 http://localhost:8000/docs,Swagger 文档里直接调试。


Docker 一键部署

docker-compose build
docker-compose up -d

四个容器:Nginx + FastAPI + Celery + Redis,开箱即用。


路线图(做完了的和想做的)

  • 单图推理 API
  • 视频文件异步推理 + 进度轮询
  • RTSP 流实时推理 + WebSocket 推送
  • 多客户端独立订阅过滤
  • 断流指数退避重连
  • ByteTrack 多目标追踪
  • Docker 容器化部署
  • 标注视频生成(推理结果画框合成新视频)
  • 前端可视化面板
  • 多 GPU 并行推理
  • Prometheus + Grafana 监控

最后

这个项目从架构设计到全部代码落地,前后断断续续搞了一段时间。最花时间的其实不是推理本身,而是那些"看起来不起眼但没它真不行"的工程细节——断流重连怎么优雅、多个客户端怎么各自过滤、模型怎么不重复加载、进度怎么让前端知道——这些东西加起来才是从 Demo 到能用的关键。