文章目录
- 前言
- 数据准备
- 模型定义
- 训练模型
- 预测物体
前言
目标检测是计算机视觉中的重要任务,它可以用于物体追踪、自动驾驶、智能安防等领域。在实际应用中,我们常常需要针对不同的场景和数据集设计不同的目标检测算法,因此一个灵活、可扩展的目标检测框架是非常有用的。
本文将介绍一个目标检测实战框架,该框架基于 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 函数来筛选出置信度高的物体,并在图片上绘制它们的位置框和类别标签。最后,我们就可以看到模型预测的结果了。