【小ネタ】Unreal Engine 4.22.3 + UnrealCVでsegmentation maskデータを取る方法。【備忘録】

2020.10.14

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

せーのでございます。

秋に近づいてきますと、誰しも「ああ、segmentationのマスクデータが欲しいな。」、と感じるかと思います。
今日はそんなマスクデータをUE4からUnrealCVで取得しよう、というお話です。

経緯

UnrealCVというのはUnreal Engineのプラグインで、コンピュータビジョンのためのデータの取得や設定をコマンドからUEの世界に向けて入れられるようにするものです。

Unreal Engine 4で作ったバーチャル空間上で3Dモデルがぬるぬるアニメーションしているところを複数視点でcubemosの骨格検知にかけてみた

こんな感じで機械学習の推論などを仮想世界であるUnreal Engine上に展開できたりします。

逆にUnrealCVを使ってUnreal Engine(以下UE)上に配置されているモノや人などの各種データを取得することで、機械学習に使う教師データなどを簡単に作れます。

例えばobject_maskというコマンドを使うと、UEに置いてあるモノや人をマスクしたデータを取得できます。

こんなシーンから

こういう感じですね。

ただこのUnrealCV、リリースノートを見るとUE4.16までにしか対応していません。現在UEは4.25.3が最新です。最新とは言わないまでも、なるべく新しいバージョンに対応してほしい、というのが人情です。

そこで現在の開発を見ると、一番進んでいる開発ブランチは4.22であることがわかりました。

さっそくプラグインをビルドしてUE4.22.3に入れてみると、、、何も映らない

問い合わせてみると、少し工夫が必要だそうなので、備忘録としてブログを書いておきます。

やり方

プラグインをビルド

まずはgithubから4.22ブランチのソースをチェックアウト(もしくはクローン)し、build用のスクリプトを叩きます。

F:\unrealcv>python build.py
Found UE4 in the following path, please make a selection:
1 : C:\Program Files\Epic Games\UE_4.20
2 : C:\Program Files\Epic Games\UE_4.22
2
Running AutomationTool...
Parsing command line: BuildPlugin -plugin=F:\unrealcv\UnrealCV.uplugin -package=F:\unrealcv\Plugins\UnrealCV -rocket -targetplatforms=Win64
Copying 233 file(s) using max 64 thread(s)
Reading plugin from F:\unrealcv\Plugins\UnrealCV\HostProject\Plugins\UnrealCV\UnrealCV.uplugin...
Building plugin for host platforms: Win64
......
......
......
    [63/73] SensorBPLib.gen.cpp
    [64/73] PlayerViewMode.gen.cpp
    [65/73] PawnCamSensor.gen.cpp
    [66/73] SerializeBPLib.gen.cpp
    [67/73] TcpServer.gen.cpp
    [68/73] UnrealCV.init.gen.cpp
    [69/73] ExecStatus.cpp
    [70/73] UnrealcvGameMode.gen.cpp
    [71/73] StereoCameraActor.gen.cpp
    [72/73] VisionBPLib.gen.cpp
    [73/73] ActorController.cpp
  Total time in Parallel executor: 148.19 seconds
  Total execution time: 157.15 seconds
Took 157.235092s to run UnrealBuildTool.exe, ExitCode=0
Reading filter rules from F:\unrealcv\Plugins\UnrealCV\HostProject\Plugins\UnrealCV\Config\FilterPlugin.ini
BUILD SUCCESSFUL
AutomationTool exiting with ExitCode=0 (Success)

複数のバージョンが入っている場合はどのバージョン用のプラグインを作るか聞いてくる場合もありますので、その時は4.22のバージョンを選んでください。5分ほど待つとビルドが完了します。

出来上がると、ソースをダウンロードした場所に「Plugins」というフォルダができています。

F:\unrealcv>dir
 F:\unrealcv のディレクトリ

2020/10/14  13:08    <DIR>          .
2020/10/14  13:08    <DIR>          ..
2020/10/13  01:46    <DIR>          .github
2020/10/14  13:07               691 .gitignore
2020/10/13  01:46             2,038 build.py
2020/10/13  01:46    <DIR>          client
2020/10/13  01:46    <DIR>          Config
2020/10/14  13:07    <DIR>          Content
2020/10/14  13:07    <DIR>          docs
2020/10/14  13:07             1,847 dodo.py
2020/10/13  01:46    <DIR>          examples
2020/10/13  01:46             1,089 LICENSE
2020/10/14  13:08    <DIR>          Plugins
2020/10/13  01:46             1,991 README.md
2020/10/13  01:46    <DIR>          Resources
2020/10/13  01:46    <DIR>          Source
2020/10/13  01:46    <DIR>          test
2020/10/13  01:46               238 tox.ini
2020/10/14  13:07               716 UnrealCV.uplugin
               7 個のファイル               8,610 バイト
              12 個のディレクトリ  13,029,199,872 バイトの空き領域

プロジェクトにデプロイ

できた「Plugins」フォルダをUnrealCVが使いたいプロジェクトにまるっとコピーします。

プロジェクトを開いている場合は一旦閉じて、開きなおします。
[edit]=>[plugins]でプラグインパネルを開き、UnrealCVが入っていればOKです。

カメラを追加

デフォルトのカメラではデータが取れないので、「Fusion Camera Actor」というカメラを追加します。
画面左にある「Modes」というパネルからFusion Camera Actorというアクターを検索し、レベルにドラッグアンドドロップします。

追加したカメラの位置を調整します。私の場合はど真ん中に置いてみました。

デフォルトカメラを変更(やらなくてもよい)

このままPlayしてコマンドを叩いても動くのですが、画面上ではデフォルトカメラの映像が写っていますので、画角を確認するために追加したカメラからの映像を画面に映します。
画面上部の「Blueprints」から「Open level Blueprints」をクリックして、Blueprintsのパネルを開きます。

中央にある「Event Graph」エリアで右クリックして、Playした最初を表す「Event BeginPlay」、プレイヤー(つまり私)の入力を表す「Get Player Controller」をそれぞれ選択します。右クリックで出てくるアクションはものすごく多いのですが、出てくるパネルの上部に検索窓がついているので、それぞれ途中まで検索すると簡単に入れられます。

そして最後に「World Outliner」から先ほど入れた「FusionCameraActor1」(デフォルトではそういう名前になってます)をEvent Graphにドラッグアンドドロップします。World Outlinerは今ポップアップで出てくる前の画面、右側にあります。

それぞれパネルで管理されていてポップアップで新しいウィンドウがでてきたり、それをドラッグして元のウィンドウに結合させたりできるのはAdobe系のソフトのUIに似てますね。

さて、3つのノードがそろったら、これらをつなげていきます。まず、「Get Player Controller」の「Return Value」をマウスでつかんでEvent Graphの領域にドラッグアンドドロップすると、コントローラの入力値をどう返すかを選ぶポップアップが出てきますので、ここから「Set View Target with Blend」を選びます。

次にPlayを押した最初にこのBlueprintが動いてほしいので「Event BeginPlay」下の矢印を「Set View Target with Blend」まで伸ばします。

そして最後に「FusionCamerActor1」を「Set View Target with Blend」の「New View Target」までドラッグしてつなげます。

できたら画面中央上のコントロールパネルから「Compile」をクリックして完成です。出来たらこの画面は閉じても構いません。

play、コマンド入力

さて、いよいよコマンド入力です。先ほどの画面から中央上の「Play」ボタンをクリックすると、新しく追加したカメラからの画面が映ります。

この画面(ビューポート、と言います)をクリックすると、コントロールがUE内の世界に入ります。その状態で「`」(バッククォート)キーを押すとUnrealCVのコマンドが入力できます。一回押すと画面下部にコマンドラインが表示され、二回押すと画面上にコンソールとして表示されます。コンソールの場合、戻り値も表示されます。

試しに現在のステータスの状態を見てみます。ステータスを見るコマンドは

vget /unrealcv/status

です。

コマンドが通っていることを確認出来たら、マスクデータを取得していきましょう。
単に今見ている画面を変えたい場合は[vset /viewmode <変えたいモード>]、データとして取得したい場合は[vget /camera//<変えたいモード> <保存したいパス>]で取得できます。カメラのIDは現在0か1の2つが選べるのですが、ID:0(デフォルトのカメラ)だとobject_maskがうまく取れませんでした。今回のようにカメラを追加することでID:1のカメラでobject_maskを取ることができます。

モードには

  • lit: 通常のRGB画面
  • depth: 深度データ(カメラからの距離を表す)
  • normal: 法線マップ(カメラのある面に対しての法線ベクトルを色で表す。凹凸を表現するのに使う)
  • object_mask: マスクデータ。今回の目的。

なんかがあります。

vset /viewmode depth

vset /viewmode normal

vget /camera/1/object_mask F:\objectmask.png(保存先のパス。任意)

うん、いい感じですね。

人だけ取ってマスクデータ化する

UEでは自分で好きなものやキャラクターを配置して動かしていますので、当然オブジェクトごとの識別もできます。それはつまり、必要なデータだけをマスクして取ってくる、ということも可能なわけです。機械学習の教師データを作るときには大事な要素ですね。

UnrealCVのチュートリアルを元に、Jupyter notebookで人だけをマスクしたデータを取得してみます。前提としてunrealcvモジュールをインストールしておきます。

pip install unrealcv

まず必要なライブラリと関数を定義しておきます。

%matplotlib inline
import time; print(time.strftime("The last update of this file: %Y-%m-%d %H:%M:%S", time.gmtime()))

from __future__ import division, absolute_import, print_function
import os, sys, time, re, json
import numpy as np
import matplotlib.pyplot as plt

imread = plt.imread
def imread8(im_file):
    ''' Read image as a 8-bit numpy array '''
    im = np.asarray(Image.open(im_file))
    return im

def read_png(res):
    import io
    from io import StringIO
    import PIL.Image
    img = PIL.Image.open(io.BytesIO(res))
    return np.asarray(img)

def read_npy(res):
    import io
    from io import StringIO
    return np.load(io.BytesIO(res))

次に動作中のUEに接続します。

from unrealcv import client
client.connect()
if not client.isconnected():
    print('UnrealCV server is not running. Run the game downloaded from http://unrealcv.github.io first.')
    sys.exit(-1)
INFO:__init__:192:Got connection confirm: b'connected to MyProject4'

疎通確認のためステータス確認のコマンドを投げます。

res = client.request('vget /unrealcv/status')
# The image resolution and port is configured in the config file.
print(res)
Is Listening
Client Connected
9000
Configuration
Config file: C:/Program Files/Epic Games/UE_4.22/Engine/Binaries/Win64/unrealcv.ini
Port: 9000
Width: 640
Height: 480
FOV: 90.000000
EnableInput: true
EnableRightEye: false

人のオブジェクトのマスクした際の色を取得します。人のIDはUE4上の「World Outliner」で人のオブジェクトにマウスオーバーしても確認できますし

UnrealCVでは

vget /objects

でも一覧を取得できます。

UnrealCVにて

vget /object/<オブジェクトのID>/color

と入力するとマスクした色を取得できます。上の画像でわかるように今回人をマスクした時の色はピンクですので結果は

(R=255,G=127,B=255,A=255)

と返ってきます。
これを使って、該当する色以外はつぶしてしまうマスク関数を作って、必要なオブジェクトだけ残します。

scene_objects = client.request('vget /objects').split(' ')
print('Number of objects in this scene:', len(scene_objects))

# TODO: replace this with a better implementation
class Color(object):
    ''' A utility class to parse color value '''
    regexp = re.compile('\(R=(.*),G=(.*),B=(.*),A=(.*)\)')
    def __init__(self, color_str):
        self.color_str = color_str
        #print(color_str)
        match = self.regexp.match(color_str)
        (self.R, self.G, self.B, self.A) = [int(match.group(i)) for i in range(1,5)]

    def __repr__(self):
        return self.color_str

id2color = {} # Map from object id to the labeling color
for obj_id in scene_objects:
    print("id: " + obj_id)
    obj = client.request('vget /object/%s/color' % obj_id)
    print(obj)
    
    if obj == "error Can not find object":
        scene_objects.remove(obj_id)
        print("remove id: " + obj_id)
        continue

    color = Color(obj)
    id2color[obj_id] = color
    print('%s : %s' % (obj_id, str(color)))

scene_objects.pop()
print (scene_objects)
def match_color(object_mask, target_color, tolerance=3):
    match_region = np.ones(object_mask.shape[0:2], dtype=bool)
    for c in range(3): # r,g,b
        min_val = target_color - tolerance
        max_val = target_color + tolerance
        channel_region = (object_mask[:,:,c] >= min_val) & (object_mask[:,:,c] <= max_val)
        match_region &= channel_region

    if match_region.sum() != 0:
        return match_region
    else:
        return None

id2mask = {}
for obj_id in scene_objects:
    color = id2color[obj_id]
    mask = match_color(object_mask, [color.R, color.G, color.B], tolerance = 3)
    if mask is not None:
        id2mask[obj_id] = mask
mask = id2mask['Petting_Animal_2']
plt.figure(); plt.imshow(mask)

キャラクターが女の子だけに若干怖くなってしまいましたが気にしません。

データを見てみる

ちなみにこのマスクをデータの形でprintすると

[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
 ...
 ...
 ...
  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
  False False False False False False False False  True  True  True  True
   True  True  True  True  True  True  True  True  True  True  True  True
   True  True  True  True False False False  True  True  True  True 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
...
...
...

とこんな感じでマスクしてあるところだけTrueになった全ピクセル分のデータになっています。
これと元画像("vget /camera/1/lit png" でとれます)をセットにしてぐるぐる回してあげれば、学習データがいい感じで取れそうですね。

まとめ

ということでUE4.22.3を使ったobject maskの取得方法を書きました。
そのうちUnrealCVも開発が進んでデフォルトの状態でもスパっととれるようになると思いますが、それまではこういう方法を使っていきましょう。