Siyad798 commited on
Commit
732f331
·
verified ·
1 Parent(s): 270abf2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +191 -62
app.py CHANGED
@@ -1,77 +1,206 @@
1
  import os
2
  import shutil
 
 
 
 
 
3
  import gradio as gr
4
- from nnunet.inference.predict import predict_from_folder
5
- from nnunet.paths import default_plans_identifier, network_training_output_dir, preprocessing_output_dir
 
 
 
 
 
6
 
 
7
  # 1. CONFIGURATION
8
- # We have to "trick" nnU-Net into finding the model in the standard folder structure
9
- # because it relies on strict paths.
10
-
11
- # Set environment variables required by nnU-Net
12
- os.environ['nnUNet_raw_data_base'] = '/tmp/nnUNet_raw_data_base'
13
- os.environ['nnUNet_preprocessed'] = '/tmp/nnUNet_preprocessed'
14
- os.environ['RESULTS_FOLDER'] = '/tmp/nnUNet_trained_models'
15
-
16
- # Function to setup the "Fake" directory structure nnU-Net expects
17
- def setup_model_folder(uploaded_model_path):
18
- target_folder = "/tmp/nnUNet_trained_models/nnUNet/3d_fullres/Task012_Ischemic_Stroke_TM_Fullset/nnUNetTrainerV2_DDP__nnUNetPlansv2.1/fold_0"
19
- os.makedirs(target_folder, exist_ok=True)
20
-
21
- # Move the uploaded model to the expected location
22
- # Note: nnU-Net expects 'model_best.model' and 'model_best.model.pkl'
23
- # You might need to upload both or ZIP them.
24
- # For this script, we assume you uploaded a ZIP of the 'fold_0' folder.
25
- import zipfile
26
- with zipfile.ZipFile(uploaded_model_path, 'r') as zip_ref:
27
- zip_ref.extractall(target_folder)
28
-
29
- return target_folder
30
-
31
- # 2. INFERENCE LOGIC
32
- def segment_stroke(dwi, adc, model_zip):
33
- # Setup Input Folder
34
- input_folder = "/tmp/input_data"
35
- output_folder = "/tmp/output_prediction"
36
- os.makedirs(input_folder, exist_ok=True)
37
- os.makedirs(output_folder, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- # nnU-Net expects specific naming: CASE_0000.nii.gz, CASE_0001.nii.gz
40
- # We rename our inputs to match what the model was trained on (Channel 0=DWI, Channel 1=ADC)
41
- # Note: You must check the DeepISLES readme for channel order. Assuming DWI=0000, ADC=0001.
42
- shutil.copy(dwi.name, os.path.join(input_folder, "test_0000.nii.gz"))
43
- shutil.copy(adc.name, os.path.join(input_folder, "test_0001.nii.gz"))
44
 
45
- # Setup Model
46
- setup_model_folder(model_zip.name)
 
 
 
47
 
48
- # Run Inference
49
- # This calls the internal nnU-Net prediction function
50
- # -i input, -o output, -t Task ID, -m Model Type, -f fold
51
- command = f"nnUNet_predict -i {input_folder} -o {output_folder} -t 012 -m 3d_fullres -f 0"
52
- os.system(command)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- # Find Output
55
- output_file = os.path.join(output_folder, "test.nii.gz")
56
- if os.path.exists(output_file):
57
- return output_file
58
- else:
59
- return "Error: Segmentation failed."
60
-
61
- # 3. UI
62
- with gr.Blocks() as demo:
63
- gr.Markdown("# 🧠 DeepISLES (SEALS Node)")
64
- gr.Markdown("Upload DWI + ADC and the 'fold_0.zip' model weights.")
 
65
 
66
- with gr.Row():
67
- dwi_in = gr.File(label="DWI Scan")
68
- adc_in = gr.File(label="ADC Scan")
69
- model_in = gr.File(label="Model Weights (Zip of fold_0 folder)")
 
 
 
70
 
71
- btn = gr.Button("Segment")
72
- out = gr.File(label="Lesion Mask")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- btn.click(segment_stroke, inputs=[dwi_in, adc_in, model_in], outputs=out)
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  if __name__ == "__main__":
77
  demo.launch()
 
1
  import os
2
  import shutil
3
+ import zipfile
4
+ import torch
5
+ import numpy as np
6
+ import nibabel as nib
7
+ import matplotlib.pyplot as plt
8
  import gradio as gr
9
+ from monai.networks.nets import SwinUNETR
10
+ from monai.inferers import sliding_window_inference
11
+ from monai.transforms import (
12
+ Compose, LoadImaged, EnsureChannelFirstd, Spacingd,
13
+ Orientationd, ScaleIntensityd, ToTensord, MapTransform,
14
+ Resized, Resize
15
+ )
16
 
17
+ # ==========================================
18
  # 1. CONFIGURATION
19
+ # ==========================================
20
+ MODEL_FILE = "vision_agent_best.pth"
21
+ device = torch.device("cpu") # Use CPU for Free Tier Spaces
22
+
23
+ print("--- 👁️ Initializing Vision Agent ---")
24
+
25
+ # Initialize Model (Must match training architecture: 2 Channel IN -> 2 Channel OUT)
26
+ model = SwinUNETR(
27
+ img_size=(96, 96, 96),
28
+ in_channels=2,
29
+ out_channels=2,
30
+ feature_size=48,
31
+ use_checkpoint=False,
32
+ spatial_dims=3
33
+ ).to(device)
34
+
35
+ # Load Weights
36
+ if os.path.exists(MODEL_FILE):
37
+ try:
38
+ state_dict = torch.load(MODEL_FILE, map_location=device)
39
+ model.load_state_dict(state_dict)
40
+ model.eval()
41
+ print("✅ Model Loaded Successfully.")
42
+ except Exception as e:
43
+ print(f"❌ Error loading weights: {e}")
44
+ else:
45
+ print(f"⚠️ Warning: {MODEL_FILE} not found. Upload it to Files.")
46
+
47
+ # ==========================================
48
+ # 2. SMART FILE HANDLER (Matches Gatekeeper Logic)
49
+ # ==========================================
50
+ def handle_uploads(file_paths):
51
+ """Flattens zips and lists into single file paths."""
52
+ if not file_paths: return []
53
+ processed_files = []
54
+ extract_root = "/tmp/vision_uploads"
55
+ if os.path.exists(extract_root): shutil.rmtree(extract_root)
56
+ os.makedirs(extract_root, exist_ok=True)
57
+
58
+ for path in file_paths:
59
+ if path.endswith(".zip"):
60
+ try:
61
+ with zipfile.ZipFile(path, 'r') as zip_ref:
62
+ zip_ref.extractall(extract_root)
63
+ for root, _, files in os.walk(extract_root):
64
+ for file in files:
65
+ if not file.startswith("."):
66
+ processed_files.append(os.path.join(root, file))
67
+ except: pass
68
+ else:
69
+ processed_files.append(path)
70
+ return processed_files
71
+
72
+ def find_sequences(file_list):
73
+ """Scans inputs to find DWI and ADC automatically."""
74
+ all_files = handle_uploads(file_list)
75
+ dwi_path = None
76
+ adc_path = None
77
 
78
+ print(f"Scanning {len(all_files)} files for DWI/ADC...")
 
 
 
 
79
 
80
+ for path in all_files:
81
+ fname = os.path.basename(path).lower()
82
+ # Heuristic matching
83
+ if "dwi" in fname: dwi_path = path
84
+ elif "adc" in fname: adc_path = path
85
 
86
+ return dwi_path, adc_path
87
+
88
+ # ==========================================
89
+ # 3. PREPROCESSING PIPELINE
90
+ # ==========================================
91
+ class HandleMissingADCd(MapTransform):
92
+ def __call__(self, data):
93
+ # If ADC is missing (flagged in data dict), zero it out
94
+ if data.get("missing_adc", False):
95
+ data["image"][1] = data["image"][1] * 0.0
96
+ return data
97
+
98
+ preprocess = Compose([
99
+ LoadImaged(keys=["image"]),
100
+ EnsureChannelFirstd(keys=["image"]),
101
+ HandleMissingADCd(keys=["image"]),
102
+ ScaleIntensityd(keys=["image"]),
103
+ Orientationd(keys=["image"], axcodes="RAS"),
104
+ Spacingd(keys=["image"], pixdim=(1.5, 1.5, 1.5), mode=("bilinear")),
105
+ Resized(keys=["image"], spatial_size=(96, 96, 96), mode=("bilinear")),
106
+ ToTensord(keys=["image"]),
107
+ ])
108
+
109
+ def generate_overlay(original_dwi_path, mask_data):
110
+ """Visualizes the stroke slice."""
111
+ img = nib.load(original_dwi_path)
112
+ dwi_vol = img.get_fdata()
113
+ dwi_vol = np.rot90(dwi_vol, k=1, axes=(0, 1))
114
+ mask_vol = np.rot90(mask_data, k=1, axes=(0, 1))
115
+
116
+ # Find slice with largest lesion
117
+ slice_sums = np.sum(mask_vol, axis=(0, 1))
118
+ max_slice = np.argmax(slice_sums) if np.max(slice_sums) > 0 else dwi_vol.shape[2] // 2
119
+
120
+ fig, ax = plt.subplots(figsize=(5, 5))
121
+ ax.imshow(dwi_vol[:, :, max_slice], cmap="gray")
122
+ if np.max(mask_vol) > 0:
123
+ mask_display = np.zeros((*mask_vol.shape[:2], 4))
124
+ mask_display[mask_vol[:, :, max_slice] > 0] = [1, 0, 0, 0.6] # Red
125
+ ax.imshow(mask_display)
126
 
127
+ ax.axis("off")
128
+ output_path = "/tmp/lesion_overlay.png"
129
+ plt.savefig(output_path, bbox_inches='tight')
130
+ plt.close()
131
+ return output_path
132
+
133
+ # ==========================================
134
+ # 4. INFERENCE ENGINE
135
+ # ==========================================
136
+ def segment_stroke(file_list):
137
+ # 1. Find Data
138
+ dwi_path, adc_path = find_sequences(file_list)
139
 
140
+ if not dwi_path:
141
+ return {"error": "CRITICAL: No DWI sequence found."}, None
142
+
143
+ # 2. Prepare Input
144
+ # If ADC is missing, duplicate DWI but flag it for zero-filling
145
+ img_list = [dwi_path, adc_path] if adc_path else [dwi_path, dwi_path]
146
+ input_data = {"image": img_list, "missing_adc": not bool(adc_path)}
147
 
148
+ try:
149
+ # 3. Preprocess
150
+ tensor_data = preprocess(input_data)["image"].unsqueeze(0).to(device)
151
+
152
+ # 4. Inference
153
+ with torch.no_grad():
154
+ output = model(tensor_data)
155
+ pred_mask_small = torch.argmax(output, dim=1).cpu().numpy()[0]
156
+
157
+ # 5. Volume Recalculation (Unsquash)
158
+ original_img = nib.load(dwi_path)
159
+ resizer = Resize(spatial_size=original_img.shape, mode="nearest")
160
+ pred_mask_original = resizer(torch.from_numpy(pred_mask_small).unsqueeze(0))[0].numpy()
161
+
162
+ # 6. Calc Volume
163
+ voxel_vol_mm3 = np.prod(original_img.header.get_zooms())
164
+ lesion_voxels = np.count_nonzero(pred_mask_original)
165
+ volume_ml = (lesion_voxels * voxel_vol_mm3) / 1000.0
166
+
167
+ # 7. Report
168
+ risk = "LOW"
169
+ if volume_ml > 70: risk = "HIGH (Futile?)"
170
+ elif volume_ml > 50: risk = "MODERATE"
171
+
172
+ result = {
173
+ "status": "SUCCESS",
174
+ "core_volume_ml": round(volume_ml, 2),
175
+ "risk_category": risk,
176
+ "modality_status": "DWI+ADC" if adc_path else "DWI Only (Approximation)"
177
+ }
178
+
179
+ return result, generate_overlay(dwi_path, pred_mask_original)
180
+
181
+ except Exception as e:
182
+ return {"error": str(e)}, None
183
+
184
+ # ==========================================
185
+ # 5. GRADIO UI
186
+ # ==========================================
187
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
188
+ gr.Markdown("# 👁️ Neuro-Triage: Vision Agent")
189
 
190
+ with gr.Row():
191
+ with gr.Column():
192
+ file_input = gr.File(
193
+ label="Upload Patient Data",
194
+ file_count="multiple",
195
+ type="filepath"
196
+ )
197
+ btn = gr.Button("Calculate Core Volume", variant="primary")
198
+
199
+ with gr.Column():
200
+ out_json = gr.JSON(label="Volumetric Report")
201
+ out_img = gr.Image(label="Lesion Map")
202
+
203
+ btn.click(segment_stroke, inputs=file_input, outputs=[out_json, out_img])
204
 
205
  if __name__ == "__main__":
206
  demo.launch()