YOLOv8 ONNX输出的cls与box提取详解------从[1,84,8400]到检测框
配套代码 :本文末附完整可运行的 Python 代码演示,可复制到本地直接运行
一句话总结 :YOLOv8 默认导出 ONNX 后,输出
[1, 84, 8400]------通道 0-3 是 box 坐标(值域 0~640),通道 4-83 是 cls 置信度(值域 0~1,已过 Sigmoid)。box 值域是 cls 的 ~700 倍。
目录
- [YOLOv8 ONNX输出的cls与box提取详解------从
[1,84,8400]到检测框](#YOLOv8 ONNX输出的cls与box提取详解——从[1,84,8400]到检测框)- 目录
- [一、ONNX 输出结构](#一、ONNX 输出结构)
- [二、8400 个 anchor 的来源](#二、8400 个 anchor 的来源)
- [三、box 通道 (0-3)](#三、box 通道 (0-3))
- [DFL 解码已在 ONNX 图中完成](#DFL 解码已在 ONNX 图中完成)
- [坐标系:已经映射到 640×640 输入图像](#坐标系:已经映射到 640×640 输入图像)
- [四、cls 通道 (4-83)](#四、cls 通道 (4-83))
- [Sigmoid 在哪里执行的?](#Sigmoid 在哪里执行的?)
- [提取 cls 的常用指标](#提取 cls 的常用指标)
- [五、box 和 cls 的值域差异](#五、box 和 cls 的值域差异)
- 六、代码演示
- 七、总结
- 自测
一、ONNX 输出结构
YOLOv8 默认导出的 ONNX 模型只有 1 个输出 output0,shape 为 [1, 84, 8400]。
output0 = [1, 84, 8400]
│ │ └── 8400 个检测锚点 (anchor)
│ │ = 80×80 + 40×40 + 20×20
│ └── 84 个通道
│ 0-3: box 坐标 (cx, cy, w, h)
│ 4-83: cls 分类置信度 (80 个 COCO 类别)
└── batch 维度,恒为 1
提取代码:
python
box = output0[0, 0:4, :] # [4, 8400] --- cx, cy, w, h
cls_all = output0[0, 4:84, :] # [80, 8400] --- 80 个类别
cls_per_anchor = cls_all.max(axis=0) # [8400] --- 每个 anchor 的最高置信度
二、8400 个 anchor 的来源
YOLOv8 有 3 个检测头,每个检测头输出一个特征图,三个检测头的输出在 ONNX 图中 Concat 拼接:
| 检测头 | 特征图 | anchor 数量 | anchor 索引范围 | 负责 |
|---|---|---|---|---|
| P3 (浅层) | 80×80 | 6400 | 0 ~ 6399 | 小目标 |
| P4 (中层) | 40×40 | 1600 | 6400 ~ 7999 | 中目标 |
| P5 (深层) | 20×20 | 400 | 8000 ~ 8399 | 大目标 |
| 合计 | --- | 8400 | --- | --- |
浅层/中层/深层:这个叫法来自 FPN(Feature Pyramid Network),指网络深度------P3 离输入最近(下采样 3 次),感受野小,细节多,适合小目标;P5 离输入最远(下采样 5 次),感受野大,语义强,适合大目标。
三、box 通道 (0-3)
| 通道 | 含义 | 值域 | 说明 |
|---|---|---|---|
| 0 | cx | 0 ~ 640 | 目标中心点 x 坐标 |
| 1 | cy | 0 ~ 640 | 目标中心点 y 坐标 |
| 2 | w | 0 ~ 640 | 目标宽度 |
| 3 | h | 0 ~ 640 | 目标高度 |
值域特点 :box 坐标的值域大约是 0 ~ 637 (最大不超过输入尺寸 640),是一个大值域。
DFL 解码已在 ONNX 图中完成
这 4 个值已经是最终坐标,不需要在后处理中再做 DFL 解码。对比两种导出模式:
9 输出模式(旧):
ONNX 输出: 64 个原始 logits → 后处理中做 DFL 解码
1 输出模式(默认):
ONNX 图中: 64 个 logits → Softmax → Conv(dfl) → Sub → Div → 最终坐标
ONNX 输出: 4 个坐标值,已解码完毕,拿过来直接用
所以 output0[0, 0:4, :] 取出来的 cx, cy, w, h 就是可以直接用的最终坐标:
python
cx, cy, w, h = output0[0, 0:4, i] # 直接就是最终坐标,无需再解码
坐标系:已经映射到 640×640 输入图像
DFL 解码得到的坐标经过缩放(ONNX 图中由 Mul 按各检测头 stride 8/16/32 放大),将坐标从特征图像素空间映射到了 640×640 的输入图像空间。因此值域 0~640 就是输入图像上的像素位置:
特征图坐标 (0~80) → ×stride(8/16/32) → 640×640 输入图像坐标 (0~640)
注意:这个坐标是相对于 640×640 的 letterbox 输入,不是原图。如果原图不是 640×640,需要按 letterbox 的 scale 和 dx/dy 反算回原图坐标。
python
box = output0[0, 0:4, :] # shape [4, 8400]
# 典型输出(bus.jpg,640×640):
# cx: min=3.66, max=635.63, mean=320.00
# cy: min=3.06, max=637.28, mean=321.99
# w: min=6.27, max=456.73, mean=66.87
# h: min=4.23, max=607.05, mean=79.59
四、cls 通道 (4-83)
| 通道 | 含义 | 值域 | 说明 |
|---|---|---|---|
| 4 | 类别 0 | 0 ~ 1 | 已过 Sigmoid |
| 5 | 类别 1 | 0 ~ 1 | 已过 Sigmoid |
| ... | ... | ... | ... |
| 83 | 类别 79 | 0 ~ 1 | 已过 Sigmoid |
值域特点 :cls 置信度的值域是 0 ~ 1 ,是一个小值域。
Sigmoid 在哪里执行的?
Sigmoid 不在后处理代码中执行 ,而是在 ONNX 图内部已经完成。三个检测头的 cls 特征在 ONNX 图中先各自卷积(cv3),经 Reshape 后 Concat 合并为 [1, 80, 8400],最后统一过一个 Sigmoid 激活:
ONNX 图内部:
3 个检测头 cv3 卷积(各 80 通道 cls 特征)
→ Reshape → Concat 合并 [1, 80, 8400]
→ Sigmoid 激活 → 存入 output0[4:83]
所以从 output0 中取出的 cls 值已经是 0~1 的置信度,不需要再做任何激活函数。
提取 cls 的常用指标
python
cls = output0[0, 4:, :].max(axis=0) # [8400] --- 每个 anchor 的最高置信度
cls.max() # 所有 anchor 中最高 cls 置信度
cls.mean() # 所有 anchor 的 cls 均值
(cls > 0.25).sum() # 置信度 > 0.25 的 anchor 数量
(cls > 0.50).sum() # 置信度 > 0.50 的 anchor 数量
五、box 和 cls 的值域差异
同一张图上,box 和 cls 的值域差异巨大:
python
# 典型输出(同一张 bus.jpg):
# box 通道: min=3.06, max=637.28, mean=197.11 ← 大值域
# cls 通道: min=0.00, max=0.88, mean=0.0001 ← 小值域
box 的值域是 cls 的 ~700 倍。 这个差异在两个值域合并到同一个张量后,对 INT8 量化有决定性影响------量化尺度会由 box 的大值域决定,cls 的小值域通道在量化后可能损失大部分精度,甚至被压成 0。
六、代码演示
完整代码演示如下(可直接复制运行):
bash
# 依赖: onnxruntime, numpy, opencv-python
打印内容:
| 序号 | 内容 |
|---|---|
| 1 | output0 整体结构(shape / dtype / ndim) |
| 2 | box 逐通道统计(cx, cy, w, h 的 min/max/mean/std) |
| 3 | box 前 20 个 anchor 值 |
| 4 | cls 整体统计(min/max/mean/std) |
| 5 | cls 各阈值 anchor 数(>0.0, >0.01, >0.05, >0.10, >0.25, >0.50, >0.75, >0.90) |
| 6 | 80 个类别逐类统计(每类的 max/mean/>0.25 的 anchor 数) |
| 7 | Top-30 高置信度 anchor(cls_max + cls_argmax + 4 个 box 坐标) |
| 8 | Top-10 的 80 个类别通道详细值(展开每个 anchor 的 cls 向量) |
| 9 | cls 分布直方图(文本,10 个区间柱状图) |
| 10 | 三个检测头分别的 cls 分布(P3/P4/P5) |
核心代码片段:
python
import onnxruntime as ort
import numpy as np
# 加载模型
session = ort.InferenceSession("yolov8n.onnx", providers=['CPUExecutionProvider'])
# 预处理图片为 [1, 3, 640, 640]
x = preprocess("bus.jpg")
# 推理
outputs = session.run(None, {"images": x})
output0 = outputs[0] # [1, 84, 8400]
# 拆分 box 和 cls
box = output0[0, 0:4, :] # [4, 8400]
cls_all = output0[0, 4:84, :] # [80, 8400]
# 每个 anchor 的最高置信度
cls_per_anchor = cls_all.max(axis=0) # [8400]
# 高置信度 anchor
high_conf = (cls_per_anchor > 0.25).sum()
print(f"cls > 0.25 的 anchor 数: {high_conf}")
# Top-K 详细打印
top_indices = np.argsort(cls_per_anchor)[::-1][:10]
for idx in top_indices:
print(f"anchor {idx}: cls={cls_per_anchor[idx]:.4f}, "
f"box=({box[0,idx]:.1f}, {box[1,idx]:.1f}, "
f"{box[2,idx]:.1f}, {box[3,idx]:.1f})")
七、总结
- 输出结构 :
[1, 84, 8400],通道 0-3 是 box,通道 4-83 是 cls - anchor 来源:P3(80×80) + P4(40×40) + P5(20×20) = 8400 个
- box 值域:0~640,大值域
- cls 值域:0~1,小值域,已过 Sigmoid
- 值域差:box 是 cls 的 ~700 倍,值域悬殊让单一量化尺度难以同时覆盖两者,量化时小值域通道会显著损失精度
自测
Q1:output0 的 shape 是什么?box 和 cls 分别在哪几个通道?
点击查看答案
output0 shape = [1, 84, 8400]。
- box: 通道 0-3(cx, cy, w, h)
- cls: 通道 4-83(80 个 COCO 类别)
Q2:8400 个 anchor 是怎么来的?
点击查看答案
三个检测头的特征图拼接:
- P3 (80×80) = 6400 个
- P4 (40×40) = 1600 个
- P5 (20×20) = 400 个
合计 8400 个。
Q3:cls 还需要做 Sigmoid 吗?
点击查看答案
不需要。Sigmoid 在 ONNX 图内部已完成(三个检测头的 cls 特征 Concat 合并后统一执行),output0[4:83] 取出的值已经是 0~1 的置信度。
Q4:box 和 cls 的值域差多少?为什么这个差异重要?
点击查看答案
box 值域 0~640,cls 值域 0~1,box 是 cls 的 ~700 倍。在 INT8 量化时,量化尺度由 box 的大值域决定,cls 的小值域通道会被压成 0,分类信息随之丢失。
一句话记住 :
output0[0, 0:4, :]取 box(值域 0~640),output0[0, 4:84, :].max(axis=0)取每个 anchor 的最高 cls(值域 0~1),box 值域是 cls 的 ~700 倍。