Anirudh Balaraman commited on
Commit
8d661c2
·
1 Parent(s): cc28ce1

update viewer

Browse files
Files changed (4) hide show
  1. app.py +120 -3
  2. pirads_inference.ipynb +397 -0
  3. run_inference.py +22 -5
  4. src/utils.py +32 -0
app.py CHANGED
@@ -4,7 +4,6 @@ import os
4
  import shutil
5
  import subprocess
6
 
7
- import matplotlib.patches as patches
8
  import matplotlib.pyplot as plt
9
  import nrrd
10
  import numpy as np
@@ -45,6 +44,119 @@ def load_nrrd(file_path):
45
  return data, header
46
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  def display_slicer(scan_paths, mask_path=None, bboxes=None, title="Scan Viewer", key_suffix=""):
49
  """
50
  Displays slicer with Multi-Background Support, Mask Overlay, and Bounding Box Multiselect.
@@ -156,6 +268,8 @@ def display_slicer(scan_paths, mask_path=None, bboxes=None, title="Scan Viewer",
156
  ax.axis("off")
157
  st.pyplot(fig, use_container_width=False)
158
 
 
 
159
 
160
  @st.cache_resource
161
  def download_all_models():
@@ -571,6 +685,7 @@ if st.session_state.inference_done:
571
  dwi_vis_path = None
572
  adc_vis_path = None
573
  mask_vis_path = None
 
574
 
575
  t2_vis_dir = os.path.join(OUTPUT_DIR, "t2_registered")
576
  if os.path.exists(t2_vis_dir) and len(os.listdir(t2_vis_dir)) > 0:
@@ -595,6 +710,8 @@ if st.session_state.inference_done:
595
  else:
596
  print("No mask dir")
597
 
 
 
598
  roi_bbox = None
599
  if "coords" in st.session_state:
600
  detected_boxes = []
@@ -614,7 +731,7 @@ if st.session_state.inference_done:
614
  display_slicer(
615
  scan_paths=scan_dict, # <--- Pass the Dict here
616
  mask_path=mask_vis_path,
617
- bboxes=detected_boxes,
618
  title="Salient Patch Viewer",
619
  key_suffix="main_viz",
620
  )
@@ -622,7 +739,7 @@ if st.session_state.inference_done:
622
  display_slicer(
623
  scan_paths=scan_dict, # <--- Pass the Dict here
624
  mask_path=mask_vis_path,
625
- bboxes=None,
626
  title="Salient Patch Viewer",
627
  key_suffix="main_viz",
628
  )
 
4
  import shutil
5
  import subprocess
6
 
 
7
  import matplotlib.pyplot as plt
8
  import nrrd
9
  import numpy as np
 
44
  return data, header
45
 
46
 
47
+ def display_slicer(
48
+ scan_paths, mask_path=None, heatmap_path=None, title="Scan Viewer", key_suffix=""
49
+ ):
50
+ """
51
+ Displays slicer with Multi-Background Support, Mask Overlay, and Attention Heatmap.
52
+
53
+ Args:
54
+ scan_paths: Dict of {Label: FilePath}. Example: {"T2W": "path/to/t2.nrrd"}
55
+ mask_path: Path to the segmentation mask (optional)
56
+ heatmap_path: Path to the 3D attention heatmap nrrd file (optional)
57
+ """
58
+ # 1. Layout: Image/Slider (Left) | Controls (Right)
59
+ c_viewer, c_controls = st.columns([3, 1.5])
60
+
61
+ # --- CONTROLS SECTION (Right Column) ---
62
+ with c_controls:
63
+ st.write(f"**{title} Controls**")
64
+
65
+ # A. Background Selection
66
+ available_scans = list(scan_paths.keys())
67
+ selected_scan_name = st.radio(
68
+ "Background Image", available_scans, index=0, key=f"bg_{key_suffix}"
69
+ )
70
+ current_file_path = scan_paths[selected_scan_name]
71
+
72
+ # B. Heatmap Controls
73
+ st.write("---")
74
+ show_heatmap = False
75
+ if heatmap_path and os.path.exists(heatmap_path):
76
+ show_heatmap = st.checkbox("Show Attention Heatmap", value=True, key=f"hm_{key_suffix}")
77
+ if show_heatmap:
78
+ hm_alpha = st.slider(
79
+ "Heatmap Opacity", 0.1, 1.0, 0.5, 0.1, key=f"hm_a_{key_suffix}"
80
+ )
81
+ # Threshold to hide low-attention areas (assumes heatmap is normalized 0-1)
82
+ hm_thresh = st.slider(
83
+ "Hide Values Below", 0.0, 1.0, 0.1, 0.05, key=f"hm_t_{key_suffix}"
84
+ )
85
+
86
+ # C. Mask Controls
87
+ st.write("---")
88
+ show_mask = False
89
+ if mask_path and os.path.exists(mask_path):
90
+ show_mask = st.checkbox("Show Mask Overlay", value=False, key=f"mk_{key_suffix}")
91
+
92
+ # --- VIEWER SECTION (Left Column) ---
93
+ with c_viewer:
94
+ if not os.path.exists(current_file_path):
95
+ st.error(f"File not found: {current_file_path}")
96
+ return
97
+
98
+ # Load the selected background image
99
+ data, _ = load_nrrd(current_file_path)
100
+
101
+ if len(data.shape) != 3:
102
+ st.warning("Data is not 3D.")
103
+ return
104
+
105
+ total_slices = data.shape[2]
106
+
107
+ # D. Slider Logic
108
+ start_slice = total_slices // 2
109
+
110
+ slice_idx = st.slider(
111
+ "Select Slice (Z-Axis)", 0, total_slices - 1, start_slice, key=f"sl_{key_suffix}"
112
+ )
113
+
114
+ # E. Plotting
115
+ img_slice = data[:, :, slice_idx]
116
+
117
+ # Normalize Image (0-1)
118
+ img_slice = img_slice.astype(float)
119
+
120
+ fig, ax = plt.subplots(figsize=(5, 5))
121
+ ax.imshow(img_slice, cmap="gray", origin="upper")
122
+
123
+ # 1. Overlay Heatmap
124
+ if show_heatmap:
125
+ h_data, _ = load_nrrd(heatmap_path)
126
+
127
+ if h_data.shape == data.shape:
128
+ h_slice = h_data[:, :, slice_idx].astype(float)
129
+
130
+ # Normalize the heatmap slice to 0-1 if it isn't already
131
+ max_val = np.max(h_slice)
132
+ if max_val > 0:
133
+ h_slice = h_slice / max_val
134
+
135
+ # Mask out values below the user-defined threshold
136
+ h_overlay = np.ma.masked_where(h_slice < hm_thresh, h_slice)
137
+
138
+ # Overlay using 'jet', 'inferno', or 'hot' colormap
139
+ ax.imshow(h_overlay, cmap="jet", alpha=hm_alpha, origin="upper")
140
+ else:
141
+ ax.text(5, 5, "Heatmap shape mismatch", color="red", fontsize=8)
142
+
143
+ # 2. Overlay Mask
144
+ if show_mask:
145
+ m_data, _ = load_nrrd(mask_path)
146
+
147
+ if m_data.shape == data.shape:
148
+ mslice = m_data[:, :, slice_idx]
149
+ overlay = np.ma.masked_where(mslice == 0, mslice)
150
+ ax.imshow(overlay, cmap="Reds", alpha=0.5, origin="upper")
151
+ else:
152
+ # Placed slightly lower so it doesn't overlap with the heatmap warning
153
+ ax.text(5, 15, "Mask shape mismatch", color="red", fontsize=8)
154
+
155
+ ax.axis("off")
156
+ st.pyplot(fig, use_container_width=False)
157
+
158
+
159
+ '''
160
  def display_slicer(scan_paths, mask_path=None, bboxes=None, title="Scan Viewer", key_suffix=""):
161
  """
162
  Displays slicer with Multi-Background Support, Mask Overlay, and Bounding Box Multiselect.
 
268
  ax.axis("off")
269
  st.pyplot(fig, use_container_width=False)
270
 
271
+ '''
272
+
273
 
274
  @st.cache_resource
275
  def download_all_models():
 
685
  dwi_vis_path = None
686
  adc_vis_path = None
687
  mask_vis_path = None
688
+ att_map_path = None
689
 
690
  t2_vis_dir = os.path.join(OUTPUT_DIR, "t2_registered")
691
  if os.path.exists(t2_vis_dir) and len(os.listdir(t2_vis_dir)) > 0:
 
710
  else:
711
  print("No mask dir")
712
 
713
+ att_map_path = os.path.join(OUTPUT_DIR, "attention_weights.nrrd")
714
+
715
  roi_bbox = None
716
  if "coords" in st.session_state:
717
  detected_boxes = []
 
731
  display_slicer(
732
  scan_paths=scan_dict, # <--- Pass the Dict here
733
  mask_path=mask_vis_path,
734
+ heatmap_path=att_map_path,
735
  title="Salient Patch Viewer",
736
  key_suffix="main_viz",
737
  )
 
739
  display_slicer(
740
  scan_paths=scan_dict, # <--- Pass the Dict here
741
  mask_path=mask_vis_path,
742
+ heatmap_path=None,
743
  title="Salient Patch Viewer",
744
  key_suffix="main_viz",
745
  )
pirads_inference.ipynb ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "dfc57266",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import argparse\n",
11
+ "import json\n",
12
+ "import logging\n",
13
+ "import os\n",
14
+ "import shutil\n",
15
+ "import sys\n",
16
+ "import time\n",
17
+ "from pathlib import Path\n",
18
+ "\n",
19
+ "import numpy as np\n",
20
+ "import torch\n",
21
+ "import wandb\n",
22
+ "import yaml\n",
23
+ "from monai.utils import set_determinism\n",
24
+ "from sklearn.preprocessing import StandardScaler\n",
25
+ "from torch.utils.tensorboard import SummaryWriter\n",
26
+ "\n",
27
+ "from src.data.data_loader import get_dataloader\n",
28
+ "from src.model.mil import MILModel3D\n",
29
+ "from src.train.train_pirads import train_epoch, val_epoch\n",
30
+ "from src.utils import save_pirads_checkpoint, setup_logging\n",
31
+ "from monai.metrics import Cumulative, CumulativeAverage\n",
32
+ "from sklearn.metrics import cohen_kappa_score\n",
33
+ "from tqdm import tqdm"
34
+ ]
35
+ },
36
+ {
37
+ "cell_type": "code",
38
+ "execution_count": 2,
39
+ "id": "b1264f17",
40
+ "metadata": {},
41
+ "outputs": [],
42
+ "source": [
43
+ "import yaml\n",
44
+ "from argparse import Namespace\n",
45
+ "\n",
46
+ "# 1. Read the YAML file into a dictionary\n",
47
+ "with open('/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate/config/config_pirads_test.yaml', 'r') as file:\n",
48
+ " config_dict = yaml.safe_load(file)\n",
49
+ "\n",
50
+ "# 2. Convert the dictionary directly to a Namespace\n",
51
+ "args = Namespace(**config_dict)"
52
+ ]
53
+ },
54
+ {
55
+ "cell_type": "code",
56
+ "execution_count": 3,
57
+ "id": "6b12b122",
58
+ "metadata": {},
59
+ "outputs": [],
60
+ "source": [
61
+ "args.project_dir = '/sc-scratch/sc-scratch-cc06-ag-ki-radiologie/prostate_foundation/WSAttention-Prostate'\n",
62
+ "args.run_name = 'pirads_inf'\n",
63
+ "args.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
64
+ "args.batch_size = 4\n",
65
+ "args.workers = 0\n",
66
+ "\n",
67
+ "args.logdir = os.path.join(args.project_dir, \"logs\", args.run_name)\n",
68
+ "os.makedirs(args.logdir, exist_ok=True)\n",
69
+ "args.logfile = os.path.join(args.logdir, f\"{args.run_name}.log\")\n",
70
+ "setup_logging(args.logfile)\n",
71
+ "if args.device == torch.device(\"cpu\"):\n",
72
+ " args.amp = False"
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": 4,
78
+ "id": "bd3292b2",
79
+ "metadata": {},
80
+ "outputs": [
81
+ {
82
+ "name": "stderr",
83
+ "output_type": "stream",
84
+ "text": [
85
+ "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"
86
+ ]
87
+ }
88
+ ],
89
+ "source": [
90
+ "if args.device == torch.device(\"cuda\"):\n",
91
+ " torch.backends.cudnn.benchmark = True\n",
92
+ "\n",
93
+ "model = MILModel3D(num_classes=args.num_classes, mil_mode=args.mil_mode)\n",
94
+ "start_epoch = 0\n",
95
+ "best_acc = 0.0\n",
96
+ "checkpoint = torch.load(args.checkpoint, map_location=\"cpu\")\n",
97
+ "model.load_state_dict(checkpoint[\"state_dict\"])\n",
98
+ "\n",
99
+ "if \"epoch\" in checkpoint:\n",
100
+ " start_epoch = checkpoint[\"epoch\"]\n",
101
+ "if \"best_acc\" in checkpoint:\n",
102
+ " best_acc = checkpoint[\"best_acc\"]\n",
103
+ "logging.info(\n",
104
+ " \"=> loaded checkpoint %s (epoch %d) (bestacc %f)\",\n",
105
+ " args.checkpoint,\n",
106
+ " start_epoch,\n",
107
+ " best_acc,\n",
108
+ ")\n",
109
+ "cache_dir_ = os.path.join(args.logdir, \"cache\")\n",
110
+ "model.to(args.device)\n",
111
+ "params = model.parameters()\n",
112
+ "\n",
113
+ "scaler = StandardScaler()\n",
114
+ "with open(os.path.join(args.project_dir, \"dataset\", \"PICAI_cspca_updated_with_psa_updated_vol.json\")) as f:\n",
115
+ " dataset_json = json.load(f)\n",
116
+ "train_clinical = [i[\"psa\"] for i in dataset_json[\"train\"]]\n",
117
+ "_ = scaler.fit_transform(train_clinical)\n",
118
+ "args.psa_mean = scaler.mean_.tolist()\n",
119
+ "args.psa_std = scaler.scale_.tolist()"
120
+ ]
121
+ },
122
+ {
123
+ "cell_type": "code",
124
+ "execution_count": 5,
125
+ "id": "e7ffa937",
126
+ "metadata": {},
127
+ "outputs": [
128
+ {
129
+ "name": "stderr",
130
+ "output_type": "stream",
131
+ "text": [
132
+ "0it [00:00, ?it/s]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",
133
+ "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",
134
+ "204it [10:13, 3.01s/it]\n"
135
+ ]
136
+ }
137
+ ],
138
+ "source": [
139
+ "args.num_seeds = 1\n",
140
+ "args.epochs = 0\n",
141
+ "seed = 56\n",
142
+ "set_determinism(seed=seed)\n",
143
+ "valid_loader = get_dataloader(args, split='test')\n",
144
+ "\n",
145
+ "preds_cumulative = Cumulative()\n",
146
+ "targets_cumulative = Cumulative()\n",
147
+ "\n",
148
+ "model.eval()\n",
149
+ "with torch.no_grad():\n",
150
+ " for idx, batch_data in tqdm(enumerate(valid_loader)):\n",
151
+ " data = batch_data[\"image\"].as_subclass(torch.Tensor).to(args.device)\n",
152
+ " target = batch_data[\"pirads\"].as_subclass(torch.Tensor).to(args.device)\n",
153
+ " target = target.long()\n",
154
+ "\n",
155
+ " with torch.amp.autocast(device_type=str(args.device), enabled=args.amp):\n",
156
+ " logits = model(data)\n",
157
+ "\n",
158
+ " data = data.to(\"cpu\")\n",
159
+ " target = target.to(\"cpu\")\n",
160
+ " logits = logits.to(\"cpu\")\n",
161
+ " pred = torch.argmax(logits, dim=1)\n",
162
+ "\n",
163
+ " preds_cumulative.extend(pred.detach().cpu())\n",
164
+ " targets_cumulative.extend(target.detach().cpu())\n",
165
+ "\n",
166
+ "\n",
167
+ " del data, target, logits\n",
168
+ " # torch.cuda.empty_cache()\n",
169
+ "\n",
170
+ " # Calculate QWK metric (Quadratic Weigted Kappa) https://en.wikipedia.org/wiki/Cohen%27s_kappa\n",
171
+ " preds_cumulative = preds_cumulative.get_buffer().cpu().numpy()\n",
172
+ " targets_cumulative = targets_cumulative.get_buffer().cpu().numpy()\n",
173
+ "\n",
174
+ "\n",
175
+ "\n",
176
+ "\n",
177
+ "if os.path.exists(cache_dir_):\n",
178
+ "\n",
179
+ " shutil.rmtree(cache_dir_)\n"
180
+ ]
181
+ },
182
+ {
183
+ "cell_type": "code",
184
+ "execution_count": 7,
185
+ "id": "5a544d4b",
186
+ "metadata": {},
187
+ "outputs": [],
188
+ "source": [
189
+ "np.save('pred.npy', preds_cumulative)\n",
190
+ "np.save('target.npy', targets_cumulative)"
191
+ ]
192
+ },
193
+ {
194
+ "cell_type": "code",
195
+ "execution_count": 6,
196
+ "id": "c854d497",
197
+ "metadata": {},
198
+ "outputs": [],
199
+ "source": [
200
+ "pred = preds_cumulative\n",
201
+ "targets = targets_cumulative"
202
+ ]
203
+ },
204
+ {
205
+ "cell_type": "code",
206
+ "execution_count": 7,
207
+ "id": "f1a42cbe",
208
+ "metadata": {},
209
+ "outputs": [],
210
+ "source": [
211
+ "targets_clipped = np.clip(targets, a_min = 2, a_max = 5)\n",
212
+ "target = targets_clipped - 2\n"
213
+ ]
214
+ },
215
+ {
216
+ "cell_type": "code",
217
+ "execution_count": 8,
218
+ "id": "868e3173",
219
+ "metadata": {},
220
+ "outputs": [
221
+ {
222
+ "data": {
223
+ "text/plain": [
224
+ "0.23748254253920043"
225
+ ]
226
+ },
227
+ "execution_count": 8,
228
+ "metadata": {},
229
+ "output_type": "execute_result"
230
+ }
231
+ ],
232
+ "source": [
233
+ "qwk = cohen_kappa_score(\n",
234
+ " target.astype(np.float64), pred.astype(np.float64)\n",
235
+ ")\n",
236
+ "qwk"
237
+ ]
238
+ },
239
+ {
240
+ "cell_type": "code",
241
+ "execution_count": 9,
242
+ "id": "4a496dc8",
243
+ "metadata": {},
244
+ "outputs": [
245
+ {
246
+ "data": {
247
+ "text/plain": [
248
+ "0.2053401834331301"
249
+ ]
250
+ },
251
+ "execution_count": 9,
252
+ "metadata": {},
253
+ "output_type": "execute_result"
254
+ }
255
+ ],
256
+ "source": [
257
+ "pred_mod = np.clip(pred, a_min = 0, a_max = 2)\n",
258
+ "target_mod = np.clip(target, a_min = 0, a_max = 2)\n",
259
+ "qwk = cohen_kappa_score(\n",
260
+ " target_mod.astype(np.float64), pred_mod.astype(np.float64)\n",
261
+ ")\n",
262
+ "qwk"
263
+ ]
264
+ },
265
+ {
266
+ "cell_type": "code",
267
+ "execution_count": 10,
268
+ "id": "50e3195b",
269
+ "metadata": {},
270
+ "outputs": [
271
+ {
272
+ "name": "stdout",
273
+ "output_type": "stream",
274
+ "text": [
275
+ "Counts for predicted classes [0, 1, 2, 3]:\n",
276
+ "[ 27 88 113 14]\n"
277
+ ]
278
+ }
279
+ ],
280
+ "source": [
281
+ "\n",
282
+ "preds_for_class_0 = pred[target == 0]\n",
283
+ "\n",
284
+ "\n",
285
+ "distribution = np.bincount(preds_for_class_0, minlength=4)\n",
286
+ "\n",
287
+ "print(\"Counts for predicted classes [0, 1, 2, 3]:\")\n",
288
+ "print(distribution)\n",
289
+ "\n"
290
+ ]
291
+ },
292
+ {
293
+ "cell_type": "code",
294
+ "execution_count": 11,
295
+ "id": "b812ce5c",
296
+ "metadata": {},
297
+ "outputs": [
298
+ {
299
+ "name": "stdout",
300
+ "output_type": "stream",
301
+ "text": [
302
+ "Counts for predicted classes [0, 1, 2, 3]:\n",
303
+ "[ 6 19 25 12]\n"
304
+ ]
305
+ }
306
+ ],
307
+ "source": [
308
+ "preds_for_class_0 = pred[target == 1]\n",
309
+ "\n",
310
+ "\n",
311
+ "distribution = np.bincount(preds_for_class_0, minlength=4)\n",
312
+ "\n",
313
+ "print(\"Counts for predicted classes [0, 1, 2, 3]:\")\n",
314
+ "print(distribution)"
315
+ ]
316
+ },
317
+ {
318
+ "cell_type": "code",
319
+ "execution_count": 12,
320
+ "id": "46c55552",
321
+ "metadata": {},
322
+ "outputs": [
323
+ {
324
+ "name": "stdout",
325
+ "output_type": "stream",
326
+ "text": [
327
+ "Counts for predicted classes [0, 1, 2, 3]:\n",
328
+ "[ 12 34 129 49]\n"
329
+ ]
330
+ }
331
+ ],
332
+ "source": [
333
+ "preds_for_class_0 = pred[target == 2]\n",
334
+ "\n",
335
+ "\n",
336
+ "distribution = np.bincount(preds_for_class_0, minlength=4)\n",
337
+ "\n",
338
+ "print(\"Counts for predicted classes [0, 1, 2, 3]:\")\n",
339
+ "print(distribution)"
340
+ ]
341
+ },
342
+ {
343
+ "cell_type": "code",
344
+ "execution_count": 13,
345
+ "id": "73283018",
346
+ "metadata": {},
347
+ "outputs": [
348
+ {
349
+ "name": "stdout",
350
+ "output_type": "stream",
351
+ "text": [
352
+ "Counts for predicted classes [0, 1, 2, 3]:\n",
353
+ "[ 3 16 87 182]\n"
354
+ ]
355
+ }
356
+ ],
357
+ "source": [
358
+ "preds_for_class_0 = pred[target == 3]\n",
359
+ "\n",
360
+ "\n",
361
+ "distribution = np.bincount(preds_for_class_0, minlength=4)\n",
362
+ "\n",
363
+ "print(\"Counts for predicted classes [0, 1, 2, 3]:\")\n",
364
+ "print(distribution)"
365
+ ]
366
+ },
367
+ {
368
+ "cell_type": "code",
369
+ "execution_count": null,
370
+ "id": "9cc17b5a",
371
+ "metadata": {},
372
+ "outputs": [],
373
+ "source": []
374
+ }
375
+ ],
376
+ "metadata": {
377
+ "kernelspec": {
378
+ "display_name": "foundation",
379
+ "language": "python",
380
+ "name": "python3"
381
+ },
382
+ "language_info": {
383
+ "codemirror_mode": {
384
+ "name": "ipython",
385
+ "version": 3
386
+ },
387
+ "file_extension": ".py",
388
+ "mimetype": "text/x-python",
389
+ "name": "python",
390
+ "nbconvert_exporter": "python",
391
+ "pygments_lexer": "ipython3",
392
+ "version": "3.9.21"
393
+ }
394
+ },
395
+ "nbformat": 4,
396
+ "nbformat_minor": 5
397
+ }
run_inference.py CHANGED
@@ -6,6 +6,7 @@ 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
@@ -19,7 +20,13 @@ 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
@@ -105,7 +112,7 @@ if __name__ == "__main__":
105
 
106
  scaler = StandardScaler()
107
  with open(
108
- os.path.join(args.project_dir, "dataset", "PICAI_cspca_updated_with_psa_updated_vol.json")
109
  ) as f:
110
  dataset_json = json.load(f)
111
  train_clinical = [i["psa"] for i in dataset_json["train"]]
@@ -169,10 +176,10 @@ if __name__ == "__main__":
169
  a = cspca_model.backbone.attention(x)
170
  a = torch.softmax(a, dim=1)
171
  a = a.view(-1)
172
- top5_values, top5_indices = torch.topk(a, 5)
173
 
174
  patches_top_5 = []
175
- for i in range(5):
176
  patch_temp = data[0, top5_indices.cpu().numpy()[i]][0].cpu().numpy()
177
  patches_top_5.append(patch_temp)
178
  patches_top_5_list.append(patches_top_5)
@@ -193,8 +200,18 @@ if __name__ == "__main__":
193
  "Predicted PIRAD Score": pirads_list[i] + 2.0,
194
  "csPCa risk": cspca_risk_list[i],
195
  "Prostate Volume": args.data_list[i]["psa"][1],
196
- "Top left coordinate of top 5 patches(x,y,z)": coords_list[i],
197
  }
198
 
 
 
 
 
 
 
 
 
 
 
199
  with open(os.path.join(args.output_dir, "results.json"), "w") as f:
200
  json.dump(output_dict, f, indent=4)
 
6
  from collections.abc import Callable
7
  from pathlib import Path
8
 
9
+ import nrrd
10
  import streamlit as st
11
  import torch
12
  import yaml
 
20
  from src.preprocessing.generate_heatmap import get_heatmap
21
  from src.preprocessing.prostate_mask import get_segmask
22
  from src.preprocessing.register_and_crop import register_files
23
+ from src.utils import (
24
+ create_additive_heatmap,
25
+ get_parent_image,
26
+ get_patch_coordinate,
27
+ get_prostate_volume,
28
+ setup_logging,
29
+ )
30
 
31
 
32
  @st.cache_resource
 
112
 
113
  scaler = StandardScaler()
114
  with open(
115
+ os.path.join(args.project_dir, "dataset", "cspca_train_tcia.json.json")
116
  ) as f:
117
  dataset_json = json.load(f)
118
  train_clinical = [i["psa"] for i in dataset_json["train"]]
 
176
  a = cspca_model.backbone.attention(x)
177
  a = torch.softmax(a, dim=1)
178
  a = a.view(-1)
179
+ top5_values, top5_indices = torch.topk(a, args.tile_count)
180
 
181
  patches_top_5 = []
182
+ for i in range(args.tile_count):
183
  patch_temp = data[0, top5_indices.cpu().numpy()[i]][0].cpu().numpy()
184
  patches_top_5.append(patch_temp)
185
  patches_top_5_list.append(patches_top_5)
 
200
  "Predicted PIRAD Score": pirads_list[i] + 2.0,
201
  "csPCa risk": cspca_risk_list[i],
202
  "Prostate Volume": args.data_list[i]["psa"][1],
203
+ "Top left coordinates of the patches(x,y,z)": coords_list[i],
204
  }
205
 
206
+ pmask, _ = nrrd.read(args.data_list[0]["smooth_mask"])
207
+ score_map = create_additive_heatmap(
208
+ coords_list[0],
209
+ top5_values.cpu().numpy(),
210
+ parent_image.shape,
211
+ (args.tile_size, args.tile_size, args.depth),
212
+ pmask,
213
+ )
214
+ nrrd.write(os.path.join(args.output_dir, "attention_weights.nrrd"), score_map)
215
+
216
  with open(os.path.join(args.output_dir, "results.json"), "w") as f:
217
  json.dump(output_dict, f, indent=4)
src/utils.py CHANGED
@@ -18,6 +18,7 @@ from monai.transforms import (
18
  LoadImaged,
19
  ToTensord,
20
  )
 
21
 
22
  from .data.custom_transforms import ClipMaskIntensityPercentilesd, NormalizeIntensity_customd
23
 
@@ -262,3 +263,34 @@ def get_prostate_volume(mask_path) -> np.ndarray:
262
  true_volume_cc = true_volume_mm3 / 1000.0 # Convert mm³ to cc (mL)
263
 
264
  return true_volume_cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  LoadImaged,
19
  ToTensord,
20
  )
21
+ from scipy.ndimage import gaussian_filter
22
 
23
  from .data.custom_transforms import ClipMaskIntensityPercentilesd, NormalizeIntensity_customd
24
 
 
263
  true_volume_cc = true_volume_mm3 / 1000.0 # Convert mm³ to cc (mL)
264
 
265
  return true_volume_cc
266
+
267
+
268
+ def create_additive_heatmap(
269
+ patch_coords, attention_scores, volume_shape, patch_size, pmask, apply_blur=True
270
+ ):
271
+ """
272
+ Sums attention scores in overlapping regions.
273
+ """
274
+ heatmap = np.zeros(volume_shape, dtype=np.float32)
275
+ dz, dy, dx = patch_size
276
+ for (z, y, x), score in zip(patch_coords, attention_scores):
277
+ # Define boundaries starting from top-left corner
278
+ z_end = min(volume_shape[0], z + dz)
279
+ y_end = min(volume_shape[1], y + dy)
280
+ x_end = min(volume_shape[2], x + dx)
281
+
282
+ # Add scores to the region
283
+ heatmap[z:z_end, y:y_end, x:x_end] += score
284
+ """
285
+ heatmap[z:z_end, y:y_end, x:x_end] = np.maximum(
286
+ heatmap[z:z_end, y:y_end, x:x_end], score
287
+ )
288
+ """
289
+ heatmap_masked = heatmap # * (pmask > 0)
290
+ if heatmap_masked.max() > 0:
291
+ heatmap_masked /= heatmap_masked.max()
292
+
293
+ if apply_blur:
294
+ heatmap = gaussian_filter(heatmap_masked, sigma=1.0)
295
+
296
+ return heatmap