目标检测基础
目标检测基础
简介
目标检测不仅要判断图像里"有什么",还要定位"在哪里"。相比分类任务,它同时处理类别预测和边界框回归,因此在工业缺陷检测、安防监控、自动驾驶、OCR 前处理和零售盘点中都有广泛应用。目标检测的输出是一组边界框(Bounding Box),每个框包含四个坐标值(x1, y1, x2, y2)、一个类别标签和一个置信度分数。根据推理流程的不同,主流检测器可以分为一阶段(One-Stage)和两阶段(Two-Stage)两大路线,各有优劣。
特点
核心概念
什么是目标检测
目标检测(Object Detection)的输入是一张图像,输出是一组检测结果,每个结果包含:
- 边界框(Bounding Box):用 (x1, y1, x2, y2) 或 (cx, cy, w, h) 表示目标在图像中的位置
- 类别标签(Class Label):检测到的目标属于哪个类别
- 置信度分数(Confidence Score):模型对检测结果的确信程度
与图像分类不同,分类只回答"这是什么",检测还要回答"在哪里"。与图像分割不同,检测用矩形框定位,分割用像素级掩码精确勾勒轮廓。
一阶段 vs 两阶段检测器
一阶段检测器:
- YOLO / SSD / RetinaNet
- 优点:速度快,适合实时场景
- 缺点:小目标、复杂场景下可能略逊
两阶段检测器:
- Faster R-CNN / Mask R-CNN
- 优点:精度通常更高
- 缺点:推理更慢一阶段检测器(如 YOLO)直接在特征图上密集预测边界框和类别,不需要候选区域提议阶段,因此速度快。两阶段检测器(如 Faster R-CNN)先通过 RPN(Region Proposal Network)生成候选区域,再对每个候选区域进行分类和边界框精修,精度更高但速度较慢。
Anchor 机制
Anchor(锚框)是预定义的一组不同大小和宽高比的参考框。检测器通过预测 Anchor 与真实目标框之间的偏移量来进行定位。Anchor 的设计需要根据数据集中目标的实际尺寸分布来调整,不合理的 Anchor 设计会直接影响检测效果。
近年来,Anchor-free 检测器(如 CenterNet、FCOS)逐渐流行,它们直接预测目标的中心点和宽高,省去了 Anchor 设计的繁琐过程。
实现
使用 Torchvision 训练/推理 Faster R-CNN
import torch
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.transforms import functional as F
from PIL import Image
model = fasterrcnn_resnet50_fpn(weights="DEFAULT")
model.eval()
image = Image.new("RGB", (640, 480), color="white")
input_tensor = F.to_tensor(image)
with torch.no_grad():
outputs = model([input_tensor])[0]
print(outputs.keys())
print(outputs["boxes"][:3])
print(outputs["scores"][:3])
print(outputs["labels"][:3])过滤低置信度框
# 过滤低置信度框
threshold = 0.5
keep = outputs["scores"] >= threshold
boxes = outputs["boxes"][keep]
scores = outputs["scores"][keep]
labels = outputs["labels"][keep]
print(len(boxes), len(scores), len(labels))自定义数据集目标格式
# 自定义数据集目标格式(简化)
target = {
"boxes": torch.tensor([[50, 40, 200, 180], [260, 100, 400, 300]], dtype=torch.float32),
"labels": torch.tensor([1, 2], dtype=torch.int64),
"image_id": torch.tensor([1]),
"area": torch.tensor([21000.0, 28000.0]),
"iscrowd": torch.tensor([0, 0], dtype=torch.int64)
}完整训练流程
# 示例:使用 Torchvision 训练 Faster R-CNN
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision.models.detection import fasterrcnn_resnet50_fpn, FasterRCNN_ResNet50_FPN_Weights
from torchvision.transforms import functional as F
class DetectionDataset(Dataset):
def __init__(self, num_samples=100):
self.num_samples = num_samples
def __len__(self):
return self.num_samples
def __getitem__(self, idx):
image = torch.rand(3, 480, 640)
boxes = torch.tensor([[50.0, 40.0, 200.0, 180.0]])
labels = torch.tensor([1], dtype=torch.int64)
target = {"boxes": boxes, "labels": labels}
return image, target
dataset = DetectionDataset(50)
loader = DataLoader(dataset, batch_size=4, shuffle=True, collate_fn=lambda x: tuple(zip(*x)))
model = fasterrcnn_resnet50_fpn(weights=FasterRCNN_ResNet50_FPN_Weights.DEFAULT)
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005, momentum=0.9, weight_decay=5e-4)
model.train()
for epoch in range(5):
for images, targets in loader:
loss_dict = model(images, targets)
losses = sum(loss for loss in loss_dict.values())
optimizer.zero_grad()
losses.backward()
optimizer.step()
print(f"Epoch {epoch+1}, Loss: {losses.item():.4f}")YOLO 风格后处理与 NMS
import torch
from torchvision.ops import nms
boxes = torch.tensor([
[10, 10, 100, 100],
[12, 12, 98, 98],
[200, 200, 320, 320]
], dtype=torch.float32)
scores = torch.tensor([0.95, 0.91, 0.88])
keep_idx = nms(boxes, scores, iou_threshold=0.5)
print("keep idx:", keep_idx)
print("kept boxes:", boxes[keep_idx])# 将模型输出转为 [x1, y1, x2, y2] 格式后做 NMS
pred_boxes = torch.tensor([[30, 40, 180, 220], [32, 42, 182, 224]])
pred_scores = torch.tensor([0.87, 0.82])
filtered = nms(pred_boxes, pred_scores, iou_threshold=0.45)
print(filtered)多类别 NMS
# 示例:针对每个类别分别做 NMS
from torchvision.ops import nms, batched_nms
boxes = torch.tensor([
[10, 10, 100, 100], # 类别 0
[12, 12, 98, 98], # 类别 0(与上一个高度重叠)
[200, 200, 320, 320], # 类别 1
[210, 210, 330, 330], # 类别 1(与上一个重叠)
], dtype=torch.float32)
scores = torch.tensor([0.95, 0.91, 0.88, 0.85])
class_ids = torch.tensor([0, 0, 1, 1])
# batched_nms 按类别分组做 NMS
keep = batched_nms(boxes, scores, class_ids, iou_threshold=0.5)
print("batched NMS keep:", keep)
print("kept boxes:", boxes[keep])IoU 计算与评估
# IoU 计算示意
def iou(box_a, box_b):
x1 = max(box_a[0], box_b[0])
y1 = max(box_a[1], box_b[1])
x2 = min(box_a[2], box_b[2])
y2 = min(box_a[3], box_b[3])
inter = max(0, x2 - x1) * max(0, y2 - y1)
area_a = (box_a[2] - box_a[0]) * (box_a[3] - box_a[1])
area_b = (box_b[2] - box_b[0]) * (box_b[3] - box_b[1])
union = area_a + area_b - inter
return inter / union if union > 0 else 0
print(iou([10, 10, 100, 100], [20, 20, 110, 110]))常见评估指标:
- Precision / Recall
- AP(某类别平均精度)
- mAP@0.5
- mAP@0.5:0.95
- FPS / latencyCOCO 风格标注格式
# COCO 风格标注示意
annotation = {
"images": [{"id": 1, "file_name": "a.jpg", "width": 640, "height": 480}],
"annotations": [{"id": 1, "image_id": 1, "category_id": 1, "bbox": [50, 40, 150, 140], "area": 21000, "iscrowd": 0}],
"categories": [{"id": 1, "name": "defect"}]
}
print(annotation)mAP 计算原理
# 示例:简化的 mAP 计算流程
import numpy as np
def calculate_ap(recall, precision):
"""计算 Average Precision(AP)"""
# 在 recall 的 11 个等分点上插值
recall_levels = np.linspace(0, 1, 11)
ap = 0
for r in recall_levels:
# 找到 recall >= r 时的最大 precision
mask = recall >= r
if mask.any():
ap += np.max(precision[mask])
return ap / 11
# 模拟数据
recall = np.array([0.1, 0.3, 0.5, 0.7, 0.9, 0.9, 0.9])
precision = np.array([1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7])
ap = calculate_ap(recall, precision)
print(f"AP@0.5 = {ap:.4f}")
# mAP 是所有类别 AP 的平均值
# 假设有 3 个类别,AP 分别为 0.85, 0.72, 0.91
aps = [0.85, 0.72, 0.91]
mAP = np.mean(aps)
print(f"mAP = {mAP:.4f}")数据增强策略
目标检测的数据增强需要同时变换图像和对应的标注框:
# 示例:使用 Albumentations 进行检测数据增强
# pip install albumentations
import albumentations as A
import cv2
import numpy as np
transform = A.Compose([
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.3),
A.GaussNoise(p=0.2),
A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=10, p=0.5),
A.RandomCrop(width=512, height=512, p=0.3),
], bbox_params=A.BboxParams(
format='pascal_voc', # [x1, y1, x2, y2]
min_area=100,
min_visibility=0.3,
label_fields=['labels']
))
# 使用示例
image = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8)
bboxes = [[50, 40, 200, 180]]
labels = [1]
transformed = transform(image=image, bboxes=bboxes, labels=labels)
transformed_image = transformed['image']
transformed_bboxes = transformed['bboxes']
print(f"原始框: {bboxes}")
print(f"增强后框: {transformed_bboxes}")常用的检测数据增强技巧:
- Mosaic:将 4 张图拼接成 1 张(YOLOv4 引入),增加小目标的上下文信息
- MixUp:两张图像按权重混合,增强泛化能力
- Copy-Paste:将一张图中的目标复制粘贴到另一张图,增加目标多样性
- 随机擦除:随机遮挡图像的一部分,模拟遮挡场景
小目标检测优化
小目标(面积小于 32x32 像素)检测是目标检测中的难点,常见优化策略:
# 示例:使用特征金字塔增强小目标检测
import torch
import torch.nn as nn
from torchvision.models.detection import fasterrcnn_resnet50_fpn
# FPN(Feature Pyramid Network)自动生成多尺度特征图
# 浅层特征图分辨率高,适合检测小目标
# 深层特征图语义强,适合检测大目标
model = fasterrcnn_resnet50_fpn(pretrained=True)
# 查看特征金字塔的层级
# FPN 输出的特征图尺度(相对于输入图像的下采样倍数)
# P2: 1/4, P3: 1/8, P4: 1/16, P5: 1/32
# 优化小目标检测的技巧:
# 1. 提高输入图像分辨率(如从 640 提高到 1280)
# 2. 增加小目标的 Anchor 尺寸
# 3. 使用高分辨率特征图(P2 层)进行检测
# 4. 使用 SAHI(Slicing Aided Hyper Inference)切图推理
print("模型加载完成,FPN 已内置多尺度特征融合")部署优化
ONNX 导出与推理
# 示例:将检测模型导出为 ONNX 格式
import torch
from torchvision.models.detection import fasterrcnn_resnet50_fpn
model = fasterrcnn_resnet50_fpn(pretrained=True)
model.eval()
dummy_input = torch.randn(1, 3, 480, 640)
# 导出 ONNX(需要自定义后处理部分,因为 NMS 不是标准算子)
torch.onnx.export(
model,
dummy_input,
"detection_model.onnx",
input_names=["images"],
output_names=["boxes", "scores", "labels"],
dynamic_axes={"images": {0: "batch_size"}},
opset_version=12
)
print("ONNX 模型导出完成")TensorRT 加速
# 示例:使用 TensorRT 加速推理(伪代码)
# 实际使用需要安装 tensorrt 和 torch2trt
# ONNX -> TensorRT Engine
# trtexec --onnx=detection_model.onnx --workspace=4GB --saveEngine=detection.engine
# TensorRT 优化的关键点:
# 1. 层融合(Layer Fusion):将多个层合并为单个 kernel
# 2. 精度校准(Precision Calibration):FP32 -> FP16/INT8
# 3. 内核自动调优(Kernel Auto-Tuning):选择最优计算核
# 4. 动态内存管理(Dynamic Memory Management):减少内存碎片
# 推理加速效果通常可达 2-5 倍SAHI 切图推理
大图与小目标检测利器
# pip install sahi
# SAHI(Slicing Aided Hyper Inference)通过将大图切片推理再合并
# 特别适合高分辨率图像中的小目标检测
from sahi import AutoDetectionModel, get_sliced_prediction
# 加载 YOLOv8 模型
detection_model = AutoDetectionModel.from_pretrained(
model_type="yolov8",
model_path="yolov8n.pt",
confidence_threshold=0.3,
device="cuda:0",
)
# 切片推理
result = get_sliced_prediction(
"large_image.jpg",
detection_model,
slice_height=640,
slice_width=640,
overlap_height_ratio=0.2,
overlap_width_ratio=0.2,
)
# 导出结果
result.export_visuals(export_dir="output/")
# 遍历检测结果
for prediction in result.object_prediction_list:
bbox = prediction.bbox
score = prediction.score.value
category = prediction.category.name
print(f"类别: {category}, 置信度: {score:.3f}, 框: {bbox}")YOLO 系列实战
YOLOv8 快速推理
# pip install ultralytics
from ultralytics import YOLO
# 加载预训练模型
model = YOLO("yolov8n.pt") # nano 版本,速度最快
# 推理单张图片
results = model("image.jpg")
# 解析结果
for result in results:
boxes = result.boxes
for box in boxes:
cls_id = int(box.cls[0])
conf = float(box.conf[0])
xyxy = box.xyxy[0].tolist() # [x1, y1, x2, y2]
print(f"类别ID: {cls_id}, 置信度: {conf:.3f}, 框: {xyxy}")
# 推理视频
results = model("video.mp4", stream=True)
for result in results:
# 逐帧处理
pass
# YOLOv8 不同规模的模型选择
# yolov8n.pt — Nano(最快,3.2M 参数)
# yolov8s.pt — Small(11.2M 参数)
# yolov8m.pt — Medium(25.9M 参数)
# yolov8l.pt — Large(43.7M 参数)
# yolov8x.pt — Extra Large(68.2M 参数,最准)YOLOv8 自定义训练
from ultralytics import YOLO
# 加载预训练模型(迁移学习)
model = YOLO("yolov8s.pt")
# 准备数据集配置文件 data.yaml
# path: /datasets/my_project
# train: images/train
# val: images/val
# names:
# 0: defect
# 1: scratch
# 训练
results = model.train(
data="data.yaml",
epochs=100,
imgsz=640,
batch=16,
lr0=0.01,
patience=20, # 早停耐心值
augment=True,
mosaic=1.0, # Mosaic 数据增强概率
mixup=0.1, # MixUp 增强
degrees=10, # 旋转角度
translate=0.1, # 平移
scale=0.5, # 缩放
flipud=0.5, # 上下翻转
fliplr=0.5, # 左右翻转
project="runs/train",
name="defect_detection",
)
# 验证
metrics = model.val()
print(f"mAP50: {metrics.box.map50:.4f}")
print(f"mAP50-95: {metrics.box.map:.4f}")
# 导出 ONNX
model.export(format="onnx", imgsz=640, simplify=True)DETR 端到端检测
Transformer-based 检测器
# DETR(DEtection TRansformer)消除了 NMS 和 Anchor 设计
# 使用 Transformer 的 encoder-decoder 架构直接预测检测结果
# DETR 的核心优势:
# 1. 无需 NMS 后处理
# 2. 无需 Anchor 设计
# 3. 端到端训练,使用二分图匹配损失
# 4. 对大目标和拥挤场景表现优秀
# DETR 的局限:
# 1. 训练收敛慢(需要 500 epochs)
# 2. 对小目标检测不如 YOLO 系列
# 3. 计算量较大
# 使用 torchvision 的 DETR
import torch
from torchvision.models.detection import detr_resnet50
model = detr_resnet50(pretrained=True)
model.eval()
image = torch.rand(1, 3, 800, 800)
with torch.no_grad():
outputs = model(image)
# outputs 包含 pred_logits(类别)和 pred_boxes(归一化坐标)
pred_logits = outputs["pred_logits"] # [1, 100, num_classes+1]
pred_boxes = outputs["pred_boxes"] # [1, 100, 4],cx, cy, w, h 归一化
# 后处理:过滤背景类和低置信度
prob = pred_logits.softmax(-1)[0, :, :-1] # 去掉背景类
scores, labels = prob.max(-1)
keep = scores > 0.7
boxes = pred_boxes[0, keep]
labels = labels[keep]
scores = scores[keep]标注工具与数据集构建
常用标注工具
标注工具选择指南:
1. LabelImg / Label Studio
- 支持矩形框标注
- 输出 VOC / YOLO / COCO 格式
- 适合中小规模数据集
2. CVAT (Computer Vision Annotation Tool)
- 支持多人协作标注
- 支持目标检测、分割、跟踪标注
- 适合大规模项目
3. Roboflow
- 在线标注 + 数据增强 + 格式转换
- 直接导出 YOLO / COCO / VOC 格式
- 适合快速原型验证
4. VoTT (Visual Object Tagging Tool)
- 微软开源
- 支持视频标注和关键帧提取
- 适合时序数据标注
标注质量检查清单:
- 框是否紧贴目标边缘(不要太大也不要太小)
- 遮挡目标是否标注为 occluded
- 小目标是否被遗漏
- 同一目标是否被重复标注
- 类别标签是否一致(大小写、缩写统一)YOLO 格式数据集结构
# YOLO 格式数据集目录结构
# dataset/
# ├── images/
# │ ├── train/
# │ │ ├── img001.jpg
# │ │ └── img002.jpg
# │ └── val/
# │ ├── img101.jpg
# │ └── img102.jpg
# ├── labels/
# │ ├── train/
# │ │ ├── img001.txt # 每行:class cx cy w h(归一化坐标)
# │ │ └── img002.txt
# │ └── val/
# │ ├── img101.txt
# │ └── img102.txt
# └── data.yaml
# data.yaml 示例内容
data_yaml = """
path: /datasets/my_project
train: images/train
val: images/val
nc: 3
names: ['defect', 'scratch', 'dent']
"""
print(data_yaml)
# 标注文件格式说明
# 每行格式:class_id center_x center_y width height
# 坐标值归一化到 [0, 1]
# 示例:
# 0 0.45 0.55 0.3 0.4 # 类别0,中心(0.45, 0.55),宽0.3 高0.4推理后处理优化
Soft-NMS 与 WBF
# Soft-NMS:不直接删除重叠框,而是降低其分数
# 适用于密集目标场景(如人群检测)
from torchvision.ops import nms
def soft_nms(boxes, scores, iou_threshold=0.3, sigma=0.5, score_threshold=0.01):
"""Soft-NMS 实现"""
import numpy as np
boxes_np = boxes.cpu().numpy().copy()
scores_np = scores.cpu().numpy().copy()
keep = []
for i in range(len(boxes_np)):
max_idx = np.argmax(scores_np)
keep.append(max_idx)
# 计算 IoU
xx1 = np.maximum(boxes_np[max_idx, 0], boxes_np[:, 0])
yy1 = np.maximum(boxes_np[max_idx, 1], boxes_np[:, 1])
xx2 = np.minimum(boxes_np[max_idx, 2], boxes_np[:, 2])
yy2 = np.minimum(boxes_np[max_idx, 3], boxes_np[:, 3])
inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1)
area_i = (boxes_np[max_idx, 2] - boxes_np[max_idx, 0]) * \
(boxes_np[max_idx, 3] - boxes_np[max_idx, 1])
area_j = (boxes_np[:, 2] - boxes_np[:, 0]) * (boxes_np[:, 3] - boxes_np[:, 1])
iou = inter / (area_i + area_j - inter + 1e-6)
# 高斯衰减而非直接删除
scores_np = scores_np * np.exp(-(iou ** 2) / sigma)
scores_np[max_idx] = 0 # 已选中的不再参与
keep = [k for k in keep if scores_np[k] > score_threshold or k in keep[:1]]
return keep
# WBF(Weighted Box Fusion):融合多个模型的预测框
# pip install ensemble-boxes
# from ensemble_boxes import weighted_boxes_fusion
# boxes_list, scores_list, labels_list = [[...]], [[...]], [[...]]
# boxes, scores, labels = weighted_boxes_fusion(
# boxes_list, scores_list, labels_list,
# iou_thr=0.55, skip_box_thr=0.01
# )优点
缺点
总结
目标检测的关键不只是选 YOLO 还是 Faster R-CNN,而是先搞清任务目标:检测什么、漏检和误检哪个更不可接受、推理速度要求多少、部署设备是什么。只有把这些前提说清楚,模型与后处理策略才有明确方向。同时要注意数据质量、标注一致性、后处理参数(置信度阈值、NMS IoU 阈值)对最终效果的影响往往比模型结构的选择更大。
关键知识点
- 目标检测本质上是分类 + 边界框回归联合任务。
- 一阶段更快,两阶段通常更稳;要按场景选型。
- NMS 阈值会直接影响误检与漏检平衡。
- mAP 只是整体指标,必须结合业务关注的错误类型看。
项目落地视角
- 工业缺陷检测更关注漏检率,宁可多一些误检再复核。
- 实时视频监控更关注 FPS 和延迟,常优先 YOLO 系列。
- 文档 OCR 前处理常用检测框先定位文本区域。
- 生产部署前要在真实摄像头、真实光照、真实背景上验证。
常见误区
- 只看公开数据集 mAP,不看自己业务数据分布。
- 标注框不一致,却直接怀疑模型结构不行。
- 只调网络,不调置信度阈值和 NMS。
- 把离线图片效果当作视频实时效果,忽略时延约束。
进阶路线
- 学习 FPN、Anchor、Anchor-free、DETR 等检测思路。
- 使用 MMDetection / Detectron2 做更系统训练。
- 深入研究小目标检测、类别不平衡和数据增强策略。
- 将检测与跟踪、分割、OCR 组合成完整视觉流水线。
适用场景
- 工业视觉缺陷检测。
- 安防监控、人数统计、车辆识别。
- 文档版面分析、OCR 前定位。
- 零售货架识别、仓储盘点、机器人视觉。
落地建议
- 先从标注质量和数据集代表性入手,再调模型。
- 指标要同时看 mAP、漏检率、误检率和推理时延。
- 上线前先在真实业务环境做端到端压测。
- 把后处理参数(阈值、NMS)纳入实验记录和版本管理。
排错清单
- 检查标注框是否一致、是否存在漏标和错标。
- 检查小目标、遮挡、模糊图像是否单独评估。
- 检查置信度阈值和 NMS 是否设置过严或过松。
- 检查模型推理瓶颈是在网络本身还是预处理/后处理。
复盘问题
- 当前业务更不能接受漏检还是误检?
- 你的模型问题,是数据问题、后处理问题还是结构问题?
- 如果部署设备从 GPU 换成边缘端,方案是否还能成立?
- 哪类样本最常失败,是否值得单独做数据增强或细分建模?
