Anirudh Balaraman commited on
Commit
efc95db
·
1 Parent(s): f1a8b97

update inference

Browse files
.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, validate_steps
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['psa'] for i in dataset_json['test']]
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(f"EPOCH {epoch} TRAIN loss: {train_loss:.4f} TRAIN ATTN LOSS: {train_attn_loss:.4f} TRAIN AUC: {train_auc:.4f}")
 
 
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("--run_name", type=str, default="default_cspca", help="run name for log file")
 
 
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, type=int)
64
- parser.add_argument("--tile_count", default=24, type=int)
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
- "histogram_match": histmatch,
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", "histogram_match", "get_heatmap"]
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
- cspca_model = CSPCAModel(backbone=pirads_model).to(args.device)
113
- checkpt = torch.load(
114
- os.path.join(args.project_dir, "models", "cspca_model.pth"), map_location="cpu"
115
- )
116
- cspca_model.load_state_dict(checkpt["state_dict"])
117
- cspca_model = cspca_model.to(args.device)
118
- '''
119
- transform = data_transform(args)
 
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 json
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['psa'] for i in dataset_json['test']]
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 , dtype=np.float32)
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
- RandWeightedCropd,
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(keys=["image", "final_heatmap", "smooth_mask"], prob=0.6, spatial_axes=(0, 1), max_k=3),
 
 
 
 
 
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=[ "dwi", "adc", "heatmap", "mask"]),
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=[ "dwi", "adc", "heatmap", "mask"]),
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) # 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,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(d_model=nfc, nhead=8, dropout=trans_dropout, batch_first=True)
 
 
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 = 0.0, adc_max = 3500.0):
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 SimpleITK as sitk
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 ) + (masked_heatmap_adc * 0.7)
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()) / (mix_mask[mask>0].max() - mix_mask[mask>0].min())
154
- mix_mask = np.where(mask > 0, mix_mask, 0)
 
 
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 as e:
114
- logging.warning(f"Top slices for {file} are not continuous: {top_slices}. Proceeding with non-continuous slices.")
 
 
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 = shuffled_images, psa_data = psa_data)
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 = data, psa_data = psa_data)
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 json"
 
20
  ]
21
  },
22
  {
@@ -52,15 +50,15 @@
52
  }
53
  ],
54
  "source": [
55
- "data_dir = '/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/manifest-1777460989222/prostate_mri_us_biopsy'\n",
56
- "out_dir = '/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/'\n",
57
  "print(len(os.listdir(data_dir)))\n",
58
- "'''\n",
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
- " \n",
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] # We only need to check the first slice's header\n",
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'b\\d+', desc_lower):\n",
112
  " sequence_type = \"dwi\"\n",
113
  "\n",
114
- " \n",
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). \n",
126
  " # This function groups them by their unique Series Instance UID.\n",
127
  " series_IDs = reader.GetGDCMSeriesIDs(dicom_dir)\n",
128
- " \n",
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
- " \n",
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
- " '''\n",
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
- " '''\n",
157
  " sitk.WriteImage(image, output_filepath)\n",
158
- " #print(f\"Successfully saved 3D volume to: {output_filepath}\")\n",
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='dicom_warnings.log',\n",
178
- " filemode='a',\n",
179
  " level=logging.WARNING,\n",
180
- " format='%(asctime)s - %(levelname)s - %(message)s'\n",
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
- " \n",
188
  " # --- NEW ADDITIONS ---\n",
189
  " self.current_uid = None\n",
190
- " self.problematic_uids = set() # Using a set so we don't get duplicates\n",
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
- " \n",
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): self._logger.info(s.rstrip())\n",
207
- " def DisplayGenericOutputText(self, s): self._logger.info(s.rstrip())\n",
208
- " def DisplayDebugText(self, s): self._logger.debug(s.rstrip())"
 
 
 
 
 
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, \n",
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
- " \n",
239
- " return set(f for f in os.listdir(folder_path) if not f.startswith('.'))\n",
 
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
- " \n",
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
- " \n",
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
- " \n",
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
- " \n",
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: print(f\" - {f}\")\n",
 
269
  " print()\n",
270
- " \n",
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: print(f\" - {f}\")\n",
 
274
  " print()\n",
275
- " \n",
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: print(f\" - {f}\")\n",
 
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)\n"
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('.'))\n",
 
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
- " \n",
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
- " \n",
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
- " \n",
348
  " total_deleted = 0\n",
349
- " \n",
350
  " print(f\"--- Running in {'DRY RUN (Safe)' if dry_run else 'ACTIVE DELETE'} mode ---\\n\")\n",
351
- " \n",
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
- " \n",
356
  " if not orphans:\n",
357
  " print(f\"✅ {name} folder is already clean.\")\n",
358
  " continue\n",
359
- " \n",
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
- " \n",
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
- " \n",
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
- " \"T2\": t2_dir,\n",
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
- " \n",
535
  " print(f\"--- Running in {'DRY RUN (Safe)' if dry_run else 'ACTIVE DELETE'} mode ---\\n\")\n",
536
- " \n",
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
- " \n",
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
- " \n",
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
- " \n",
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. \n",
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\") \n",
658
  "\n",
659
  " dicom_to_nrrd(seq_path, output_nrrd_path)\n",
660
  " bool_ = True\n",
661
  "\n",
662
- " \n",
663
- " \n",
664
  "bad_uids = list(tracker.problematic_uids)\n",
665
  "for uid in bad_uids:\n",
666
- " print(uid) \n",
667
  "\n",
668
  "\n",
669
- "'''\n",
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 = '/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/'\n",
729
- "meta_folder = \"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/manifest-1777460989222/metadata\"\n",
 
 
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(os.path.join(meta_folder, \"Prostate-MRI-US-Biopsy-NBIA-manifest_v2_20231020-nbia-digest.xlsx\"))\n",
746
- "df = pd.read_excel(os.path.join(meta_folder, \"TCIA-Biopsy-Data_2020-07-14.xlsx\"))\n"
 
 
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
- " \n",
779
- " t2_id = filtered_parent[filtered_parent[\"Series Description\"].str.contains(\"t2\", case=False, na=False)][\"Series Instance UID\"].iloc[0]\n",
 
 
780
  " filtered_df = df[df[\"Series Instance UID (MRI)\"] == t2_id]\n",
781
- " assert filtered_df[\"Patient Number\"].unique()[0] == patient_id, 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",
 
 
782
  " if filtered_df[\"Series Instance UID (US)\"].unique().shape[0] == 1:\n",
783
- " assert filtered_df[\"PSA (ng/mL)\"].unique().shape[0] == 1, f\"Expected 1 unique PSA value for Study Instance UID {id}, but found {filtered_df['PSA (ng/mL)'].unique().shape[0]}\"\n",
784
- " assert filtered_df[\"Prostate Volume (CC)\"].unique().shape[0] == 1, f\"Expected 1 unique Volume value for Study Instance UID {id}, but found {filtered_df['Volume (cc)'].unique().shape[0]}\"\n",
785
- " gg = (filtered_df['Primary Gleason'].fillna(0) + filtered_df['Secondary Gleason'].fillna(0)).max()\n",
 
 
 
 
 
 
786
  " temp = {}\n",
787
  " temp[\"image\"] = file\n",
788
- " temp[\"psa\"] = [filtered_df[\"PSA (ng/mL)\"].unique()[0], filtered_df[\"Prostate Volume (CC)\"].unique()[0] ]\n",
789
- " temp[\"dwi\"] = os.path.join(\"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/DWI_registered\", file)\n",
790
- " temp[\"adc\"] = os.path.join(\"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/ADC_clipped\", file)\n",
791
- " temp[\"heatmap\"] = os.path.join(\"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/heatmaps\", file)\n",
792
- " temp[\"mask\"] = os.path.join(\"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/prostate_mask\", file)\n",
793
- " temp[\"smooth_mask\"] = os.path.join(\"/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/smooth_prostate_mask\", file)\n",
794
- " temp[\"label\"] = 1.0 if gg>=7 else 0.0\n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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['label'] for i in test_list ]\n",
853
- "np.unique(np.array(labs), return_counts=True)\n"
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 argparse\n",
19
  "import logging\n",
20
  "import os\n",
21
  "import shutil\n",
22
  "import sys\n",
23
  "from pathlib import Path\n",
24
- "import json\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
  "\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 train_epoch, val_epoch\n",
35
- "from src.utils import get_metrics, save_cspca_checkpoint, setup_logging\n",
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\", \"r\") as f:\n",
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 = 'test'\n",
62
  "args.project_dir = None\n",
63
  "args.project_dir = Path.cwd()\n",
64
- "args.run_name = 'test_dummy'\n",
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['psa'] for i in dataset_json['train']]\n",
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 = '/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/TCIA_test_data_updated_mask.json'\n"
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}.\")\n"
140
  ]
141
  },
142
  {
@@ -169,7 +167,7 @@
169
  "metrics_dict[\"specificity\"].append(test_metric[\"specificity\"])\n",
170
  "\n",
171
  "\n",
172
- "import shutil\n",
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
- "os.environ['CUDA_LAUNCH_BLOCKING'] = \"1\"\n",
 
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 = data, psa_data = psa_data)\n",
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
- " '''\n",
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
- " '''\n",
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
- "'''\n",
 
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
- "'''\n",
 
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
- " f\"(95% CI: {auc_ci[0]:.3f} - {auc_ci[1]:.3f})\")\n",
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
- " print(f\"target: {t} | pred: {p:.4f}\")"
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
- "import numpy as np\n",
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('updated_segmentations/')\n",
838
  "c = 0\n",
839
  "for i in false_negative_ids[0]:\n",
840
- " if test_data['test'][i]['image'] in updated_masks:\n",
841
- " print(i)\n"
842
  ]
843
  },
844
  {
@@ -896,10 +887,13 @@
896
  "metadata": {},
897
  "outputs": [],
898
  "source": [
899
- "\n",
900
  "import json\n",
 
901
  "import nrrd\n",
902
- "with open('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/TCIA_test_data.json', 'r') as f:\n",
 
 
 
903
  " test_data = json.load(f)"
904
  ]
905
  },
@@ -936,18 +930,19 @@
936
  }
937
  ],
938
  "source": [
939
- "\n",
940
- "t2_dir_proc = '/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed/t2_registered'\n",
 
941
  "\n",
942
  "id = 195\n",
943
  "\n",
944
  "\n",
945
- "file = test_data['test'][id]['image']\n",
946
  "\n",
947
- "t2_proc, _ = nrrd.read(os.path.join(t2_dir_proc,file))\n",
948
- "dwi_proc, _ = nrrd.read(test_data['test'][id]['dwi'])\n",
949
- "adc_proc, _ = nrrd.read(test_data['test'][id]['adc'])#\n",
950
- "heats, _ = nrrd.read(test_data['test'][id]['heatmap'])\n",
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
- " \n",
973
- " # If there is only exactly 1 slice with a heatmap, axes is 1D. \n",
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
- " \n",
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
- " \n",
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
- " \n",
989
  " # --- Column 1: T2 + Heatmap ---\n",
990
- " axes[i, 0].imshow(t2_slice, cmap='gray')\n",
991
- " axes[i, 0].imshow(masked_heat, cmap='jet', alpha=0.5)\n",
992
- " axes[i, 0].set_title(f'T2 + Heatmap (Slice {z})', fontsize=14)\n",
993
- " axes[i, 0].axis('off')\n",
994
- " \n",
995
  " # --- Column 2: ADC + Heatmap ---\n",
996
- " axes[i, 1].imshow(adc_slice, cmap='gray')\n",
997
- " axes[i, 1].imshow(masked_heat, cmap='jet', alpha=0.5)\n",
998
- " axes[i, 1].set_title(f'ADC + Heatmap (Slice {z})', fontsize=14)\n",
999
- " axes[i, 1].axis('off')\n",
1000
- " \n",
1001
  " # --- Column 3: DWI (No Heatmap) ---\n",
1002
- " axes[i, 2].imshow(dwi_slice, cmap='gray')\n",
1003
- " axes[i, 2].set_title(f'DWI (Slice {z})', fontsize=14)\n",
1004
- " axes[i, 2].axis('off')\n",
1005
- " \n",
1006
  " plt.tight_layout()\n",
1007
  " plt.show()\n",
1008
- "print(file)\n"
1009
  ]
1010
  },
1011
  {
@@ -1017,10 +1012,10 @@
1017
  "source": [
1018
  "import shutil\n",
1019
  "\n",
1020
- "in_folder = '/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/nrrd_files/processed'\n",
1021
- "out_folder = '/sc-projects/sc-proj-cc06-ag-ki-radiologie/TCIA_prostate/keno_files'\n",
1022
- "for i in ['t2_registered', 'ADC_clipped', 'DWI_registered', 'smooth_prostate_mask']:\n",
1023
- " shutil.copy(os.path.join(in_folder, i, file), os.path.join(out_folder, i.split('_')[0], file))"
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 train_epoch, val_epoch\n",
1059
- "from src.utils import save_pirads_checkpoint, setup_logging\n",
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\", \"r\") as f:\n",
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 = 'test'\n",
1080
  "args.project_dir = Path.cwd()\n",
1081
- "args.run_name = 'test_dummy_pirads'\n",
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 = 'Dummy'\n",
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
- ")\n"
1137
  ]
1138
  },
1139
  {
@@ -1197,15 +1188,15 @@
1197
  "metadata": {},
1198
  "outputs": [],
1199
  "source": [
1200
- "import argparse\n",
1201
  "import logging\n",
1202
- "import time\n",
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_)\n"
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['pirads'], df['cspca'])\n",
1334
  "\n",
1335
  "# Add totals\n",
1336
  "ct_with_totals = ct.copy()\n",
1337
- "ct_with_totals['Total'] = ct_with_totals.sum(axis=1) # row totals\n",
1338
- "ct_with_totals.loc['Total'] = ct_with_totals.sum(axis=0) # column totals\n",
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['pirads'], df['cspca'])\n",
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, # position to the right\n",
1370
  " i + 0.5,\n",
1371
  " str(val),\n",
1372
- " va='center'\n",
1373
  " )\n",
1374
  "\n",
1375
  "# Label for row totals\n",
1376
- "ax.text(ct.shape[1] + 0.3, -0.3, 'Total', ha='center')\n",
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, # position below\n",
1383
  " str(val),\n",
1384
- " ha='center'\n",
1385
  " )\n",
1386
  "\n",
1387
  "# Label for column totals\n",
1388
- "ax.text(-0.5, ct.shape[0] + 0.3, 'Total', va='center')\n",
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('PI-RADS vs csPCa')\n",
1396
- "ax.set_xlabel('csPCa')\n",
1397
- "ax.set_ylabel('PI-RADS')\n",
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
- " classification_report\n",
 
 
1420
  ")\n",
1421
- "import numpy as np\n",
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 argparse\n",
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 train_epoch, val_epoch\n",
1489
- "from src.utils import save_pirads_checkpoint, setup_logging"
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\", \"r\") as f:\n",
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 = 'rain'\n",
1517
  "args.project_dir = Path.cwd()\n",
1518
- "args.run_name = 'test_dummy_pirads'\n",
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 = 'Dummy'\n",
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
- ")\n"
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['psa'] for i in dataset_json['train']]\n",
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": null,
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('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/test_data_updated_with_psa.json', 'r') as f:\n",
 
 
24
  " data = json.load(f)\n",
25
- " \n",
26
- "neg_label = [i for i in data['test'] if i['label'] == 0]"
27
  ]
28
  },
29
  {
@@ -34,8 +37,8 @@
34
  "outputs": [],
35
  "source": [
36
  "print(len(neg_label))\n",
37
- "pirads_label = np.array([i['pirads'] for i in neg_label])\n",
38
- "print(np.unique(pirads_label, return_counts=True))\n"
39
  ]
40
  },
41
  {
@@ -53,7 +56,9 @@
53
  "metadata": {},
54
  "outputs": [],
55
  "source": [
56
- "data = pd.read_csv(\"/sc-projects/sc-proj-cc06-ag-ki-radiologie/pirad_model_test_PICAI/marksheet.csv\")\n",
 
 
57
  "data.head()"
58
  ]
59
  },
@@ -64,40 +69,45 @@
64
  "metadata": {},
65
  "outputs": [],
66
  "source": [
67
- "with open('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/PICAI_cspca_updated.json', 'r') as f:\n",
 
 
68
  " json_data = json.load(f)\n",
69
  "\n",
70
- "new_train =[] \n",
71
- "for i in json_data['train']:\n",
72
- " p_id = i['image'].split('_')[0]\n",
73
- " st_id = i['image'].split('_')[1].split('.')[0]\n",
74
  " row = data[(data[\"patient_id\"] == int(p_id)) & (data[\"study_id\"] == int(st_id))]\n",
75
- " pvol = row['prostate_volume'].iloc[0]\n",
76
- " psa = row['psa'].iloc[0]\n",
77
  " if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
78
- " i['psa'] = [psa, pvol]\n",
79
  " new_train.append(i)\n",
80
  "\n",
81
  "print(len(new_train))\n",
82
- "print(len(json_data['train']))\n",
83
  "\n",
84
- "new_test =[] \n",
85
- "for i in json_data['test']:\n",
86
- " p_id = i['image'].split('_')[0]\n",
87
- " st_id = i['image'].split('_')[1].split('.')[0]\n",
88
  " row = data[(data[\"patient_id\"] == int(p_id)) & (data[\"study_id\"] == int(st_id))]\n",
89
- " pvol = row['prostate_volume'].iloc[0]\n",
90
- " psa = row['psa'].iloc[0]\n",
91
  " if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
92
- " i['psa'] = [psa, pvol]\n",
93
  " new_test.append(i)\n",
94
  "\n",
95
  "print(len(new_test))\n",
96
- "print(len(json_data['test']))\n",
97
  "\n",
98
- "json_data['train'] = new_train\n",
99
- "json_data['test'] = new_test\n",
100
- "with open('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/PICAI_cspca_updated_with_psa.json', 'w') as f:\n",
 
 
 
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('/sc-projects/sc-proj-cc06-ag-ki-radiologie/prostate_test/COMFORT_data_mpMRI/anon_data.xlsx')\n",
 
 
120
  "test_df.head()"
121
  ]
122
  },
@@ -153,28 +165,33 @@
153
  "metadata": {},
154
  "outputs": [],
155
  "source": [
156
- "with open('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/test_data_updated.json', 'r') as f:\n",
 
 
157
  " json_data = json.load(f)\n",
158
  "\n",
159
  "\n",
160
- "new_test =[] \n",
161
- "for i in json_data['test']:\n",
162
- " p_id = i['image'].split('.')[0]\n",
163
- " \n",
164
- " row = test_df[(test_df[\"PATIENT_ID\"] == int(p_id)) ]\n",
165
  "\n",
166
- " pvol = row['PROSTATE_VOLUME'].iloc[0]\n",
167
- " psa = row['PSA'].iloc[0]\n",
168
  " if pd.notna(psa) and psa != \"\" and pd.notna(pvol) and pvol != \"\":\n",
169
- " i['psa'] = [float(psa), float(pvol)]\n",
170
  " new_test.append(i)\n",
171
  "\n",
172
  "print(len(new_test))\n",
173
- "print(len(json_data['test']))\n",
174
  "\n",
175
  "\n",
176
- "json_data['test'] = new_test\n",
177
- "with open('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/test_data_updated_with_psa.json', 'w') as f:\n",
 
 
 
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
- "#from src.model.cspca_model import CSPCAModel\n",
 
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(\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/config/config_cspca_train.yaml\", \"r\") as f:\n",
 
 
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 = 'train'\n",
245
- "args.project_dir = \"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/\"\n",
 
 
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\n"
267
  ]
268
  },
269
  {
@@ -274,8 +288,10 @@
274
  "outputs": [],
275
  "source": [
276
  "import json\n",
 
277
  "from sklearn.preprocessing import StandardScaler\n",
278
- "args.num_classes =4\n",
 
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['psa'] for i in dataset_json['train']]\n",
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
- " \n",
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) # Helps stabilize the merged scale\n",
379
  " )\n",
380
- " \n",
381
  " self.fc_dim = backbone.myfc.in_features\n",
382
- " self.fc_cspca = SimpleNN(input_dim=self.fc_dim + self.projection_dim) \n",
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
- " \n",
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\n"
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
- "from sklearn.metrics import confusion_matrix, roc_auc_score\n",
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\n"
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(\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/logs/cspca_train_randmodel_newtrain_tcia/cspca_train_randmodel_newtrain_tcia.log\", \"r\") as f:\n",
 
 
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(\"/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/logs/cspca_train_randmodel_newtrain/cspca_train_randmodel_newtrain.log\", \"r\") as f:\n",
 
 
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 = [val_loss_dict[e] for e in epochs]\n",
619
- "train_auc = [train_auc_dict[e] for e in epochs]\n",
620
- "val_auc = [val_auc_dict[e] for e in epochs]\n",
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, RandFlipd, RandRotate90d, LoadImaged"
667
  ]
668
  },
669
  {
@@ -673,10 +691,12 @@
673
  "metadata": {},
674
  "outputs": [],
675
  "source": [
676
- "with open('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/PI-RADS_data_updated.json', 'r') as f:\n",
 
 
677
  " data = json.load(f)\n",
678
- "train_data = data['train']\n",
679
- "t2_dir = '/sc-projects/sc-proj-cc06-ag-ki-radiologie/prostate-foundation/processed/t2_registered'\n"
680
  ]
681
  },
682
  {
@@ -686,7 +706,7 @@
686
  "metadata": {},
687
  "outputs": [],
688
  "source": [
689
- "from monai.networks.nets import resnet\n"
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=('group', {'num_groups': 8}),\n",
718
- " pretrained=True\n",
719
  ")"
720
  ]
721
  },
@@ -726,28 +746,32 @@
726
  "metadata": {},
727
  "outputs": [],
728
  "source": [
729
- "og_transforms = Compose([\n",
730
- " # 1. Flip Left-Right (Axis 0) - Very safe, medically common\n",
731
- " LoadImaged(\n",
732
- " keys=[\"image\"],\n",
733
- " reader=\"ITKReader\",\n",
734
- " ensure_channel_first=True,\n",
735
- " dtype=np.float32,\n",
736
- " ),\n",
737
- " #RandFlipd(keys=[\"image\"], spatial_axis=0, prob=0.5),\n",
738
- " #RandRotate90d(keys=[\"image\"], spatial_axes=(0, 1), prob=0.5),\n",
739
- "])\n",
740
- "rand_transforms = Compose([\n",
741
- " # 1. Flip Left-Right (Axis 0) - Very safe, medically common\n",
742
- " LoadImaged(\n",
743
- " keys=[\"image\"],\n",
744
- " reader=\"ITKReader\",\n",
745
- " ensure_channel_first=True,\n",
746
- " dtype=np.float32,\n",
747
- " ),\n",
748
- " #RandFlipd(keys=[\"image\"], spatial_axis=0, prob=0.9),\n",
749
- " RandRotate90d(keys=[\"image\"], spatial_axes=(0, 1), prob=0.9),\n",
750
- "])"
 
 
 
 
751
  ]
752
  },
753
  {
@@ -757,9 +781,9 @@
757
  "metadata": {},
758
  "outputs": [],
759
  "source": [
760
- "og = og_transforms({'image': os.path.join(t2_dir, train_data[0]['image'])})\n",
761
- "rand = rand_transforms({'image': os.path.join(t2_dir, train_data[0]['image'])})\n",
762
- "BasicViewer(og['image'][0]).show()\n"
763
  ]
764
  },
765
  {
@@ -769,7 +793,7 @@
769
  "metadata": {},
770
  "outputs": [],
771
  "source": [
772
- "BasicViewer(rand['image'][0]).show()"
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 numpy as np"
 
 
 
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('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/dataset/PICAI_cspca_updated_with_psa.json', 'r') as f:\n",
 
 
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['train'][9]\n",
809
- "print(i['label'])\n",
810
- "p_id = i['image'].split('_')[0]\n",
811
- "st_id = i['image'].split('_')[1].split('.')[0]\n",
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 torch.utils.data.dataloader import default_collate\n",
846
  "\n",
847
  "from src.data.custom_transforms import (\n",
848
  " ClipMaskIntensityPercentilesd,\n",
849
  " ElementwiseProductd,\n",
850
  " NormalizeIntensity_customd,\n",
851
- " NormalizePSAd,\n",
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(keys=[\"image\", \"final_heatmap\", \"smooth_mask\"], prob=0.9, spatial_axes=(1, 2), max_k=3),\n",
 
 
880
  " ]\n",
881
  ")"
882
  ]
@@ -932,7 +946,7 @@
932
  "metadata": {},
933
  "outputs": [],
934
  "source": [
935
- "a['image'].shape"
936
  ]
937
  },
938
  {
@@ -942,7 +956,7 @@
942
  "metadata": {},
943
  "outputs": [],
944
  "source": [
945
- "plt.imshow(a['image'][0,:,:,10])"
946
  ]
947
  },
948
  {
@@ -952,7 +966,7 @@
952
  "metadata": {},
953
  "outputs": [],
954
  "source": [
955
- "plt.imshow(a['image'][0,:,:,10])"
956
  ]
957
  },
958
  {
@@ -962,7 +976,7 @@
962
  "metadata": {},
963
  "outputs": [],
964
  "source": [
965
- "plt.imshow(a['image'][0,:,:,11])"
966
  ]
967
  },
968
  {
@@ -972,7 +986,7 @@
972
  "metadata": {},
973
  "outputs": [],
974
  "source": [
975
- "plt.imshow(a['image'][0,:,:,11])"
976
  ]
977
  },
978
  {
@@ -982,7 +996,7 @@
982
  "metadata": {},
983
  "outputs": [],
984
  "source": [
985
- "plt.imshow(a['image'][0,:,:,12])"
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
- att_labels, shuffled_images = get_attention_scores(data, target, heatmap, mock_args)
 
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
+ '''