Spaces:
Sleeping
Sleeping
Anirudh Balaraman commited on
Commit ·
8d661c2
1
Parent(s): cc28ce1
update viewer
Browse files- app.py +120 -3
- pirads_inference.ipynb +397 -0
- run_inference.py +22 -5
- 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 |
-
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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", "
|
| 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,
|
| 173 |
|
| 174 |
patches_top_5 = []
|
| 175 |
-
for i in range(
|
| 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
|
| 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
|