複数の読影者と STAPLE で CT ボリュームをセグメンテーションする
Potato で三人の読影者に同じ NIfTI の CT スキャンを与え、各読影者の脾臓マスクをエクスポートし、SimpleITK で STAPLE コンセンサスに対して各読影者を採点します。
3D Slicer や ITK-SNAP で CT スキャンをセグメンテーションしている医用画像の研究者は、このチュートリアルに従って、Potato 2.13.1 で三人の読影者に同じスキャンを与え、読影者間の 3D Dice 一致度を Potato で確認し、各読影者のマスクを NIfTI ラベルボリュームとしてエクスポートし、STAPLE でコンセンサスマスクを作成できます。STAPLE(Simultaneous Truth and Performance Level Estimation)は、複数の読影者のマスクから、そのコンセンサスに対する各読影者の感度と特異度とともに、コンセンサスのセグメンテーションを推定します。Dice 係数は二つのマスクの重なりを測り、共有するボクセルがない場合の 0 から、同一のマスクの場合の 1 までの値をとります。
Potato 2.13.1 は、読影者間の Dice を計算し、読影者が三人以上の場合は、各読影者の STAPLE の感度と特異度を計算しますが、コンセンサスマスクはエクスポートしません。このチュートリアルのコンセンサスマスクは、Potato がエクスポートしたマスクに対する SimpleITK で作成します。実例では、Medical Segmentation Decathlon の三つの CT スキャンで脾臓をセグメンテーションします。このページの数値は、それらのスキャンに対する Potato 2.12.1 での実行によるもので、一致度レポートは、保存されたマスクに対して Potato 2.13.1 で再実行しました。Potato は、臨床上または規制上の承認を持たない研究用ソフトウェアであり、このページのいかなる内容も、患者のケアのためのものではありません。
スキャン
Medical Segmentation Decathlon(MSD)は、ベンチマーク用の十のセグメンテーションタスクを公開しました(Antonelli et al., 2022)。Task09 は脾臓で、Memorial Sloan Kettering Cancer Center の 61 の CT ボリュームがあります。41 の学習用ボリュームには専門家の脾臓マスクが付いており、20 のテスト用ボリュームには付いていません。MSD のサイトとタスクの dataset.json は、どちらもライセンスを CC BY-SA 4.0 としています。CC BY-SA 4.0 の下では、帰属表示を行えばスキャンを共有、改変でき、改変物は同じライセンスで共有する必要があります。
Task09 は、1.6 GB の一つのアーカイブとしてダウンロードされます。このチュートリアルでは、最小クラスの学習用スキャン三つ(合わせて約 30 MB)を使い、後でコンセンサスを確認するための正解キーとして、専門家のマスクは別に取っておきます。
curl -O https://msd-for-monai.s3-us-west-2.amazonaws.com/Task09_Spleen.tar
tar -xf Task09_Spleen.tar \
Task09_Spleen/imagesTr/spleen_3.nii.gz Task09_Spleen/labelsTr/spleen_3.nii.gz \
Task09_Spleen/imagesTr/spleen_8.nii.gz Task09_Spleen/labelsTr/spleen_8.nii.gz \
Task09_Spleen/imagesTr/spleen_56.nii.gz Task09_Spleen/labelsTr/spleen_56.nii.gz各スキャンは 1 スライスあたり 512 × 512 ボクセルで、スライス厚は 5.0 から 7.5 mm、脾臓は 9 から 19 枚の軸位断スライスにわたります。
| スキャン | スライス数 | ピクセルサイズ(mm) | スライス厚(mm) | 脾臓のあるスライス |
|---|---|---|---|---|
| spleen_3 | 40 | 0.74 | 5.0 | 14 |
| spleen_8 | 31 | 0.79 | 7.5 | 19 |
| spleen_56 | 33 | 0.70 | 7.0 | 9 |
ボリューム対応版の Potato のインストール
ボリュームビューアーは、nibabel で NIfTI を、pydicom で DICOM を読み込み、highdicom で DICOM SEG を書き出します。volumes エクストラは、この三つのライブラリをインストールします。
pip install 'potato-annotation[volumes]==2.13.1'Potato は、ビューアーのスクリプトとスタイルを、インストールされたパッケージから配信します。テスト実行でのアノテーションページ上のヘッドレスブラウザは、他のどのホストにもリクエストを送りませんでした。パッケージとスキャンがマシンに入れば、チュートリアル全体がネットワーク接続なしで動作し、その設定はエアギャップ環境でのデプロイで説明されています。
プロジェクトフォルダー
三つのスキャンを media/ フォルダーに置き、データファイルに一行につき一つのケースを列挙します。
spleen-readers/
├── config.yaml
├── data/
│ └── cases.jsonl
└── media/
├── spleen_3.nii.gz
├── spleen_8.nii.gz
└── spleen_56.nii.gzvolume は media/ からの相対でスキャンを指定し、text はビューアーの上に表示されるキャプションです。
{"id": "spleen_3", "volume": "spleen_3.nii.gz", "text": "MSD Task09 spleen_3"}
{"id": "spleen_8", "volume": "spleen_8.nii.gz", "text": "MSD Task09 spleen_8"}
{"id": "spleen_56", "volume": "spleen_56.nii.gz", "text": "MSD Task09 spleen_56"}volume の値には、一つの DICOM シリーズを収めたフォルダーも指定できます。ファイル名とフォルダー名はブラウザに届くため、ケースのファイルとフォルダーには仮名の名前を付けてください。
設定
下の設定は完全なもので、2.12.1 と 2.13.1 の両方で python -m potato.validate_cli config.yaml --strict を通ります。
annotation_task_name: Spleen segmentation, three readers
task_dir: .
output_annotation_dir: annotation_output/
media_directory: media
data_files:
- data/cases.jsonl
item_properties:
id_key: id
text_key: text
user_config:
allow_all_users: true
users: []
# Every reader segments every case
num_annotators_per_item: 3
annotation_schemes:
- annotation_type: volume_annotation
name: spleen
description: Paint the spleen on every axial slice that shows the spleen
source_field: volume
labels:
- {name: Spleen, color: "#e6553a", key_value: "1"}
# Boxes are saved as separate shapes and never become mask voxels
tools: [brush, eraser]
brush_size: 6
# A NIfTI file has no modality header, so the CT windows are named here
window_presets:
- {name: Abdomen, center: 50, width: 400}
- {name: Liver, center: 30, width: 150}
default_window: Abdomen三つの設定には理由があります。
tools: [brush, eraser]。 ビューアーにはボックスとポリゴンのツールもあります。ボリュームビューアーのボックスは別の図形として保存され、マスクのボクセルにはならず、Dice のスコアにも NIfTI ラベルボリュームにも反映されません。Potato 2.13.1 からは、ポリゴンを閉じると、そのスライス上でポリゴンの領域がマスクに塗られ、Potato 2.12.1 はポリゴンを図形として保存していました。この実行の読影者はブラシを使いました。ナビゲートツールは常に追加されます。brush_size: 6。 ブラシの半径はボクセル単位で、デフォルトは 2、スライダーでは 15 までです。これらのスキャンの 0.7 から 0.8 mm のピクセルでは、半径 6 は幅約 9 mm の円板を塗ります。window_presets。 Potato は、ヘッダーが CT と示す DICOM シリーズにのみ、組み込みの CT ウィンドウを提供します。NIfTI ファイルにはそのようなヘッダーがなく、プリセットがないと、ビューアーは spleen_3 を、中心 -380、幅 1,287 の自動ウィンドウで開きます。自動ウィンドウは 1,287 ハウンズフィールド単位をグレースケール全体に広げるため、脾臓と隣接する臓器との間のコントラストがほとんど残りません。二つのプリセットは、Radiopaedia の腹部軟部組織ウィンドウと肝臓ウィンドウです。
スキャンの読影
プロジェクトフォルダーでサーバーを起動します。
potato start config.yaml -p 8000各読影者はサーバーのアドレスを開き、ユーザー名とパスワードで登録して、三つのケースすべてを受け取ります。num_annotators_per_item: 3 が、すべてのケースを三人の読影者に与えるためです。ビューアーは、放射線学の慣例に従って、軸位断、冠状断、矢状断のビューを表示し、患者の右側がビューアーの左側になります。ウィンドウメニューは Abdomen (50/400) で開き、Liver (30/150) と自動ウィンドウも一覧にあります。
b でブラシを、1 で Spleen ラベルを選び、脾臓をスライスごとに塗ります。冠状断または矢状断のビューでのストロークは、そのストロークが横切るすべての軸位断スライスに書き込まれ、消しゴム(e)はボクセルを取り除きます。すべてのスライスを塗る必要はありません。脾臓の最初と最後の軸位断スライスと、その間で輪郭が変わる数枚のスライスを塗ってから、i を押すと、Potato の形状ベースの補間で間のスライスが埋められます。埋められたスライスは通常のマスクのボクセルで、元に戻すと、埋められたスライスは取り除かれます。「Next」をクリックするとケースが保存されます。
読影を始める前に、脾門部の血管が脾臓に含まれるかどうかといった書面の規則に合意し、すべての読影者に同じ写しを渡してください。下の一致度の数値は、読影者の注意深さと同じくらい、それらの規則を測定しています。途中で規則を追加すると、それ以前のケースは別の規則で読影されたままになります。
このページの裏付けとなったテスト実行
このページの数値は、動作中のプロジェクトを通じて作業し、ビューアーが送るのと同じリクエストで各ケースを保存した、三人のスクリプト化された読影者によるものです。各読影者のマスクは、MSD の専門家のマスクに、次の三つのいずれかの変更を加えて作りました。
- ana は、専門家の輪郭の 1 ボクセル内側で、すべてのスライスを塗りました。
- ben は、最初のスライス、最後のスライス、その間の 3 枚おきのスライスを正確に塗ってから、補間を押しました。spleen_3 では、ben は 14 枚のうち 6 枚を塗り、補間が残りの 8 枚を埋めました。
- cai は、専門家の輪郭の 3 ボクセル外側で、すべてのスライスを塗り、脾臓の最上部と最下部のスライスを見逃しました。
スクリプト化された読影者によって、結果を専門家のマスクと照合できます。人間の誤りは、三つのスクリプト化されたパターンより規則性が低くなります。
Potato での読影者間の一致度
Potato の一致度レポートは、volume_annotation のマスクを、ケースごと、ラベルごとに、読影者の各ペアについて 3D Dice で採点します。どちらの読影者もラベルを塗らなかったペアは、一致として数えられず、そのラベルについては除外されます。ブラウザで /admin を開き、プロジェクトフォルダーの admin_api_key.txt にある管理者キーを入力してから、/admin/iaa を開きます。テスト実行では、レポートは、それぞれ三人の読影者全員を持つ三つのケースを一覧し、次の値を示しました。
mean_dice 0.887
dice_by_label.Spleen 0.887
n_pairs_by_label.Spleen 90.887 は、ケースあたり三つ、9 ペアにわたる平均です。Potato 2.13.1 からは、レポートは読影者が三人以上のケースごとに STAPLE も実行し、各読影者の率をケースにわたって平均します。このページの保存されたマスクで実行すると、2.13.1 のレポートは次を追加しました。
staple.Spleen.sensitivity ana 0.959 ben 0.939 cai 0.935
staple.Spleen.specificity ana 1.000 ben 0.998 cai 0.944感度は、後述の SimpleITK の値の平均に等しくなります。200 万ボクセルを超えるボリュームでは、Potato は STAPLE を、すべての読影者のボクセルを囲むボックスの内側で実行するため、特異度はそのボックス内のボクセルだけを数え、SimpleITK によるスキャン全体の値より低くなります。レポートにはペアごとの Dice はありません。エクスポートされたマスクから計算すると、ペアは 0.954(spleen_3 の ana と ben)から 0.785(spleen_56 の ben と cai)の範囲にあり、cai を含むすべてのペアは、同じケースでの ana と ben より低いスコアでした。Dice は偶然補正されていません。Potato の偶然補正された幾何学的一致度は、2D 画像上のボックスとポリゴンを対象としており、ボリュームは採点しません。reliability エクスポートは選択式と評定の質問のみを対象とし、この設定で python -m potato.export -c config.yaml -f reliability -o export/ を実行すると、「No question type with a reliability coefficient」で停止します。
NIfTI ラベルボリュームのエクスポート
volume_nifti エクスポートは、読影者とケースごとに一つのラベルボリュームを書き出します。
python -m potato.export -c config.yaml -f volume_nifti -o export/export/labels__spleen.json
export/ana/spleen_3__spleen.nii.gz
export/ana/spleen_8__spleen.nii.gz
export/ana/spleen_56__spleen.nii.gz
export/ben/...
export/cai/...各 .nii.gz ファイルは、背景が 0、Spleen が 1 の uint8 ボリュームで、元のスキャンのボクセルの順序とアフィンを保持します。テスト実行では、エクスポートされたすべてのボリュームが、元のスキャンと同じ形状とアフィンを持っていました。したがって、マスクはスキャンとボクセル単位で一致します。labels__spleen.json は、値をラベル名に対応付けます。スキームに複数のラベルがある場合、二つのラベルが共有するボクセルは、スキームのリストで後のラベルになります。
DICOM シリーズでは、-f dicom_seg が、元のシリーズを参照する DICOM Segmentation を書き出します。リリースの CT ファントムの例では、エクスポートは、コンテンツの説明にアノテーターのユーザー名を入れた一つのバイナリ SEG を書き出しました。DICOM 規格は、SEG に元のシリーズの患者モジュールと検査モジュールをコピーすることを求めており、ファントムの SEG はファントムの患者名を保持していました。この理由から、SEG のエクスポートは識別可能なデータとして扱ってください。このチュートリアルの NIfTI スキャンでは、dicom_seg はファイルを書き出さず、volume_nifti を使うよう警告を表示します。
STAPLE コンセンサスと読影者ごとの Dice
下のスクリプトは、ケースごとに三人の読影者のマスクに対して SimpleITK の STAPLE フィルターを実行し、コンセンサス確率が 0.5 以上のボクセルを残し、コンセンサスを読影者のエクスポートの隣に書き出します。次にスクリプトは、読影者ごとの STAPLE の感度と特異度、およびコンセンサスに対する各読影者の Dice を表示します。pip install SimpleITK で SimpleITK をインストールし(テスト実行では 2.5.6 を使いました)、プロジェクトフォルダーでスクリプトを実行します。
from pathlib import Path
import SimpleITK as sitk
EXPORT = Path("export")
READERS = sorted(p.name for p in EXPORT.iterdir() if p.is_dir() and p.name != "staple")
CASES = sorted({f.name.split("__")[0] for r in READERS for f in (EXPORT / r).glob("*__spleen.nii.gz")})
(EXPORT / "staple").mkdir(exist_ok=True)
def dice(a, b):
overlap = sitk.LabelOverlapMeasuresImageFilter()
overlap.Execute(a, b)
return overlap.GetDiceCoefficient()
print(f"{'case':10} {'reader':6} {'sens':>6} {'spec':>8} {'dice':>6}")
summary = {}
for case in CASES:
masks = [sitk.ReadImage(str(EXPORT / r / f"{case}__spleen.nii.gz"), sitk.sitkUInt8) for r in READERS]
staple = sitk.STAPLEImageFilter()
staple.SetForegroundValue(1)
probability = staple.Execute(masks)
consensus = sitk.Cast(probability >= 0.5, sitk.sitkUInt8)
consensus.CopyInformation(masks[0])
sitk.WriteImage(consensus, str(EXPORT / "staple" / f"{case}__spleen.nii.gz"))
for reader, mask, sens, spec in zip(READERS, masks, staple.GetSensitivity(), staple.GetSpecificity()):
d = dice(mask, consensus)
summary.setdefault(reader, []).append(d)
print(f"{case:10} {reader:6} {sens:6.3f} {spec:8.5f} {d:6.3f}")
print("mean Dice against STAPLE:", {r: round(sum(v) / len(v), 3) for r, v in summary.items()})case reader sens spec dice
spleen_3 ana 0.966 1.00000 0.984
spleen_3 ben 0.948 0.99997 0.971
spleen_3 cai 0.960 0.99907 0.901
spleen_56 ana 0.951 1.00000 0.980
spleen_56 ben 0.921 0.99998 0.954
spleen_56 cai 0.860 0.99947 0.834
spleen_8 ana 0.960 1.00000 0.981
spleen_8 ben 0.948 0.99993 0.971
spleen_8 cai 0.985 0.99795 0.921
mean Dice against STAPLE: {'ana': 0.982, 'ben': 0.965, 'cai': 0.885}まず感度と Dice を読んでください。背景のボクセルは、spleen_8 では脾臓のボクセルの 74 倍、spleen_56 では 378 倍あります。したがって、どの読影者の特異度も 0.998 以上に丸められ、それでも小さな差は、各読影者が脾臓の外側をどれだけ塗ったかによって読影者を順位付けしています。cai は輪郭の外側を塗り、三つのスキャンすべてで最も低い特異度でした。spleen_56 での cai の感度は 0.860 で、表の中で最も低く、これは、cai が見逃した二枚のスライスが、わずか 9 枚にわたる脾臓の中でより大きな割合を占めたためです。ben の補間されたマスクは、すべてのケースで ana と cai の間のスコアでした。
これら三つのスキャンでは、STAPLE のコンセンサスは、三人中二人の多数決と同じボクセルの集合でした。読影者が三人で、他より大きく劣る読影者がいない場合、STAPLE の重みはどのボクセルの結果も変えませんでした。その場合、STAPLE は読影者ごとの感度と特異度を加えるものであり、コンセンサス自体は、読影者が増える場合や、不注意な読影者が注意深い読影者を数で上回る場合には、多数決と異なることがあります。
専門家のマスクに対するコンセンサスの確認
MSD の専門家のマスクによって、テスト実行を照合できます。各読影者のマスクと STAPLE コンセンサスを、専門家のマスクに対する Dice で採点しました。
| スキャン | ana | ben | cai | STAPLE コンセンサス |
|---|---|---|---|---|
| spleen_3 | 0.973 | 0.956 | 0.909 | 0.981 |
| spleen_8 | 0.975 | 0.957 | 0.925 | 0.983 |
| spleen_56 | 0.964 | 0.940 | 0.841 | 0.974 |
コンセンサスは、すべてのスキャンで、どの個々の読影者よりも専門家のマスクに対して高いスコアを示しました。ana は輪郭の 1 ボクセル内側を、cai は 3 ボクセル外側を描き、三人中二人のコンセンサスは、二つの輪郭の間に収まりました。逆方向のスクリプト化された誤りはコンセンサスに有利に働くため、実際の読影者を対象とする研究では、同じ結果を主張する前に、独自の専門家の基準が必要です。
CT セグメンテーションのための他のツール
3D Slicer は、一人による詳細なセグメンテーションのための無料のデスクトップアプリケーションです。Segment Editor の Fill between slices と Grow from seeds のエフェクトは、数枚の塗られたスライスやシードからセグメンテーションを完成させ、SlicerRT 拡張の Segment Comparison モジュールは、二つのセグメンテーションの間の Dice と Hausdorff 距離を計算します。ITK-SNAP は、半自動のアクティブコンターによる 3D セグメンテーションのための無料のデスクトップアプリケーションで、バージョン 4.4.0 で AI 機能が追加されました。MONAI Label は、AI 支援のラベル付けとアクティブラーニングのための Apache-2.0 のサーバーで、3D Slicer、MITK、OHIF などのビューアーから使われます。OHIF Viewer は、DICOMweb アーカイブから検査を読み込み、セグメンテーションのワークフローに対応する、MIT ライセンスのウェブビューアーです。
Potato は、同じケースを、自分で運用するサーバー上のブラウザを通じて複数の読影者に渡す場合や、調査が読影者間の一致度を必要とする場合に適しています。Slicer や ITK-SNAP のセグメンテーションツールを好む読影者は、Potato の NIfTI エクスポートをそれらで開けます。医用画像と臨床テキストのアノテーションツールの比較は、これらのツールを Potato と並べて、さらに詳しく比較しています。
CT セグメンテーションにおける Potato の限界
- Potato にはコンセンサスマスクがありません。 Potato 2.13.1 は、ペアごとの Dice と、読影者ごとの STAPLE の率を報告し、コンセンサスマスクはエクスポートされたマスクに対する SimpleITK で得られます。
- ペアごとの Dice はありません。
/admin/iaaは、すべてのペアにわたるラベルごとの平均 Dice を一つ示します。ペアごとの数値は、上のようにエクスポートから計算してください。 - ボリュームビューアーには対話型の AI ツールがありません。 マスクは、ブラシ、消しゴム、閉じたポリゴン、補間、項目で指定された RTSTRUCT の輪郭から得られ、バージョン 2.14 からは、調査自身のマスクで学習した
volume-unetモデルからも得られます。対話型のモデル支援 3D セグメンテーションは、MONAI Label、ITK-SNAP、OHIF が提供しています。 - 4D ボリューム、融合 PET/CT、斜め再構成はありません。 複数のボリュームを含む NIfTI ファイルは拒否され、斜めの撮像は、最も近い直交グリッドで表示されます。
- 匿名化はありません。 Potato は、設定に列挙されていない限り、DICOM ヘッダーのフィールドを読影者から隠し、ファイル、ファイル名、ピクセルはアップロードされたままにします。
次に読むもの
- ヘルスケアと臨床のアノテーション - Potato の臨床テキストと画像の機能を一か所にまとめたもの
- Potato による医用画像アノテーション - DICOM シリーズ、RTSTRUCT の初期マスク、DICOM ヘッダーのフィルタリングについて
- 医用画像と臨床テキストのアノテーションツールの比較 - 3D Slicer、ITK-SNAP、MONAI Label、OHIF、有料プラットフォームと Potato の比較
- エクスポート形式 -
volume_nifti、dicom_seg、その他の形式について - タスク割り当て - Potato が読影者にケースを配分する方法について
- エアギャップ環境でのデプロイ - 外部への経路のないネットワークで Potato を実行するために
- ボリューム・点群・動画でモデルを学習する - Potato 2.14.0 の
volume-unetトレーナーで、読影者が塗ったマスクから 3D U-Net を学習するために
参考文献
- Antonelli, M., Reinke, A., Bakas, S., Farahani, K., Kopp-Schneider, A., Landman, B. A., Litjens, G., Menze, B., Ronneberger, O., Summers, R. M., van Ginneken, B., Bilello, M., Bilic, P., Christ, P. F., Do, R. K. G., Gollub, M. J., Heckers, S. H., Huisman, H., Jarnagin, W. R., et al. (2022). The Medical Segmentation Decathlon. Nature Communications, 13, 4128. https://doi.org/10.1038/s41467-022-30695-9
- Warfield, S. K., Zou, K. H., & Wells, W. M. (2004). Simultaneous truth and performance level estimation (STAPLE): An algorithm for the validation of image segmentation. IEEE Transactions on Medical Imaging, 23(7), 903-921. https://doi.org/10.1109/TMI.2004.828354