CNNを用いた物体検出シリーズの第三弾としてFaster R-CNNを紹介していきます。
- 第一弾: R-CNNを使った物体検出で驚異的な精度向上!その仕組みと応用例をわかりやすく解説
- 第二弾: Fast R-CNNを使った物体検出でより高速かつ高精度なモデルの仕組みと活用方法をわかりやすく解説
Contents
Faster R-CNN
Faster R-CNNは、物体検出において現在、標準的に使用されている深層学習アルゴリズムの一つです。これまで紹介してきたR-CNN、Fast R-CNNをより高速に、高精度にしたモデルです。
以下では、Faster R-CNNの概要とその機能の説明を行います。
Faster R-CNNのアーキテクチャ
Faster R-CNNのアーキテクチャは、以下の3つの要素から構成されています。基本的にはFast R-CNNを元として、領域提案(Region Proposal)の部分をニューラルネットワークで置き換えEnd-to-Endで学習できるようにしています。このEnd-to-Endの物体検出が可能になったことが、Faster R-CNNの最も大きい貢献と言えるでしょう!
- 特徴抽出ネットワーク(Feature Extraction Network):画像から特徴を抽出するためのネットワークです。Faster R-CNNでは、一般的にImageNetで学習された深層学習モデル(VGG16やResNetなど)が使用されます。
- 候補領域抽出ネットワーク(Region Proposal Network, RPN):物体が存在する可能性が高い領域(候補領域)を抽出するためのネットワークです。RPNは、画像の各位置で複数のアンカーボックス(Anchor Box)を設定し、各アンカーボックスが物体である確率を計算します。
- 物体検出ネットワーク(Object Detection Network):候補領域から物体を検出するためのネットワークです。このネットワークは、各候補領域に対して、物体のクラスと位置を推定します。全結合層から構成されています。
特徴抽出
画像から特徴を抽出するために、事前に学習された特徴抽出ネットワークを使用します。この部分はFast R-CNNと同様でVGG16やResNetといった一般的なCNNが使用されています。
Region Proposal Network (RPN) による物体領域の提案
次に、RPNを使用して、物体領域の提案を行います。RPNは、CNNによって生成された特徴マップに対して適用され、各位置で複数の候補領域を提案します。これらの候補領域は、固定されたアスペクト比とスケールで生成され、それぞれの位置に対して、物体が存在する確率を予測するための分類と、候補領域のバウンディングボックスを微調整するための回帰を行います。
ここでの固定されたアスペクト比とスケールの矩形領域のことをアンカーボックスといいます。アンカーとは特徴抽出CNNから取り出された特徴マップの各ピクセルに対して打たれた点です。このアンカーを起点にし、異なる固定された矩形領域に対して、物体か背景かをオブジェクトスコアとして出力し、さらに真のバウンディングボックスとのズレを回帰します。
RoIプーリング
RPNによって生成された候補領域は、各種のサイズやアスペクト比を持ち、それぞれが異なる形状をしています。しかし、CNNは入力として固定されたサイズの画像を想定しているため、候補領域をCNNにそのまま入力することはできません。そこで、RoIプーリングと呼ばれる操作を使用して、候補領域を固定サイズの特徴マップに変換します。これも前回のFast R-CNNと同じですね。
バウンディングボックスの回帰・クラス分類
抽出された候補領域に対して、物体のクラスと位置を推定するために、物体検出ネットワークを使用します。物体検出ネットワークは、各候補領域に対して、物体が存在する確率と、物体が存在する場合の物体の位置(バウンディングボックス)を推定します。また、物体が存在するクラスを推定するために、クラス分類器が使用されます。この部分も基本的にはFast R-CNNと同様です。
Faster R-CNNの実装
シリーズのこれまでと同様にPythonの深層学習フレームワークであるPyTorchを使用してFaster R-CNNを実装していきます。Faster R-CNNは現在では標準的な物体検出モデルですのでPyTorchの一部であるTorchvisonというライブラリに便利なモジュールがたくさんあります。なので、今回は主にTorchvisionを使用して実装を行なっていきます。
データセットとDatasetクラスの準備
これまでと同様にデータセットはPASCAL VOC 2007を使用します。さらにクラスを車だけに絞ります(これもこれまでと同様)。データセットの準備に関してはこれまでと同様ですので割愛します。また、今回も深層学習のフレームワークとしてPyTorchを使用します。ですので、例によってPyTorchのDatasetクラスを準備していきます。今回使用するPyTorchのコンピュータビジョン向けライブラリであるtorchvisionで用意されているFaster R-CNNでは次にような形式でデータを渡してあげる必要があります。
以下が実際に書いたDatasetクラスのコードです。
class CarVOCDataset(torch.utils.data.Dataset):
def __init__(self, root_dir, transforms=None, split="train"):
self.root_dir = root_dir
self.transforms = transforms
self.split = split
self.imgs = list(
sorted(os.listdir(os.path.join(root_dir, split, "JPEGImages")))
)
self.annotations = list(
sorted(os.listdir(os.path.join(root_dir, split, "Annotations")))
)
print(
"len of imgs: ",
len(self.imgs),
"len of annotations: ",
len(self.annotations),
)
def __getitem__(self, idx):
img_path = os.path.join(
self.root_dir,
"train" if self.split == "train" else "val",
"JPEGImages",
self.imgs[idx],
)
img = read_image(img_path)
xml_path = os.path.join(
self.root_dir,
"train" if self.split == "train" else "val",
"Annotations",
self.annotations[idx],
)
bndboxs = parse_xml(xml_path)
num_objs = len(bndboxs)
boxes = bndboxs
labels = torch.ones((num_objs,), dtype=torch.int64)
target = {}
target["boxes"] = boxes
target["labels"] = labels
if self.transforms is not None:
# scale the bounding boxes
h, w = img.shape[-2:]
_h, _w = 224, 224
target["boxes"] = torch.as_tensor(target["boxes"], dtype=torch.float32)
target["boxes"][:, 0] *= _w / w
target["boxes"][:, 1] *= _h / h
target["boxes"][:, 2] *= _w / w
target["boxes"][:, 3] *= _h / h
img = self.transforms(img)
is_crowd = torch.zeros((num_objs,), dtype=torch.int64)
target["iscrowd"] = is_crowd
area = (target["boxes"][:, 3] - target["boxes"][:, 1]) * (
target["boxes"][:, 2] - target["boxes"][:, 0]
)
target["area"] = area
image_id = torch.tensor([idx])
target["image_id"] = image_id
return img, target
def __len__(self):
return len(self.imgs)
学習
以下が学習用のスクリプトです。例によって学習には時間がかかりますので、スクリプトを乗せるのみとなってしまいますが、fine-tuneを実際にやって見たいという方はぜひ動かして見てください!
import os
import xmltodict
import numpy as np
import torch
from torchvision.io.image import read_image
from torchvision.models.detection import (
fasterrcnn_resnet50_fpn_v2,
faster_rcnn,
FasterRCNN_ResNet50_FPN_V2_Weights,
)
import torchvision.transforms as transforms
def parse_xml(xml_path):
"""
アノテーションのバウンディングボックスの座標を返すためにxmlファイルをパースする
"""
with open(xml_path, "rb") as f:
xml_dict = xmltodict.parse(f)
bndboxs = list()
objects = xml_dict["annotation"]["object"]
if isinstance(objects, list):
for obj in objects:
obj_name = obj["name"]
difficult = int(obj["difficult"])
if "car".__eq__(obj_name) and difficult != 1:
bndbox = obj["bndbox"]
bndboxs.append(
(
int(bndbox["xmin"]),
int(bndbox["ymin"]),
int(bndbox["xmax"]),
int(bndbox["ymax"]),
)
)
elif isinstance(objects, dict):
obj_name = objects["name"]
difficult = int(objects["difficult"])
if "car".__eq__(obj_name) and difficult != 1:
bndbox = objects["bndbox"]
bndboxs.append(
(
int(bndbox["xmin"]),
int(bndbox["ymin"]),
int(bndbox["xmax"]),
int(bndbox["ymax"]),
)
)
else:
pass
return np.array(bndboxs)
# custom dataset class for pascal voc object detection dataset to train on faster rcnn using pytorch pytorch
class CarVOCDataset(torch.utils.data.Dataset):
def __init__(self, root_dir, transforms=None, split="train"):
self.root_dir = root_dir
self.transforms = transforms
self.split = split
self.imgs = list(
sorted(os.listdir(os.path.join(root_dir, split, "JPEGImages")))
)
self.annotations = list(
sorted(os.listdir(os.path.join(root_dir, split, "Annotations")))
)
print(
"len of imgs: ",
len(self.imgs),
"len of annotations: ",
len(self.annotations),
)
def __getitem__(self, idx):
img_path = os.path.join(
self.root_dir,
"train" if self.split == "train" else "val",
"JPEGImages",
self.imgs[idx],
)
img = read_image(img_path)
xml_path = os.path.join(
self.root_dir,
"train" if self.split == "train" else "val",
"Annotations",
self.annotations[idx],
)
bndboxs = parse_xml(xml_path)
num_objs = len(bndboxs)
boxes = bndboxs
labels = torch.ones((num_objs,), dtype=torch.int64)
target = {}
target["boxes"] = boxes
target["labels"] = labels
if self.transforms is not None:
# scale the bounding boxes
h, w = img.shape[-2:]
_h, _w = 224, 224
target["boxes"] = torch.as_tensor(target["boxes"], dtype=torch.float32)
target["boxes"][:, 0] *= _w / w
target["boxes"][:, 1] *= _h / h
target["boxes"][:, 2] *= _w / w
target["boxes"][:, 3] *= _h / h
img = self.transforms(img)
is_crowd = torch.zeros((num_objs,), dtype=torch.int64)
target["iscrowd"] = is_crowd
area = (target["boxes"][:, 3] - target["boxes"][:, 1]) * (
target["boxes"][:, 2] - target["boxes"][:, 0]
)
target["area"] = area
image_id = torch.tensor([idx])
target["image_id"] = image_id
return img, target
def __len__(self):
return len(self.imgs)
if __name__ == "__main__":
# set the device to train on
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# load the dataset
transform = transforms.Compose(
[
transforms.ToPILImage(),
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
]
)
root_dir = "./data/voc_car"
train_dir = os.path.join(root_dir, "train")
train_dataset = CarVOCDataset(root_dir, transform, split="train")
dataloader = torch.utils.data.DataLoader(
train_dataset,
batch_size=4,
shuffle=True,
num_workers=4,
collate_fn=lambda x: tuple(zip(*x)),
)
# create the model
weights = FasterRCNN_ResNet50_FPN_V2_Weights.DEFAULT
model = fasterrcnn_resnet50_fpn_v2(weights=weights, box_score_thresh=0.3)
# modify the model to fit the number of classes, the number of classes is 2, background and car
in_features = model.roi_heads.box_predictor.cls_score.in_features
num_classes = 2
model.roi_heads.box_predictor = faster_rcnn.FastRCNNPredictor(
in_features, num_classes
)
model.to(device)
optimizer = torch.optim.SGD(
model.parameters(), lr=0.005, momentum=0.9, weight_decay=0.0005
)
# train the model
epochs = 10
best_loss = 1e10
for i in range(epochs):
print("Epoch {}/{}".format(i, epochs))
print("-" * 10)
model.train()
running_loss = 0.0
running_corrects = 0
for images, targets in dataloader:
input_images =
input_images = torch.stack(input_images, dim=0)
inputs = input_images.to(device)
targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
optimizer.zero_grad()
outputs = model(inputs, targets)
loss = sum(loss for loss in outputs.values())
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
epoch_loss = running_loss / len(dataloader.dataset)
print(f"loss: {epoch_loss:.4f}")
torch.save(model.state_dict(), "model.pth")
推論
次のスクリプトはFaster R-CNNの推論用スクリプトです。これまでのR-CNN、Fast R-CNNよりもさらに高速に、高精度になっているのを感じられると思います。このFaster R-CNNから物体検出におけるEnd-to-Endの時代が始まりました。現在ではTransformerを使用したモデルも登場し、さらに高速に、高精度になってきています。
おわりに
以上でCNNを用いた物体検出シリーズは終了となります。現在では多様化し、発展を続けている物体検出モデルですが、その先駆けとしてのR-CNN、Fast R-CNN、Faster R-CNNを理解することはマストといえます。ぜひ、学習用コードを自分のデータセット用にカスタマイズしたりして実装面からも理解してみてくださいね!
参考文献
- Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks, https://arxiv.org/pdf/1506.01497.pdf
- Fast R-CNN, https://www.cv-foundation.org/openaccess/content_iccv_2015/papers/Girshick_Fast_R-CNN_ICCV_2015_paper.pdf