文章目录

  • 前言
  • 数据准备
  • 模型定义
  • 训练模型
  • 预测物体



前言

目标检测是计算机视觉中的重要任务,它可以用于物体追踪、自动驾驶、智能安防等领域。在实际应用中,我们常常需要针对不同的场景和数据集设计不同的目标检测算法,因此一个灵活、可扩展的目标检测框架是非常有用的。

本文将介绍一个目标检测实战框架,该框架基于 Python 和 PyTorch 开发,支持常见的目标检测算法(如 Faster R-CNN、SSD、YOLOv5 等)、多任务学习和分布式训练。下面通过一个例子来演示如何使用该框架进行目标检测。

数据准备

首先,我们需要准备数据集。本例使用的是 COCO 数据集,可以在官网上下载。我们只需要下载训练集和验证集,把它们都放在一个文件夹中,如下所示:

data/
├── train2017/
├── val2017/
└── annotations/
    ├── instances_train2017.json
    └── instances_val2017.json

其中,train2017/ 和 val2017/ 分别存放训练集和验证集的图片,annotations/ 存放 COCO 数据集的标注文件(以 JSON 格式保存)。

接下来,我们需要使用 COCO API 来读取数据和标注。COCO API 是官方提供的一组 Python 工具,方便读取、处理和可视化 COCO 数据集。可以在 GitHub 上下载并安装。

!git clone https://github.com/cocodataset/cocoapi.git
!cd cocoapi/PythonAPI && python setup.py install --user

然后,我们可以编写数据加载器 coco.py,它会调用 COCO API 来读取数据和标注,并将它们转换为 PyTorch 的张量格式。具体代码如下:

import os
import torch
import torchvision.transforms as T
from pycocotools.coco import COCO
from torch.utils.data import DataLoader, Dataset


class CocoDataset(Dataset):
    def __init__(self, root, year, mode, transforms=None):
        super().__init__()
        ann_dir = os.path.join(root, "annotations")
        img_dir = os.path.join(root, f"{mode}{year}")
        ann_file = os.path.join(ann_dir, f"instances_{mode}{year}.json")
        self.coco = COCO(ann_file)
        self.image_ids = sorted(self.coco.getImgIds())
        self.img_dir = img_dir
        self.transforms = transforms

    def __getitem__(self, index):
        image_id = self.image_ids[index]
        image_info = self.coco.loadImgs(image_id)[0]
        image_path = os.path.join(self.img_dir, image_info["file_name"])
        image = torch.load(image_path)

        ann_ids = self.coco.getAnnIds(imgIds=image_id)
        annotations = self.coco.loadAnns(ann_ids)
        boxes = []
        labels = []
        for ann in annotations:
            box = ann["bbox"]
            label = ann["category_id"]
            boxes.append(box)
            labels.append(label)

        boxes = torch.FloatTensor(boxes)
        labels = torch.LongTensor(labels)
        target = {"boxes": boxes, "labels": labels}

        if self.transforms:
            image, target = self.transforms(image, target)

        return image, target

    def __len__(self):
        return len(self.image_ids)

在这个数据加载器中,我们使用了 PyTorch 的 Transforms 来对图片和标注进行预处理。具体地,我们使用了以下 Transforms:

class Resize(object):
    def __init__(self, size):
        self.size = size

    def __call__(self, image, target=None):
        w, h = image.size
        image = image.resize(self.size)
        if target is not None:
            boxes = target["boxes"].clone()
            boxes[:, [0, 2]] = boxes[:, [0, 2]] * self.size[0] / w
            boxes[:, [1, 3]] = boxes[:, [1, 3]] * self.size[1] / h
            target["boxes"] = boxes
        return image, target


class ToTensor(object):
    def __call__(self, image, target=None):
        image = T.ToTensor()(image)
        if target is not None:
            boxes = target["boxes"]
            labels = target["labels"]
            boxes = torch.cat([boxes[:, :2], boxes[:, :2] + boxes[:, 2:]], dim=-1)  # [x1, y1, x2, y2]
            boxes = torch.FloatTensor(boxes)
            labels = torch.LongTensor(labels)
            target = {"boxes": boxes, "labels": labels}
        return image, target

其中,Resize 把图片缩放到指定大小,并相应地调整标注的坐标;ToTensor 把 PIL.Image 对象转换为 PyTorch 的张量,并把标注转换为 PyTorch 的张量格式,方便后续的训练。

模型定义

接下来,我们需要定义模型。本例使用的是 Faster R-CNN 算法,它由一个预测器(用于检测物体的位置和类别)和一个区域生成器(用于生成候选框)组成。我们使用了官方提供的 Faster R-CNN 模型,并对其进行微调。具体代码如下:

import torchvision
from torch import nn


class FasterRCNN(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.backbone = torchvision.models.vgg16(pretrained=True).features
        feature_map_size = 512
        self.roi_pool = nn.AdaptiveMaxPool2d((7, 7))
        self.fc1 = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5)
        )
        self.fc2 = nn.Sequential(
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5)
        )
        self.cls_score = nn.Linear(4096, num_classes + 1)
        self.bbox_pred = nn.Linear(4096, 4 * (num_classes + 1))

    def forward(self, x, proposals=None):
        x = self.backbone(x)
        x = self.roi_pool(x)
        x = x.view(x.size(0), -1)
        x = self.fc1(x)
        x = self.fc2(x)
        cls_scores = self.cls_score(x)
        bbox_deltas = self.bbox_pred(x)

        if proposals is not None:
            cls_scores = cls_scores[proposals.batch_indices]  # [N, num_classes + 1]
            bbox_deltas = bbox_deltas[proposals.batch_indices]  # [N, 4 * (num_classes + 1)]
            proposals_boxes = proposals.boxes  # [N, 4]

            # convert bounding box coordinates from [x, y, w, h] to [x1, y1, x2, y2]
            num_classes = cls_scores.size(-1) - 1
            bbox_deltas = bbox_deltas.view(-1, num_classes, 4)
            xmin, ymin, xmax, ymax = proposals_boxes.unbind(1)
            xc, yc, w, h = bbox_deltas.unbind(2)
            x1 = xc * w + xmin
            y1 = yc * h + ymin
            x2 = (xc + torch.exp(w)) * xmax
            y2 = (yc + torch.exp(h)) * ymax
            proposals_boxes = torch.stack([x1, y1, x2, y2], dim=-1)

            return proposals_boxes, cls_scores

        return cls_scores, bbox_deltas

在这个模型中,我们首先使用了 VGG16 作为 backbone,并提取了其最后一个 feature map。然后,我们使用一个自适应池化层将 feature map 缩放到一定大小(这里设为 7x7),并使用两个全连接层对其进行预测。最后,我们分别预测每个物体的分类和边界框,在训练时使用这些预测与标注进行损失函数的计算。在预测时,我们需要使用区域生成器生成若干个候选框,并输入到预测器中得到最终的预测结果。

训练模型

有了数据加载器和模型,我们就可以开始训练了。这里我们使用了 PyTorch Lightning 框架来简化训练过程。PyTorch Lightning 是一个高效、简洁、可扩展的 PyTorch 框架,提供了很多实用的功能(如自动化训练、分布式训练、日志记录等),方便用户快速搭建和训练模型。

我们可以定义一个 LightningModule 来封装数据加载器、模型和优化器,并在其中重载 training_step 和 validation_step 函数来定义训练和验证过程。具体代码如下:

import pytorch_lightning as pl

class DetectionModule(pl.LightningModule):
    def __init__(self, num_classes, lr=0.001, batch_size=32):
        super().__init__()
        self.num_classes = num_classes
        self.lr = lr
        self.batch_size = batch_size

        # define model
        self.model = FasterRCNN(num_classes)

        # define optimizer
        self.optimizer = torch.optim.Adam(self.model.parameters(), lr=lr)

    def forward(self, x, proposals=None):
        return self.model(x, proposals)

    def configure_optimizers(self):
        return self.optimizer

    def training_step(self, batch, batch_idx):
        images, targets = batch
        proposals = generate_proposals(images, self.model)
        outputs = self.model(images, proposals)
        loss = compute_loss(outputs, targets)
        self.log('train_loss', loss)
        return loss

    def validation_step(self, batch, batch_idx):
        images, targets = batch
        proposals = generate_proposals(images, self.model)
        outputs = self.model(images, proposals)
        loss = compute_loss(outputs, targets)
        self.log('val_loss', loss)

在这个 LightningModule 中,我们首先定义了训练信息(如类别数、学习率和批大小)。然后,我们在 init 中定义了模型和优化器,并在 configure_optimizers 中返回优化器。最后,我们在 training_step 和 validation_step 中对一个批次的数据进行训练和验证,分别计算损失函数并把结果记录到日志中。

接下来,我们可以创建一个 Trainer,并使用下面的命令来开始训练:

from pytorch_lightning import Trainer
from pytorch_lightning.callbacks import ModelCheckpoint

# create data loaders
train_loader = DataLoader(
    CocoDataset("./data", "2017", "train", transforms=transforms),
    batch_size=batch_size, shuffle=True, num_workers=4)
val_loader = DataLoader(
    CocoDataset("./data", "2017", "val", transforms=transforms),
    batch_size=batch_size, shuffle=False, num_workers=4)

# create model and trainer
detector = DetectionModule(num_classes=num_classes, lr=lr, batch_size=batch_size)
checkpoint_callback = ModelCheckpoint(monitor='val_loss')
trainer = Trainer(gpus=1, max_epochs=max_epochs, checkpoint_callback=checkpoint_callback)
trainer.fit(detector, train_loader, val_loader)

其中,我们使用了 DataLoader 加载数据,并传递给 Trainer 进行训练。我们还使用了 ModelCheckpoint 回调来保存每个 epoch 中的最佳模型。最后,我们调用 fit 函数训练模型。在训练过程中,PyTorch Lightning 会自动优化模型、记录训练信息和计算验证指标。如果需要,我们还可以使用 HorovodTrainer 来进行分布式训练。

预测物体

训练完成后,我们可以使用训练好的模型来预测物体。具体地,我们可以读取一张图片并输入到模型中,得到每个物体的位置和类别。具体代码如下:

import torchvision.transforms.functional as F

def predict_image(model, image_path):
    image = Image.open(image_path).convert("RGB")
    image_tensor = F.to_tensor(F.resize(image, (800, 800)))
    proposals = generate_proposals(image_tensor.unsqueeze(0), model)
    boxes, scores = model(image_tensor.unsqueeze(0), proposals)
    boxes, labels, scores = filter_predictions(boxes, scores, 0.5)
    draw_boxes(image, boxes, labels)
    plt.imshow(image)
    plt.show()

predict_image(detector, "test.jpg")

在这个预测函数中,我们首先读取一张图片,并将其转换为 PyTorch 的张量格式。然后,我们输入该张量到模型中,得到每个物体的位置和类别。最后,我们使用 filter_predictions 和 draw_boxes 函数来筛选出置信度高的物体,并在图片上绘制它们的位置框和类别标签。最后,我们就可以看到模型预测的结果了。