引言
目标检测是 CV 里需求最旺盛的任务:安防要找人车、工业要找缺陷、零售要找商品。它比分类难,因为要同时回答「在哪」和「是什么」,还要处理同一张图里数量不定的目标。工程上真正的挑战是:标注质量、小目标、密集遮挡、NMS 调参、以及从训练到部署的一致性。
本文按「范式 → 核心概念 → 指标 → YOLO 演进 → 训练 → 部署」的顺序讲清检测全流程,重点把 NMS、mAP、YOLO 的工程细节讲透,并给出可直接运行的代码。
目录
- 检测任务的输出与难点
- 两大范式
- 核心概念
- 评估指标
- YOLO 演进
- 代码:YOLOv8 推理
- 代码:手写 NMS
- 训练要点
- 部署与加速
- 模型对比表
- 标注格式与数据准备
- 扩展任务:分割、姿态与跟踪
- 权衡取舍
- 常见坑清单
- 小结
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. 评估指标
| 指标 | 含义 | 说明 |
|---|---|---|
| Precision | TP / (TP + FP) | 误报率相关 |
| Recall | TP / (TP + FN) | 漏报率相关 |
| AP | PR 曲线下面积 | 单类别 |
| mAP@0.5 | IoU 0.5 时各类 AP 平均 | 宽松 |
| mAP@0.5 :0.95 | IoU 0.5 到 0.95 步长 0.05 平均 | COCO 主指标 |
读指标时要结合业务:安防更看重 Recall(漏报代价高),质检分拣更看重 Precision(误报增加人工)。mAP@0.5:0.95 是综合指标,但线上往往更关心某个置信度阈值下的实际精确率与召回率。
5. YOLO 演进
YOLO 从 2016 年至今经历多次迭代,主线是「更快更准、更好训练」。
| 版本 | 年份 | 代表模型 | 参数量 | mAP@0.5 :0.95 | 特点 |
|---|---|---|---|---|---|
| YOLOv3 | 2018 | Darknet-53 | 62M | 33.0 | 多尺度预测 |
| YOLOv5 | 2020 | yolov5s | 7.2M | 37.4 | 工程化,PyTorch |
| YOLOv8 | 2023 | yolov8n | 3.2M | 37.3 | anchor-free,统一框架 |
| YOLOv8 | 2023 | yolov8m | 25.9M | 50.2 | 精度与速度平衡 |
| YOLOv10 | 2024 | yolov10n | 2.3M | 38.5 | 无 NMS 训练 |
| YOLOv11 | 2024 | yolo11n | 2.6M | 39.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.95 | T4 延迟 | 场景 |
|---|---|---|---|---|
| Faster R-CNN R50 | 41M | 37.4 | 高 | 离线高精度 |
| YOLOv8n | 3.2M | 37.3 | 约 1.5ms | 端侧实时 |
| YOLOv8m | 25.9M | 50.2 | 约 6ms | 云端均衡 |
| YOLOv8x | 68.2M | 53.9 | 约 12ms | 云端高精度 |
| YOLOv10n | 2.3M | 38.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 阈值、输入分辨率,它们直接决定漏检与误报的平衡。
延伸阅读
- 图像分割与实例分割 — 从框到像素级掩码
- 数据标注与数据集工程 — 检测标注格式与质量
- 卷积网络与经典骨干架构 — 检测骨干的选型
- 模型压缩与端侧部署 — 检测模型的量化与端侧落地
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。