一、先建立一个最重要的模型
无论 YOLOv5、YOLOv8、YOLO11,本质上你先把它理解成:
YOLO模型
│
图片 ──→ 预处理 ──→ [ 一堆神经网络 ] ──→ 输出Tensor
│
↓
后处理
│
↓
框、类别、置信度所以你现在只需要研究两头:
你关心 你暂时不关心
↓ ↓
┌──────────────┐
输入 → │ YOLO内部 │ → 输出 → 后处理
└──────────────┘
↑ ↑
研究这里 研究这里以后别人扔给你一个模型,你先问 6 个问题:
- 输入几个 Tensor?
- 输入 shape 是什么?
- 输入数据怎么预处理?
- 输出几个 Tensor?
- 每个输出 shape 是什么?
- 输出里的每一个数字代表什么?
把这 6 个搞明白,后处理基本就出来了。
二、先从输入开始:[1, 3, 640, 640] 是什么?
你以后经常看到:
input:
shape = [1, 3, 640, 640]
dtype = float32它通常表示:
[N, C, H, W]
N = Batch
C = Channel
H = Height
W = Width也就是:
[1, 3, 640, 640]
│ │ │ │
│ │ │ └─ 宽 640
│ │ └────── 高 640
│ └────────── 3通道 RGB
└───────────── 一次输入1张所以你以后看到:
1 × 3 × 736 × 1280马上应该能反应过来:
batch=1,RGB 三通道,输入尺寸 1280×736。
三、但“输入 shape”还远远不够
这点非常重要。
假设模型输入是:
[1, 3, 640, 640]你不能直接:
model(image);因为模型还有一个输入契约。
至少要知道:
原始图片
1920×1080 BGR uint8
│
↓
resize / letterbox
│
↓
640×640
│
↓
BGR → RGB
│
↓
uint8 → float32
│
↓
/ 255.0
│
↓
HWC → CHW
│
↓
[1,3,640,640]比如 OpenCV 给你的通常是:
HWC
[640, 640, 3]
BGR
uint8
0~255YOLO 的 PyTorch/ONNX 输入可能要求:
CHW
[3,640,640]
RGB
float32
0~1这就是为什么:
shape 对了,不代表模型输入就对了。
你以后检查一个 YOLO 输入,至少检查:
① Width / Height
② NCHW 还是 NHWC
③ RGB 还是 BGR
④ uint8 / float16 / float32 / int8
⑤ 是否 /255
⑥ resize 还是 letterbox尤其你以后搞 RKNN:
ONNX:
NCHW float32
↓ RKNN转换
RKNN:
可能使用 NHWC uint8所以永远以实际模型 Tensor 属性为准,不要背死。
四、接下来才是最关键的:YOLO 输出怎么看?
先从最容易理解的 YOLOv5 HBB 开始。
假设:
输入:
[1,3,640,640]
输出:
[1,25200,85]第一次看到:
25200 × 85很多人都会懵。
其实拆开非常简单。
五、先看最后面的 85
假设这个 YOLO 是 COCO 80 类:
85 = 4 + 1 + 80也就是:
┌────────────┬──────────────┬─────────────┐
│ 4个框参数 │ 1个obj │ 80个类别 │
├────────────┼──────────────┼─────────────┤
│ x y w h │ objectness │ cls0...cls79 │
└────────────┴──────────────┴─────────────┘所以其中某一个目标可能是:
[
320,
240,
100,
60,
0.9,
0.01,
0.02,
0.80,
0.03,
...
]我们拆一下。
框:
cx = 320
cy = 240
w = 100
h = 60表示:
w=100
┌───────────────┐
│ │
│ ● │
│ (320,240) │ h=60
│ │
└───────────────┘注意它通常是:
cx cy w h不是:
x1 y1 x2 y2六、obj 是什么?
例如:
objectness = 0.9粗略理解:
YOLO 认为“这里确实有个东西”的可信程度。
然后:
class0 = 0.01
class1 = 0.02
class2 = 0.80
...假设:
class2 = car那么:
类别 = carYOLOv5 常见最终 confidence:
confidence
=
objectness × class_probability
=
0.9 × 0.8
=
0.72所以最终变成:
car
confidence = 0.72
cx = 320
cy = 240
w = 100
h = 60这时候其实神经网络的任务已经基本结束了。
剩下的,就是你的:
后处理。
七、后处理第一步:置信度过滤
比如你设置:
conf_threshold = 0.25;那么:
25200 个候选框
↓ confidence > 0.25
可能剩 100 个大量垃圾框直接删掉。
八、第二步:把 xywh 转成 xyxy
模型给你:
cx = 320
cy = 240
w = 100
h = 60你画框通常需要:
x1 y1 ┌─────────────┐
│ │
│ │
└─────────────┘ x2 y2所以:
x1 = cx - w/2
y1 = cy - h/2
x2 = cx + w/2
y2 = cy + h/2代进去:
x1 = 320 - 50 = 270
y1 = 240 - 30 = 210
x2 = 320 + 50 = 370
y2 = 240 + 30 = 270于是:
[270,210,370,270]九、第三步:NMS
这是你后处理里一定会遇到的。
比如 YOLO 检测同一辆车:
┌───────────┐
│ car 0.91 │
│ ┌────────────┐
│ │ car 0.85 │
│ │ │
└────│────────────┘
└────────────┘它可能吐出来很多高度重叠的框:
框A car 0.91
框B car 0.85
框C car 0.72实际上都是同一辆车。
NMS 就负责:
保留 A
删除 B
删除 C所以:
模型输出
↓
confidence filter
↓
NMS
↓
真正的检测结果十、最后还有一个你以后一定会踩坑的:坐标映射
你的摄像头可能是:
1920 × 1080但模型是:
640 × 640模型输出:
x = 320
y = 200这是:
640×640 模型坐标系里的位置。
而不是原始:
1920×1080所以最后还要:
模型坐标
↓
去掉 letterbox padding
↓
除以 resize ratio
↓
原图坐标这一步叫:
scale coordinates或者:
逆 letterbox所以完整后处理其实已经出来了:
YOLO输出
│
↓
解析每个 candidate
│
↓
计算 confidence
│
↓
confidence过滤
│
↓
xywh → xyxy
│
↓
NMS
│
↓
映射回原始图像尺寸
│
↓
class / score / bbox这就是你当前阶段所谓的:
YOLO 后处理。
十一、那 25200 又是什么?
这个你也应该理解,但不需要研究神经网络内部。
YOLOv5 640×640 一般有三个检测尺度:
80×80
40×40
20×20因为 YOLOv5 每个位置有 3 个 Anchor:
80 × 80 × 3 = 19200
40 × 40 × 3 = 4800
20 × 20 × 3 = 1200加起来:
19200 + 4800 + 1200
= 25200所以:
[1,25200,85]你现在完全可以理解成:
这一张图片
模型认为一共有
25200 个“候选位置”
每个候选位置提供
85 个数字然后你的后处理把 25200 个候选结果筛到:
5 个
10 个
20 个真正的目标。
十二、YOLOv8 / YOLO11 又会不一样
这就是为什么我不希望你单纯背:
YOLO输出 = [1,25200,85]那样以后换 YOLO11 你又懵了。
比如一个典型 YOLOv8 / YOLO11 Detect ONNX 模型,你可能看到:
[1,84,8400]为什么是 84?
COCO 80 类:
84 = 4 + 80你发现:
YOLOv5
4 + 1 + 80
↑
obj
YOLOv8/11
4 + 80典型 Ultralytics YOLOv8/11 检测输出:
没有独立 obj所以:
confidence = max(class_score)而不是 YOLOv5 的:
obj × class_score十三、为什么是 8400?
因为 YOLOv8/11 是 anchor-free:
80 × 80 = 6400
40 × 40 = 1600
20 × 20 = 400于是:
6400 + 1600 + 400
= 8400所以你以后看到:
[1,84,8400]脑子里应该马上尝试:
84 = 4 + nc?
8400 = 80×80 + 40×40 + 20×20?如果:
nc = 3那么你自己的模型可能看到:
[1,7,8400]
7 = 4 + 3一下就读懂了。
十四、这里教你一个非常重要的“拆 shape”思维
以后看到任何 YOLO 输出:
[1, X, Y]不要慌。
开始猜:
有没有:
4 + nc
5 + nc
4 + 1 + nc
4 + nc + angle
5 + nc + angle然后另外一个数字:
是不是:
8400
25200
80×80
40×40
20×20你就会逐渐发现:
YOLO 输出看起来很玄学,其实很多时候就是小学拆数字。
十五、再说你真正会碰到的 OBB
你现在做的很多是:
YOLO OBBHBB 普通框:
cx cy w h只能这样:
┌─────────────┐
│ │
│ 汽车 │
│ │
└─────────────┘OBB:
cx cy w h θ多一个:
θ = angle于是可以:
╱────────╲
╱ ╲
╲ ╱
╲──────────╱也就是说,本质上的变化只是:
HBB
[x,y,w,h]变成:
OBB
[x,y,w,h,angle]当然,不同 OBB 项目可能输出:
cx cy w h angle也可能直接:
x1 y1
x2 y2
x3 y3
x4 y4所以:
OBB 绝对不能看到 shape 就凭经验硬猜,一定要确认模型实现。
尤其你的 YOLOv5 OBB 并不是 Ultralytics 当前官方 OBB 那一套,输出格式很可能和 YOLO11-OBB 不完全一样。
十六、OBB 后处理有什么不同?
大流程其实没有变:
模型输出
↓
解析 bbox
↓
解析 angle
↓
解析 class
↓
confidence过滤
↓
Rotated NMS
↓
坐标恢复
↓
OBB区别主要在两个地方:
普通 YOLO:
xywh → xyxy
普通 IoU
普通 NMSOBB:
xywhr
旋转框 IoU / Polygon IoU
Rotated NMS所以你不需要把它当成完全不同的一套东西。
十七、还有一个大坑:模型输出可能分两种
这个和你最近考虑的:
“是否去检测头”
直接相关。
第一种:已经 Decode 的输出
例如:
[1,25200,85]里面已经基本是:
x
y
w
h
obj
classes...那么你的工作非常舒服:
过滤
↓
NMS
↓
坐标转换第二种:Raw Head 输出
你可能看到三个输出:
output0
[1,3,80,80,85]
output1
[1,3,40,40,85]
output2
[1,3,20,20,85]这时候里面的:
tx
ty
tw
th还不是最终的 x y w h。
你还必须自己 Decode。
例如 YOLOv5 大致有:
x = (sigmoid(tx) * 2 - 0.5 + grid_x) * stride
y = (sigmoid(ty) * 2 - 0.5 + grid_y) * stride
w = (sigmoid(tw) * 2)^2 * anchor_w
h = (sigmoid(th) * 2)^2 * anchor_h这时候:
Raw Tensor
↓
Sigmoid
↓
Grid
↓
Stride
↓
Anchor
↓
真正的 xywh
↓
confidence
↓
NMS这才是真正意义上比较完整的:
YOLO 后处理。
十八、YOLOv8 / YOLO11 Raw Head 又不一样
这也是为什么你的“通用 YOLO 转换工具”不能认为:
v5后处理代码
复制一下
↓
v8
↓
v11不行。
YOLOv8/11 bbox regression 一般涉及:
Anchor Point
Stride
DFL而不是 YOLOv5 那套:
Anchor Width
Anchor Height大概可以先理解成:
YOLOv5
raw
↓
grid + stride + anchor
↓
bbox而:
YOLOv8/11
raw
↓
DFL decode
↓
anchor point + stride
↓
bboxDFL 你现在完全不用学。
等你已经能独立看懂普通输出以后,我们再学。
十九、所以我建议你的学习顺序非常明确
不要现在一股脑:
YOLOv5
YOLOv8
YOLO11
RKNN
INT8
OBB
DFL
NMS
Detect Head全部一起学。
很容易再次乱掉。
你按这个顺序:
第一阶段
YOLOv5 HBB
已Decode输出
[1,25200,5+nc]
目标:
看懂 xywh + obj + class
↓
第二阶段
亲手写最简单的 Python 后处理
conf filter
xywh→xyxy
NMS
scale box
↓
第三阶段
YOLOv5 Raw Head
80×80
40×40
20×20
学习:
grid
stride
anchor
sigmoid
decode
↓
第四阶段
YOLOv8 / YOLO11
理解:
anchor-free
8400
无单独objectness
DFL
↓
第五阶段
OBB
理解:
xywhr
angle
rotated NMS
↓
第六阶段
RKNN
把前面的东西移植成:
RKNN Tensor
↓
C/C++ postprocess这个路径会顺很多。
二十、而且你现在不用“学习所有 YOLO”
你真正应该形成的是这样一种习惯:
比如别人丢给你:
model.onnx你第一件事情不是:
“这是 YOLO11,我得去网上搜 YOLO11 后处理代码。”
而应该是:
model.onnx
↓
输入是什么?
↓
输出有几个?
↓
shape是什么?
↓
每个维度是什么?
↓
输出已经Decode了吗?
↓
有没有obj?
↓
bbox是什么表示形式?
↓
score有没有做sigmoid?
↓
需不需要DFL?
↓
需不需要NMS?到这个程度,你就真的开始有模型部署能力了。
二十一、你现在可以马上做一个非常简单的练习
比如以后看到:
input:
[1,3,640,640]
output:
[1,25200,8]你应该尝试分析:
假设:
nc = 3那么:
8
= 4 + 1 + 3所以极有可能:
0 cx
1 cy
2 w
3 h
4 objectness
5 class0
6 class1
7 class2再比如:
output:
[1,7,8400]模型也是 3 类。
那么:
7 = 4 + 3结合:
8400
=
80×80 + 40×40 + 20×20你就应该高度怀疑:
YOLOv8 / YOLO11 风格
4 bbox + 3 classes
无独立objectness你已经开始会“看模型”了。
二十二、以后你可以把 YOLO 部署看成这三层
我建议你脑子里长期保留这个图:
┌─────────────────────────────┐
│ 第一层:模型输入 │
│ │
│ shape │
│ dtype │
│ RGB/BGR │
│ NCHW/NHWC │
│ resize/letterbox │
│ normalize │
└──────────────┬──────────────┘
↓
┌─────────────────────────────┐
│ 第二层:模型输出 │
│ │
│ output数量 │
│ shape │
│ bbox │
│ objectness │
│ class │
│ angle │
│ raw / decoded │
└──────────────┬──────────────┘
↓
┌─────────────────────────────┐
│ 第三层:后处理 │
│ │
│ decode │
│ confidence │
│ threshold │
│ NMS / Rotated NMS │
│ coordinate scale │
│ final result │
└─────────────────────────────┘你现在只学这三层。
至于:
Backbone
Neck
C3
C2f
SPPF
Bottleneck
Conv
BatchNorm现在可以全部放一边。
我建议我们下一步直接实战
而且先不要拿 YOLO11 OBB 开刀,那个变量太多。
我们就拿一个最普通的:
YOLOv5
640×640
HBB我下一步可以带你真正从:
一张 1920×1080 图片
↓
letterbox
↓
[1,3,640,640]
↓
YOLO
↓
[1,25200,85]
↓
取出其中一个 candidate
↓
算 confidence
↓
xywh → xyxy
↓
NMS
↓
恢复成 1920×1080 坐标手算一遍。
这遍做完以后,你对“YOLO 输入输出 + 后处理”就不会再只是一个模糊概念了。
