[Amazon SageMaker] Amazon SageMaker Ground Truth で作成したデータをVoTT形式のデータに変換してみました

2020.04.14

1 はじめに

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

Amazon SageMaker Ground Truth (以下、Ground Truth)では、作業を完了したデータセットで、「少し(一部)だけ、修正したい」というような要求に対する仕組みは用意されていません。

今回は、物体検出(Bounding box)用に作成されたデータセットを、VoTT用に変換して、一部のアノテーションの修正・変更などが行えるようにしてみました。

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

2 Ground Truthの出力

最初に、Ground Truthの出力である、画像とアノテーションデータ(output.manifest)をローカルにダウン ロードします。

output.manifestは、各行が各画像用のアノテーション情報であるJSONになっています。(以下、参考)

3 VoTTのプロジェクト作成

VoTT用に変換するためには、VoTT上での画像とアノテーション定義の関係を知る必要がありますが、この関係は、VoTTを起動しないと定義されないため、以下の手順で作業を進めます。

(1) Connection Settings

VoTTを起動し、Connection Settingsで、上でGround Truthの出力をダウンロードしたフォルダを定義します。

下図では、Ground Truthという名前で定義しています。Providerは、Local File Systemをし、先のフォルダが指定されています。

(2) プロジェクト作成

続いてNew Projectでプロジェクトを作成します。

プロジェクト名を指定(任意の名前)し、Source Connection及び、Target Coonnectionをともに、先に作成したGround Truthにします。

Save Projectでプロジェクトを作成すると、画像が読み込まれている事を確認できます(まだ、アノテーションは反映されていません)

サムネイルの画像を選択するごとに、オレンジ色のアイコンが追加されますので、全てのサムネイル画像を一回選択して下さい。このオレンジのアイコンが表示された時点で、画像とアドレーション設定の関連が生成されたことになります。

(3) ラベルの定義

画面右上のTAGSの+から、必要なラベルを定義します。(今回は、AHIRUとDOGに2つです)

(4) プロジェクトファイル

ここまでの作業を終えると、フォルダ内にVoTTのプロジェクトファイル(ここでは、GroundTruthData.vott)が増えている事を確認できます。

このプロジェクトファイルを開くと、以下のような構造となっており、assetsに、画像ファイル名ごとにidが振られていることがわかります。 このidから、アノテーション定義JSONのファイル名が決まります。

{id}-asset.json
{
    "name": "GroundTruthData",
//・・・(略)
    "sourceConnection": {
        "name": "GroundTruth",
//・・・(略)
    },
    "targetConnection": {
        "name": "GroundTruth",
//・・・(略)
    },
    "tags": [
        {
            "name": "AHIRU",
            "color": "#5db300"
        },
        {
            "name": "DOG",
            "color": "#e81123"
        }
    ],
//・・・(略)
    "assets": {
        "cdd6e852fddb2ca802ffc5f851c2fde2": {
            "format": "jpg",
            "id": "cdd6e852fddb2ca802ffc5f851c2fde2",
            "name": "AHIRU-1586397259091391.jpg",
            "path": "file:/Users/xxxxxxxxxxxxxxxxxxxxxxxxxxxx/GroundTruth/AHIRU-1586397259091391.jpg",
            "size": {
                "width": 800,
                "height": 600
            },
            "state": 1,
            "type": 1
        },
        "821128a3e272a0ae103d8b04b005f97a": {
            "format": "jpg",
            "id": "821128a3e272a0ae103d8b04b005f97a",
            "name": "AHIRU-1586397378592848.jpg",
            "path": "file:/Users/xxxxxxxxxxxxxxxxxxxxxxxxxxxx/GroundTruth/AHIRU-1586397378592848.jpg",
            "size": {
                "width": 800,
                "height": 600
            },
            "state": 1,
            "type": 1
        },
//・・・(略)

4 変換

下記のプログラムを実行することで、変換が行われます。設定が必要なのは、以下の2つです。

  • targetPath 画像、output.minifest, VoTTのプロジェクトファイルが存在するフォルダ
  • projectFile VoTTのプロジェクトファイル(ここでは、GroundTruthData.vott)

プログラムが行っているのは、output.manifestの内容を該当するVoTTのJSONファイルに反映しているだけです。

import json
import glob
import os
import shutil
import random, string

# 定義
targetPath = '/Users/xxxxxxxxxx/GroundTruth'
projectFile = 'GroundTruthData.vott'
manifest = 'output.manifest'

# 1件のデータを表現するクラス
class Data():
    def __init__(self, src):
        # プロジェクト名の取得
        for key in src.keys():
            index = key.rfind("-metadata")
            if(index!=-1):
                projectName = key[0:index]

        # メタデータの取得
        metadata = src[projectName + '-metadata']
        class_map = metadata["class-map"]

        # 画像名の取得
        self.imgFileName = os.path.basename(src["source-ref"])
        self.baseName = self.imgFileName.split('.')[0]
        # 画像サイズの取得
        project = src[projectName]
        image_size = project["image_size"]
        self.img_width = image_size[0]["width"]
        self.img_height = image_size[0]["height"]

        self.annotations = []
        # アノテーションの取得
        for annotation in project["annotations"]:
            class_id = annotation["class_id"]
            top = annotation["top"]
            left = annotation["left"]
            width = annotation["width"]
            height = annotation["height"]

            self.annotations.append({
                "label": class_map[str(class_id)],
                "width": width,
                "top": top,
                "height": height,
                "left": left
            })

# 全てのJSONデータを読み込む
def getDataList(inputPath, manifest):
    dataList = []
    with open("{}/{}".format(inputPath, manifest), 'r') as f:
        srcList = f.read().split('\n')
        for src in srcList:
            if(src != ''):
                json_src = json.loads(src)
                dataList.append(Data(json.loads(src)))
    return dataList

# 画像名からデータを検索する
def getData(dataList, imgName):
    for data in dataList:
        if(data.imgFileName == imgName):
            return data
    return None

# ランダム文字列生成
def randomname(n):
   return ''.join(random.choices(string.ascii_letters + string.digits, k=n))

def main():

    # 全てのJSONデータを読み込む
    dataList = getDataList(targetPath, manifest)
    log = "全データ: {}件 ".format(len(dataList))

    # VoTTのプロジェクトファイルを読み込む
    with open("{}/{}".format(targetPath, projectFile), 'r') as f:
        project = json.loads(f.read())

    assets = project["assets"]
    # プロジェクトからidの列挙
    for id in assets.keys():
        # 画像ファイル名の取得
        imgName = assets[id]["name"]
        # 画像名からデータを検索する
        data = getData(dataList, imgName)
        # アノテーション情報
        vott_json = {}
        vott_json["asset"] = {
            "format": "jpg",
            "id": id,
            "name": imgName,
            "path": "file:{}/{}".format(targetPath, imgName),
            "size": {
                "width": data.img_width,
                "height": data.img_height
            },
            "state": 2,
            "type": 1
        }
        vott_json["regions"] = []
        for annotation in data.annotations:
            height = int(annotation["height"])
            width = int(annotation["width"])
            top = int(annotation["top"])
            left = int(annotation["left"])
            label = annotation["label"]
            vott_json["regions"].append({
                "id": randomname(9),
                "type": "RECTANGLE",
                "tags": [
                    label
                ],
                "boundingBox": {
                    "height": height,
                    "width": width,
                    "left": left,
                    "top": top
                },
                "points": [
                    {
                        "x": left,
                        "y": top
                    },
                    {
                        "x": left + width,
                        "y": top
                    },
                    {
                        "x": left + width,
                        "y": top+height
                    },
                    {
                        "x": left,
                        "y": top + height
                    }
                ]
            })
        vott_json["version"] = "2.1.0"

        # jsonの保存
        with open("{}/{}-asset.json".format(targetPath, id), mode='w') as f:
            json.dump(vott_json, f)

main()

5 確認

変換が終わって、再びVoTTを開くと、アノテーションが反映されていることが確認できます。

統計情報で、ラベル数も確認できます。

6 最後に

今回は、Ground Truthで作成したデータセットをVoTTで利用可能なように変換してみました。

「オブジェクト検出」においては、なんだかんだ言っても、データセットを軽易に確認・修正出来る事が、結構大事だと感じています。ローカルで操作できるVoTTは、非常に強力だと思います。