✨ 目标检测入门:从零实现一个简易检测框架(附完整代码)

目标检测作为计算机视觉的核心技术,一直是工业界和学术界的研究热点。与单纯的图像分类不同,目标检测不仅需要识别图像中的物体类别,还需要精确定位它们的位置。本文将带你从零开始,用 PyTorch 实现一个简易的目标检测框架,理解其核心原理与实现流程。

🎯 什么是目标检测?

目标检测(Object Detection)的核心目标是:在图像中定位出所有感兴趣的目标,并为每个目标分配对应的类别标签。通俗来说,就是让计算机回答两个问题:“这是什么?”(分类)和 “它在哪里?”(定位)。

从技术发展来看,目标检测经历了三个重要阶段:

  • 传统方法:基于滑动窗口 + 手工特征(如 HOG、SIFT),精度低且速度慢;
  • 深度学习早期:以 R-CNN 为代表的两阶段方法,通过候选区域生成 + 分类回归实现高精度检测;
  • 现代方法:以 YOLO、SSD 为代表的单阶段方法,实现端到端检测,大幅提升速度。

目标检测的应用场景极为广泛:

  • 自动驾驶中的行人、车辆、交通标志检测;
  • 安防监控中的异常行为识别与追踪;
  • 工业质检中的产品缺陷检测;
  • 手机相册中的人物、宠物自动分类。

📦 环境准备

本次实践基于 PyTorch 框架,需要安装以下依赖:

bash

# 核心依赖
pip install torch torchvision  # PyTorch深度学习框架
# 辅助工具
pip install matplotlib  # 可视化
pip install numpy       # 数值计算
pip install tqdm        # 进度条显示

建议使用 Python 3.8 + 版本,确保兼容性。

🛠️ 第一步:数据准备

目标检测的数据集通常包含三部分信息:图像、目标类别标签、目标边界框坐标。我们先通过模拟数据理解数据结构,后续可替换为真实数据集(如 VOC、COCO)。

1. 数据集结构解析

一个典型的目标检测样本包含:

  • 图像(Image):原始像素数据(如 3 通道 RGB 图像);
  • 边界框(Bounding Box):用坐标标记目标位置,通常格式为(xmin, ymin, xmax, ymax)(左上角和右下角坐标);
  • 类别标签(Label):目标所属类别(如 “人”“车”,通常用整数表示)。

2. 实现简易数据集类

我们用torch.utils.data.Dataset创建一个生成假数据的数据集,模拟包含人脸的图像数据:

python

运行

import torch
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

class SimpleFaceDataset(Dataset):
    def __init__(self, num_samples=500, img_size=(224, 224)):
        """
        生成包含简单人脸目标的模拟数据集
        :param num_samples: 样本数量
        :param img_size: 图像尺寸 (height, width)
        """
        self.num_samples = num_samples
        self.img_size = img_size
        self.transform = transforms.ToTensor()  # 将图像转为Tensor并归一化
        
    def __len__(self):
        return self.num_samples

    def __getitem__(self, idx):
        # 1. 生成随机图像(模拟人脸和背景)
        # 创建黑色背景
        img = np.zeros((self.img_size[0], self.img_size[1], 3), dtype=np.float32)
        # 随机生成一个"人脸"区域(用灰色块模拟)
        face_size = np.random.randint(50, 100)  # 人脸尺寸50-100像素
        x1 = np.random.randint(0, self.img_size[1] - face_size)
        y1 = np.random.randint(0, self.img_size[0] - face_size)
        x2 = x1 + face_size
        y2 = y1 + face_size
        img[y1:y2, x1:x2, :] = 0.7  # 灰色块模拟人脸
        
        # 2. 生成边界框(确保在图像范围内)
        bbox = np.array([x1, y1, x2, y2], dtype=np.float32)
        # 可添加轻微噪声,模拟标注误差
        bbox += np.random.normal(0, 3, 4)
        bbox = np.clip(bbox, 0, max(self.img_size))  # 截断到图像范围内
        
        # 3. 生成类别标签(0: 背景, 1: 人脸)
        label = np.random.choice([1], size=1)[0]  # 这里简化为只有人脸类别
        
        # 4. 转为Tensor
        img_tensor = self.transform(img)
        bbox_tensor = torch.from_numpy(bbox)
        label_tensor = torch.tensor(label, dtype=torch.long)
        
        return img_tensor, bbox_tensor, label_tensor

# 测试数据集
if __name__ == "__main__":
    dataset = SimpleFaceDataset(num_samples=10)
    # 取一个样本可视化
    img, bbox, label = dataset[0]
    # 转换为可显示格式(HWC,0-1范围)
    img_np = img.permute(1, 2, 0).numpy()
    
    plt.figure(figsize=(6, 6))
    plt.imshow(img_np)
    # 绘制边界框
    x1, y1, x2, y2 = bbox.numpy()
    plt.gca().add_patch(
        plt.Rectangle(
            (x1, y1), x2 - x1, y2 - y1,
            fill=False, edgecolor='red', linewidth=2
        )
    )
    plt.title(f"Label: {label.item()} (1=face)")
    plt.axis('off')
    plt.show()

3. 数据加载器(DataLoader)

使用DataLoader实现批量加载和打乱数据:

python

运行

# 创建数据集和数据加载器
dataset = SimpleFaceDataset(num_samples=500)
dataloader = DataLoader(
    dataset,
    batch_size=8,  # 每批8个样本
    shuffle=True,  # 打乱数据
    num_workers=0  # 单线程加载(Windows系统建议设为0)
)

代码说明

  • SimpleFaceDataset类通过__len____getitem__方法实现数据集接口,生成包含 “人脸”(灰色块)和边界框的模拟数据;
  • 可视化代码验证了数据格式的正确性,确保边界框能准确包围 “人脸” 区域;
  • DataLoader负责将数据集按批次加载,shuffle=True保证训练时样本顺序随机,提升模型泛化能力。

🏗️ 第二步:搭建目标检测模型

目标检测模型通常包含两个核心分支:分类分支(预测目标类别)和回归分支(预测边界框坐标)。我们实现一个简易的两分支模型:

python

运行

import torch.nn as nn
import torch.nn.functional as F

class SimpleDetector(nn.Module):
    def __init__(self, num_classes=2):
        """
        简易目标检测模型
        :param num_classes: 类别数(含背景)
        """
        super(SimpleDetector, self).__init__()
        # 1. 特征提取网络(卷积层)
        self.features = nn.Sequential(
            # 输入:3x224x224
            nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1),  # 16x112x112
            nn.ReLU(inplace=True),
            nn.BatchNorm2d(16),  # 批量归一化,加速训练
            
            nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1), # 32x56x56
            nn.ReLU(inplace=True),
            nn.BatchNorm2d(32),
            
            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), # 64x28x28
            nn.ReLU(inplace=True),
            nn.BatchNorm2d(64),
            
            nn.AdaptiveAvgPool2d((1, 1))  # 全局平均池化,输出64x1x1
        )
        
        # 2. 分类分支(预测类别)
        self.classifier = nn.Sequential(
            nn.Linear(64, 32),
            nn.ReLU(inplace=True),
            nn.Linear(32, num_classes)  # 输出类别概率(logits)
        )
        
        # 3. 边界框回归分支(预测x1,y1,x2,y2)
        self.bbox_regressor = nn.Sequential(
            nn.Linear(64, 32),
            nn.ReLU(inplace=True),
            nn.Linear(32, 4)  # 输出4个坐标值
        )

    def forward(self, x):
        """
        前向传播
        :param x: 输入图像,shape [batch_size, 3, 224, 224]
        :return: cls_logits (类别logits), bbox_pred (边界框预测)
        """
        # 提取特征
        feat = self.features(x)  # [batch_size, 64, 1, 1]
        feat = feat.flatten(1)   # 展平为 [batch_size, 64]
        
        # 分类预测
        cls_logits = self.classifier(feat)  # [batch_size, num_classes]
        
        # 边界框回归预测
        bbox_pred = self.bbox_regressor(feat)  # [batch_size, 4]
        
        return cls_logits, bbox_pred

# 测试模型
if __name__ == "__main__":
    model = SimpleDetector(num_classes=2)
    # 生成随机输入(batch_size=2)
    dummy_input = torch.randn(2, 3, 224, 224)
    # 前向传播
    cls_logits, bbox_pred = model(dummy_input)
    print(f"分类输出 shape: {cls_logits.shape}")  # 应为 [2, 2]
    print(f"边界框输出 shape: {bbox_pred.shape}")  # 应为 [2, 4]

模型解析

  • 特征提取网络:由 3 个卷积层组成,通过stride=2实现下采样,最终将 224x224 的图像压缩为 1x1 的特征图,提取全局特征;
  • 分类分支:通过全连接层输出类别 logits(未经过 softmax),用于后续计算分类损失;
  • 回归分支:输出 4 个值(x1, y1, x2, y2),直接预测边界框坐标;
  • 测试代码验证了模型输入输出的形状正确性,确保前向传播无错误。

这个模型虽然简单,但包含了目标检测的核心思想:共享特征提取,并行预测类别和边界框

🔄 第三步:模型训练

训练目标检测模型需要同时优化分类损失和边界框回归损失,我们使用以下策略:

1. 定义损失函数与优化器

python

运行

import torch.optim as optim

# 初始化模型、损失函数和优化器
model = SimpleDetector(num_classes=2)

# 优化器:Adam优化器,学习率0.001
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 分类损失:交叉熵损失(适用于多类别分类)
cls_criterion = nn.CrossEntropyLoss()

# 边界框回归损失:L1损失(平均绝对误差)
bbox_criterion = nn.L1Loss()

损失函数选择理由

  • 交叉熵损失(CrossEntropyLoss):自动结合 softmax 和 NLLLoss,适合类别预测,计算真实类别与预测概率的差距;
  • L1 损失(L1Loss):计算预测边界框与真实边界框的绝对误差,对异常值更稳健,适合边界框回归。

2. 训练循环实现

python

运行

import tqdm

# 训练参数
num_epochs = 20  # 训练轮数

# 记录训练损失
train_losses = []

# 切换模型为训练模式
model.train()

# 训练循环
for epoch in range(num_epochs):
    running_loss = 0.0
    # 使用tqdm显示进度条
    for images, bboxes, labels in tqdm.tqdm(dataloader, desc=f"Epoch {epoch+1}/{num_epochs}"):
        # 清零梯度
        optimizer.zero_grad()
        
        # 前向传播:获取预测结果
        cls_logits, bbox_preds = model(images)
        
        # 计算分类损失
        cls_loss = cls_criterion(cls_logits, labels)
        
        # 计算边界框回归损失
        bbox_loss = bbox_criterion(bbox_preds, bboxes)
        
        # 总损失:分类损失 + 回归损失(可根据任务调整权重)
        total_loss = cls_loss + bbox_loss
        
        # 反向传播:计算梯度
        total_loss.backward()
        
        # 优化器更新参数
        optimizer.step()
        
        # 累计损失
        running_loss += total_loss.item() * images.size(0)  # 乘以batch_size
    
    # 计算本轮平均损失
    epoch_loss = running_loss / len(dataset)
    train_losses.append(epoch_loss)
    
    # 打印训练信息
    print(f"Epoch [{epoch+1}/{num_epochs}], Average Loss: {epoch_loss:.4f}")

# 保存训练好的模型
torch.save(model.state_dict(), "simple_detector.pth")
print("模型已保存为 simple_detector.pth")

# 绘制损失曲线
plt.figure(figsize=(10, 5))
plt.plot(range(1, num_epochs+1), train_losses, marker='o', color='b')
plt.title('Training Loss Curve')
plt.xlabel('Epoch')
plt.ylabel('Average Loss')
plt.grid(True)
plt.show()

训练过程解析

  • 训练模式:通过model.train()启用 Dropout 和 BatchNorm 的训练模式;
  • 梯度清零optimizer.zero_grad()确保每次迭代的梯度不累积;
  • 损失加权:总损失为分类损失与回归损失的和,实际应用中可通过权重(如total_loss = 1.0*cls_loss + 0.5*bbox_loss)调整两者重要性;
  • 进度监控:使用tqdm显示每轮训练进度,记录并绘制损失曲线,直观观察模型收敛情况。

正常情况下,随着训练进行,损失会逐渐下降并趋于稳定,表明模型在学习如何预测类别和边界框。

📊 第四步:模型预测与结果可视化

训练完成后,我们用模型对新数据进行预测,并可视化结果:

python

运行

# 加载训练好的模型
model = SimpleDetector(num_classes=2)
model.load_state_dict(torch.load("simple_detector.pth"))
model.eval()  # 切换为评估模式

# 从数据加载器取一批测试数据
images, true_bboxes, true_labels = next(iter(dataloader))

# 关闭梯度计算(加速推理)
with torch.no_grad():
    cls_logits, bbox_preds = model(images)
    # 分类概率(通过softmax转换)
    cls_probs = F.softmax(cls_logits, dim=1)
    # 预测类别(取概率最大的类别)
    pred_labels = torch.argmax(cls_probs, dim=1)

# 可视化前4个样本的预测结果
plt.figure(figsize=(16, 12))
for i in range(4):
    # 原始图像(转换为HWC格式)
    img = images[i].permute(1, 2, 0).numpy()
    
    # 预测边界框
    pred_bbox = bbox_preds[i].numpy()
    x1, y1, x2, y2 = pred_bbox
    
    # 真实边界框
    true_bbox = true_bboxes[i].numpy()
    tx1, ty1, tx2, ty2 = true_bbox
    
    # 预测类别和概率
    pred_cls = pred_labels[i].item()
    pred_prob = cls_probs[i, pred_cls].item()
    
    # 绘制图像
    plt.subplot(2, 2, i+1)
    plt.imshow(img)
    # 绘制预测边界框(红色)
    plt.gca().add_patch(
        plt.Rectangle(
            (x1, y1), x2 - x1, y2 - y1,
            fill=False, edgecolor='red', linewidth=2,
            label=f"Pred: cls={pred_cls}, prob={pred_prob:.2f}"
        )
    )
    # 绘制真实边界框(绿色)
    plt.gca().add_patch(
        plt.Rectangle(
            (tx1, ty1), tx2 - tx1, ty2 - ty1,
            fill=False, edgecolor='green', linewidth=2,
            label=f"True: cls={true_labels[i].item()}"
        )
    )
    plt.title(f"Sample {i+1}")
    plt.legend()
    plt.axis('off')

plt.tight_layout()
plt.show()

可视化解析

  • 评估模式model.eval()关闭 Dropout,固定 BatchNorm 参数,确保预测结果稳定;
  • 推理加速with torch.no_grad()禁用梯度计算,减少内存占用,加快推理速度;
  • 结果对比:通过红色(预测)和绿色(真实)边界框的对比,直观评估模型性能。理想情况下,红色框应与绿色框高度重合,且预测类别正确。

📌 小结与核心思路

本文实现的简易目标检测框架虽然简化了真实场景的复杂性,但包含了目标检测的核心要素:

  1. 数据表示:图像、边界框、类别标签的三元组结构;
  2. 模型设计:共享特征提取 + 双分支预测(分类 + 回归);
  3. 损失函数:联合优化分类损失和边界框回归损失;
  4. 训练与推理:标准的深度学习训练流程,推理时需转换为评估模式。

这个框架的局限性也很明显:

  • 仅能处理单目标(每张图像一个目标);
  • 模型结构简单,特征提取能力有限;
  • 未考虑目标尺度变化和复杂背景干扰。

🚀 延伸学习方向

要构建工业级目标检测系统,可从以下方向深入:

  1. 复杂模型设计

    • 使用更深的骨干网络(如 ResNet、EfficientNet)提升特征提取能力;
    • 引入多尺度特征融合(如 FPN),处理不同大小的目标;
    • 采用锚框(Anchor Box)机制,支持多目标检测(如 YOLO、Faster R-CNN)。
  2. 损失函数优化

    • 分类损失:使用 Focal Loss 解决类别不平衡问题;
    • 回归损失:使用 IoU 损失(如 GIoU、DIoU)直接优化边界框重合度。
  3. 工程实践技巧

    • 数据增强:通过旋转、缩放、裁剪等扩充数据集,提升模型鲁棒性;
    • 学习率调度:使用余弦退火、ReduceLROnPlateau 等策略动态调整学习率;
    • 模型部署:通过 ONNX、TensorRT 等工具优化模型,提升推理速度。
  4. 经典框架学习

    • 单阶段方法:YOLOv5/YOLOv8(速度快,适合实时场景);
    • 两阶段方法:Faster R-CNN(精度高,适合对精度要求高的场景)。

📚 参考资源

通过本文的实践,你已掌握目标检测的基本流程和核心思想。在此基础上,结合经典论文和开源工具,可逐步深入复杂目标检测系统的设计与实现。祝你在计算机视觉的道路上不断进步!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐