SageMaker GT に Semantic Segmentation データセットを取り込む

## 概要

* 手元のセマンティックセグメンテーションタスクのデータセットを SageMaker Ground Truth に取り込み、アノテーションを調整したい
* 目的は達成できたが、０ コストの労力で実現はできなかった
  * 社内の協力者がいたためなんとかなった
  * 画像枚数が 2,000 枚以下なのでなんとかなった

## 背景

* 数年前、とある案件で AWS アカウントを作成
* このアカウント上で SageMaker GT を使いアノテーション作業を実施
* クラウドからアノテーション結果のダウンロードを行い、加工した後に gdrive で保管
* 案件終了と共に AWS アカウントを削除
* 再度アノテーションデータの追加・調整の要望

上記の経緯があり、再度手元のデータセットを SageMaker GT にインポートする必要を迫られた。この要件が満たせるか不安だったが無事目的を達成できたため共有したい。

## 手順

おおまかな手順は下記を踏んだ。それぞれのステップに関して後述する。
**画像（サンプル）の枚数に対して協力者が少ない場合は別の手段を講じる必要がある。**

* 手元のデータセットのラベル情報の確認をする
* 画像セットを s3 bucket にアップロードする
* ↑をデータソースとして新規のラベリングジョブを作成する
* アノテーション無しで一度ジョブを完了させる
* S3 上のカラーラベル画像を上書きする
* 調整用のラベリングジョブを作成する

### 手元のデータセットのラベル情報の確認をする

SageMaker GT のラベリングジョブ作成時にラベル情報を登録する必要があるため、始めに手元のデータセットのアノテーションデータ（カラーラベル画像）を合わせる必要がある。

私のセマンティックセグメンテーション向けのデータセットは以下のようなファイルレイアウトとなっており、学習時には `images`, `labels` 以下の画像ファイルを使っている。`images` 下は入力画像で、`labels` をラベル画像と呼称している。ラベル画像は 1ch で画素にセマンティッククラスの ID が 0-255 の値で格納されている。（0 が背景、1 が対象物A、2 が対象物B といった具合）。このため、ラベル画像は単に画像ビュアーで開いてもアノテーション結果が目視で確認できない。このため、3ch のカラーラベル画像 (`color_labels`) を用意している。

また、これらサンプルとして対応関係を保つためにファイル名は規則を設けている。 例として、`0001` をサンプルの名称とする時、それぞれ`images/0001.jpg`, `labels/0001.png`として `0001` の部分で一意に特定できるようにしている 。

```
dataset_root/
├── color_labels // *.png
├── images // *.jpg
├── labels // *.png
```

ラベル画像は手元にあるが、カラーラベル画像が存在しない場合は用意する必要がある。後述する output.manifest に関係する部分として、ラベリングジョブ作成時に特別な指定無しに進めている場合、下記の `internal-color-map` のようなラベルが SageMaker GT で作られて個々のアノテーションタスクで付与されている。カラーラベル画像を作成する際は、このカラーコード（ `hex-color` ）に従い RGB 値を求めて作成する。`hex-color` の変換は下記の python コードの断片記載のように求めることができる。

```python
def code2rgb(code):
    code = code.lstrip('#')
    return tuple(int(code[i:i+2], 16) for i in (0, 2, 4))
```

```json
    "internal-color-map": {
      "0": {
        "class-name": "BACKGROUND",
        "hex-color": "#ffffff",
        "confidence": 0
      },
      "1": {
        "class-name": "A",
        "hex-color": "#2ca02c",
        "confidence": 0
      },
      "2": {
        "class-name": "B",
        "hex-color": "#1f77b4",
        "confidence": 0
      },
      "3": {
        "class-name": "C",
        "hex-color": "#ff7f0e",
        "confidence": 0
      },
      "4": {
        "class-name": "D",
        "hex-color": "#d62728",
        "confidence": 0
      },
      "5": {
        "class-name": "E",
        "hex-color": "#9467bd",
        "confidence": 0
      },
      "6": {
        "class-name": "F",
        "hex-color": "#8c564b",
        "confidence": 0
      }
```

### 画像セットを S3 バケットにアップロードする

* データセットの `images/*.jpg` を任意のバケットにアップロードする

### 新規のラベリングジョブを作成する

* 先ほどアップロード先の S3 バケットを指定して作成する
* 出力先は別のバケットを用意するとよい
* ラベルの作成は id とカラーコードを示す色に注意しつつ設定する

### 一度ラベリングジョブを完了させる

* アノテーション画面で「Submit」 ボタンの横に「Nothing to label」 のボタンがあるため、ひたすら押下を行いジョブを１度完了させる

### S3 上のカラーラベル画像を上書きする

* 出力先の S3 バケット内の `output.manifest` を確認する
* 下記のようにラベリングタスクのメタデータが確認できる
  * `experimet1` がジョブ名
  * `source-ref` が入力画像の位置
  * `experiment1-ref` がジョブ `experiment1` でアノテーションされたカラーラベル画像の位置

```json
{
  "source-ref": "s3://img-source/105_4_00012.jpg",
  "experiment1-ref": "s3://anno-output/experiment1/annotations/consolidated-annotation/output/2_2022-10-03T04:39:30.089215.png",
  "experiment1-ref-metadata": {
    "internal-color-map": "略",
    "type": "groundtruth/semantic-segmentation",
    "human-annotated": "yes",
    "creation-date": "2022-10-03T04:39:30.271567",
    "job-name": "labeling-job/experiment1"
  }
}
```

* 上記のメタデータに従い、下記のようなスクリプトを用意してカラーラベル画像を上書きする

```python
import argparse
import json
import boto3


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description="指定されたラベリングジョブの consolidated annotation output のファイルを上書きする")
    parser.add_argument('--bucket', type=str, required=True, help='bucket where a manifest file exists')
    parser.add_argument('--job-name', type=str, required=True, help='labeling job name which create the manifest file')
    parser.add_argument('--data-root', type=str, required=True, help='path to local dataset root')
    args = parser.parse_args()

    job_name = args.job_name
    data_root = args.data_root

    s3 = boto3.client('s3')
    key = f'{job_name}/manifests/output/output.manifest'
    s3.download_file(args.bucket, key, 'output.manifest')

    with open('output.manifest', 'r') as f:
        for line in f.readlines():
            meta = json.loads(line)

            img_filename = meta['source-ref'].split('/')[-1]
            ano_filename = img_filename.replace('.jpg', '.png')
            ano_filepath = f'{data_root}/color_labels/{ano_filename}'

            with open(ano_filepath, 'rb') as f2:
                ano_data = f2.read()

            output_path = meta[f'{job_name}-ref']
            path_components = output_path.replace('s3://', '').split('/')
            bucket = path_components[0]
            key = '/'.join(path_components[1:])
            s3.put_object(Bucket=bucket, Key=key, Body=ano_data)

```

* ラベリングジョブの概要からアノテーション結果が更新されていれば良い

### 調整用のラベリングジョブを作成する

* **「ラベリングジョブ作成」から行うこと**
　　* 「ジョブのチェーン」、「ジョブのクローン」は使わない
　
ラベリングジョブの作成画面では **「入力データのセットアップ」から「手動によるデータのセットアップ」を選び、先ほどの `output.manifest` を指定する。** 「次へ」から「既存ラベルの表示」の中の「このジョブのデータセットから既存のラベルを表示したいです」から、 **前述のラベリングジョブを指定してラベル情報** を読み込む。

## まとめ

* ダウンロードの逆をやればよいが、中間ファイルを上書きすれば本当に良いのか不明な点であり不安があった
* 2022/12/20 時点では、記載の内容は有効だった
    * SageMaker の出力する output.manifest はパッと見て、数年前から変わっていないようなので、しばらくは再現性があるのではないか

## 余談

* ラベリングジョブのタスクの有効期限が過ぎたなどで、アノテーションタスクに下記のようなエラーが格納されている場合があるので、ダウンロード時は注意が必要だった

```json
{
    "retry-count": 1,
    "failure-reason": "ClientError: Task failed to render: [ InvalidParameters: '\\\"grant_read_access\\\" input is not a valid S3 URI: \\\"\\\".' ].",
    "human-annotated": "true"
}
```