AI-深度学习

嵌入式视觉模型进阶 11:完整 YOLOv8 推理:从图片到原图目标框

展开合集目录
  1. 嵌入式视觉模型进阶 01:怎样读懂一个实际模型
  2. 嵌入式视觉模型进阶 02:卷积进阶:分组、深度、逐点与空洞卷积
  3. 嵌入式视觉模型进阶 03:MobileNet:深度可分离卷积与倒残差
  4. 嵌入式视觉模型进阶 04:通道注意力与 MobileNetV3
  5. 嵌入式视觉模型进阶 05:ShuffleNet:通道拆分与重排
  6. 嵌入式视觉模型进阶 06:结构重参数化:训练分支怎样合为一个卷积
  7. 嵌入式视觉模型进阶 07:Backbone、Neck、Head 与多尺度特征
  8. 嵌入式视觉模型进阶 08:FPN 与 PAN:怎样融合不同尺度的信息
  9. 嵌入式视觉模型进阶 09:检测头:分类、位置与置信度
  10. 嵌入式视觉模型进阶 10:检测训练:标签分配与损失计算
  11. 嵌入式视觉模型进阶 11:完整 YOLOv8 推理:从图片到原图目标框
  12. 嵌入式视觉模型进阶 12:训练自己的检测器与错误分析
  13. 嵌入式视觉模型进阶 13:检测器导出对照与其他结构路线
  14. 嵌入式视觉模型进阶 14:跟踪基础:运动预测、匹配与轨迹生命周期
  15. 嵌入式视觉模型进阶 15:ByteTrack 思路:用低分框恢复已有轨迹
  16. 嵌入式视觉模型进阶 16:外观特征与 ReID:怎样比较两个目标
  17. 嵌入式视觉模型进阶 17:目标跟随:从轨迹到云台控制接口
  18. 嵌入式视觉模型进阶 18:视频系统:队列、时间戳与异步流水线
  19. 嵌入式视觉模型进阶 19:人脸处理流程:检测、关键点与对齐
  20. 嵌入式视觉模型进阶 20:特征识别:人脸向量、相似度与阈值
  21. 嵌入式视觉模型进阶 21:人体关键点与姿态估计
  22. 嵌入式视觉模型进阶 22:OCR 检测:从文字区域到矫正图片
  23. 嵌入式视觉模型进阶 23:OCR 识别:序列模型与 CTC
  24. 嵌入式视觉模型进阶 24:完整 OCR 流程与识别错误分析
  25. 嵌入式视觉模型进阶 25:轻量语义分割与实例分割
  26. 嵌入式视觉模型进阶 26:工业异常检测:缺陷样本少时怎么办
  27. 嵌入式视觉模型进阶 27:深度估计与图像恢复:两个专题入口
  28. 嵌入式视觉模型进阶 28:面向 NPU 的计算图与算子兼容
  29. 嵌入式视觉模型进阶 29:量化、蒸馏与剪枝的实际取舍
  30. 嵌入式视觉模型进阶 30:综合项目:检测、跟踪与云台跟随验收
嵌入式视觉模型进阶 11:完整 YOLOv8 推理:从图片到原图目标框的封面

第 11 / 30 课。 前置:上一课。本篇的运行范围以实验说明和验证记录为准,构造样例不代表真实任务精度或板端性能。

先固定一个可以检查的版本契约

本课限定 Ultralytics YOLOv8 的普通检测 ONNX:batch=1、固定方形输入、FP32、nms=False,输出 [1,4+C,A],前四行是输入像素坐标的中心宽高,后面 C 行是类别分数,不另乘 objectness。分割、姿态、带 NMS 导出、其他 YOLO 家族不能直接复用。

本课自写推理入口不导入 Ultralytics。它接收本地 ONNX、图片和类别数;启动时检查类型、布局、通道与输出数量。真实权重不随包提供。默认无参数运行使用构造输出,验证完整前后处理链。

预处理要留下逆变换信息

保持纵横比缩放到输入框内,剩余边缘填充 114。记录实际缩放后的宽高以及左上 padding。OpenCV 读取 BGR,模型输入转 RGB、除以 255、转 NCHW,并保持 float32。整数舍入会让两个轴的实际缩放比略有差异,代码分别记录 sx、sy。

输出解码与筛选

每个候选取最大类别分数,再做阈值过滤。将 cxcywh 转 xyxy;NMS 按类别执行,同位置的不同类别不会互相抑制。最后减 padding、除缩放比例并裁剪到原图范围,丢弃退化框。

本课候选按最高分截断到 3000 以限制 Python NMS 开销,这个上限可能影响拥挤场景召回,需要在验收协议中记录。

真实模型运行

安装包内依赖后运行 python3 main.py --model model.onnx --image frame.jpg --classes 80。类别数必须与你的模型一致。程序输出 JSON 框列表,不自动下载权重。

用包含边缘目标和非方形尺寸的图片,与原训练框架比较结果;比较同一输入上的原始 logits,再比较后处理。只看画出的框“差不多”不足以确认精度。

练习

把构造图片改成竖图,核对横向 padding;把两个高重叠框类别设为不同,确认它们都保留。若模型输出已经是 xyxy 或已带 NMS,应写新的契约适配器,不能只修改一个 transpose 蒙混通过。

配套实验与运行记录

实验:完整 YOLOv8 推理:从图片到原图目标框

查看完整代码:main.py
"""固定 YOLOv8 检测导出契约,完成 letterbox、分类别 NMS 与坐标还原。"""

import argparse, json
from pathlib import Path
import cv2
import numpy as np


def preprocess(image, size):
    h, w = image.shape[:2]
    ratio = min(size / w, size / h)
    nw, nh = round(w * ratio), round(h * ratio)
    left, top = (size - nw) // 2, (size - nh) // 2
    canvas = np.full((size, size, 3), 114, np.uint8)
    canvas[top : top + nh, left : left + nw] = cv2.resize(image, (nw, nh))
    tensor = (
        np.ascontiguousarray(
            canvas[:, :, ::-1].transpose(2, 0, 1)[None], dtype=np.float32
        )
        / 255
    )
    return tensor, (nw / w, nh / h, left, top, w, h)


def iou(a, b):
    lo = np.maximum(a[:2], b[:2])
    hi = np.minimum(a[2:], b[2:])
    inter = np.maximum(hi - lo, 0).prod()
    union = (
        np.maximum(a[2:] - a[:2], 0).prod()
        + np.maximum(b[2:] - b[:2], 0).prod()
        - inter
    )
    return float(inter / union) if union > 0 else 0.0


def decode(raw, meta, classes, score=0.25, threshold=0.45):
    if raw.ndim != 3 or raw.shape[0] != 1 or raw.shape[1] != 4 + classes:
        raise ValueError("expected [1,4+C,A], decoded YOLOv8 detection export")
    if not np.isfinite(raw).all():
        raise ValueError("nonfinite output")
    rows = raw[0].T
    labels = rows[:, 4:].argmax(1)
    scores = rows[np.arange(len(rows)), 4 + labels]
    candidates = []
    for i in np.argsort(-scores)[:3000]:
        if scores[i] < score:
            continue
        cx, cy, w, h = rows[i, :4]
        if w <= 0 or h <= 0:
            continue
        box = np.array([cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2])
        if all(
            int(labels[i]) != c or iou(box, b) <= threshold for b, _, c in candidates
        ):
            candidates.append((box, float(scores[i]), int(labels[i])))
    sx, sy, left, top, width, height = meta
    result = []
    for box, s, c in candidates:
        box = (box - np.array([left, top, left, top])) / np.array([sx, sy, sx, sy])
        box[[0, 2]] = box[[0, 2]].clip(0, width)
        box[[1, 3]] = box[[1, 3]].clip(0, height)
        if box[2] > box[0] and box[3] > box[1]:
            result.append({"box": box.tolist(), "score": s, "class_id": c})
    return result


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--model", type=Path)
    parser.add_argument("--image", type=Path)
    parser.add_argument("--classes", type=int, default=2)
    args = parser.parse_args()
    if args.classes < 1:
        parser.error("classes must be positive")
    if bool(args.model) != bool(args.image):
        parser.error("model and image must be supplied together")
    if args.model:
        import onnxruntime as ort

        session = ort.InferenceSession(
            str(args.model), providers=["CPUExecutionProvider"]
        )
        inputs = session.get_inputs()
        if len(inputs) != 1:
            raise ValueError("one input required")
        inp = inputs[0]
        if (
            inp.type != "tensor(float)"
            or len(inp.shape) != 4
            or inp.shape[:2] != [1, 3]
            or not isinstance(inp.shape[2], int)
            or inp.shape[2] != inp.shape[3]
        ):
            raise ValueError("expected static FP32 [1,3,S,S]")
        image = cv2.imread(str(args.image))
        if image is None:
            raise ValueError("cannot read image")
        x, meta = preprocess(image, inp.shape[2])
        outputs = session.run(None, {inp.name: x})
        if len(outputs) != 1:
            raise ValueError("one detection output required")
        result = decode(outputs[0], meta, args.classes)
    else:
        x, meta = preprocess(np.zeros((360, 640, 3), np.uint8), 640)
        raw = np.array(
            [[[150, 151], [240, 241], [100, 100], [100, 100], [0.9, 0.8], [0.1, 0.2]]],
            np.float32,
        )
        result = decode(raw, meta, 2)
        assert len(result) == 1
        np.testing.assert_allclose(result[0]["box"], [100, 50, 200, 150])
        assert x.shape == (1, 3, 640, 640)
        print("constructed output smoke check; no trained weights")
    print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
下载代码

在项目根目录执行;下载包解压后,在解压目录执行 python3 main.py。

cd 'content/posts/AI-深度学习/嵌入式视觉模型进阶/11-完整 YOLOv8 推理/code' && python3 main.py

在本篇代码目录执行 python3 main.py。依赖安装、可选入口见包内 README.md;实际运行结果见 verification.md。下载包包含本课使用的辅助模块与样例。

参考与进一步阅读

原始论文或官方文档用于核对结构、接口和适用范围。本篇代码是围绕教学目标编写的最小实验;除明确注明的完整入口外,不视为对应论文模型的完整复现。


完整进阶目录 · 入门系列 · 上一篇 · 下一篇