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

以前の記事にて、PyTorchを利用し、PyTorch内蔵の学習済み`resnet101`を利用した物体検出を行いました。今回はPyTorchとTensorflowでどれくらい違うのだろう...ということを見ておきたく、同じ推論をTensorflow.keras上で試してみました。今回もMacBook Pro上のVisual Studio Codeを使って実装しています。 **今回の実装は、PyTorchと違ってGPUにモデルや入力テンソルを転送する`.to("mps")`が無いため、本手順はMacBook Proで実行していますがUbuntu等の環境と手順やソースコードは同じになっていると思います。** 

![スクリーンショット 2024-11-28 19.21.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/e0500593-6968-d094-ad5a-c81aa76d0a50.png)


#### 参考記事

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

#### 事前準備

事前準備としてMacBook ProにTensorflowの開発環境を作ります。

https://qiita.com/xtrizeShino/items/56e3c1e027107debe331

## 作業ディレクトリと仮想環境を作成する

まず作業を始めるにあたって、作業ディレクトリと仮想環境を作成し、`tensorflow-deps`、`tensorflow-macos`、`tensorflow-metal`をインストールしMacBook ProのGPUをTensorflowから使えるようにします。

```bash
# 作業ディレクトリを作成する
(base) $ pwd
(base) $ mkdir tf_resnet
(base) $ cd tf_resnet 

# Python 3.10の仮想環境を作成し、ログインする
(base) $ conda create --prefix ./env python=3.8
(base) $ conda activate /Users/shino/tf_resnet/env
(env) $

# MacBook ProのGPUを利用できるTenserflowをインストールする
(env) $ conda install -c apple tensorflow-deps
(env) $ python -m pip install tensorflow-macos
(env) $ python -m pip install tensorflow-metal

# 科学技術系のパッケージをインストールする
(env) $ conda install jupyter pandas numpy matplotlib scikit-learn

# 画像を扱うOpenCVのパッケージをインストールする
(env) $ pip install opencv-python
```

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

### Visual Studio Codeを起動して作業フォルダを開く

続いて、[Homebrewなどを利用してインストールしたVisual Studio Code](https://qiita.com/xtrizeShino/items/56e3c1e027107debe331#homebrew%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)を起動し【ファイル】-【フォルダを開く】から、先の手順で作成したフォルダを開きましょう。

```bash
$ brew --version
# Homebrew 4.4.6
$ brew install --cask visual-studio-code
```

![スクリーンショット 2024-11-28 16.22.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/1240d9fa-be41-6465-4e45-4781e9a8e072.png)

開いたのち、ターミナルを開いて`conda activate ./env`を実行し仮想環境に入ります。そして【エクスプローラー(左側のバー)】-【新しいファイル】で`test_detect.py`を作成してください。これが推論を行うメインのソースコードになります。

![スクリーンショット 2024-11-28 16.23.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919880/70ccabeb-7fab-d239-88dd-63989f6ea335.png)

# Tensorflow.keras内蔵のResNetを扱う

#### kerasに内蔵されているモデルリストを参照する

kerasに内蔵されている学習済みのモデルは下記のコマンドにて参照することができます。今回はこの中から`ResNet50`を利用しました。

```bash
$ python
>>> import tensorflow.keras.applications as app
>>> print(dir(app))
# ['ConvNeXtBase', 'ConvNeXtLarge', 'ConvNeXtSmall', 
#  'ConvNeXtTiny', 'ConvNeXtXLarge', 'DenseNet121', 
#  'DenseNet169', 'DenseNet201', 'EfficientNetB0',
#  'EfficientNetB1', 'EfficientNetB2', 'EfficientNetB3', 'EfficientNetB4', 
#  ...
#  'NASNetLarge', 'NASNetMobile', 'ResNet101', 'ResNet101V2', 'ResNet152', 
#  'ResNet152V2', 'ResNet50', 'ResNet50V2', 'VGG16', 'VGG19', 'Xception', 
#  ...
#  'resnet_v2', 'vgg16', 'vgg19', 'xception']
```

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

### 推論処理を実装する

Tensorflow.kerasで学習済みモデルを使うことはとても簡単です。`tensorflow.keras.applications`(上記)に含まれているモデルを選択した後に`ResNet50(weights='imagenet')`のように何のデータセットを利用して学習したweightsを使うかを指定するだけです。そして、入力画像をモデルの入力テンソルに変換する処理は`preprocess_input`として提供されており、モデルから出力されたテンソルを読める状態へと変換する処理は`decode_predictions`として提供されています。 **このあたりはPyTorchと比べると簡単に感じます。**

https://qiita.com/xtrizeShino/items/dd861ec5f312245def32#normalize%E3%81%AB%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%9F%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF

### 撮影処理を実装する

撮影処理では、MacBook Pro内蔵カメラに`cv2.VideoCapture()`メソッドでアクセスし、撮影サイズとバッファ深度を指定します。撮影した画像はBGR形式となっていますので、OpenCVを使ってRGB形式へと変換します。モデルへの入力をバッチサイズ=1、縦横224pxのRGB形式とするため、`np.expand_dims(img_attr1, axis=0)`を行い、Shapeを`([1, 224, 224, 3])`とします。 **PyTorchに含まれているresnetの場合は`(1, 3, 224, 224)`のため、同じResNetでもONNXなどモデルをエクスポートする際には注意が必要です。** 

### 推論を実行し、結果を得る

撮影した画像を`preprocess_input`により入力テンソルへと変換し、`model.predict`を使って推論、出力テンソル`predictions`を取得します。出力テンソルには分類クラスそれぞれの確率が含まれており、。`decode_predictions`を使うことにより、クラスの名前に加えて、指定した数の上位となっている推論結果を得ることができます。 **このあたりはとても便利だと感じました。** 

ResNetの出力テンソルに含まれている「確率」は文字列で格納されているため、floatに変換して、推論結果と結合、不要な列を削除して扱いやすいようにテンソルを作り直します。最後に確率が一定以上の場合に`cv2.putText`メソッドを利用し出力画像に検出できたクラス名を書き込み、画面に表示すれば無事完了となります。

#### コード全文

```python
import tensorflow as tf
import tensorflow.keras as keras
from keras.utils import load_img, img_to_array
import tensorflow.keras.applications as app
from tensorflow.keras.applications.resnet50 import ResNet50, preprocess_input, decode_predictions

# 出力テンソルを扱うためにインポート
import numpy as np
# 画像を扱うライブラリPILをインポート
from PIL import Image
# Webカメラを制御するOpenCVをインポート
import cv2
# 出力するラベル数
OUTPUT_PREDICTION = 1
# 表示する確率の閾値
THRESHOLD = 30
# /dev/video0を指定
DEV_ID = 0
# 撮影するサイズ
WIDTH = 640
HEIGHT = 480

### tensorflow.keras.applicationsに含まれるモデル
# print(dir(app))

# imagenetで学習済みのモデルを取得する
keras.backend.set_learning_phase(0)
tf.keras.backend.set_image_data_format('channels_last')
model = ResNet50(weights='imagenet')

##############
# 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:
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break

            # 画像をBGRからRGBの並びに変換し、中央を切り抜く
            input_img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            img_detect = cv2.resize(input_img, (224, 224))
            ####
            # 入力画像を取得(静止画の場合)　
            #img_detect = load_img('dog.jpg', target_size=(224,224))

            # 入力画像をResNet50の入力テンソルの形に合わせる
            img_attr1 = img_to_array(img_detect)
            # print(img_attr1.shape) # -->> (224, 224, 3)
            img_attr2 = np.expand_dims(img_attr1, axis=0)
            # print(img_attr2.shape) # -->> (1, 224, 224, 3)
            img_attr3 = preprocess_input(img_attr2)
            # print(img_attr3.shape) # -->> (1, 224, 224, 3)

            # 推論を実行
            predictions = model.predict(img_attr3)

            # 推論結果の上位1個のみ取り出す
            decoded = decode_predictions(predictions, top=OUTPUT_PREDICTION)[0]

            # 確率の列を取り出して100をかける
            perc_str = np.array(decoded)[:,2]
            perc1 = ([float(s) for s in perc_str])
            perc1 = np.array(([f * 100 for f in perc1]))
            # 最後の列はいらない
            trimed_decoded = np.delete(decoded, 0, axis=1)
            perc2 = np.expand_dims(perc1, 1)
            # 確率(パーセンテージ)とラベルを結合
            output_array = np.append(trimed_decoded, perc2, axis=1)
            output_array = np.delete(output_array, 1, 1)
            # 確率を出力する
            print(output_array)
            
            # 撮影したフレームを表示、確率が一定以上ならクラス名を出力する
            if (float(output_array[0][1]) > THRESHOLD):
                cv2.putText(frame, output_array[0][0], (0, 50), cv2.FONT_HERSHEY_PLAIN, 3, (255, 255, 255), 2, cv2.LINE_AA)
            cv2.imshow('frame', frame)

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

無事動作しました。まだまだPythonのnumpy周りに慣れていないため、出力テンソルの加工に手間取ってしまいました。このあたりは慣れていかないと...

ありがとうございました！


#### 参考にさせていただきましたサイト

https://qiita.com/SZZZUJg97M/items/f416269a2a9dc114da75

https://axa.biopapyrus.jp/ia/opencv/puttext.html

https://www.learning-nao.com/?p=1173

https://qiita.com/yamadasuzaku/items/a5819f6ab3bce9a84217