[Amazon Rekognition] VoTTで作成したデータをCustom Labelsで利用可能なAmazon SageMaker Ground Truth形式に変換してみました

2020.04.08

1 はじめに

CX事業本部の平内(SIN)です。

Amazon Recognition のCustom Labelsでオブジェクト検出のモデルを作成する場合、データセットの準備が必要です。 画像に対するアノテーションは、Custom LabelsのUIでも提供さていますが、今回は、これを、microsoft/VoTTで作成してみました。

microsoft/VoTTは、マイクロソフトによって開発されているアプリケーションで、ローカルでアノテーションの定義が可能なツールです。

Custom Labelsでは、Ground Truth の出力からデータセットが生成できますので、VoTT形式のデータをGround Truth 形式に変換する作業になります。

2 VoTT

動作確認のためにVoTTで簡単なアノテーションを定義しました。

この状態で、作業フォルダを確認すると、プロジェクトファイル(tmp0.vott)と各画像に対応したアノテーション定義ファイル(.json)が生成されています。なお、Source CorrectionTarget Correctonは、同じになっています。

3 VoTT形式

VoTTが生成する、アノテーション情報の入ったJSONファイルは、以下のようになっています。 ここで、変換に必要なのは、対応する画像ファイル名(/tmp0/IMG_9281.jpg)や、アノテーションの座標情報(boundingBox)、ラベル情報(tags)です。

{
    "asset": {
        "format": "jpg",
        "id": "7eb927d9136e5357a2a2e5e3bb745f7f",
        "name": "IMG_9281.jpg",
        "path": "file:/tmp0/IMG_9281.jpg",
        "size": {
            "width": 600,
            "height": 800
        },
        "state": 2,
        "type": 1
    },
    "regions": [
        {
            "id": "SBoWJxOx1",
            "type": "RECTANGLE",
            "tags": [
                "AHIRU"
            ],
            "boundingBox": {
                "height": 516.9595926412616,
                "width": 336.0018060420315,
                "left": 166.73872591943956,
                "top": 62.86957950065703
            },
            "points": [
                {
                    "x": 166.73872591943956,
                    "y": 62.86957950065703
                },
                {
                    "x": 502.7405319614711,
                    "y": 62.86957950065703
                },
                {
                    "x": 502.7405319614711,
                    "y": 579.8291721419185
                },
                {
                    "x": 166.73872591943956,
                    "y": 579.8291721419185
                }
            ]
        }
    ],
    "version": "2.1.0"
}

4 Ground Truth 形式

一方、変換したいGround Truthのファイル形式は、以下のようになっています。変換は、元データから取得したファイル名等を、該当する箇所に埋め込むだけです。

下記の例は、JSONが見やすいように、コロンの後ろに空白や改行や空白が入っていますが、実際に生成するファイルでは、空白や改行があるとエラーとなって読み込めませんので、注意が必要です。

{
    "source-ref": "s3://data-set-creater/TMP/IMG_9281.jpg",
    "boxlabel": {
        "image_size": [
            {
                "width": 600,
                "height": 800,
                "depth": 3
            }
        ],
        "annotations": [
            {
                "class_id": 0,
                "width": 336.0018060420315,
                "top": 62.86957950065703,
                "height": 516.9595926412616,
                "left": 166.73872591943956
            }
        ]
    },
    "boxlabel-metadata": {
        "job-name": "7eb927d9136e5357a2a2e5e3bb745f7f",
        "class-map": {
            "0": "AHIRU"
        },
        "human-annotated": "yes",
        "objects": {
            "confidence": 1
        },
        "creation-date": "2020-04-08T14:32:32.252239",
        "type": "groundtruth/object-detection"
    }
}

変換で生成されたデータは、以下のように出力先フォルダに出力されます。 なお、manifest(output.manifest)ファイルは、各jsonファイルを1行として集めたテキストファイルです。Custon Labelsからは、このmanifest(output.manifest)ファイルが読みこまれます。

5 S3

出力フォルダのファイルのうち、画像ファイルとmanifest(output.manifest)を、S3にアップロードします。各jsonファイルの内容は、先のとおり、manifest(output.manifest)に集められていますので、ここでは必要ありません。

6 Custom Labels

S3に置いたmanifest(output.manifest)を指定してデータセットを作成します。注意書きにもありますように、画像等を置いたS3バケットには、Rekognitionからアクセスするためのパーミッションの追加が必要です。

データセットが作成された様子です。VoTTで 設定したアノテーションが、反映されていることを確認できます。

7 プログラム

変換のためのプログラムは、以下のとおりです。下記のパスを設定して利用できます。

  • inputPath 入力フォルダ(VoTTのSource Correction/Target Correcton
  • outputPath 出力フォルダ(変換したファイルの出力先)
  • s3Path 画像が置かれるS3のバケット及び、プレフィックス(Rekognitionからデータセットを作成する場合に使用される)
import json
import glob
import os
import shutil
import datetime

# 定義
inputPath = '/tmp/input'
outputPath = '/tmp/output'
manifest = 'output.manifest'
s3Path = 's3://my-dataset/TMP'

# 出力先フォルダ生成
os.makedirs(outputPath, exist_ok=True)
# マニュフェスト出力用
outputText = ''

srcFiles = glob.glob("{}/*.json".format(inputPath))

# class_idが、全データで共通となるラベルの一覧を管理する
labels = []

for srcFile in srcFiles:
    basename = os.path.basename(srcFile)
    dst_json = {}
    with open(srcFile, 'r') as f:
        src_json = json.load(f)

        asset = src_json["asset"]
        path = asset["path"]
        id = asset["id"]
        name = asset["name"]
        size_width = asset["size"]["width"]
        size_height = asset["size"]["height"]

        dst_json["source-ref"] = "{}/{}".format(s3Path,name)

        dst_json["boxlabel"] = {}

        dst_json["boxlabel"]["image_size"] = []
        dst_json["boxlabel"]["image_size"].append({
            "width": size_width,
            "height": size_height,
            "depth": 3
        })

        dst_json["boxlabel"]["annotations"]=[]
        regions = src_json["regions"]
        for i,region in enumerate(regions):
            tag = region["tags"][0]
            if(labels.count(tag)==0):
                labels.append(tag)
                cls_id = len(labels)-1
            else:
                cls_id = labels.index(tag)

            boundingBox = region["boundingBox"]
            left = boundingBox["left"]
            top = boundingBox["top"]
            width = boundingBox["width"]
            height = boundingBox["height"]
            dst_json["boxlabel"]["annotations"].append({
                "class_id": cls_id,
                "width": int(width),
                "top": int(top),
                "height": int(height),
                "left": int(left)
            })
        dst_json["boxlabel-metadata"] = {}
        dst_json["boxlabel-metadata"]["job-name"] = id
        dst_json["boxlabel-metadata"]["class-map"] = {}
        for i,label in enumerate(labels):
            dst_json["boxlabel-metadata"]["class-map"][i]= label
        dst_json["boxlabel-metadata"]["human-annotated"] = "yes"
        dst_json["boxlabel-metadata"]["objects"] = [{
            "confidence": 1
        }]
        dt = datetime.datetime.now()
        dst_json["boxlabel-metadata"]["creation-date"] = dt.isoformat(timespec='microseconds')
        dst_json["boxlabel-metadata"]["type"] = "groundtruth/object-detection"


    outFile = "{}/{}.json".format(outputPath, name.split('.')[0])
    with open(outFile, 'w') as f:
        json.dump(dst_json, f)

    #outputText += str(dst_json) + '\n'
    outputText += json.dumps(dst_json) + '\n'

    outFile = "{}/{}".format(outputPath, name)
    shutil.copyfile(path.replace('file:',''), outFile)

with open("{}/{}".format(outputPath, manifest), mode='w') as f:
    f.write(outputText)

8 最後に

アノテーション作業は、大量になると結構大変な作業ですが、ローカルで作業できるVoTTは、非常に快適です。 VoTTでCustom Labelsのデータセットが作成できるのは、個人的には非常に嬉しいです。