VoTTを使って超お手軽に画像のタグ付け~ChainerCVのデータセットへの変換
はじめに
今回は画像向けの機械学習を用いるときに大変なタグ付けを楽にすることができるツールを紹介します。
また、最終的にComputer Vision向けに特化したChainerCVのR-CNN系で使用できるデータセット形式に変換していきます。
枚数が多く前処理アプリなどを独自で作ることも検討している方は必見です。
VoTTとは?
Microsoftが主導で進めているタグ付け(Tagging)のためのツールで、Visual Object Tagging Toolの略です。
GUIで簡単にタグ付けができるため、非常に便利なツールです。
Electronで作られているため、Windows, Mac, Linux問わずクロスプラットフォームで動く優れものです。
また、Github(こちら)上でコードが公開されているため、独自に改良することも可能です。
画像向けと動画向けのタグ付けができ、動画ではトラッキングの機能も備えています。
百聞は一見にしかず、さっそく使い方を見ていきましょう。
VoTTのインストール
VoTTはこちらからインストールを行うことができます。
ビルド済みの release packageで配布されているため、こちらをダウンロードしましょう。
Windows版もMac版もあるため、OSに合わせてものをダウンロードしましょう。
※ 最新版のv1.0.9は動作が安定していないため、v1.0.8をお勧めします。
ダウンロード後は解凍して、デスクトップなどわかりやすい場所に移しておきましょう。
VoTT.exe
のような実行ファイルを選択し、VoTTを起動しましょう。
手動でタグ付けを行おう
ツールを使って便利になるとはいえ、人手で主導のタグ付けが必要になります。
今回は犬と猫を分けるようなタグ付けを行っていくこととします。
まずは、画像のタグ付けを選択しましょう。
タグ付けをしたい画像の入っているフォルダ選択の画面になるため、フォルダを選択しましょう。
タグ付けのジョブ設定ですが、Region Typeは特別なことがない限りは長方形のRectangle
としておきましょう。
Labelsのところは、タグ付けを行いたいラベルの名前をカンマで区切って入力しましょう。今回は、cat
とdog
としました。
ジョブ設定が終わると、画像が表示されるため、ドラッグドロップであとはとにかく下記の2ステップに基づいて、ラベル付けを行っていきます。
- ステップ1:ドラッグドロップで領域を選択
- ステップ2:選択した領域に当てはまるラベルを選択
タグ付けを行っていくと、フォルダと同じ階層に cat_dog.json
のようなタグ付けを行った結果が格納されたJSONファイルが作成されます。
TensorFlowやCNTKであればそのまま使用できるデータセット形式でExportできるのですが、Chainer向けの形式は残念ながらないため、Python側で簡単に加工していきましょう。
タグ付けした結果をChainerCVのデータセット形式へ変換
出力されたJSONファイルの確認
cat_dog.json
をうまくパースして、データセットに変換していきましょう。
Pathはお手元の状況に合わせてうまく調整してください。
import json
import pandas as pd
with open('images/cat_dog.json') as f:
result = json.load(f)
ファイルの中を確認していきましょう。
result
{'framerate': '1',
'frames': {'0': [{'height': 397,
'id': 0,
'name': 1,
'tags': ['cat'],
'type': 'Rectangle',
'width': 452,
'x1': 65,
'x2': 209,
'y1': 106,
'y2': 370},
{'height': 397,
'id': 1,
'name': 2,
'tags': ['dog'],
'type': 'Rectangle',
'width': 452,
'x1': 195,
'x2': 407,
'y1': 20,
'y2': 370}]},
'inputTags': 'cat,dog',
'scd': False,
'suggestiontype': 'track',
'visitedFrames': [0]}
変数の中を確認していくと、frames
の中に各ラベルのデータが格納されていることがわかると思います。
result['frames']['0']
[{'height': 397,
'id': 0,
'name': 1,
'tags': ['cat'],
'type': 'Rectangle',
'width': 452,
'x1': 65,
'x2': 209,
'y1': 106,
'y2': 370},
{'height': 397,
'id': 1,
'name': 2,
'tags': ['dog'],
'type': 'Rectangle',
'width': 452,
'x1': 195,
'x2': 407,
'y1': 20,
'y2': 370}]
VoTTの結果として、少し困ったこととは、どの画像に対するタグ付けした結果が格納されていないことです。
基本的には、画像名の昇順に対する結果であるため、filepathを取得するときは昇順にソートしておくことを忘れないようにしましょう。
タグ付けされた位置の確認
たとえば、一番最初のサンプルに対して、タグ付けの結果を確認しましょう。
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from glob import glob
from PIL import Image
filepaths = sorted(glob('images/cat_dog/*.jpg'))
len(filepaths)
-> 269
# 一番最初のデータ
filepath = filepaths[0]
img = Image.open(filepath)
plt.imshow(img)
この画像に対するタグ付けされた領域を切り出していきましょう。
vals = result['frames']['0']
vals
[{'height': 397,
'id': 0,
'name': 1,
'tags': ['cat'],
'type': 'Rectangle',
'width': 452,
'x1': 65,
'x2': 209,
'y1': 106,
'y2': 370},
{'height': 397,
'id': 1,
'name': 2,
'tags': ['dog'],
'type': 'Rectangle',
'width': 452,
'x1': 195,
'x2': 407,
'y1': 20,
'y2': 370}]
len(vals)
-> 2
val = vals[0]
val
{'height': 397,
'id': 0,
'name': 1,
'tags': ['cat'],
'type': 'Rectangle',
'width': 452,
'x1': 65,
'x2': 209,
'y1': 106,
'y2': 370}
配列の切り出しを行う場合は numpy の形式に変換しておくと便利です。
img = np.array(img)
img_part = img[val['y1']:val['y2'], val['x1']:val['x2']]
それでは切り出した領域をプロットしてみましょう。
plt.imshow(img_part)
<matplotlib.image.AxesImage at 0x1da07d9d048>
こちらのように全く的外れな部分のタグ付けとなっているのですが、実はheight
とwidth
の部分がヒントで、その大きさにresize
してあげた後の座標の値を指しています。
val = vals[0]
val
{'height': 397,
'id': 0,
'name': 1,
'tags': ['cat'],
'type': 'Rectangle',
'width': 452,
'x1': 65,
'x2': 209,
'y1': 106,
'y2': 370}
画像のリサイズをかけてあげましょう。
img = Image.open(filepath).resize((val['width'], val['height']))
plt.imshow(img)
<matplotlib.image.AxesImage at 0x1da07c373c8>
img = np.array(img)
img_part = img[val['y1']:val['y2'], val['x1']:val['x2']]
plt.imshow(img_part)
<matplotlib.image.AxesImage at 0x1da07d89fd0>
このようにタグ付けした部分を取得できました。
全体に適用
少し難しいですが、下記のようにforでループを回して、すべての画像に対して選択した領域などを抽出し、imgs
, bboxes
, labels
を切り出せるように設定しています。
注意として、画像をPillowやOpenCVで読み込んだ際は、(Height, Width, Channels)
の順で格納されていますが、ChainerCV含め、多くのディープラーニングフレームワークでは(Channels, Height, Width)
のようにChannels
を先頭にもってくる必要があるため、この時点でnp.transpose
を使って変更しています。
imgs, bboxes, labels = [], [], []
for (key, vals) in result['frames'].items():
# ラベル付きの画像のみ
if len(vals) > 0:
# Part1 画像の読み込み
filepath = filepaths[int(key)]
val = vals[0]
img = Image.open(filepath).resize((val['width'], val['height'])) # 読み込みの際にリサイズ
img = np.transpose(img, (2, 0, 1)) # (H, W, C) -> (C, H, W)
# Part2 特定領域の読み込み
_bboxes, _labels = [], []
for val in vals:
x1, x2 = val['x1'], val['x2']
y1, y2 = val['y1'], val['y2']
bbox = [y1, x1, y2, x2]
_bboxes.append(bbox)
_labels.append(val['name'] - 1) # 始まりが1のため
# Part3 numpyに変換
img = np.array(img, 'f')
_bboxes = np.array(_bboxes, 'f')
_labels = np.array(_labels, 'i')
# Part4 リストに追加
imgs.append(img)
bboxes.append(_bboxes)
labels.append(_labels)
切り出した後のデータは極力中を確認するようにしましょう。
bboxes
->
[array([[106., 65., 370., 209.],
[ 20., 195., 370., 407.]], dtype=float32)]
labels
-> [array([0, 1], dtype=int32)]
ChainerのDataset形式に変換してファイルに保存
最後に、後々の学習にしようできるようにPickleでファイルとして保存しておきましょう。
Chainerでは、(画像, バウンディングボックス, ラベル)の順にTuppleDataset
へ渡してあげればDatasetとして使用できます。
特に画像など容量の大きいものは dataset = list(zip(imgs, bboxes, labels))
のようにdatasetの形式へと変換すると、メモリに乗り切らず、学習時になぜか遅いといった問題に遭遇するため注意してください(ChainerのTuppleDataset
は一旦ファイルシステムに置き、必要な分を読み出すとのことです)。
import chainer
dataset = chainer.datasets.TupleDataset(imgs, bboxes, labels)
import pickle
with open('dataset_cad_dog.pickle', mode='wb') as f:
pickle.dump(dataset, f)
これでファイルに保存できていれば、またpickle
で読み込めば完了です。
おわりに
画像のラベル付けなどあまり参考書ではフォーカスが当たらない部分ですが、実務に入る際には必要となり、結構時間がかかるところを今回は紹介しました。
こういった実務寄りの話はなかなか参考書としてまとまっていることは少ないのですが、私自身良い記事を見つけたらツイートするようにしているため、良ければTwitterのフォローをお待ちしています。
Twitter: @yoshizaki_kkgk
それでは、楽しいディープラーニングライフを送りましょう!
著者
株式会社キカガク 代表取締役社長
吉崎 亮介