Grand-challenge向けの環境準備 Part.2（ローカルPCでの提出用ファイルの準備方法）


[前記事](https://qiita.com/chihiro1209/items/6310a3e26e46115938f9)からの続きです。Grand Challengeへの提出物の例を作ってみた際のメモです。

# 4. ローカルPCでのアルゴリズム・コンテナ(Algorithm container)の準備
[Grand Challenge Documentation -> Algorithms -> Create your own algorithm -> Creating an Algorithm container](https://grand-challenge.org/documentation/creating-an-algorithm-container/) に従って進めます。

## 4.1 参考とするアルゴリズムの準備
### 4.1.1 ファイルのコピー（クローン作成）
Anaconda PowerShell prompt で以下の通り、Githubからクローンを作成します。

```PowerShell:Anaconda PowerShell prompt
(base) PS C:\Users\(ユーザー名)> git clone https://github.com/DIAGNijmegen/drive-vessels-unet.git 
```

:::note
このファイルは参考にするだけなので、実はわざわざコピー（クローン）する必要はありませんでした。GithubのWebサイト上でソースコードを閲覧し適宜コピーすようにしても問題ありません。
:::

:::note
今回は、C:\Users\(ユーザー名)\git_work という作業用フォルダを作成して作業しました。以下、C:\Users\ (ユーザー名)\git_work を作業用フォルダとして説明します。
:::

### 4.1.2 ファイル構成の理解
コピーしてきたファイルは以下のような構成になっていました。
```:ファイル構成
drive-vessels-unet/
 ├ train.py 									# モデルの学習を行うPythonファイル
 ├ dataloader.py 								# train.pyから呼び出されるデータの読み出し機能のモジュール
 ├ best_metric_model_segmentation2d_dict.pth　	# train.pyで学習したモデルの重みファイル
 ├ inference.py 								# 学習済みの重みファイルを使って推論を行うPythonファイル
 ├ VesselSegmentation/ 							# 推論を行うプログラムをGrand Challengeに提出できる形態に変えた完成形
     ├ (省略)
 ├ (省略)
```
[train.py](https://github.com/DIAGNijmegen/drive-vessels-unet/blob/master/train.py) と [inference.py](https://github.com/DIAGNijmegen/drive-vessels-unet/blob/master/inference.py) をざっと見ると、それぞれかなりシンプルなコードとなっています。

VesselSegmentation フォルダ以下がGrand Challengeに提出できる形態なので、そのまま提出すればよいわけですが、自分で作ったアルゴリズムをGrand Challengeに提出する形態に変える方法を習得するために以下のことを行います。
1. train.py の学習プログラムをGooogle Colab上で実行できるようにする。
2. interference.py の推論プログラムを Gooogle Colab上で実行できるようにする。
3. interference.py の推論プログラムを ローカルPC上で Grand-challengeに提出できる形態に整える。

機械学習プログラムを作るときの順番は1→2→3ですが、この記事では先に3からやり方を習得します。

## 4.1.3 inference.pyの理解
移植元の`inference.py`を眺めて理解しました。以下のコードの(1)モデルの読み込みと(2)推論部分を、後ほど提出用プログラムに移植します。

```Python:inference.py
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
import SimpleITK
import torch
import monai
import numpy as np
from skimage import transform
from scipy.special import expit
import matplotlib.pyplot as plt


# (1). モデル定義と重みファイルの読み込み　↓ここから
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model = monai.networks.nets.UNet(
    dimensions=2,
    in_channels=3,
    out_channels=1,
    channels=(16, 32, 64, 128, 256),
    strides=(2, 2, 2, 2),
    num_res_units=2,
).to(device)

model.load_state_dict(torch.load("./best_metric_model_segmentation2d_dict.pth"))
# ↑(1).ここまで

fig, ax = plt.subplots(1, 2, figsize=(7, 7), constrained_layout=True)

# Read the image
image = SimpleITK.ReadImage("vesselSegmentor/test/1000.0.mha")
# (2) 推論の実行　↓ここから
image = SimpleITK.GetArrayFromImage(image)
image = np.array(image)
shape = image.shape

# Plot the image
ax[0].imshow(image)
ax[0].axis("off")
ax[0].set_title("input")

# Pre-process the image
image = transform.resize(image, (512, 512), order=3)
image = image.astype(np.float32) / 255.
image = image.transpose((2, 0, 1))
image = torch.from_numpy(image).to(device).reshape(1, 3, 512, 512)

# Do the forward pass
out = model(image).squeeze().data.cpu().numpy()

# Post-process the image
out = transform.resize(out, shape[:-1], order=3)
out = (expit(out) > 0.99)
out = (out * 255).astype(np.uint8)
# ↑(2) ここまで

# Plot the prediction
ax[1].imshow(out, cmap="gray")
ax[1].axis("off")
ax[1].set_title("prediction")

plt.show()

```
:::note
以下２つのモジュールが私にとっては初見でした。いずれも医療画像特有のモジュールらしいです。
* SimpleITK : 画像処理モジュール。OpenCVみたいなもの。医療画像の標準ファイル形式であるDicomを読み書きできる。
* monai : PyTorch上で動く医用画像の処理に特化したAIフレームワーク
:::

## 4.2 evalutilsによる雛形ファイルの生成
### 4.2.1 雛形ファイル生成
Anaconda PowerShell prompt で以下の通り、evalutils により雛形ファイルを生成します。
```PowerShell:Anaconda PowerShell prompt
(base) PS C:\Users\(ユーザー名)\git_work> evalutils init algorithm VesselSegmentationContainer 
```
"VesselSegmentationContainer" は生成する雛形につけるプロジェクト名であ、生成されるフォルダ名となります。
コマンドを実行すると以下の質問文が現れました。
```PowerShell:Anaconda PowerShell prompt
What kind of algorithm is this? (Classification, Segmentation, Detection):
```
今回の例では ```Segmentation```と入力します。これで雛形ファイルが自動的に生成されました。

### 4.2.2 生成されたファイルの理解
生成された雛形ファイルは以下のような構成になっていました。
```:ファイル構成
VesselSegmentationContainer/ 
 ├ Dockerfile 
 ├ build.sh 
 ├ export.sh
 ├ test.sh 
 ├ requirements.txt 
 ├ process.py 
 ├ test/
   ├ 1.0.000.000000.0.00.0.0000000000.0000.0000000000.000.mhd 
   ├ 1.0.000.000000.0.00.0.0000000000.0000.0000000000.000.zraw 
   └ expected_output.json 
```
拡張子が`.sh`はシェルスクリプトファイルです。


## 4.3. 推論プログラムの移植作業

evalutilsで生成した雛形ファイルに対して、以下の順番で推論プログラムの移植作業を行います。
4.3.1. process.py に推論を行うコードを移植する。
4.3.2. Dockerfileとrequirement.txt を修正し、依存関係にあるモジュール等を記載する。
4.3.3. build.sh でdockerコンテナがビルドできるか試してみる。
4.3.4. test.sh でdockerが構築した仮想環境上で推論が行えるかをチェックする（提出前に正しく動くかをチェックする）
4.3.5. export.sh でdockerコンテナの提出形態ファイル（.tar.gz）を作成する

### 4.3.1 process.py に推論を行うコードを移植

自動生成された`process.py`の中身を確認します。

```Python:process.py(自動生成されたまま)
import SimpleITK
import numpy as np

from evalutils import SegmentationAlgorithm
from evalutils.validators import (
    UniquePathIndicesValidator,
    UniqueImagesValidator,
)

class Vesselsegmentationcontainer(SegmentationAlgorithm):
    def __init__(self):
        super().__init__(
            validators=dict(
                input_image=(
                    UniqueImagesValidator(),
                    UniquePathIndicesValidator(),
                )
            ),
        )
        # (1) ここに初期化処理を記載する

    def predict(self, *, input_image: SimpleITK.Image) -> SimpleITK.Image:
        # (2) ここに推論処理を記載する。 return 処理も書き換える。
        # Segment all values greater than 2 in the input image
        return SimpleITK.BinaryThreshold(
            image1=input_image, lowerThreshold=2, insideValue=1, outsideValue=0
        )

if __name__ == "__main__":
    Vesselsegmentationcontainer().process()
```

:::note
`def predict(self, *, input_image: SimpleITK.Image) -> SimpleITK.Image:` のコード表現が、私にとっては見慣れない書き方でした。
* 引数に`*`が指定されると、`*`以降に定義されている引数、この場合だと`input_image` をキーワード引数(引数名=値)として受け取ることを強制します。 `*`自体は引数にはなりません。([参考Webサイト](https://dev.classmethod.jp/articles/what-does-asterisk-mean-at-args/))
* `: SimpleITK.Image` は関数アノテーションと呼び、引数`input_image`が`SimpleITK.Image`型であることを示しています。ただし、あくまで関数の使用者への説明用で、プログラム実行時に型チェックがされるわけではない。
* `-> SimpleITK.Image` も関数アノテーションであり、関数`predict`の返り値が`SimpleITK.Image`型であることを説明しています。
:::

4.1.3項で確認したinference.pyの(1)モデル読み込み部 と (2)推論実施部 を、process.pyの(1), (2)に移植しました。

```Python:process.py(移植後)

import SimpleITK
import numpy as np
import torch
import monai
from skimage import transform
from scipy.special import expit

from evalutils import SegmentationAlgorithm
from evalutils.validators import (
    UniquePathIndicesValidator,
    UniqueImagesValidator,
)


class Vesselsegmentationcontainer(SegmentationAlgorithm):
    def __init__(self):
        super().__init__(
            validators=dict(
                input_image=(
                    UniqueImagesValidator(),
                    UniquePathIndicesValidator(),
                )
            ),
        )
                
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        
        self.model = monai.networks.nets.UNet(
            dimensions=2,
            in_channels=3,
            out_channels=1,
            channels=(16, 32, 64, 128, 256),
            strides=(2, 2, 2, 2),
            num_res_units=2,
        ).to(self.device)
        
        self.model.load_state_dict(torch.load("./best_metric_model_segmentation2d_dict.pth"))


    def predict(self, *, input_image: SimpleITK.Image) -> SimpleITK.Image:
        # Read the image
        image = SimpleITK.GetArrayFromImage(input_image)
        image = np.array(image)
        shape = image.shape

        # Pre-process the image
        image = transform.resize(image, (512, 512), order=3)
        image = image.astype(np.float32) / 255.
        image = image.transpose((2, 0, 1))
        image = torch.from_numpy(image).to(self.device).reshape(1, 3, 512, 512)
        
        # Do the forward pass
        out = self.model(image).squeeze().data.cpu().numpy()
        
        # Post-process the image
        out = transform.resize(out, shape[:-1], order=3)
        out = (expit(out) > 0.99)
        out = (out * 255).astype(np.uint8)
        
        out = SimpleITK.GetImageFromArray(out)
        return out


if __name__ == "__main__":
    Vesselsegmentationcontainer().process()

```

(1)の移植について
* (1)移植部で `torch`、`monai`に関するモジュールインポートが必要なため、`import torch`、`import monai`を追加します。
* 変数`device`、`model`は、クラス内の別の関数でも読み出せるように、`self.device`、`self.model`にします。

(2)の移植について
* `interference.py`では`Image`となっていた変数は、`process.py`は`predict`関数の引数`input_image`として与えられるので書き換えます。
* `ax[0].` から始まる3行はグラフ表示用なので移植しません。
* `out = SimpleITK.GetImageFromArray(out)`を追加し、`return out`とします。
* (2)移植部で `transform`、`expit` を使うため、`from skimage import transform`、`from scipy.special import expit` を追加します。
* 変数`device`、`model`は、`self.device`、`self.model`にする。

:::note
ローカルPCにGPU環境がない場合
以下のコードを書き換えます。
```Python:process.py(修正前)
        self.model.load_state_dict(torch.load("./best_metric_model_segmentation2d_dict.pth"))
```

```Python:process.py(修正後)
        self.model.load_state_dict(
            torch.load(
                "./best_metric_model_segmentation2d_dict.pth",
                map_location=torch.device('cpu')
            )
        )
```
:::

### 4.3.2 Dockerfileとrequirements.txt 修正
#### 4.3.2.1 Dockerfileの確認
evalutilsに生成されたDockerfileの中身を確認します。
```Dockerfile:Dockerfile
FROM python:3.9-slim 

RUN groupadd -r algorithm && useradd -m --no-log-init -r -g algorithm algorithm
RUN mkdir -p /opt/algorithm /input /output \
    && chown algorithm:algorithm /opt/algorithm /input /output
USER algorithm
WORKDIR /opt/algorithm
ENV PATH="/home/algorithm/.local/bin:${PATH}"
RUN python -m pip install --user -U pip

COPY --chown=algorithm:algorithm requirements.txt /opt/algorithm/
RUN python -m pip install --user -rrequirements.txt
COPY --chown=algorithm:algorithm process.py /opt/algorithm/
ENTRYPOINT python -m process $0 $@
```
:::note
登場するコマンド（各行の冒頭部）の意味は以下の通り。([参考](https://qiita.com/soushiy/items/0945bcbc7ecce4822985))
| ｛命令｝ | 用途 |
|:-|:-|
|FROM|元となるDockerイメージの指定|
|RUN|コマンドの実行|
|USER|実行ユーザーの指定|
|WORKDIR|作業ディレクトリの指定|
|ENV|環境変数の指定|
|COPY|Dockerイメージのファイルシステム上で使いたいファイルをコピー|
|ENTRYPOINT|コンテナーの実行コマンド|
:::
このDokerfileは以下のようなことをしていると理解しました。
* 基となるDockerイメージとして、[python:3.9-slim](https://hub.docker.com/_/python)を指定
* グループユーザー名 algorithm、ユーザー名 algorithm を作成し、このユーザーで操作を行う
* 下記のフォルダを作成し、/opt/algorithm　をワーキングディレクトリとする。
* requirements.txtを/opt/algorithmにコピーして実行し、必要な依存モジュールをインストールする
* process.pyを/opt/algorithmにコピーして実行する
```:Dockerイメージ内で作成されるフォルダ構成
./ 
 ├ opt/ 
 │ └─ algorithm/ 
 ├ input/ 
 └ output/
```

#### 4.3.2.2 Dockerfileの修正

* 1行目の`FROM python:3.9-slim`を、`FROM pytorch/pytorch `に書き換えます。
    * `From`はDockerイメージのベースを指定しています。もとはpythonが動作するイメージをベースにしていましたが、pytorchがプレインストールされたイメージに変更します。

* 学習済みの重みファイルをDockerが構築する仮想環境内で使えるようにします。
    * drive-vessels-unetのファイル構成（4.1.2項で確認したもの）に含まれていた、best_metric_model_segmentation2d_dict.pth が学習済みの重みファイルです。これを、4.2.2項で確認したVesselSegmentationContainerフォルダ直下にコピーします。（普通にWindows エキスプローラでコピーしました）
    * Dockerfileに以下のコマンドを追加します。場所は`COPY --chown=algorithm:algorithm process.py /opt/algorithm/`と`ENTRYPOINT python -m process $0 $@`の間です。
    
```Dockerfile:Dockerfile(追記部のみ抜粋)
COPY --chown=algorithm:algorithm best_metric_model_segmentation2d_dict.pth /opt/algorithm/best_metric_model_segmentation2d_dict.pth 
```

#### 4.3.2.3 requirements.txt の確認
evalutilsに生成されたrequirements.txt の中身は以下のようになっていました。

```text
evalutils==0.3.1
scikit-learn==0.24.2
scipy==1.6.3
```

#### 4.3.2.4 requirements.txt の修正・追加
4.3.1項で移植したプログラムを動かすのに必要なモジュールをrequirements.txtに追加します。
* monaiとskimageの追加が必要です。skimageはscikit-imageです。
* torch(Pytorch)も必要ですが、Dockerのベースイメージ指定を`FROM pytorch/pytorch `としたためrequirements.txtでは指定不要になります。
:::note
evalutilsに生成されたrequirements.txt では、バージョンが完全指定(==)されていましたが、>=に書き換えました。
あとで、Google Colabの環境でもこのrequirement.txtを使って環境設定します。その際に、Google Colabではscikit-learn、scipy、scikit-imageなどは最新版を使っており、古いバージョンを指定すると毎回ランタイムの再起動が必要となり、やや面倒に感じたためです。
:::

```text
evalutils>=0.3.1
scikit-learn>=0.24.2
scipy>=1.6.3
monai>=0.4.0
scikit-image>=0.18.1
```

### 4.3.3 build.sh でdockerコンテナのビルド試行

#### 4.3.3.1 build.sh の確認
evalutilsに生成されたbuild.sh の中身は以下のようになっていました。
変更の必要はありませんでしたが、理解のために中身を確認しておきます。

```bash:build.sh
#!/usr/bin/env bash
SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"

docker build -t vesselsegmentationcontainer "$SCRIPTPATH"
```
主なコマンドは、`docker build`だけです。
* `#!/usr/bin/env bash`はこのファイルがbashで実行されることを指定しています。
* `SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"`は、このファイルのディレクトリ名を`SCRIPTPATH`に入れています。
	* スクリプトファイルで`$( コマンド )`と書くと、コマンドが実行された結果がそこに代入されます。
	* `$0`はスクリプトファイル（今回はbuild.sh）のパスです。
	* `cd`、`dirname`、`pwd`はそれぞれコマンドです。
* 'docker build (パス)'で(パス)にあるDockerfileとコンテキストを使ってDockerイメージを構築します。`-t vesselsegmentationcontainer`のオプションにより、構築されたDockerイメージはvesselsegmentationcontainerという名前になります。
 
#### 4.3.3.2 build.sh の実行
PowerShell上でWSLを起動させてから、build.sh を実行しました。

```PowerShell:PowerShell
PS C:\Users\(ユーザー名)> wsl
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

(ユーザー名)@(PC名):/mnt/c/Users/(ユーザー名)$ ./git_work/VesselSegmentationContainer/build.sh
```
a

:::note warn
はじめ、PowerShell上でwslを立ち上げずにコマンドを実行してしまい、別のコマンド画面が立ち上がって一瞬だけエラーが映って消えてしまう現象にはまりました。一瞬すぎてエラーが読めないですし、挙動もおかしいです。

この問題は、PowerShell上でwslを立ち上げてからshファイルを実行することで解決できました。PowerShell上でwslを使わずに、build.shを実行すると、gitbashという別のプロンプトが立ち上がるようです。gitbashはdockerと連携していないためエラーとなり、さらにgitbashが終了するとウインドウも消えてしまうことが原因だったようです。

<details><summary>なお、一瞬だけ見えるエラーをスクリーンキャプチャで取ると以下のようなエラー文でした。</summary>

error during connect: This error may indicate that the docker daemon is not running.; Post"http://%2F%2Fpipe%2Fdocker_engine/v1.24/build?buildargs=%7B%7D&cashefrom=%5B%5D&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels=%7B%7D&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&t=vesselsegmentationcontainer&target=&ulimits=null&version=1": open //./pipe/docker_engine: The system cannot find the file specified/

</details>

:::

::: note warn
Ubuntuがインストールされていない状態で、wsl上でbuild.shを実行すると、dockerというコマンドが見つからない、というエラーが発生しました。この解決法は3.4項の「UbuntuのインストールとDockerの設定」の通りです。
:::

うまくいけば、以下のような途中経過が出てから完了となります。

```PowerShell:PowerShell
(ユーザー名)@(PC名):/mnt/c/Users/(ユーザー名)$ ./git_work/VesselSegmentationContainer/build.sh
[+] Building 126.5s (14/14) FINISHED
 => [internal] load build definition from Dockerfile                                                               2.0s
 => => transferring dockerfile: 735B                                                                               0.3s
 => [internal] load .dockerignore                                                                                  2.5s
 => => transferring context: 2B                                                                                    0.1s
 => [internal] load metadata for docker.io/pytorch/pytorch:latest                                                  6.3s
 => [internal] load build context                                                                                  1.3s
 => => transferring context: 6.54MB                                                                                0.2s
 => [1/9] FROM docker.io/pytorch/pytorch@sha256:9904a7e081eaca29e3ee46afac87f2879676dd3bf7b5e9b8450454d84e074ef0   0.0s
 => CACHED [2/9] RUN groupadd -r algorithm && useradd -m --no-log-init -r -g algorithm algorithm                   0.0s
 => CACHED [3/9] RUN mkdir -p /opt/algorithm /input /output     && chown algorithm:algorithm /opt/algorithm /inpu  0.0s
 => CACHED [4/9] WORKDIR /opt/algorithm                                                                            0.0s
 => CACHED [5/9] RUN python -m pip install --user -U pip                                                           0.0s
 => [6/9] COPY --chown=algorithm:algorithm requirements.txt /opt/algorithm/                                        2.4s
 => [7/9] RUN python -m pip install --user -rrequirements.txt                                                     95.4s
 => [8/9] COPY --chown=algorithm:algorithm process.py /opt/algorithm/                                              2.7s
 => [9/9] COPY --chown=algorithm:algorithm best_metric_model_segmentation2d_dict.pth /opt/algorithm/best_metric_m  2.3s
 => exporting to image                                                                                            12.3s
 => => exporting layers                                                                                           11.3s
 => => writing image sha256:bf7813a3085923a6b3ec9b1980064825fc1c4f8b5324dc7ae22ef98d7881b5ec                       0.1s
 => => naming to docker.io/library/vesselsegmentationcontainer                                                     0.1s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
```

:::note
初回のbuild.shの実行は10分以上掛かりました。2回目以降はキャッシュを使うため数秒で終わります。
:::


### 4.3.4. test.sh でdockerが構築した仮想環境上で推論が行えるかの確認

test.sh でdockerが構築した仮想環境上で推論が行えるかをチェックします。提出前に正しく動くかのチェックです。


#### 4.3.4.1 test.shの確認・修正
evalutilsに生成されたtest.sh の中身は以下のようになっていました。

```bash:test.sh
#!/usr/bin/env bash

SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"

./build.sh

VOLUME_SUFFIX=$(dd if=/dev/urandom bs=32 count=1 | md5sum | cut --delimiter=' ' --fields=1)
MEM_LIMIT="4g"  # Maximum is currently 30g, configurable in your algorithm image settings on grand challenge

docker volume create vesselsegmentationcontainer-output-$VOLUME_SUFFIX

# Do not change any of the parameters to docker run, these are fixed
docker run --rm \
        --memory="${MEM_LIMIT}" \
        --memory-swap="${MEM_LIMIT}" \
        --network="none" \
        --cap-drop="ALL" \
        --security-opt="no-new-privileges" \
        --shm-size="128m" \
        --pids-limit="256" \
        -v $SCRIPTPATH/test/:/input/ \
        -v vesselsegmentationcontainer-output-$VOLUME_SUFFIX:/output/ \
        vesselsegmentationcontainer

docker run --rm \
        -v vesselsegmentationcontainer-output-$VOLUME_SUFFIX:/output/ \
        python:3.9-slim cat /output/results.json | python -m json.tool

docker run --rm \
        -v vesselsegmentationcontainer-output-$VOLUME_SUFFIX:/output/ \
        -v $SCRIPTPATH/test/:/input/ \
        python:3.9-slim python -c "import json, sys; f1 = json.load(open('/output/results.json')); f2 = json.load(open('/input/expected_output.json')); sys.exit(f1 != f2);"

if [ $? -eq 0 ]; then
    echo "Tests successfully passed..."
else
    echo "Expected output was not found..."
fi

docker volume rm vesselsegmentationcontainer-output-$VOLUME_SUFFIX
```

* build.shを呼び出してDockerイメージを構築しています。
	* build.sh内ではvesselsegmentationcontainerというDockerイメージ名で構築されます。
* 変数'VOLUME_SUFFIX'にランダムな文字列を生成し、`docker volume create`で vesselsegmentationcontainer-output-(ランダムな文字列) というDocker ボリューム（保存領域）を作成しています。
	* Dockerボリュームについて→ [参考サイト](https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32)
* 構築したDockerイメージ vesselsegmentationcontainer をコンテナとして実行(run)しています。
	* `-v ホスト側ソース:コンテナ側送信先`オプションでボリュームをマウントしています。
	* Dockerイメージ vesselsegmentationcontainer は、Dockerfileに従って process.pyが実行されます。
		* コンテナのinputフォルダ（ホストのtestフォルダがマウントされている）からファイルを読み込みます。
		* コンテナのoutputフォルダ（Dockerボリュームのvesselsegmentationcontainer-output-(ランダムな文字列)がマウントされています）に結果ファイルを書き込みます。
* 2つめのDocker runでは、/output/results.jsonファイルに対して改行・インデントなど綺麗に整形しています（[参考Webサイト](https://maku77.github.io/python/io/json-tool.html)）。
	* python:3.9-slim は既存のDockerイメージ。pythonが動く最低限の環境
	* `コマンド1 | コマンド2` バイプ「|」は、複数のコマンドをつなぎ、標準出力を次のコマンドに渡す。
	* `cat (ファイル名)`はファイル名の中身を標準出力する。
	* `python -m json.toolでjsonファイルを整形します。
* 3つめのDocker runでは、/output/results.jsonファイルと、/input/expected_output.jsonを比較しています。
	* /input/expected_output.jsonはあらかじめ用意しておく正解ファイルです。

:::note
あとでエラーが出たため、１か所書き換えました。
inputフォルダ内のすべてのフォルダを読み込んでしまうため、読み込みたい画像だけが入ったフォルダを指定します。（[参考](https://github.com/comic/evalutils/issues/310)）
```bash:(該当部修正前)
docker run --rm \
	(途中省略)
        -v $SCRIPTPATH/test/:/input/ \
	(省略)
```

```bash:(該当部修正前)
docker run --rm \
	(途中省略)
        -v $SCRIPTPATH/test/input/:/input/ \
	(省略)
```
:::

#### 4.3.4.2 推論対象のテスト画像の準備


evalutilsで生成したVesselSegmentationContainer/testフォルダにインプット画像などを入れます。
VesselSegmentationContainer/testフォルダには、もともと以下の３つのファイルが入っています。

```Text
VesselSegmentationContainer/
└ test/
	├ 1.0.000.000000.0.00.0.0000000000.0000.0000000000.000.mhd
	├ 1.0.000.000000.0.00.0.0000000000.0000.0000000000.000.zraw
	└ expected_output.json
```
このうち、mhdとzrawは削除します。

4.1項でコピーしてきた元のアルゴリズムにテスト画像が入っています。
https://github.com/DIAGNijmegen/drive-vessels-unet/blob/master/VesselSegmentation/test/input/01_test.tif
この画像を以下の位置にコピーします。
```Text
VesselSegmentationContainer/
└ test/
	├ input/
		└01_test.tif
	└ expected_output.json
```


expected_output.jsonは以下の通り書き換えます。

```json:expected_output.json
[
    {
        "outputs": [
            {
                "type": "metaio_image",
                "filename": "01_test.tif"
            }
        ],
        "inputs": [
            {
                "type": "metaio_image",
                "filename": "01_test.tif"
            }
        ],
        "error_messages": []
    }
]
```
#### 4.3.4.3 test.shの実行
PowerShell上でWSLを起動させてから、test.sh を実行します。

```PowerShell:PowerShell
(ユーザー名)@(PC名):/mnt/c/Users/(ユーザー名)$ cd git_work/VesselSegmentationContainer/
(ユーザー名)@(PC名):/mnt/c/Users/(ユーザー名)/git_work/VesselSegmentationContainer$ ./test.sh
```

:::note warn
cdせずに``のようにtest.shを実行すると以下のようなエラーが出ました。

```PowerShekk:PowerShell
./git_work/VesselSegmentationContainer/test.sh: line 5: ./build.sh: No such file or directory
```
cdしてからtest.shを実行することで解決しました。

:::

:::note warn
以下のエラーが発生しました。
RuntimeError: Attempting to deserialize object on a CUDA device but torch.cuda.is_available() is False. If you are running on a CPU-only machine, please use torch.load with map_location=torch.device('cpu') to map your storages to the CPU.
解決方法は4.3.1項「ローカルPCにGPU環境がない場合」の通りです。
:::

:::note warn
はじめ、[Video tutorial](https://grand-challenge.org/documentation/create-your-own-algorithm/)に従って、以下のフォルダ構成で実行したところエラーが出ました。

```Text:Video tutorialに従ったフォルダ構成
VesselSegmentationContainer/
└ test/
	├ 01_test.tif
	└ expected_output.json
```

以下のエラーが発生しました（長いので抜粋）

```Text
(前略)
HDF5-DIAG: Error detected in HDF5 (1.10.6) thread 0:
  #000: /tmp/SimpleITK-build/ITK/Modules/ThirdParty/HDF5/src/itkhdf5/src/H5F.c line 370 in itk_H5Fis_hdf5(): unable open file
    major: File accessibilty
    minor: Not an HDF5 file
  #001: /tmp/SimpleITK-build/ITK/Modules/ThirdParty/HDF5/src/itkhdf5/src/H5Fint.c line 830 in itk_H5F__is_hdf5(): unable to locate file signature
    major: File accessibilty
    minor: Not an HDF5 file
  #002: /tmp/SimpleITK-build/ITK/Modules/ThirdParty/HDF5/src/itkhdf5/src/H5FDint.c line 126 in itk_H5FD_locate_signature(): unable to read file signature
    major: Low-level I/O
    minor: Unable to initialize object
  #003: /tmp/SimpleITK-build/ITK/Modules/ThirdParty/HDF5/src/itkhdf5/src/H5FDint.c line 205 in itk_H5FD_read(): driver read request failed
    major: Virtual File Layer
    minor: Read failed
  #004: /tmp/SimpleITK-build/ITK/Modules/ThirdParty/HDF5/src/itkhdf5/src/H5FDsec2.c line 725 in H5FD_sec2_read(): file read failed: time = Mon Mar 21 02:13:13 2022
, filename = '/input/expected_output', file descriptor = 4, errno = 21, error message = 'Is a directory', buf = 0x7fff1d10c7a8, total read size = 8, bytes this sub-read = 8, bytes actually read = 18446744073709551615, offset = 0
    major: Low-level I/O
    minor: Read failed
Could not load expected_output using <evalutils.io.SimpleITKLoader object at 0x7fad55844c10>.
(中略)
```

この原因はinputフォルダ（testフォルダをマウント）内のすべてのファイルを読み込んで推論しようとするためでした。4.3.4.2項の通りinputフォルダを分けて、4.3.4.1項の通りtest.shを修正することで解消しました。
:::

:::note warn
次のエラーが出ました。
```Text
./test.sh: line 27: python: command not found
```
test.shでは、Dockerイメージ「python:3.9-slim」を使ってPythonのコマンド・プログラムを動かしますが、「python:3.9-slim」のなかでPythonがうまく起動しなかったようです。以下の対応で動くようになりました。
* Dockerイメージを「python:3.9-slim」から「python:3」に変えてみた。
* コマンドを「python」から「python3」に変えてみた。

```bash:test.sh(抜粋:修正前)
docker run --rm \
	(省略)
        python:3.9-slim cat /output/results.json | python -m json.tool

docker run --rm \
	(省略)
        python:3.9-slim python -c "import json, sys; f1 = json.load(open('/output/results.json')); f2 = json.load(open('/input/expected_output.json')); sys.exit(f1 != f2);"

```

```bash:test.sh(抜粋:修正後)
docker run --rm \
	(省略)
        python:3 cat /output/results.json | python3 -m json.tool
docker run --rm \
	(省略)
        python:3 python3 -c "import json, sys; f1 = json.load(open('/output/results.json')); f2 = json.load(open('/input/expected_output.json')); sys.exit(f1 != f2);"
```
:::

:::note
test.shでは、推論結果はDockerボリュームの「vesselsegmentationcontainer-output-(ランダムな文字列)」に保存されますが、test.shが完了すると`docker volume rm`によりDockerボリュームは削除されてしまいます。よって、本当に推論結果が出力されているのか、わかりづらいです。
そこで、test.shを以下のように変えると、出力結果はDockerボリュームではなく、ホストPCのフォルダに保存され、test.shの完了後に出力結果のファイルを確認できます。
```bash:test2.sh(test.shの変更版)
#!/usr/bin/env bash

SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"

./build.sh

VOLUME_SUFFIX=$(dd if=/dev/urandom bs=32 count=1 | md5sum | cut --delimiter=' ' --fields=1)
MEM_LIMIT="4g"  # Maximum is currently 30g, configurable in your algorithm image settings on grand challenge

# docker volume create vesselsegmentationcontainer-output-$VOLUME_SUFFIX

# Do not change any of the parameters to docker run, these are fixed
docker run --rm \
        --memory="${MEM_LIMIT}" \
        --memory-swap="${MEM_LIMIT}" \
        --network="none" \
        --cap-drop="ALL" \
        --security-opt="no-new-privileges" \
        --shm-size="128m" \
        --pids-limit="256" \
        -v $SCRIPTPATH/test/input/:/input/ \
        -v $SCRIPTPATH/test/output/:/output/ \
        vesselsegmentationcontainer

docker run --rm \
        -v $SCRIPTPATH/test/output/:/output/ \
        python:3 cat /output/results.json | python3 -m json.tool

echo CHECKPOINT

docker run --rm \
        -v $SCRIPTPATH/test/output/:/output/ \
        -v $SCRIPTPATH/test/:/input/ \
        python:3 python3 -c "import json, sys; f1 = json.load(open('/output/results.json')); f2 = json.load(open('/input/expected_output.json')); sys.exit(f1 != f2);"

if [ $? -eq 0 ]; then
    echo "Tests successfully passed..."
else
    echo "Expected output was not found..."
fi

#docker volume rm vesselsegmentationcontainer-output-$VOLUME_SUFFIX

```
:::

### 4.3.5. export.sh によるdockerコンテナの提出形態ファイル（.tar.gz）の作成

#### 4.3.5.1 export.shの中身の確認
evalutilsに生成されたexport.sh の中身は以下のようになっていました。

```bash:expose.sh
#!/usr/bin/env bash
./build.sh
docker save vesselsegmentationcontainer | gzip -c > VesselSegmentationContainer.tar.gz
```
* build.shを実行し、Dockerイメージ「vesselsegmentationcontainer」を構築しています。
* `docker save`コマンドにより、Dockerイメージ「vesselsegmentationcontainer」をtar.gzファイルとして保存しています。

#### 4.3.5.2 export.shの実行
PowerShell上でWSLを起動させてから、export.sh を実行します。
```PowerShell:PowerShell
(ユーザー名)@(PC名):/mnt/c/Users/(ユーザー名)/git_work/VesselSegmentationContainer$ ./export.sh
```
完了すると、VesselSegmentationContainer.tar.gz ファイルがexport.shと同じフォルダに保存されています。
このファイルをGrand challengeに提出すればよいはずです。

:::note warn
私の環境では実行に3時間くらい掛かりました。
また出力ファイルも3GBくらいありました。
:::

ここまでで、Grand Challengeへの提出物の例を作るのは一段落です。次の記事では、今回の推論プログラムの例に至る前の学習プログラムをGoogle Colab上で動かしてみます。