PyTorch内蔵の学習済みResNetとMacBook Pro搭載カメラで物体検出の推論をする

前回はM4 Pro MacBook Pro上にPyTorchの実行環境を構築しました。

https://qiita.com/xtrizeShino/items/abe5b9caaadecbd1efd6

今回は、PyTorchの`torchvision`に含まれている学習済みの`resnet101`を使ってWebカメラで撮影した物体を検出する推論を実装してみました。`torchvision`は、人気の高いデータセット、モデルアーキテクチャ、コンピュータビジョンでよく利用される画像の加工機能を含むライブラリです。`resnet101`は物体検出を行うネットワークで、101層のニューラルネットワーク層から構成されています。

![スクリーンショット 2024-11-16 7.49.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/3fb5327f-2b76-2fec-8c5b-68fc9be9afe1.png)

## 実行環境

ハードウェアリソースとしてMacBook Proを使用しました。 **WebカメラはMacBook Pro内蔵カメラを利用しています。** ソフトウェアの実装にはVisual Studio Codeを利用しました。Visual Studio Codeには、下記の拡張機能を入れています。

- Japanese Language Pack
- Python
- Python Debugger
- Pylance

## Pythonの仮想環境を作成し、必要なコンポーネントをインストールする

作業ディレクトリを作成し、`conda`コマンドでPythonの仮想環境を作成します。作成したPython仮想環境に入り、`PyTorch`に加えて、Webカメラを制御するOpenCVと連携するための`opencv-python`をインストールします。

```bash
(base) $ mkdir pytorchbox
(base) $ cd pytorchbox
(base) $ conda create --prefix ./env python=3.8
(base) $ conda activate ./env
(env) $ conda install pytorch::pytorch torchvision torchaudio -c pytorch
(env) $ pip install opencv-python
```

#### torchvisionに含まれているモデル

`torchvision`に含まれているモデルは、以下の手順で確認できます。すべて学習済みのモデルなので、学習用のデータセットを集めることなく、すぐに物体検知のプログラムを組むことができます。勿論、学習させることもできますので、特に、ネットワークが既に決まっている方、はじめて推論プログラムを書く方にオススメです。

```python
(env) $ python
>>> from torchvision import models
>>> print(dir(models))
['AlexNet', 'AlexNet_Weights', 'ConvNeXt', ...
'ResNet', 'ResNet101_Weights', 'ResNet152_Weights', ...
'resnet', 'resnet101', 'resnet152', ...
'wide_resnet101_2', 'wide_resnet50_2']
```

#### 学習済みResNetの分類クラス一覧をダウンロードする

`torchvision`に含まれているResNetはデータセット`imagenet`を使って学習されています。作業ディレクトリにimagenetのクラス一覧をダウンロードしておきましょう。

```bash
# 特にenv以下で作業する必要はないですが...
(env) $ wget https://raw.githubusercontent.com/pytorch/hub/refs/heads/master/imagenet_classes.txt
(env) $ ls
env   imagenet_classes.txt
```

### Visual Studio Codeで作業ディレクトリを開く

Visual Studio Codeで作業ディレクトリを開き、ターミナルでPythonの仮想環境を有効化します。また、 Pythonファイルを作成します。今回は`testtorch.py`という名前のファイルを作成しました。

```bash
# Visual Studio Codeのターミナルも仮想環境を有効化する
(base) $ conda activate ./env
(env) $
```

![スクリーンショット 2024-11-16 5.51.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/3e05dfcd-ac24-ed78-40c0-226b86b1adfd.png)

## OpenCVでWebカメラの画像を採取し、推論する

推論モデルと撮影処理を別々に準備し、最後に結合します。

### 推論側の実装

まず推論側では、推論に使う`resnet101`を`torchvision`から読み込み、MacBookのアクセラレータ（MPS:Metal Perfomance Shaders）に転送します。また、入力画像をresnetの入力テンソルに変換するための`preprocess`も定義します。この定義には`torchvision`の提供する`transforms`を利用しました。`transforms.Compose`を利用することにより処理をまとめることが可能で、前処理を並べておくと、入力画像に対して順番に適用するフィルタとなってくれます。本例では、5段階の前処理を適用しています。

- OpenCVのRGB形式を画像を扱うPIL対応の形式に変換
- 画像のリサイズ(256 x 256px に縮小)
- 画像のクロップ(中央の224 x 224pxを抽出)
- テンソルに変換
- RGBそれぞれの値をNormalize

#### Normalizeに指定したパラメータ

ここでのポイントは、画像分類データセットを構成している画像に含まれる**RGBの平均と標準偏差** が **mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]** なので、この値を使ってNormalizeしている点です。ですので、異なる学習データセットに変更してresnetを利用する場合は、そのデータセットの平均値と標準偏差にあわせる必要があります。

### 撮影側の実装

OpenCV(撮影)側では、MacBook Pro内蔵カメラに`cv2.VideoCapture()`メソッドでアクセスし、撮影サイズとバッファ深度を指定します。OpenCVで撮影した画像はBGR形式となっていますので、PILライブラリの扱いやすいRGB形式へと変換し、推論モデルの入力へと変換するため前手順で実装した`preprocess`に投入します。shapeを確認し、入力バッチとなる`torch.Size([1, 3, 224, 224])`になっていればOKです。最後に`.to("mps")`メソッドを利用して入力バッチをアクセラレータへ転送します。

### 撮影結果をresnetで分類する

最後に、撮影側の実装で作成した`batch_t`を`resnet`へ入力し、出力テンソル`out`を取得します。出力テンソルには分類クラスそれぞれの確率が含まれています。`max`メソッドを使って並び替えることにより先頭の要素のインデックスを得ることができますので`labels`を参照することで、カメラが何を撮影したのか認識することができます。先頭の確率が低い場合、特に何も撮影できていない可能性がありますので、今回は`if main_percentage > 50:`として、50%以上の場合のみラベルを出力するものとしました。

## ソースコード全体

以上の内容を実装した結果が以下となります。

```python
# 実行のベースとなるtorchをインポート
import torch
# torchvisionからモデルと画像変換機能をインポート
from torchvision import models
from torchvision import transforms
# 画像を扱うライブラリPILをインポート
from PIL import Image
# Webカメラを制御するOpenCVをインポート
import cv2

# /dev/video0を指定
DEV_ID = 0
# 撮影するサイズ
WIDTH = 640
HEIGHT = 480

##############
# resnetの準備
##############
# モデルに含まれているresnetを、学習済みの状態で取得
resnet = models.resnet101(pretrained=True)
# resnetのネットワーク構造を表示
print(resnet)
# resnetを推論モードに切り替え
resnet.eval()
# resnetのモデルをMPS(アクセラレータ)へ転送
resnet.to("mps")
# resnetの分類(クラス)をlabelsに読み込む
with open('./imagenet_classes.txt') as f:
    labels = [line.strip() for line in f.readlines()] 

# OpenCVで撮影した画像をresnetの入力テンソルに変換する
preprocess = transforms.Compose([
    transforms.ToPILImage(), # numpy.ndarray -> pillow
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225])
])

##############
# Webカメラに接続する
##############
cap = cv2.VideoCapture(DEV_ID)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

##############
# OpenCVで撮影し、resnetテンソルに変換
##############
while(True):
    try:
        # 1フレーム撮影する
        ret, frame = cap.read()
        if ret:
            # 撮影したフレームを表示
            cv2.imshow('frame', frame)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
            # 画像をBGRからRGBの並びに変換
            input_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            # 画像をresnetの入力テンソルに変換する
            input_tensor = preprocess(input_image)
            # 画像をバッチに内包する(1次元追加)
            batch_t = torch.unsqueeze(input_tensor, dim=0)
            #print(input_tensor.shape) # -->> torch.Size([3, 224, 224])
            #print(batch_t.shape) # -->> torch.Size([1, 3, 224, 224])
            # 入力テンソルをアクセラレータに転送
            batch_t = batch_t.to("mps")

            ##############
            # 入力テンソルをresnetで推論、結果を表示する
            ##############
            # 入力テンソルに変換したカメラの画像をresnetで推論
            out = resnet(batch_t)
            # 最大確率のインデックスを取得
            _, index = torch.max(out, 1)
            # 確率をパーセンテージ表記に変更
            percentage = torch.nn.functional.softmax(out, dim=1)[0] * 100
            main_percentage = percentage[index[0]].item()
            main_label = labels[index[0]]
            # 確率50%以上であれば結果を出力
            if main_percentage > 50:
                print('%s(%s)' % (main_label, "{:.1f}".format(main_percentage, 1)))

    except KeyboardInterrupt:
        print('abort')
```

もちろん、別の学習済みモデルも使えますので`resnet`を初期化している箇所を以下のように書き換えるだけで`resnet101`の推論器から`resnext50_32x4d`の推論器に切り替え、推論時間を短縮することが可能です。しかも`.to("mps")`でアクセラレータも使えてしまうとは...PyTorchの学習済みモデル、本当に便利だなぁ。

```python
# モデルに含まれているresnetを、学習済みの状態で取得
#resnet = models.resnet101(pretrained=True)
resnet = models.resnext50_32x4d(pretrained=True)
```

以上、ありがとうございました。