YOLO 完全指南(七):YOLO识别工程化 (上)

从单张推理到视频工程化:Ultralytics YOLO 批量处理与逐帧落地

前言:推理不只是"跑通一张图"

很多朋友在学会用 YOLO 做单张图片推理后,会觉得"模型能用了"。但当你真正要把视觉能力落地到业务里,问题才刚刚开始:

  • 几百张图片,总不能一张张手动跑吧?批量一次 forward 真的更快吗?
  • 检测结果出来了,怎么变成 CSV 给下游业务系统用?
  • 视频要逐帧跑,满屏 30fps 每帧都推理太浪费,跳帧时框会不会一闪一闪消失?

这篇文章对照配套 notebook test_4.ipynb,把从 batch vs 逐张计时目录批量导出 CSV视频逐帧检测并落盘跳帧 + 复用上一帧标注 的完整链路走一遍。权重使用目录里实际存在的 yolov8n.pt(写成 yolo8n.pt 会直接 FileNotFoundError)。

一、如何做批量推理?

Ultralytics 的 predict 支持把多张图一次喂进去,用 batch= 控制每次 forward 的张数。

1.1 计时脚本在测什么

配套代码做了三件事:

  1. 不要污染验证集 :测试图写到独立目录 ./bench_images,不要写进 yolo_dataset/images/val
  2. 没有现成图就造噪声图 :随机 uint8 图也能走完整推理路径,专门用来比吞吐,不看检测质量。
  3. 预热可加、对比必须公平 :下面示例里逐张和 batch 都走 model.predict,并固定 device=0stream=False
python 复制代码
"""
batch vs 逐张推理 时间对比
"""
import time
from pathlib import Path

import cv2
import numpy as np
from ultralytics import YOLO

MODEL_PATH = "./yolov8n.pt"
NUM_IMAGES = 512
IMG_SIZE = 640
BATCH_SIZE = 64
TEST_DIR = Path("./bench_images")


def make_test_images(num=NUM_IMAGES, size=IMG_SIZE, out_dir=TEST_DIR):
    out_dir.mkdir(parents=True, exist_ok=True)
    existing = sorted(out_dir.glob("*.jpg"))
    if len(existing) >= num:
        print(f"[i] 已有 {len(existing)} 张测试图片,跳过生成")
        return existing[:num]

    print(f"[i] 生成 {num} 张 {size}x{size} 测试图片...")
    for i in range(num):
        img = np.random.randint(0, 255, (size, size, 3), dtype=np.uint8)
        cv2.imwrite(str(out_dir / f"test_{i:04d}.jpg"), img)
    return sorted(out_dir.glob("*.jpg"))[:num]


def bench_one_by_one(model, img_paths):
    """逐张推理:每次 forward 1 张"""
    t0 = time.perf_counter()
    for p in img_paths:
        model.predict(source=str(p), verbose=False, stream=False, device=0)
    return time.perf_counter() - t0


def bench_batch(model, img_paths, batch_size):
    """batch 推理:一次调用里按 batch_size 切分 forward"""
    t0 = time.perf_counter()
    model.predict(
        source=[str(p) for p in img_paths],
        batch=batch_size,
        verbose=False,
        stream=False,
        device=0,
    )
    return time.perf_counter() - t0


img_paths = make_test_images()
n = len(img_paths)
model = YOLO(MODEL_PATH)

t_one = bench_one_by_one(model, img_paths)
t_batch = bench_batch(model, img_paths, BATCH_SIZE)

print(f"逐张总耗时 : {t_one:.3f} s  |  {n / t_one:.1f} img/s")
print(f"Batch 总耗时: {t_batch:.3f} s  |  {n / t_batch:.1f} img/s")
print(f"加速比     : {t_one / t_batch:.2f}x")

1.2 一次实测结果(不要照抄成结论)

在本机 notebook 里,512 张 640×640、yolov8n.ptbatch=64 的结果是:

方式 总耗时 平均每张 吞吐
逐张(每次 1 张) 4.818 s 9.41 ms 106.3 img/s
Batch=64 4.721 s 9.22 ms 108.4 img/s

加速比大约 1.02x ,只省了约 2% 时间。这说明:当单张已经很快(小模型 + GPU 已经吃得较满、或 I/O / 后处理占比高)时,加大 batch 未必有数量级提升。换更大模型、更大分辨率、更慢的磁盘,数字会完全不一样------所以工程里要把这段基准留下来,改配置后重跑。

关键点batch 是"一次 predict 调用内部每次 forward 的张数";

二、图片批量推理:从文件夹到 CSV

可视化图片只是给人看的,业务系统通常要结构化记录:哪张图、什么类、置信度、框坐标。下面三张来自 yolo_dataset/images/valconf=0.25 时每张大约十几个框(人、车为主):

2.1 目录预测

predict 可以直接吃目录或文件列表,不必自己写读图循环。需要自定义字段时,遍历 Results.boxes 比依赖 to_csv() 更稳(旧版本没有这些方法):

python 复制代码
import csv
from pathlib import Path
from ultralytics import YOLO

model = YOLO("yolov8n.pt")
image_dir = Path("./yolo_dataset/images/val")

image_files = list(image_dir.glob("*.jpg")) + list(image_dir.glob("*.png"))

with open("detections.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["image", "class_id", "class_name", "conf", "x1", "y1", "x2", "y2"])

    results = model.predict(source=image_files, stream=True, conf=0.25, verbose=False)

    for r in results:
        image_name = Path(r.path).name
        boxes = r.boxes
        if boxes is None:
            continue
        for box in boxes:
            x1, y1, x2, y2 = box.xyxy[0].tolist()
            conf = float(box.conf[0])
            cls_id = int(box.cls[0])
            cls_name = model.names[cls_id]
            writer.writerow([image_name, cls_id, cls_name, conf, x1, y1, x2, y2])

print("CSV 已生成:detections.csv")

写出后每一行对应一个检测框(不是一张图一行)。

关键点stream=Trueresults 变成生成器,逐张写出 CSV,不会一次性把所有 Results 堆在内存里。几百上千张图时,这是避免 OOM 的常规设置。注意它和上一节的 batch= 可以同时存在,职责不同:一个管吞吐,一个管内存。

三、视频逐帧推理:先能写出文件,再谈窗口

用 OpenCV 读帧 + 逐帧 model(frame) 最可控:输入可以是本地文件、摄像头编号或 RTSP。落地时有两件比"会调用 imshow"更重要的事。

3.1 写出视频时的三个坑

  1. 打开失败要立刻报错 ,不要带着坏的 VideoCapture 空转。
  2. 部分视频 CAP_PROP_FPS 会是 0 ,写出时退回 25,否则 VideoWriter 参数非法。
  3. VideoWriter 的宽高必须和即将 write 的图一致 ,否则经常是静默失败、文件打不开或几乎是空的。plot() 默认不改分辨率,用原视频的 w, h 即可。

另外:循环结束后一定要 cap.release() / out.release(),否则 mp4 可能无法播放。读到文件尾 ret=False 就停。

python 复制代码
import os
from pathlib import Path

import cv2
from ultralytics import YOLO

HAS_DISPLAY = bool(os.environ.get("DISPLAY"))

VIDEO_IN = Path("video.mp4")
VIDEO_OUT = Path("yolo_video_out.mp4")

model = YOLO("./yolov8n.pt")

cap = cv2.VideoCapture(str(VIDEO_IN))
if not cap.isOpened():
    raise RuntimeError(f"无法打开视频:{VIDEO_IN}")

fps = cap.get(cv2.CAP_PROP_FPS) or 25
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
out = cv2.VideoWriter(
    str(VIDEO_OUT),
    cv2.VideoWriter_fourcc(*"mp4v"),
    fps,
    (w, h),
)

n = 0
while True:
    ret, frame = cap.read()
    if not ret:
        break

    results = model(frame, verbose=False)
    annotated = results[0].plot()  # 在拷贝上画框,不改原 frame
    out.write(annotated)
    n += 1

    if HAS_DISPLAY:
        cv2.imshow("YOLO", annotated)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

cap.release()
out.release()
if HAS_DISPLAY:
    cv2.destroyAllWindows()

print(f"处理 {n} 帧,已保存:{VIDEO_OUT.resolve()}")

本机对 video.mp4 跑完约 297 帧 ,输出 yolo_video_out.mp4。解码阶段有时会刷 Invalid NAL unit size 一类 h264 警告,多半是源视频封装不规范;只要 VideoCapture 能打开、帧数对得上、输出能播,可以先当噪声日志,不必和检测逻辑绑在一起排查。

model(frame)model.predict(source=frame) 等价;verbose=False 避免每帧刷日志。frame 是 BGR 的 numpy 数组,形状 (H, W, 3)

video.mp4 第 0 帧原图 vs plot() 之后(本例检出 2 个 person):

四、跳帧检测:省算力,但不要让框闪没

视频 30fps 时相邻帧差异很小,连续每帧推理往往是在重复上一帧的结论。STRIDE=3 表示只在第 0、3、6、... 帧做推理,297 帧大约只检测 99 帧。

Ultralytics 的 predict 也有 vid_stride,但自己用 OpenCV 控跳帧更灵活:跳过的帧可以复用上一帧的标注图,而不是直接写原图------否则检测框会按 stride 周期"闪一下又消失",观感比省下的算力更糟。

下图用 video.mp4 前 4 帧、STRIDE=3 对比两种写法:上行把跳过的帧写成原图,框在 n=1、n=2 消失;下行缓存 last_annotated,框一直在。

python 复制代码
import os
from pathlib import Path

import cv2
from ultralytics import YOLO

HAS_DISPLAY = bool(os.environ.get("DISPLAY"))

VIDEO_IN = Path("video.mp4")
VIDEO_OUT = Path("yolo_video_out_STRIDE.mp4")
STRIDE = 3  # 1 等价于逐帧;3 表示每 3 帧检测一次

model = YOLO("./yolov8n.pt")

cap = cv2.VideoCapture(str(VIDEO_IN))
if not cap.isOpened():
    raise RuntimeError(f"无法打开视频:{VIDEO_IN}")

fps = cap.get(cv2.CAP_PROP_FPS) or 25
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
out = cv2.VideoWriter(
    str(VIDEO_OUT),
    cv2.VideoWriter_fourcc(*"mp4v"),
    fps,
    (w, h),
)

n = 0
n_detected = 0
last_annotated = None

while True:
    ret, frame = cap.read()
    if not ret:
        break

    if n % STRIDE == 0:
        results = model(frame, verbose=False)
        annotated = results[0].plot()
        last_annotated = annotated
        n_detected += 1
    else:
        annotated = last_annotated if last_annotated is not None else frame

    out.write(annotated)
    n += 1

    if HAS_DISPLAY:
        cv2.imshow("YOLO", annotated)
        if cv2.waitKey(1) & 0xFF == ord("q"):
            break

cap.release()
out.release()
if HAS_DISPLAY:
    cv2.destroyAllWindows()

print(
    f"处理 {n} 帧,其中检测 {n_detected} 帧(STRIDE={STRIDE}),"
    f"已保存:{VIDEO_OUT.resolve()}"
)

因为循环从 n=0 开始,第一帧一定会被检测,last_annotated 随后才有缓存。跳过的帧写的是上一检测帧的画面 ,目标快速移动时框会略滞后,这是用吞吐换平滑度的代价;若要框跟着当前像素走,需要光流或跟踪,那是下一步,不是单纯 STRIDE 能解决的。

五、总结

这篇文章把 YOLO 从"单张跑通"推进到了"批量可导出、视频可落盘、跳帧可复用"的工程化闭环,但真正的落地远不止这几步:批量推理的加速比要靠基准说话,CSV 只是给下游系统的第一手结构化数据,视频逐帧则要先把打开失败、FPS 为 0、宽高不一致这些坑填平。跳帧复用上一帧标注,是用少量滞后换明显算力节省的务实取舍,而要让框真正跟住快速运动的目标,就得引入光流或跟踪------那是另一层工程问题。下一步,不妨把这套链路接到你自己的数据上,把计时、导出、跳帧参数都跑一遍,再决定哪些优化值得留在生产环境里。

相关推荐
Forerror20261 小时前
深入理解大模型网关解决什么问题:MAIGateway(魔芋企业级AI网关)架构与核心价值解读
人工智能·ai网关·maigateway·大模型财务管控·企业级大模型治理网关
AI深栈1 小时前
第 10 章 · Embedding、VectorStore 与 RAG
java·人工智能
jsl_jsl_jsl1 小时前
《一个 Agent 平台怎么接入多家大模型:Provider 槽位制设计》
人工智能
ι:1 小时前
Codex 自主调用 Visio 绘图完整教程
人工智能·visio·codex
梦帮科技1 小时前
一次性会员支付系统的可靠性设计:幂等、金额校验、Webhook 与链上确认
人工智能·神经网络·mysql·区块链·建造者模式·合成复用原则·加密货币
维克兜率天1 小时前
【维克】模块3总结:从一行空数据,到一个能跑的模型
python·深度学习·算法
AgentMaster1 小时前
智能客服系统技术选型实战:从架构设计到落地实施的完整指南
大数据·人工智能
lailai04101 小时前
图像处理的技术路径与实现方式考察
人工智能
回眸&啤酒鸭1 小时前
【回眸】GPT-5.6 Luna 批量处理实战指南
人工智能