シェルスクリプトマガジン

Pythonあれこれ(Vol.92掲載)

著者:飯尾 淳

本連載では「Pythonを昔から使っているものの、それほど使いこなしてはいない」という筆者が、いろいろな日常業務をPythonで処理することで、立派な「蛇使い」に育つことを目指します。その過程を温かく見守ってください。皆さんと共に勉強していきましょう。第22回では、機械学習ライブラリ「PyTorch」を使って、画像内の、歩行者が写っている領域を自動認識するPythonプログラムを作成します。

シェルスクリプトマガジン Vol.92は以下のリンク先でご購入できます。

図3 47番のデータを確認するPythonコード

import matplotlib.pyplot as plt
from torchvision.io import read_image

image =read_image(
    "data/PennFudanPed/PNGImages/FudanPed00047.png")
mask = read_image(
    "data/PennFudanPed/PedMasks/FudanPed00047_mask.png")
plt.figure(figsize=(16, 8))
plt.subplot(121)
plt.title("Image")
plt.imshow(image.permute(1, 2, 0))
plt.subplot(122)
plt.title("Mask")
plt.imshow(mask.permute(1, 2, 0))

図5 PennFudanDataSetクラスを定義するPythonコード

import os
import torch
from torchvision.io import read_image
from torchvision.ops.boxes import masks_to_boxes
from torchvision import tv_tensors
from torchvision.transforms.v2 import functional as F

class PennFudanDataset(torch.utils.data.Dataset):
  def __init__(self, root, transforms):
    self.root = root
    self.transforms = transforms
    # イメージデータとマスクデータをロードし、ソートして並べておく
    self.imgs = \
      list(sorted(os.listdir(os.path.join(root, "PNGImages"))))
    self.masks = \
      list(sorted(os.listdir(os.path.join(root, "PedMasks"))))

  def __getitem__(self, idx):
    # イメージとマスクのロード
    img_path = \
      os.path.join(self.root, "PNGImages", self.imgs[idx])
    mask_path = \
      os.path.join(self.root, "PedMasks", self.masks[idx])
    img = read_image(img_path)
    mask = read_image(mask_path)
    # インスタンスは色別にエンコードされていて……
    obj_ids = torch.unique(mask)
    # 最初のIDは背景色なので削除
    obj_ids = obj_ids[1:]
    num_objs = len(obj_ids)
    # 色別にエンコードされているマスクを2値マスクに分ける
    masks = \
      (mask == obj_ids[:, None, None]).to(dtype=torch.uint8)
    # マスクデータに対してバウンディングボックスを求める
    boxes = masks_to_boxes(masks)
    labels = torch.ones((num_objs,), dtype=torch.int64)
    image_id = idx
    area = (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0])
    # 群衆でないと仮定
    iscrowd = torch.zeros((num_objs,), dtype=torch.int64)
    # tv_tensorsイメージに変換する
    img = tv_tensors.Image(img)
    target = {}
    target["boxes"] = \
      tv_tensors.BoundingBoxes(boxes,
        format="XYXY", canvas_size=F.get_size(img))
    target["masks"] = tv_tensors.Mask(masks)
    target["labels"] = labels
    target["image_id"] = image_id
    target["area"] = area
    target["iscrowd"] = iscrowd

    if self.transforms is not None:
      img, target = self.transforms(img, target)

    return img, target

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

図6 get_model_instance_segmentation()関数を定義するPythonコード

import torchvision
from torchvision.models.detection.faster_rcnn \
  import FastRCNNPredictor
from torchvision.models.detection.mask_rcnn \
  import MaskRCNNPredictor

def get_model_instance_segmentation(num_classes):
  # COCOで事前学習済みのモデルデータをロードする
  model = torchvision.models.detection.maskrcnn_resnet50_fpn(
            weights="DEFAULT")
  # 入力特徴量
  in_features = \
    model.roi_heads.box_predictor.cls_score.in_features
  # num_classesで指定するクラスの分類器をセット(今回は2クラス)
  model.roi_heads.box_predictor = \
    FastRCNNPredictor(in_features, num_classes)
  # マスク判別器も同様に設定
  in_features_mask = \
    model.roi_heads.mask_predictor.conv5_mask.in_channels
  hidden_layer = 256
  model.roi_heads.mask_predictor = MaskRCNNPredictor(
    in_features_mask,
    hidden_layer,
    num_classes
  )
  return model

図7 get_transform()関数を定義するPythonコード

from torchvision.transforms import v2 as T

def get_transform(train):
  transforms = []
  if train:
    transforms.append(T.RandomHorizontalFlip(0.5))
  transforms.append(T.ToDtype(torch.float, scale=True))
  transforms.append(T.ToPureTensor())
  return T.Compose(transforms)

図8 追加学習の準備をするためのPythonコード

import utils
from engine import train_one_epoch, evaluate

device = torch.device('cuda') \
  if torch.cuda.is_available() else torch.device('cpu')
# 背景と人物の2クラス判別器を作成する
num_classes = 2
# データセットはすでにロード済みのものを用いる
dataset = \
  PennFudanDataset('data/PennFudanPed',
                   get_transform(train=True))
dataset_test = \
  PennFudanDataset('data/PennFudanPed',
                   get_transform(train=False))
# データセットを学習用とテスト用に分ける
indices = torch.randperm(len(dataset)).tolist()
dataset = torch.utils.data.Subset(dataset, indices[:-50])
dataset_test = torch.utils.data.Subset(
                 dataset_test, indices[-50:])
# 学習と評価用のデータローダを定義
data_loader = torch.utils.data.DataLoader(
  dataset,
  batch_size=2,
  shuffle=True,
  num_workers=4,
  collate_fn=utils.collate_fn
)
data_loader_test = torch.utils.data.DataLoader(
  dataset_test,
  batch_size=1,
  shuffle=False,
  num_workers=4,
  collate_fn=utils.collate_fn
)
# 先ほど定義したヘルパーファンクションを用いてモデルを用意
model = get_model_instance_segmentation(num_classes)
# モデルをデバイスに結び付ける
model.to(device)
# 最適化器を作成
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(
  params,
  lr=0.005,
  momentum=0.9,
  weight_decay=0.0005
)
# 学習レートのスケジューラを設定
lr_scheduler = torch.optim.lr_scheduler.StepLR(
  optimizer,
  step_size=3,
  gamma=0.1
)

図9 物体認識と領域セグメンテーションを実施するPythonコード

import matplotlib.pyplot as plt
from torchvision.utils \
  import draw_bounding_boxes, draw_segmentation_masks

image = \
  read_image("data/PennFudanPed/PNGImages/FudanPed00047.png")
eval_transform = get_transform(train=False)
model.eval()
with torch.no_grad():
  x = eval_transform(image)
  # RGBA -> RGBにコンバートしてデバイスにひも付ける
  x = x[:3, ...].to(device)
  predictions = model([x, ])
  pred = predictions[0]
image = (255.0 * (image - image.min()) / 
        (image.max() - image.min())).to(torch.uint8)
image = image[:3, ...]
pred_labels = [f"pedestrian: {score:.3f}" for label, \
               score in zip(pred["labels"], pred["scores"])]
pred_boxes = pred["boxes"].long()
output_image = draw_bounding_boxes(image, 
                 pred_boxes, pred_labels, colors="red")
masks = (pred["masks"] > 0.7).squeeze(1)
output_image = draw_segmentation_masks(output_image, 
                 masks, alpha=0.5, colors="blue")
plt.figure(figsize=(12, 12))
plt.imshow(output_image.permute(1, 2, 0))

図11 Penn-Fudanデータセットを用いて追加学習するPythonコード

# 2エポックだけ学習させてみる
num_epochs = 2
for epoch in range(num_epochs):
  # 10回ごとに表示させながら1エポックの学習を実行
  train_one_epoch(model, optimizer, data_loader, 
                  device, epoch, print_freq=10)
  # 学習レートをアップデート
  lr_scheduler.step()
  # テストデータで評価
  evaluate(model, data_loader_test, device=device)