この記事はFOSS4G Advent Calendar 2019の9日目の記事です。
はじめに
機械学習に限らず、画像分類や認識の教師付き学習のためには、教師データの作成が必要になりますね。この教師データの作成のためのツールがアノテーションツールです。この記事は、QGISをアノテーションツールとして使ってみたよ、という内容です。
求めるアノテーションツールに必要だったこと
今年は、機械学習による画像分類のための教師作成の作業があったので、アノテーション作業を進めるべく、以下の視点で専用ツールをいくつか試してみました。
- 四角形(矩形)ではなくポリゴンが利用できること
- JSON出力が可能であること
- 複数の画像ファイルをまとめて扱えること
- 与えた値が視覚的に確認できること(カテゴリ値で塗り分ける)
- 元に戻す&やり直しが可能であること
- パン&拡大縮小が容易であること
- カテゴリ入力が容易であること
さて、色々と試してみましたが、どうも痒いところに手が届きません。何かが背中を這い回っています。むう、痒い、痒いぞ…ッッッ!!
当初、デジタイズ的なトレース作業とか、カテゴリでのシンボル分類表示とか、自由な拡大縮小とか、GISだったら簡単にできるので、てっきり同じようにできると簡単に考えていたのですが、世のアノテーションツールが追求するポイントはそこじゃないのかもしれません。むう、困った。
と、そうこうしているうちに、**デジタイズと同じようなものなんだったらQGISでやればよくね?**と思いつき、QGISでアノテーションデータを作ってみることにしました。
こんなんできました
作成するアノテーションデータに必要なこと
今回必要としたアノテーションデータの要件は以下でした。
- アノテーションデータはポリゴンであること
- それぞれのポリゴンには、対象を表すカテゴリー(ラベル)が付与されていること
また、アノテーションを行う対象画像の枚数が多く数千枚もあったことから、画像ファイル毎にアノテーションファイルを生成するような方法は扱いが面倒なので、出力するアノテーションファイル(JSON)には画像ファイルとポリゴンを紐付けられるようにする必要がありました。これは、ポリゴンの属性値に対象となる画像ファイル名を入力することで解決します。
まとめると、ここで作成すべきアノテーションデータは、複数の画像をトレースして作成したポリゴンに、ラベルと対象の画像ファイル名が属性として記録されているJSONファイルとなります。
手順
細かいことは置いといて、早速QGISで入力するための手順を書きます。
新規のプロジェクトを作ります
画像レイヤのグループに画像を追加します
アノテーションデータを格納するベクタレイヤを作ります
- 出力はJSONですが、マスタファイルはGeoPackageとします。
- ここでは、data.gpkgというGeoPackageにannotationというテーブルを追加しました。
- ジオメトリタイプはポリゴンとします。
- ジオメトリにはいずれかの空間参照を指定する必要があるため、EPSG:4326 - WGS 84を指定します。トレースで生成した地物の座標値は、ラスタのピクセル座標になります。(左上が原点)
- 属性値を格納するフィールドとして、以下を追加します。
- image ・・・テキストデータ(対応する画像ファイル名を格納する)
- category ・・・テキストデータ(各ポリゴンのラベルを格納する)
表示されている画像に対応したポリゴンだけを表示するようにします
これから、このポリゴンレイヤにポリゴンを追加していくわけですが、ポリゴンレイヤにはすべてのアノテーションデータが追加されていくので、現在表示されている画像に対応したポリゴンだけを表示するようにする必要がありそうです。特定のポリゴンだけを表示すること自体は、レイヤスタイルを使い、imageフィールドの値でフィルタリングすることなどで実現できそうですが、そのためには「今表示されているラスタレイヤ名」を随時取得しなければなりません。
そこで、ユーザー定義関数を使って「いま表示されているレイヤ名を取得する関数」を作りました。
annotationレイヤのプロパティから、シンボロジー調整メニューを表示し、シンボルのルールをカテゴリ値による定義とし、その下にある値のプルダウンメニュー右のεをクリックして数式ダイアログを表示します。
式ダイアログの上部タブから関数エディタを選択し、ダイアログ下部の「+」ボタンから新たな関数ファイルannotationを作成します。入力するスクリプトは以下の通りです。
from PyQt5.QtWidgets import QDialog
from qgis.core import *
from qgis.gui import *
@qgsfunction(args='auto', group='Custom')
def get_visiblelayername(s_group, feature, parent):
# グループ名がs_groupなグループのオブジェクトを取得したら
g = QgsProject.instance().layerTreeRoot().findGroup(s_group)
for lyr in g.findLayers(): # グループ内のレイヤを順番に
# レイヤがVisibleかどうかを判定して
flg = QgsProject.instance().layerTreeRoot().findLayer(lyr.layer()).isVisible()
if flg:
break # Trueなら
return lyr.name() # そのレイヤの名前を返す
ここで定義している関数get_visiblelayername(s_group)は、文字列の引数s_groupで指定したグループ内に所属するレイヤを走査して、現在表示されているレイヤ名を返します。入力ができたら、関数の保存と読み込みをクリックしてダイアログを閉じます。
さて、これでレイヤ名を取得できるようになりましたので、カテゴリ値による定義で指定する「値」に、"image" is get_visiblelayername('images')
と入力し、シンボルを「+」ボタンから直接追加し、指定する値を「1」として、適当なシンボル色などを指定します。
それではトレースしてみましょう
いったんプロジェクトファイルを保存しました。
あとは、普通にデジタイズのノリでポリゴンを入力します。フィールド値の入力は以下の要領です。
ちゃんと鳥さんのトレースデータの表示が消えましたね。(環境によっては一時的に表示が残る場合があるみたいですが、マップキャンバスを動かしたりすると消えます。)
もっと楽ちんにしましょう
この状態だと、地物を追加する度に、対応するラスタレイヤ名を入力する必要があります。それは大変なので、この入力を自動化します。
annotationレイヤのプロパティから、属性フォームを選択し、imageフィールドの「制約」と「既定値」にそれぞれ数式get_visiblelayername('images')
を入力します。また「編集可能」のチェックを外し、「NULLを除く」「非NULL制約を強制」にチェックを入れておくとよいと思います。
さらに、categoryフィールドの値はプルダウンから選択できるようにします。ウィジェットタイプを「バリューマップ」として、入力するラベルを指定しておきます。また、こちらも「NULLを除く」「非NULL制約を強制」にチェックを入れておきます。
もちろん、ラベルで塗り分けもできます
シンボロジの設定で、カテゴリ値の値をif("image" is get_visiblelayername('images'), "category", 0)
とすれば、現在表示されているレイヤに属したラベルだけが取得できます。
あとは、適宜annotationレイヤをGeoJSONで吐き出し、使用する分類や認識モデルのインプットに合うようにJSONを書き換えていい感じに調理すれば、教師データのできあがりです!(雑
{
"type": "FeatureCollection",
"name": "annoation",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "fid": 1, "image": "01", "category": "鳥さん" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 7.314285714285688, -306.057142857142878 ], ... [ 5.942857142857115, -337.6 ], [ 7.314285714285688, -306.057142857142878 ] ] ] } },
{ "type": "Feature", "properties": { "fid": 2, "image": "01", "category": "鳥さん" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 481.828571428571308, -182.628571428571433 ], ... [ 484.571428571428498, -194.971428571428589 ], [ 481.828571428571308, -182.628571428571433 ] ] ] } },
{ "type": "Feature", "properties": { "fid": 3, "image": "02", "category": "狼さん" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 254.171428571428521, -481.6 ], ... [ 267.885714285714243, -481.6 ], [ 254.171428571428521, -481.6 ] ] ] } },
{ "type": "Feature", "properties": { "fid": 4, "image": "03", "category": "カワウソさん" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 359.771428571428487, -302.62857142857149 ], ... [ 369.37142857142851, -319.771428571428601 ], [ 359.771428571428487, -302.62857142857149 ] ] ] } },
{ "type": "Feature", "properties": { "fid": 5, "image": "04", "category": "猫さん" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 544.228571428571286, -319.085714285714346 ], ... [ 581.257142857142753, -333.48571428571438 ], [ 544.228571428571286, -319.085714285714346 ] ] ] } },
{ "type": "Feature", "properties": { "fid": 6, "image": "04", "category": "猫さん" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 749.942857142856838, -397.257142857142924 ], ... [ 780.114285714285415, -419.2 ], [ 749.942857142856838, -397.257142857142924 ] ] ] } }
]
}
めでたしめでたし。
最後に
これまで1,000枚を越える画像を放り込んで作業していますが、ちゃんと動作しています。ただ、見えてないだけで背景には大量のポリゴンがあるため、スナップは無効にしておくが吉です。
ちなみに、マウスでクリックしまくるんじゃなくてお絵かき感覚でトレースしたいよって人は、こういうプラグインを使うと幸せになれるかもしれません。
それではみなさま、よいお年を!(早
本記事のライセンス
この記事は クリエイティブ・コモンズ 表示 4.0 国際 ライセンスのもとに提供します。
なお、サンプルとして登場していただいた動物さんたちの画像はいずれもパブリック・ドメイン・マーク 1.0のもと提供されている以下の画像を使用しました。