3. 目标检测基础:任务定义、IoU、NMS 与 Anchor 机制

各位同学,欢迎来到第三章。从这一章开始,我们正式进入目标检测的核心地带。很多初学者一上来就调 YOLO 的代码,跑通了就觉得自己会了。但遇到工业场景里的刁钻问题——比如小目标漏检、重复框太多、正负样本不平衡——马上就懵了。

为什么?因为底层的四个概念没吃透:任务定义、IoU、NMS 和 Anchor。今天我就把这四个东西掰开了讲,结合我踩过的坑,帮你把地基打牢。

3.1 目标检测任务定义

先问个问题:图像分类和目标检测,到底差在哪?

分类只回答「这是什么」,检测要回答「这是什么 + 在哪」。所以一个检测模型的输出,本质上是一组 边界框 + 类别标签 + 置信度 的集合。

检测输出的标准形式:
每张图输出 N 个检测结果,每个结果包含:
[x_center, y_center, width, height, class_id, confidence]

在工业项目里,我习惯把检测任务拆成两个子问题:

  • 定位——框得准不准(用 IoU 衡量)
  • 分类——认得对不对(用置信度 + 类别概率衡量)

你想想看,如果框偏了但类别对了,产线可能误判;如果框准了但类别错了,那更麻烦。所以两个都得抓。

3.2 IoU(交并比)—— 框的「重合度」尺子

IoU 全称 Intersection over Union,说白了就是两个框的交集面积 ÷ 并集面积。值域 [0, 1],越大说明框得越准。

def compute_iou(box1, box2):
    # 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 = 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 = area1 + area2 - inter

    return inter / union if union > 0 else 0.0

这段代码我几乎每个项目都会用到。注意那个 max(0, ...),防止两个框不相交时出现负面积。

我的经验: 工业场景里,IoU > 0.5 算「检出」,IoU > 0.75 算「精准」。但有些精密装配项目,客户要求 IoU > 0.9,那训练难度直接翻倍。

IoU 还有几个变种,我列个表给你参考:

指标 公式 特点
IoU 交集 / 并集 最常用,对尺度不敏感
GIoU IoU - (外接矩形差 / 外接矩形) 解决 IoU=0 时梯度消失
DIoU IoU - (中心点距离² / 外接对角线²) 加入中心点距离惩罚
CIoU DIoU + 长宽比惩罚 YOLOv5/v8 默认损失

我曾经在一个 PCB 缺陷检测项目里,用普通 IoU 做 NMS,结果两个挨得很近的焊点总被合并成一个。换成 DIoU 后,召回率直接提了 5 个点。所以别小看这个指标,选对了事半功倍。

3.3 NMS(非极大值抑制)—— 去重利器

模型预测时,同一个目标周围会冒出几十个框。NMS 的作用就是:只留下最好的那个。

算法流程其实很简单:

  1. 把所有框按置信度从高到低排序
  2. 选置信度最高的框,保留它
  3. 跟剩下的框算 IoU,如果 IoU > 阈值(比如 0.5),就干掉
  4. 重复 2-3,直到没框了
def nms(boxes, scores, iou_threshold=0.5):
    indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
    keep = []

    while indices:
        i = indices[0]
        keep.append(i)
        rest = indices[1:]

        # 计算当前框与其余框的 IoU
        ious = [compute_iou(boxes[i], boxes[j]) for j in rest]
        indices = [rest[j] for j in range(len(rest)) if ious[j] <= iou_threshold]

    return keep
避坑指南: 我曾经在一个密集小目标场景(比如药片计数)里,把 NMS 阈值设成 0.3,结果大量重叠的药片被误删。后来改成 0.1 才勉强保住召回。记住:密集场景用低阈值,稀疏场景用高阈值。

NMS 也有改进版:Soft-NMS。它不直接删框,而是按 IoU 大小降低置信度。我试过几次,效果有提升但速度慢了些。工业部署时,如果算力有限,还是用标准 NMS 加调参更实在。

3.4 Anchor 机制 —— 检测的「先验框」

Anchor 是什么?说白了就是预设好的一组框的尺寸和比例。模型不是直接预测框的坐标,而是预测「相对于 Anchor 的偏移量」。

为什么要这么干?因为直接回归坐标太难了。给模型一个先验知识——「目标大概这么宽、这么高」——训练会快很多。

Anchor 的核心思想:
预测值 = Anchor 基准值 + 偏移量
模型只需要学「微调」,不需要从零学「定位」。

YOLOv5 的 Anchor 是怎么来的?用 K-Means 对训练集的标注框做聚类。我一般设 9 个 Anchor,分 3 个尺度:

特征图尺度 感受野 Anchor 尺寸(举例) 适合目标
大 (80×80) 小 (10,13), (16,30), (33,23) 小目标
中 (40×40) 中 (30,61), (62,45), (59,119) 中等目标
小 (20×20) 大 (116,90), (156,198), (373,326) 大目标

注意,这些尺寸是相对于输入图像(比如 640×640)的像素值。你换一个输入尺寸,Anchor 要重新算。

我的习惯: 每次换数据集,第一件事就是跑一下 kmeans_anchors.py,看看默认 Anchor 跟实际标注框的匹配度。如果平均 IoU 低于 0.6,说明 Anchor 不合适,必须重新聚类。

Anchor 的匹配策略也很关键。训练时,每个标注框会跟所有 Anchor 算 IoU,选 IoU 最大的那个作为正样本。如果最大 IoU 都小于阈值(比如 0.3),就当成背景。这个阈值设高了,正样本太少;设低了,噪声太多。我一般设在 0.3~0.4 之间。

3.5 知识结构总览

下面这张图把本章四个核心概念串起来了,你可以对照着复习:

目标检测基础 · 知识结构 目标检测任务 任务定义 IoU 交并比 NMS 非极大值抑制 Anchor 机制 定位 + 分类 + 置信度 GIoU / DIoU / CIoU Soft-NMS / 阈值调参 K-Means 聚类 / 多尺度

这四个概念环环相扣:任务定义告诉你「做什么」,IoU 告诉你「做多好」,NMS 帮你「去冗余」,Anchor 帮你「定起点」。后面讲 YOLO 网络结构时,你会发现每个模块都在跟这四个东西打交道。

本章小结:
· 目标检测 = 定位 + 分类
· IoU 是框重合度的核心度量,有多个改进版本
· NMS 用于去除重复检测框,阈值选择看场景
· Anchor 是预设先验框,需要根据数据集聚类调整