目标检测与 YOLO 系列工程落地

本文系统讲解目标检测与 YOLO 系列的工程落地,回答 anchor 与 anchor-free 怎么选、NMS 与置信度阈值怎么调、mAP 指标怎么读等实战问题。覆盖两阶段与单阶段范式、IoU 与 CIoU 损失、NMS 与 Soft-NMS、PR 曲线与 mAP@0.5:0.95,并给出 YOLOv8 推理与手写 NMS 两段可运行代码,附模型对比表、权衡与常见坑。

引言

目标检测是 CV 里需求最旺盛的任务:安防要找人车、工业要找缺陷、零售要找商品。它比分类难,因为要同时回答「在哪」和「是什么」,还要处理同一张图里数量不定的目标。工程上真正的挑战是:标注质量、小目标、密集遮挡、NMS 调参、以及从训练到部署的一致性。

本文按「范式 → 核心概念 → 指标 → YOLO 演进 → 训练 → 部署」的顺序讲清检测全流程,重点把 NMS、mAP、YOLO 的工程细节讲透,并给出可直接运行的代码。

目录

  1. 检测任务的输出与难点
  2. 两大范式
  3. 核心概念
  4. 评估指标
  5. YOLO 演进
  6. 代码:YOLOv8 推理
  7. 代码:手写 NMS
  8. 训练要点
  9. 部署与加速
  10. 模型对比表
  11. 标注格式与数据准备
  12. 扩展任务:分割、姿态与跟踪
  13. 权衡取舍
  14. 常见坑清单
  15. 小结

1. 检测任务的输出与难点

检测的输出是一组 (x1, y1, x2, y2, class_id, score)。难点集中在三处:

  • 数量不定:一张图可能 0 个目标,也可能几百个,无法用固定长度向量表示。
  • 尺度变化:目标可能只有十几像素,也可能占满整图。
  • 密集与遮挡:相邻目标框高度重叠,后处理稍有不慎就漏检或重复检测。

这些难点决定了检测器必须「密集预测 + 后处理」,而不是像分类那样直接输出。

2. 两大范式

2.1 两阶段检测器

以 Faster R-CNN 为代表:第一阶段用 RPN 生成候选区域(proposal),第二阶段对每个候选做分类与回归。精度高、对小目标友好,但速度慢,适合离线或对精度敏感的场景。Faster R-CNN(ResNet-50-FPN)在 COCO 上约 37.4 mAP。

2.2 单阶段检测器

以 YOLO、SSD、RetinaNet 为代表:直接在特征图上密集预测类别与框,一步到位,速度快。早期单阶段精度落后,直到 RetinaNet 用 Focal Loss 解决正负样本极度不平衡,单阶段才追平两阶段。YOLO 系列是工业界最常用的单阶段检测器。

3. 核心概念

3.1 anchor 与 anchor-free

  • anchor-based:在每个位置预设若干不同尺度与长宽比的先验框,网络预测相对 anchor 的偏移。需要聚类数据集得到合适的 anchor 尺寸。
  • anchor-free:直接预测中心点或角点,省去 anchor 设计,代表有 FCOS、CenterNet、YOLOv8 的 anchor-free 头。调参更简单,对新数据集更友好。

3.2 IoU 与损失

IoU 是预测框与真实框的交并比,既是评估基础,也是回归损失的来源。直接优化 IoU 有梯度问题,演进出 GIoU、DIoU、CIoU,其中 CIoU 同时考虑重叠面积、中心距离与长宽比,是 YOLO 默认的回归损失。

3.3 NMS 与 Soft-NMS

NMS 去重:按置信度排序,保留最高分框,删除与它 IoU 超过阈值的框,重复直到处理完。Soft-NMS 不直接删除,而是按重叠程度降低分数,对密集目标更友好。

import numpy as np

def nms(boxes, scores, iou_threshold=0.5):
    order = scores.argsort()[::-1]
    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(int(i))
        if order.size == 1:
            break
        rest = order[1:]
        xx1 = np.maximum(boxes[i, 0], boxes[rest, 0])
        yy1 = np.maximum(boxes[i, 1], boxes[rest, 1])
        xx2 = np.minimum(boxes[i, 2], boxes[rest, 2])
        yy2 = np.minimum(boxes[i, 3], boxes[rest, 3])
        w = np.maximum(0.0, xx2 - xx1)
        h = np.maximum(0.0, yy2 - yy1)
        inter = w * h
        area_i = (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1])
        area_r = (boxes[rest, 2] - boxes[rest, 0]) * (boxes[rest, 3] - boxes[rest, 1])
        iou = inter / (area_i + area_r - inter + 1e-6)
        order = rest[iou <= iou_threshold]
    return keep

NMS 阈值是检测调参里最敏感的参数之一:调低会更激进地去掉重叠框,密集场景容易漏检;调高会保留更多框,密集场景容易重复。

3.4 置信度阈值与 PR 曲线

置信度阈值决定哪些预测算「正」。阈值高,精确率高、召回率低;阈值低则相反。把不同阈值下的精确率与召回率画成曲线就是 PR 曲线,曲线下面积就是 AP。

4. 评估指标

指标含义说明
PrecisionTP / (TP + FP)误报率相关
RecallTP / (TP + FN)漏报率相关
APPR 曲线下面积单类别
mAP@0.5IoU 0.5 时各类 AP 平均宽松
mAP@0.5 :0.95IoU 0.5 到 0.95 步长 0.05 平均COCO 主指标

读指标时要结合业务:安防更看重 Recall(漏报代价高),质检分拣更看重 Precision(误报增加人工)。mAP@0.5:0.95 是综合指标,但线上往往更关心某个置信度阈值下的实际精确率与召回率。

5. YOLO 演进

YOLO 从 2016 年至今经历多次迭代,主线是「更快更准、更好训练」。

版本年份代表模型参数量mAP@0.5 :0.95特点
YOLOv32018Darknet-5362M33.0多尺度预测
YOLOv52020yolov5s7.2M37.4工程化,PyTorch
YOLOv82023yolov8n3.2M37.3anchor-free,统一框架
YOLOv82023yolov8m25.9M50.2精度与速度平衡
YOLOv102024yolov10n2.3M38.5无 NMS 训练
YOLOv112024yolo11n2.6M39.5更高效率

数据为 COCO val2017,输入 640,实际值随实现略有浮动。YOLOv10 与 v11 的一个趋势是「去 NMS」,通过一致性匹配训练减少对 NMS 的依赖。

6. 代码:YOLOv8 推理

ultralytics 把训练、验证、推理、导出统一到一个接口,是目前最省心的工程选择。

from ultralytics import YOLO

model = YOLO("yolov8n.pt")   # 自动下载 COCO 预训练权重

# 推理:conf 是置信度阈值,iou 是 NMS 阈值
results = model("street.jpg", conf=0.25, iou=0.45)[0]

for box in results.boxes:
    cls_id = int(box.cls)
    conf = float(box.conf)
    xyxy = box.xyxy[0].tolist()
    print(results.names[cls_id], f"{conf:.3f}", [round(v, 1) for v in xyxy])

print("detected:", len(results.boxes))

调参经验:conf 从 0.25 起步,业务要求高召回时降到 0.1,要求高精确时升到 0.4;iou 密集场景调高到 0.6,稀疏场景用 0.45。

7. 代码:手写 NMS 验证理解

用自己的 NMS 替换框架内置版本,便于理解与调试。

import numpy as np
import torch

def nms_torch(boxes, scores, iou_threshold=0.5):
    keep = nms(boxes.numpy(), scores.numpy(), iou_threshold)
    return torch.tensor(keep, dtype=torch.long)

boxes = torch.tensor([[0, 0, 10, 10], [1, 1, 11, 11], [50, 50, 60, 60]], dtype=torch.float32)
scores = torch.tensor([0.9, 0.8, 0.7])
print("keep:", nms_torch(boxes, scores, 0.5))   # 保留框 0 与 2,框 1 被抑制

# 逐类 NMS:多类别时必须按类别分别做,否则不同类别会互相抑制
def multiclass_nms(boxes, scores, labels, iou_thr=0.5):
    keep_all = []
    for c in labels.unique():
        m = labels == c
        keep_all += [int(i) for i in nms_torch(boxes[m], scores[m], iou_thr)]
    return keep_all

print(multiclass_nms(boxes, scores, torch.tensor([0, 0, 1])))

一个高频错误是「所有类别一起做 NMS」,导致重叠的不同类别目标互相抑制。多类别检测必须逐类做 NMS。

8. 训练要点

  • 数据格式:YOLO 用每张图一个 txt,每行 class cx cy w h,坐标是相对图像宽高的归一化值(0 到 1)。
  • 预训练:从 COCO 预训练权重微调,比从头训练收敛快得多,小数据集必备。
  • 增强:mosaic(四图拼接)、HSV 抖动、随机缩放与翻转,ultralytics 默认开启,小数据集建议保留。
  • 超参:初始学习率约 0.01(SGD),余弦退火,warmup 若干轮。
  • 类别平衡:长尾类别用重采样或调整损失权重。
  • 验证:固定验证集,关注每类 AP,找出拖后腿的类别补数据。
from ultralytics import YOLO

model = YOLO("yolov8n.pt")
model.train(
    data="dataset.yaml",   # 指定 train/val 路径与类别名
    epochs=100,
    imgsz=640,
    batch=16,
    lr0=0.01,
    patience=20,           # 早停
    mosaic=1.0,
    device=0,
)
metrics = model.val()
print(metrics.box.map, metrics.box.map50)

9. 部署与加速

训练完的模型需要导出为推理友好的格式:

from ultralytics import YOLO

model = YOLO("runs/detect/train/weights/best.pt")
model.export(format="onnx", imgsz=640, opset=12, simplify=True)
model.export(format="engine", half=True)   # TensorRT FP16,需 GPU
  • ONNX:跨平台通用,配合 ONNX Runtime 可在 CPU 与 GPU 上跑。
  • TensorRT:NVIDIA GPU 上最快,FP16 通常能带来 2 倍左右加速,INT8 更快但需校准。
  • NCNN / TFLite:端侧与移动端。

部署时要注意「预处理与后处理必须与训练一致」:letterbox 缩放、归一化、NMS 阈值,任何一处不一致都会掉点。检测的端侧压缩见模型压缩与端侧部署,服务化见视觉服务化与推理加速。

10. 模型对比表

模型参数量mAP@0.5 :0.95T4 延迟场景
Faster R-CNN R5041M37.4高离线高精度
YOLOv8n3.2M37.3约 1.5ms端侧实时
YOLOv8m25.9M50.2约 6ms云端均衡
YOLOv8x68.2M53.9约 12ms云端高精度
YOLOv10n2.3M38.5约 1.4ms无 NMS 实时

延迟为 Tesla T4、FP16、batch 1 的量级参考,实际随实现与预处理浮动。

11. 标注格式与数据准备

检测训练的第一道坎是数据格式。三种主流格式的差异如下:

格式载体坐标备注
Pascal VOC每图一个 XML绝对像素 xmin/ymin/xmax/ymax经典,小数据集
COCO单个 JSON绝对像素 x/y/width/height工业标准,支持分割与关键点
YOLO每图一个 txt归一化 cx/cy/w/h训练直接用

从 COCO 转 YOLO 的常见脚本:

import json
from pathlib import Path

def coco_to_yolo(coco_json, out_dir, img_dir):
    data = json.loads(Path(coco_json).read_text())
    images = {im["id"]: im for im in data["images"]}
    cats = {c["id"]: i for i, c in enumerate(data["categories"])}
    out = Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)

    for ann in data["annotations"]:
        im = images[ann["image_id"]]
        W, H = im["width"], im["height"]
        x, y, w, h = ann["bbox"]                       # COCO 是左上角加宽高
        cx, cy = (x + w / 2) / W, (y + h / 2) / H      # 转归一化中心点
        line = f"{cats[ann['category_id']]} {cx:.6f} {cy:.6f} {w/W:.6f} {h/H:.6f}"
        with open(out / (Path(im["file_name"]).stem + ".txt"), "a") as f:
            f.write(line + "\n")

coco_to_yolo("annotations/instances_train.json", "labels/train", "images/train")

要点:COCO 的 bbox 是左上角加宽高,YOLO 是归一化中心点加宽高,转换时别漏了除以图像宽高。类别 id 要重映射为从 0 开始的连续整数。

12. 扩展任务:分割、姿态与跟踪

YOLOv8 把检测头替换后即可支持其他任务,工程上非常划算:

  • YOLOv8-seg:实例分割,输出框加掩码。
  • YOLOv8-pose:人体姿态,输出框加关键点。
  • YOLOv8-cls:分类。
  • 跟踪:把 YOLOv8 与 ByteTrack 结合,给每帧的框分配稳定 ID。
from ultralytics import YOLO

# 实例分割
seg = YOLO("yolov8n-seg.pt")
r = seg("street.jpg")[0]
print("masks:", None if r.masks is None else r.masks.data.shape)

# 姿态估计
pose = YOLO("yolov8n-pose.pt")
rp = pose("people.jpg")[0]
print("keypoints:", None if rp.keypoints is None else rp.keypoints.data.shape)

# 多目标跟踪:persist=True 在视频帧间保持轨迹
track = YOLO("yolov8n.pt")
for frame in ["f1.jpg", "f2.jpg", "f3.jpg"]:
    res = track.track(frame, persist=True)[0]
    ids = res.boxes.id
    print("track ids:", None if ids is None else ids.tolist())

这套「一个框架多种任务」的设计,让检测、分割、姿态、跟踪共享同一份数据与部署管线,大幅降低工程成本。

13. 权衡取舍

  • 精度与速度:YOLOv8n 到 YOLOv8x 精度差约 16 个点,延迟差约 8 倍,按硬件选。
  • 小目标与分辨率:输入从 640 提到 1280 能显著改善小目标,但计算量翻倍。
  • NMS 阈值:密集场景调高减少漏检,稀疏场景调低减少重复。
  • anchor-free 与 anchor-based:前者省心,后者在特定数据集上可能更准。
  • 两阶段与单阶段:追求极致精度用两阶段,追求吞吐与实时用单阶段。

14. 常见坑清单

  • 标签坐标越界:归一化坐标必须落在 0 到 1,越界会导致训练报错或静默丢弃。
  • 类别 id 从 0 开始:YOLO 从 0 开始,若标注从 1 开始则整体错位。
  • 全类别一起 NMS:不同类别互相抑制,密集多类场景严重掉点。
  • 训练与部署预处理不一致:letterbox 与直接 resize 的差异会让精度掉几个点。
  • 只盯 mAP 不看每类:总 mAP 高但某关键类别 AP 极低,业务仍不可用。
  • 验证集泄漏:训练集与验证集来自同一视频相邻帧,验证指标虚高。
  • 忽略小目标:默认 640 输入对小于 16 像素的目标几乎无效。
  • 忘记置信度阈值与业务对齐:默认 0.25 未必是业务最优。
  • 量化后不重测:INT8 导出后精度可能显著下降,必须重新评估。

15. 小结

目标检测的工程主线是:选范式(单阶段优先)→ 准备 YOLO 格式标注 → COCO 预训练微调 → 调 conf 与 iou → 导出 ONNX 或 TensorRT → 保证预处理一致。指标上,mAP@0.5:0.95 用于横向比较,线上更关心特定阈值下的精确率与召回率。记住三个调参旋钮:置信度阈值、NMS 阈值、输入分辨率,它们直接决定漏检与误报的平衡。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机视觉」更多文章

  1. 检测与分割的评估指标
  2. 多模态视觉语言模型
  3. 3D 视觉:点云与深度估计