13-YOLOv8的ONNX输出结构------9个张量如何变成检测框
一句话总结:YOLOv8 导出为 ONNX 后,输出 9 个张量------3 个检测头 × 3 个分支(box、cls、DFL)。每个张量是一个 4 维数组,6400+1600+400 个网格各装一个"信息包",解码后得到框坐标和类别置信度。
目录
- 13-YOLOv8的ONNX输出结构------9个张量如何变成检测框
- 目录
- [前置知识:ONNX 导出时发生了什么](#前置知识:ONNX 导出时发生了什么)
- [第一层:9 个输出的来源](#第一层:9 个输出的来源)
- [3 个检测头](#3 个检测头)
- [3 个分支](#3 个分支)
- [9 个输出一览](#9 个输出一览)
- 第二层:每个张量的形状和含义
- [box 特征 →
[1, 64, H, W]](#box 特征 → [1, 64, H, W]) - [cls 特征 →
[1, 80, H, W]](#cls 特征 → [1, 80, H, W]) - [DFL 特征 →
[1, 64, H, W]](#DFL 特征 → [1, 64, H, W])
- [box 特征 →
- [第三层:NCHW vs NHWC------怎么理解更直观](#第三层:NCHW vs NHWC——怎么理解更直观)
- 第四层:一个网格的完整信息包
- [第五层:从 9 个张量到最终检测框](#第五层:从 9 个张量到最终检测框)
- [以网格 (10, 20) 为例的完整解码](#以网格 (10, 20) 为例的完整解码)
- [第六层:为什么 box 和 DFL 是两个独立输出](#第六层:为什么 box 和 DFL 是两个独立输出)
- 自测
- 一句话记住
前置知识:ONNX 导出时发生了什么
YOLOv8 的 PyTorch 模型导出为 ONNX 时,ONNX 图包含 Backbone、Neck 和检测头的卷积部分。DFL 解码、NMS 等后处理逻辑不在 ONNX 图中,而是在推理代码中完成。
这种"后处理外置"的方式是工业界常用的部署方式------ONNX 只负责特征提取和原始预测,推理代码灵活可控。
第一层:9 个输出的来源
3 个检测头 × 3 个分支 = 9 个输出
3 个检测头
| 检测头 | 特征图尺寸 | 网格数 | 负责 |
|---|---|---|---|
| P3 | 80×80 | 6400 | 小目标(鸟、硬币) |
| P4 | 40×40 | 1600 | 中目标(人脸、猫) |
| P5 | 20×20 | 400 | 大目标(汽车、建筑) |
| 合计 | --- | 8400 | --- |
3 个分支
| 分支 | 通道数 | 含义 |
|---|---|---|
| box | 64 | 框坐标(4 条边 × 16 个 bin) |
| cls | 80 | 类别(COCO 80 个类别) |
| DFL | 64 | 与 box 相同,标记走 DFL 解码链 |
9 个输出一览
检测头 P3(小目标,80×80 网格):
├── 输出1: box特征 [1, 64, 80, 80] ← 64 = 4条边 × 16个bin
├── 输出2: cls特征 [1, 80, 80, 80] ← 80 = COCO 80个类别
└── 输出3: DFL特征 [1, 64, 80, 80] ← 和box一样
检测头 P4(中目标,40×40 网格):
├── 输出4: box特征 [1, 64, 40, 40]
├── 输出5: cls特征 [1, 80, 40, 40]
└── 输出6: DFL特征 [1, 64, 40, 40]
检测头 P5(大目标,20×20 网格):
├── 输出7: box特征 [1, 64, 20, 20]
├── 输出8: cls特征 [1, 80, 20, 20]
└── 输出9: DFL特征 [1, 64, 20, 20]
第二层:每个张量的形状和含义
box 特征 → [1, 64, H, W]
64 个通道 = 4 条边 × 16 个 bin
通道 0~15: left 的 16 个 bin → 候选偏移 0.0, 0.5, 1.0, ..., 7.5
通道 16~31: top 的 16 个 bin
通道 32~47: right 的 16 个 bin
通道 48~63: bottom 的 16 个 bin
每个通道的值是原始分数(未归一化),正负无穷都行,没有范围限制。
以 P3 检测头网格 (10, 20) 为例:
left: [2.0, 5.0, 1.0, 0.5, 0.2, 0.1, 0.0, ..., 0.0] ← 16 个 bin
top: [1.5, 4.0, 2.0, 0.3, 0.1, 0.0, ..., 0.0]
right: [3.0, 6.0, 0.5, 0.1, 0.0, 0.0, ..., 0.0]
bottom: [2.5, 5.5, 1.5, 0.2, 0.1, 0.0, ..., 0.0]
cls 特征 → [1, 80, H, W]
80 个通道 = COCO 80 个类别
通道 0: person 分数 9.5 ← 最高,是人!
通道 1: bicycle 分数 0.1
通道 2: car 分数 0.2
通道 3: motorcycle 分数 0.0
通道 4: airplane 分数 0.0
...
通道 15: cat 分数 0.3
通道 16: dog 分数 0.1
...
通道 79: toothbrush 分数 0.0
每个通道的值也是原始分数,需要 Sigmoid 变成 0~1 的置信度。
DFL 特征 → [1, 64, H, W]
和 box 特征的数据完全相同。为什么导出两个?因为推理代码需要知道"这 64 个通道是 DFL 格式的,要走 Softmax→Conv(dfl)→Sub→Div 这条解码链"。
第三层:NCHW vs NHWC------怎么理解更直观
ONNX 默认用 NCHW 排列(PyTorch 惯例),但理解的时候完全可以脑补成 NHWC:
NCHW [1, 64, 80, 80]: 先按通道分,每个通道里是 80×80 平面
像 64 张纸叠在一起
NHWC [1, 80, 80, 64]: 先按空间分,每个格子里是 64 个值
像 6400 个格子,每个格子存 64 个数
NHWC 的思维模型更直观:
输出1 box: [1, 80, 80, 64] ← 6400个格子,每个格子 64 个值(框在哪)
输出2 cls: [1, 80, 80, 80] ← 6400个格子,每个格子 80 个值(框里是啥)
输出3 DFL: [1, 80, 80, 64] ← 和box一样,标记走DFL链
第四层:一个网格的完整信息包
网格 (10, 20) 的"信息包":
┌──────────────────────────────────────────────────────┐
│ 网格 (10, 20) 的信息包 │
│ │
│ box: 64 个值 → 4条边 × 16个bin │
│ left [2.0, 5.0, 1.0, 0.5, ...] │
│ top [1.5, 4.0, 2.0, 0.3, ...] │
│ right [3.0, 6.0, 0.5, 0.1, ...] │
│ bottom[2.5, 5.5, 1.5, 0.2, ...] │
│ │
│ cls: 80 个值 → 80个类别 │
│ [9.5, 0.1, 0.2, 0.0, ..., 0.3, 0.1, ..., 0.0] │
│ │
│ DFL: 64 个值 → 和box一样 │
│ │
│ 共 64 + 80 = 144 个值 │
└──────────────────────────────────────────────────────┘
6400 个格子 × 144 个值 = 921,600 个数字,这就是 P3 检测头的全部输出。
第五层:从 9 个张量到最终检测框
第1步:解码
8400 个网格(3 个检测头)
每个网格:
64 个值 → DFL 解码 → 框坐标 (x1, y1, x2, y2)
80 个值 → Sigmoid → 80 个置信度
第2步:过滤
8400 个候选框 → 去掉置信度 < 0.25 的 → 剩几百个
第3步:NMS
几百个框 → 去掉重叠的 → 剩几个最终框
第4步:画框
最终框 → 在原图上画矩形 + 类别标签
以网格 (10, 20) 为例的完整解码
ONNX 输出(P3 头):
box [1,64,80,80] → 取网格(10,20) → 64个值
cls [1,80,80,80] → 取网格(10,20) → 80个值
DFL [1,64,80,80] → 取网格(10,20) → 64个值
│
▼
┌─────────────────────────────────────────────────────┐
│ 对 DFL 的 64 个值: │
│ │
│ left 16个bin → Softmax → Conv(dfl) → 偏移量 │
│ top 16个bin → Softmax → Conv(dfl) → 偏移量 │
│ right 16个bin → Softmax → Conv(dfl) → 偏移量 │
│ bottom 16个bin → Softmax → Conv(dfl) → 偏移量 │
│ │
│ → Sub → Div → 得到 4 个坐标值 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 对 cls 的 80 个值: │
│ │
│ 80个值 → Sigmoid → 80个置信度 │
│ 取最大值: person 置信度 0.9997 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 最终结果: │
│ │
│ 检测到: person │
│ 框坐标: (80, 60, 320, 400) ← 像素坐标 │
│ 置信度: 0.9997 │
└─────────────────────────────────────────────────────┘
第六层:为什么 box 和 DFL 是两个独立输出
检测头的最后几层卷积:
特征图 ──┬── Conv(box) ──→ [1, 64, H, W] ← 回归分支
│
└── Conv(cls) ──→ [1, 80, H, W] ← 分类分支
box 和 DFL 数据完全一样,为什么导出两个?
| 原因 | 说明 |
|---|---|
| 语义标记 | DFL 输出告诉推理代码"这 64 个通道需要走 DFL 解码链" |
| ONNX 规范 | ONNX 图中每个输出节点有独立的名字,推理代码通过名字识别 |
| 灵活性 | 推理代码可以分别处理 box 和 DFL,比如做剪枝或优化 |
实际上,推理代码拿到 DFL 特征后,走 Softmax→Conv(dfl)→Sub→Div 这条解码链,box 特征通常和 DFL 特征合并处理。
自测
Q1:YOLOv8 的 ONNX 为什么输出 9 个张量?
点击查看答案
3 个检测头(P3/P4/P5)× 3 个分支(box/cls/DFL)= 9 个。每个检测头用自己的特征图尺寸(80×80/40×40/20×20),每个分支输出不同含义的数据。
Q2:[1, 64, 80, 80] 中的 64 个通道分别代表什么?
点击查看答案
通道 0~15: left 的 16 个 bin;通道 16~31: top 的 16 个 bin;通道 32~47: right 的 16 个 bin;通道 48~63: bottom 的 16 个 bin。4 条边 × 16 个 bin = 64。
Q3:一个网格最终输出多少个值?分别是什么?
点击查看答案
144 个值。box/DFL 64 个值(框坐标的分布信息)+ cls 80 个值(80 个类别的原始分数)。
Q4:从 9 个张量到最终画框,经历了哪几步?
点击查看答案
- 解码:64 个值 → DFL 解码 → 框坐标;80 个值 → Sigmoid → 置信度
- 过滤:去掉置信度 < 0.25 的候选框
- NMS:去掉重叠的框
- 画框:在原图上画矩形 + 类别标签
一句话记住
9 个输出 = 3 个检测头 × 3 个分支。每个检测头输出 6400/1600/400 个网格,每个网格装一个"信息包"(64 个框值 + 80 个类别值),解码后得到框坐标和置信度,过滤 + NMS 后画框。