nnunet-segmentation について
nnunet-segmentationは、注釈付きデータを扱う生物医学画像の研究者や実務者向けのnnU-Net v2スキルです。自動設定から学習、推論までの手順を扱います。
主な機能
- 01ネットワーク構成と前処理の自動設定
- 022D・3Dセグメンテーション構成
- 03交差検証による学習とモデルアンサンブル
- 04コマンドラインとPythonでの推論
- 05予測結果の評価と可視化
活用シーン
- 01CTやMRIの注釈から臓器・病変モデルを学習
- 02事前学習済みnnU-Netモデルを新規画像に適用
- 03再現可能なセグメンテーションパイプラインの構築
- 04構成やアンサンブルの比較
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jaechang-hits/ sciagent-skills --skill nnunet-segmentationインストール後、Claude Code で/nnunet-segmentationを入力して読み込まれたか確認します。
詳細
nnU-Net による医用画像の自動セグメンテーション
概要
nnU-Net(no-new-Net)は、生物医学画像のセグメンテーションに対応した、自己設定型のディープラーニングフレームワークです。ラベル付きの学習データセットを与えると、nnU-Net は最適なネットワークアーキテクチャ(2D、3D フル解像度、または 3D カスケード)、前処理の手順(リサンプリング、正規化、パッチサイズ)、学習スケジュール、後処理を自動的に決定します。手動でハイパーパラメーターを調整しなくても、多様な画像モダリティや解剖学的構造において一貫して最先端の性能を達成します。nnU-Net v2(nnunetv2)は現在のリリースで、標準の CLI に加えて推論用の Python API を備えています。
使用する場面
- 20 件以上のアノテーション付き学習ケースがある場合に、CT または MRI スキャン内の解剖学的構造(臓器、腫瘍、病変)をセグメンテーションする
- 3D 蛍光顕微鏡または電子顕微鏡のボリューム画像で、細胞または核のセグメンテーションを自動化する
- U-Net を手動で調整せず、新しいセグメンテーション課題の強力なベースラインを確立する
- 公開されたチャレンジの事前学習済み nnU-Net モデルを使用して、新しい画像に対して推論を実行する
- セグメンテーション手法を比較する:nnU-Net の自動設定されたアンサンブルは、厳密なベースラインとして機能します
- 学習は一度だけ行い、多数の新しいケースに対して推論を繰り返す、本番用のセグメンテーションパイプラインを構築する
- アノテーション付き学習データなしで 2D 蛍光細胞セグメンテーションを行う場合は、代わりに Cellpose(
cellpose-cell-segmentation)を使用してください。nnU-Net にはアノテーション付き学習ケースが必要です - ディープラーニングの学習データを利用できない画像に、古典的なしきい値処理や領域拡張を用いたルールベースのセグメンテーションを行う場合は、代わりに SimpleITK(
simpleitk-image-registration)を使用してください
前提条件
- Python パッケージ:
nnunetv2>=2.2、torch>=2.0(GPU 学習には CUDA が必要です) - データ要件:NIfTI 形式(
.nii.gz)の画像、バイナリまたはマルチクラスのラベルマスク。学習ケースは最低 20 件を推奨(最高の性能には 50 件以上) - 環境変数:
nnUNet_raw、nnUNet_preprocessed、nnUNet_resultsを設定する必要があります - ハードウェア:学習には 8 GB 以上の VRAM を搭載した GPU を推奨します。CPU 推論もサポートされていますが、低速です
- 環境:Python 3.9 以降。Linux または macOS(Windows もサポートされていますが、公式には推奨されていません)
pip install nnunetv2
# Verify installation
nnUNetv2_train --help
# Set required environment variables (add to ~/.bashrc or ~/.zshrc)
export nnUNet_raw=/data/nnUNet_raw
export nnUNet_preprocessed=/data/nnUNet_preprocessed
export nnUNet_results=/data/nnUNet_results
mkdir -p $nnUNet_raw $nnUNet_preprocessed $nnUNet_results
クイックスタート
# Minimal end-to-end pipeline: convert dataset → preprocess → train → predict
# Assumes dataset is in Medical Segmentation Decathlon format
# 1. Set environment
export nnUNet_raw=/data/nnUNet_raw
export nnUNet_preprocessed=/data/nnUNet_preprocessed
export nnUNet_results=/data/nnUNet_results
# 2. Convert Medical Segmentation Decathlon dataset (dataset ID 7 = Pancreas)
nnUNetv2_convert_MSD_dataset -i /data/Task07_Pancreas -overwrite_id 7
# 3. Plan preprocessing and verify dataset integrity
nnUNetv2_plan_and_preprocess -d 7 --verify_dataset_integrity
# 4. Train 3D full-res model, fold 0 (of 5-fold cross-validation)
nnUNetv2_train 7 3d_fullres 0 --npz
# 5. Predict on new images
nnUNetv2_predict -i /data/test_images/ -o /data/predictions/ -d 7 -c 3d_fullres -f 0
echo "Segmentation predictions saved to /data/predictions/"
ワークフロー
ステップ 1:データセットを nnU-Net 形式で準備する
nnU-Net では、dataset.json 記述ファイルとともに、特定のディレクトリ構造に整理された NIfTI 形式の画像が必要です。
# Dataset directory structure (Dataset007_Pancreas as example):
# $nnUNet_raw/
# └── Dataset007_Pancreas/
# ├── dataset.json ← metadata descriptor
# ├── imagesTr/ ← training images
# │ ├── pancreas_001_0000.nii.gz (0000 = channel/modality index)
# │ └── pancreas_002_0000.nii.gz
# ├── labelsTr/ ← training segmentation masks
# │ ├── pancreas_001.nii.gz
# │ └── pancreas_002.nii.gz
# └── imagesTs/ ← test images (no labels required)
# └── pancreas_101_0000.nii.gz
# For Medical Segmentation Decathlon datasets, convert automatically:
nnUNetv2_convert_MSD_dataset -i /data/Task07_Pancreas -overwrite_id 7
echo "Dataset 7 created at $nnUNet_raw/Dataset007_Pancreas/"
import json
from pathlib import Path
import shutil
# Create dataset.json for a custom dataset (single CT modality, 2-class segmentation)
dataset_id = 8
dataset_name = f"Dataset{dataset_id:03d}_MyOrgan"
dataset_dir = Path(f"{dataset_name}")
(dataset_dir / "imagesTr").mkdir(parents=True, exist_ok=True)
(dataset_dir / "labelsTr").mkdir(parents=True, exist_ok=True)
(dataset_dir / "imagesTs").mkdir(parents=True, exist_ok=True)
dataset_json = {
"channel_names": {
"0": "CT" # for MRI: "0": "T1", "1": "T2" (multi-modal = multiple channels)
},
"labels": {
"background": 0,
"organ": 1 # add more classes: "tumor": 2, "vessel": 3
},
"numTraining": 50, # number of training cases
"file_ending": ".nii.gz"
}
with open(dataset_dir / "dataset.json", "w") as f:
json.dump(dataset_json, f, indent=2)
print(f"Created dataset.json with {dataset_json['numTraining']} training cases")
print(f"Labels: {dataset_json['labels']}")
print(f"Channels: {dataset_json['channel_names']}")
print(f"\nPlace training images as: imagesTr/case_NNN_0000.nii.gz")
print(f"Place training labels as: labelsTr/case_NNN.nii.gz")
ステップ 2:プランニングと前処理
nnU-Net はデータセットのフィンガープリント(画像の強度、間隔、サイズ)を分析し、アーキテクチャと前処理を自動的に設定します。大規模なデータセットでは、このステップに 30~120 分かかることがあります。
# Plan preprocessing for dataset 7 with integrity checks
nnUNetv2_plan_and_preprocess -d 7 --verify_dataset_integrity
# This creates:
# $nnUNet_preprocessed/Dataset007_Pancreas/
# ├── nnUNetPlans.json ← architecture configurations (patch size, batch size, etc.)
# ├── dataset_fingerprint.json ← image statistics used for auto-configuration
# ├── nnUNetPlans_2d/ ← preprocessed 2D slices
# ├── nnUNetPlans_3d_fullres/ ← preprocessed 3D full-resolution patches
# └── nnUNetPlans_3d_lowres/ ← preprocessed 3D low-resolution patches (if applicable)
echo "Preprocessing complete. Plans stored in $nnUNet_preprocessed/Dataset007_Pancreas/"
import json
from pathlib import Path
import os
# Inspect the auto-generated plans to understand what nnU-Net configured
preprocessed_dir = Path(os.environ["nnUNet_preprocessed"])
plans_file = preprocessed_dir / "Dataset007_Pancreas" / "nnUNetPlans.json"
with open(plans_file) as f:
plans = json.load(f)
print("nnU-Net auto-configuration summary:")
print(f" Dataset name: {plans['dataset_name']}")
print(f" Original spacing (mm): {plans['original_median_spacing_after_transp']}")
for config_name, config in plans["configurations"].items():
print(f"\n Configuration: {config_name}")
print(f" Patch size: {config.get('patch_size', 'N/A')}")
print(f" Batch size: {config.get('batch_size', 'N/A')}")
print(f" Spacing: {config.get('spacing', 'N/A')}")
print(f" Network arch: {config.get('network_arch_class_name', 'N/A')}")
ステップ 3:学習(5 分割交差検証)
nnU-Net はデフォルトで 5 分割交差検証を使用します。最良のアンサンブル性能を得るには、各構成ですべての 5 分割を学習してください。最初のモデルを手早く作成する場合は、fold 0 のみを学習してください。
# Train 3D full-resolution configuration, fold 0 (on GPU)
# --npz saves softmax predictions for ensembling
nnUNetv2_train 7 3d_fullres 0 --npz
# Train all 5 folds (submit as 5 parallel jobs on a cluster)
for fold in 0 1 2 3 4; do
nnUNetv2_train 7 3d_fullres $fold --npz
done
# Also train 2D configuration for potential ensemble
nnUNetv2_train 7 2d 0 --npz
echo "Training outputs in $nnUNet_results/Dataset007_Pancreas/"
# Resume training from a checkpoint (if interrupted)
nnUNetv2_train 7 3d_fullres 0 --npz --c
# Training on CPU only (slow — for testing only)
nnUNetv2_train 7 3d_fullres 0 --npz --device cpu
# Monitor GPU memory and training progress
watch -n 10 nvidia-smi
# Check training log for loss curves
tail -f $nnUNet_results/Dataset007_Pancreas/nnUNetTrainer__nnUNetPlans__3d_fullres/fold_0/training_log.txt
ステップ 4:最適な構成を見つける
学習後、nnU-Net は学習済みのすべての構成とそのアンサンブルを評価し、最適な単一モデルまたはアンサンブルを推奨します。
# Determine which configuration or ensemble performs best on validation folds
# Must have trained multiple folds (0-4) for accurate comparison
nnUNetv2_find_best_configuration 7 -c 2d 3d_fullres
# Output example:
# Best configuration: 3d_fullres
# Best ensemble: 2d + 3d_fullres (Dice = 0.823 vs 3d_fullres alone = 0.814)
# Recommended: ensemble of 2d + 3d_fullres
# Also run post-processing determination
# (removes small connected components if it helps validation Dice)
nnUNetv2_apply_postprocessing --help
import json
from pathlib import Path
import os
# Parse cross-validation results to see per-fold performance
results_dir = Path(os.environ["nnUNet_results"])
summary_file = results_dir / "Dataset007_Pancreas" / \
"nnUNetTrainer__nnUNetPlans__3d_fullres" / "fold_0" / "validation" / "summary.json"
if summary_file.exists():
with open(summary_file) as f:
summary = json.load(f)
metrics = summary["foreground_mean"]
print("3D full-res fold_0 validation metrics:")
for metric_name, value in metrics.items():
print(f" {metric_name}: {value:.4f}")
# Expected output:
# Dice: 0.81
# IoU: 0.68
# HD95: 12.3 (mm)
else:
print(f"Summary not found at {summary_file}")
print("Run nnUNetv2_train first to generate validation metrics")
ステップ 5:新しい画像を予測する
学習済みモデルを使用して、新しい画像のフォルダーに対して推論を実行します。
# Predict using the best single configuration (fold 0 only, fast)
nnUNetv2_predict \
-i /data/test_images/ \
-o /data/predictions_3d/ \
-d 7 \
-c 3d_fullres \
-f 0 \
--save_probabilities # save softmax probabilities for later ensembling
# Predict using all 5 folds (better performance, slower)
nnUNetv2_predict \
-i /data/test_images/ \
-o /data/predictions_allfolds/ \
-d 7 \
-c 3d_fullres \
-f 0 1 2 3 4
echo "Predictions saved to /data/predictions_allfolds/"
import torch
from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
from pathlib import Path
import os
# Python API for inference — useful for integration into custom pipelines
results_dir = os.environ["nnUNet_results"]
model_folder = f"{results_dir}/Dataset007_Pancreas/nnUNetTrainer__nnUNetPlans__3d_fullres"
# Initialize predictor
predictor = nnUNetPredictor(
tile_step_size=0.5, # overlap fraction between tiles (higher = better quality, slower)
use_gaussian=True, # Gaussian weighting at tile edges (reduces boundary artifacts)
use_mirroring=True, # test-time augmentation with flips (improves accuracy)
device=torch.device("cuda", 0), # use GPU 0; set to torch.device("cpu") for CPU
verbose=False,
allow_tqdm=True,
)
predictor.initialize_from_trained_model_folder(
model_folder,
use_folds=(0,), # which folds to use; (0,1,2,3,4) for ensemble of all folds
checkpoint_name="checkpoint_best.pth", # or "checkpoint_final.pth"
)
# Predict from files
input_folder = "/data/test_images/"
output_folder = "/data/predictions/"
Path(output_folder).mkdir(exist_ok=True)
predictor.predict_from_files(
list_of_lists_or_source_folder=input_folder,
output_folder_or_list_of_truncated_output_files=output_folder,
save_probabilities=False,
overwrite=True,
num_processes_preprocessing=2,
num_processes_segmentation_export=2,
)
print(f"Python API inference complete → {output_folder}")
ステップ 6:予測をアンサンブルする
複数の構成(例:2D と 3D フル解像度)または fold の予測を組み合わせ、セグメンテーションの精度を向上させます。
# Ensemble predictions from two configurations
# Both must have been predicted with --save_probabilities
nnUNetv2_ensemble \
-i /data/predictions_2d/ /data/predictions_3d/ \
-o /data/predictions_ensemble/ \
-np 4
echo "Ensemble predictions saved to /data/predictions_ensemble/"
# Apply post-processing (e.g., remove small connected components)
# Post-processing file generated during nnUNetv2_find_best_configuration
nnUNetv2_apply_postprocessing \
-i /data/predictions_ensemble/ \
-o /data/predictions_final/ \
-pp_pkl_file $nnUNet_results/Dataset007_Pancreas/nnUNetTrainer__nnUNetPlans__3d_fullres/crossval_results_folds_0_1_2_3_4/postprocessing.pkl \
-np 4 \
-plans_json $nnUNet_preprocessed/Dataset007_Pancreas/nnUNetPlans.json
echo "Post-processing complete → /data/predictions_final/"
ステップ 7:セグメンテーション品質を評価する
予測されたセグメンテーションと正解データのセグメンテーションとの間で、Dice スコアと Hausdorff 距離を計算します。
import SimpleITK as sitk
import numpy as np
import pandas as pd
from pathlib import Path
def compute_dice(pred_path: str, gt_path: str, label_value: int = 1) -> dict:
"""Compute Dice coefficient and Hausdorff distance for a single case."""
pred = sitk.GetArrayFromImage(sitk.ReadImage(pred_path))
gt = sitk.GetArrayFromImage(sitk.ReadImage(gt_path))
pred_bin = (pred == label_value).astype(bool)
gt_bin = (gt == label_value).astype(bool)
intersection = (pred_bin & gt_bin).sum()
dice = 2.0 * intersection / (pred_bin.sum() + gt_bin.sum() + 1e-8)
# Surface distance for Hausdorff (using SimpleITK)
pred_sitk = sitk.ReadImage(pred_path)
gt_sitk = sitk.ReadImage(gt_path)
pred_label = sitk.BinaryThreshold(pred_sitk, label_value, label_value, 1, 0)
gt_label = sitk.BinaryThreshold(gt_sitk, label_value, label_value, 1, 0)
hausdorff_filter = sitk.HausdorffDistanceImageFilter()
hausdorff_filter.Execute(pred_label, gt_label)
hd95 = hausdorff_filter.GetAverageHausdorffDistance()
return {
"dice": dice,
"hd_avg_mm": hd95,
"pred_volume_ml": pred_bin.sum() * np.prod(pred_sitk.GetSpacing()) / 1000,
"gt_volume_ml": gt_bin.sum() * np.prod(gt_sitk.GetSpacing()) / 1000,
}
# Evaluate all predictions
pred_dir = Path("/data/predictions_ensemble/")
gt_dir = Path("/data/test_labels/")
rows = []
for pred_file in sorted(pred_dir.glob("*.nii.gz")):
gt_file = gt_dir / pred_file.name
if gt_file.exists():
metrics = compute_dice(str(pred_file), str(gt_file), label_value=1)
metrics["case"] = pred_file.stem
rows.append(metrics)
df = pd.DataFrame(rows)
print("Segmentation evaluation summary:")
print(df[["case", "dice", "hd_avg_mm", "pred_volume_ml", "gt_volume_ml"]].to_string(index=False))
print(f"\nMean Dice: {df['dice'].mean():.3f} ± {df['dice'].std():.3f}")
print(f"Mean Avg HD: {df['hd_avg_mm'].mean():.1f} ± {df['hd_avg_mm'].std():.1f} mm")
df.to_csv("segmentation_evaluation.csv", index=False)
print("Saved: segmentation_evaluation.csv")
ステップ 8:セグメンテーション結果を可視化する
予測されたセグメンテーションを入力画像に重ね合わせ、目視による品質管理を行います。
import SimpleITK as sitk
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from pathlib import Path
def visualize_segmentation(image_path: str, pred_path: str, gt_path: str = None,
output_path: str = "segmentation_qc.png",
n_slices: int = 5, label_value: int = 1):
"""Create a multi-slice overlay figure for visual QC."""
image = sitk.GetArrayFromImage(sitk.ReadImage(image_path)) # shape: (z, y, x)
pred = sitk.GetArrayFromImage(sitk.ReadImage(pred_path))
# Select evenly spaced slices where segmentation is present
label_slices = np.where((pred == label_value).any(axis=(1, 2)))[0]
if len(label_slices) == 0:
print("No foreground voxels found in prediction")
return
step = max(1, len(label_slices) // n_slices)
selected_slices = label_slices[::step][:n_slices]
n_cols = n_slices
n_rows = 2 if gt_path is None else 3
fig, axes = plt.subplots(n_rows, n_cols, figsize=(4 * n_cols, 4 * n_rows))
# Normalize image for display
vmin, vmax = np.percentile(image, [1, 99])
cmap_label = mcolors.ListedColormap(["none", "#FF4500"]) # orange-red for prediction
for col, z in enumerate(selected_slices):
ax_img = axes[0, col] if n_rows > 1 else axes[col]
ax_img.imshow(image[z], cmap="gray", vmin=vmin, vmax=vmax)
ax_img.imshow((pred[z] == label_value), cmap=cmap_label, alpha=0.5, vmin=0, vmax=1)
ax_img.set_title(f"Pred z={z}", fontsize=9)
ax_img.axis("off")
if gt_path is not None:
gt = sitk.GetArrayFromImage(sitk.ReadImage(gt_path))
ax_gt = axes[1, col]
ax_gt.imshow(image[z], cmap="gray", vmin=vmin, vmax=vmax)
ax_gt.imshow((gt[z] == label_value), cmap=cmap_label, alpha=0.5, vmin=0, vmax=1)
ax_gt.set_title(f"GT z={z}", fontsize=9)
ax_gt.axis("off")
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches="tight")
plt.close()
print(f"QC figure saved: {output_path}")
# Generate QC plots for first 3 test cases
pred_dir = Path("/data/predictions_ensemble/")
image_dir = Path("/data/test_images/")
gt_dir = Path("/data/test_labels/")
for i, pred_file in enumerate(sorted(pred_dir.glob("*.nii.gz"))[:3]):
case_id = pred_file.stem.replace(".nii", "")
image_file = image_dir / f"{case_id}_0000.nii.gz"
gt_file = gt_dir / pred_file.name
if image_file.exists():
visualize_segmentation(
image_path=str(image_file),
pred_path=str(pred_file),
gt_path=str(gt_file) if gt_file.exists() else None,
output_path=f"qc_{case_id}.png",
n_slices=5
)
主なパラメーター
| パラメーター | デフォルト | 範囲/オプション | 効果 |
|---|---|---|---|
-c (configuration) | — | 2d, 3d_fullres, 3d_lowres, 3d_cascade_fullres | ネットワークアーキテクチャです。3d_fullres は標準的な開始点です |
-f (fold) | — | 0, 1, 2, 3, 4, all | 学習または推論に使用する交差検証の fold です |
--npz (train) | off | flag | 学習中にソフトマックス確率マップを保存します。アンサンブルに必要です |
tile_step_size (Python API) | 0.5 | 0.3–0.9 | 推論タイル間のオーバーラップです。値を大きくすると速度は低下しますが、精度が向上します |
use_mirroring (Python API) | True | True, False | 軸反転によるテスト時拡張です。無効にすると、Dice がわずかに低下する代わりに速度が 2~4 倍になります |
--num_epochs (train) | 1000 | 250–2000 | 学習エポック数です。デフォルトの 1000 はほとんどのデータセットに適しています |
checkpoint_name (Python API) | "checkpoint_best.pth" | "checkpoint_best.pth", "checkpoint_final.pth" | 読み込む保存済みチェックポイントです。_best は検証 Dice が最も高かったスナップショットです |
-np (ensemble/postproc) | 3 | 1–CPU count | アンサンブルまたは後処理に使用する並列プロセス数です |
minimumObjectSize (postprocessing) | auto | voxel count | 保持する連結成分の最小サイズです。find_best_configuration によって自動的に決定されます |
--device (train) | cuda | cuda, cpu, mps | 学習デバイスです。Apple Silicon GPU には mps を指定します |
よく使うレシピ
レシピ:学習済みコミュニティモデルを使った推論
使用する場面:対象の解剖部位(例:腹部臓器、脳腫瘍)に対する公開済みチャレンジモデルがある場合です。再学習せずにダウンロードして適用します。
import torch
from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
from pathlib import Path
# Use a downloaded pretrained model folder
# Community models: https://zenodo.org/record/7783116 (abdominal organs)
model_folder = "/data/pretrained_models/Dataset003_Liver/nnUNetTrainer__nnUNetPlans__3d_fullres"
predictor = nnUNetPredictor(
tile_step_size=0.5,
use_gaussian=True,
use_mirroring=True,
device=torch.device("cuda", 0),
allow_tqdm=True,
)
predictor.initialize_from_trained_model_folder(
model_folder,
use_folds=(0, 1, 2, 3, 4), # ensemble all folds
checkpoint_name="checkpoint_best.pth",
)
input_dir = "/data/new_ct_scans/"
output_dir = "/data/liver_predictions/"
Path(output_dir).mkdir(exist_ok=True)
predictor.predict_from_files(
list_of_lists_or_source_folder=input_dir,
output_folder_or_list_of_truncated_output_files=output_dir,
save_probabilities=False,
overwrite=True,
num_processes_preprocessing=4,
num_processes_segmentation_export=4,
)
print(f"Inference complete → {output_dir}")
レシピ:マルチチャンネル(マルチモーダル)データセットのセットアップ
使用する場面:同一ケースの複数の画像モダリティ(例:脳腫瘍セグメンテーション用の T1+T2+FLAIR)を使って学習する場合です。
import json
from pathlib import Path
import shutil
# Multi-channel dataset: T1 + T2 + FLAIR brain MRI
dataset_dir = Path("Dataset001_BrainTumor")
for subdir in ["imagesTr", "labelsTr", "imagesTs"]:
(dataset_dir / subdir).mkdir(parents=True, exist_ok=True)
# File naming convention: case_{ID}_{channel_index}.nii.gz
# For case 001: T1=001_0000, T2=001_0001, FLAIR=001_0002
# Rename your files accordingly:
case_id = "001"
modalities = {"T1": "0000", "T2": "0001", "FLAIR": "0002"}
for modality, idx in modalities.items():
src = Path(f"/data/raw/{case_id}_{modality}.nii.gz")
dst = dataset_dir / "imagesTr" / f"BrainTumor_{case_id}_{idx}.nii.gz"
if src.exists():
shutil.copy(src, dst)
print(f"Copied {modality} → {dst.name}")
# dataset.json with multiple channels
dataset_json = {
"channel_names": {"0": "T1", "1": "T2", "2": "FLAIR"},
"labels": {"background": 0, "edema": 1, "non_enhancing_tumor": 2, "enhancing_tumor": 3},
"numTraining": 200,
"file_ending": ".nii.gz"
}
with open(dataset_dir / "dataset.json", "w") as f:
json.dump(dataset_json, f, indent=2)
print("Multi-channel dataset.json created")
print(f"Channels: {dataset_json['channel_names']}")
print(f"Classes: {dataset_json['labels']}")
レシピ:大きな構造物に対するカスケード学習
使用する場面:セグメンテーション対象の構造物が画像に対して非常に大きく(例:脳全体、低解像度 CT の大きな臓器)、メモリの都合で 3D フル解像度での学習が現実的でない場合です。
# First train the low-res model (predicts a coarse segmentation)
nnUNetv2_train 7 3d_lowres 0 --npz
nnUNetv2_train 7 3d_lowres 1 --npz
nnUNetv2_train 7 3d_lowres 2 --npz
nnUNetv2_train 7 3d_lowres 3 --npz
nnUNetv2_train 7 3d_lowres 4 --npz
# Then train the high-res cascade (refines the low-res prediction)
# The cascade uses the low-res output as additional input
nnUNetv2_train 7 3d_cascade_fullres 0 --npz
nnUNetv2_train 7 3d_cascade_fullres 1 --npz
nnUNetv2_train 7 3d_cascade_fullres 2 --npz
nnUNetv2_train 7 3d_cascade_fullres 3 --npz
nnUNetv2_train 7 3d_cascade_fullres 4 --npz
# Predict with cascade: automatically uses lowres then cascade_fullres
nnUNetv2_predict -i /data/test/ -o /data/cascade_preds/ \
-d 7 -c 3d_cascade_fullres -f 0 1 2 3 4
echo "Cascade prediction complete"
想定される出力
| 出力 | 場所 | 説明 |
|---|---|---|
nnUNetPlans.json | $nnUNet_preprocessed/DatasetXXX/ | 自動設定されたアーキテクチャ、パッチサイズ、バッチサイズ、間隔 |
dataset_fingerprint.json | $nnUNet_preprocessed/DatasetXXX/ | 自動設定に使用されるデータセットの統計情報 |
checkpoint_best.pth | $nnUNet_results/.../fold_N/ | 検証 Dice スコアが最も高かったモデルのスナップショット |
checkpoint_final.pth | $nnUNet_results/.../fold_N/ | すべての学習エポック終了後の最終モデルチェックポイント |
training_log.txt | $nnUNet_results/.../fold_N/ | エポックごとの学習/検証損失と Dice スコア |
summary.json | $nnUNet_results/.../fold_N/validation/ | 検証 fold の Dice、IoU、Hausdorff 距離 |
{case}.nii.gz | 予測出力フォルダー | 整数ラベルマップ(0=背景、1,2,...=クラス) |
{case}.npz | 予測フォルダー(--save_probabilities を指定した場合) | 各クラスのソフトマックス確率マップ |
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
RuntimeError: CUDA out of memory | パッチサイズに対してGPU VRAMが不足しています | nnUNetPlans.json でパッチサイズを小さくします(patch_size と batch_size を編集します)。3Dではなく2D構成を使用します |
FileNotFoundError: nnUNet_raw not set | 環境変数が設定されていません | export nnUNet_raw=/path/to/raw を実行します。echo $nnUNet_raw で確認します |
| データセットの整合性チェックに失敗する | 画像とラベルのサイズまたは命名が一致していません | すべての labelsTr/case_NNN.nii.gz に対応する imagesTr/case_NNN_0000.nii.gz があることを確認します。間隔が一致しているか確認します |
| 学習後のDiceが低い | 学習ケースが少なすぎる、またはラベルの品質が低い | 最低20ケースを用意します。ラベルの品質を目視で確認します。dataset.json でクラスの不均衡を確認します |
| 学習が停滞する(200エポック後も損失が減少しない) | 学習率が高すぎる、または低すぎる | nnU-Net は自動学習率スケジュールを使用します。停滞が続く場合は、numTraining のサンプル数を増やすか、データ拡張を確認します |
ValueError: images contain incorrect pixel type | 画像がfloat形式またはuint形式ではありません | imagesTr/ に配置する前に、SimpleITKで sitk.Cast(image, sitk.sitkFloat32) を使ってfloat32に変換します |
| 予測結果がすべて背景になる | モデルフォルダーまたはチェックポイントのパスが間違っています | model_folder のパスを確認します。checkpoint_best.pth が存在することを確認します。データセットIDが学習済みモデルと一致していることを確認します |
| 推論が非常に遅い(1ケースあたり10分超) | CPUでの推論、または完全なミラーリングを伴う大きなパッチ | device=torch.device("cuda",0) を設定します。4倍高速化するには use_mirroring=False を設定します。tile_step_size を0.3に減らします |
参考資料
- nnU-Net GitHub — 公式ソースコード、インストール手順、ドキュメント
- Isensee F et al. (2021) Nature Methods 18:203–211 — nnU-Netの原著論文:「nnU-Net: a self-configuring method for deep learning-based biomedical image segmentation」
- nnU-Net v2 documentation — 移行ガイド、データセット形式、推論API、カスタムトレーナー
- Medical Segmentation Decathlon — 学習と検証に使用できる、nnU-Net互換形式のベンチマークデータセット10件
- Zenodo: pretrained nnU-Net models — 腹部臓器やその他の構造を対象とする、コミュニティ共有の事前学習済みモデル