今日目标 :理解目标检测的核心概念(Anchor/IoU/NMS/mAP),掌握两阶段和单阶段检测器的本质区别
预计阅读 :10分钟 | 动手操作:40分钟
一、分类 vs 检测:多了一个框
图像分类:这张图里是什么? → "猫"
目标检测:图里有什么?在哪? → "猫"在(x=100, y=50, w=200, h=300)
检测 = 分类 + 定位
检测比分类多了一个"画框"的步骤,但难度翻了10倍!
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
torch.manual_seed(42)
二、核心概念:检测的基石
2.1 IoU(交并比)--- 衡量框的相似度
python
def compute_iou(box1, box2):
"""
计算两个框的IoU(Intersection over Union)
IoU = 交集面积 / 并集面积
值域:[0, 1],越大越接近
这是目标检测中最核心的指标!
判断检测是否正确、NMS、匹配Anchor都用它
"""
# box格式: [x1, y1, x2, y2] 左上角和右下角
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])
# 交集面积
inter_area = max(0, x2 - x1) * max(0, y2 - y1)
# 各自面积
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
# 并集面积
union_area = area1 + area2 - inter_area
return inter_area / union_area if union_area > 0 else 0
def compute_iou_matrix(boxes1, boxes2):
"""
批量计算IoU矩阵
boxes1: (N, 4) 格式 [x1, y1, x2, y2]
boxes2: (M, 4)
返回: (N, M) 的IoU矩阵
"""
# 计算交集
lt = np.maximum(boxes1[:, None, :2], boxes2[None, :, :2]) # (N, M, 2)
rb = np.minimum(boxes1[:, None, 2:], boxes2[None, :, 2:]) # (N, M, 2)
wh = np.maximum(0, rb - lt) # (N, M, 2)
inter = wh[:, :, 0] * wh[:, :, 1] # (N, M)
# 计算各自面积
area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1]) # (N,)
area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1]) # (M,)
# 并集
union = area1[:, None] + area2[None, :] - inter
return inter / union
# 演示IoU计算
box_gt = [50, 50, 150, 150] # 真实框
preds = [
[50, 50, 150, 150], # 完美匹配 → IoU=1.0
[60, 60, 140, 140], # 稍微偏小 → IoU≈0.64
[30, 30, 120, 120], # 偏移较多 → IoU≈0.34
[200, 200, 300, 300], # 完全不重叠 → IoU=0
]
print("IoU计算演示:")
print(f" GT框: {box_gt}")
for i, pred in enumerate(preds):
iou = compute_iou(box_gt, pred)
bar = '█' * int(iou * 20)
print(f" 预测{i+1}: {pred} → IoU={iou:.3f} {bar}")
2.2 NMS(非极大值抑制)--- 去掉重复框
python
def nms(boxes, scores, iou_threshold=0.5):
"""
非极大值抑制 (Non-Maximum Suppression)
目标:从一堆重叠的检测框中,只保留最好的那个
算法流程:
1. 按置信度降序排列
2. 取置信度最高的框
3. 删除所有和它IoU > threshold的框
4. 重复2-3直到没有框
这是目标检测后处理的标配!
"""
if len(boxes) == 0:
return []
boxes = np.array(boxes)
scores = np.array(scores)
# 按置信度降序排列
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
# 当前置信度最高的框
i = order[0]
keep.append(i)
if order.size == 1:
break
# 计算当前框和剩余框的IoU
ious = compute_iou(boxes[i], boxes[order[1:]])
# 保留IoU < threshold的框
mask = ious < iou_threshold
order = order[1:][mask]
return keep
# 演示NMS
def nms_demo():
boxes = np.array([
[100, 100, 200, 200], # 置信度最高的框
[105, 105, 195, 195], # 和框1高度重叠 → 会被抑制
[300, 300, 400, 400], # 另一个物体 → 保留
[110, 110, 210, 210], # 和框1重叠 → 会被抑制
[310, 310, 390, 390], # 和框3重叠 → 会被抑制
])
scores = np.array([0.9, 0.85, 0.8, 0.75, 0.7])
keep = nms(boxes, scores, iou_threshold=0.5)
print("\nNMS演示:")
print("输入: 5个框")
print(f"输出: {len(keep)}个框 (idx: {keep})")
for i in keep:
print(f" 保留框{i}: {boxes[i].tolist()}, score={scores[i]}")
nms_demo()
2.3 Anchor(锚框)--- 预设的候选框
python
def generate_anchors(base_size=16, scales=[8, 16, 32], ratios=[0.5, 1, 2]):
"""
生成Anchor框
Anchor = 预先定义的一组不同大小和长宽比的框
神经网络的任务是:
1. 判断每个Anchor是否包含物体(分类)
2. 调整Anchor的位置和大小以更精确地框住物体(回归)
不用Anchor的方法:直接预测框的坐标 → Anchor-Free检测器
"""
anchors = []
for scale in scales:
for ratio in ratios:
w = base_size * scale * np.sqrt(ratio)
h = base_size * scale / np.sqrt(ratio)
anchors.append([-w/2, -h/2, w/2, h/2]) # 以原点为中心
return np.array(anchors)
def anchor_demo():
"""演示Anchor的意义"""
# 特征图上一个点,生成9个不同形状的Anchor
anchors = generate_anchors()
print("\nAnchor演示(特征图上一点生成的9个Anchor):")
print(f"{'Anchor':<10s} {'宽':>8s} {'高':>8s} {'面积':>8s} {'宽高比':>8s}")
print('-' * 50)
for i, anchor in enumerate(anchors):
w = anchor[2] - anchor[0]
h = anchor[3] - anchor[1]
area = w * h
ratio = w / h
print(f"Anchor {i+1:<3d} {w:>8.0f} {h:>8.0f} {area:>8.0f} {ratio:>8.2f}")
anchor_demo()
三、两阶段检测器:精雕细琢
3.1 R-CNN → Fast R-CNN → Faster R-CNN
python
"""
两阶段检测器的进化:
┌──────────────┬────────────────┬──────────────┬──────────────────────┐
│ 模型 │ 年份 │ 核心贡献 │ 问题 │
├──────────────┼────────────────┼──────────────┼──────────────────────┤
│ R-CNN │ 2014 │ 首个CNN检测器│ 每张图2000次CNN推理 │
│ │ │ │ 极慢:47秒/张 │
│ Fast R-CNN │ 2015 │ 共享卷积特征 │ 外部候选框生成慢 │
│ │ │ RoI Pooling │ │
│ Faster R-CNN │ 2015 │ RPN自动生成 │ 第一个端到端检测器 │
│ │ │ 候选框 │ 准但不快 │
└──────────────┴────────────────┴──────────────┴──────────────────────┘
两阶段检测器的核心思想:
第一阶段:找出"可能有物体"的区域(候选框)
第二阶段:对每个候选框做精细分类和回归
"""
3.2 Faster R-CNN架构解析
python
class FasterRCNNArchitecture:
"""
Faster R-CNN = Backbone + RPN + RoI Head
┌──────────────────────────────────────────────┐
│ 输入图像 │
└─────────────────┬────────────────────────────┘
┌─────────────────▼────────────────────────────┐
│ Backbone (VGG/ResNet) │
│ 提取共享特征图 │
└─────────────────┬────────────────────────────┘
│
┌────────────┴────────────┐
┌────▼────┐ ┌─────▼─────┐
│ RPN │ │ 特征图 │
│ 候选框 │ │ (共享) │
│ 生成网络 │ │ │
└────┬────┘ └─────┬─────┘
│ 候选框坐标 │
┌────▼────────────────────────▼─────┐
│ RoI Pooling │
│ 将不同大小的候选框变成固定尺寸 │
└────────────────┬──────────────────┘
┌────────────────▼──────────────────┐
│ RoI Head │
│ 分类: 这个框是什么物体? │
│ 回归: 框的位置需要怎么调整? │
└───────────────────────────────────┘
"""
def __init__(self):
self.backbone = "ResNet-50 (去掉最后的fc层)"
self.rpn = "RPN: 3×3卷积 → 两条分支(分类/回归)"
self.roi_pooling = "RoI Pooling: 将任意大小的候选框→7×7固定大小"
self.roi_head = "全连接层 → 分类 + 回归"
def describe(self):
print("Faster R-CNN架构:")
print(f" 1. Backbone: {self.backbone}")
print(f" 2. RPN: {self.rpn}")
print(f" 3. RoI Pooling: {self.roi_pooling}")
print(f" 4. RoI Head: {self.roi_head}")
print(f"\n 特点: 精度高,速度慢(~5 FPS on V100)")
print(f" 端侧: 不适合!太慢了")
FasterRCNNArchitecture().describe()
四、单阶段检测器:唯快不破
4.1 YOLO系列和SSD
python
"""
单阶段检测器:一步到位
核心思想:直接在特征图上预测类别和位置
不需要RPN → 不需要RoI Pooling → 速度快!
┌──────────────┬────────────────┬──────────────┬──────────────────────┐
│ 模型 │ 年份 │ 核心贡献 │ 端侧AI │
├──────────────┼────────────────┼──────────────┼──────────────────────┤
│ YOLOv1 │ 2016 │ 首个实时检测器│ 快但不够准 │
│ SSD │ 2016 │ 多尺度特征图 │ 端侧可用 │
│ YOLOv2/v3 │ 2017/2018 │ Anchor+FPN │ 端侧常用 │
│ YOLOv4/v5 │ 2020 │ 各种trick组合 │ 端侧最流行! │
│ YOLOv8/v10 │ 2023/2024 │ Anchor-Free │ 端侧首选! │
└──────────────┴────────────────┴──────────────┴──────────────────────┘
"""
class YOLOArchitecture:
"""
YOLO = You Only Look Once
把检测问题转化为回归问题:
1. 将图像分成S×S的网格
2. 每个网格预测B个边界框和C个类别概率
3. 一次前向传播,同时输出所有检测结果
YOLO的损失函数(核心):
loss = λ_coord * 坐标损失
+ 置信度损失(有物体的框)
+ λ_noobj * 置信度损失(没有物体的框)
+ 分类损失
其中 λ_coord=5, λ_noobj=0.5 是为了平衡正负样本
"""
def __init__(self):
self.core_idea = "You Only Look Once → 一次前向传播完成检测"
self.grid = "S×S网格 → 每格预测B个框 + C个类别"
self.speed = "30-200+ FPS (取决于版本)"
self.edge = "端侧AI事实标准!"
def describe(self):
print("YOLO架构:")
print(f" 核心思想: {self.core_idea}")
print(f" 网格划分: {self.grid}")
print(f" 速度: {self.speed}")
print(f" 端侧: {self.edge}")
YOLOArchitecture().describe()
4.2 两阶段 vs 单阶段 完整对比
python
def compare_detectors():
"""两阶段和单阶段检测器的全面对比"""
comparison = {
'两阶段 (Faster R-CNN, Cascade R-CNN)': {
'精度': '⭐⭐⭐⭐⭐ 高',
'速度': '⭐⭐ 慢 (~5 FPS)',
'小目标检测': '⭐⭐⭐⭐ 好',
'训练复杂度': '⭐⭐⭐⭐ 复杂',
'端侧部署': '⭐⭐ 不适合',
'适用场景': '学术研究、高精度要求、离线分析',
'代表模型': 'Faster R-CNN, Mask R-CNN, Cascade R-CNN',
},
'单阶段 (YOLO, SSD, RetinaNet)': {
'精度': '⭐⭐⭐⭐ 较高',
'速度': '⭐⭐⭐⭐⭐ 快 (30-200+ FPS)',
'小目标检测': '⭐⭐⭐ 中等(YOLOv8+改善很多)',
'训练复杂度': '⭐⭐ 简单',
'端侧部署': '⭐⭐⭐⭐⭐ 非常适合',
'适用场景': '实时检测、端侧部署、嵌入式设备',
'代表模型': 'YOLOv5/v8/v10, SSD, RetinaNet',
},
}
print("两阶段 vs 单阶段 完整对比:\n")
for category, metrics in comparison.items():
print(f"【{category}】")
for k, v in metrics.items():
print(f" {k:<12s}: {v}")
print()
compare_detectors()
五、动手实践
5.1 实战:从零实现IoU和NMS的完整评估
python
def evaluate_detections(pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels, iou_threshold=0.5):
"""
评估目标检测结果
核心流程:
1. NMS去重
2. 匹配预测框和真实框(IoU > threshold)
3. 计算TP/FP/FN
4. 计算Precision/Recall/mAP
"""
# NMS
keep = nms(pred_boxes, pred_scores, iou_threshold)
pred_boxes = pred_boxes[keep]
pred_scores = pred_scores[keep]
pred_labels = pred_labels[keep]
# 匹配预测和真实框
matched_gt = set()
tp = np.zeros(len(pred_boxes), dtype=bool)
fp = np.zeros(len(pred_boxes), dtype=bool)
for i, (p_box, p_label) in enumerate(zip(pred_boxes, pred_labels)):
best_iou = 0
best_gt_idx = -1
for j, (g_box, g_label) in enumerate(zip(gt_boxes, gt_labels)):
if j in matched_gt:
continue
if p_label != g_label:
continue
iou = compute_iou(p_box, g_box)
if iou > best_iou:
best_iou = iou
best_gt_idx = j
if best_iou >= iou_threshold:
tp[i] = True
matched_gt.add(best_gt_idx)
else:
fp[i] = True
fn = len(gt_boxes) - len(matched_gt)
# 计算指标
precision = tp.sum() / (tp.sum() + fp.sum()) if (tp.sum() + fp.sum()) > 0 else 0
recall = tp.sum() / (tp.sum() + fn) if (tp.sum() + fn) > 0 else 0
return {
'TP': int(tp.sum()),
'FP': int(fp.sum()),
'FN': fn,
'Precision': precision,
'Recall': recall,
'F1': 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0,
}
# 演示
def detection_eval_demo():
pred_boxes = np.array([
[50, 50, 150, 150], # 正确检测
[60, 60, 140, 140], # 和上面重复(NMS会去掉)
[200, 200, 300, 300], # 错误检测(假阳性)
[400, 400, 500, 500], # 正确检测
])
pred_scores = np.array([0.9, 0.85, 0.8, 0.7])
pred_labels = np.array([0, 0, 0, 1])
gt_boxes = np.array([
[50, 50, 150, 150], # 被检测到
[400, 400, 500, 500], # 被检测到
[600, 600, 700, 700], # 漏检!
])
gt_labels = np.array([0, 1, 2])
result = evaluate_detections(pred_boxes, pred_scores, pred_labels,
gt_boxes, gt_labels)
print("\n检测评估演示:")
print(f" 预测框: {len(pred_boxes)}个")
print(f" 真实框: {len(gt_boxes)}个")
print(f" TP={result['TP']}, FP={result['FP']}, FN={result['FN']}")
print(f" Precision={result['Precision']:.2%}, Recall={result['Recall']:.2%}")
print(f" F1={result['F1']:.2%}")
detection_eval_demo()
5.2 实战:用torchvision体验Faster R-CNN
python
def demo_faster_rcnn():
"""用torchvision体验Faster R-CNN"""
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.models.detection import FasterRCNN_ResNet50_FPN_Weights
# 加载预训练模型
weights = FasterRCNN_ResNet50_FPN_Weights.DEFAULT
model = fasterrcnn_resnet50_fpn(weights=weights)
model.eval()
# 预处理
preprocess = weights.transforms()
print("Faster R-CNN 使用示例:")
print(" 1. 加载预训练模型: fasterrcnn_resnet50_fpn(weights='DEFAULT')")
print(" 2. 预处理: weights.transforms()(image)")
print(" 3. 推理: model([image_tensor])")
print(" 4. 输出: boxes, labels, scores")
print(f"\n 可检测类别数: 91 (COCO数据集)")
print(f" 参数量: {sum(p.numel() for p in model.parameters()):,}")
# 推理示例
# from PIL import Image
# img = Image.open('test.jpg')
# batch = [preprocess(img)]
# with torch.no_grad():
# prediction = model(batch)[0]
# prediction['boxes'] # (N, 4) 检测框
# prediction['labels'] # (N,) 类别
# prediction['scores'] # (N,) 置信度
demo_faster_rcnn()
六、端侧AI检测器选型指南
python
def edge_detection_guide():
"""
端侧AI目标检测模型选型
核心考虑因素:
1. 推理速度(FPS)
2. 模型大小
3. 精度(mAP)
4. 部署难度
"""
guide = {
'Jetson Nano/Orin Nano': {
'推荐': 'YOLOv5s/v8n + TensorRT',
'FPS': '30-60 FPS',
'模型大小': '~10MB',
'精度': '中等',
'为什么': 'YOLO+TensorRT是Jetson的最佳组合',
},
'手机端 (Android/iOS)': {
'推荐': 'YOLOv8n/nano + NCNN/MNN',
'FPS': '15-30 FPS',
'模型大小': '~5MB',
'精度': '中等',
'为什么': '超轻量,适合移动端',
},
'边缘服务器 (T4/A2)': {
'推荐': 'YOLOv5m/v8m + TensorRT/ONNX',
'FPS': '100+ FPS',
'模型大小': '~50MB',
'精度': '高',
'为什么': '算力足够,追求精度',
},
'超低功耗 (树莓派/单片机)': {
'推荐': 'YOLO-Fastest / NanoDet',
'FPS': '5-15 FPS',
'模型大小': '~1-2MB',
'精度': '低',
'为什么': '极致的轻量化',
},
}
print("端侧AI检测器选型指南:")
for platform, config in guide.items():
print(f"\n [{platform}]")
for k, v in config.items():
print(f" {k}: {v}")
edge_detection_guide()
七、常见坑点
坑1:NMS的IoU阈值调不对
python
# IoU阈值太高 → 同一物体多个框(假阳性多)
# IoU阈值太低 → 不同物体被合并(漏检多)
# 经验值:0.5 是常用值,密集场景可以降到0.3-0.4
坑2:Anchor尺寸和数据集不匹配
python
# COCO预训练的Anchor → 用自己的数据(全是小物体)→ 效果差
# 解决方案:用K-Means聚类分析自己数据集的框尺寸,重新设置Anchor
坑3:正负样本不平衡
python
# 一张图上Anchor可能有上万个,但真实物体只有几个
# 正样本(有物体): 负样本(没物体)≈ 1:1000
# 解决方案:Focal Loss(RetinaNet)、OHEM
坑4:检测框坐标格式混乱
python
# 不同框架的框格式不同:
# [x1, y1, x2, y2] ← 左上角+右下角(Pascal VOC, COCO)
# [x, y, w, h] ← 中心点+宽高(YOLO)
# [x, y, w, h] 归一化 ← 除以图像宽高(YOLO训练标签)
# 混用格式 → 框完全错位!
八、今日作业
- 手写IoU和NMS:实现IoU和NMS,用测试用例验证正确性
- 检测评估:跑通检测评估函数,理解TP/FP/FN和Precision/Recall的关系
- 模型调研:调研你所在端侧场景最常用的检测模型,记录模型大小和FPS
- 打卡 :评论区发你的调研结果,格式:"Day 28/100 打卡:目标检测核心概念已掌握!"
今日小结
今天你学会了:
✅ 检测 = 分类 + 定位
✅ IoU(交并比):衡量两个框的相似度
✅ NMS(非极大值抑制):去除重复框
✅ Anchor(锚框):预设的候选框
✅ 两阶段检测器:R-CNN → Fast → Faster (精但慢)
✅ 单阶段检测器:YOLO/SSD (快,端侧首选)
✅ 两阶段vs单阶段完整对比
✅ 检测评估:TP/FP/FN → Precision/Recall/F1
✅ 端侧AI检测器选型指南
✅ 4个经典坑点
明日预告
Day 29:YOLO系列详解 --- 从v1到v8
YOLO进化史、Darknet、CSPNet、PANet、Decoupled Head、Anchor-Free
🔥 关注我,每天解锁一个端侧AI技能!
微信公众号:xxx | 小红书:xxx | CSDN:xxx
评论区打卡,一起坚持100天!
附:小红书图文版
封面标题建议:目标检测入门 | IoU/NMS/Anchor一文搞懂 🎯
P1 --- 封面
标题:目标检测入门
副标题:IoU / NMS / Anchor / 两阶段vs单阶段
关键词:目标检测 / YOLO / Faster R-CNN
P2 --- 三大核心概念
IoU:交并比,衡量两个框有多像
NMS:非极大值抑制,去掉重复框
Anchor:预先定义的参考框,检测的"锚点"
P3 --- 两阶段检测器
R-CNN → Fast → Faster R-CNN
第一阶段:找到可能有物体的区域
第二阶段:精细分类+回归
精度高,速度慢(~5 FPS)
P4 --- 单阶段检测器
YOLO/SSD:一步到位!
直接在特征图上预测
速度快(30-200+ FPS)
端侧AI的事实标准!
P5 --- 端侧选型指南
Jetson: YOLO + TensorRT
手机: YOLOv8n + NCNN
树莓派: YOLO-Fastest/NanoDet
记住:YOLO是端侧首选!
P6 --- 今日作业
手写IoU和NMS + 端侧模型调研
评论区打卡 Day 28/100
标签:#目标检测 #IoU #NMS #YOLO #FasterRCNN
CSDN发布提示:CSDN版本建议在IoU部分放一张两个矩形交并集的示意图,在NMS部分放去重前后的对比图,在Faster R-CNN架构部分放流程图,在两阶段vs单阶段部分放对比表。