じゅころぐAR

AR/VRメインのブログ。時々ドローン。

Mask R-CNNを試してみた

AR x AIで使えそうなMask R-CNNというOSSを教えてもらったので動かしてみました。

github.com

Mask R-CNNでできること

Githubリポジトリに載っている画像がわかりやすいです。

https://github.com/matterport/Mask_RCNN/raw/master/assets/street.png

画像の中の物体(人とか車とか)を検知(Object Detection)して、検知した物体を背景と分割(Object Segmentation)する形でマスクしてくれます。

これとよく似た画像って、どこかで見たことがあるような?

japanese.engadget.com

Niantic Real World Platform」の記事で、現実と溶け込んだARの要素技術として物体検知が紹介されていました。
リアルタイムに物体検知を行いつつ、物体との距離も推定してマスクを行うことができれば、リアルなオクルージョンが実現できそうです。
AR以外の用途だと、写真投稿サイトで写り込んだ人物をぼかしたり、自動運転やナビゲーションで人を避けたり、とか。

環境構築

GithubのREADMEをざっくり読むと、Jupyter Notebookを使ってステップごとの結果が可視化できるらしい。

jupyter.org

また、物体検知用に提供されているMS COCOというデータセットを使うらしい。MSはMicrosoftの略。

COCO - Common Objects in Context

github.com

Jupyter Notebookのインストール

$ pip install jupyter

それでは早速入れてみ・・・エラーがいっぱい出ました!
メッセージを眺めるとライブラリのバージョンがどうのこうの言っているので、Pythonのバージョンを確認したところ2系でした。pyenv使っててglobalは古いままだった・・・。

$ python --version
Python 2.7.10

Mask_R_CNNはPython3で動作と明記されているので、3系に切り替えます。

pyenv versionsでインストール済みのバージョンを確認。3.6.6があったので、localのバージョンを切り替えて再度実行。

$ pyenv local 3.6.6
$ pip install jupyter

今度はインストールできたので、以下のコマンドで実行。

$ jupyter notebook

f:id:jyuko49:20181110224038p:plain

何か表示されました。

で、ここからどうすれば?

必要ライブラリのインストール

Getting Startedを見ると、とりあえずdemo.ipynbを動かしてみるのがよいらしい。
拡張子が.ipynbになっているファイルが、Jupyter Notebookの実行ファイルのようです。JSONPythonのコードを突っ込んだようなフォーマットになってます。

Jupyter Notebookを起動して"samples > demo.ipynb"をクリックすると、以下のような画面が表示されました。

f:id:jyuko49:20181111000428p:plain

Runボタンをクリックしていくと次の処理に進みますが、途中でエラーメッセージが出ます。

f:id:jyuko49:20181110234358p:plain

ModuleNotFoundError: No module named 'numpy'

どうやらライブラリが足りない様子です。

これらのインストール方法はREADMEの下の方に書いてありました(もうちょっと上の方に書いておいて欲しさ)

$ pip install -r requirements.txt
$ python setup.py install

requirements.txtにパッケージ名が列記されていて、上記コマンドを実行すれば必要なライブラリがインストールされます。

COCO APIのインストール

前述の手順でインストールしただけだと、以下のエラーが解消されません。

ModuleNotFoundError: No module named 'pycocotools'

上記はCOCO APIのインストールで入るようです。

github.com

Githubからcloneして、とりあえずプロジェクトフォルダ直下に配置。Python APIをインストールします。

$ git clone https://github.com/cocodataset/cocoapi.git
$ cd coco/PythonAPI
$ python setup.py install

これで環境は構築できたはず。
Runコマンドをポチポチしていくと、最終的に以下のような画像が表示されます。(画像は毎回変わる)

f:id:jyuko49:20181111000636p:plain

コードを読んでみる

サンプルスクリプトが何をやっているのか、ざっと見てみてみます。

In[1]

最初のブロックでは、ライブラリのインポートと定数を定義しているだけ。

import os
import sys
import random
import math
import numpy as np
import skimage.io
import matplotlib
import matplotlib.pyplot as plt

# Root directory of the project
ROOT_DIR = os.path.abspath("../")

# Import Mask RCNN
sys.path.append(ROOT_DIR)  # To find local version of the library
from mrcnn import utils
import mrcnn.model as modellib
from mrcnn import visualize
# Import COCO config
sys.path.append(os.path.join(ROOT_DIR, "samples/coco/"))  # To find local version
import coco

%matplotlib inline 

# Directory to save logs and trained model
MODEL_DIR = os.path.join(ROOT_DIR, "logs")

# Local path to trained weights file
COCO_MODEL_PATH = os.path.join(ROOT_DIR, "mask_rcnn_coco.h5")
# Download COCO trained weights from Releases if needed
if not os.path.exists(COCO_MODEL_PATH):
    utils.download_trained_weights(COCO_MODEL_PATH)

# Directory of images to run detection on
IMAGE_DIR = os.path.join(ROOT_DIR, "images")

mask_rcnn_coco.h5が学習済みモデルで、ローカルになければダウンロードしてくるようになっている。
IMAGE_DIRが物体検知を行う画像フォルダのパス。

In[2]: Configurations

CocoConfigクラスを継承して、InferenceConfigクラスで一部プロパティを上書きしている。

class InferenceConfig(coco.CocoConfig):
    # Set batch size to 1 since we'll be running inference on
    # one image at a time. Batch size = GPU_COUNT * IMAGES_PER_GPU
    GPU_COUNT = 1
    IMAGES_PER_GPU = 1

config = InferenceConfig()
config.display()

In[3]: Create Model and Load Trained Weights

モデルの生成部分です。先程作成したconfigを渡してます。

# Create model object in inference mode.
model = modellib.MaskRCNN(mode="inference", model_dir=MODEL_DIR, config=config)

# Load weights trained on MS-COCO
model.load_weights(COCO_MODEL_PATH, by_name=True)

コードの最初の方に、

import mrcnn.model as modellib

とあるので、'mrcnn/model.py'が本体のようです。

In[4]: Class Names

検知した物体のクラス名の定義。ラベルを付けるときに使う。

# COCO Class names
# Index of the class in the list is its ID. For example, to get ID of
# the teddy bear class, use: class_names.index('teddy bear')
class_names = ['BG', 'person', 'bicycle', 'car', 'motorcycle', 'airplane',
               'bus', 'train', 'truck', 'boat', 'traffic light',
               'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird',
               'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear',
               'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie',
               'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
               'kite', 'baseball bat', 'baseball glove', 'skateboard',
               'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup',
               'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
               'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
               'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed',
               'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote',
               'keyboard', 'cell phone', 'microwave', 'oven', 'toaster',
               'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors',
               'teddy bear', 'hair drier', 'toothbrush']

データセットが参照できれば以下のコードで置き換えられるらしい(試してないです)

# Load COCO dataset
dataset = coco.CocoDataset()
dataset.load_coco(COCO_DIR, "train")
dataset.prepare()

# Print class names
print(dataset.class_names)

In[5]: Run Object Detection

ここがメインの処理ですね。

# Load a random image from the images folder
file_names = next(os.walk(IMAGE_DIR))[2]
image = skimage.io.imread(os.path.join(IMAGE_DIR, random.choice(file_names)))

# Run detection
results = model.detect([image], verbose=1)

# Visualize results
r = results[0]
visualize.display_instances(image, r['rois'], r['masks'], r['class_ids'], 
                            class_names, r['scores'])

画像データをファイルから取得してmodel.detect()を行うと、resultsに結果のデータが入ってくるので、それをvisualize.display_instances()という処理に渡しています。
visualizeはコードの最初の方でimportしてあるライブラリで、mrcnn/visualize.pyに実際の処理が書いてあります。

from mrcnn import visualize

visualize.py内の処理はコード書きながら理解することにします。

使ってみる

サンプルと同じことをやるだけですが、自分でコードを書いてみます。

.ipynb形式はコーディングしづらいので、test.pyを作成して、In[1]〜[5]のコードをコピペしていきます。
ファイルができたら、とりあえず以下の3ヶ所だけ変更しておきます。

#%matplotlib inline  #よくわかんないけどエラーになるのでコメントアウト
ROOT_DIR = os.path.abspath("../")  #実行ファイルからルートへの相対パスに直す
# 画像を加工&表示する部分は自作するのでコメントアウト
#result = visualize.display_instances(image, r['rois'], r['masks'], r['class_ids'],
#                        class_names, r['scores'])

コマンドラインで実行します。

$ python test.py

バックエンドがMacOSだとなんちゃらみたいなmatplotlibのエラーが出た場合、以下の記事で解決しました。

qiita.com

結果の画像を表示する

skimage.ioとmatplotlib.pyplotがインポートされているので、これを使います。

result_image = image.copy()

skimage.io.imshow(result_image)
plt.show()

こんな感じで表示されるはず。

f:id:jyuko49:20181112142300p:plain

ウィンドウを閉じると、コマンドラインで実行したプログラムも終了します。

検知結果の中身を確認する

resultsに何が入っているか見たいので、printしてみます。

print(r['rois'])
print(r['masks'])
print(r['class_ids'])
print(r['scores'])

rois

[[ 38 265 493 533]
 [163 482 229 530]
 [250  31 587 782]]

検出した物体を囲う矩形(Bounding box)の頂点座標が入っています。配列の要素数が検知した物体の数になります。

masks

[[[False False False]
  [False False False]
  [False False False]
  ...
  [False False False]
  [False False False]
  [False False False]]

 [[False False False]
  [False False False]
  [False False False]
  ...

(長いので省略)

  [False False False]
  [False False False]
  [False False False]]]

rois内でマスクをかける範囲が配列で入っている。詳しいところはよくわかってない。

class_ids

[ 1 47 61]

検知した物体のクラスに対応するIDが入っています。
class_namesを使うことでラベル文字列を取得できます。

for id in r['class_ids']:
    print(class_names[id])
person
banana
dining table

scores

[0.99927574 0.9297102  0.91985023]

検知の精度がスコアとして入っています。数値が1に近いほど、検出の正確性が高いはず。

コードを書く

resultsの中身がわかったので、resultsを使って結果を描画していきます。

前提として、

for roi in r['rois']:
    # 矩形の描画

for mask in r['masks']:
    # マスクの描画

のようなコーディングだと、ループがたくさんになるので、

N = r['rois'].shape[0]  #配列の大きさ(要素数)
for i in range(N):
    # r['rois'][i]を描画
    # r['masks'][i]を描画

とした方がループが1回で速い(たぶん)

矩形の描画

visualize.draw_box(image, box, color)でimageにboxを重ねることができます。

result_image = visualize.draw_box(result_image, r['rois'][i], rgb)

もしくはOpenCVを使って、

import cv2

result_image = cv2.rectangle(result_image, 
    (r['rois'][i][1],  r['rois'][i][0]), ( r['rois'][i][3],  r['rois'][i][2]), rgb)

みたいな実装でもできます。

ラベルとスコアの描画

OpenCVを使いました。位置は対応する矩形(r['rois'][i])の左上の座標を使っています。

font = cv2.FONT_HERSHEY_SIMPLEX
text = class_names[r['class_ids'][i]] + ':' + str(r['scores'][i])
result_image = cv2.putText(result_image, text,
    (r['rois'][i][1],r['rois'][i][0]), font, 0.8, rgb, 2, cv2.LINE_AA)

マスクの描画

こはちょっと難しいので、visualize.apply_mask(image, mask, color)を使いました。

mask = r['masks'][:, :, i]
result_image = visualize.apply_mask(result_image, mask, color)

実行結果

f:id:jyuko49:20181112153856p:plain f:id:jyuko49:20181112153914p:plain f:id:jyuko49:20181112154021p:plain f:id:jyuko49:20181112154027p:plain

精巧なフィギュアやイラストは、人として認識されるんですね。

他にも、忍ちゃんの髪がバナナと判別されたり、パドックのOの字がフリスビーと判別されたりしていますが、ARで使う場合は、物体が何であるかの検知よりも物体がそこにあることを検知してくれた方が都合良さそうです。
検知(判別はともかく)の精度という意味では、パドックの写真で後ろ向きの人物をすべて検出していてすごい。

  • マスクあり版
  • マスクなし版

マスクなし版はGoogleVision APIをローカルで実行している感じですね。

複数のオブジェクトを検出する  |  Cloud Vision API ドキュメント  |  Google Cloud

完成版のコード

Gistにアップしました。
Mask_RCNN直下のファイルにコピペして、test_imageフォルダを作って画像を置けば動くはず。

今後

大体の動作はわかったので、AR開発で使ってみたいです。
というか、これからのARはAIありきになっていきそう。

リアルタイムの動画でテストする

各フレームに対して同様の処理を行えばよいので、実装自体は難しくないと思う。
フレームレートがどれくらい落ちるかの方が懸念。

Unityで使えるか調べる

ARアプリで使うには、学習済みモデルをUnityで動かしたい。
mask_rcnn_coco.h5はKeras用のファイルのようなので、TensorFlowで使えるようProtocol Bufferで保存して、TensorFlowSharpでごにょごにょする感じでしょうか。

オクルージョンに使う

マスクしている箇所をオクルージョンするシェーダを書けばいいはず。
書ければ。

他の学習モデルも試す

物体検知のモデルとしては、R-CNN、YOLO、SSD(Solid State Driveではない)が有名らしい。
以下の記事を見ると、R-CNNよりもYOLOの方がFPSが出てるので、同様のマスク処理ができるなら両方試した方がいいかもしれない。

qiita.com

追加学習させる

今回は学習済みモデルをそのまま使いましたが、自分で学習データを追加して用途に応じてチューニングできた方が便利です。が、教師データの準備とか大変そうなので優先度は低め。

3D対応を試す

READMEにて、3Dに対応したデータセットとしてMatterport3Dが紹介されています。

github.com

お問い合わせしないとダウンロードできないので試せませんでしたが、3Dでの物体検知は興味があります。

また、別のモデルで単眼カメラの画像からデプスを推定するvid2depthというのもあったので、これと物体検知を組み合わせて使うのも良さそう。

models/research/vid2depth at master · tensorflow/models · GitHub

↓続きです
jyuko49.hatenablog.com