Spaces:
Sleeping
Sleeping
Anirudh Balaraman commited on
Commit ·
efc95db
1
Parent(s): f1a8b97
update inference
Browse files- .gitignore +4 -0
- config/config_run_inference.yaml +9 -0
- preprocess_main.py +6 -4
- pyproject.toml +2 -0
- run_cspca.py +9 -5
- run_inference.py +40 -40
- run_pirads.py +5 -5
- src/data/custom_transforms.py +9 -4
- src/data/data_loader.py +19 -22
- src/model/cspca_model.py +5 -5
- src/model/mil.py +3 -2
- src/preprocessing/clip_intensity.py +3 -5
- src/preprocessing/generate_heatmap.py +14 -47
- src/preprocessing/prostate_mask.py +6 -2
- src/preprocessing/register_and_crop.py +0 -82
- src/train/train_cspca.py +11 -10
- src/train/train_pirads.py +2 -2
- src/utils.py +17 -0
- tcia_dataset.ipynb +136 -114
- temp copy.ipynb +369 -165
- temp_2.ipynb +156 -142
- tests/test_run.py +12 -8
.gitignore
CHANGED
|
@@ -12,3 +12,7 @@ __pycache__/
|
|
| 12 |
site/
|
| 13 |
updated_segmentations.zip
|
| 14 |
updated_segmentations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
site/
|
| 13 |
updated_segmentations.zip
|
| 14 |
updated_segmentations
|
| 15 |
+
temp_2.ipynb
|
| 16 |
+
temp copy.ipynb
|
| 17 |
+
tcia_dataset.ipynb
|
| 18 |
+
check_tum_datatset.ipynb
|
config/config_run_inference.yaml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
t2_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/t2
|
| 2 |
+
dwi_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/dwi
|
| 3 |
+
adc_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/adc
|
| 4 |
+
output_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed
|
| 5 |
+
json_path: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/psa_val.json
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
preprocess_main.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
| 1 |
import argparse
|
| 2 |
import logging
|
| 3 |
import os
|
|
|
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
|
| 6 |
import yaml
|
| 7 |
|
| 8 |
-
from src.preprocessing.generate_heatmap import get_heatmap
|
| 9 |
from src.preprocessing.clip_intensity import clip_adc
|
|
|
|
| 10 |
from src.preprocessing.prostate_mask import get_segmask
|
| 11 |
from src.preprocessing.register_and_crop import register_files
|
| 12 |
-
from src.utils import setup_logging
|
| 13 |
|
| 14 |
|
| 15 |
def parse_args():
|
|
@@ -50,7 +52,7 @@ if __name__ == "__main__":
|
|
| 50 |
if args.project_dir is None:
|
| 51 |
args.project_dir = Path(__file__).resolve().parent # Set project directory
|
| 52 |
|
| 53 |
-
FUNCTIONS = {
|
| 54 |
"register_and_crop": register_files,
|
| 55 |
"clip_adc": clip_adc,
|
| 56 |
"get_segmentation_mask": get_segmask,
|
|
@@ -62,7 +64,7 @@ if __name__ == "__main__":
|
|
| 62 |
logging.info("Starting preprocessing")
|
| 63 |
if args.steps is None:
|
| 64 |
args.steps = ["register_and_crop", "get_segmentation_mask", "clip_adc", "get_heatmap"]
|
| 65 |
-
#validate_steps(args.steps)
|
| 66 |
for step in args.steps:
|
| 67 |
func = FUNCTIONS[step]
|
| 68 |
args = func(args)
|
|
|
|
| 1 |
import argparse
|
| 2 |
import logging
|
| 3 |
import os
|
| 4 |
+
from argparse import Namespace
|
| 5 |
+
from collections.abc import Callable
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
import yaml
|
| 9 |
|
|
|
|
| 10 |
from src.preprocessing.clip_intensity import clip_adc
|
| 11 |
+
from src.preprocessing.generate_heatmap import get_heatmap
|
| 12 |
from src.preprocessing.prostate_mask import get_segmask
|
| 13 |
from src.preprocessing.register_and_crop import register_files
|
| 14 |
+
from src.utils import setup_logging
|
| 15 |
|
| 16 |
|
| 17 |
def parse_args():
|
|
|
|
| 52 |
if args.project_dir is None:
|
| 53 |
args.project_dir = Path(__file__).resolve().parent # Set project directory
|
| 54 |
|
| 55 |
+
FUNCTIONS: dict[str, Callable[[Namespace], Namespace]] = {
|
| 56 |
"register_and_crop": register_files,
|
| 57 |
"clip_adc": clip_adc,
|
| 58 |
"get_segmentation_mask": get_segmask,
|
|
|
|
| 64 |
logging.info("Starting preprocessing")
|
| 65 |
if args.steps is None:
|
| 66 |
args.steps = ["register_and_crop", "get_segmentation_mask", "clip_adc", "get_heatmap"]
|
| 67 |
+
# validate_steps(args.steps)
|
| 68 |
for step in args.steps:
|
| 69 |
func = FUNCTIONS[step]
|
| 70 |
args = func(args)
|
pyproject.toml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
[tool.ruff]
|
| 2 |
line-length = 100
|
| 3 |
target-version = "py39" # Ensures ruff uses 3.9 compatible syntax
|
|
|
|
| 4 |
|
| 5 |
[tool.ruff.lint]
|
| 6 |
select = ["E", "W", "F", "I", "B", "N", "UP"]
|
|
@@ -22,6 +23,7 @@ skip-magic-trailing-comma = false
|
|
| 22 |
ignore-names = ["sitk", "NormalizeIntensity_custom", "NormalizeIntensity_customd"]
|
| 23 |
|
| 24 |
[tool.mypy]
|
|
|
|
| 25 |
ignore_missing_imports = true
|
| 26 |
disable_error_code = ["override", "import-untyped"]
|
| 27 |
mypy_path = "."
|
|
|
|
| 1 |
[tool.ruff]
|
| 2 |
line-length = 100
|
| 3 |
target-version = "py39" # Ensures ruff uses 3.9 compatible syntax
|
| 4 |
+
extend-exclude = ["*.ipynb"]
|
| 5 |
|
| 6 |
[tool.ruff.lint]
|
| 7 |
select = ["E", "W", "F", "I", "B", "N", "UP"]
|
|
|
|
| 23 |
ignore-names = ["sitk", "NormalizeIntensity_custom", "NormalizeIntensity_customd"]
|
| 24 |
|
| 25 |
[tool.mypy]
|
| 26 |
+
|
| 27 |
ignore_missing_imports = true
|
| 28 |
disable_error_code = ["override", "import-untyped"]
|
| 29 |
mypy_path = "."
|
run_cspca.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
import argparse
|
|
|
|
| 2 |
import logging
|
| 3 |
import os
|
| 4 |
import shutil
|
| 5 |
import sys
|
| 6 |
from pathlib import Path
|
| 7 |
-
import json
|
| 8 |
|
| 9 |
import torch
|
| 10 |
import yaml
|
|
@@ -21,11 +21,11 @@ from src.utils import get_metrics, save_cspca_checkpoint, setup_logging
|
|
| 21 |
def main_worker(args):
|
| 22 |
mil_model = MILModel3D(num_classes=args.num_classes, mil_mode=args.mil_mode)
|
| 23 |
cache_dir_path = Path(os.path.join(args.logdir, "cache"))
|
| 24 |
-
|
| 25 |
scaler = StandardScaler()
|
| 26 |
with open(os.path.join(args.project_dir, "dataset", "PICAI_cspca_updated_with_psa.json")) as f:
|
| 27 |
dataset_json = json.load(f)
|
| 28 |
-
train_clinical = [i[
|
| 29 |
_ = scaler.fit_transform(train_clinical)
|
| 30 |
args.psa_mean = scaler.mean_.tolist()
|
| 31 |
args.psa_std = scaler.scale_.tolist()
|
|
@@ -60,7 +60,9 @@ def main_worker(args):
|
|
| 60 |
train_loss, train_attn_loss, train_auc = train_epoch(
|
| 61 |
cspca_model, train_loader, optimizer, epoch=epoch, args=args
|
| 62 |
)
|
| 63 |
-
logging.info(
|
|
|
|
|
|
|
| 64 |
val_metric = val_epoch(cspca_model, valid_loader, epoch=epoch, args=args)
|
| 65 |
logging.info(
|
| 66 |
f"EPOCH {epoch} VAL loss: {val_metric['loss']:.4f} AUC: {val_metric['auc']:.4f}"
|
|
@@ -110,7 +112,9 @@ def parse_args():
|
|
| 110 |
required=True,
|
| 111 |
help="Operation mode: train or infer",
|
| 112 |
)
|
| 113 |
-
parser.add_argument(
|
|
|
|
|
|
|
| 114 |
parser.add_argument("--config", type=str, help="Path to YAML config file")
|
| 115 |
parser.add_argument("--project_dir", default=None, help="path to project firectory")
|
| 116 |
parser.add_argument("--data_root", default=None, help="path to root folder of images")
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
import json
|
| 3 |
import logging
|
| 4 |
import os
|
| 5 |
import shutil
|
| 6 |
import sys
|
| 7 |
from pathlib import Path
|
|
|
|
| 8 |
|
| 9 |
import torch
|
| 10 |
import yaml
|
|
|
|
| 21 |
def main_worker(args):
|
| 22 |
mil_model = MILModel3D(num_classes=args.num_classes, mil_mode=args.mil_mode)
|
| 23 |
cache_dir_path = Path(os.path.join(args.logdir, "cache"))
|
| 24 |
+
|
| 25 |
scaler = StandardScaler()
|
| 26 |
with open(os.path.join(args.project_dir, "dataset", "PICAI_cspca_updated_with_psa.json")) as f:
|
| 27 |
dataset_json = json.load(f)
|
| 28 |
+
train_clinical = [i["psa"] for i in dataset_json["train"]]
|
| 29 |
_ = scaler.fit_transform(train_clinical)
|
| 30 |
args.psa_mean = scaler.mean_.tolist()
|
| 31 |
args.psa_std = scaler.scale_.tolist()
|
|
|
|
| 60 |
train_loss, train_attn_loss, train_auc = train_epoch(
|
| 61 |
cspca_model, train_loader, optimizer, epoch=epoch, args=args
|
| 62 |
)
|
| 63 |
+
logging.info(
|
| 64 |
+
f"EPOCH {epoch} TRAIN loss: {train_loss:.4f} TRAIN ATTN LOSS: {train_attn_loss:.4f} TRAIN AUC: {train_auc:.4f}"
|
| 65 |
+
)
|
| 66 |
val_metric = val_epoch(cspca_model, valid_loader, epoch=epoch, args=args)
|
| 67 |
logging.info(
|
| 68 |
f"EPOCH {epoch} VAL loss: {val_metric['loss']:.4f} AUC: {val_metric['auc']:.4f}"
|
|
|
|
| 112 |
required=True,
|
| 113 |
help="Operation mode: train or infer",
|
| 114 |
)
|
| 115 |
+
parser.add_argument(
|
| 116 |
+
"--run_name", type=str, default="default_cspca", help="run name for log file"
|
| 117 |
+
)
|
| 118 |
parser.add_argument("--config", type=str, help="Path to YAML config file")
|
| 119 |
parser.add_argument("--project_dir", default=None, help="path to project firectory")
|
| 120 |
parser.add_argument("--data_root", default=None, help="path to root folder of images")
|
run_inference.py
CHANGED
|
@@ -2,52 +2,52 @@ import argparse
|
|
| 2 |
import json
|
| 3 |
import logging
|
| 4 |
import os
|
|
|
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
-
import json
|
| 7 |
|
|
|
|
| 8 |
import torch
|
| 9 |
import yaml
|
| 10 |
from monai.data import Dataset
|
|
|
|
| 11 |
|
| 12 |
from src.data.data_loader import data_transform, list_data_collate
|
| 13 |
from src.model.cspca_model import CSPCAModel
|
| 14 |
from src.model.mil import MILModel3D
|
|
|
|
| 15 |
from src.preprocessing.generate_heatmap import get_heatmap
|
| 16 |
-
from src.preprocessing.histogram_match import histmatch
|
| 17 |
from src.preprocessing.prostate_mask import get_segmask
|
| 18 |
from src.preprocessing.register_and_crop import register_files
|
| 19 |
-
from src.utils import get_parent_image, get_patch_coordinate, setup_logging
|
| 20 |
-
import streamlit as st
|
| 21 |
|
| 22 |
-
@st.cache_resource
|
| 23 |
-
def load_pirads_model(num_classes, mil_mode, project_dir, device):
|
| 24 |
|
|
|
|
|
|
|
| 25 |
model = MILModel3D(num_classes=num_classes, mil_mode=mil_mode)
|
| 26 |
-
checkpoint = torch.load(
|
| 27 |
-
os.path.join(project_dir, "models", "pirads.pt"), map_location="cpu"
|
| 28 |
-
)
|
| 29 |
model.load_state_dict(checkpoint["state_dict"])
|
| 30 |
model.to(device)
|
| 31 |
-
|
| 32 |
model.eval()
|
| 33 |
return model
|
|
|
|
|
|
|
| 34 |
@st.cache_resource
|
| 35 |
def load_cspca_model(_pirads_model, project_dir, device):
|
| 36 |
-
|
| 37 |
model = CSPCAModel(backbone=_pirads_model).to(device)
|
| 38 |
-
checkpt = torch.load(
|
| 39 |
-
os.path.join(project_dir, "models", "cspca_model.pth"), map_location="cpu"
|
| 40 |
-
)
|
| 41 |
model.load_state_dict(checkpt["state_dict"])
|
| 42 |
model = model.to(device)
|
| 43 |
-
|
| 44 |
-
model.eval()
|
| 45 |
return model
|
| 46 |
|
| 47 |
|
| 48 |
def parse_args():
|
| 49 |
parser = argparse.ArgumentParser(description="File preprocessing")
|
| 50 |
parser.add_argument("--config", type=str, help="Path to YAML config file")
|
|
|
|
| 51 |
parser.add_argument("--t2_dir", default=None, help="Path to T2W files")
|
| 52 |
parser.add_argument("--dwi_dir", default=None, help="Path to DWI files")
|
| 53 |
parser.add_argument("--adc_dir", default=None, help="Path to ADC files")
|
|
@@ -60,8 +60,8 @@ def parse_args():
|
|
| 60 |
parser.add_argument("--mil_mode", default="att_trans", type=str)
|
| 61 |
parser.add_argument("--use_heatmap", default=True, type=bool)
|
| 62 |
parser.add_argument("--use_psa", default=True, type=bool)
|
| 63 |
-
parser.add_argument("--tile_size", default=
|
| 64 |
-
parser.add_argument("--tile_count", default=
|
| 65 |
parser.add_argument("--depth", default=3, type=int)
|
| 66 |
parser.add_argument("--project_dir", default=None, help="Project directory")
|
| 67 |
|
|
@@ -78,9 +78,9 @@ if __name__ == "__main__":
|
|
| 78 |
if args.project_dir is None:
|
| 79 |
args.project_dir = Path(__file__).resolve().parent # Set project directory
|
| 80 |
|
| 81 |
-
FUNCTIONS = {
|
| 82 |
"register_and_crop": register_files,
|
| 83 |
-
"
|
| 84 |
"get_segmentation_mask": get_segmask,
|
| 85 |
"get_heatmap": get_heatmap,
|
| 86 |
}
|
|
@@ -88,7 +88,7 @@ if __name__ == "__main__":
|
|
| 88 |
args.logfile = os.path.join(args.output_dir, "inference.log")
|
| 89 |
setup_logging(args.logfile)
|
| 90 |
logging.info("Starting preprocessing")
|
| 91 |
-
steps = ["register_and_crop", "get_segmentation_mask", "
|
| 92 |
for step in steps:
|
| 93 |
func = FUNCTIONS[step]
|
| 94 |
args = func(args)
|
|
@@ -99,26 +99,23 @@ if __name__ == "__main__":
|
|
| 99 |
|
| 100 |
logging.info("Loading PIRADS model")
|
| 101 |
pirads_model = load_pirads_model(args.num_classes, args.mil_mode, args.project_dir, args.device)
|
| 102 |
-
|
| 103 |
-
pirads_checkpoint = torch.load(
|
| 104 |
-
os.path.join(args.project_dir, "models", "pirads.pt"), map_location="cpu"
|
| 105 |
-
)
|
| 106 |
-
pirads_model.load_state_dict(pirads_checkpoint["state_dict"])
|
| 107 |
-
pirads_model.to(args.device)
|
| 108 |
-
'''
|
| 109 |
logging.info("Loading csPCa model")
|
| 110 |
cspca_model = load_cspca_model(pirads_model, args.project_dir, args.device)
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
| 120 |
files = os.listdir(args.t2_dir)
|
| 121 |
args.data_list = []
|
|
|
|
|
|
|
| 122 |
for file in files:
|
| 123 |
temp = {}
|
| 124 |
temp["image"] = os.path.join(args.t2_dir, file)
|
|
@@ -127,6 +124,9 @@ if __name__ == "__main__":
|
|
| 127 |
temp["heatmap"] = os.path.join(args.heatmapdir, file)
|
| 128 |
temp["mask"] = os.path.join(args.seg_dir, file)
|
| 129 |
temp["label"] = 0 # dummy label
|
|
|
|
|
|
|
|
|
|
| 130 |
args.data_list.append(temp)
|
| 131 |
|
| 132 |
dataset = Dataset(data=args.data_list, transform=transform)
|
|
@@ -149,21 +149,21 @@ if __name__ == "__main__":
|
|
| 149 |
with torch.no_grad():
|
| 150 |
for _, batch_data in enumerate(loader):
|
| 151 |
data = batch_data["image"].as_subclass(torch.Tensor).to(args.device)
|
|
|
|
| 152 |
logits = pirads_model(data)
|
| 153 |
pirads_score = torch.argmax(logits, dim=1)
|
| 154 |
pirads_list.append(pirads_score.item())
|
| 155 |
|
| 156 |
-
output = cspca_model(data)
|
| 157 |
output = output.squeeze(1)
|
|
|
|
| 158 |
cspca_risk_list.append(output.item())
|
| 159 |
|
| 160 |
sh = data.shape
|
| 161 |
x = data.reshape(sh[0] * sh[1], sh[2], sh[3], sh[4], sh[5])
|
| 162 |
x = cspca_model.backbone.net(x)
|
| 163 |
x = x.reshape(sh[0], sh[1], -1)
|
| 164 |
-
x = x.permute(1, 0, 2)
|
| 165 |
x = cspca_model.backbone.transformer(x)
|
| 166 |
-
x = x.permute(1, 0, 2)
|
| 167 |
a = cspca_model.backbone.attention(x)
|
| 168 |
a = torch.softmax(a, dim=1)
|
| 169 |
a = a.view(-1)
|
|
|
|
| 2 |
import json
|
| 3 |
import logging
|
| 4 |
import os
|
| 5 |
+
from argparse import Namespace
|
| 6 |
+
from collections.abc import Callable
|
| 7 |
from pathlib import Path
|
|
|
|
| 8 |
|
| 9 |
+
import streamlit as st
|
| 10 |
import torch
|
| 11 |
import yaml
|
| 12 |
from monai.data import Dataset
|
| 13 |
+
from sklearn.preprocessing import StandardScaler
|
| 14 |
|
| 15 |
from src.data.data_loader import data_transform, list_data_collate
|
| 16 |
from src.model.cspca_model import CSPCAModel
|
| 17 |
from src.model.mil import MILModel3D
|
| 18 |
+
from src.preprocessing.clip_intensity import clip_adc
|
| 19 |
from src.preprocessing.generate_heatmap import get_heatmap
|
|
|
|
| 20 |
from src.preprocessing.prostate_mask import get_segmask
|
| 21 |
from src.preprocessing.register_and_crop import register_files
|
| 22 |
+
from src.utils import get_parent_image, get_patch_coordinate, get_prostate_volume, setup_logging
|
|
|
|
| 23 |
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
@st.cache_resource
|
| 26 |
+
def load_pirads_model(num_classes, mil_mode, project_dir, device):
|
| 27 |
model = MILModel3D(num_classes=num_classes, mil_mode=mil_mode)
|
| 28 |
+
checkpoint = torch.load(os.path.join(project_dir, "models", "pirads.pt"), map_location="cpu")
|
|
|
|
|
|
|
| 29 |
model.load_state_dict(checkpoint["state_dict"])
|
| 30 |
model.to(device)
|
| 31 |
+
|
| 32 |
model.eval()
|
| 33 |
return model
|
| 34 |
+
|
| 35 |
+
|
| 36 |
@st.cache_resource
|
| 37 |
def load_cspca_model(_pirads_model, project_dir, device):
|
|
|
|
| 38 |
model = CSPCAModel(backbone=_pirads_model).to(device)
|
| 39 |
+
checkpt = torch.load(os.path.join(project_dir, "models", "cspca_model.pth"), map_location="cpu")
|
|
|
|
|
|
|
| 40 |
model.load_state_dict(checkpt["state_dict"])
|
| 41 |
model = model.to(device)
|
| 42 |
+
|
| 43 |
+
model.eval()
|
| 44 |
return model
|
| 45 |
|
| 46 |
|
| 47 |
def parse_args():
|
| 48 |
parser = argparse.ArgumentParser(description="File preprocessing")
|
| 49 |
parser.add_argument("--config", type=str, help="Path to YAML config file")
|
| 50 |
+
parser.add_argument("--json_path", default=None, help="Path to JSON file containing PSA values")
|
| 51 |
parser.add_argument("--t2_dir", default=None, help="Path to T2W files")
|
| 52 |
parser.add_argument("--dwi_dir", default=None, help="Path to DWI files")
|
| 53 |
parser.add_argument("--adc_dir", default=None, help="Path to ADC files")
|
|
|
|
| 60 |
parser.add_argument("--mil_mode", default="att_trans", type=str)
|
| 61 |
parser.add_argument("--use_heatmap", default=True, type=bool)
|
| 62 |
parser.add_argument("--use_psa", default=True, type=bool)
|
| 63 |
+
parser.add_argument("--tile_size", default=48, type=int)
|
| 64 |
+
parser.add_argument("--tile_count", default=40, type=int)
|
| 65 |
parser.add_argument("--depth", default=3, type=int)
|
| 66 |
parser.add_argument("--project_dir", default=None, help="Project directory")
|
| 67 |
|
|
|
|
| 78 |
if args.project_dir is None:
|
| 79 |
args.project_dir = Path(__file__).resolve().parent # Set project directory
|
| 80 |
|
| 81 |
+
FUNCTIONS: dict[str, Callable[[Namespace], Namespace]] = {
|
| 82 |
"register_and_crop": register_files,
|
| 83 |
+
"clip_adc": clip_adc,
|
| 84 |
"get_segmentation_mask": get_segmask,
|
| 85 |
"get_heatmap": get_heatmap,
|
| 86 |
}
|
|
|
|
| 88 |
args.logfile = os.path.join(args.output_dir, "inference.log")
|
| 89 |
setup_logging(args.logfile)
|
| 90 |
logging.info("Starting preprocessing")
|
| 91 |
+
steps = ["register_and_crop", "get_segmentation_mask", "clip_adc", "get_heatmap"]
|
| 92 |
for step in steps:
|
| 93 |
func = FUNCTIONS[step]
|
| 94 |
args = func(args)
|
|
|
|
| 99 |
|
| 100 |
logging.info("Loading PIRADS model")
|
| 101 |
pirads_model = load_pirads_model(args.num_classes, args.mil_mode, args.project_dir, args.device)
|
| 102 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
logging.info("Loading csPCa model")
|
| 104 |
cspca_model = load_cspca_model(pirads_model, args.project_dir, args.device)
|
| 105 |
+
|
| 106 |
+
scaler = StandardScaler()
|
| 107 |
+
with open(os.path.join(args.project_dir, "dataset", "PICAI_cspca_updated_with_psa.json")) as f:
|
| 108 |
+
dataset_json = json.load(f)
|
| 109 |
+
train_clinical = [i["psa"] for i in dataset_json["train"]]
|
| 110 |
+
_ = scaler.fit_transform(train_clinical)
|
| 111 |
+
args.psa_mean = scaler.mean_.tolist()
|
| 112 |
+
args.psa_std = scaler.scale_.tolist()
|
| 113 |
+
|
| 114 |
+
transform = data_transform(args, split="test")
|
| 115 |
files = os.listdir(args.t2_dir)
|
| 116 |
args.data_list = []
|
| 117 |
+
with open(args.json_path) as f:
|
| 118 |
+
psa_data = json.load(f)
|
| 119 |
for file in files:
|
| 120 |
temp = {}
|
| 121 |
temp["image"] = os.path.join(args.t2_dir, file)
|
|
|
|
| 124 |
temp["heatmap"] = os.path.join(args.heatmapdir, file)
|
| 125 |
temp["mask"] = os.path.join(args.seg_dir, file)
|
| 126 |
temp["label"] = 0 # dummy label
|
| 127 |
+
temp["smooth_mask"] = os.path.join(args.smooth_seg_dir, file)
|
| 128 |
+
prostate_vol = get_prostate_volume(temp["mask"])
|
| 129 |
+
temp["psa"] = [psa_data[file.split(".nrrd")[0]], prostate_vol]
|
| 130 |
args.data_list.append(temp)
|
| 131 |
|
| 132 |
dataset = Dataset(data=args.data_list, transform=transform)
|
|
|
|
| 149 |
with torch.no_grad():
|
| 150 |
for _, batch_data in enumerate(loader):
|
| 151 |
data = batch_data["image"].as_subclass(torch.Tensor).to(args.device)
|
| 152 |
+
psa_data = batch_data["psa"].as_subclass(torch.Tensor).to(args.device)
|
| 153 |
logits = pirads_model(data)
|
| 154 |
pirads_score = torch.argmax(logits, dim=1)
|
| 155 |
pirads_list.append(pirads_score.item())
|
| 156 |
|
| 157 |
+
output = cspca_model(x=data, psa_data=psa_data)
|
| 158 |
output = output.squeeze(1)
|
| 159 |
+
output = torch.sigmoid(output)
|
| 160 |
cspca_risk_list.append(output.item())
|
| 161 |
|
| 162 |
sh = data.shape
|
| 163 |
x = data.reshape(sh[0] * sh[1], sh[2], sh[3], sh[4], sh[5])
|
| 164 |
x = cspca_model.backbone.net(x)
|
| 165 |
x = x.reshape(sh[0], sh[1], -1)
|
|
|
|
| 166 |
x = cspca_model.backbone.transformer(x)
|
|
|
|
| 167 |
a = cspca_model.backbone.attention(x)
|
| 168 |
a = torch.softmax(a, dim=1)
|
| 169 |
a = a.view(-1)
|
run_pirads.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import argparse
|
|
|
|
| 2 |
import logging
|
| 3 |
import os
|
| 4 |
import shutil
|
|
@@ -11,9 +12,8 @@ import torch
|
|
| 11 |
import wandb
|
| 12 |
import yaml
|
| 13 |
from monai.utils import set_determinism
|
| 14 |
-
from torch.utils.tensorboard import SummaryWriter
|
| 15 |
from sklearn.preprocessing import StandardScaler
|
| 16 |
-
import
|
| 17 |
|
| 18 |
from src.data.data_loader import get_dataloader
|
| 19 |
from src.model.mil import MILModel3D
|
|
@@ -45,15 +45,15 @@ def main_worker(args):
|
|
| 45 |
cache_dir_ = os.path.join(args.logdir, "cache")
|
| 46 |
model.to(args.device)
|
| 47 |
params = model.parameters()
|
| 48 |
-
|
| 49 |
scaler = StandardScaler()
|
| 50 |
with open(os.path.join(args.project_dir, "dataset", "PICAI_cspca_updated_with_psa.json")) as f:
|
| 51 |
dataset_json = json.load(f)
|
| 52 |
-
train_clinical = [i[
|
| 53 |
_ = scaler.fit_transform(train_clinical)
|
| 54 |
args.psa_mean = scaler.mean_.tolist()
|
| 55 |
args.psa_std = scaler.scale_.tolist()
|
| 56 |
-
|
| 57 |
if args.mode == "train":
|
| 58 |
train_loader = get_dataloader(args, split="train")
|
| 59 |
valid_loader = get_dataloader(args, split="test")
|
|
|
|
| 1 |
import argparse
|
| 2 |
+
import json
|
| 3 |
import logging
|
| 4 |
import os
|
| 5 |
import shutil
|
|
|
|
| 12 |
import wandb
|
| 13 |
import yaml
|
| 14 |
from monai.utils import set_determinism
|
|
|
|
| 15 |
from sklearn.preprocessing import StandardScaler
|
| 16 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 17 |
|
| 18 |
from src.data.data_loader import get_dataloader
|
| 19 |
from src.model.mil import MILModel3D
|
|
|
|
| 45 |
cache_dir_ = os.path.join(args.logdir, "cache")
|
| 46 |
model.to(args.device)
|
| 47 |
params = model.parameters()
|
| 48 |
+
|
| 49 |
scaler = StandardScaler()
|
| 50 |
with open(os.path.join(args.project_dir, "dataset", "PICAI_cspca_updated_with_psa.json")) as f:
|
| 51 |
dataset_json = json.load(f)
|
| 52 |
+
train_clinical = [i["psa"] for i in dataset_json["train"]]
|
| 53 |
_ = scaler.fit_transform(train_clinical)
|
| 54 |
args.psa_mean = scaler.mean_.tolist()
|
| 55 |
args.psa_std = scaler.scale_.tolist()
|
| 56 |
+
|
| 57 |
if args.mode == "train":
|
| 58 |
train_loader = get_dataloader(args, split="train")
|
| 59 |
valid_loader = get_dataloader(args, split="test")
|
src/data/custom_transforms.py
CHANGED
|
@@ -15,6 +15,7 @@ from monai.utils.enums import TransformBackends
|
|
| 15 |
from monai.utils.type_conversion import convert_data_type, convert_to_dst_type, convert_to_tensor
|
| 16 |
from scipy.ndimage import binary_dilation
|
| 17 |
|
|
|
|
| 18 |
class LabelEncodeIntegerGraded(MapTransform):
|
| 19 |
"""
|
| 20 |
Convert an integer label to encoded array representation of length num_classes,
|
|
@@ -42,13 +43,14 @@ class LabelEncodeIntegerGraded(MapTransform):
|
|
| 42 |
for key in self.keys:
|
| 43 |
label = int(d[key])
|
| 44 |
|
| 45 |
-
lz = np.zeros(self.num_classes
|
| 46 |
lz[:label] = 1.0
|
| 47 |
# alternative oneliner lz=(np.arange(self.num_classes)<int(label)).astype(np.float32) #same oneliner
|
| 48 |
d[key] = lz
|
| 49 |
|
| 50 |
return d
|
| 51 |
|
|
|
|
| 52 |
class DilateAndSaveMaskd(MapTransform):
|
| 53 |
"""
|
| 54 |
Custom transform to dilate binary mask and save a copy.
|
|
@@ -80,7 +82,8 @@ class DilateAndSaveMaskd(MapTransform):
|
|
| 80 |
) # Add channel dimension back
|
| 81 |
|
| 82 |
return d
|
| 83 |
-
|
|
|
|
| 84 |
class NormalizePSAd(MapTransform):
|
| 85 |
"""
|
| 86 |
Custom transform to dilate binary mask and save a copy.
|
|
@@ -88,7 +91,7 @@ class NormalizePSAd(MapTransform):
|
|
| 88 |
|
| 89 |
def __init__(self, keys, mean, std):
|
| 90 |
super().__init__(keys)
|
| 91 |
-
self.mean = torch.tensor(mean)
|
| 92 |
self.std = torch.tensor(std)
|
| 93 |
|
| 94 |
def __call__(self, data):
|
|
@@ -386,7 +389,7 @@ class NormalizeIntensity_custom(Transform):
|
|
| 386 |
masked_img = img
|
| 387 |
"""
|
| 388 |
slices = None
|
| 389 |
-
#mask_data = mask_data.squeeze(0)
|
| 390 |
slices_mask = mask_data > 0
|
| 391 |
masked_img = img[slices_mask]
|
| 392 |
|
|
@@ -440,6 +443,8 @@ class NormalizeIntensity_custom(Transform):
|
|
| 440 |
sub=self.subtrahend[i] if self.subtrahend is not None else None,
|
| 441 |
div=self.divisor[i] if self.divisor is not None else None,
|
| 442 |
)
|
|
|
|
|
|
|
| 443 |
img[i] = norm_temp.squeeze(0)
|
| 444 |
else:
|
| 445 |
img = self._normalize(img, mask_data, self.subtrahend, self.divisor)
|
|
|
|
| 15 |
from monai.utils.type_conversion import convert_data_type, convert_to_dst_type, convert_to_tensor
|
| 16 |
from scipy.ndimage import binary_dilation
|
| 17 |
|
| 18 |
+
|
| 19 |
class LabelEncodeIntegerGraded(MapTransform):
|
| 20 |
"""
|
| 21 |
Convert an integer label to encoded array representation of length num_classes,
|
|
|
|
| 43 |
for key in self.keys:
|
| 44 |
label = int(d[key])
|
| 45 |
|
| 46 |
+
lz = np.zeros(self.num_classes, dtype=np.float32)
|
| 47 |
lz[:label] = 1.0
|
| 48 |
# alternative oneliner lz=(np.arange(self.num_classes)<int(label)).astype(np.float32) #same oneliner
|
| 49 |
d[key] = lz
|
| 50 |
|
| 51 |
return d
|
| 52 |
|
| 53 |
+
|
| 54 |
class DilateAndSaveMaskd(MapTransform):
|
| 55 |
"""
|
| 56 |
Custom transform to dilate binary mask and save a copy.
|
|
|
|
| 82 |
) # Add channel dimension back
|
| 83 |
|
| 84 |
return d
|
| 85 |
+
|
| 86 |
+
|
| 87 |
class NormalizePSAd(MapTransform):
|
| 88 |
"""
|
| 89 |
Custom transform to dilate binary mask and save a copy.
|
|
|
|
| 91 |
|
| 92 |
def __init__(self, keys, mean, std):
|
| 93 |
super().__init__(keys)
|
| 94 |
+
self.mean = torch.tensor(mean)
|
| 95 |
self.std = torch.tensor(std)
|
| 96 |
|
| 97 |
def __call__(self, data):
|
|
|
|
| 389 |
masked_img = img
|
| 390 |
"""
|
| 391 |
slices = None
|
| 392 |
+
# mask_data = mask_data.squeeze(0)
|
| 393 |
slices_mask = mask_data > 0
|
| 394 |
masked_img = img[slices_mask]
|
| 395 |
|
|
|
|
| 443 |
sub=self.subtrahend[i] if self.subtrahend is not None else None,
|
| 444 |
div=self.divisor[i] if self.divisor is not None else None,
|
| 445 |
)
|
| 446 |
+
if isinstance(norm_temp, np.ndarray):
|
| 447 |
+
norm_temp = torch.from_numpy(norm_temp)
|
| 448 |
img[i] = norm_temp.squeeze(0)
|
| 449 |
else:
|
| 450 |
img = self._normalize(img, mask_data, self.subtrahend, self.divisor)
|
src/data/data_loader.py
CHANGED
|
@@ -11,14 +11,11 @@ from monai.transforms import (
|
|
| 11 |
DeleteItemsd,
|
| 12 |
EnsureTyped,
|
| 13 |
LoadImaged,
|
| 14 |
-
NormalizeIntensityd,
|
| 15 |
RandCropByPosNegLabeld,
|
| 16 |
-
|
| 17 |
ToTensord,
|
| 18 |
Transform,
|
| 19 |
Transposed,
|
| 20 |
-
RandFlipd,
|
| 21 |
-
RandRotate90d,
|
| 22 |
)
|
| 23 |
from torch.utils.data.dataloader import default_collate
|
| 24 |
|
|
@@ -27,10 +24,7 @@ from .custom_transforms import (
|
|
| 27 |
ElementwiseProductd,
|
| 28 |
NormalizeIntensity_customd,
|
| 29 |
NormalizePSAd,
|
| 30 |
-
LabelEncodeIntegerGraded,
|
| 31 |
-
|
| 32 |
)
|
| 33 |
-
from sklearn.preprocessing import StandardScaler
|
| 34 |
|
| 35 |
|
| 36 |
def list_data_collate(batch: list):
|
|
@@ -51,18 +45,18 @@ def list_data_collate(batch: list):
|
|
| 51 |
batch[i] = data
|
| 52 |
return default_collate(batch)
|
| 53 |
|
|
|
|
| 54 |
def data_transform(args: argparse.Namespace, split) -> Transform:
|
| 55 |
if split == "train":
|
| 56 |
-
|
| 57 |
transform = Compose(
|
| 58 |
[
|
| 59 |
LoadImaged(
|
| 60 |
-
keys=["image", "mask", "dwi", "adc", "heatmap","smooth_mask"],
|
| 61 |
reader="ITKReader",
|
| 62 |
ensure_channel_first=True,
|
| 63 |
dtype=np.float32,
|
| 64 |
),
|
| 65 |
-
#LabelEncodeIntegerGraded(keys=["label"], num_classes=args.num_classes),
|
| 66 |
ClipMaskIntensityPercentilesd(keys=["image"], lower=0, upper=99.5, mask_key="mask"),
|
| 67 |
ClipMaskIntensityPercentilesd(keys=["dwi"], lower=0, upper=99.5, mask_key="mask"),
|
| 68 |
NormalizeIntensity_customd(keys=["image"], mask_key="mask"),
|
|
@@ -79,11 +73,16 @@ def data_transform(args: argparse.Namespace, split) -> Transform:
|
|
| 79 |
neg=0,
|
| 80 |
num_samples=args.tile_count,
|
| 81 |
),
|
| 82 |
-
RandRotate90d(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
NormalizePSAd(keys=["psa"], mean=args.psa_mean, std=args.psa_std),
|
| 84 |
EnsureTyped(keys=["label", "psa"], dtype=torch.float32),
|
| 85 |
Transposed(keys=["image"], indices=(0, 3, 1, 2)),
|
| 86 |
-
DeleteItemsd(keys=[
|
| 87 |
ToTensord(keys=["image", "label", "final_heatmap", "smooth_mask", "psa"]),
|
| 88 |
]
|
| 89 |
)
|
|
@@ -91,12 +90,12 @@ def data_transform(args: argparse.Namespace, split) -> Transform:
|
|
| 91 |
transform = Compose(
|
| 92 |
[
|
| 93 |
LoadImaged(
|
| 94 |
-
keys=["image", "mask", "dwi", "adc", "heatmap","smooth_mask"],
|
| 95 |
reader="ITKReader",
|
| 96 |
ensure_channel_first=True,
|
| 97 |
dtype=np.float32,
|
| 98 |
),
|
| 99 |
-
#LabelEncodeIntegerGraded(keys=["label"], num_classes=args.num_classes),
|
| 100 |
ClipMaskIntensityPercentilesd(keys=["image"], lower=0, upper=99.5, mask_key="mask"),
|
| 101 |
ClipMaskIntensityPercentilesd(keys=["dwi"], lower=0, upper=99.5, mask_key="mask"),
|
| 102 |
NormalizeIntensity_customd(keys=["image"], mask_key="mask"),
|
|
@@ -116,12 +115,14 @@ def data_transform(args: argparse.Namespace, split) -> Transform:
|
|
| 116 |
NormalizePSAd(keys=["psa"], mean=args.psa_mean, std=args.psa_std),
|
| 117 |
EnsureTyped(keys=["label", "psa"], dtype=torch.float32),
|
| 118 |
Transposed(keys=["image"], indices=(0, 3, 1, 2)),
|
| 119 |
-
DeleteItemsd(keys=[
|
| 120 |
ToTensord(keys=["image", "label", "final_heatmap", "smooth_mask", "psa"]),
|
| 121 |
]
|
| 122 |
)
|
| 123 |
return transform
|
| 124 |
-
|
|
|
|
|
|
|
| 125 |
def data_transform(args: argparse.Namespace) -> Transform:
|
| 126 |
if args.use_heatmap:
|
| 127 |
if args.use_psa:
|
|
@@ -251,22 +252,18 @@ def data_transform(args: argparse.Namespace) -> Transform:
|
|
| 251 |
]
|
| 252 |
)
|
| 253 |
return transform
|
| 254 |
-
|
| 255 |
|
| 256 |
|
| 257 |
def get_dataloader(
|
| 258 |
args: argparse.Namespace, split: Literal["train", "test"]
|
| 259 |
) -> torch.utils.data.DataLoader:
|
| 260 |
-
|
| 261 |
data_list = load_decathlon_datalist(
|
| 262 |
data_list_file_path=args.dataset_json,
|
| 263 |
data_list_key=split,
|
| 264 |
base_dir=args.data_root,
|
| 265 |
)
|
| 266 |
-
data_list_updated = [
|
| 267 |
-
{**i, 'psa': i.get('psa', [0, 0])}
|
| 268 |
-
for i in data_list
|
| 269 |
-
]
|
| 270 |
cache_dir_ = os.path.join(args.logdir, "cache")
|
| 271 |
os.makedirs(os.path.join(cache_dir_, split), exist_ok=True)
|
| 272 |
transform = data_transform(args, split)
|
|
|
|
| 11 |
DeleteItemsd,
|
| 12 |
EnsureTyped,
|
| 13 |
LoadImaged,
|
|
|
|
| 14 |
RandCropByPosNegLabeld,
|
| 15 |
+
RandRotate90d,
|
| 16 |
ToTensord,
|
| 17 |
Transform,
|
| 18 |
Transposed,
|
|
|
|
|
|
|
| 19 |
)
|
| 20 |
from torch.utils.data.dataloader import default_collate
|
| 21 |
|
|
|
|
| 24 |
ElementwiseProductd,
|
| 25 |
NormalizeIntensity_customd,
|
| 26 |
NormalizePSAd,
|
|
|
|
|
|
|
| 27 |
)
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
def list_data_collate(batch: list):
|
|
|
|
| 45 |
batch[i] = data
|
| 46 |
return default_collate(batch)
|
| 47 |
|
| 48 |
+
|
| 49 |
def data_transform(args: argparse.Namespace, split) -> Transform:
|
| 50 |
if split == "train":
|
|
|
|
| 51 |
transform = Compose(
|
| 52 |
[
|
| 53 |
LoadImaged(
|
| 54 |
+
keys=["image", "mask", "dwi", "adc", "heatmap", "smooth_mask"],
|
| 55 |
reader="ITKReader",
|
| 56 |
ensure_channel_first=True,
|
| 57 |
dtype=np.float32,
|
| 58 |
),
|
| 59 |
+
# LabelEncodeIntegerGraded(keys=["label"], num_classes=args.num_classes),
|
| 60 |
ClipMaskIntensityPercentilesd(keys=["image"], lower=0, upper=99.5, mask_key="mask"),
|
| 61 |
ClipMaskIntensityPercentilesd(keys=["dwi"], lower=0, upper=99.5, mask_key="mask"),
|
| 62 |
NormalizeIntensity_customd(keys=["image"], mask_key="mask"),
|
|
|
|
| 73 |
neg=0,
|
| 74 |
num_samples=args.tile_count,
|
| 75 |
),
|
| 76 |
+
RandRotate90d(
|
| 77 |
+
keys=["image", "final_heatmap", "smooth_mask"],
|
| 78 |
+
prob=0.6,
|
| 79 |
+
spatial_axes=(0, 1),
|
| 80 |
+
max_k=3,
|
| 81 |
+
),
|
| 82 |
NormalizePSAd(keys=["psa"], mean=args.psa_mean, std=args.psa_std),
|
| 83 |
EnsureTyped(keys=["label", "psa"], dtype=torch.float32),
|
| 84 |
Transposed(keys=["image"], indices=(0, 3, 1, 2)),
|
| 85 |
+
DeleteItemsd(keys=["dwi", "adc", "heatmap", "mask"]),
|
| 86 |
ToTensord(keys=["image", "label", "final_heatmap", "smooth_mask", "psa"]),
|
| 87 |
]
|
| 88 |
)
|
|
|
|
| 90 |
transform = Compose(
|
| 91 |
[
|
| 92 |
LoadImaged(
|
| 93 |
+
keys=["image", "mask", "dwi", "adc", "heatmap", "smooth_mask"],
|
| 94 |
reader="ITKReader",
|
| 95 |
ensure_channel_first=True,
|
| 96 |
dtype=np.float32,
|
| 97 |
),
|
| 98 |
+
# LabelEncodeIntegerGraded(keys=["label"], num_classes=args.num_classes),
|
| 99 |
ClipMaskIntensityPercentilesd(keys=["image"], lower=0, upper=99.5, mask_key="mask"),
|
| 100 |
ClipMaskIntensityPercentilesd(keys=["dwi"], lower=0, upper=99.5, mask_key="mask"),
|
| 101 |
NormalizeIntensity_customd(keys=["image"], mask_key="mask"),
|
|
|
|
| 115 |
NormalizePSAd(keys=["psa"], mean=args.psa_mean, std=args.psa_std),
|
| 116 |
EnsureTyped(keys=["label", "psa"], dtype=torch.float32),
|
| 117 |
Transposed(keys=["image"], indices=(0, 3, 1, 2)),
|
| 118 |
+
DeleteItemsd(keys=["dwi", "adc", "heatmap", "mask"]),
|
| 119 |
ToTensord(keys=["image", "label", "final_heatmap", "smooth_mask", "psa"]),
|
| 120 |
]
|
| 121 |
)
|
| 122 |
return transform
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
"""
|
| 126 |
def data_transform(args: argparse.Namespace) -> Transform:
|
| 127 |
if args.use_heatmap:
|
| 128 |
if args.use_psa:
|
|
|
|
| 252 |
]
|
| 253 |
)
|
| 254 |
return transform
|
| 255 |
+
"""
|
| 256 |
|
| 257 |
|
| 258 |
def get_dataloader(
|
| 259 |
args: argparse.Namespace, split: Literal["train", "test"]
|
| 260 |
) -> torch.utils.data.DataLoader:
|
|
|
|
| 261 |
data_list = load_decathlon_datalist(
|
| 262 |
data_list_file_path=args.dataset_json,
|
| 263 |
data_list_key=split,
|
| 264 |
base_dir=args.data_root,
|
| 265 |
)
|
| 266 |
+
data_list_updated = [{**i, "psa": i.get("psa", [0, 0])} for i in data_list]
|
|
|
|
|
|
|
|
|
|
| 267 |
cache_dir_ = os.path.join(args.logdir, "cache")
|
| 268 |
os.makedirs(os.path.join(cache_dir_, split), exist_ok=True)
|
| 269 |
transform = data_transform(args, split)
|
src/model/cspca_model.py
CHANGED
|
@@ -70,17 +70,17 @@ class CSPCAModel(nn.Module):
|
|
| 70 |
def __init__(self, backbone: nn.Module) -> None:
|
| 71 |
super().__init__()
|
| 72 |
self.backbone = backbone
|
| 73 |
-
|
| 74 |
self.clinical_dim = 2
|
| 75 |
self.projection_dim = 32
|
| 76 |
self.clinical_projection = nn.Sequential(
|
| 77 |
nn.Linear(self.clinical_dim, self.projection_dim),
|
| 78 |
nn.ReLU(),
|
| 79 |
-
nn.BatchNorm1d(self.projection_dim)
|
| 80 |
)
|
| 81 |
-
|
| 82 |
self.fc_dim = backbone.myfc.in_features
|
| 83 |
-
self.fc_cspca = SimpleNN(input_dim=self.fc_dim + self.projection_dim)
|
| 84 |
|
| 85 |
def forward(self, x, psa_data):
|
| 86 |
sh = x.shape
|
|
@@ -91,7 +91,7 @@ class CSPCAModel(nn.Module):
|
|
| 91 |
a = self.backbone.attention(x)
|
| 92 |
a = torch.softmax(a, dim=1)
|
| 93 |
x = torch.sum(x * a, dim=1)
|
| 94 |
-
|
| 95 |
psa_features = self.clinical_projection(psa_data)
|
| 96 |
x = torch.cat((x, psa_features), dim=1)
|
| 97 |
|
|
|
|
| 70 |
def __init__(self, backbone: nn.Module) -> None:
|
| 71 |
super().__init__()
|
| 72 |
self.backbone = backbone
|
| 73 |
+
|
| 74 |
self.clinical_dim = 2
|
| 75 |
self.projection_dim = 32
|
| 76 |
self.clinical_projection = nn.Sequential(
|
| 77 |
nn.Linear(self.clinical_dim, self.projection_dim),
|
| 78 |
nn.ReLU(),
|
| 79 |
+
nn.BatchNorm1d(self.projection_dim), # Helps stabilize the merged scale
|
| 80 |
)
|
| 81 |
+
|
| 82 |
self.fc_dim = backbone.myfc.in_features
|
| 83 |
+
self.fc_cspca = SimpleNN(input_dim=self.fc_dim + self.projection_dim)
|
| 84 |
|
| 85 |
def forward(self, x, psa_data):
|
| 86 |
sh = x.shape
|
|
|
|
| 91 |
a = self.backbone.attention(x)
|
| 92 |
a = torch.softmax(a, dim=1)
|
| 93 |
x = torch.sum(x * a, dim=1)
|
| 94 |
+
|
| 95 |
psa_features = self.clinical_projection(psa_data)
|
| 96 |
x = torch.cat((x, psa_features), dim=1)
|
| 97 |
|
src/model/mil.py
CHANGED
|
@@ -130,7 +130,9 @@ class MILModel3D(nn.Module):
|
|
| 130 |
self.attention = nn.Sequential(nn.Linear(nfc, 2048), nn.Tanh(), nn.Linear(2048, 1))
|
| 131 |
|
| 132 |
elif self.mil_mode == "att_trans":
|
| 133 |
-
transformer = nn.TransformerEncoderLayer(
|
|
|
|
|
|
|
| 134 |
self.transformer = nn.TransformerEncoder(transformer, num_layers=trans_blocks)
|
| 135 |
self.attention = nn.Sequential(nn.Linear(nfc, 2048), nn.Tanh(), nn.Linear(2048, 1))
|
| 136 |
|
|
@@ -190,7 +192,6 @@ class MILModel3D(nn.Module):
|
|
| 190 |
x = self.myfc(x)
|
| 191 |
|
| 192 |
elif self.mil_mode == "att_trans" and self.transformer is not None:
|
| 193 |
-
|
| 194 |
x = self.transformer(x)
|
| 195 |
|
| 196 |
a = self.attention(x)
|
|
|
|
| 130 |
self.attention = nn.Sequential(nn.Linear(nfc, 2048), nn.Tanh(), nn.Linear(2048, 1))
|
| 131 |
|
| 132 |
elif self.mil_mode == "att_trans":
|
| 133 |
+
transformer = nn.TransformerEncoderLayer(
|
| 134 |
+
d_model=nfc, nhead=8, dropout=trans_dropout, batch_first=True
|
| 135 |
+
)
|
| 136 |
self.transformer = nn.TransformerEncoder(transformer, num_layers=trans_blocks)
|
| 137 |
self.attention = nn.Sequential(nn.Linear(nfc, 2048), nn.Tanh(), nn.Linear(2048, 1))
|
| 138 |
|
|
|
|
| 192 |
x = self.myfc(x)
|
| 193 |
|
| 194 |
elif self.mil_mode == "att_trans" and self.transformer is not None:
|
|
|
|
| 195 |
x = self.transformer(x)
|
| 196 |
|
| 197 |
a = self.attention(x)
|
src/preprocessing/clip_intensity.py
CHANGED
|
@@ -7,20 +7,18 @@ import numpy as np
|
|
| 7 |
from tqdm import tqdm
|
| 8 |
|
| 9 |
|
| 10 |
-
def clip_adc(args: argparse.Namespace, adc_min
|
| 11 |
-
|
| 12 |
files = os.listdir(args.adc_dir)
|
| 13 |
clip_adc_dir = os.path.join(args.output_dir, "ADC_clipped")
|
| 14 |
os.makedirs(clip_adc_dir, exist_ok=True)
|
| 15 |
logging.info("Starting clipping ADC")
|
| 16 |
|
| 17 |
for file in tqdm(files):
|
| 18 |
-
|
| 19 |
adc, header_adc = nrrd.read(os.path.join(args.adc_dir, file))
|
| 20 |
-
|
| 21 |
if np.percentile(adc, 99) < 100:
|
| 22 |
adc = adc * 100
|
| 23 |
-
|
| 24 |
adc_clipped = np.clip(adc, adc_min, adc_max)
|
| 25 |
adc_normalized = adc_clipped / adc_max
|
| 26 |
nrrd.write(os.path.join(clip_adc_dir, file), adc_normalized, header_adc)
|
|
|
|
| 7 |
from tqdm import tqdm
|
| 8 |
|
| 9 |
|
| 10 |
+
def clip_adc(args: argparse.Namespace, adc_min=0.0, adc_max=3500.0):
|
|
|
|
| 11 |
files = os.listdir(args.adc_dir)
|
| 12 |
clip_adc_dir = os.path.join(args.output_dir, "ADC_clipped")
|
| 13 |
os.makedirs(clip_adc_dir, exist_ok=True)
|
| 14 |
logging.info("Starting clipping ADC")
|
| 15 |
|
| 16 |
for file in tqdm(files):
|
|
|
|
| 17 |
adc, header_adc = nrrd.read(os.path.join(args.adc_dir, file))
|
| 18 |
+
|
| 19 |
if np.percentile(adc, 99) < 100:
|
| 20 |
adc = adc * 100
|
| 21 |
+
|
| 22 |
adc_clipped = np.clip(adc, adc_min, adc_max)
|
| 23 |
adc_normalized = adc_clipped / adc_max
|
| 24 |
nrrd.write(os.path.join(clip_adc_dir, file), adc_normalized, header_adc)
|
src/preprocessing/generate_heatmap.py
CHANGED
|
@@ -1,50 +1,18 @@
|
|
| 1 |
import argparse
|
| 2 |
import logging
|
| 3 |
import os
|
| 4 |
-
import
|
| 5 |
|
| 6 |
import nrrd
|
| 7 |
import numpy as np
|
|
|
|
| 8 |
from tqdm import tqdm
|
| 9 |
-
from scipy.ndimage import binary_dilation
|
| 10 |
-
import shutil
|
| 11 |
|
| 12 |
-
'''
|
| 13 |
-
def dilate_prostate_mask_xyz(mask_array, expand_mm=2.0, pixel_spacing_xy=0.4):
|
| 14 |
-
"""
|
| 15 |
-
Dilates a 3D prostate mask only in the axial (X,Y) plane.
|
| 16 |
-
Specifically designed for arrays where Z is the LAST dimension (X, Y, Z).
|
| 17 |
-
|
| 18 |
-
Args:
|
| 19 |
-
mask_array: 3D numpy array of the mask (shape: 232, 232, 23)
|
| 20 |
-
expand_mm: How many millimeters of safety margin you want to add
|
| 21 |
-
pixel_spacing_xy: The in-plane spacing (e.g., 0.4 mm)
|
| 22 |
-
"""
|
| 23 |
-
# 1. Calculate how many pixels to expand radially
|
| 24 |
-
iterations = int(np.round(expand_mm / pixel_spacing_xy))
|
| 25 |
-
|
| 26 |
-
# 2. Create the structuring element for (X, Y, Z)
|
| 27 |
-
structuring_element = np.zeros((3, 3, 3), dtype=bool)
|
| 28 |
-
|
| 29 |
-
# We set the middle Z-index (index 1) to True across all X and Y.
|
| 30 |
-
# This forces the math to spread outward in X and Y, but never up/down in Z.
|
| 31 |
-
structuring_element[:, :, 1] = True
|
| 32 |
-
|
| 33 |
-
# 3. Apply the dilation
|
| 34 |
-
dilated_mask = binary_dilation(mask_array > 0,
|
| 35 |
-
structure=structuring_element,
|
| 36 |
-
iterations=iterations)
|
| 37 |
-
|
| 38 |
-
# 4. Return as the original integer type
|
| 39 |
-
return dilated_mask.astype(mask_array.dtype)
|
| 40 |
-
'''
|
| 41 |
|
| 42 |
def smoothen_mask(args, sigma=1.0):
|
| 43 |
-
|
| 44 |
files = os.listdir(args.seg_dir)
|
| 45 |
-
|
| 46 |
for file in files:
|
| 47 |
-
|
| 48 |
mask = sitk.ReadImage(os.path.join(args.seg_dir, file))
|
| 49 |
mask = sitk.Cast(mask, sitk.sitkFloat32)
|
| 50 |
|
|
@@ -54,12 +22,11 @@ def smoothen_mask(args, sigma=1.0):
|
|
| 54 |
# Threshold back to binary
|
| 55 |
smooth_mask = smoothed > 0.5
|
| 56 |
smooth_mask = sitk.Cast(smooth_mask, sitk.sitkUInt8)
|
| 57 |
-
|
| 58 |
out_path = os.path.join(args.smooth_seg_dir_temp, file)
|
| 59 |
sitk.WriteImage(smooth_mask, out_path)
|
| 60 |
|
| 61 |
|
| 62 |
-
|
| 63 |
def get_heatmap(args: argparse.Namespace) -> argparse.Namespace:
|
| 64 |
"""
|
| 65 |
Generate heatmaps from DWI (Diffusion Weighted Imaging) and ADC (Apparent Diffusion Coefficient) medical imaging data.
|
|
@@ -95,13 +62,13 @@ def get_heatmap(args: argparse.Namespace) -> argparse.Namespace:
|
|
| 95 |
args.smooth_seg_dir = os.path.join(args.output_dir, "smooth_prostate_mask/")
|
| 96 |
os.makedirs(args.smooth_seg_dir, exist_ok=True)
|
| 97 |
smoothen_mask(args)
|
| 98 |
-
|
| 99 |
logging.info("Starting heatmap generation")
|
| 100 |
for file in tqdm(files):
|
| 101 |
bool_dwi = False
|
| 102 |
bool_adc = False
|
| 103 |
mask_temp, header_mask = nrrd.read(os.path.join(args.seg_dir, file))
|
| 104 |
-
#spacing = np.linalg.norm(header_mask['space directions'], axis=1)
|
| 105 |
dwi, header_dwi = nrrd.read(os.path.join(args.dwi_dir, file))
|
| 106 |
adc, header_adc = nrrd.read(os.path.join(args.adc_dir, file))
|
| 107 |
nonzero_vals_dwi = dwi[mask_temp > 0]
|
|
@@ -109,12 +76,11 @@ def get_heatmap(args: argparse.Namespace) -> argparse.Namespace:
|
|
| 109 |
mask = np.maximum(mask, mask_temp)
|
| 110 |
|
| 111 |
if len(nonzero_vals_dwi) > 0:
|
| 112 |
-
#min_val = nonzero_vals_dwi.min()
|
| 113 |
-
#max_val = nonzero_vals_dwi.max()
|
| 114 |
min_val, max_val = np.percentile(nonzero_vals_dwi, [1, 99])
|
| 115 |
clipped_dwi = np.clip(dwi, min_val, max_val)
|
| 116 |
|
| 117 |
-
|
| 118 |
heatmap_dwi = np.zeros_like(clipped_dwi, dtype=np.float32)
|
| 119 |
|
| 120 |
if min_val != max_val:
|
|
@@ -135,13 +101,12 @@ def get_heatmap(args: argparse.Namespace) -> argparse.Namespace:
|
|
| 135 |
heatmap_adc = (max_val - adc) / (max_val - min_val)
|
| 136 |
masked_heatmap_adc = np.where(mask > 0, heatmap_adc, 0)
|
| 137 |
|
| 138 |
-
|
| 139 |
else:
|
| 140 |
bool_adc = True
|
| 141 |
|
| 142 |
if not bool_dwi and not bool_adc:
|
| 143 |
-
mix_mask = (masked_heatmap_dwi * 0.3
|
| 144 |
-
#mix_mask = (masked_heatmap_dwi**0.5) * (masked_heatmap_adc**2.0)
|
| 145 |
write_header = header_dwi
|
| 146 |
elif bool_dwi:
|
| 147 |
mix_mask = masked_heatmap_adc
|
|
@@ -150,8 +115,10 @@ def get_heatmap(args: argparse.Namespace) -> argparse.Namespace:
|
|
| 150 |
mix_mask = np.ones_like(adc, dtype=np.float32)
|
| 151 |
write_header = header_dwi
|
| 152 |
|
| 153 |
-
mix_mask = (mix_mask - mix_mask[mask>0].min()) / (
|
| 154 |
-
|
|
|
|
|
|
|
| 155 |
nrrd.write(os.path.join(args.heatmapdir, file), mix_mask, write_header)
|
| 156 |
nrrd.write(os.path.join(args.smooth_seg_dir, file), mask, header_mask)
|
| 157 |
|
|
|
|
| 1 |
import argparse
|
| 2 |
import logging
|
| 3 |
import os
|
| 4 |
+
import shutil
|
| 5 |
|
| 6 |
import nrrd
|
| 7 |
import numpy as np
|
| 8 |
+
import SimpleITK as sitk
|
| 9 |
from tqdm import tqdm
|
|
|
|
|
|
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
def smoothen_mask(args, sigma=1.0):
|
|
|
|
| 13 |
files = os.listdir(args.seg_dir)
|
| 14 |
+
|
| 15 |
for file in files:
|
|
|
|
| 16 |
mask = sitk.ReadImage(os.path.join(args.seg_dir, file))
|
| 17 |
mask = sitk.Cast(mask, sitk.sitkFloat32)
|
| 18 |
|
|
|
|
| 22 |
# Threshold back to binary
|
| 23 |
smooth_mask = smoothed > 0.5
|
| 24 |
smooth_mask = sitk.Cast(smooth_mask, sitk.sitkUInt8)
|
| 25 |
+
|
| 26 |
out_path = os.path.join(args.smooth_seg_dir_temp, file)
|
| 27 |
sitk.WriteImage(smooth_mask, out_path)
|
| 28 |
|
| 29 |
|
|
|
|
| 30 |
def get_heatmap(args: argparse.Namespace) -> argparse.Namespace:
|
| 31 |
"""
|
| 32 |
Generate heatmaps from DWI (Diffusion Weighted Imaging) and ADC (Apparent Diffusion Coefficient) medical imaging data.
|
|
|
|
| 62 |
args.smooth_seg_dir = os.path.join(args.output_dir, "smooth_prostate_mask/")
|
| 63 |
os.makedirs(args.smooth_seg_dir, exist_ok=True)
|
| 64 |
smoothen_mask(args)
|
| 65 |
+
|
| 66 |
logging.info("Starting heatmap generation")
|
| 67 |
for file in tqdm(files):
|
| 68 |
bool_dwi = False
|
| 69 |
bool_adc = False
|
| 70 |
mask_temp, header_mask = nrrd.read(os.path.join(args.seg_dir, file))
|
| 71 |
+
# spacing = np.linalg.norm(header_mask['space directions'], axis=1)
|
| 72 |
dwi, header_dwi = nrrd.read(os.path.join(args.dwi_dir, file))
|
| 73 |
adc, header_adc = nrrd.read(os.path.join(args.adc_dir, file))
|
| 74 |
nonzero_vals_dwi = dwi[mask_temp > 0]
|
|
|
|
| 76 |
mask = np.maximum(mask, mask_temp)
|
| 77 |
|
| 78 |
if len(nonzero_vals_dwi) > 0:
|
| 79 |
+
# min_val = nonzero_vals_dwi.min()
|
| 80 |
+
# max_val = nonzero_vals_dwi.max()
|
| 81 |
min_val, max_val = np.percentile(nonzero_vals_dwi, [1, 99])
|
| 82 |
clipped_dwi = np.clip(dwi, min_val, max_val)
|
| 83 |
|
|
|
|
| 84 |
heatmap_dwi = np.zeros_like(clipped_dwi, dtype=np.float32)
|
| 85 |
|
| 86 |
if min_val != max_val:
|
|
|
|
| 101 |
heatmap_adc = (max_val - adc) / (max_val - min_val)
|
| 102 |
masked_heatmap_adc = np.where(mask > 0, heatmap_adc, 0)
|
| 103 |
|
|
|
|
| 104 |
else:
|
| 105 |
bool_adc = True
|
| 106 |
|
| 107 |
if not bool_dwi and not bool_adc:
|
| 108 |
+
mix_mask = (masked_heatmap_dwi * 0.3) + (masked_heatmap_adc * 0.7)
|
| 109 |
+
# mix_mask = (masked_heatmap_dwi**0.5) * (masked_heatmap_adc**2.0)
|
| 110 |
write_header = header_dwi
|
| 111 |
elif bool_dwi:
|
| 112 |
mix_mask = masked_heatmap_adc
|
|
|
|
| 115 |
mix_mask = np.ones_like(adc, dtype=np.float32)
|
| 116 |
write_header = header_dwi
|
| 117 |
|
| 118 |
+
mix_mask = (mix_mask - mix_mask[mask > 0].min()) / (
|
| 119 |
+
mix_mask[mask > 0].max() - mix_mask[mask > 0].min()
|
| 120 |
+
)
|
| 121 |
+
mix_mask = np.where(mask > 0, mix_mask, 0)
|
| 122 |
nrrd.write(os.path.join(args.heatmapdir, file), mix_mask, write_header)
|
| 123 |
nrrd.write(os.path.join(args.smooth_seg_dir, file), mask, header_mask)
|
| 124 |
|
src/preprocessing/prostate_mask.py
CHANGED
|
@@ -22,10 +22,12 @@ from tqdm import tqdm
|
|
| 22 |
|
| 23 |
set_determinism(43)
|
| 24 |
|
|
|
|
| 25 |
def is_continuous(idx):
|
| 26 |
idx = sorted(idx)
|
| 27 |
return all(b - a == 1 for a, b in zip(idx, idx[1:]))
|
| 28 |
|
|
|
|
| 29 |
def get_segmask(args: argparse.Namespace) -> argparse.Namespace:
|
| 30 |
"""
|
| 31 |
Generate prostate segmentation masks using a pre-trained deep learning model.
|
|
@@ -110,8 +112,10 @@ def get_segmask(args: argparse.Namespace) -> argparse.Namespace:
|
|
| 110 |
top_slices = np.argsort(nonzero_counts)[-10:]
|
| 111 |
try:
|
| 112 |
assert is_continuous(top_slices), "Top slices are not continuous"
|
| 113 |
-
except AssertionError
|
| 114 |
-
logging.warning(
|
|
|
|
|
|
|
| 115 |
output_ = np.zeros_like(pred_nrrd)
|
| 116 |
output_[:, :, top_slices] = pred_nrrd[:, :, top_slices]
|
| 117 |
|
|
|
|
| 22 |
|
| 23 |
set_determinism(43)
|
| 24 |
|
| 25 |
+
|
| 26 |
def is_continuous(idx):
|
| 27 |
idx = sorted(idx)
|
| 28 |
return all(b - a == 1 for a, b in zip(idx, idx[1:]))
|
| 29 |
|
| 30 |
+
|
| 31 |
def get_segmask(args: argparse.Namespace) -> argparse.Namespace:
|
| 32 |
"""
|
| 33 |
Generate prostate segmentation masks using a pre-trained deep learning model.
|
|
|
|
| 112 |
top_slices = np.argsort(nonzero_counts)[-10:]
|
| 113 |
try:
|
| 114 |
assert is_continuous(top_slices), "Top slices are not continuous"
|
| 115 |
+
except AssertionError:
|
| 116 |
+
logging.warning(
|
| 117 |
+
f"Top slices for {file} are not continuous: {top_slices}. Proceeding with non-continuous slices."
|
| 118 |
+
)
|
| 119 |
output_ = np.zeros_like(pred_nrrd)
|
| 120 |
output_[:, :, top_slices] = pred_nrrd[:, :, top_slices]
|
| 121 |
|
src/preprocessing/register_and_crop.py
CHANGED
|
@@ -84,85 +84,3 @@ def register_files(args: argparse.Namespace) -> argparse.Namespace:
|
|
| 84 |
args.adc_dir = adc_registered_dir
|
| 85 |
|
| 86 |
return args
|
| 87 |
-
'''
|
| 88 |
-
|
| 89 |
-
def create_master_grid(image, target_spacing=[0.4, 0.4, 3.0]):
|
| 90 |
-
"""
|
| 91 |
-
Resamples an image to the target spacing to create the Master Grid.
|
| 92 |
-
"""
|
| 93 |
-
original_spacing = image.GetSpacing()
|
| 94 |
-
original_size = image.GetSize()
|
| 95 |
-
|
| 96 |
-
# Calculate the new dimensions based on the target spacing
|
| 97 |
-
new_size = [
|
| 98 |
-
int(np.round(original_size[0] * (original_spacing[0] / target_spacing[0]))),
|
| 99 |
-
int(np.round(original_size[1] * (original_spacing[1] / target_spacing[1]))),
|
| 100 |
-
int(np.round(original_size[2] * (original_spacing[2] / target_spacing[2])))
|
| 101 |
-
]
|
| 102 |
-
|
| 103 |
-
# Setup the resampler
|
| 104 |
-
resampler = sitk.ResampleImageFilter()
|
| 105 |
-
resampler.SetSize(new_size)
|
| 106 |
-
resampler.SetOutputSpacing(target_spacing)
|
| 107 |
-
resampler.SetOutputOrigin(image.GetOrigin())
|
| 108 |
-
resampler.SetOutputDirection(image.GetDirection())
|
| 109 |
-
# B-Spline is excellent for T2W anatomical structure
|
| 110 |
-
resampler.SetInterpolator(sitk.sitkBSpline)
|
| 111 |
-
|
| 112 |
-
return resampler.Execute(image)
|
| 113 |
-
|
| 114 |
-
def register_files(args: argparse.Namespace) -> argparse.Namespace:
|
| 115 |
-
|
| 116 |
-
files = os.listdir(args.t2_dir)
|
| 117 |
-
new_spacing = [0.4, 0.4, 3.0]
|
| 118 |
-
t2_registered_dir = os.path.join(args.output_dir, "t2_registered")
|
| 119 |
-
dwi_registered_dir = os.path.join(args.output_dir, "DWI_registered")
|
| 120 |
-
adc_registered_dir = os.path.join(args.output_dir, "ADC_registered")
|
| 121 |
-
os.makedirs(t2_registered_dir, exist_ok=True)
|
| 122 |
-
os.makedirs(dwi_registered_dir, exist_ok=True)
|
| 123 |
-
os.makedirs(adc_registered_dir, exist_ok=True)
|
| 124 |
-
logging.info("Starting registration and cropping")
|
| 125 |
-
for file in tqdm(files):
|
| 126 |
-
|
| 127 |
-
t2w = sitk.ReadImage(os.path.join(args.t2_dir, file), sitk.sitkFloat32)
|
| 128 |
-
dwi = sitk.ReadImage(os.path.join(args.dwi_dir, file), sitk.sitkFloat32)
|
| 129 |
-
adc = sitk.ReadImage(os.path.join(args.adc_dir, file), sitk.sitkFloat32)
|
| 130 |
-
|
| 131 |
-
t2w_master = create_master_grid(t2w, target_spacing=new_spacing)
|
| 132 |
-
|
| 133 |
-
registration = sitk.ImageRegistrationMethod()
|
| 134 |
-
registration.SetMetricAsMattesMutualInformation(numberOfHistogramBins=50)
|
| 135 |
-
registration.SetMetricSamplingStrategy(registration.RANDOM)
|
| 136 |
-
registration.SetMetricSamplingPercentage(0.1)
|
| 137 |
-
registration.SetOptimizerAsGradientDescent(learningRate=1.0,
|
| 138 |
-
numberOfIterations=100,
|
| 139 |
-
convergenceMinimumValue=1e-6,
|
| 140 |
-
convergenceWindowSize=10)
|
| 141 |
-
|
| 142 |
-
initial_transform = sitk.CenteredTransformInitializer(
|
| 143 |
-
t2w_master, dwi, sitk.TranslationTransform(3), sitk.CenteredTransformInitializerFilter.GEOMETRY
|
| 144 |
-
)
|
| 145 |
-
registration.SetInitialTransform(initial_transform, inPlace=False)
|
| 146 |
-
final_transform = registration.Execute(t2w_master, dwi)
|
| 147 |
-
|
| 148 |
-
dwi_aligned = sitk.Resample(
|
| 149 |
-
dwi, t2w_master, final_transform, sitk.sitkLinear, 0.0, dwi.GetPixelID()
|
| 150 |
-
)
|
| 151 |
-
adc_aligned = sitk.Resample(
|
| 152 |
-
adc, t2w_master, final_transform, sitk.sitkLinear, 0.0, adc.GetPixelID()
|
| 153 |
-
)
|
| 154 |
-
|
| 155 |
-
cropped_t2 = crop(t2w_master, [args.margin, args.margin, 0.0])
|
| 156 |
-
cropped_dwi = crop(dwi_aligned, [args.margin, args.margin, 0.0])
|
| 157 |
-
cropped_adc = crop(adc_aligned, [args.margin, args.margin, 0.0])
|
| 158 |
-
|
| 159 |
-
sitk.WriteImage(cropped_t2, os.path.join(t2_registered_dir, file))
|
| 160 |
-
sitk.WriteImage(cropped_dwi, os.path.join(dwi_registered_dir, file))
|
| 161 |
-
sitk.WriteImage(cropped_adc, os.path.join(adc_registered_dir, file))
|
| 162 |
-
|
| 163 |
-
args.t2_dir = t2_registered_dir
|
| 164 |
-
args.dwi_dir = dwi_registered_dir
|
| 165 |
-
args.adc_dir = adc_registered_dir
|
| 166 |
-
|
| 167 |
-
return args
|
| 168 |
-
'''
|
|
|
|
| 84 |
args.adc_dir = adc_registered_dir
|
| 85 |
|
| 86 |
return args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/train/train_cspca.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
|
|
|
|
|
| 1 |
import torch
|
| 2 |
import torch.nn as nn
|
| 3 |
from monai.metrics import Cumulative, CumulativeAverage
|
| 4 |
from sklearn.metrics import confusion_matrix, roc_auc_score
|
| 5 |
-
import argparse
|
| 6 |
|
| 7 |
|
| 8 |
def get_lambda_att(epoch: int, max_lambda: float = 2.0, warmup_epochs: int = 10) -> float:
|
|
@@ -10,7 +11,8 @@ def get_lambda_att(epoch: int, max_lambda: float = 2.0, warmup_epochs: int = 10)
|
|
| 10 |
return (epoch / warmup_epochs) * max_lambda
|
| 11 |
else:
|
| 12 |
return max_lambda
|
| 13 |
-
|
|
|
|
| 14 |
def get_attention_scores(
|
| 15 |
data: torch.Tensor,
|
| 16 |
target: torch.Tensor,
|
|
@@ -68,10 +70,10 @@ def get_attention_scores(
|
|
| 68 |
|
| 69 |
return att_labels, shuffled_images
|
| 70 |
|
|
|
|
| 71 |
def train_epoch(cspca_model, loader, optimizer, epoch, args):
|
| 72 |
-
|
| 73 |
lambda_att = get_lambda_att(epoch, warmup_epochs=25)
|
| 74 |
-
|
| 75 |
cspca_model.train()
|
| 76 |
criterion = nn.BCEWithLogitsLoss()
|
| 77 |
att_criterion = nn.CosineSimilarity(dim=1, eps=1e-6)
|
|
@@ -87,7 +89,7 @@ def train_epoch(cspca_model, loader, optimizer, epoch, args):
|
|
| 87 |
data = batch_data["image"].as_subclass(torch.Tensor).to(args.device)
|
| 88 |
target = batch_data["label"].as_subclass(torch.Tensor).to(args.device)
|
| 89 |
psa_data = batch_data["psa"].as_subclass(torch.Tensor).to(args.device)
|
| 90 |
-
|
| 91 |
if args.use_heatmap:
|
| 92 |
att_labels, shuffled_images = get_attention_scores(
|
| 93 |
data, target, batch_data["final_heatmap"], batch_data["smooth_mask"], args
|
|
@@ -95,9 +97,9 @@ def train_epoch(cspca_model, loader, optimizer, epoch, args):
|
|
| 95 |
att_labels = att_labels + eps
|
| 96 |
else:
|
| 97 |
shuffled_images = data.to(args.device)
|
| 98 |
-
|
| 99 |
optimizer.zero_grad()
|
| 100 |
-
output = cspca_model(x
|
| 101 |
output = output.squeeze(1)
|
| 102 |
class_loss = criterion(output, target)
|
| 103 |
if args.use_heatmap:
|
|
@@ -117,8 +119,7 @@ def train_epoch(cspca_model, loader, optimizer, epoch, args):
|
|
| 117 |
else:
|
| 118 |
loss = class_loss
|
| 119 |
attn_loss = torch.tensor(0.0)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
loss.backward()
|
| 123 |
optimizer.step()
|
| 124 |
|
|
@@ -149,7 +150,7 @@ def val_epoch(cspca_model, loader, epoch, args):
|
|
| 149 |
target = batch_data["label"].as_subclass(torch.Tensor).to(args.device)
|
| 150 |
psa_data = batch_data["psa"].as_subclass(torch.Tensor).to(args.device)
|
| 151 |
|
| 152 |
-
output = cspca_model(x
|
| 153 |
output = output.squeeze(1)
|
| 154 |
loss = criterion(output, target)
|
| 155 |
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
|
| 3 |
import torch
|
| 4 |
import torch.nn as nn
|
| 5 |
from monai.metrics import Cumulative, CumulativeAverage
|
| 6 |
from sklearn.metrics import confusion_matrix, roc_auc_score
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
def get_lambda_att(epoch: int, max_lambda: float = 2.0, warmup_epochs: int = 10) -> float:
|
|
|
|
| 11 |
return (epoch / warmup_epochs) * max_lambda
|
| 12 |
else:
|
| 13 |
return max_lambda
|
| 14 |
+
|
| 15 |
+
|
| 16 |
def get_attention_scores(
|
| 17 |
data: torch.Tensor,
|
| 18 |
target: torch.Tensor,
|
|
|
|
| 70 |
|
| 71 |
return att_labels, shuffled_images
|
| 72 |
|
| 73 |
+
|
| 74 |
def train_epoch(cspca_model, loader, optimizer, epoch, args):
|
|
|
|
| 75 |
lambda_att = get_lambda_att(epoch, warmup_epochs=25)
|
| 76 |
+
|
| 77 |
cspca_model.train()
|
| 78 |
criterion = nn.BCEWithLogitsLoss()
|
| 79 |
att_criterion = nn.CosineSimilarity(dim=1, eps=1e-6)
|
|
|
|
| 89 |
data = batch_data["image"].as_subclass(torch.Tensor).to(args.device)
|
| 90 |
target = batch_data["label"].as_subclass(torch.Tensor).to(args.device)
|
| 91 |
psa_data = batch_data["psa"].as_subclass(torch.Tensor).to(args.device)
|
| 92 |
+
|
| 93 |
if args.use_heatmap:
|
| 94 |
att_labels, shuffled_images = get_attention_scores(
|
| 95 |
data, target, batch_data["final_heatmap"], batch_data["smooth_mask"], args
|
|
|
|
| 97 |
att_labels = att_labels + eps
|
| 98 |
else:
|
| 99 |
shuffled_images = data.to(args.device)
|
| 100 |
+
|
| 101 |
optimizer.zero_grad()
|
| 102 |
+
output = cspca_model(x=shuffled_images, psa_data=psa_data)
|
| 103 |
output = output.squeeze(1)
|
| 104 |
class_loss = criterion(output, target)
|
| 105 |
if args.use_heatmap:
|
|
|
|
| 119 |
else:
|
| 120 |
loss = class_loss
|
| 121 |
attn_loss = torch.tensor(0.0)
|
| 122 |
+
|
|
|
|
| 123 |
loss.backward()
|
| 124 |
optimizer.step()
|
| 125 |
|
|
|
|
| 150 |
target = batch_data["label"].as_subclass(torch.Tensor).to(args.device)
|
| 151 |
psa_data = batch_data["psa"].as_subclass(torch.Tensor).to(args.device)
|
| 152 |
|
| 153 |
+
output = cspca_model(x=data, psa_data=psa_data)
|
| 154 |
output = output.squeeze(1)
|
| 155 |
loss = criterion(output, target)
|
| 156 |
|
src/train/train_pirads.py
CHANGED
|
@@ -152,7 +152,7 @@ def train_epoch(model, loader, optimizer, scaler, epoch, args):
|
|
| 152 |
start_time = time.time()
|
| 153 |
|
| 154 |
del data, target, shuffled_images, logits, logits_attn
|
| 155 |
-
#torch.cuda.empty_cache()
|
| 156 |
batch_norm_epoch = batch_norm.aggregate()
|
| 157 |
attn_loss_epoch = run_att_loss.aggregate()
|
| 158 |
loss_epoch = run_loss.aggregate()
|
|
@@ -197,7 +197,7 @@ def val_epoch(model, loader, epoch, args):
|
|
| 197 |
start_time = time.time()
|
| 198 |
|
| 199 |
del data, target, logits
|
| 200 |
-
#torch.cuda.empty_cache()
|
| 201 |
|
| 202 |
# Calculate QWK metric (Quadratic Weigted Kappa) https://en.wikipedia.org/wiki/Cohen%27s_kappa
|
| 203 |
preds_cumulative = preds_cumulative.get_buffer().cpu().numpy()
|
|
|
|
| 152 |
start_time = time.time()
|
| 153 |
|
| 154 |
del data, target, shuffled_images, logits, logits_attn
|
| 155 |
+
# torch.cuda.empty_cache()
|
| 156 |
batch_norm_epoch = batch_norm.aggregate()
|
| 157 |
attn_loss_epoch = run_att_loss.aggregate()
|
| 158 |
loss_epoch = run_loss.aggregate()
|
|
|
|
| 197 |
start_time = time.time()
|
| 198 |
|
| 199 |
del data, target, logits
|
| 200 |
+
# torch.cuda.empty_cache()
|
| 201 |
|
| 202 |
# Calculate QWK metric (Quadratic Weigted Kappa) https://en.wikipedia.org/wiki/Cohen%27s_kappa
|
| 203 |
preds_cumulative = preds_cumulative.get_buffer().cpu().numpy()
|
src/utils.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import Any, Union
|
|
| 8 |
import cv2
|
| 9 |
import matplotlib.patches as patches
|
| 10 |
import matplotlib.pyplot as plt
|
|
|
|
| 11 |
import numpy as np
|
| 12 |
import torch
|
| 13 |
from monai.data import Dataset
|
|
@@ -245,3 +246,19 @@ def visualise_patches(coords, image, tile_size=64, depth=3):
|
|
| 245 |
plt.subplots_adjust(left=0.06)
|
| 246 |
plt.tight_layout()
|
| 247 |
plt.show()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import cv2
|
| 9 |
import matplotlib.patches as patches
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
+
import nrrd
|
| 12 |
import numpy as np
|
| 13 |
import torch
|
| 14 |
from monai.data import Dataset
|
|
|
|
| 246 |
plt.subplots_adjust(left=0.06)
|
| 247 |
plt.tight_layout()
|
| 248 |
plt.show()
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def get_prostate_volume(mask_path) -> np.ndarray:
|
| 252 |
+
mask_data, header = nrrd.read(mask_path)
|
| 253 |
+
space_directions = header.get("space directions")
|
| 254 |
+
spacing = np.array([np.linalg.norm(vec) for vec in space_directions if np.any(vec)])
|
| 255 |
+
|
| 256 |
+
if len(spacing) != 3:
|
| 257 |
+
raise ValueError(f"Expected 3 spatial dimensions, found {len(spacing)}")
|
| 258 |
+
|
| 259 |
+
voxel_volume_mm3 = np.prod(spacing)
|
| 260 |
+
voxel_count = np.sum(mask_data > 0)
|
| 261 |
+
true_volume_mm3 = voxel_count * voxel_volume_mm3
|
| 262 |
+
true_volume_cc = true_volume_mm3 / 1000.0 # Convert mm³ to cc (mL)
|
| 263 |
+
|
| 264 |
+
return true_volume_cc
|
tcia_dataset.ipynb
CHANGED
|
@@ -7,16 +7,14 @@
|
|
| 7 |
"metadata": {},
|
| 8 |
"outputs": [],
|
| 9 |
"source": [
|
|
|
|
| 10 |
"import os\n",
|
| 11 |
-
"import nrrd\n",
|
| 12 |
-
"import numpy as np\n",
|
| 13 |
-
"import nrrd\n",
|
| 14 |
-
"import SimpleITK as sitk\n",
|
| 15 |
-
"from AIAH_utility.viewer import BasicViewer\n",
|
| 16 |
-
"from tqdm import tqdm\n",
|
| 17 |
"import re\n",
|
|
|
|
|
|
|
| 18 |
"import pandas as pd\n",
|
| 19 |
-
"import
|
|
|
|
| 20 |
]
|
| 21 |
},
|
| 22 |
{
|
|
@@ -52,15 +50,15 @@
|
|
| 52 |
}
|
| 53 |
],
|
| 54 |
"source": [
|
| 55 |
-
"data_dir =
|
| 56 |
-
"out_dir =
|
| 57 |
"print(len(os.listdir(data_dir)))\n",
|
| 58 |
-
"
|
| 59 |
"for file in os.listdir(data_dir):\n",
|
| 60 |
" if not os.path.isfile(os.path.join(data_dir, file)):\n",
|
| 61 |
" if len(os.listdir(os.path.join(data_dir, file))) > 1:\n",
|
| 62 |
" print(file)\n",
|
| 63 |
-
"
|
| 64 |
]
|
| 65 |
},
|
| 66 |
{
|
|
@@ -70,20 +68,18 @@
|
|
| 70 |
"metadata": {},
|
| 71 |
"outputs": [],
|
| 72 |
"source": [
|
| 73 |
-
"\n",
|
| 74 |
-
"\n",
|
| 75 |
"def identify_mri_sequence(dicom_dir):\n",
|
| 76 |
" # 1. Get all DICOM files in the folder\n",
|
| 77 |
" reader = sitk.ImageSeriesReader()\n",
|
| 78 |
" series_IDs = reader.GetGDCMSeriesIDs(dicom_dir)\n",
|
| 79 |
-
"
|
| 80 |
" if not series_IDs:\n",
|
| 81 |
" print(\"No DICOM files found.\")\n",
|
| 82 |
" return None\n",
|
| 83 |
"\n",
|
| 84 |
" # Get the filenames for the first series found\n",
|
| 85 |
" dicom_names = reader.GetGDCMSeriesFileNames(dicom_dir, series_IDs[0])\n",
|
| 86 |
-
" first_file = dicom_names[0]
|
| 87 |
"\n",
|
| 88 |
" # 2. Read only the header information (fast, doesn't load image pixels)\n",
|
| 89 |
" file_reader = sitk.ImageFileReader()\n",
|
|
@@ -97,7 +93,7 @@
|
|
| 97 |
" except RuntimeError:\n",
|
| 98 |
" series_description = \"unknown (tag missing)\"\n",
|
| 99 |
"\n",
|
| 100 |
-
" #print(f\"Raw DICOM Series Description: '{series_description}'\")\n",
|
| 101 |
"\n",
|
| 102 |
" # 4. Keyword matching to identify the sequence\n",
|
| 103 |
" sequence_type = \"Unclassified\"\n",
|
|
@@ -108,42 +104,40 @@
|
|
| 108 |
" sequence_type = \"t2\"\n",
|
| 109 |
" elif \"adc\" in desc_lower:\n",
|
| 110 |
" sequence_type = \"adc\"\n",
|
| 111 |
-
" elif any(word in desc_lower for word in dwi_keywords) or re.search(r
|
| 112 |
" sequence_type = \"dwi\"\n",
|
| 113 |
"\n",
|
| 114 |
-
"
|
| 115 |
-
" #print(f\"Identified Sequence Type: {sequence_type}\")\n",
|
| 116 |
" return sequence_type, series_description\n",
|
| 117 |
"\n",
|
| 118 |
"\n",
|
| 119 |
-
"\n",
|
| 120 |
"def dicom_to_nrrd(dicom_dir, output_filepath):\n",
|
| 121 |
" # 1. Initialize the ImageSeriesReader\n",
|
| 122 |
" reader = sitk.ImageSeriesReader()\n",
|
| 123 |
"\n",
|
| 124 |
" # 2. Find the DICOM series within the folder\n",
|
| 125 |
-
" # A single folder might contain multiple scans (e.g., a T1 and a T2).
|
| 126 |
" # This function groups them by their unique Series Instance UID.\n",
|
| 127 |
" series_IDs = reader.GetGDCMSeriesIDs(dicom_dir)\n",
|
| 128 |
-
"
|
| 129 |
" if not series_IDs:\n",
|
| 130 |
" print(f\"Error: No DICOM series found in the directory {dicom_dir}.\")\n",
|
| 131 |
" return\n",
|
| 132 |
"\n",
|
| 133 |
" # Assuming you want the first (or only) series in the folder\n",
|
| 134 |
" series_ID = series_IDs[0]\n",
|
| 135 |
-
"
|
| 136 |
" # 3. Get the list of files belonging to this specific series.\n",
|
| 137 |
" # SimpleITK automatically sorts them by spatial position/instance number here.\n",
|
| 138 |
" dicom_names = reader.GetGDCMSeriesFileNames(dicom_dir, series_ID)\n",
|
| 139 |
" reader.SetFileNames(dicom_names)\n",
|
| 140 |
"\n",
|
| 141 |
-
" #print(f\"Found series {series_ID}\")\n",
|
| 142 |
-
" #print(f\"Reading {len(dicom_names)} DICOM files...\")\n",
|
| 143 |
"\n",
|
| 144 |
" # 4. Read the files into a single 3D image object\n",
|
| 145 |
" image = reader.Execute()\n",
|
| 146 |
-
"
|
| 147 |
" # (Optional) Print out the header info that SimpleITK extracted\n",
|
| 148 |
" print(\"\\nExtracted Header Information:\")\n",
|
| 149 |
" print(f\"Size (X, Y, Z): {image.GetSize()}\")\n",
|
|
@@ -153,13 +147,9 @@
|
|
| 153 |
"\n",
|
| 154 |
" # 5. Write the 3D image to an NRRD file\n",
|
| 155 |
" # SimpleITK automatically translates the spatial metadata into the NRRD header format.\n",
|
| 156 |
-
"
|
| 157 |
" sitk.WriteImage(image, output_filepath)\n",
|
| 158 |
-
" #print(f\"Successfully saved 3D volume to: {output_filepath}\")
|
| 159 |
-
"\n",
|
| 160 |
-
"\n",
|
| 161 |
-
"\n",
|
| 162 |
-
"\n"
|
| 163 |
]
|
| 164 |
},
|
| 165 |
{
|
|
@@ -171,28 +161,28 @@
|
|
| 171 |
"source": [
|
| 172 |
"import logging\n",
|
| 173 |
"\n",
|
| 174 |
-
"\n",
|
| 175 |
"# Keep your basic logging config\n",
|
| 176 |
"logging.basicConfig(\n",
|
| 177 |
-
" filename=
|
| 178 |
-
" filemode=
|
| 179 |
" level=logging.WARNING,\n",
|
| 180 |
-
" format=
|
| 181 |
")\n",
|
| 182 |
"\n",
|
|
|
|
| 183 |
"class SimpleITKWarningTracker(sitk.LoggerBase):\n",
|
| 184 |
" def __init__(self, logger: logging.Logger = logging.getLogger(\"SimpleITK\")):\n",
|
| 185 |
" super().__init__()\n",
|
| 186 |
" self._logger = logger\n",
|
| 187 |
-
"
|
| 188 |
" # --- NEW ADDITIONS ---\n",
|
| 189 |
" self.current_uid = None\n",
|
| 190 |
-
" self.problematic_uids = set()
|
| 191 |
"\n",
|
| 192 |
" def DisplayWarningText(self, s):\n",
|
| 193 |
" # 1. Log to the file, but inject the UID so you know exactly which one failed\n",
|
| 194 |
" self._logger.warning(f\"[{self.current_uid}] {s.rstrip()}\")\n",
|
| 195 |
-
"
|
| 196 |
" # 2. Capture the UID into our set\n",
|
| 197 |
" if self.current_uid is not None:\n",
|
| 198 |
" self.problematic_uids.add(self.current_uid)\n",
|
|
@@ -203,9 +193,14 @@
|
|
| 203 |
" self.problematic_uids.add(self.current_uid)\n",
|
| 204 |
"\n",
|
| 205 |
" # Standard passthroughs for everything else\n",
|
| 206 |
-
" def DisplayText(self, s):
|
| 207 |
-
"
|
| 208 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
]
|
| 210 |
},
|
| 211 |
{
|
|
@@ -224,26 +219,24 @@
|
|
| 224 |
}
|
| 225 |
],
|
| 226 |
"source": [
|
| 227 |
-
"\n",
|
| 228 |
-
"\n",
|
| 229 |
-
"\n",
|
| 230 |
"def get_clean_file_set(folder_path):\n",
|
| 231 |
" \"\"\"\n",
|
| 232 |
-
" Returns a set of filenames in a folder,
|
| 233 |
" ignoring hidden files like .DS_Store.\n",
|
| 234 |
" \"\"\"\n",
|
| 235 |
" if not os.path.exists(folder_path):\n",
|
| 236 |
" print(f\"Warning: Folder not found -> {folder_path}\")\n",
|
| 237 |
" return set()\n",
|
| 238 |
-
"
|
| 239 |
-
" return set(f for f in os.listdir(folder_path) if not f.startswith(
|
|
|
|
| 240 |
"\n",
|
| 241 |
"def audit_mri_folders(t2_dir, dwi_dir, adc_dir):\n",
|
| 242 |
" # 1. Grab the sets of files from each folder\n",
|
| 243 |
" t2_files = get_clean_file_set(t2_dir)\n",
|
| 244 |
" dwi_files = get_clean_file_set(dwi_dir)\n",
|
| 245 |
" adc_files = get_clean_file_set(adc_dir)\n",
|
| 246 |
-
"
|
| 247 |
" # 2. Check for absolute perfection\n",
|
| 248 |
" if t2_files == dwi_files == adc_files:\n",
|
| 249 |
" print(\"✅ SUCCESS: All three folders contain the exact same files.\")\n",
|
|
@@ -252,32 +245,36 @@
|
|
| 252 |
"\n",
|
| 253 |
" # 3. If they don't match, figure out exactly what went wrong\n",
|
| 254 |
" print(\"❌ MISMATCH DETECTED: The folders do not have the same files.\\n\")\n",
|
| 255 |
-
"
|
| 256 |
" # Create a master list of EVERY unique file found across all three folders\n",
|
| 257 |
" all_known_files = t2_files | dwi_files | adc_files\n",
|
| 258 |
" print(f\"Total unique cases found across all folders: {len(all_known_files)}\\n\")\n",
|
| 259 |
-
"
|
| 260 |
" # Subtracting a folder's files from the master list reveals exactly what it is missing\n",
|
| 261 |
" missing_from_t2 = all_known_files - t2_files\n",
|
| 262 |
" missing_from_dwi = all_known_files - dwi_files\n",
|
| 263 |
" missing_from_adc = all_known_files - adc_files\n",
|
| 264 |
-
"
|
| 265 |
" # 4. Print the detailed report\n",
|
| 266 |
" if missing_from_t2:\n",
|
| 267 |
" print(f\"Missing from T2 ({len(missing_from_t2)} files):\")\n",
|
| 268 |
-
" for f in missing_from_t2:
|
|
|
|
| 269 |
" print()\n",
|
| 270 |
-
"
|
| 271 |
" if missing_from_dwi:\n",
|
| 272 |
" print(f\"Missing from DWI ({len(missing_from_dwi)} files):\")\n",
|
| 273 |
-
" for f in missing_from_dwi:
|
|
|
|
| 274 |
" print()\n",
|
| 275 |
-
"
|
| 276 |
" if missing_from_adc:\n",
|
| 277 |
" print(f\"Missing from ADC ({len(missing_from_adc)} files):\")\n",
|
| 278 |
-
" for f in missing_from_adc:
|
|
|
|
| 279 |
" print()\n",
|
| 280 |
"\n",
|
|
|
|
| 281 |
"# ==========================================\n",
|
| 282 |
"# Run the auditor\n",
|
| 283 |
"# ==========================================\n",
|
|
@@ -285,7 +282,7 @@
|
|
| 285 |
"t2_folder = os.path.join(out_dir, \"t2\")\n",
|
| 286 |
"adc_folder = os.path.join(out_dir, \"adc\")\n",
|
| 287 |
"dwi_folder = os.path.join(out_dir, \"dwi\")\n",
|
| 288 |
-
"audit_mri_folders(t2_folder, dwi_folder, adc_folder)
|
| 289 |
]
|
| 290 |
},
|
| 291 |
{
|
|
@@ -323,44 +320,44 @@
|
|
| 323 |
}
|
| 324 |
],
|
| 325 |
"source": [
|
| 326 |
-
"\n",
|
| 327 |
"def get_clean_file_set(folder_path):\n",
|
| 328 |
" if not os.path.exists(folder_path):\n",
|
| 329 |
" return set()\n",
|
| 330 |
-
" return set(f for f in os.listdir(folder_path) if not f.startswith(
|
|
|
|
| 331 |
"\n",
|
| 332 |
"def remove_orphan_files(t2_dir, dwi_dir, adc_dir, dry_run=True):\n",
|
| 333 |
" # 1. Grab the sets of files from each folder\n",
|
| 334 |
" t2_files = get_clean_file_set(t2_dir)\n",
|
| 335 |
" dwi_files = get_clean_file_set(dwi_dir)\n",
|
| 336 |
" adc_files = get_clean_file_set(adc_dir)\n",
|
| 337 |
-
"
|
| 338 |
" # 2. Find the \"Perfect Matches\" (files that exist in ALL three folders)\n",
|
| 339 |
" common_files = t2_files & dwi_files & adc_files\n",
|
| 340 |
-
"
|
| 341 |
" # 3. Setup directories to check\n",
|
| 342 |
" directories = [\n",
|
| 343 |
" (\"T2\", t2_dir, t2_files),\n",
|
| 344 |
" (\"DWI\", dwi_dir, dwi_files),\n",
|
| 345 |
-
" (\"ADC\", adc_dir, adc_files)\n",
|
| 346 |
" ]\n",
|
| 347 |
-
"
|
| 348 |
" total_deleted = 0\n",
|
| 349 |
-
"
|
| 350 |
" print(f\"--- Running in {'DRY RUN (Safe)' if dry_run else 'ACTIVE DELETE'} mode ---\\n\")\n",
|
| 351 |
-
"
|
| 352 |
" # 4. Loop through each folder and delete files that aren't in the common pool\n",
|
| 353 |
" for name, dir_path, files in directories:\n",
|
| 354 |
" orphans = files - common_files\n",
|
| 355 |
-
"
|
| 356 |
" if not orphans:\n",
|
| 357 |
" print(f\"✅ {name} folder is already clean.\")\n",
|
| 358 |
" continue\n",
|
| 359 |
-
"
|
| 360 |
" print(f\"🧹 Cleaning {name} folder ({len(orphans)} orphan files found)...\")\n",
|
| 361 |
" for orphan in orphans:\n",
|
| 362 |
" file_path = os.path.join(dir_path, orphan)\n",
|
| 363 |
-
"
|
| 364 |
" if dry_run:\n",
|
| 365 |
" print(f\" [Would Delete] -> {file_path}\")\n",
|
| 366 |
" else:\n",
|
|
@@ -370,16 +367,15 @@
|
|
| 370 |
" total_deleted += 1\n",
|
| 371 |
" except Exception as e:\n",
|
| 372 |
" print(f\" [Error] -> Could not delete {file_path}: {e}\")\n",
|
| 373 |
-
"
|
| 374 |
" # 5. Final Summary\n",
|
| 375 |
-
" print(\"\\n\" + \"=\"*40)\n",
|
| 376 |
" if dry_run:\n",
|
| 377 |
" print(\"🛑 This was a DRY RUN. No files were actually deleted.\")\n",
|
| 378 |
" print(\"To permanently delete these files, change 'dry_run=False' in the script.\")\n",
|
| 379 |
" else:\n",
|
| 380 |
" print(f\"✅ Cleanup complete. {total_deleted} orphan files permanently deleted.\")\n",
|
| 381 |
-
" print(\"=\"*40)\n",
|
| 382 |
-
"\n",
|
| 383 |
"\n",
|
| 384 |
"\n",
|
| 385 |
"# Run it once with True to see what will happen.\n",
|
|
@@ -521,28 +517,25 @@
|
|
| 521 |
"source": [
|
| 522 |
"import os\n",
|
| 523 |
"\n",
|
|
|
|
| 524 |
"def delete_bad_uids(t2_dir, dwi_dir, adc_dir, bad_uids_list, dry_run=True):\n",
|
| 525 |
" # The folders we need to check\n",
|
| 526 |
-
" directories = {\n",
|
| 527 |
-
"
|
| 528 |
-
" \"DWI\": dwi_dir,\n",
|
| 529 |
-
" \"ADC\": adc_dir\n",
|
| 530 |
-
" }\n",
|
| 531 |
-
" \n",
|
| 532 |
" total_deleted = 0\n",
|
| 533 |
" total_not_found = 0\n",
|
| 534 |
-
"
|
| 535 |
" print(f\"--- Running in {'DRY RUN (Safe)' if dry_run else 'ACTIVE DELETE'} mode ---\\n\")\n",
|
| 536 |
-
"
|
| 537 |
" # Loop through every bad UID in your list\n",
|
| 538 |
" for uid in bad_uids_list:\n",
|
| 539 |
" filename = f\"{uid}.nrrd\"\n",
|
| 540 |
" print(f\"Targeting: {filename}\")\n",
|
| 541 |
-
"
|
| 542 |
" # Check all three folders for this specific file\n",
|
| 543 |
" for folder_name, folder_path in directories.items():\n",
|
| 544 |
" file_path = os.path.join(folder_path, filename)\n",
|
| 545 |
-
"
|
| 546 |
" if os.path.exists(file_path):\n",
|
| 547 |
" if dry_run:\n",
|
| 548 |
" print(f\" [{folder_name}] -> [Would Delete]: {file_path}\")\n",
|
|
@@ -557,16 +550,15 @@
|
|
| 557 |
" # Optional: Uncomment the line below if you want to know when a file was already missing\n",
|
| 558 |
" # print(f\" [{folder_name}] -> Not found (already gone)\")\n",
|
| 559 |
" total_not_found += 1\n",
|
| 560 |
-
"
|
| 561 |
" # Final Summary\n",
|
| 562 |
-
" print(\"\\n\" + \"=\"*40)\n",
|
| 563 |
" if dry_run:\n",
|
| 564 |
" print(\"🛑 This was a DRY RUN. No files were actually deleted.\")\n",
|
| 565 |
" print(\"To permanently delete these files, change 'dry_run=False' in the script.\")\n",
|
| 566 |
" else:\n",
|
| 567 |
" print(f\"✅ Cleanup complete. {total_deleted} files permanently deleted.\")\n",
|
| 568 |
-
" print(\"=\"*40)\n",
|
| 569 |
-
"\n",
|
| 570 |
"\n",
|
| 571 |
"\n",
|
| 572 |
"# Paste your list of bad UIDs here\n",
|
|
@@ -574,7 +566,7 @@
|
|
| 574 |
"\n",
|
| 575 |
"bad_uids = list(tracker.problematic_uids)\n",
|
| 576 |
"\n",
|
| 577 |
-
"# Run it once with True to verify.
|
| 578 |
"# Change to False to actually delete the files.\n",
|
| 579 |
"delete_bad_uids(t2_folder, dwi_folder, adc_folder, bad_uids, dry_run=False)"
|
| 580 |
]
|
|
@@ -654,19 +646,18 @@
|
|
| 654 |
" for seq in os.listdir(os.path.join(data_dir, file, st_uid)):\n",
|
| 655 |
" seq_path = os.path.join(data_dir, file, st_uid, seq)\n",
|
| 656 |
" seq_type, _ = identify_mri_sequence(seq_path)\n",
|
| 657 |
-
" output_nrrd_path = os.path.join(out_dir,seq_type, f\"{st_uid}.nrrd\")
|
| 658 |
"\n",
|
| 659 |
" dicom_to_nrrd(seq_path, output_nrrd_path)\n",
|
| 660 |
" bool_ = True\n",
|
| 661 |
"\n",
|
| 662 |
-
"
|
| 663 |
-
" \n",
|
| 664 |
"bad_uids = list(tracker.problematic_uids)\n",
|
| 665 |
"for uid in bad_uids:\n",
|
| 666 |
-
" print(uid)
|
| 667 |
"\n",
|
| 668 |
"\n",
|
| 669 |
-
"
|
| 670 |
"\n",
|
| 671 |
"st_uid = os.listdir(os.path.join(data_dir, file))[0]\n",
|
| 672 |
"print(st_uid)\n",
|
|
@@ -675,7 +666,7 @@
|
|
| 675 |
"print(seqs)\n",
|
| 676 |
"seq = os.path.join(data_dir, file, st_uid, seqs[1])\n",
|
| 677 |
"len(os.listdir(seq))\n",
|
| 678 |
-
"
|
| 679 |
]
|
| 680 |
},
|
| 681 |
{
|
|
@@ -725,8 +716,10 @@
|
|
| 725 |
}
|
| 726 |
],
|
| 727 |
"source": [
|
| 728 |
-
"data_dir =
|
| 729 |
-
"meta_folder = \
|
|
|
|
|
|
|
| 730 |
"t2_dir = os.path.join(data_dir, \"t2\")\n",
|
| 731 |
"adc_dir = os.path.join(data_dir, \"adc\")\n",
|
| 732 |
"dwi_dir = os.path.join(data_dir, \"dwi\")\n",
|
|
@@ -742,8 +735,10 @@
|
|
| 742 |
"source": [
|
| 743 |
"st_id = os.listdir(t2_dir)\n",
|
| 744 |
"\n",
|
| 745 |
-
"df_parent = pd.read_excel(
|
| 746 |
-
"
|
|
|
|
|
|
|
| 747 |
]
|
| 748 |
},
|
| 749 |
{
|
|
@@ -767,7 +762,6 @@
|
|
| 767 |
"exclude = []\n",
|
| 768 |
"test_list = []\n",
|
| 769 |
"for file in st_id:\n",
|
| 770 |
-
" \n",
|
| 771 |
" id = file.split(\".nrrd\")[0]\n",
|
| 772 |
" filtered_parent = df_parent[df_parent[\"Study Instance UID\"] == id]\n",
|
| 773 |
" patient_ids = filtered_parent[\"Patient ID\"].unique()\n",
|
|
@@ -775,23 +769,51 @@
|
|
| 775 |
" print(f\"Warning: Multiple patient IDs found for Study Instance UID {id}: {patient_ids}\")\n",
|
| 776 |
" else:\n",
|
| 777 |
" patient_id = patient_ids[0]\n",
|
| 778 |
-
"
|
| 779 |
-
" t2_id = filtered_parent[
|
|
|
|
|
|
|
| 780 |
" filtered_df = df[df[\"Series Instance UID (MRI)\"] == t2_id]\n",
|
| 781 |
-
" assert filtered_df[\"Patient Number\"].unique()[0] == patient_id,
|
|
|
|
|
|
|
| 782 |
" if filtered_df[\"Series Instance UID (US)\"].unique().shape[0] == 1:\n",
|
| 783 |
-
" assert filtered_df[\"PSA (ng/mL)\"].unique().shape[0] == 1,
|
| 784 |
-
"
|
| 785 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
" temp = {}\n",
|
| 787 |
" temp[\"image\"] = file\n",
|
| 788 |
-
" temp[\"psa\"] = [
|
| 789 |
-
"
|
| 790 |
-
"
|
| 791 |
-
"
|
| 792 |
-
" temp[\"
|
| 793 |
-
"
|
| 794 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 795 |
" test_list.append(temp)\n",
|
| 796 |
" else:\n",
|
| 797 |
" exclude.append(id)\n",
|
|
@@ -849,8 +871,8 @@
|
|
| 849 |
}
|
| 850 |
],
|
| 851 |
"source": [
|
| 852 |
-
"labs = [i[
|
| 853 |
-
"np.unique(np.array(labs), return_counts=True)
|
| 854 |
]
|
| 855 |
},
|
| 856 |
{
|
|
|
|
| 7 |
"metadata": {},
|
| 8 |
"outputs": [],
|
| 9 |
"source": [
|
| 10 |
+
"import json\n",
|
| 11 |
"import os\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"import re\n",
|
| 13 |
+
"\n",
|
| 14 |
+
"import numpy as np\n",
|
| 15 |
"import pandas as pd\n",
|
| 16 |
+
"import SimpleITK as sitk\n",
|
| 17 |
+
"from tqdm import tqdm"
|
| 18 |
]
|
| 19 |
},
|
| 20 |
{
|
|
|
|
| 50 |
}
|
| 51 |
],
|
| 52 |
"source": [
|
| 53 |
+
"data_dir = \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/manifest-1777460989222/prostate_mri_us_biopsy\"\n",
|
| 54 |
+
"out_dir = \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/\"\n",
|
| 55 |
"print(len(os.listdir(data_dir)))\n",
|
| 56 |
+
"\"\"\"\n",
|
| 57 |
"for file in os.listdir(data_dir):\n",
|
| 58 |
" if not os.path.isfile(os.path.join(data_dir, file)):\n",
|
| 59 |
" if len(os.listdir(os.path.join(data_dir, file))) > 1:\n",
|
| 60 |
" print(file)\n",
|
| 61 |
+
"\"\"\""
|
| 62 |
]
|
| 63 |
},
|
| 64 |
{
|
|
|
|
| 68 |
"metadata": {},
|
| 69 |
"outputs": [],
|
| 70 |
"source": [
|
|
|
|
|
|
|
| 71 |
"def identify_mri_sequence(dicom_dir):\n",
|
| 72 |
" # 1. Get all DICOM files in the folder\n",
|
| 73 |
" reader = sitk.ImageSeriesReader()\n",
|
| 74 |
" series_IDs = reader.GetGDCMSeriesIDs(dicom_dir)\n",
|
| 75 |
+
"\n",
|
| 76 |
" if not series_IDs:\n",
|
| 77 |
" print(\"No DICOM files found.\")\n",
|
| 78 |
" return None\n",
|
| 79 |
"\n",
|
| 80 |
" # Get the filenames for the first series found\n",
|
| 81 |
" dicom_names = reader.GetGDCMSeriesFileNames(dicom_dir, series_IDs[0])\n",
|
| 82 |
+
" first_file = dicom_names[0] # We only need to check the first slice's header\n",
|
| 83 |
"\n",
|
| 84 |
" # 2. Read only the header information (fast, doesn't load image pixels)\n",
|
| 85 |
" file_reader = sitk.ImageFileReader()\n",
|
|
|
|
| 93 |
" except RuntimeError:\n",
|
| 94 |
" series_description = \"unknown (tag missing)\"\n",
|
| 95 |
"\n",
|
| 96 |
+
" # print(f\"Raw DICOM Series Description: '{series_description}'\")\n",
|
| 97 |
"\n",
|
| 98 |
" # 4. Keyword matching to identify the sequence\n",
|
| 99 |
" sequence_type = \"Unclassified\"\n",
|
|
|
|
| 104 |
" sequence_type = \"t2\"\n",
|
| 105 |
" elif \"adc\" in desc_lower:\n",
|
| 106 |
" sequence_type = \"adc\"\n",
|
| 107 |
+
" elif any(word in desc_lower for word in dwi_keywords) or re.search(r\"b\\d+\", desc_lower):\n",
|
| 108 |
" sequence_type = \"dwi\"\n",
|
| 109 |
"\n",
|
| 110 |
+
" # print(f\"Identified Sequence Type: {sequence_type}\")\n",
|
|
|
|
| 111 |
" return sequence_type, series_description\n",
|
| 112 |
"\n",
|
| 113 |
"\n",
|
|
|
|
| 114 |
"def dicom_to_nrrd(dicom_dir, output_filepath):\n",
|
| 115 |
" # 1. Initialize the ImageSeriesReader\n",
|
| 116 |
" reader = sitk.ImageSeriesReader()\n",
|
| 117 |
"\n",
|
| 118 |
" # 2. Find the DICOM series within the folder\n",
|
| 119 |
+
" # A single folder might contain multiple scans (e.g., a T1 and a T2).\n",
|
| 120 |
" # This function groups them by their unique Series Instance UID.\n",
|
| 121 |
" series_IDs = reader.GetGDCMSeriesIDs(dicom_dir)\n",
|
| 122 |
+
"\n",
|
| 123 |
" if not series_IDs:\n",
|
| 124 |
" print(f\"Error: No DICOM series found in the directory {dicom_dir}.\")\n",
|
| 125 |
" return\n",
|
| 126 |
"\n",
|
| 127 |
" # Assuming you want the first (or only) series in the folder\n",
|
| 128 |
" series_ID = series_IDs[0]\n",
|
| 129 |
+
"\n",
|
| 130 |
" # 3. Get the list of files belonging to this specific series.\n",
|
| 131 |
" # SimpleITK automatically sorts them by spatial position/instance number here.\n",
|
| 132 |
" dicom_names = reader.GetGDCMSeriesFileNames(dicom_dir, series_ID)\n",
|
| 133 |
" reader.SetFileNames(dicom_names)\n",
|
| 134 |
"\n",
|
| 135 |
+
" # print(f\"Found series {series_ID}\")\n",
|
| 136 |
+
" # print(f\"Reading {len(dicom_names)} DICOM files...\")\n",
|
| 137 |
"\n",
|
| 138 |
" # 4. Read the files into a single 3D image object\n",
|
| 139 |
" image = reader.Execute()\n",
|
| 140 |
+
" \"\"\"\n",
|
| 141 |
" # (Optional) Print out the header info that SimpleITK extracted\n",
|
| 142 |
" print(\"\\nExtracted Header Information:\")\n",
|
| 143 |
" print(f\"Size (X, Y, Z): {image.GetSize()}\")\n",
|
|
|
|
| 147 |
"\n",
|
| 148 |
" # 5. Write the 3D image to an NRRD file\n",
|
| 149 |
" # SimpleITK automatically translates the spatial metadata into the NRRD header format.\n",
|
| 150 |
+
" \"\"\"\n",
|
| 151 |
" sitk.WriteImage(image, output_filepath)\n",
|
| 152 |
+
" # print(f\"Successfully saved 3D volume to: {output_filepath}\")"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
]
|
| 154 |
},
|
| 155 |
{
|
|
|
|
| 161 |
"source": [
|
| 162 |
"import logging\n",
|
| 163 |
"\n",
|
|
|
|
| 164 |
"# Keep your basic logging config\n",
|
| 165 |
"logging.basicConfig(\n",
|
| 166 |
+
" filename=\"dicom_warnings.log\",\n",
|
| 167 |
+
" filemode=\"a\",\n",
|
| 168 |
" level=logging.WARNING,\n",
|
| 169 |
+
" format=\"%(asctime)s - %(levelname)s - %(message)s\",\n",
|
| 170 |
")\n",
|
| 171 |
"\n",
|
| 172 |
+
"\n",
|
| 173 |
"class SimpleITKWarningTracker(sitk.LoggerBase):\n",
|
| 174 |
" def __init__(self, logger: logging.Logger = logging.getLogger(\"SimpleITK\")):\n",
|
| 175 |
" super().__init__()\n",
|
| 176 |
" self._logger = logger\n",
|
| 177 |
+
"\n",
|
| 178 |
" # --- NEW ADDITIONS ---\n",
|
| 179 |
" self.current_uid = None\n",
|
| 180 |
+
" self.problematic_uids = set() # Using a set so we don't get duplicates\n",
|
| 181 |
"\n",
|
| 182 |
" def DisplayWarningText(self, s):\n",
|
| 183 |
" # 1. Log to the file, but inject the UID so you know exactly which one failed\n",
|
| 184 |
" self._logger.warning(f\"[{self.current_uid}] {s.rstrip()}\")\n",
|
| 185 |
+
"\n",
|
| 186 |
" # 2. Capture the UID into our set\n",
|
| 187 |
" if self.current_uid is not None:\n",
|
| 188 |
" self.problematic_uids.add(self.current_uid)\n",
|
|
|
|
| 193 |
" self.problematic_uids.add(self.current_uid)\n",
|
| 194 |
"\n",
|
| 195 |
" # Standard passthroughs for everything else\n",
|
| 196 |
+
" def DisplayText(self, s):\n",
|
| 197 |
+
" self._logger.info(s.rstrip())\n",
|
| 198 |
+
"\n",
|
| 199 |
+
" def DisplayGenericOutputText(self, s):\n",
|
| 200 |
+
" self._logger.info(s.rstrip())\n",
|
| 201 |
+
"\n",
|
| 202 |
+
" def DisplayDebugText(self, s):\n",
|
| 203 |
+
" self._logger.debug(s.rstrip())"
|
| 204 |
]
|
| 205 |
},
|
| 206 |
{
|
|
|
|
| 219 |
}
|
| 220 |
],
|
| 221 |
"source": [
|
|
|
|
|
|
|
|
|
|
| 222 |
"def get_clean_file_set(folder_path):\n",
|
| 223 |
" \"\"\"\n",
|
| 224 |
+
" Returns a set of filenames in a folder,\n",
|
| 225 |
" ignoring hidden files like .DS_Store.\n",
|
| 226 |
" \"\"\"\n",
|
| 227 |
" if not os.path.exists(folder_path):\n",
|
| 228 |
" print(f\"Warning: Folder not found -> {folder_path}\")\n",
|
| 229 |
" return set()\n",
|
| 230 |
+
"\n",
|
| 231 |
+
" return set(f for f in os.listdir(folder_path) if not f.startswith(\".\"))\n",
|
| 232 |
+
"\n",
|
| 233 |
"\n",
|
| 234 |
"def audit_mri_folders(t2_dir, dwi_dir, adc_dir):\n",
|
| 235 |
" # 1. Grab the sets of files from each folder\n",
|
| 236 |
" t2_files = get_clean_file_set(t2_dir)\n",
|
| 237 |
" dwi_files = get_clean_file_set(dwi_dir)\n",
|
| 238 |
" adc_files = get_clean_file_set(adc_dir)\n",
|
| 239 |
+
"\n",
|
| 240 |
" # 2. Check for absolute perfection\n",
|
| 241 |
" if t2_files == dwi_files == adc_files:\n",
|
| 242 |
" print(\"✅ SUCCESS: All three folders contain the exact same files.\")\n",
|
|
|
|
| 245 |
"\n",
|
| 246 |
" # 3. If they don't match, figure out exactly what went wrong\n",
|
| 247 |
" print(\"❌ MISMATCH DETECTED: The folders do not have the same files.\\n\")\n",
|
| 248 |
+
"\n",
|
| 249 |
" # Create a master list of EVERY unique file found across all three folders\n",
|
| 250 |
" all_known_files = t2_files | dwi_files | adc_files\n",
|
| 251 |
" print(f\"Total unique cases found across all folders: {len(all_known_files)}\\n\")\n",
|
| 252 |
+
"\n",
|
| 253 |
" # Subtracting a folder's files from the master list reveals exactly what it is missing\n",
|
| 254 |
" missing_from_t2 = all_known_files - t2_files\n",
|
| 255 |
" missing_from_dwi = all_known_files - dwi_files\n",
|
| 256 |
" missing_from_adc = all_known_files - adc_files\n",
|
| 257 |
+
"\n",
|
| 258 |
" # 4. Print the detailed report\n",
|
| 259 |
" if missing_from_t2:\n",
|
| 260 |
" print(f\"Missing from T2 ({len(missing_from_t2)} files):\")\n",
|
| 261 |
+
" for f in missing_from_t2:\n",
|
| 262 |
+
" print(f\" - {f}\")\n",
|
| 263 |
" print()\n",
|
| 264 |
+
"\n",
|
| 265 |
" if missing_from_dwi:\n",
|
| 266 |
" print(f\"Missing from DWI ({len(missing_from_dwi)} files):\")\n",
|
| 267 |
+
" for f in missing_from_dwi:\n",
|
| 268 |
+
" print(f\" - {f}\")\n",
|
| 269 |
" print()\n",
|
| 270 |
+
"\n",
|
| 271 |
" if missing_from_adc:\n",
|
| 272 |
" print(f\"Missing from ADC ({len(missing_from_adc)} files):\")\n",
|
| 273 |
+
" for f in missing_from_adc:\n",
|
| 274 |
+
" print(f\" - {f}\")\n",
|
| 275 |
" print()\n",
|
| 276 |
"\n",
|
| 277 |
+
"\n",
|
| 278 |
"# ==========================================\n",
|
| 279 |
"# Run the auditor\n",
|
| 280 |
"# ==========================================\n",
|
|
|
|
| 282 |
"t2_folder = os.path.join(out_dir, \"t2\")\n",
|
| 283 |
"adc_folder = os.path.join(out_dir, \"adc\")\n",
|
| 284 |
"dwi_folder = os.path.join(out_dir, \"dwi\")\n",
|
| 285 |
+
"audit_mri_folders(t2_folder, dwi_folder, adc_folder)"
|
| 286 |
]
|
| 287 |
},
|
| 288 |
{
|
|
|
|
| 320 |
}
|
| 321 |
],
|
| 322 |
"source": [
|
|
|
|
| 323 |
"def get_clean_file_set(folder_path):\n",
|
| 324 |
" if not os.path.exists(folder_path):\n",
|
| 325 |
" return set()\n",
|
| 326 |
+
" return set(f for f in os.listdir(folder_path) if not f.startswith(\".\"))\n",
|
| 327 |
+
"\n",
|
| 328 |
"\n",
|
| 329 |
"def remove_orphan_files(t2_dir, dwi_dir, adc_dir, dry_run=True):\n",
|
| 330 |
" # 1. Grab the sets of files from each folder\n",
|
| 331 |
" t2_files = get_clean_file_set(t2_dir)\n",
|
| 332 |
" dwi_files = get_clean_file_set(dwi_dir)\n",
|
| 333 |
" adc_files = get_clean_file_set(adc_dir)\n",
|
| 334 |
+
"\n",
|
| 335 |
" # 2. Find the \"Perfect Matches\" (files that exist in ALL three folders)\n",
|
| 336 |
" common_files = t2_files & dwi_files & adc_files\n",
|
| 337 |
+
"\n",
|
| 338 |
" # 3. Setup directories to check\n",
|
| 339 |
" directories = [\n",
|
| 340 |
" (\"T2\", t2_dir, t2_files),\n",
|
| 341 |
" (\"DWI\", dwi_dir, dwi_files),\n",
|
| 342 |
+
" (\"ADC\", adc_dir, adc_files),\n",
|
| 343 |
" ]\n",
|
| 344 |
+
"\n",
|
| 345 |
" total_deleted = 0\n",
|
| 346 |
+
"\n",
|
| 347 |
" print(f\"--- Running in {'DRY RUN (Safe)' if dry_run else 'ACTIVE DELETE'} mode ---\\n\")\n",
|
| 348 |
+
"\n",
|
| 349 |
" # 4. Loop through each folder and delete files that aren't in the common pool\n",
|
| 350 |
" for name, dir_path, files in directories:\n",
|
| 351 |
" orphans = files - common_files\n",
|
| 352 |
+
"\n",
|
| 353 |
" if not orphans:\n",
|
| 354 |
" print(f\"✅ {name} folder is already clean.\")\n",
|
| 355 |
" continue\n",
|
| 356 |
+
"\n",
|
| 357 |
" print(f\"🧹 Cleaning {name} folder ({len(orphans)} orphan files found)...\")\n",
|
| 358 |
" for orphan in orphans:\n",
|
| 359 |
" file_path = os.path.join(dir_path, orphan)\n",
|
| 360 |
+
"\n",
|
| 361 |
" if dry_run:\n",
|
| 362 |
" print(f\" [Would Delete] -> {file_path}\")\n",
|
| 363 |
" else:\n",
|
|
|
|
| 367 |
" total_deleted += 1\n",
|
| 368 |
" except Exception as e:\n",
|
| 369 |
" print(f\" [Error] -> Could not delete {file_path}: {e}\")\n",
|
| 370 |
+
"\n",
|
| 371 |
" # 5. Final Summary\n",
|
| 372 |
+
" print(\"\\n\" + \"=\" * 40)\n",
|
| 373 |
" if dry_run:\n",
|
| 374 |
" print(\"🛑 This was a DRY RUN. No files were actually deleted.\")\n",
|
| 375 |
" print(\"To permanently delete these files, change 'dry_run=False' in the script.\")\n",
|
| 376 |
" else:\n",
|
| 377 |
" print(f\"✅ Cleanup complete. {total_deleted} orphan files permanently deleted.\")\n",
|
| 378 |
+
" print(\"=\" * 40)\n",
|
|
|
|
| 379 |
"\n",
|
| 380 |
"\n",
|
| 381 |
"# Run it once with True to see what will happen.\n",
|
|
|
|
| 517 |
"source": [
|
| 518 |
"import os\n",
|
| 519 |
"\n",
|
| 520 |
+
"\n",
|
| 521 |
"def delete_bad_uids(t2_dir, dwi_dir, adc_dir, bad_uids_list, dry_run=True):\n",
|
| 522 |
" # The folders we need to check\n",
|
| 523 |
+
" directories = {\"T2\": t2_dir, \"DWI\": dwi_dir, \"ADC\": adc_dir}\n",
|
| 524 |
+
"\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
" total_deleted = 0\n",
|
| 526 |
" total_not_found = 0\n",
|
| 527 |
+
"\n",
|
| 528 |
" print(f\"--- Running in {'DRY RUN (Safe)' if dry_run else 'ACTIVE DELETE'} mode ---\\n\")\n",
|
| 529 |
+
"\n",
|
| 530 |
" # Loop through every bad UID in your list\n",
|
| 531 |
" for uid in bad_uids_list:\n",
|
| 532 |
" filename = f\"{uid}.nrrd\"\n",
|
| 533 |
" print(f\"Targeting: {filename}\")\n",
|
| 534 |
+
"\n",
|
| 535 |
" # Check all three folders for this specific file\n",
|
| 536 |
" for folder_name, folder_path in directories.items():\n",
|
| 537 |
" file_path = os.path.join(folder_path, filename)\n",
|
| 538 |
+
"\n",
|
| 539 |
" if os.path.exists(file_path):\n",
|
| 540 |
" if dry_run:\n",
|
| 541 |
" print(f\" [{folder_name}] -> [Would Delete]: {file_path}\")\n",
|
|
|
|
| 550 |
" # Optional: Uncomment the line below if you want to know when a file was already missing\n",
|
| 551 |
" # print(f\" [{folder_name}] -> Not found (already gone)\")\n",
|
| 552 |
" total_not_found += 1\n",
|
| 553 |
+
"\n",
|
| 554 |
" # Final Summary\n",
|
| 555 |
+
" print(\"\\n\" + \"=\" * 40)\n",
|
| 556 |
" if dry_run:\n",
|
| 557 |
" print(\"🛑 This was a DRY RUN. No files were actually deleted.\")\n",
|
| 558 |
" print(\"To permanently delete these files, change 'dry_run=False' in the script.\")\n",
|
| 559 |
" else:\n",
|
| 560 |
" print(f\"✅ Cleanup complete. {total_deleted} files permanently deleted.\")\n",
|
| 561 |
+
" print(\"=\" * 40)\n",
|
|
|
|
| 562 |
"\n",
|
| 563 |
"\n",
|
| 564 |
"# Paste your list of bad UIDs here\n",
|
|
|
|
| 566 |
"\n",
|
| 567 |
"bad_uids = list(tracker.problematic_uids)\n",
|
| 568 |
"\n",
|
| 569 |
+
"# Run it once with True to verify.\n",
|
| 570 |
"# Change to False to actually delete the files.\n",
|
| 571 |
"delete_bad_uids(t2_folder, dwi_folder, adc_folder, bad_uids, dry_run=False)"
|
| 572 |
]
|
|
|
|
| 646 |
" for seq in os.listdir(os.path.join(data_dir, file, st_uid)):\n",
|
| 647 |
" seq_path = os.path.join(data_dir, file, st_uid, seq)\n",
|
| 648 |
" seq_type, _ = identify_mri_sequence(seq_path)\n",
|
| 649 |
+
" output_nrrd_path = os.path.join(out_dir, seq_type, f\"{st_uid}.nrrd\")\n",
|
| 650 |
"\n",
|
| 651 |
" dicom_to_nrrd(seq_path, output_nrrd_path)\n",
|
| 652 |
" bool_ = True\n",
|
| 653 |
"\n",
|
| 654 |
+
"\n",
|
|
|
|
| 655 |
"bad_uids = list(tracker.problematic_uids)\n",
|
| 656 |
"for uid in bad_uids:\n",
|
| 657 |
+
" print(uid)\n",
|
| 658 |
"\n",
|
| 659 |
"\n",
|
| 660 |
+
"\"\"\"\n",
|
| 661 |
"\n",
|
| 662 |
"st_uid = os.listdir(os.path.join(data_dir, file))[0]\n",
|
| 663 |
"print(st_uid)\n",
|
|
|
|
| 666 |
"print(seqs)\n",
|
| 667 |
"seq = os.path.join(data_dir, file, st_uid, seqs[1])\n",
|
| 668 |
"len(os.listdir(seq))\n",
|
| 669 |
+
"\"\"\""
|
| 670 |
]
|
| 671 |
},
|
| 672 |
{
|
|
|
|
| 716 |
}
|
| 717 |
],
|
| 718 |
"source": [
|
| 719 |
+
"data_dir = \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/\"\n",
|
| 720 |
+
"meta_folder = (\n",
|
| 721 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/manifest-1777460989222/metadata\"\n",
|
| 722 |
+
")\n",
|
| 723 |
"t2_dir = os.path.join(data_dir, \"t2\")\n",
|
| 724 |
"adc_dir = os.path.join(data_dir, \"adc\")\n",
|
| 725 |
"dwi_dir = os.path.join(data_dir, \"dwi\")\n",
|
|
|
|
| 735 |
"source": [
|
| 736 |
"st_id = os.listdir(t2_dir)\n",
|
| 737 |
"\n",
|
| 738 |
+
"df_parent = pd.read_excel(\n",
|
| 739 |
+
" os.path.join(meta_folder, \"Prostate-MRI-US-Biopsy-NBIA-manifest_v2_20231020-nbia-digest.xlsx\")\n",
|
| 740 |
+
")\n",
|
| 741 |
+
"df = pd.read_excel(os.path.join(meta_folder, \"TCIA-Biopsy-Data_2020-07-14.xlsx\"))"
|
| 742 |
]
|
| 743 |
},
|
| 744 |
{
|
|
|
|
| 762 |
"exclude = []\n",
|
| 763 |
"test_list = []\n",
|
| 764 |
"for file in st_id:\n",
|
|
|
|
| 765 |
" id = file.split(\".nrrd\")[0]\n",
|
| 766 |
" filtered_parent = df_parent[df_parent[\"Study Instance UID\"] == id]\n",
|
| 767 |
" patient_ids = filtered_parent[\"Patient ID\"].unique()\n",
|
|
|
|
| 769 |
" print(f\"Warning: Multiple patient IDs found for Study Instance UID {id}: {patient_ids}\")\n",
|
| 770 |
" else:\n",
|
| 771 |
" patient_id = patient_ids[0]\n",
|
| 772 |
+
"\n",
|
| 773 |
+
" t2_id = filtered_parent[\n",
|
| 774 |
+
" filtered_parent[\"Series Description\"].str.contains(\"t2\", case=False, na=False)\n",
|
| 775 |
+
" ][\"Series Instance UID\"].iloc[0]\n",
|
| 776 |
" filtered_df = df[df[\"Series Instance UID (MRI)\"] == t2_id]\n",
|
| 777 |
+
" assert filtered_df[\"Patient Number\"].unique()[0] == patient_id, (\n",
|
| 778 |
+
" f\"Mismatch: Patient ID from parent manifest ({patient_id}) does not match Patient Number in biopsy data ({filtered_df['Patient Number'].unique()[0]}) for Study Instance UID {id}\"\n",
|
| 779 |
+
" )\n",
|
| 780 |
" if filtered_df[\"Series Instance UID (US)\"].unique().shape[0] == 1:\n",
|
| 781 |
+
" assert filtered_df[\"PSA (ng/mL)\"].unique().shape[0] == 1, (\n",
|
| 782 |
+
" f\"Expected 1 unique PSA value for Study Instance UID {id}, but found {filtered_df['PSA (ng/mL)'].unique().shape[0]}\"\n",
|
| 783 |
+
" )\n",
|
| 784 |
+
" assert filtered_df[\"Prostate Volume (CC)\"].unique().shape[0] == 1, (\n",
|
| 785 |
+
" f\"Expected 1 unique Volume value for Study Instance UID {id}, but found {filtered_df['Volume (cc)'].unique().shape[0]}\"\n",
|
| 786 |
+
" )\n",
|
| 787 |
+
" gg = (\n",
|
| 788 |
+
" filtered_df[\"Primary Gleason\"].fillna(0) + filtered_df[\"Secondary Gleason\"].fillna(0)\n",
|
| 789 |
+
" ).max()\n",
|
| 790 |
" temp = {}\n",
|
| 791 |
" temp[\"image\"] = file\n",
|
| 792 |
+
" temp[\"psa\"] = [\n",
|
| 793 |
+
" filtered_df[\"PSA (ng/mL)\"].unique()[0],\n",
|
| 794 |
+
" filtered_df[\"Prostate Volume (CC)\"].unique()[0],\n",
|
| 795 |
+
" ]\n",
|
| 796 |
+
" temp[\"dwi\"] = os.path.join(\n",
|
| 797 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/DWI_registered\",\n",
|
| 798 |
+
" file,\n",
|
| 799 |
+
" )\n",
|
| 800 |
+
" temp[\"adc\"] = os.path.join(\n",
|
| 801 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/ADC_clipped\",\n",
|
| 802 |
+
" file,\n",
|
| 803 |
+
" )\n",
|
| 804 |
+
" temp[\"heatmap\"] = os.path.join(\n",
|
| 805 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/heatmaps\",\n",
|
| 806 |
+
" file,\n",
|
| 807 |
+
" )\n",
|
| 808 |
+
" temp[\"mask\"] = os.path.join(\n",
|
| 809 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/prostate_mask\",\n",
|
| 810 |
+
" file,\n",
|
| 811 |
+
" )\n",
|
| 812 |
+
" temp[\"smooth_mask\"] = os.path.join(\n",
|
| 813 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/smooth_prostate_mask\",\n",
|
| 814 |
+
" file,\n",
|
| 815 |
+
" )\n",
|
| 816 |
+
" temp[\"label\"] = 1.0 if gg >= 7 else 0.0\n",
|
| 817 |
" test_list.append(temp)\n",
|
| 818 |
" else:\n",
|
| 819 |
" exclude.append(id)\n",
|
|
|
|
| 871 |
}
|
| 872 |
],
|
| 873 |
"source": [
|
| 874 |
+
"labs = [i[\"label\"] for i in test_list]\n",
|
| 875 |
+
"np.unique(np.array(labs), return_counts=True)"
|
| 876 |
]
|
| 877 |
},
|
| 878 |
{
|
temp copy.ipynb
CHANGED
|
@@ -15,26 +15,25 @@
|
|
| 15 |
"metadata": {},
|
| 16 |
"outputs": [],
|
| 17 |
"source": [
|
| 18 |
-
"import
|
| 19 |
"import logging\n",
|
| 20 |
"import os\n",
|
| 21 |
"import shutil\n",
|
| 22 |
"import sys\n",
|
| 23 |
"from pathlib import Path\n",
|
| 24 |
-
"import
|
| 25 |
"\n",
|
| 26 |
"import torch\n",
|
| 27 |
"import yaml\n",
|
| 28 |
"from monai.utils import set_determinism\n",
|
| 29 |
"from sklearn.preprocessing import StandardScaler\n",
|
|
|
|
| 30 |
"\n",
|
| 31 |
"from src.data.data_loader import get_dataloader\n",
|
| 32 |
"from src.model.cspca_model import CSPCAModel\n",
|
| 33 |
"from src.model.mil import MILModel3D\n",
|
| 34 |
-
"from src.train.train_cspca import
|
| 35 |
-
"from src.utils import
|
| 36 |
-
"from types import SimpleNamespace\n",
|
| 37 |
-
"from tqdm import tqdm"
|
| 38 |
]
|
| 39 |
},
|
| 40 |
{
|
|
@@ -52,28 +51,28 @@
|
|
| 52 |
}
|
| 53 |
],
|
| 54 |
"source": [
|
| 55 |
-
"with open(\"config/config_cspca_test.yaml\"
|
| 56 |
" config = yaml.safe_load(f)\n",
|
| 57 |
"\n",
|
| 58 |
"args = SimpleNamespace(**config)\n",
|
| 59 |
"print(args.data_root)\n",
|
| 60 |
"\n",
|
| 61 |
-
"args.mode =
|
| 62 |
"args.project_dir = None\n",
|
| 63 |
"args.project_dir = Path.cwd()\n",
|
| 64 |
-
"args.run_name =
|
| 65 |
"args.checkpoint_pirads = None\n",
|
| 66 |
"\n",
|
| 67 |
"scaler = StandardScaler()\n",
|
| 68 |
"with open(os.path.join(args.project_dir, \"dataset\", \"PICAI_cspca_updated_with_psa.json\")) as f:\n",
|
| 69 |
" dataset_json = json.load(f)\n",
|
| 70 |
-
"train_clinical = [i[
|
| 71 |
"_ = scaler.fit_transform(train_clinical)\n",
|
| 72 |
"args.psa_mean = scaler.mean_.tolist()\n",
|
| 73 |
"args.psa_std = scaler.scale_.tolist()\n",
|
| 74 |
"args.use_psa = True\n",
|
| 75 |
"args.batch_size = 1\n",
|
| 76 |
-
"args.dataset_json =
|
| 77 |
]
|
| 78 |
},
|
| 79 |
{
|
|
@@ -83,7 +82,6 @@
|
|
| 83 |
"metadata": {},
|
| 84 |
"outputs": [],
|
| 85 |
"source": [
|
| 86 |
-
"\n",
|
| 87 |
"args.logdir = os.path.join(args.project_dir, \"logs\", args.run_name)\n",
|
| 88 |
"os.makedirs(args.logdir, exist_ok=True)\n",
|
| 89 |
"args.logfile = os.path.join(args.logdir, f\"{args.run_name}.log\")\n",
|
|
@@ -136,7 +134,7 @@
|
|
| 136 |
" f\"csPCa Model loaded from {args.checkpoint_cspca} with AUC: {auc}, Sensitivity: {sens}, Specificity: {spec} on the test set.\"\n",
|
| 137 |
" )\n",
|
| 138 |
"else:\n",
|
| 139 |
-
" logging.info(f\"csPCa Model loaded from {args.checkpoint_cspca}.\")
|
| 140 |
]
|
| 141 |
},
|
| 142 |
{
|
|
@@ -169,7 +167,7 @@
|
|
| 169 |
"metrics_dict[\"specificity\"].append(test_metric[\"specificity\"])\n",
|
| 170 |
"\n",
|
| 171 |
"\n",
|
| 172 |
-
"
|
| 173 |
"shutil.rmtree(cache_dir_path)\n",
|
| 174 |
"\n",
|
| 175 |
"metrics_dict"
|
|
@@ -206,19 +204,18 @@
|
|
| 206 |
}
|
| 207 |
],
|
| 208 |
"source": [
|
|
|
|
| 209 |
"import torch\n",
|
| 210 |
"import torch.nn as nn\n",
|
| 211 |
"from monai.metrics import Cumulative, CumulativeAverage\n",
|
| 212 |
"from sklearn.metrics import confusion_matrix, roc_auc_score\n",
|
| 213 |
"\n",
|
| 214 |
-
"from monai.utils import set_determinism\n",
|
| 215 |
-
"from sklearn.metrics import roc_curve\n",
|
| 216 |
-
"import numpy as np\n",
|
| 217 |
"spec_list = []\n",
|
| 218 |
"\n",
|
| 219 |
"auc_list = []\n",
|
| 220 |
"import os\n",
|
| 221 |
-
"
|
|
|
|
| 222 |
"\n",
|
| 223 |
"for st in list(range(10)):\n",
|
| 224 |
" set_determinism(seed=st)\n",
|
|
@@ -236,7 +233,7 @@
|
|
| 236 |
" target = batch_data[\"label\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 237 |
" psa_data = batch_data[\"psa\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 238 |
"\n",
|
| 239 |
-
" output = cspca_model(x
|
| 240 |
" output = output.squeeze(1)\n",
|
| 241 |
" loss = criterion(output, target)\n",
|
| 242 |
"\n",
|
|
@@ -247,9 +244,9 @@
|
|
| 247 |
" loss_epoch = run_loss.aggregate()\n",
|
| 248 |
" target_list = targets_cumulative.get_buffer().cpu().numpy()\n",
|
| 249 |
" pred_list = preds_cumulative.get_buffer().cpu().numpy()\n",
|
| 250 |
-
" #auc_epoch = roc_auc_score(target_list, pred_list)\n",
|
| 251 |
" y_pred_categoric = pred_list >= 0.5\n",
|
| 252 |
-
"
|
| 253 |
" tn, fp, fn, tp = confusion_matrix(target_list, y_pred_categoric).ravel()\n",
|
| 254 |
" sens_epoch = tp / (tp + fn)\n",
|
| 255 |
" spec_epoch = tn / (tn + fp)\n",
|
|
@@ -261,7 +258,7 @@
|
|
| 261 |
" \"specificity\": spec_epoch,\n",
|
| 262 |
" }\n",
|
| 263 |
" return val_epoch_metric\n",
|
| 264 |
-
"
|
| 265 |
" shutil.rmtree(cache_dir_path)\n",
|
| 266 |
" auc_epoch = roc_auc_score(target_list, pred_list)\n",
|
| 267 |
" auc_list.append(auc_epoch)"
|
|
@@ -322,16 +319,17 @@
|
|
| 322 |
}
|
| 323 |
],
|
| 324 |
"source": [
|
| 325 |
-
"import numpy as np\n",
|
| 326 |
"from scipy import stats\n",
|
| 327 |
-
"
|
|
|
|
| 328 |
"# Example values from 10 seeds\n",
|
| 329 |
"auc_list = [0.81, 0.84, 0.79, 0.83, 0.82,\n",
|
| 330 |
" 0.85, 0.80, 0.84, 0.83, 0.81]\n",
|
| 331 |
"\n",
|
| 332 |
"spec_list = [0.72, 0.75, 0.70, 0.74, 0.73,\n",
|
| 333 |
" 0.76, 0.71, 0.75, 0.74, 0.72]\n",
|
| 334 |
-
"
|
|
|
|
| 335 |
"\n",
|
| 336 |
"def mean_confidence_interval(values, confidence=0.95):\n",
|
| 337 |
" values = np.array(values)\n",
|
|
@@ -339,25 +337,19 @@
|
|
| 339 |
" mean = np.mean(values)\n",
|
| 340 |
" sem = stats.sem(values) # standard error\n",
|
| 341 |
"\n",
|
| 342 |
-
" ci = stats.t.interval(\n",
|
| 343 |
-
" confidence,\n",
|
| 344 |
-
" df=len(values) - 1,\n",
|
| 345 |
-
" loc=mean,\n",
|
| 346 |
-
" scale=sem\n",
|
| 347 |
-
" )\n",
|
| 348 |
"\n",
|
| 349 |
" return mean, ci\n",
|
| 350 |
"\n",
|
| 351 |
"\n",
|
| 352 |
"auc_mean, auc_ci = mean_confidence_interval(auc_list)\n",
|
| 353 |
-
"#spec_mean, spec_ci = mean_confidence_interval(spec_list)\n",
|
| 354 |
"\n",
|
| 355 |
-
"print(f\"AUC: {auc_mean:.3f} \"\n",
|
| 356 |
-
"
|
| 357 |
-
"'''\n",
|
| 358 |
"print(f\"Specificity: {spec_mean:.3f} \"\n",
|
| 359 |
" f\"(95% CI: {spec_ci[0]:.3f} - {spec_ci[1]:.3f})\")\n",
|
| 360 |
-
"
|
| 361 |
]
|
| 362 |
},
|
| 363 |
{
|
|
@@ -663,7 +655,7 @@
|
|
| 663 |
],
|
| 664 |
"source": [
|
| 665 |
"for t, p in zip(target_list, pred_list):\n",
|
| 666 |
-
"
|
| 667 |
]
|
| 668 |
},
|
| 669 |
{
|
|
@@ -703,14 +695,13 @@
|
|
| 703 |
"pred_cat = [1.0 if i > 0.5 else 0.0 for i in pred_list]\n",
|
| 704 |
"from sklearn.metrics import (\n",
|
| 705 |
" accuracy_score,\n",
|
|
|
|
|
|
|
|
|
|
| 706 |
" precision_score,\n",
|
| 707 |
" recall_score,\n",
|
| 708 |
-
" f1_score,\n",
|
| 709 |
-
" balanced_accuracy_score,\n",
|
| 710 |
-
" confusion_matrix,\n",
|
| 711 |
-
" classification_report\n",
|
| 712 |
")\n",
|
| 713 |
-
"
|
| 714 |
"pred_cat = np.array(pred_cat)\n",
|
| 715 |
"# Basic metrics\n",
|
| 716 |
"acc = accuracy_score(target_list, pred_cat)\n",
|
|
@@ -834,11 +825,11 @@
|
|
| 834 |
}
|
| 835 |
],
|
| 836 |
"source": [
|
| 837 |
-
"updated_masks = os.listdir(
|
| 838 |
"c = 0\n",
|
| 839 |
"for i in false_negative_ids[0]:\n",
|
| 840 |
-
" if test_data[
|
| 841 |
-
" print(i)
|
| 842 |
]
|
| 843 |
},
|
| 844 |
{
|
|
@@ -896,10 +887,13 @@
|
|
| 896 |
"metadata": {},
|
| 897 |
"outputs": [],
|
| 898 |
"source": [
|
| 899 |
-
"\n",
|
| 900 |
"import json\n",
|
|
|
|
| 901 |
"import nrrd\n",
|
| 902 |
-
"
|
|
|
|
|
|
|
|
|
|
| 903 |
" test_data = json.load(f)"
|
| 904 |
]
|
| 905 |
},
|
|
@@ -936,18 +930,19 @@
|
|
| 936 |
}
|
| 937 |
],
|
| 938 |
"source": [
|
| 939 |
-
"\n",
|
| 940 |
-
"
|
|
|
|
| 941 |
"\n",
|
| 942 |
"id = 195\n",
|
| 943 |
"\n",
|
| 944 |
"\n",
|
| 945 |
-
"file = test_data[
|
| 946 |
"\n",
|
| 947 |
-
"t2_proc, _ = nrrd.read(os.path.join(t2_dir_proc,file))\n",
|
| 948 |
-
"dwi_proc, _ = nrrd.read(test_data[
|
| 949 |
-
"adc_proc, _ = nrrd.read(test_data[
|
| 950 |
-
"heats, _ = nrrd.read(test_data[
|
| 951 |
"import matplotlib.pyplot as plt\n",
|
| 952 |
"import numpy as np\n",
|
| 953 |
"\n",
|
|
@@ -969,12 +964,12 @@
|
|
| 969 |
"else:\n",
|
| 970 |
" # 3. Create the dynamic grid based on how many valid slices were found\n",
|
| 971 |
" fig, axes = plt.subplots(nrows=num_slices, ncols=3, figsize=(15, 5 * num_slices))\n",
|
| 972 |
-
"
|
| 973 |
-
" # If there is only exactly 1 slice with a heatmap, axes is 1D.
|
| 974 |
" # We force it to 2D so our loop doesn't break.\n",
|
| 975 |
" if num_slices == 1:\n",
|
| 976 |
" axes = np.expand_dims(axes, axis=0)\n",
|
| 977 |
-
"
|
| 978 |
" # 4. Plot the columns for the filtered slices\n",
|
| 979 |
" for i, z in enumerate(valid_slices):\n",
|
| 980 |
" # Extract the 2D slices\n",
|
|
@@ -982,30 +977,30 @@
|
|
| 982 |
" adc_slice = adc_proc[:, :, z].T\n",
|
| 983 |
" dwi_slice = dwi_proc[:, :, z].T\n",
|
| 984 |
" heat_slice = heats[:, :, z].T\n",
|
| 985 |
-
"
|
| 986 |
" # Mask the background zeroes of the heatmap so it doesn't tint the healthy tissue\n",
|
| 987 |
" masked_heat = np.ma.masked_where(heat_slice <= 0.01, heat_slice)\n",
|
| 988 |
-
"
|
| 989 |
" # --- Column 1: T2 + Heatmap ---\n",
|
| 990 |
-
" axes[i, 0].imshow(t2_slice, cmap=
|
| 991 |
-
" axes[i, 0].imshow(masked_heat, cmap=
|
| 992 |
-
" axes[i, 0].set_title(f
|
| 993 |
-
" axes[i, 0].axis(
|
| 994 |
-
"
|
| 995 |
" # --- Column 2: ADC + Heatmap ---\n",
|
| 996 |
-
" axes[i, 1].imshow(adc_slice, cmap=
|
| 997 |
-
" axes[i, 1].imshow(masked_heat, cmap=
|
| 998 |
-
" axes[i, 1].set_title(f
|
| 999 |
-
" axes[i, 1].axis(
|
| 1000 |
-
"
|
| 1001 |
" # --- Column 3: DWI (No Heatmap) ---\n",
|
| 1002 |
-
" axes[i, 2].imshow(dwi_slice, cmap=
|
| 1003 |
-
" axes[i, 2].set_title(f
|
| 1004 |
-
" axes[i, 2].axis(
|
| 1005 |
-
"
|
| 1006 |
" plt.tight_layout()\n",
|
| 1007 |
" plt.show()\n",
|
| 1008 |
-
"print(file)
|
| 1009 |
]
|
| 1010 |
},
|
| 1011 |
{
|
|
@@ -1017,10 +1012,10 @@
|
|
| 1017 |
"source": [
|
| 1018 |
"import shutil\n",
|
| 1019 |
"\n",
|
| 1020 |
-
"in_folder =
|
| 1021 |
-
"out_folder =
|
| 1022 |
-
"for i in [
|
| 1023 |
-
" shutil.copy(os.path.join(in_folder, i, file), os.path.join(out_folder, i.split(
|
| 1024 |
]
|
| 1025 |
},
|
| 1026 |
{
|
|
@@ -1038,26 +1033,24 @@
|
|
| 1038 |
"metadata": {},
|
| 1039 |
"outputs": [],
|
| 1040 |
"source": [
|
| 1041 |
-
"import argparse\n",
|
| 1042 |
"import logging\n",
|
| 1043 |
"import os\n",
|
| 1044 |
"import shutil\n",
|
| 1045 |
"import sys\n",
|
| 1046 |
"import time\n",
|
| 1047 |
"from pathlib import Path\n",
|
|
|
|
| 1048 |
"\n",
|
| 1049 |
"import numpy as np\n",
|
| 1050 |
"import torch\n",
|
| 1051 |
"import wandb\n",
|
| 1052 |
"import yaml\n",
|
| 1053 |
"from monai.utils import set_determinism\n",
|
| 1054 |
-
"from torch.utils.tensorboard import SummaryWriter\n",
|
| 1055 |
"\n",
|
| 1056 |
"from src.data.data_loader import get_dataloader\n",
|
| 1057 |
"from src.model.mil import MILModel3D\n",
|
| 1058 |
-
"from src.train.train_pirads import
|
| 1059 |
-
"from src.utils import
|
| 1060 |
-
"from types import SimpleNamespace\n"
|
| 1061 |
]
|
| 1062 |
},
|
| 1063 |
{
|
|
@@ -1069,22 +1062,22 @@
|
|
| 1069 |
"source": [
|
| 1070 |
"from types import SimpleNamespace\n",
|
| 1071 |
"\n",
|
| 1072 |
-
"with open(\"config/config_pirads_test.yaml\"
|
| 1073 |
" config = yaml.safe_load(f)\n",
|
| 1074 |
"\n",
|
| 1075 |
"\n",
|
| 1076 |
"args = SimpleNamespace(**config)\n",
|
| 1077 |
"print(args.data_root)\n",
|
| 1078 |
"\n",
|
| 1079 |
-
"args.mode =
|
| 1080 |
"args.project_dir = Path.cwd()\n",
|
| 1081 |
-
"args.run_name =
|
| 1082 |
"args.checkpoint_pirads = None\n",
|
| 1083 |
"args.wandb = False\n",
|
| 1084 |
"args.optim_lr = 2e-5\n",
|
| 1085 |
"args.batch_size = 8\n",
|
| 1086 |
"args.epochs = 1\n",
|
| 1087 |
-
"args.project_name =
|
| 1088 |
"args.workers = 0\n",
|
| 1089 |
"args.use_psa = False"
|
| 1090 |
]
|
|
@@ -1096,8 +1089,6 @@
|
|
| 1096 |
"metadata": {},
|
| 1097 |
"outputs": [],
|
| 1098 |
"source": [
|
| 1099 |
-
"\n",
|
| 1100 |
-
"\n",
|
| 1101 |
"args.logdir = os.path.join(args.project_dir, \"logs\", args.run_name)\n",
|
| 1102 |
"os.makedirs(args.logdir, exist_ok=True)\n",
|
| 1103 |
"args.logfile = os.path.join(args.logdir, f\"{args.run_name}.log\")\n",
|
|
@@ -1133,7 +1124,7 @@
|
|
| 1133 |
" dir=os.path.join(args.logdir, \"wandb\"),\n",
|
| 1134 |
" config=config_wandb,\n",
|
| 1135 |
" mode=mode_wandb,\n",
|
| 1136 |
-
")
|
| 1137 |
]
|
| 1138 |
},
|
| 1139 |
{
|
|
@@ -1197,15 +1188,15 @@
|
|
| 1197 |
"metadata": {},
|
| 1198 |
"outputs": [],
|
| 1199 |
"source": [
|
| 1200 |
-
"import argparse\n",
|
| 1201 |
"import logging\n",
|
| 1202 |
-
"
|
| 1203 |
-
"from tqdm import tqdm\n",
|
| 1204 |
"import numpy as np\n",
|
| 1205 |
"import torch\n",
|
| 1206 |
"import torch.nn as nn\n",
|
| 1207 |
"from monai.metrics import Cumulative, CumulativeAverage\n",
|
| 1208 |
"from sklearn.metrics import cohen_kappa_score\n",
|
|
|
|
|
|
|
| 1209 |
"valid_loader = get_dataloader(args, split=args.mode)\n",
|
| 1210 |
"\n",
|
| 1211 |
"criterion = nn.CrossEntropyLoss()\n",
|
|
@@ -1230,7 +1221,6 @@
|
|
| 1230 |
" with torch.amp.autocast(device_type=str(args.device), enabled=args.amp):\n",
|
| 1231 |
" logits = model(data)\n",
|
| 1232 |
"\n",
|
| 1233 |
-
"\n",
|
| 1234 |
" data = data.to(\"cpu\")\n",
|
| 1235 |
" target = target.to(\"cpu\")\n",
|
| 1236 |
" logits = logits.to(\"cpu\")\n",
|
|
@@ -1242,7 +1232,6 @@
|
|
| 1242 |
" targets_cumulative.extend(target.detach().cpu())\n",
|
| 1243 |
" targets_cumulative_bin.extend(target_bin.detach().cpu())\n",
|
| 1244 |
"\n",
|
| 1245 |
-
"\n",
|
| 1246 |
" del data, target, logits\n",
|
| 1247 |
" torch.cuda.empty_cache()\n",
|
| 1248 |
"\n",
|
|
@@ -1253,7 +1242,7 @@
|
|
| 1253 |
"preds_updated = preds_cumulative + 2\n",
|
| 1254 |
"\n",
|
| 1255 |
"if os.path.exists(cache_dir_):\n",
|
| 1256 |
-
" shutil.rmtree(cache_dir_)
|
| 1257 |
]
|
| 1258 |
},
|
| 1259 |
{
|
|
@@ -1263,9 +1252,7 @@
|
|
| 1263 |
"metadata": {},
|
| 1264 |
"outputs": [],
|
| 1265 |
"source": [
|
| 1266 |
-
"qwk = cohen_kappa_score(\n",
|
| 1267 |
-
" targets_cumulative.astype(np.float64), preds_updated.astype(np.float64)\n",
|
| 1268 |
-
")\n",
|
| 1269 |
"qwk"
|
| 1270 |
]
|
| 1271 |
},
|
|
@@ -1288,10 +1275,7 @@
|
|
| 1288 |
"source": [
|
| 1289 |
"import pandas as pd\n",
|
| 1290 |
"\n",
|
| 1291 |
-
"df = pd.DataFrame({\n",
|
| 1292 |
-
" \"target\": targets_cumulative,\n",
|
| 1293 |
-
" \"pred\": preds_updated\n",
|
| 1294 |
-
"})\n",
|
| 1295 |
"\n",
|
| 1296 |
"# Group by class and show summary stats\n",
|
| 1297 |
"\n",
|
|
@@ -1309,6 +1293,7 @@
|
|
| 1309 |
"outputs": [],
|
| 1310 |
"source": [
|
| 1311 |
"from sklearn.metrics import roc_auc_score\n",
|
|
|
|
| 1312 |
"roc_auc_score(targets_cumulative_bin, preds_cumulative)"
|
| 1313 |
]
|
| 1314 |
},
|
|
@@ -1321,28 +1306,23 @@
|
|
| 1321 |
"source": [
|
| 1322 |
"import pandas as pd\n",
|
| 1323 |
"\n",
|
| 1324 |
-
"df = pd.DataFrame({\n",
|
| 1325 |
-
" 'pirads': preds_updated,\n",
|
| 1326 |
-
" 'cspca': targets_cumulative_bin\n",
|
| 1327 |
-
"})\n",
|
| 1328 |
-
"import seaborn as sns\n",
|
| 1329 |
"import matplotlib.pyplot as plt\n",
|
| 1330 |
-
"\n",
|
| 1331 |
"import pandas as pd\n",
|
|
|
|
| 1332 |
"\n",
|
| 1333 |
-
"ct = pd.crosstab(df[
|
| 1334 |
"\n",
|
| 1335 |
"# Add totals\n",
|
| 1336 |
"ct_with_totals = ct.copy()\n",
|
| 1337 |
-
"ct_with_totals[
|
| 1338 |
-
"ct_with_totals.loc[
|
| 1339 |
-
"import pandas as pd\n",
|
| 1340 |
-
"import seaborn as sns\n",
|
| 1341 |
"import matplotlib.pyplot as plt\n",
|
| 1342 |
"import numpy as np\n",
|
|
|
|
| 1343 |
"\n",
|
| 1344 |
"# Base crosstab\n",
|
| 1345 |
-
"ct = pd.crosstab(df[
|
| 1346 |
"\n",
|
| 1347 |
"# Row totals\n",
|
| 1348 |
"row_totals = ct.sum(axis=1)\n",
|
|
@@ -1351,50 +1331,43 @@
|
|
| 1351 |
"col_totals = ct.sum(axis=0)\n",
|
| 1352 |
"\n",
|
| 1353 |
"# ---- Plot ----\n",
|
| 1354 |
-
"fig, ax = plt.subplots(figsize=(7,5))\n",
|
| 1355 |
"\n",
|
| 1356 |
"# Heatmap ONLY for actual data\n",
|
| 1357 |
-
"sns.heatmap(\n",
|
| 1358 |
-
" ct,\n",
|
| 1359 |
-
" annot=True,\n",
|
| 1360 |
-
" fmt='d',\n",
|
| 1361 |
-
" cmap='Blues',\n",
|
| 1362 |
-
" cbar=False,\n",
|
| 1363 |
-
" ax=ax\n",
|
| 1364 |
-
")\n",
|
| 1365 |
"\n",
|
| 1366 |
"# ---- Add row totals (right side, no color) ----\n",
|
| 1367 |
"for i, val in enumerate(row_totals):\n",
|
| 1368 |
" ax.text(\n",
|
| 1369 |
-
" ct.shape[1] + 0.3,
|
| 1370 |
" i + 0.5,\n",
|
| 1371 |
" str(val),\n",
|
| 1372 |
-
" va=
|
| 1373 |
" )\n",
|
| 1374 |
"\n",
|
| 1375 |
"# Label for row totals\n",
|
| 1376 |
-
"ax.text(ct.shape[1] + 0.3, -0.3,
|
| 1377 |
"\n",
|
| 1378 |
"# ---- Add column totals (below x-axis) ----\n",
|
| 1379 |
"for j, val in enumerate(col_totals):\n",
|
| 1380 |
" ax.text(\n",
|
| 1381 |
" j + 0.5,\n",
|
| 1382 |
-
" ct.shape[0] + 0.3,
|
| 1383 |
" str(val),\n",
|
| 1384 |
-
" ha=
|
| 1385 |
" )\n",
|
| 1386 |
"\n",
|
| 1387 |
"# Label for column totals\n",
|
| 1388 |
-
"ax.text(-0.5, ct.shape[0] + 0.3,
|
| 1389 |
"\n",
|
| 1390 |
"# ---- Adjust limits so text is visible ----\n",
|
| 1391 |
"ax.set_xlim(0, ct.shape[1] + 1)\n",
|
| 1392 |
"ax.set_ylim(ct.shape[0] + 1, 0)\n",
|
| 1393 |
"\n",
|
| 1394 |
"# Titles and labels\n",
|
| 1395 |
-
"ax.set_title(
|
| 1396 |
-
"ax.set_xlabel(
|
| 1397 |
-
"ax.set_ylabel(
|
| 1398 |
"\n",
|
| 1399 |
"plt.tight_layout()\n",
|
| 1400 |
"plt.show()"
|
|
@@ -1407,18 +1380,19 @@
|
|
| 1407 |
"metadata": {},
|
| 1408 |
"outputs": [],
|
| 1409 |
"source": [
|
| 1410 |
-
"pirads_pred = [1.0 if i >=3 else 0.0 for i in preds_updated]\n",
|
| 1411 |
"pirads_pred\n",
|
|
|
|
| 1412 |
"from sklearn.metrics import (\n",
|
| 1413 |
" accuracy_score,\n",
|
| 1414 |
-
" precision_score,\n",
|
| 1415 |
-
" recall_score,\n",
|
| 1416 |
-
" f1_score,\n",
|
| 1417 |
" balanced_accuracy_score,\n",
|
|
|
|
| 1418 |
" confusion_matrix,\n",
|
| 1419 |
-
"
|
|
|
|
|
|
|
| 1420 |
")\n",
|
| 1421 |
-
"
|
| 1422 |
"pred_cat = np.array(pirads_pred)\n",
|
| 1423 |
"target_list = targets_cumulative_bin\n",
|
| 1424 |
"# Basic metrics\n",
|
|
@@ -1466,7 +1440,7 @@
|
|
| 1466 |
"metadata": {},
|
| 1467 |
"outputs": [],
|
| 1468 |
"source": [
|
| 1469 |
-
"import
|
| 1470 |
"import logging\n",
|
| 1471 |
"import os\n",
|
| 1472 |
"import shutil\n",
|
|
@@ -1479,14 +1453,12 @@
|
|
| 1479 |
"import wandb\n",
|
| 1480 |
"import yaml\n",
|
| 1481 |
"from monai.utils import set_determinism\n",
|
| 1482 |
-
"from torch.utils.tensorboard import SummaryWriter\n",
|
| 1483 |
"from sklearn.preprocessing import StandardScaler\n",
|
| 1484 |
-
"import json\n",
|
| 1485 |
"\n",
|
| 1486 |
"from src.data.data_loader import get_dataloader\n",
|
| 1487 |
"from src.model.mil import MILModel3D\n",
|
| 1488 |
-
"from src.train.train_pirads import
|
| 1489 |
-
"from src.utils import
|
| 1490 |
]
|
| 1491 |
},
|
| 1492 |
{
|
|
@@ -1506,22 +1478,22 @@
|
|
| 1506 |
"source": [
|
| 1507 |
"from types import SimpleNamespace\n",
|
| 1508 |
"\n",
|
| 1509 |
-
"with open(\"config/config_pirads_train.yaml\"
|
| 1510 |
" config = yaml.safe_load(f)\n",
|
| 1511 |
"\n",
|
| 1512 |
"\n",
|
| 1513 |
"args = SimpleNamespace(**config)\n",
|
| 1514 |
"print(args.data_root)\n",
|
| 1515 |
"\n",
|
| 1516 |
-
"args.mode =
|
| 1517 |
"args.project_dir = Path.cwd()\n",
|
| 1518 |
-
"args.run_name =
|
| 1519 |
"args.checkpoint = None\n",
|
| 1520 |
"args.wandb = False\n",
|
| 1521 |
"args.optim_lr = 2e-5\n",
|
| 1522 |
"args.batch_size = 8\n",
|
| 1523 |
"args.epochs = 1\n",
|
| 1524 |
-
"args.project_name =
|
| 1525 |
"args.workers = 0\n",
|
| 1526 |
"args.use_psa = False"
|
| 1527 |
]
|
|
@@ -1542,8 +1514,6 @@
|
|
| 1542 |
}
|
| 1543 |
],
|
| 1544 |
"source": [
|
| 1545 |
-
"\n",
|
| 1546 |
-
"\n",
|
| 1547 |
"args.logdir = os.path.join(args.project_dir, \"logs\", args.run_name)\n",
|
| 1548 |
"os.makedirs(args.logdir, exist_ok=True)\n",
|
| 1549 |
"args.logfile = os.path.join(args.logdir, f\"{args.run_name}.log\")\n",
|
|
@@ -1579,7 +1549,7 @@
|
|
| 1579 |
" dir=os.path.join(args.logdir, \"wandb\"),\n",
|
| 1580 |
" config=config_wandb,\n",
|
| 1581 |
" mode=mode_wandb,\n",
|
| 1582 |
-
")
|
| 1583 |
]
|
| 1584 |
},
|
| 1585 |
{
|
|
@@ -1616,7 +1586,7 @@
|
|
| 1616 |
"scaler = StandardScaler()\n",
|
| 1617 |
"with open(os.path.join(args.project_dir, \"dataset\", \"PICAI_cspca_updated_with_psa.json\")) as f:\n",
|
| 1618 |
" dataset_json = json.load(f)\n",
|
| 1619 |
-
"train_clinical = [i[
|
| 1620 |
"_ = scaler.fit_transform(train_clinical)\n",
|
| 1621 |
"args.psa_mean = scaler.mean_.tolist()\n",
|
| 1622 |
"args.psa_std = scaler.scale_.tolist()"
|
|
@@ -1631,9 +1601,7 @@
|
|
| 1631 |
"source": [
|
| 1632 |
"train_loader = get_dataloader(args, split=\"train\")\n",
|
| 1633 |
"valid_loader = get_dataloader(args, split=\"test\")\n",
|
| 1634 |
-
"logging.info(\n",
|
| 1635 |
-
" f\"Dataset training: {len(train_loader.dataset)}, test: {len(valid_loader.dataset)}\"\n",
|
| 1636 |
-
")\n",
|
| 1637 |
"\n",
|
| 1638 |
"if args.mil_mode in [\"att_trans\", \"att_trans_pyramid\"]:\n",
|
| 1639 |
" params = [\n",
|
|
@@ -1641,15 +1609,13 @@
|
|
| 1641 |
" \"params\": list(model.attention.parameters())\n",
|
| 1642 |
" + list(model.myfc.parameters())\n",
|
| 1643 |
" + list(model.net.parameters())\n",
|
| 1644 |
-
" #+ list(model.feature_proj.parameters()),\n",
|
| 1645 |
" },\n",
|
| 1646 |
" {\"params\": list(model.transformer.parameters()), \"lr\": 6e-5, \"weight_decay\": 0.1},\n",
|
| 1647 |
" ]\n",
|
| 1648 |
"\n",
|
| 1649 |
"optimizer = torch.optim.AdamW(params, lr=args.optim_lr, weight_decay=args.weight_decay)\n",
|
| 1650 |
-
"scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n",
|
| 1651 |
-
" optimizer, T_max=args.epochs, eta_min=0\n",
|
| 1652 |
-
")\n",
|
| 1653 |
"scaler = torch.amp.GradScaler(device=str(args.device), enabled=args.amp)"
|
| 1654 |
]
|
| 1655 |
},
|
|
@@ -1660,7 +1626,6 @@
|
|
| 1660 |
"metadata": {},
|
| 1661 |
"outputs": [],
|
| 1662 |
"source": [
|
| 1663 |
-
"import argparse\n",
|
| 1664 |
"import logging\n",
|
| 1665 |
"import time\n",
|
| 1666 |
"\n",
|
|
@@ -1694,10 +1659,10 @@
|
|
| 1694 |
" # Classification Loss\n",
|
| 1695 |
" logits_attn = model(shuffled_images, no_head=True)\n",
|
| 1696 |
" x = logits_attn.to(torch.float32)\n",
|
| 1697 |
-
" #x = x.permute(1, 0, 2)\n",
|
| 1698 |
-
" #x = model.feature_proj(x)\n",
|
| 1699 |
" x = model.transformer(x)\n",
|
| 1700 |
-
" #x = x.permute(1, 0, 2)\n",
|
| 1701 |
" a = model.attention(x)\n",
|
| 1702 |
" a = torch.softmax(a, dim=1)\n",
|
| 1703 |
" x = torch.sum(x * a, dim=1)\n",
|
|
@@ -1940,7 +1905,7 @@
|
|
| 1940 |
}
|
| 1941 |
],
|
| 1942 |
"source": [
|
| 1943 |
-
"(pred_pirads == target.sum(dim=1)).sum()/len(pred_pirads)"
|
| 1944 |
]
|
| 1945 |
},
|
| 1946 |
{
|
|
@@ -2000,11 +1965,250 @@
|
|
| 2000 |
"outputs": [],
|
| 2001 |
"source": []
|
| 2002 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2003 |
{
|
| 2004 |
"cell_type": "code",
|
| 2005 |
-
"execution_count":
|
| 2006 |
"id": "df58821b",
|
| 2007 |
"metadata": {},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2008 |
"outputs": [],
|
| 2009 |
"source": []
|
| 2010 |
}
|
|
|
|
| 15 |
"metadata": {},
|
| 16 |
"outputs": [],
|
| 17 |
"source": [
|
| 18 |
+
"import json\n",
|
| 19 |
"import logging\n",
|
| 20 |
"import os\n",
|
| 21 |
"import shutil\n",
|
| 22 |
"import sys\n",
|
| 23 |
"from pathlib import Path\n",
|
| 24 |
+
"from types import SimpleNamespace\n",
|
| 25 |
"\n",
|
| 26 |
"import torch\n",
|
| 27 |
"import yaml\n",
|
| 28 |
"from monai.utils import set_determinism\n",
|
| 29 |
"from sklearn.preprocessing import StandardScaler\n",
|
| 30 |
+
"from tqdm import tqdm\n",
|
| 31 |
"\n",
|
| 32 |
"from src.data.data_loader import get_dataloader\n",
|
| 33 |
"from src.model.cspca_model import CSPCAModel\n",
|
| 34 |
"from src.model.mil import MILModel3D\n",
|
| 35 |
+
"from src.train.train_cspca import val_epoch\n",
|
| 36 |
+
"from src.utils import setup_logging"
|
|
|
|
|
|
|
| 37 |
]
|
| 38 |
},
|
| 39 |
{
|
|
|
|
| 51 |
}
|
| 52 |
],
|
| 53 |
"source": [
|
| 54 |
+
"with open(\"config/config_cspca_test.yaml\") as f:\n",
|
| 55 |
" config = yaml.safe_load(f)\n",
|
| 56 |
"\n",
|
| 57 |
"args = SimpleNamespace(**config)\n",
|
| 58 |
"print(args.data_root)\n",
|
| 59 |
"\n",
|
| 60 |
+
"args.mode = \"test\"\n",
|
| 61 |
"args.project_dir = None\n",
|
| 62 |
"args.project_dir = Path.cwd()\n",
|
| 63 |
+
"args.run_name = \"test_dummy\"\n",
|
| 64 |
"args.checkpoint_pirads = None\n",
|
| 65 |
"\n",
|
| 66 |
"scaler = StandardScaler()\n",
|
| 67 |
"with open(os.path.join(args.project_dir, \"dataset\", \"PICAI_cspca_updated_with_psa.json\")) as f:\n",
|
| 68 |
" dataset_json = json.load(f)\n",
|
| 69 |
+
"train_clinical = [i[\"psa\"] for i in dataset_json[\"train\"]]\n",
|
| 70 |
"_ = scaler.fit_transform(train_clinical)\n",
|
| 71 |
"args.psa_mean = scaler.mean_.tolist()\n",
|
| 72 |
"args.psa_std = scaler.scale_.tolist()\n",
|
| 73 |
"args.use_psa = True\n",
|
| 74 |
"args.batch_size = 1\n",
|
| 75 |
+
"args.dataset_json = \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/TCIA_test_data_updated_mask.json\""
|
| 76 |
]
|
| 77 |
},
|
| 78 |
{
|
|
|
|
| 82 |
"metadata": {},
|
| 83 |
"outputs": [],
|
| 84 |
"source": [
|
|
|
|
| 85 |
"args.logdir = os.path.join(args.project_dir, \"logs\", args.run_name)\n",
|
| 86 |
"os.makedirs(args.logdir, exist_ok=True)\n",
|
| 87 |
"args.logfile = os.path.join(args.logdir, f\"{args.run_name}.log\")\n",
|
|
|
|
| 134 |
" f\"csPCa Model loaded from {args.checkpoint_cspca} with AUC: {auc}, Sensitivity: {sens}, Specificity: {spec} on the test set.\"\n",
|
| 135 |
" )\n",
|
| 136 |
"else:\n",
|
| 137 |
+
" logging.info(f\"csPCa Model loaded from {args.checkpoint_cspca}.\")"
|
| 138 |
]
|
| 139 |
},
|
| 140 |
{
|
|
|
|
| 167 |
"metrics_dict[\"specificity\"].append(test_metric[\"specificity\"])\n",
|
| 168 |
"\n",
|
| 169 |
"\n",
|
| 170 |
+
"\n",
|
| 171 |
"shutil.rmtree(cache_dir_path)\n",
|
| 172 |
"\n",
|
| 173 |
"metrics_dict"
|
|
|
|
| 204 |
}
|
| 205 |
],
|
| 206 |
"source": [
|
| 207 |
+
"import numpy as np\n",
|
| 208 |
"import torch\n",
|
| 209 |
"import torch.nn as nn\n",
|
| 210 |
"from monai.metrics import Cumulative, CumulativeAverage\n",
|
| 211 |
"from sklearn.metrics import confusion_matrix, roc_auc_score\n",
|
| 212 |
"\n",
|
|
|
|
|
|
|
|
|
|
| 213 |
"spec_list = []\n",
|
| 214 |
"\n",
|
| 215 |
"auc_list = []\n",
|
| 216 |
"import os\n",
|
| 217 |
+
"\n",
|
| 218 |
+
"os.environ[\"CUDA_LAUNCH_BLOCKING\"] = \"1\"\n",
|
| 219 |
"\n",
|
| 220 |
"for st in list(range(10)):\n",
|
| 221 |
" set_determinism(seed=st)\n",
|
|
|
|
| 233 |
" target = batch_data[\"label\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 234 |
" psa_data = batch_data[\"psa\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 235 |
"\n",
|
| 236 |
+
" output = cspca_model(x=data, psa_data=psa_data)\n",
|
| 237 |
" output = output.squeeze(1)\n",
|
| 238 |
" loss = criterion(output, target)\n",
|
| 239 |
"\n",
|
|
|
|
| 244 |
" loss_epoch = run_loss.aggregate()\n",
|
| 245 |
" target_list = targets_cumulative.get_buffer().cpu().numpy()\n",
|
| 246 |
" pred_list = preds_cumulative.get_buffer().cpu().numpy()\n",
|
| 247 |
+
" # auc_epoch = roc_auc_score(target_list, pred_list)\n",
|
| 248 |
" y_pred_categoric = pred_list >= 0.5\n",
|
| 249 |
+
" \"\"\"\n",
|
| 250 |
" tn, fp, fn, tp = confusion_matrix(target_list, y_pred_categoric).ravel()\n",
|
| 251 |
" sens_epoch = tp / (tp + fn)\n",
|
| 252 |
" spec_epoch = tn / (tn + fp)\n",
|
|
|
|
| 258 |
" \"specificity\": spec_epoch,\n",
|
| 259 |
" }\n",
|
| 260 |
" return val_epoch_metric\n",
|
| 261 |
+
" \"\"\"\n",
|
| 262 |
" shutil.rmtree(cache_dir_path)\n",
|
| 263 |
" auc_epoch = roc_auc_score(target_list, pred_list)\n",
|
| 264 |
" auc_list.append(auc_epoch)"
|
|
|
|
| 319 |
}
|
| 320 |
],
|
| 321 |
"source": [
|
|
|
|
| 322 |
"from scipy import stats\n",
|
| 323 |
+
"\n",
|
| 324 |
+
"\"\"\"\n",
|
| 325 |
"# Example values from 10 seeds\n",
|
| 326 |
"auc_list = [0.81, 0.84, 0.79, 0.83, 0.82,\n",
|
| 327 |
" 0.85, 0.80, 0.84, 0.83, 0.81]\n",
|
| 328 |
"\n",
|
| 329 |
"spec_list = [0.72, 0.75, 0.70, 0.74, 0.73,\n",
|
| 330 |
" 0.76, 0.71, 0.75, 0.74, 0.72]\n",
|
| 331 |
+
"\"\"\"\n",
|
| 332 |
+
"\n",
|
| 333 |
"\n",
|
| 334 |
"def mean_confidence_interval(values, confidence=0.95):\n",
|
| 335 |
" values = np.array(values)\n",
|
|
|
|
| 337 |
" mean = np.mean(values)\n",
|
| 338 |
" sem = stats.sem(values) # standard error\n",
|
| 339 |
"\n",
|
| 340 |
+
" ci = stats.t.interval(confidence, df=len(values) - 1, loc=mean, scale=sem)\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
"\n",
|
| 342 |
" return mean, ci\n",
|
| 343 |
"\n",
|
| 344 |
"\n",
|
| 345 |
"auc_mean, auc_ci = mean_confidence_interval(auc_list)\n",
|
| 346 |
+
"# spec_mean, spec_ci = mean_confidence_interval(spec_list)\n",
|
| 347 |
"\n",
|
| 348 |
+
"print(f\"AUC: {auc_mean:.3f} (95% CI: {auc_ci[0]:.3f} - {auc_ci[1]:.3f})\")\n",
|
| 349 |
+
"\"\"\"\n",
|
|
|
|
| 350 |
"print(f\"Specificity: {spec_mean:.3f} \"\n",
|
| 351 |
" f\"(95% CI: {spec_ci[0]:.3f} - {spec_ci[1]:.3f})\")\n",
|
| 352 |
+
"\"\"\""
|
| 353 |
]
|
| 354 |
},
|
| 355 |
{
|
|
|
|
| 655 |
],
|
| 656 |
"source": [
|
| 657 |
"for t, p in zip(target_list, pred_list):\n",
|
| 658 |
+
" print(f\"target: {t} | pred: {p:.4f}\")"
|
| 659 |
]
|
| 660 |
},
|
| 661 |
{
|
|
|
|
| 695 |
"pred_cat = [1.0 if i > 0.5 else 0.0 for i in pred_list]\n",
|
| 696 |
"from sklearn.metrics import (\n",
|
| 697 |
" accuracy_score,\n",
|
| 698 |
+
" balanced_accuracy_score,\n",
|
| 699 |
+
" classification_report,\n",
|
| 700 |
+
" f1_score,\n",
|
| 701 |
" precision_score,\n",
|
| 702 |
" recall_score,\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
")\n",
|
| 704 |
+
"\n",
|
| 705 |
"pred_cat = np.array(pred_cat)\n",
|
| 706 |
"# Basic metrics\n",
|
| 707 |
"acc = accuracy_score(target_list, pred_cat)\n",
|
|
|
|
| 825 |
}
|
| 826 |
],
|
| 827 |
"source": [
|
| 828 |
+
"updated_masks = os.listdir(\"updated_segmentations/\")\n",
|
| 829 |
"c = 0\n",
|
| 830 |
"for i in false_negative_ids[0]:\n",
|
| 831 |
+
" if test_data[\"test\"][i][\"image\"] in updated_masks:\n",
|
| 832 |
+
" print(i)"
|
| 833 |
]
|
| 834 |
},
|
| 835 |
{
|
|
|
|
| 887 |
"metadata": {},
|
| 888 |
"outputs": [],
|
| 889 |
"source": [
|
|
|
|
| 890 |
"import json\n",
|
| 891 |
+
"\n",
|
| 892 |
"import nrrd\n",
|
| 893 |
+
"\n",
|
| 894 |
+
"with open(\n",
|
| 895 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/TCIA_test_data.json\",\n",
|
| 896 |
+
") as f:\n",
|
| 897 |
" test_data = json.load(f)"
|
| 898 |
]
|
| 899 |
},
|
|
|
|
| 930 |
}
|
| 931 |
],
|
| 932 |
"source": [
|
| 933 |
+
"t2_dir_proc = (\n",
|
| 934 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/t2_registered\"\n",
|
| 935 |
+
")\n",
|
| 936 |
"\n",
|
| 937 |
"id = 195\n",
|
| 938 |
"\n",
|
| 939 |
"\n",
|
| 940 |
+
"file = test_data[\"test\"][id][\"image\"]\n",
|
| 941 |
"\n",
|
| 942 |
+
"t2_proc, _ = nrrd.read(os.path.join(t2_dir_proc, file))\n",
|
| 943 |
+
"dwi_proc, _ = nrrd.read(test_data[\"test\"][id][\"dwi\"])\n",
|
| 944 |
+
"adc_proc, _ = nrrd.read(test_data[\"test\"][id][\"adc\"]) #\n",
|
| 945 |
+
"heats, _ = nrrd.read(test_data[\"test\"][id][\"heatmap\"])\n",
|
| 946 |
"import matplotlib.pyplot as plt\n",
|
| 947 |
"import numpy as np\n",
|
| 948 |
"\n",
|
|
|
|
| 964 |
"else:\n",
|
| 965 |
" # 3. Create the dynamic grid based on how many valid slices were found\n",
|
| 966 |
" fig, axes = plt.subplots(nrows=num_slices, ncols=3, figsize=(15, 5 * num_slices))\n",
|
| 967 |
+
"\n",
|
| 968 |
+
" # If there is only exactly 1 slice with a heatmap, axes is 1D.\n",
|
| 969 |
" # We force it to 2D so our loop doesn't break.\n",
|
| 970 |
" if num_slices == 1:\n",
|
| 971 |
" axes = np.expand_dims(axes, axis=0)\n",
|
| 972 |
+
"\n",
|
| 973 |
" # 4. Plot the columns for the filtered slices\n",
|
| 974 |
" for i, z in enumerate(valid_slices):\n",
|
| 975 |
" # Extract the 2D slices\n",
|
|
|
|
| 977 |
" adc_slice = adc_proc[:, :, z].T\n",
|
| 978 |
" dwi_slice = dwi_proc[:, :, z].T\n",
|
| 979 |
" heat_slice = heats[:, :, z].T\n",
|
| 980 |
+
"\n",
|
| 981 |
" # Mask the background zeroes of the heatmap so it doesn't tint the healthy tissue\n",
|
| 982 |
" masked_heat = np.ma.masked_where(heat_slice <= 0.01, heat_slice)\n",
|
| 983 |
+
"\n",
|
| 984 |
" # --- Column 1: T2 + Heatmap ---\n",
|
| 985 |
+
" axes[i, 0].imshow(t2_slice, cmap=\"gray\")\n",
|
| 986 |
+
" axes[i, 0].imshow(masked_heat, cmap=\"jet\", alpha=0.5)\n",
|
| 987 |
+
" axes[i, 0].set_title(f\"T2 + Heatmap (Slice {z})\", fontsize=14)\n",
|
| 988 |
+
" axes[i, 0].axis(\"off\")\n",
|
| 989 |
+
"\n",
|
| 990 |
" # --- Column 2: ADC + Heatmap ---\n",
|
| 991 |
+
" axes[i, 1].imshow(adc_slice, cmap=\"gray\")\n",
|
| 992 |
+
" axes[i, 1].imshow(masked_heat, cmap=\"jet\", alpha=0.5)\n",
|
| 993 |
+
" axes[i, 1].set_title(f\"ADC + Heatmap (Slice {z})\", fontsize=14)\n",
|
| 994 |
+
" axes[i, 1].axis(\"off\")\n",
|
| 995 |
+
"\n",
|
| 996 |
" # --- Column 3: DWI (No Heatmap) ---\n",
|
| 997 |
+
" axes[i, 2].imshow(dwi_slice, cmap=\"gray\")\n",
|
| 998 |
+
" axes[i, 2].set_title(f\"DWI (Slice {z})\", fontsize=14)\n",
|
| 999 |
+
" axes[i, 2].axis(\"off\")\n",
|
| 1000 |
+
"\n",
|
| 1001 |
" plt.tight_layout()\n",
|
| 1002 |
" plt.show()\n",
|
| 1003 |
+
"print(file)"
|
| 1004 |
]
|
| 1005 |
},
|
| 1006 |
{
|
|
|
|
| 1012 |
"source": [
|
| 1013 |
"import shutil\n",
|
| 1014 |
"\n",
|
| 1015 |
+
"in_folder = \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed\"\n",
|
| 1016 |
+
"out_folder = \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/keno_files\"\n",
|
| 1017 |
+
"for i in [\"t2_registered\", \"ADC_clipped\", \"DWI_registered\", \"smooth_prostate_mask\"]:\n",
|
| 1018 |
+
" shutil.copy(os.path.join(in_folder, i, file), os.path.join(out_folder, i.split(\"_\")[0], file))"
|
| 1019 |
]
|
| 1020 |
},
|
| 1021 |
{
|
|
|
|
| 1033 |
"metadata": {},
|
| 1034 |
"outputs": [],
|
| 1035 |
"source": [
|
|
|
|
| 1036 |
"import logging\n",
|
| 1037 |
"import os\n",
|
| 1038 |
"import shutil\n",
|
| 1039 |
"import sys\n",
|
| 1040 |
"import time\n",
|
| 1041 |
"from pathlib import Path\n",
|
| 1042 |
+
"from types import SimpleNamespace\n",
|
| 1043 |
"\n",
|
| 1044 |
"import numpy as np\n",
|
| 1045 |
"import torch\n",
|
| 1046 |
"import wandb\n",
|
| 1047 |
"import yaml\n",
|
| 1048 |
"from monai.utils import set_determinism\n",
|
|
|
|
| 1049 |
"\n",
|
| 1050 |
"from src.data.data_loader import get_dataloader\n",
|
| 1051 |
"from src.model.mil import MILModel3D\n",
|
| 1052 |
+
"from src.train.train_pirads import val_epoch\n",
|
| 1053 |
+
"from src.utils import setup_logging"
|
|
|
|
| 1054 |
]
|
| 1055 |
},
|
| 1056 |
{
|
|
|
|
| 1062 |
"source": [
|
| 1063 |
"from types import SimpleNamespace\n",
|
| 1064 |
"\n",
|
| 1065 |
+
"with open(\"config/config_pirads_test.yaml\") as f:\n",
|
| 1066 |
" config = yaml.safe_load(f)\n",
|
| 1067 |
"\n",
|
| 1068 |
"\n",
|
| 1069 |
"args = SimpleNamespace(**config)\n",
|
| 1070 |
"print(args.data_root)\n",
|
| 1071 |
"\n",
|
| 1072 |
+
"args.mode = \"test\"\n",
|
| 1073 |
"args.project_dir = Path.cwd()\n",
|
| 1074 |
+
"args.run_name = \"test_dummy_pirads\"\n",
|
| 1075 |
"args.checkpoint_pirads = None\n",
|
| 1076 |
"args.wandb = False\n",
|
| 1077 |
"args.optim_lr = 2e-5\n",
|
| 1078 |
"args.batch_size = 8\n",
|
| 1079 |
"args.epochs = 1\n",
|
| 1080 |
+
"args.project_name = \"Dummy\"\n",
|
| 1081 |
"args.workers = 0\n",
|
| 1082 |
"args.use_psa = False"
|
| 1083 |
]
|
|
|
|
| 1089 |
"metadata": {},
|
| 1090 |
"outputs": [],
|
| 1091 |
"source": [
|
|
|
|
|
|
|
| 1092 |
"args.logdir = os.path.join(args.project_dir, \"logs\", args.run_name)\n",
|
| 1093 |
"os.makedirs(args.logdir, exist_ok=True)\n",
|
| 1094 |
"args.logfile = os.path.join(args.logdir, f\"{args.run_name}.log\")\n",
|
|
|
|
| 1124 |
" dir=os.path.join(args.logdir, \"wandb\"),\n",
|
| 1125 |
" config=config_wandb,\n",
|
| 1126 |
" mode=mode_wandb,\n",
|
| 1127 |
+
")"
|
| 1128 |
]
|
| 1129 |
},
|
| 1130 |
{
|
|
|
|
| 1188 |
"metadata": {},
|
| 1189 |
"outputs": [],
|
| 1190 |
"source": [
|
|
|
|
| 1191 |
"import logging\n",
|
| 1192 |
+
"\n",
|
|
|
|
| 1193 |
"import numpy as np\n",
|
| 1194 |
"import torch\n",
|
| 1195 |
"import torch.nn as nn\n",
|
| 1196 |
"from monai.metrics import Cumulative, CumulativeAverage\n",
|
| 1197 |
"from sklearn.metrics import cohen_kappa_score\n",
|
| 1198 |
+
"from tqdm import tqdm\n",
|
| 1199 |
+
"\n",
|
| 1200 |
"valid_loader = get_dataloader(args, split=args.mode)\n",
|
| 1201 |
"\n",
|
| 1202 |
"criterion = nn.CrossEntropyLoss()\n",
|
|
|
|
| 1221 |
" with torch.amp.autocast(device_type=str(args.device), enabled=args.amp):\n",
|
| 1222 |
" logits = model(data)\n",
|
| 1223 |
"\n",
|
|
|
|
| 1224 |
" data = data.to(\"cpu\")\n",
|
| 1225 |
" target = target.to(\"cpu\")\n",
|
| 1226 |
" logits = logits.to(\"cpu\")\n",
|
|
|
|
| 1232 |
" targets_cumulative.extend(target.detach().cpu())\n",
|
| 1233 |
" targets_cumulative_bin.extend(target_bin.detach().cpu())\n",
|
| 1234 |
"\n",
|
|
|
|
| 1235 |
" del data, target, logits\n",
|
| 1236 |
" torch.cuda.empty_cache()\n",
|
| 1237 |
"\n",
|
|
|
|
| 1242 |
"preds_updated = preds_cumulative + 2\n",
|
| 1243 |
"\n",
|
| 1244 |
"if os.path.exists(cache_dir_):\n",
|
| 1245 |
+
" shutil.rmtree(cache_dir_)"
|
| 1246 |
]
|
| 1247 |
},
|
| 1248 |
{
|
|
|
|
| 1252 |
"metadata": {},
|
| 1253 |
"outputs": [],
|
| 1254 |
"source": [
|
| 1255 |
+
"qwk = cohen_kappa_score(targets_cumulative.astype(np.float64), preds_updated.astype(np.float64))\n",
|
|
|
|
|
|
|
| 1256 |
"qwk"
|
| 1257 |
]
|
| 1258 |
},
|
|
|
|
| 1275 |
"source": [
|
| 1276 |
"import pandas as pd\n",
|
| 1277 |
"\n",
|
| 1278 |
+
"df = pd.DataFrame({\"target\": targets_cumulative, \"pred\": preds_updated})\n",
|
|
|
|
|
|
|
|
|
|
| 1279 |
"\n",
|
| 1280 |
"# Group by class and show summary stats\n",
|
| 1281 |
"\n",
|
|
|
|
| 1293 |
"outputs": [],
|
| 1294 |
"source": [
|
| 1295 |
"from sklearn.metrics import roc_auc_score\n",
|
| 1296 |
+
"\n",
|
| 1297 |
"roc_auc_score(targets_cumulative_bin, preds_cumulative)"
|
| 1298 |
]
|
| 1299 |
},
|
|
|
|
| 1306 |
"source": [
|
| 1307 |
"import pandas as pd\n",
|
| 1308 |
"\n",
|
| 1309 |
+
"df = pd.DataFrame({\"pirads\": preds_updated, \"cspca\": targets_cumulative_bin})\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1310 |
"import matplotlib.pyplot as plt\n",
|
|
|
|
| 1311 |
"import pandas as pd\n",
|
| 1312 |
+
"import seaborn as sns\n",
|
| 1313 |
"\n",
|
| 1314 |
+
"ct = pd.crosstab(df[\"pirads\"], df[\"cspca\"])\n",
|
| 1315 |
"\n",
|
| 1316 |
"# Add totals\n",
|
| 1317 |
"ct_with_totals = ct.copy()\n",
|
| 1318 |
+
"ct_with_totals[\"Total\"] = ct_with_totals.sum(axis=1) # row totals\n",
|
| 1319 |
+
"ct_with_totals.loc[\"Total\"] = ct_with_totals.sum(axis=0) # column totals\n",
|
|
|
|
|
|
|
| 1320 |
"import matplotlib.pyplot as plt\n",
|
| 1321 |
"import numpy as np\n",
|
| 1322 |
+
"import pandas as pd\n",
|
| 1323 |
"\n",
|
| 1324 |
"# Base crosstab\n",
|
| 1325 |
+
"ct = pd.crosstab(df[\"pirads\"], df[\"cspca\"])\n",
|
| 1326 |
"\n",
|
| 1327 |
"# Row totals\n",
|
| 1328 |
"row_totals = ct.sum(axis=1)\n",
|
|
|
|
| 1331 |
"col_totals = ct.sum(axis=0)\n",
|
| 1332 |
"\n",
|
| 1333 |
"# ---- Plot ----\n",
|
| 1334 |
+
"fig, ax = plt.subplots(figsize=(7, 5))\n",
|
| 1335 |
"\n",
|
| 1336 |
"# Heatmap ONLY for actual data\n",
|
| 1337 |
+
"sns.heatmap(ct, annot=True, fmt=\"d\", cmap=\"Blues\", cbar=False, ax=ax)\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1338 |
"\n",
|
| 1339 |
"# ---- Add row totals (right side, no color) ----\n",
|
| 1340 |
"for i, val in enumerate(row_totals):\n",
|
| 1341 |
" ax.text(\n",
|
| 1342 |
+
" ct.shape[1] + 0.3, # position to the right\n",
|
| 1343 |
" i + 0.5,\n",
|
| 1344 |
" str(val),\n",
|
| 1345 |
+
" va=\"center\",\n",
|
| 1346 |
" )\n",
|
| 1347 |
"\n",
|
| 1348 |
"# Label for row totals\n",
|
| 1349 |
+
"ax.text(ct.shape[1] + 0.3, -0.3, \"Total\", ha=\"center\")\n",
|
| 1350 |
"\n",
|
| 1351 |
"# ---- Add column totals (below x-axis) ----\n",
|
| 1352 |
"for j, val in enumerate(col_totals):\n",
|
| 1353 |
" ax.text(\n",
|
| 1354 |
" j + 0.5,\n",
|
| 1355 |
+
" ct.shape[0] + 0.3, # position below\n",
|
| 1356 |
" str(val),\n",
|
| 1357 |
+
" ha=\"center\",\n",
|
| 1358 |
" )\n",
|
| 1359 |
"\n",
|
| 1360 |
"# Label for column totals\n",
|
| 1361 |
+
"ax.text(-0.5, ct.shape[0] + 0.3, \"Total\", va=\"center\")\n",
|
| 1362 |
"\n",
|
| 1363 |
"# ---- Adjust limits so text is visible ----\n",
|
| 1364 |
"ax.set_xlim(0, ct.shape[1] + 1)\n",
|
| 1365 |
"ax.set_ylim(ct.shape[0] + 1, 0)\n",
|
| 1366 |
"\n",
|
| 1367 |
"# Titles and labels\n",
|
| 1368 |
+
"ax.set_title(\"PI-RADS vs csPCa\")\n",
|
| 1369 |
+
"ax.set_xlabel(\"csPCa\")\n",
|
| 1370 |
+
"ax.set_ylabel(\"PI-RADS\")\n",
|
| 1371 |
"\n",
|
| 1372 |
"plt.tight_layout()\n",
|
| 1373 |
"plt.show()"
|
|
|
|
| 1380 |
"metadata": {},
|
| 1381 |
"outputs": [],
|
| 1382 |
"source": [
|
| 1383 |
+
"pirads_pred = [1.0 if i >= 3 else 0.0 for i in preds_updated]\n",
|
| 1384 |
"pirads_pred\n",
|
| 1385 |
+
"import numpy as np\n",
|
| 1386 |
"from sklearn.metrics import (\n",
|
| 1387 |
" accuracy_score,\n",
|
|
|
|
|
|
|
|
|
|
| 1388 |
" balanced_accuracy_score,\n",
|
| 1389 |
+
" classification_report,\n",
|
| 1390 |
" confusion_matrix,\n",
|
| 1391 |
+
" f1_score,\n",
|
| 1392 |
+
" precision_score,\n",
|
| 1393 |
+
" recall_score,\n",
|
| 1394 |
")\n",
|
| 1395 |
+
"\n",
|
| 1396 |
"pred_cat = np.array(pirads_pred)\n",
|
| 1397 |
"target_list = targets_cumulative_bin\n",
|
| 1398 |
"# Basic metrics\n",
|
|
|
|
| 1440 |
"metadata": {},
|
| 1441 |
"outputs": [],
|
| 1442 |
"source": [
|
| 1443 |
+
"import json\n",
|
| 1444 |
"import logging\n",
|
| 1445 |
"import os\n",
|
| 1446 |
"import shutil\n",
|
|
|
|
| 1453 |
"import wandb\n",
|
| 1454 |
"import yaml\n",
|
| 1455 |
"from monai.utils import set_determinism\n",
|
|
|
|
| 1456 |
"from sklearn.preprocessing import StandardScaler\n",
|
|
|
|
| 1457 |
"\n",
|
| 1458 |
"from src.data.data_loader import get_dataloader\n",
|
| 1459 |
"from src.model.mil import MILModel3D\n",
|
| 1460 |
+
"from src.train.train_pirads import val_epoch\n",
|
| 1461 |
+
"from src.utils import setup_logging"
|
| 1462 |
]
|
| 1463 |
},
|
| 1464 |
{
|
|
|
|
| 1478 |
"source": [
|
| 1479 |
"from types import SimpleNamespace\n",
|
| 1480 |
"\n",
|
| 1481 |
+
"with open(\"config/config_pirads_train.yaml\") as f:\n",
|
| 1482 |
" config = yaml.safe_load(f)\n",
|
| 1483 |
"\n",
|
| 1484 |
"\n",
|
| 1485 |
"args = SimpleNamespace(**config)\n",
|
| 1486 |
"print(args.data_root)\n",
|
| 1487 |
"\n",
|
| 1488 |
+
"args.mode = \"rain\"\n",
|
| 1489 |
"args.project_dir = Path.cwd()\n",
|
| 1490 |
+
"args.run_name = \"test_dummy_pirads\"\n",
|
| 1491 |
"args.checkpoint = None\n",
|
| 1492 |
"args.wandb = False\n",
|
| 1493 |
"args.optim_lr = 2e-5\n",
|
| 1494 |
"args.batch_size = 8\n",
|
| 1495 |
"args.epochs = 1\n",
|
| 1496 |
+
"args.project_name = \"Dummy\"\n",
|
| 1497 |
"args.workers = 0\n",
|
| 1498 |
"args.use_psa = False"
|
| 1499 |
]
|
|
|
|
| 1514 |
}
|
| 1515 |
],
|
| 1516 |
"source": [
|
|
|
|
|
|
|
| 1517 |
"args.logdir = os.path.join(args.project_dir, \"logs\", args.run_name)\n",
|
| 1518 |
"os.makedirs(args.logdir, exist_ok=True)\n",
|
| 1519 |
"args.logfile = os.path.join(args.logdir, f\"{args.run_name}.log\")\n",
|
|
|
|
| 1549 |
" dir=os.path.join(args.logdir, \"wandb\"),\n",
|
| 1550 |
" config=config_wandb,\n",
|
| 1551 |
" mode=mode_wandb,\n",
|
| 1552 |
+
")"
|
| 1553 |
]
|
| 1554 |
},
|
| 1555 |
{
|
|
|
|
| 1586 |
"scaler = StandardScaler()\n",
|
| 1587 |
"with open(os.path.join(args.project_dir, \"dataset\", \"PICAI_cspca_updated_with_psa.json\")) as f:\n",
|
| 1588 |
" dataset_json = json.load(f)\n",
|
| 1589 |
+
"train_clinical = [i[\"psa\"] for i in dataset_json[\"train\"]]\n",
|
| 1590 |
"_ = scaler.fit_transform(train_clinical)\n",
|
| 1591 |
"args.psa_mean = scaler.mean_.tolist()\n",
|
| 1592 |
"args.psa_std = scaler.scale_.tolist()"
|
|
|
|
| 1601 |
"source": [
|
| 1602 |
"train_loader = get_dataloader(args, split=\"train\")\n",
|
| 1603 |
"valid_loader = get_dataloader(args, split=\"test\")\n",
|
| 1604 |
+
"logging.info(f\"Dataset training: {len(train_loader.dataset)}, test: {len(valid_loader.dataset)}\")\n",
|
|
|
|
|
|
|
| 1605 |
"\n",
|
| 1606 |
"if args.mil_mode in [\"att_trans\", \"att_trans_pyramid\"]:\n",
|
| 1607 |
" params = [\n",
|
|
|
|
| 1609 |
" \"params\": list(model.attention.parameters())\n",
|
| 1610 |
" + list(model.myfc.parameters())\n",
|
| 1611 |
" + list(model.net.parameters())\n",
|
| 1612 |
+
" # + list(model.feature_proj.parameters()),\n",
|
| 1613 |
" },\n",
|
| 1614 |
" {\"params\": list(model.transformer.parameters()), \"lr\": 6e-5, \"weight_decay\": 0.1},\n",
|
| 1615 |
" ]\n",
|
| 1616 |
"\n",
|
| 1617 |
"optimizer = torch.optim.AdamW(params, lr=args.optim_lr, weight_decay=args.weight_decay)\n",
|
| 1618 |
+
"scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs, eta_min=0)\n",
|
|
|
|
|
|
|
| 1619 |
"scaler = torch.amp.GradScaler(device=str(args.device), enabled=args.amp)"
|
| 1620 |
]
|
| 1621 |
},
|
|
|
|
| 1626 |
"metadata": {},
|
| 1627 |
"outputs": [],
|
| 1628 |
"source": [
|
|
|
|
| 1629 |
"import logging\n",
|
| 1630 |
"import time\n",
|
| 1631 |
"\n",
|
|
|
|
| 1659 |
" # Classification Loss\n",
|
| 1660 |
" logits_attn = model(shuffled_images, no_head=True)\n",
|
| 1661 |
" x = logits_attn.to(torch.float32)\n",
|
| 1662 |
+
" # x = x.permute(1, 0, 2)\n",
|
| 1663 |
+
" # x = model.feature_proj(x)\n",
|
| 1664 |
" x = model.transformer(x)\n",
|
| 1665 |
+
" # x = x.permute(1, 0, 2)\n",
|
| 1666 |
" a = model.attention(x)\n",
|
| 1667 |
" a = torch.softmax(a, dim=1)\n",
|
| 1668 |
" x = torch.sum(x * a, dim=1)\n",
|
|
|
|
| 1905 |
}
|
| 1906 |
],
|
| 1907 |
"source": [
|
| 1908 |
+
"(pred_pirads == target.sum(dim=1)).sum() / len(pred_pirads)"
|
| 1909 |
]
|
| 1910 |
},
|
| 1911 |
{
|
|
|
|
| 1965 |
"outputs": [],
|
| 1966 |
"source": []
|
| 1967 |
},
|
| 1968 |
+
{
|
| 1969 |
+
"cell_type": "markdown",
|
| 1970 |
+
"id": "fb57c919",
|
| 1971 |
+
"metadata": {},
|
| 1972 |
+
"source": [
|
| 1973 |
+
"### Run Inference Amend"
|
| 1974 |
+
]
|
| 1975 |
+
},
|
| 1976 |
{
|
| 1977 |
"cell_type": "code",
|
| 1978 |
+
"execution_count": 2,
|
| 1979 |
"id": "df58821b",
|
| 1980 |
"metadata": {},
|
| 1981 |
+
"outputs": [
|
| 1982 |
+
{
|
| 1983 |
+
"name": "stdout",
|
| 1984 |
+
"output_type": "stream",
|
| 1985 |
+
"text": [
|
| 1986 |
+
"If you have questions or suggestions, feel free to open an issue at https://github.com/DIAGNijmegen/picai_prep\n",
|
| 1987 |
+
"\n"
|
| 1988 |
+
]
|
| 1989 |
+
}
|
| 1990 |
+
],
|
| 1991 |
+
"source": [
|
| 1992 |
+
"import argparse\n",
|
| 1993 |
+
"import json\n",
|
| 1994 |
+
"import logging\n",
|
| 1995 |
+
"import os\n",
|
| 1996 |
+
"from pathlib import Path\n",
|
| 1997 |
+
"\n",
|
| 1998 |
+
"import streamlit as st\n",
|
| 1999 |
+
"import torch\n",
|
| 2000 |
+
"import yaml\n",
|
| 2001 |
+
"from monai.data import Dataset\n",
|
| 2002 |
+
"\n",
|
| 2003 |
+
"from src.data.data_loader import data_transform, list_data_collate\n",
|
| 2004 |
+
"from src.model.cspca_model import CSPCAModel\n",
|
| 2005 |
+
"from src.model.mil import MILModel3D\n",
|
| 2006 |
+
"from src.preprocessing.clip_intensity import clip_adc\n",
|
| 2007 |
+
"from src.preprocessing.generate_heatmap import get_heatmap\n",
|
| 2008 |
+
"from src.preprocessing.prostate_mask import get_segmask\n",
|
| 2009 |
+
"from src.preprocessing.register_and_crop import register_files\n",
|
| 2010 |
+
"from src.utils import get_parent_image, get_patch_coordinate, setup_logging, get_prostate_volume\n",
|
| 2011 |
+
"from sklearn.preprocessing import StandardScaler\n",
|
| 2012 |
+
"\n",
|
| 2013 |
+
"from argparse import Namespace\n",
|
| 2014 |
+
"from collections.abc import Callable"
|
| 2015 |
+
]
|
| 2016 |
+
},
|
| 2017 |
+
{
|
| 2018 |
+
"cell_type": "code",
|
| 2019 |
+
"execution_count": null,
|
| 2020 |
+
"id": "01f61063",
|
| 2021 |
+
"metadata": {},
|
| 2022 |
+
"outputs": [
|
| 2023 |
+
{
|
| 2024 |
+
"ename": "SyntaxError",
|
| 2025 |
+
"evalue": "invalid syntax (2184847560.py, line 1)",
|
| 2026 |
+
"output_type": "error",
|
| 2027 |
+
"traceback": [
|
| 2028 |
+
"\u001b[0;36m Cell \u001b[0;32mIn[22], line 1\u001b[0;36m\u001b[0m\n\u001b[0;31m mask_patches.shapefrom argparse import Namespace\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n"
|
| 2029 |
+
]
|
| 2030 |
+
}
|
| 2031 |
+
],
|
| 2032 |
+
"source": [
|
| 2033 |
+
"from argparse import Namespace\n",
|
| 2034 |
+
"\n",
|
| 2035 |
+
"args = Namespace(\n",
|
| 2036 |
+
" config=\"config.yaml\",\n",
|
| 2037 |
+
" json_path=\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/psa_val.json\", \n",
|
| 2038 |
+
" t2_dir=\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/t2\",\n",
|
| 2039 |
+
" dwi_dir=\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/dwi\",\n",
|
| 2040 |
+
" adc_dir=\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/adc\",\n",
|
| 2041 |
+
" seg_dir=\"/path/to/seg\",\n",
|
| 2042 |
+
" output_dir=\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed\",\n",
|
| 2043 |
+
" margin=0.2,\n",
|
| 2044 |
+
" num_classes=4,\n",
|
| 2045 |
+
" mil_mode=\"att_trans\",\n",
|
| 2046 |
+
" use_heatmap=True,\n",
|
| 2047 |
+
" use_psa=True,\n",
|
| 2048 |
+
" tile_size=48,\n",
|
| 2049 |
+
" tile_count=40,\n",
|
| 2050 |
+
" depth=3,\n",
|
| 2051 |
+
" project_dir=\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate\",\n",
|
| 2052 |
+
")"
|
| 2053 |
+
]
|
| 2054 |
+
},
|
| 2055 |
+
{
|
| 2056 |
+
"cell_type": "code",
|
| 2057 |
+
"execution_count": 8,
|
| 2058 |
+
"id": "e0117759",
|
| 2059 |
+
"metadata": {},
|
| 2060 |
+
"outputs": [
|
| 2061 |
+
{
|
| 2062 |
+
"name": "stderr",
|
| 2063 |
+
"output_type": "stream",
|
| 2064 |
+
"text": [
|
| 2065 |
+
"100%|██████████| 1/1 [00:02<00:00, 2.79s/it]\n",
|
| 2066 |
+
"You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.\n",
|
| 2067 |
+
"100%|██████████| 1/1 [00:07<00:00, 7.29s/it]\n",
|
| 2068 |
+
"100%|██████████| 1/1 [00:00<00:00, 10.55it/s]\n",
|
| 2069 |
+
"100%|██████████| 1/1 [00:00<00:00, 4.04it/s]\n"
|
| 2070 |
+
]
|
| 2071 |
+
}
|
| 2072 |
+
],
|
| 2073 |
+
"source": [
|
| 2074 |
+
"FUNCTIONS: dict[str, Callable[[Namespace], Namespace]] = {\n",
|
| 2075 |
+
" \"register_and_crop\": register_files,\n",
|
| 2076 |
+
" \"clip_adc\": clip_adc,\n",
|
| 2077 |
+
" \"get_segmentation_mask\": get_segmask,\n",
|
| 2078 |
+
" \"get_heatmap\": get_heatmap,\n",
|
| 2079 |
+
"}\n",
|
| 2080 |
+
"args.logfile = os.path.join(args.output_dir, \"inference.log\")\n",
|
| 2081 |
+
"setup_logging(args.logfile)\n",
|
| 2082 |
+
"logging.info(\"Starting preprocessing\")\n",
|
| 2083 |
+
"steps = [\"register_and_crop\", \"get_segmentation_mask\", \"clip_adc\", \"get_heatmap\"]\n",
|
| 2084 |
+
"for step in steps:\n",
|
| 2085 |
+
" #if step == 'get_segmentation_mask':\n",
|
| 2086 |
+
" # args.seg_dir = os.path.join(args.output_dir, \"prostate_mask\")\n",
|
| 2087 |
+
" # continue\n",
|
| 2088 |
+
" func = FUNCTIONS[step]\n",
|
| 2089 |
+
" \n",
|
| 2090 |
+
" args = func(args)\n",
|
| 2091 |
+
"\n",
|
| 2092 |
+
"logging.info(\"Preprocessing completed.\")\n",
|
| 2093 |
+
"args.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n"
|
| 2094 |
+
]
|
| 2095 |
+
},
|
| 2096 |
+
{
|
| 2097 |
+
"cell_type": "code",
|
| 2098 |
+
"execution_count": 9,
|
| 2099 |
+
"id": "29a500c4",
|
| 2100 |
+
"metadata": {},
|
| 2101 |
+
"outputs": [
|
| 2102 |
+
{
|
| 2103 |
+
"name": "stdout",
|
| 2104 |
+
"output_type": "stream",
|
| 2105 |
+
"text": [
|
| 2106 |
+
"config: config.yaml\n",
|
| 2107 |
+
"json_path: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/psa_val.json\n",
|
| 2108 |
+
"t2_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed/t2_registered\n",
|
| 2109 |
+
"dwi_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed/DWI_registered\n",
|
| 2110 |
+
"adc_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed/ADC_clipped\n",
|
| 2111 |
+
"seg_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed/prostate_mask\n",
|
| 2112 |
+
"output_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed\n",
|
| 2113 |
+
"margin: 0.2\n",
|
| 2114 |
+
"num_classes: 4\n",
|
| 2115 |
+
"mil_mode: att_trans\n",
|
| 2116 |
+
"use_heatmap: True\n",
|
| 2117 |
+
"use_psa: True\n",
|
| 2118 |
+
"tile_size: 48\n",
|
| 2119 |
+
"tile_count: 40\n",
|
| 2120 |
+
"depth: 3\n",
|
| 2121 |
+
"project_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate\n",
|
| 2122 |
+
"logfile: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed/inference.log\n",
|
| 2123 |
+
"heatmapdir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed/heatmaps/\n",
|
| 2124 |
+
"smooth_seg_dir_temp: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed/smooth_prostate_mask_temp/\n",
|
| 2125 |
+
"smooth_seg_dir: /sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/datatemp/processed/smooth_prostate_mask/\n",
|
| 2126 |
+
"device: cpu\n"
|
| 2127 |
+
]
|
| 2128 |
+
}
|
| 2129 |
+
],
|
| 2130 |
+
"source": [
|
| 2131 |
+
"for key, value in vars(args).items():\n",
|
| 2132 |
+
" print(f\"{key}: {value}\")"
|
| 2133 |
+
]
|
| 2134 |
+
},
|
| 2135 |
+
{
|
| 2136 |
+
"cell_type": "code",
|
| 2137 |
+
"execution_count": 10,
|
| 2138 |
+
"id": "cf6aff0e",
|
| 2139 |
+
"metadata": {},
|
| 2140 |
+
"outputs": [],
|
| 2141 |
+
"source": [
|
| 2142 |
+
"scaler = StandardScaler()\n",
|
| 2143 |
+
"with open(os.path.join(args.project_dir, \"dataset\", \"PICAI_cspca_updated_with_psa.json\")) as f:\n",
|
| 2144 |
+
" dataset_json = json.load(f)\n",
|
| 2145 |
+
"train_clinical = [i[\"psa\"] for i in dataset_json[\"train\"]]\n",
|
| 2146 |
+
"_ = scaler.fit_transform(train_clinical)\n",
|
| 2147 |
+
"args.psa_mean = scaler.mean_.tolist()\n",
|
| 2148 |
+
"args.psa_std = scaler.scale_.tolist()"
|
| 2149 |
+
]
|
| 2150 |
+
},
|
| 2151 |
+
{
|
| 2152 |
+
"cell_type": "code",
|
| 2153 |
+
"execution_count": 11,
|
| 2154 |
+
"id": "ca8c1554",
|
| 2155 |
+
"metadata": {},
|
| 2156 |
+
"outputs": [],
|
| 2157 |
+
"source": [
|
| 2158 |
+
"transform = data_transform(args, split='test')\n",
|
| 2159 |
+
"files = os.listdir(args.t2_dir)\n",
|
| 2160 |
+
"args.data_list = []\n",
|
| 2161 |
+
"with open(args.json_path, 'r') as f:\n",
|
| 2162 |
+
" psa_data = json.load(f)"
|
| 2163 |
+
]
|
| 2164 |
+
},
|
| 2165 |
+
{
|
| 2166 |
+
"cell_type": "code",
|
| 2167 |
+
"execution_count": 13,
|
| 2168 |
+
"id": "b4398c65",
|
| 2169 |
+
"metadata": {},
|
| 2170 |
+
"outputs": [],
|
| 2171 |
+
"source": [
|
| 2172 |
+
"for file in files:\n",
|
| 2173 |
+
" temp = {}\n",
|
| 2174 |
+
" temp[\"image\"] = os.path.join(args.t2_dir, file)\n",
|
| 2175 |
+
" temp[\"dwi\"] = os.path.join(args.dwi_dir, file)\n",
|
| 2176 |
+
" temp[\"adc\"] = os.path.join(args.adc_dir, file)\n",
|
| 2177 |
+
" temp[\"heatmap\"] = os.path.join(args.heatmapdir, file)\n",
|
| 2178 |
+
" temp[\"mask\"] = os.path.join(args.seg_dir, file)\n",
|
| 2179 |
+
" temp[\"label\"] = 0 # dummy label\n",
|
| 2180 |
+
" temp[\"smooth_mask\"] = os.path.join(args.smooth_seg_dir, file)\n",
|
| 2181 |
+
" prostate_vol = get_prostate_volume(temp[\"mask\"])\n",
|
| 2182 |
+
" temp[\"psa\"] = [psa_data[file.split('.nrrd')[0]] , prostate_vol]\n",
|
| 2183 |
+
" args.data_list.append(temp)"
|
| 2184 |
+
]
|
| 2185 |
+
},
|
| 2186 |
+
{
|
| 2187 |
+
"cell_type": "code",
|
| 2188 |
+
"execution_count": 15,
|
| 2189 |
+
"id": "d2b5fa61",
|
| 2190 |
+
"metadata": {},
|
| 2191 |
+
"outputs": [],
|
| 2192 |
+
"source": [
|
| 2193 |
+
"\n",
|
| 2194 |
+
"dataset = Dataset(data=args.data_list, transform=transform)\n",
|
| 2195 |
+
"loader = torch.utils.data.DataLoader(\n",
|
| 2196 |
+
" dataset,\n",
|
| 2197 |
+
" batch_size=1,\n",
|
| 2198 |
+
" shuffle=False,\n",
|
| 2199 |
+
" num_workers=0,\n",
|
| 2200 |
+
" pin_memory=True,\n",
|
| 2201 |
+
" multiprocessing_context=None,\n",
|
| 2202 |
+
" sampler=None,\n",
|
| 2203 |
+
" collate_fn=list_data_collate,\n",
|
| 2204 |
+
")"
|
| 2205 |
+
]
|
| 2206 |
+
},
|
| 2207 |
+
{
|
| 2208 |
+
"cell_type": "code",
|
| 2209 |
+
"execution_count": null,
|
| 2210 |
+
"id": "824263f5",
|
| 2211 |
+
"metadata": {},
|
| 2212 |
"outputs": [],
|
| 2213 |
"source": []
|
| 2214 |
}
|
temp_2.ipynb
CHANGED
|
@@ -7,10 +7,11 @@
|
|
| 7 |
"metadata": {},
|
| 8 |
"outputs": [],
|
| 9 |
"source": [
|
| 10 |
-
"import pandas as pd\n",
|
| 11 |
-
"import numpy as np\n",
|
| 12 |
"import json\n",
|
| 13 |
-
"import os"
|
|
|
|
|
|
|
|
|
|
| 14 |
]
|
| 15 |
},
|
| 16 |
{
|
|
@@ -20,10 +21,12 @@
|
|
| 20 |
"metadata": {},
|
| 21 |
"outputs": [],
|
| 22 |
"source": [
|
| 23 |
-
"with open(
|
|
|
|
|
|
|
| 24 |
" data = json.load(f)\n",
|
| 25 |
-
"
|
| 26 |
-
"neg_label = [i for i in data[
|
| 27 |
]
|
| 28 |
},
|
| 29 |
{
|
|
@@ -34,8 +37,8 @@
|
|
| 34 |
"outputs": [],
|
| 35 |
"source": [
|
| 36 |
"print(len(neg_label))\n",
|
| 37 |
-
"pirads_label = np.array([i[
|
| 38 |
-
"print(np.unique(pirads_label, return_counts=True))
|
| 39 |
]
|
| 40 |
},
|
| 41 |
{
|
|
@@ -53,7 +56,9 @@
|
|
| 53 |
"metadata": {},
|
| 54 |
"outputs": [],
|
| 55 |
"source": [
|
| 56 |
-
"data = pd.read_csv(\
|
|
|
|
|
|
|
| 57 |
"data.head()"
|
| 58 |
]
|
| 59 |
},
|
|
@@ -64,40 +69,45 @@
|
|
| 64 |
"metadata": {},
|
| 65 |
"outputs": [],
|
| 66 |
"source": [
|
| 67 |
-
"with open(
|
|
|
|
|
|
|
| 68 |
" json_data = json.load(f)\n",
|
| 69 |
"\n",
|
| 70 |
-
"new_train =[]
|
| 71 |
-
"for i in json_data[
|
| 72 |
-
" p_id = i[
|
| 73 |
-
" st_id = i[
|
| 74 |
" row = data[(data[\"patient_id\"] == int(p_id)) & (data[\"study_id\"] == int(st_id))]\n",
|
| 75 |
-
" pvol = row[
|
| 76 |
-
" psa = row[
|
| 77 |
" if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
|
| 78 |
-
" i[
|
| 79 |
" new_train.append(i)\n",
|
| 80 |
"\n",
|
| 81 |
"print(len(new_train))\n",
|
| 82 |
-
"print(len(json_data[
|
| 83 |
"\n",
|
| 84 |
-
"new_test =[]
|
| 85 |
-
"for i in json_data[
|
| 86 |
-
" p_id = i[
|
| 87 |
-
" st_id = i[
|
| 88 |
" row = data[(data[\"patient_id\"] == int(p_id)) & (data[\"study_id\"] == int(st_id))]\n",
|
| 89 |
-
" pvol = row[
|
| 90 |
-
" psa = row[
|
| 91 |
" if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
|
| 92 |
-
" i[
|
| 93 |
" new_test.append(i)\n",
|
| 94 |
"\n",
|
| 95 |
"print(len(new_test))\n",
|
| 96 |
-
"print(len(json_data[
|
| 97 |
"\n",
|
| 98 |
-
"json_data[
|
| 99 |
-
"json_data[
|
| 100 |
-
"with open(
|
|
|
|
|
|
|
|
|
|
| 101 |
" json.dump(json_data, f, indent=4)"
|
| 102 |
]
|
| 103 |
},
|
|
@@ -116,7 +126,9 @@
|
|
| 116 |
"metadata": {},
|
| 117 |
"outputs": [],
|
| 118 |
"source": [
|
| 119 |
-
"test_df = pd.read_excel(
|
|
|
|
|
|
|
| 120 |
"test_df.head()"
|
| 121 |
]
|
| 122 |
},
|
|
@@ -153,28 +165,33 @@
|
|
| 153 |
"metadata": {},
|
| 154 |
"outputs": [],
|
| 155 |
"source": [
|
| 156 |
-
"with open(
|
|
|
|
|
|
|
| 157 |
" json_data = json.load(f)\n",
|
| 158 |
"\n",
|
| 159 |
"\n",
|
| 160 |
-
"new_test =[]
|
| 161 |
-
"for i in json_data[
|
| 162 |
-
" p_id = i[
|
| 163 |
-
"
|
| 164 |
-
" row = test_df[(test_df[\"PATIENT_ID\"] == int(p_id))
|
| 165 |
"\n",
|
| 166 |
-
" pvol = row[
|
| 167 |
-
" psa = row[
|
| 168 |
" if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
|
| 169 |
-
" i[
|
| 170 |
" new_test.append(i)\n",
|
| 171 |
"\n",
|
| 172 |
"print(len(new_test))\n",
|
| 173 |
-
"print(len(json_data[
|
| 174 |
"\n",
|
| 175 |
"\n",
|
| 176 |
-
"json_data[
|
| 177 |
-
"with open(
|
|
|
|
|
|
|
|
|
|
| 178 |
" json.dump(json_data, f, indent=4)"
|
| 179 |
]
|
| 180 |
},
|
|
@@ -209,25 +226,20 @@
|
|
| 209 |
"metadata": {},
|
| 210 |
"outputs": [],
|
| 211 |
"source": [
|
| 212 |
-
"import argparse\n",
|
| 213 |
-
"import logging\n",
|
| 214 |
-
"import os\n",
|
| 215 |
-
"import shutil\n",
|
| 216 |
"import sys\n",
|
| 217 |
"from pathlib import Path\n",
|
| 218 |
"\n",
|
| 219 |
"import torch\n",
|
| 220 |
"import yaml\n",
|
| 221 |
-
"from monai.utils import set_determinism\n",
|
| 222 |
"\n",
|
| 223 |
"from src.data.data_loader import get_dataloader\n",
|
| 224 |
-
"
|
|
|
|
| 225 |
"from src.model.mil import MILModel3D\n",
|
| 226 |
-
"from src.train.train_cspca import train_epoch, val_epoch\n",
|
| 227 |
-
"from src.utils import get_metrics, save_cspca_checkpoint, setup_logging\n",
|
| 228 |
-
"import yaml\n",
|
| 229 |
"\n",
|
| 230 |
-
"with open(\
|
|
|
|
|
|
|
| 231 |
" cfg = yaml.safe_load(f)\n",
|
| 232 |
"from argparse import Namespace\n",
|
| 233 |
"\n",
|
|
@@ -241,8 +253,10 @@
|
|
| 241 |
"metadata": {},
|
| 242 |
"outputs": [],
|
| 243 |
"source": [
|
| 244 |
-
"args.mode =
|
| 245 |
-
"args.project_dir = \
|
|
|
|
|
|
|
| 246 |
"args.checkpoint_cspca = None\n",
|
| 247 |
"\n",
|
| 248 |
"args.run_name = \"check_dummy\"\n",
|
|
@@ -263,7 +277,7 @@
|
|
| 263 |
"\n",
|
| 264 |
"args.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 265 |
"if args.device == torch.device(\"cuda\"):\n",
|
| 266 |
-
" torch.backends.cudnn.benchmark = True
|
| 267 |
]
|
| 268 |
},
|
| 269 |
{
|
|
@@ -274,8 +288,10 @@
|
|
| 274 |
"outputs": [],
|
| 275 |
"source": [
|
| 276 |
"import json\n",
|
|
|
|
| 277 |
"from sklearn.preprocessing import StandardScaler\n",
|
| 278 |
-
"
|
|
|
|
| 279 |
"args.mil_mode = \"att_trans\"\n",
|
| 280 |
"mil_model = MILModel3D(num_classes=args.num_classes, mil_mode=args.mil_mode)\n",
|
| 281 |
"cache_dir_path = Path(os.path.join(args.logdir, \"cache\"))\n",
|
|
@@ -283,7 +299,7 @@
|
|
| 283 |
"scaler = StandardScaler()\n",
|
| 284 |
"with open(os.path.join(args.project_dir, \"dataset\", \"PICAI_cspca_updated_with_psa.json\")) as f:\n",
|
| 285 |
" dataset_json = json.load(f)\n",
|
| 286 |
-
"train_clinical = [i[
|
| 287 |
"_ = scaler.fit_transform(train_clinical)\n",
|
| 288 |
"args.psa_mean = scaler.mean_.tolist()\n",
|
| 289 |
"args.psa_std = scaler.scale_.tolist()"
|
|
@@ -369,17 +385,17 @@
|
|
| 369 |
" def __init__(self, backbone: nn.Module) -> None:\n",
|
| 370 |
" super().__init__()\n",
|
| 371 |
" self.backbone = backbone\n",
|
| 372 |
-
"
|
| 373 |
" self.clinical_dim = 2\n",
|
| 374 |
" self.projection_dim = 16\n",
|
| 375 |
" self.clinical_projection = nn.Sequential(\n",
|
| 376 |
" nn.Linear(self.clinical_dim, self.projection_dim),\n",
|
| 377 |
" nn.ReLU(),\n",
|
| 378 |
-
" nn.BatchNorm1d(self.projection_dim)
|
| 379 |
" )\n",
|
| 380 |
-
"
|
| 381 |
" self.fc_dim = backbone.myfc.in_features\n",
|
| 382 |
-
" self.fc_cspca = SimpleNN(input_dim=self.fc_dim + self.projection_dim)
|
| 383 |
"\n",
|
| 384 |
" def forward(self, x, psa_data):\n",
|
| 385 |
" sh = x.shape\n",
|
|
@@ -392,12 +408,12 @@
|
|
| 392 |
" a = self.backbone.attention(x)\n",
|
| 393 |
" a = torch.softmax(a, dim=1)\n",
|
| 394 |
" x = torch.sum(x * a, dim=1)\n",
|
| 395 |
-
"
|
| 396 |
" psa_features = self.clinical_projection(psa_data)\n",
|
| 397 |
" x = torch.cat((x, psa_features), dim=1)\n",
|
| 398 |
"\n",
|
| 399 |
" x = self.fc_cspca(x)\n",
|
| 400 |
-
" return x
|
| 401 |
]
|
| 402 |
},
|
| 403 |
{
|
|
@@ -407,7 +423,6 @@
|
|
| 407 |
"metadata": {},
|
| 408 |
"outputs": [],
|
| 409 |
"source": [
|
| 410 |
-
"\n",
|
| 411 |
"args.use_psa = True\n",
|
| 412 |
"checkpoint = torch.load(args.checkpoint_pirads, weights_only=False, map_location=\"cpu\")\n",
|
| 413 |
"mil_model.load_state_dict(checkpoint[\"state_dict\"])\n",
|
|
@@ -448,7 +463,7 @@
|
|
| 448 |
"import torch\n",
|
| 449 |
"import torch.nn as nn\n",
|
| 450 |
"from monai.metrics import Cumulative, CumulativeAverage\n",
|
| 451 |
-
"
|
| 452 |
"old_loss = float(\"inf\")\n",
|
| 453 |
"epoch = 0\n",
|
| 454 |
"cspca_model.train()\n",
|
|
@@ -470,7 +485,7 @@
|
|
| 470 |
" data = batch_data[\"image\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 471 |
" target = batch_data[\"label\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 472 |
" psa_data = batch_data[\"psa\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 473 |
-
" break
|
| 474 |
]
|
| 475 |
},
|
| 476 |
{
|
|
@@ -525,8 +540,6 @@
|
|
| 525 |
"source": [
|
| 526 |
"import re\n",
|
| 527 |
"\n",
|
| 528 |
-
"from collections import defaultdict\n",
|
| 529 |
-
"\n",
|
| 530 |
"train_loss_dict = {}\n",
|
| 531 |
"val_loss_dict = {}\n",
|
| 532 |
"train_auc_dict = {}\n",
|
|
@@ -540,7 +553,9 @@
|
|
| 540 |
"metadata": {},
|
| 541 |
"outputs": [],
|
| 542 |
"source": [
|
| 543 |
-
"with open(\
|
|
|
|
|
|
|
| 544 |
" for line in f:\n",
|
| 545 |
" m = re.search(\n",
|
| 546 |
" r\"EPOCH (\\d+) TRAIN loss: ([0-9.]+) TRAIN ATTN LOSS: ([0-9.]+) TRAIN AUC: ([0-9.]+)\",\n",
|
|
@@ -549,7 +564,7 @@
|
|
| 549 |
" if m:\n",
|
| 550 |
" e = int(m.group(1))\n",
|
| 551 |
" train_loss_dict[e] = float(m.group(2))\n",
|
| 552 |
-
" #train_attn_loss_dict[e] = float(m.group(3))\n",
|
| 553 |
" train_auc_dict[e] = float(m.group(4))\n",
|
| 554 |
"\n",
|
| 555 |
" m = re.search(\n",
|
|
@@ -569,7 +584,9 @@
|
|
| 569 |
"metadata": {},
|
| 570 |
"outputs": [],
|
| 571 |
"source": [
|
| 572 |
-
"with open(\
|
|
|
|
|
|
|
| 573 |
" for line in f:\n",
|
| 574 |
" m = re.search(r\"EPOCH (\\d+) TRAIN loss: ([0-9.]+) AUC: ([0-9.]+)\", line)\n",
|
| 575 |
" if m:\n",
|
|
@@ -615,9 +632,9 @@
|
|
| 615 |
"epochs = sorted(train_loss_dict.keys())\n",
|
| 616 |
"\n",
|
| 617 |
"train_loss = [train_loss_dict[e] for e in epochs]\n",
|
| 618 |
-
"val_loss
|
| 619 |
-
"train_auc
|
| 620 |
-
"val_auc
|
| 621 |
"\n",
|
| 622 |
"import matplotlib.pyplot as plt\n",
|
| 623 |
"\n",
|
|
@@ -659,11 +676,12 @@
|
|
| 659 |
"metadata": {},
|
| 660 |
"outputs": [],
|
| 661 |
"source": [
|
| 662 |
-
"import os\n",
|
| 663 |
"import json\n",
|
|
|
|
|
|
|
| 664 |
"import numpy as np\n",
|
| 665 |
"from AIAH_utility.viewer import BasicViewer\n",
|
| 666 |
-
"from monai.transforms import Compose,
|
| 667 |
]
|
| 668 |
},
|
| 669 |
{
|
|
@@ -673,10 +691,12 @@
|
|
| 673 |
"metadata": {},
|
| 674 |
"outputs": [],
|
| 675 |
"source": [
|
| 676 |
-
"with open(
|
|
|
|
|
|
|
| 677 |
" data = json.load(f)\n",
|
| 678 |
-
"train_data = data[
|
| 679 |
-
"t2_dir =
|
| 680 |
]
|
| 681 |
},
|
| 682 |
{
|
|
@@ -686,7 +706,7 @@
|
|
| 686 |
"metadata": {},
|
| 687 |
"outputs": [],
|
| 688 |
"source": [
|
| 689 |
-
"from monai.networks.nets import resnet
|
| 690 |
]
|
| 691 |
},
|
| 692 |
{
|
|
@@ -714,8 +734,8 @@
|
|
| 714 |
" spatial_dims=3,\n",
|
| 715 |
" n_input_channels=1,\n",
|
| 716 |
" num_classes=5,\n",
|
| 717 |
-
" norm=(
|
| 718 |
-
" pretrained=True\n",
|
| 719 |
")"
|
| 720 |
]
|
| 721 |
},
|
|
@@ -726,28 +746,32 @@
|
|
| 726 |
"metadata": {},
|
| 727 |
"outputs": [],
|
| 728 |
"source": [
|
| 729 |
-
"og_transforms = Compose(
|
| 730 |
-
"
|
| 731 |
-
"
|
| 732 |
-
"
|
| 733 |
-
"
|
| 734 |
-
"
|
| 735 |
-
"
|
| 736 |
-
"
|
| 737 |
-
"
|
| 738 |
-
"
|
| 739 |
-
"])\n",
|
| 740 |
-
"
|
| 741 |
-
"
|
| 742 |
-
"
|
| 743 |
-
"
|
| 744 |
-
"
|
| 745 |
-
"
|
| 746 |
-
"
|
| 747 |
-
"
|
| 748 |
-
"
|
| 749 |
-
"
|
| 750 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
]
|
| 752 |
},
|
| 753 |
{
|
|
@@ -757,9 +781,9 @@
|
|
| 757 |
"metadata": {},
|
| 758 |
"outputs": [],
|
| 759 |
"source": [
|
| 760 |
-
"og = og_transforms({
|
| 761 |
-
"rand = rand_transforms({
|
| 762 |
-
"BasicViewer(og[
|
| 763 |
]
|
| 764 |
},
|
| 765 |
{
|
|
@@ -769,7 +793,7 @@
|
|
| 769 |
"metadata": {},
|
| 770 |
"outputs": [],
|
| 771 |
"source": [
|
| 772 |
-
"BasicViewer(rand[
|
| 773 |
]
|
| 774 |
},
|
| 775 |
{
|
|
@@ -779,10 +803,11 @@
|
|
| 779 |
"metadata": {},
|
| 780 |
"outputs": [],
|
| 781 |
"source": [
|
| 782 |
-
"import os\n",
|
| 783 |
-
"import pandas as pd\n",
|
| 784 |
"import json\n",
|
| 785 |
-
"import
|
|
|
|
|
|
|
|
|
|
| 786 |
]
|
| 787 |
},
|
| 788 |
{
|
|
@@ -793,7 +818,9 @@
|
|
| 793 |
"outputs": [],
|
| 794 |
"source": [
|
| 795 |
"df = pd.read_csv(\"/sc-projects/sc-proj-cc06-ag-ki-radiologie/pirad_model_test_PICAI/marksheet.csv\")\n",
|
| 796 |
-
"with open(
|
|
|
|
|
|
|
| 797 |
" json_data = json.load(f)\n",
|
| 798 |
"df.head()"
|
| 799 |
]
|
|
@@ -805,10 +832,10 @@
|
|
| 805 |
"metadata": {},
|
| 806 |
"outputs": [],
|
| 807 |
"source": [
|
| 808 |
-
"i = json_data[
|
| 809 |
-
"print(i[
|
| 810 |
-
"p_id = i[
|
| 811 |
-
"st_id = i[
|
| 812 |
"row = df[(df[\"patient_id\"] == int(p_id)) & (df[\"study_id\"] == int(st_id))]\n",
|
| 813 |
"row"
|
| 814 |
]
|
|
@@ -820,9 +847,7 @@
|
|
| 820 |
"metadata": {},
|
| 821 |
"outputs": [],
|
| 822 |
"source": [
|
| 823 |
-
"import argparse\n",
|
| 824 |
"import os\n",
|
| 825 |
-
"from typing import Literal\n",
|
| 826 |
"\n",
|
| 827 |
"import numpy as np\n",
|
| 828 |
"import torch\n",
|
|
@@ -830,27 +855,16 @@
|
|
| 830 |
"from monai.transforms import (\n",
|
| 831 |
" Compose,\n",
|
| 832 |
" ConcatItemsd,\n",
|
| 833 |
-
" DeleteItemsd,\n",
|
| 834 |
-
" EnsureTyped,\n",
|
| 835 |
" LoadImaged,\n",
|
| 836 |
-
" NormalizeIntensityd,\n",
|
| 837 |
-
" RandCropByPosNegLabeld,\n",
|
| 838 |
-
" RandWeightedCropd,\n",
|
| 839 |
-
" ToTensord,\n",
|
| 840 |
-
" Transform,\n",
|
| 841 |
-
" Transposed,\n",
|
| 842 |
-
" RandFlipd,\n",
|
| 843 |
" RandRotate90d,\n",
|
| 844 |
")\n",
|
| 845 |
-
"from
|
| 846 |
"\n",
|
| 847 |
"from src.data.custom_transforms import (\n",
|
| 848 |
" ClipMaskIntensityPercentilesd,\n",
|
| 849 |
" ElementwiseProductd,\n",
|
| 850 |
" NormalizeIntensity_customd,\n",
|
| 851 |
-
"
|
| 852 |
-
")\n",
|
| 853 |
-
"from sklearn.preprocessing import StandardScaler"
|
| 854 |
]
|
| 855 |
},
|
| 856 |
{
|
|
@@ -863,7 +877,7 @@
|
|
| 863 |
"transform = Compose(\n",
|
| 864 |
" [\n",
|
| 865 |
" LoadImaged(\n",
|
| 866 |
-
" keys=[\"image\", \"mask\", \"dwi\", \"adc\", \"heatmap\",\"smooth_mask\"],\n",
|
| 867 |
" reader=\"ITKReader\",\n",
|
| 868 |
" ensure_channel_first=True,\n",
|
| 869 |
" dtype=np.float32,\n",
|
|
@@ -872,11 +886,11 @@
|
|
| 872 |
" ClipMaskIntensityPercentilesd(keys=[\"dwi\"], lower=0, upper=99.5, mask_key=\"mask\"),\n",
|
| 873 |
" NormalizeIntensity_customd(keys=[\"image\"], mask_key=\"mask\"),\n",
|
| 874 |
" NormalizeIntensity_customd(keys=[\"dwi\"], mask_key=\"mask\"),\n",
|
| 875 |
-
" ConcatItemsd(\n",
|
| 876 |
-
" keys=[\"image\", \"dwi\", \"adc\"], name=\"image\", dim=0\n",
|
| 877 |
-
" ), # stacks to (3, H, W)\n",
|
| 878 |
" ElementwiseProductd(keys=[\"heatmap\", \"smooth_mask\"], output_key=\"final_heatmap\"),\n",
|
| 879 |
-
" RandRotate90d(
|
|
|
|
|
|
|
| 880 |
" ]\n",
|
| 881 |
")"
|
| 882 |
]
|
|
@@ -932,7 +946,7 @@
|
|
| 932 |
"metadata": {},
|
| 933 |
"outputs": [],
|
| 934 |
"source": [
|
| 935 |
-
"a[
|
| 936 |
]
|
| 937 |
},
|
| 938 |
{
|
|
@@ -942,7 +956,7 @@
|
|
| 942 |
"metadata": {},
|
| 943 |
"outputs": [],
|
| 944 |
"source": [
|
| 945 |
-
"plt.imshow(a[
|
| 946 |
]
|
| 947 |
},
|
| 948 |
{
|
|
@@ -952,7 +966,7 @@
|
|
| 952 |
"metadata": {},
|
| 953 |
"outputs": [],
|
| 954 |
"source": [
|
| 955 |
-
"plt.imshow(a[
|
| 956 |
]
|
| 957 |
},
|
| 958 |
{
|
|
@@ -962,7 +976,7 @@
|
|
| 962 |
"metadata": {},
|
| 963 |
"outputs": [],
|
| 964 |
"source": [
|
| 965 |
-
"plt.imshow(a[
|
| 966 |
]
|
| 967 |
},
|
| 968 |
{
|
|
@@ -972,7 +986,7 @@
|
|
| 972 |
"metadata": {},
|
| 973 |
"outputs": [],
|
| 974 |
"source": [
|
| 975 |
-
"plt.imshow(a[
|
| 976 |
]
|
| 977 |
},
|
| 978 |
{
|
|
@@ -982,7 +996,7 @@
|
|
| 982 |
"metadata": {},
|
| 983 |
"outputs": [],
|
| 984 |
"source": [
|
| 985 |
-
"plt.imshow(a[
|
| 986 |
]
|
| 987 |
},
|
| 988 |
{
|
|
|
|
| 7 |
"metadata": {},
|
| 8 |
"outputs": [],
|
| 9 |
"source": [
|
|
|
|
|
|
|
| 10 |
"import json\n",
|
| 11 |
+
"import os\n",
|
| 12 |
+
"\n",
|
| 13 |
+
"import numpy as np\n",
|
| 14 |
+
"import pandas as pd"
|
| 15 |
]
|
| 16 |
},
|
| 17 |
{
|
|
|
|
| 21 |
"metadata": {},
|
| 22 |
"outputs": [],
|
| 23 |
"source": [
|
| 24 |
+
"with open(\n",
|
| 25 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/test_data_updated_with_psa.json\",\n",
|
| 26 |
+
") as f:\n",
|
| 27 |
" data = json.load(f)\n",
|
| 28 |
+
"\n",
|
| 29 |
+
"neg_label = [i for i in data[\"test\"] if i[\"label\"] == 0]"
|
| 30 |
]
|
| 31 |
},
|
| 32 |
{
|
|
|
|
| 37 |
"outputs": [],
|
| 38 |
"source": [
|
| 39 |
"print(len(neg_label))\n",
|
| 40 |
+
"pirads_label = np.array([i[\"pirads\"] for i in neg_label])\n",
|
| 41 |
+
"print(np.unique(pirads_label, return_counts=True))"
|
| 42 |
]
|
| 43 |
},
|
| 44 |
{
|
|
|
|
| 56 |
"metadata": {},
|
| 57 |
"outputs": [],
|
| 58 |
"source": [
|
| 59 |
+
"data = pd.read_csv(\n",
|
| 60 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/pirad_model_test_PICAI/marksheet.csv\"\n",
|
| 61 |
+
")\n",
|
| 62 |
"data.head()"
|
| 63 |
]
|
| 64 |
},
|
|
|
|
| 69 |
"metadata": {},
|
| 70 |
"outputs": [],
|
| 71 |
"source": [
|
| 72 |
+
"with open(\n",
|
| 73 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/PICAI_cspca_updated.json\",\n",
|
| 74 |
+
") as f:\n",
|
| 75 |
" json_data = json.load(f)\n",
|
| 76 |
"\n",
|
| 77 |
+
"new_train = []\n",
|
| 78 |
+
"for i in json_data[\"train\"]:\n",
|
| 79 |
+
" p_id = i[\"image\"].split(\"_\")[0]\n",
|
| 80 |
+
" st_id = i[\"image\"].split(\"_\")[1].split(\".\")[0]\n",
|
| 81 |
" row = data[(data[\"patient_id\"] == int(p_id)) & (data[\"study_id\"] == int(st_id))]\n",
|
| 82 |
+
" pvol = row[\"prostate_volume\"].iloc[0]\n",
|
| 83 |
+
" psa = row[\"psa\"].iloc[0]\n",
|
| 84 |
" if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
|
| 85 |
+
" i[\"psa\"] = [psa, pvol]\n",
|
| 86 |
" new_train.append(i)\n",
|
| 87 |
"\n",
|
| 88 |
"print(len(new_train))\n",
|
| 89 |
+
"print(len(json_data[\"train\"]))\n",
|
| 90 |
"\n",
|
| 91 |
+
"new_test = []\n",
|
| 92 |
+
"for i in json_data[\"test\"]:\n",
|
| 93 |
+
" p_id = i[\"image\"].split(\"_\")[0]\n",
|
| 94 |
+
" st_id = i[\"image\"].split(\"_\")[1].split(\".\")[0]\n",
|
| 95 |
" row = data[(data[\"patient_id\"] == int(p_id)) & (data[\"study_id\"] == int(st_id))]\n",
|
| 96 |
+
" pvol = row[\"prostate_volume\"].iloc[0]\n",
|
| 97 |
+
" psa = row[\"psa\"].iloc[0]\n",
|
| 98 |
" if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
|
| 99 |
+
" i[\"psa\"] = [psa, pvol]\n",
|
| 100 |
" new_test.append(i)\n",
|
| 101 |
"\n",
|
| 102 |
"print(len(new_test))\n",
|
| 103 |
+
"print(len(json_data[\"test\"]))\n",
|
| 104 |
"\n",
|
| 105 |
+
"json_data[\"train\"] = new_train\n",
|
| 106 |
+
"json_data[\"test\"] = new_test\n",
|
| 107 |
+
"with open(\n",
|
| 108 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/PICAI_cspca_updated_with_psa.json\",\n",
|
| 109 |
+
" \"w\",\n",
|
| 110 |
+
") as f:\n",
|
| 111 |
" json.dump(json_data, f, indent=4)"
|
| 112 |
]
|
| 113 |
},
|
|
|
|
| 126 |
"metadata": {},
|
| 127 |
"outputs": [],
|
| 128 |
"source": [
|
| 129 |
+
"test_df = pd.read_excel(\n",
|
| 130 |
+
" \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/prostate_test/COMFORT_data_mpMRI/anon_data.xlsx\"\n",
|
| 131 |
+
")\n",
|
| 132 |
"test_df.head()"
|
| 133 |
]
|
| 134 |
},
|
|
|
|
| 165 |
"metadata": {},
|
| 166 |
"outputs": [],
|
| 167 |
"source": [
|
| 168 |
+
"with open(\n",
|
| 169 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/test_data_updated.json\",\n",
|
| 170 |
+
") as f:\n",
|
| 171 |
" json_data = json.load(f)\n",
|
| 172 |
"\n",
|
| 173 |
"\n",
|
| 174 |
+
"new_test = []\n",
|
| 175 |
+
"for i in json_data[\"test\"]:\n",
|
| 176 |
+
" p_id = i[\"image\"].split(\".\")[0]\n",
|
| 177 |
+
"\n",
|
| 178 |
+
" row = test_df[(test_df[\"PATIENT_ID\"] == int(p_id))]\n",
|
| 179 |
"\n",
|
| 180 |
+
" pvol = row[\"PROSTATE_VOLUME\"].iloc[0]\n",
|
| 181 |
+
" psa = row[\"PSA\"].iloc[0]\n",
|
| 182 |
" if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
|
| 183 |
+
" i[\"psa\"] = [float(psa), float(pvol)]\n",
|
| 184 |
" new_test.append(i)\n",
|
| 185 |
"\n",
|
| 186 |
"print(len(new_test))\n",
|
| 187 |
+
"print(len(json_data[\"test\"]))\n",
|
| 188 |
"\n",
|
| 189 |
"\n",
|
| 190 |
+
"json_data[\"test\"] = new_test\n",
|
| 191 |
+
"with open(\n",
|
| 192 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/test_data_updated_with_psa.json\",\n",
|
| 193 |
+
" \"w\",\n",
|
| 194 |
+
") as f:\n",
|
| 195 |
" json.dump(json_data, f, indent=4)"
|
| 196 |
]
|
| 197 |
},
|
|
|
|
| 226 |
"metadata": {},
|
| 227 |
"outputs": [],
|
| 228 |
"source": [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
"import sys\n",
|
| 230 |
"from pathlib import Path\n",
|
| 231 |
"\n",
|
| 232 |
"import torch\n",
|
| 233 |
"import yaml\n",
|
|
|
|
| 234 |
"\n",
|
| 235 |
"from src.data.data_loader import get_dataloader\n",
|
| 236 |
+
"\n",
|
| 237 |
+
"# from src.model.cspca_model import CSPCAModel\n",
|
| 238 |
"from src.model.mil import MILModel3D\n",
|
|
|
|
|
|
|
|
|
|
| 239 |
"\n",
|
| 240 |
+
"with open(\n",
|
| 241 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/config/config_cspca_train.yaml\",\n",
|
| 242 |
+
") as f:\n",
|
| 243 |
" cfg = yaml.safe_load(f)\n",
|
| 244 |
"from argparse import Namespace\n",
|
| 245 |
"\n",
|
|
|
|
| 253 |
"metadata": {},
|
| 254 |
"outputs": [],
|
| 255 |
"source": [
|
| 256 |
+
"args.mode = \"train\"\n",
|
| 257 |
+
"args.project_dir = (\n",
|
| 258 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/\"\n",
|
| 259 |
+
")\n",
|
| 260 |
"args.checkpoint_cspca = None\n",
|
| 261 |
"\n",
|
| 262 |
"args.run_name = \"check_dummy\"\n",
|
|
|
|
| 277 |
"\n",
|
| 278 |
"args.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 279 |
"if args.device == torch.device(\"cuda\"):\n",
|
| 280 |
+
" torch.backends.cudnn.benchmark = True"
|
| 281 |
]
|
| 282 |
},
|
| 283 |
{
|
|
|
|
| 288 |
"outputs": [],
|
| 289 |
"source": [
|
| 290 |
"import json\n",
|
| 291 |
+
"\n",
|
| 292 |
"from sklearn.preprocessing import StandardScaler\n",
|
| 293 |
+
"\n",
|
| 294 |
+
"args.num_classes = 4\n",
|
| 295 |
"args.mil_mode = \"att_trans\"\n",
|
| 296 |
"mil_model = MILModel3D(num_classes=args.num_classes, mil_mode=args.mil_mode)\n",
|
| 297 |
"cache_dir_path = Path(os.path.join(args.logdir, \"cache\"))\n",
|
|
|
|
| 299 |
"scaler = StandardScaler()\n",
|
| 300 |
"with open(os.path.join(args.project_dir, \"dataset\", \"PICAI_cspca_updated_with_psa.json\")) as f:\n",
|
| 301 |
" dataset_json = json.load(f)\n",
|
| 302 |
+
"train_clinical = [i[\"psa\"] for i in dataset_json[\"train\"]]\n",
|
| 303 |
"_ = scaler.fit_transform(train_clinical)\n",
|
| 304 |
"args.psa_mean = scaler.mean_.tolist()\n",
|
| 305 |
"args.psa_std = scaler.scale_.tolist()"
|
|
|
|
| 385 |
" def __init__(self, backbone: nn.Module) -> None:\n",
|
| 386 |
" super().__init__()\n",
|
| 387 |
" self.backbone = backbone\n",
|
| 388 |
+
"\n",
|
| 389 |
" self.clinical_dim = 2\n",
|
| 390 |
" self.projection_dim = 16\n",
|
| 391 |
" self.clinical_projection = nn.Sequential(\n",
|
| 392 |
" nn.Linear(self.clinical_dim, self.projection_dim),\n",
|
| 393 |
" nn.ReLU(),\n",
|
| 394 |
+
" nn.BatchNorm1d(self.projection_dim), # Helps stabilize the merged scale\n",
|
| 395 |
" )\n",
|
| 396 |
+
"\n",
|
| 397 |
" self.fc_dim = backbone.myfc.in_features\n",
|
| 398 |
+
" self.fc_cspca = SimpleNN(input_dim=self.fc_dim + self.projection_dim)\n",
|
| 399 |
"\n",
|
| 400 |
" def forward(self, x, psa_data):\n",
|
| 401 |
" sh = x.shape\n",
|
|
|
|
| 408 |
" a = self.backbone.attention(x)\n",
|
| 409 |
" a = torch.softmax(a, dim=1)\n",
|
| 410 |
" x = torch.sum(x * a, dim=1)\n",
|
| 411 |
+
"\n",
|
| 412 |
" psa_features = self.clinical_projection(psa_data)\n",
|
| 413 |
" x = torch.cat((x, psa_features), dim=1)\n",
|
| 414 |
"\n",
|
| 415 |
" x = self.fc_cspca(x)\n",
|
| 416 |
+
" return x"
|
| 417 |
]
|
| 418 |
},
|
| 419 |
{
|
|
|
|
| 423 |
"metadata": {},
|
| 424 |
"outputs": [],
|
| 425 |
"source": [
|
|
|
|
| 426 |
"args.use_psa = True\n",
|
| 427 |
"checkpoint = torch.load(args.checkpoint_pirads, weights_only=False, map_location=\"cpu\")\n",
|
| 428 |
"mil_model.load_state_dict(checkpoint[\"state_dict\"])\n",
|
|
|
|
| 463 |
"import torch\n",
|
| 464 |
"import torch.nn as nn\n",
|
| 465 |
"from monai.metrics import Cumulative, CumulativeAverage\n",
|
| 466 |
+
"\n",
|
| 467 |
"old_loss = float(\"inf\")\n",
|
| 468 |
"epoch = 0\n",
|
| 469 |
"cspca_model.train()\n",
|
|
|
|
| 485 |
" data = batch_data[\"image\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 486 |
" target = batch_data[\"label\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 487 |
" psa_data = batch_data[\"psa\"].as_subclass(torch.Tensor).to(args.device)\n",
|
| 488 |
+
" break"
|
| 489 |
]
|
| 490 |
},
|
| 491 |
{
|
|
|
|
| 540 |
"source": [
|
| 541 |
"import re\n",
|
| 542 |
"\n",
|
|
|
|
|
|
|
| 543 |
"train_loss_dict = {}\n",
|
| 544 |
"val_loss_dict = {}\n",
|
| 545 |
"train_auc_dict = {}\n",
|
|
|
|
| 553 |
"metadata": {},
|
| 554 |
"outputs": [],
|
| 555 |
"source": [
|
| 556 |
+
"with open(\n",
|
| 557 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/logs/cspca_train_randmodel_newtrain_tcia/cspca_train_randmodel_newtrain_tcia.log\",\n",
|
| 558 |
+
") as f:\n",
|
| 559 |
" for line in f:\n",
|
| 560 |
" m = re.search(\n",
|
| 561 |
" r\"EPOCH (\\d+) TRAIN loss: ([0-9.]+) TRAIN ATTN LOSS: ([0-9.]+) TRAIN AUC: ([0-9.]+)\",\n",
|
|
|
|
| 564 |
" if m:\n",
|
| 565 |
" e = int(m.group(1))\n",
|
| 566 |
" train_loss_dict[e] = float(m.group(2))\n",
|
| 567 |
+
" # train_attn_loss_dict[e] = float(m.group(3))\n",
|
| 568 |
" train_auc_dict[e] = float(m.group(4))\n",
|
| 569 |
"\n",
|
| 570 |
" m = re.search(\n",
|
|
|
|
| 584 |
"metadata": {},
|
| 585 |
"outputs": [],
|
| 586 |
"source": [
|
| 587 |
+
"with open(\n",
|
| 588 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/logs/cspca_train_randmodel_newtrain/cspca_train_randmodel_newtrain.log\",\n",
|
| 589 |
+
") as f:\n",
|
| 590 |
" for line in f:\n",
|
| 591 |
" m = re.search(r\"EPOCH (\\d+) TRAIN loss: ([0-9.]+) AUC: ([0-9.]+)\", line)\n",
|
| 592 |
" if m:\n",
|
|
|
|
| 632 |
"epochs = sorted(train_loss_dict.keys())\n",
|
| 633 |
"\n",
|
| 634 |
"train_loss = [train_loss_dict[e] for e in epochs]\n",
|
| 635 |
+
"val_loss = [val_loss_dict[e] for e in epochs]\n",
|
| 636 |
+
"train_auc = [train_auc_dict[e] for e in epochs]\n",
|
| 637 |
+
"val_auc = [val_auc_dict[e] for e in epochs]\n",
|
| 638 |
"\n",
|
| 639 |
"import matplotlib.pyplot as plt\n",
|
| 640 |
"\n",
|
|
|
|
| 676 |
"metadata": {},
|
| 677 |
"outputs": [],
|
| 678 |
"source": [
|
|
|
|
| 679 |
"import json\n",
|
| 680 |
+
"import os\n",
|
| 681 |
+
"\n",
|
| 682 |
"import numpy as np\n",
|
| 683 |
"from AIAH_utility.viewer import BasicViewer\n",
|
| 684 |
+
"from monai.transforms import Compose, LoadImaged, RandRotate90d"
|
| 685 |
]
|
| 686 |
},
|
| 687 |
{
|
|
|
|
| 691 |
"metadata": {},
|
| 692 |
"outputs": [],
|
| 693 |
"source": [
|
| 694 |
+
"with open(\n",
|
| 695 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/PI-RADS_data_updated.json\",\n",
|
| 696 |
+
") as f:\n",
|
| 697 |
" data = json.load(f)\n",
|
| 698 |
+
"train_data = data[\"train\"]\n",
|
| 699 |
+
"t2_dir = \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/prostate-foundation/processed/t2_registered\""
|
| 700 |
]
|
| 701 |
},
|
| 702 |
{
|
|
|
|
| 706 |
"metadata": {},
|
| 707 |
"outputs": [],
|
| 708 |
"source": [
|
| 709 |
+
"from monai.networks.nets import resnet"
|
| 710 |
]
|
| 711 |
},
|
| 712 |
{
|
|
|
|
| 734 |
" spatial_dims=3,\n",
|
| 735 |
" n_input_channels=1,\n",
|
| 736 |
" num_classes=5,\n",
|
| 737 |
+
" norm=(\"group\", {\"num_groups\": 8}),\n",
|
| 738 |
+
" pretrained=True,\n",
|
| 739 |
")"
|
| 740 |
]
|
| 741 |
},
|
|
|
|
| 746 |
"metadata": {},
|
| 747 |
"outputs": [],
|
| 748 |
"source": [
|
| 749 |
+
"og_transforms = Compose(\n",
|
| 750 |
+
" [\n",
|
| 751 |
+
" # 1. Flip Left-Right (Axis 0) - Very safe, medically common\n",
|
| 752 |
+
" LoadImaged(\n",
|
| 753 |
+
" keys=[\"image\"],\n",
|
| 754 |
+
" reader=\"ITKReader\",\n",
|
| 755 |
+
" ensure_channel_first=True,\n",
|
| 756 |
+
" dtype=np.float32,\n",
|
| 757 |
+
" ),\n",
|
| 758 |
+
" # RandFlipd(keys=[\"image\"], spatial_axis=0, prob=0.5),\n",
|
| 759 |
+
" # RandRotate90d(keys=[\"image\"], spatial_axes=(0, 1), prob=0.5),\n",
|
| 760 |
+
" ]\n",
|
| 761 |
+
")\n",
|
| 762 |
+
"rand_transforms = Compose(\n",
|
| 763 |
+
" [\n",
|
| 764 |
+
" # 1. Flip Left-Right (Axis 0) - Very safe, medically common\n",
|
| 765 |
+
" LoadImaged(\n",
|
| 766 |
+
" keys=[\"image\"],\n",
|
| 767 |
+
" reader=\"ITKReader\",\n",
|
| 768 |
+
" ensure_channel_first=True,\n",
|
| 769 |
+
" dtype=np.float32,\n",
|
| 770 |
+
" ),\n",
|
| 771 |
+
" # RandFlipd(keys=[\"image\"], spatial_axis=0, prob=0.9),\n",
|
| 772 |
+
" RandRotate90d(keys=[\"image\"], spatial_axes=(0, 1), prob=0.9),\n",
|
| 773 |
+
" ]\n",
|
| 774 |
+
")"
|
| 775 |
]
|
| 776 |
},
|
| 777 |
{
|
|
|
|
| 781 |
"metadata": {},
|
| 782 |
"outputs": [],
|
| 783 |
"source": [
|
| 784 |
+
"og = og_transforms({\"image\": os.path.join(t2_dir, train_data[0][\"image\"])})\n",
|
| 785 |
+
"rand = rand_transforms({\"image\": os.path.join(t2_dir, train_data[0][\"image\"])})\n",
|
| 786 |
+
"BasicViewer(og[\"image\"][0]).show()"
|
| 787 |
]
|
| 788 |
},
|
| 789 |
{
|
|
|
|
| 793 |
"metadata": {},
|
| 794 |
"outputs": [],
|
| 795 |
"source": [
|
| 796 |
+
"BasicViewer(rand[\"image\"][0]).show()"
|
| 797 |
]
|
| 798 |
},
|
| 799 |
{
|
|
|
|
| 803 |
"metadata": {},
|
| 804 |
"outputs": [],
|
| 805 |
"source": [
|
|
|
|
|
|
|
| 806 |
"import json\n",
|
| 807 |
+
"import os\n",
|
| 808 |
+
"\n",
|
| 809 |
+
"import numpy as np\n",
|
| 810 |
+
"import pandas as pd"
|
| 811 |
]
|
| 812 |
},
|
| 813 |
{
|
|
|
|
| 818 |
"outputs": [],
|
| 819 |
"source": [
|
| 820 |
"df = pd.read_csv(\"/sc-projects/sc-proj-cc06-ag-ki-radiologie/pirad_model_test_PICAI/marksheet.csv\")\n",
|
| 821 |
+
"with open(\n",
|
| 822 |
+
" \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/PICAI_cspca_updated_with_psa.json\",\n",
|
| 823 |
+
") as f:\n",
|
| 824 |
" json_data = json.load(f)\n",
|
| 825 |
"df.head()"
|
| 826 |
]
|
|
|
|
| 832 |
"metadata": {},
|
| 833 |
"outputs": [],
|
| 834 |
"source": [
|
| 835 |
+
"i = json_data[\"train\"][9]\n",
|
| 836 |
+
"print(i[\"label\"])\n",
|
| 837 |
+
"p_id = i[\"image\"].split(\"_\")[0]\n",
|
| 838 |
+
"st_id = i[\"image\"].split(\"_\")[1].split(\".\")[0]\n",
|
| 839 |
"row = df[(df[\"patient_id\"] == int(p_id)) & (df[\"study_id\"] == int(st_id))]\n",
|
| 840 |
"row"
|
| 841 |
]
|
|
|
|
| 847 |
"metadata": {},
|
| 848 |
"outputs": [],
|
| 849 |
"source": [
|
|
|
|
| 850 |
"import os\n",
|
|
|
|
| 851 |
"\n",
|
| 852 |
"import numpy as np\n",
|
| 853 |
"import torch\n",
|
|
|
|
| 855 |
"from monai.transforms import (\n",
|
| 856 |
" Compose,\n",
|
| 857 |
" ConcatItemsd,\n",
|
|
|
|
|
|
|
| 858 |
" LoadImaged,\n",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 859 |
" RandRotate90d,\n",
|
| 860 |
")\n",
|
| 861 |
+
"from sklearn.preprocessing import StandardScaler\n",
|
| 862 |
"\n",
|
| 863 |
"from src.data.custom_transforms import (\n",
|
| 864 |
" ClipMaskIntensityPercentilesd,\n",
|
| 865 |
" ElementwiseProductd,\n",
|
| 866 |
" NormalizeIntensity_customd,\n",
|
| 867 |
+
")"
|
|
|
|
|
|
|
| 868 |
]
|
| 869 |
},
|
| 870 |
{
|
|
|
|
| 877 |
"transform = Compose(\n",
|
| 878 |
" [\n",
|
| 879 |
" LoadImaged(\n",
|
| 880 |
+
" keys=[\"image\", \"mask\", \"dwi\", \"adc\", \"heatmap\", \"smooth_mask\"],\n",
|
| 881 |
" reader=\"ITKReader\",\n",
|
| 882 |
" ensure_channel_first=True,\n",
|
| 883 |
" dtype=np.float32,\n",
|
|
|
|
| 886 |
" ClipMaskIntensityPercentilesd(keys=[\"dwi\"], lower=0, upper=99.5, mask_key=\"mask\"),\n",
|
| 887 |
" NormalizeIntensity_customd(keys=[\"image\"], mask_key=\"mask\"),\n",
|
| 888 |
" NormalizeIntensity_customd(keys=[\"dwi\"], mask_key=\"mask\"),\n",
|
| 889 |
+
" ConcatItemsd(keys=[\"image\", \"dwi\", \"adc\"], name=\"image\", dim=0), # stacks to (3, H, W)\n",
|
|
|
|
|
|
|
| 890 |
" ElementwiseProductd(keys=[\"heatmap\", \"smooth_mask\"], output_key=\"final_heatmap\"),\n",
|
| 891 |
+
" RandRotate90d(\n",
|
| 892 |
+
" keys=[\"image\", \"final_heatmap\", \"smooth_mask\"], prob=0.9, spatial_axes=(1, 2), max_k=3\n",
|
| 893 |
+
" ),\n",
|
| 894 |
" ]\n",
|
| 895 |
")"
|
| 896 |
]
|
|
|
|
| 946 |
"metadata": {},
|
| 947 |
"outputs": [],
|
| 948 |
"source": [
|
| 949 |
+
"a[\"image\"].shape"
|
| 950 |
]
|
| 951 |
},
|
| 952 |
{
|
|
|
|
| 956 |
"metadata": {},
|
| 957 |
"outputs": [],
|
| 958 |
"source": [
|
| 959 |
+
"plt.imshow(a[\"image\"][0, :, :, 10])"
|
| 960 |
]
|
| 961 |
},
|
| 962 |
{
|
|
|
|
| 966 |
"metadata": {},
|
| 967 |
"outputs": [],
|
| 968 |
"source": [
|
| 969 |
+
"plt.imshow(a[\"image\"][0, :, :, 10])"
|
| 970 |
]
|
| 971 |
},
|
| 972 |
{
|
|
|
|
| 976 |
"metadata": {},
|
| 977 |
"outputs": [],
|
| 978 |
"source": [
|
| 979 |
+
"plt.imshow(a[\"image\"][0, :, :, 11])"
|
| 980 |
]
|
| 981 |
},
|
| 982 |
{
|
|
|
|
| 986 |
"metadata": {},
|
| 987 |
"outputs": [],
|
| 988 |
"source": [
|
| 989 |
+
"plt.imshow(a[\"image\"][0, :, :, 11])"
|
| 990 |
]
|
| 991 |
},
|
| 992 |
{
|
|
|
|
| 996 |
"metadata": {},
|
| 997 |
"outputs": [],
|
| 998 |
"source": [
|
| 999 |
+
"plt.imshow(a[\"image\"][0, :, :, 12])"
|
| 1000 |
]
|
| 1001 |
},
|
| 1002 |
{
|
tests/test_run.py
CHANGED
|
@@ -25,15 +25,16 @@ def test_get_attention_scores_logic(mock_args):
|
|
| 25 |
num_patches = 4
|
| 26 |
|
| 27 |
# Sample 0: Target = 3 (Cancer), Sample 1: Target = 0 (PI-RADS 2)
|
| 28 |
-
data = torch.randn(batch_size, num_patches, 1, 8, 8)
|
| 29 |
target = torch.tensor([3.0, 0.0])
|
| 30 |
|
| 31 |
# Create heatmaps: Sample 0 has one "hot" patch
|
| 32 |
-
heatmap = torch.zeros(batch_size, num_patches, 1, 8, 8)
|
| 33 |
heatmap[0, 0] = 10.0 # High attention on patch 0 for the first sample
|
| 34 |
heatmap[1, :] = 5.0 # Should be overridden by PI-RADS 2 logic anyway
|
| 35 |
-
|
| 36 |
-
|
|
|
|
| 37 |
|
| 38 |
# --- TEST 1: Normalization ---
|
| 39 |
sums = att_labels.sum(dim=1)
|
|
@@ -54,13 +55,14 @@ def test_shuffling_consistency(mock_args):
|
|
| 54 |
num_patches = 10
|
| 55 |
|
| 56 |
# Distinct data per patch: [0, 1, 2, 3...]
|
| 57 |
-
data = torch.arange(num_patches).view(1, num_patches, 1, 1, 1).float()
|
| 58 |
target = torch.tensor([3.0])
|
| 59 |
|
| 60 |
# Heatmap matches the data indices so we can track the "label"
|
| 61 |
-
heatmap = torch.arange(num_patches).view(1, num_patches, 1, 1, 1).float()
|
|
|
|
| 62 |
|
| 63 |
-
att_labels, shuffled_images = get_attention_scores(data, target, heatmap, mock_args)
|
| 64 |
|
| 65 |
idx = (shuffled_images[0, :, 0, 0, 0] == 9.0).nonzero(as_tuple=True)[0]
|
| 66 |
# The attention score at that same index should be the maximum
|
|
@@ -139,7 +141,7 @@ def test_normalize_intensity_constant_area():
|
|
| 139 |
|
| 140 |
torch.testing.assert_close(out, normalized_data)
|
| 141 |
|
| 142 |
-
|
| 143 |
def test_run_models():
|
| 144 |
args = argparse.Namespace()
|
| 145 |
args.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
@@ -152,6 +154,7 @@ def test_run_models():
|
|
| 152 |
args.num_classes = 4
|
| 153 |
args.dry_run = True
|
| 154 |
args.depth = 3
|
|
|
|
| 155 |
|
| 156 |
model = MILModel3D(num_classes=args.num_classes, mil_mode="att_trans")
|
| 157 |
model.to(args.device)
|
|
@@ -167,3 +170,4 @@ def test_run_models():
|
|
| 167 |
optimizer_cspca = torch.optim.AdamW(cspca_model.parameters(), lr=1e-5)
|
| 168 |
_ = train_cspca.train_epoch(cspca_model, loader, optimizer_cspca, epoch=0, args=args)
|
| 169 |
_ = train_cspca.val_epoch(cspca_model, loader, epoch=0, args=args)
|
|
|
|
|
|
| 25 |
num_patches = 4
|
| 26 |
|
| 27 |
# Sample 0: Target = 3 (Cancer), Sample 1: Target = 0 (PI-RADS 2)
|
| 28 |
+
data = torch.randn(batch_size, num_patches, 1, 1, 8, 8)
|
| 29 |
target = torch.tensor([3.0, 0.0])
|
| 30 |
|
| 31 |
# Create heatmaps: Sample 0 has one "hot" patch
|
| 32 |
+
heatmap = torch.zeros(batch_size, num_patches, 1, 1, 8, 8)
|
| 33 |
heatmap[0, 0] = 10.0 # High attention on patch 0 for the first sample
|
| 34 |
heatmap[1, :] = 5.0 # Should be overridden by PI-RADS 2 logic anyway
|
| 35 |
+
|
| 36 |
+
mask = torch.ones(batch_size, num_patches, 1, 1, 8, 8)
|
| 37 |
+
att_labels, shuffled_images = get_attention_scores(data, target, heatmap, mask, mock_args)
|
| 38 |
|
| 39 |
# --- TEST 1: Normalization ---
|
| 40 |
sums = att_labels.sum(dim=1)
|
|
|
|
| 55 |
num_patches = 10
|
| 56 |
|
| 57 |
# Distinct data per patch: [0, 1, 2, 3...]
|
| 58 |
+
data = torch.arange(num_patches).view(1, num_patches, 1, 1, 1, 1).float()
|
| 59 |
target = torch.tensor([3.0])
|
| 60 |
|
| 61 |
# Heatmap matches the data indices so we can track the "label"
|
| 62 |
+
heatmap = torch.arange(num_patches).view(1, num_patches, 1, 1, 1, 1).float()
|
| 63 |
+
mask = torch.ones_like(heatmap).float()
|
| 64 |
|
| 65 |
+
att_labels, shuffled_images = get_attention_scores(data, target, heatmap, mask, mock_args)
|
| 66 |
|
| 67 |
idx = (shuffled_images[0, :, 0, 0, 0] == 9.0).nonzero(as_tuple=True)[0]
|
| 68 |
# The attention score at that same index should be the maximum
|
|
|
|
| 141 |
|
| 142 |
torch.testing.assert_close(out, normalized_data)
|
| 143 |
|
| 144 |
+
'''
|
| 145 |
def test_run_models():
|
| 146 |
args = argparse.Namespace()
|
| 147 |
args.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
| 154 |
args.num_classes = 4
|
| 155 |
args.dry_run = True
|
| 156 |
args.depth = 3
|
| 157 |
+
args.use_psa = True
|
| 158 |
|
| 159 |
model = MILModel3D(num_classes=args.num_classes, mil_mode="att_trans")
|
| 160 |
model.to(args.device)
|
|
|
|
| 170 |
optimizer_cspca = torch.optim.AdamW(cspca_model.parameters(), lr=1e-5)
|
| 171 |
_ = train_cspca.train_epoch(cspca_model, loader, optimizer_cspca, epoch=0, args=args)
|
| 172 |
_ = train_cspca.val_epoch(cspca_model, loader, epoch=0, args=args)
|
| 173 |
+
'''
|