アノテーションデータを JSON 形式から RecordIO 形式に変換する

以前の記事で [**VoTT** (Visual Object Tagging Tool) により作成したアノテーションデータ](https://qiita.com/clerk67/items/8e7207284037dcf1f9ec) を [Amazon SageMaker](https://aws.amazon.com/jp/sagemaker/) のオブジェクト検出アルゴリズムで使用するために、[**Apache MXNet RecordIO**](https://mxnet.incubator.apache.org/versions/master/architecture/note_data_loading.html) と呼ばれる形式に変換する必要が生じた。このフォーマットは、複数の画像データとそれに対応するアノテーションデータを 1 つのファイルにまとめたもので、Amazon SageMaker のオブジェクト検出アルゴリズムにおいて最も推奨されるファイル形式となっている。今回は、この変換を行うにあたり調査した内容をまとめた。

## 変換の流れ

JSON 形式のファイルから RecordIO 形式のファイルを作るために以下の手順を行う。

1. JSON 形式のファイルから .lst 形式のファイルを作成する。
2. .lst 形式のファイルから RecordIO 形式のファイルを作成する。

## .lst 形式のファイルを作成

.lst ファイルはアノテーションデータと対応する画像ファイルのパスをタブ区切りで並べたもので、以下のような形式である。

```
1	2	5	2.0000	0.4216	0.5482	0.4755	0.6148	20130826/20130826123711/output_0051.jpg
2	2	5	0.0000	0.4020	0.2920	0.5200	0.5273	20130826/20130826192757/output_0908.jpg
3	2	5	3.0000	0.4029	0.1751	0.4947	0.3644	20130826/20130826141251/output_0845.jpg
4	2	5	0.0000	0.4620	0.5035	0.5299	0.6133	20130826/20130826161633/output_0673.jpg
5	2	5	0.0000	0.3821	0.2636	0.5164	0.4494	20130826/20130826183001/output_0350.jpg
6	2	5	0.0000	0.3885	0.3606	0.5016	0.5346	20130826/20130826110101/output_1137.jpg
7	2	5	0.0000	0.3349	0.2981	0.4665	0.4824	20130826/20130826141251/output_0301.jpg
8	2	5	1.0000	0.2994	0.5396	0.3839	0.6623	20130826/20130826192757/output_0327.jpg
9	2	5	0.0000	0.4298	0.1819	0.6123	0.4691	20130826/20130826173608/output_0028.jpg
10	2	5	1.0000	0.2761	0.5324	0.3752	0.6775	20130826/20130826161633/output_0344.jpg
```

各行の 1 列目はユニークな **インデックス番号** である。2 列目の `2` という値は各行の **ヘッダサイズ** を表しており、上記の例では 2 列目と 3 列目がヘッダ列（タグや画像の座標とは関連しないデータ）であることを示している。3 列目の `5` という値は 1 つのタグが持つ **データの個数** を表しており、上記の例では 4 列目から 8 列目までの 5 個の値で 1 つのタグが表されることを示している。4 列目から 8 列目はタグに関する情報で、左から順に［ **タグ番号**、**領域の左端**、**領域の上端**、**領域の右端**、**領域の下端** ］を表している。タグ番号はタグを識別するために付与する番号で、0 から始まる連続する整数である（例えば 0 が犬、1 が猫 ... というように番号がそれぞれ物体に対応する）。領域の上下左右端は画像全体のサイズに対する比率として表現されている。最後の列は対応する **画像ファイルの相対パス** である（相対パスの基準となる位置は任意だが、RecordIO 形式のファイルを作成する際に指定する必要がある）。

1 つの画像に対して複数のタグが付与されている場合は、4 列目から 8 列目までのデータを 9 列目から 13 列目、14 列目から 18 列目 ... というようにタグの個数だけ繰り返して指定し、最後の列に画像ファイルのパスを指定すればよい。

ところで、VoTT により画像ごとに出力される JSON ファイルは以下のような形式となっている。

```json
{
    "asset": {
        "format": "jpg",
        "id": "3d22a9b3f18397dc624a04b4f7b86d66",
        "name": "output_0720.jpg",
        "path": "file:/Volumes/Video/screenshots/20130826/20130826161633/output_0720.jpg",
        "size": { "width": 1920, "height": 1080 },
        "state": 2,
        "type": 1
    },
    "regions": [
        {
            "id": "UIepmXSfA",
            "type": "RECTANGLE",
            "tags": ["blue-sign"],
            "boundingBox": {
                "height": 202.00306748466252,
                "width": 321.04037267080747,
                "left": 586.3250517598342,
                "top": 241.0433282208588
            },
            "points": [
                { "x": 586.3250517598342, "y": 241.0433282208588 },
                { "x": 907.3654244306418, "y": 241.0433282208588 },
                { "x": 907.3654244306418, "y": 443.04639570552143 },
                { "x": 586.3250517598342, "y": 443.04639570552143 }
            ]
        }
    ],
    "version": "2.1.0"
}
```

これらのファイルをまとめて .lst 形式のファイルを作成するために以下の Node.js スクリプトを用意した（機械学習の分野では Python が使用されることが多いが、個人的に Python よりも慣れている Node.js で作成した）。なお、このスクリプトは簡易的に作成したもののため、1 つの領域に複数のタグが付与されている場合が考慮されていないことに注意。

```js
const fs = require('fs');

// アノテーションデータに含まれる全てのタグ（この順番にタグ番号を付与する）
const tags = ['blue-sign', 'green-sign', 'white-sign', 'car'];

let counter = 0;
const lines = [];
const files = fs.readdirSync(__dirname);
for (let i = 0; i < files.length; i++) {
  // カレントディレクトリに存在する全ての JSON ファイルを取得する
  if (!files[i].match(/\.json$/)) continue;
  const data = JSON.parse(fs.readFileSync(files[i], 'utf8'));
  const columns = [
    ++counter, // インデックス番号
    2, // ヘッダサイズ
    5, // データの個数
  ];
  data.regions.forEach((region) => {
    columns.push(...[
      tags.findIndex(tag => tag.name === region.tags[0]).toFixed(4), // タグ番号
      (region.boundingBox.left / data.asset.size.width).toFixed(4), // 領域の左端
      (region.boundingBox.top / data.asset.size.height).toFixed(4), // 領域の上端
      ((region.boundingBox.left + region.boundingBox.width) / data.asset.size.width).toFixed(4), // 領域の右端
      ((region.boundingBox.top + region.boundingBox.height) / data.asset.size.height).toFixed(4), // 領域の下端
    ]);
  });
  columns.push(data.asset.path); // 画像ファイルのパス
  lines.push(columns.join('\t'));
}

// 結果を train.lst に出力
fs.writeFileSync('train.lst', lines.join('\n'));
```

上記のスクリプトを JSON ファイルが出力されているディレクトリで実行すると、train.lst が出力される。

## RecordIO 形式のファイルを作成

.lst 形式のファイルから RecordIO 形式のファイルを作成するには MXNet により配布されている [im2rec.py](https://github.com/apache/incubator-mxnet/blob/master/tools/im2rec.py) を使用する。スクリプトを入手したら以下のコマンドを実行することで、RecordIO 形式のファイルが得られる。ただし、`<your_lst_file_name>` は .lst ファイルの名前、`<your_image_folder>` は .lst ファイルに記述した画像ファイルの相対パスの基準となるディレクトリを表す。

```bash
pip install mxnet
pip install opencv-python

python im2rec.py --pack-label <your_lst_file_name> <your_image_folder>
```

生成される RecordIO（.rec）ファイルは画像データを含んでいるため .lst ファイルと比べて非常に大きなサイズとなる。また、ファイルの生成がかかる場合は `--num-thread <num>` オプションを指定することで高速化することができる。その他の使用可能なオプションについては以下のコマンドにより確認することができる。

```bash
python im2rec.py --help
```