Siyad798 commited on
Commit
2bafb6c
·
verified ·
1 Parent(s): 026d057

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +51 -39
app.py CHANGED
@@ -7,6 +7,7 @@ import torchvision
7
  import numpy as np
8
  import scipy.ndimage as ndimage
9
  import gradio as gr
 
10
  from monai.transforms import (
11
  Compose, LoadImaged, EnsureChannelFirstd,
12
  Resized, ScaleIntensityd, ToTensord, Lambdad,
@@ -47,19 +48,14 @@ class GatekeeperService:
47
  else:
48
  print(f"⚠️ Model file '{model_path}' not found.")
49
 
50
- # --- AI PREPROCESSING (MATCHING TRAINING EXACTLY) ---
51
- # We revert to ScaleIntensityd (Min/Max) because that is what the model learned.
52
  self.transforms = Compose([
53
  LoadImaged(keys=["image"]),
54
  EnsureChannelFirstd(keys=["image"]),
55
  Orientationd(keys=["image"], axcodes="RAS"),
56
- # We enforce the same spacing used in training (likely 1.5mm iso)
57
  Spacingd(keys=["image"], pixdim=(1.5, 1.5, 1.5), mode=("bilinear")),
58
  Lambdad(keys=["image"], func=lambda x: x[0:1]),
59
-
60
- # FIX: Reverted to Simple Scaling to match Training Data distribution
61
  ScaleIntensityd(keys=["image"]),
62
-
63
  Resized(keys=["image"], spatial_size=(96, 96, 96)),
64
  ToTensord(keys=["image"])
65
  ])
@@ -69,7 +65,6 @@ class GatekeeperService:
69
  swi_path = None
70
  if not file_paths: return None, None
71
 
72
- print(f"Scanning {len(file_paths)} uploaded files...")
73
  for path in file_paths:
74
  filename = os.path.basename(path).lower()
75
  if "flair" in filename or "t2_flair" in filename:
@@ -80,32 +75,51 @@ class GatekeeperService:
80
 
81
  def physics_check_hemorrhage(self, swi_path):
82
  """
83
- Independent Logic: Uses its own robust normalization logic
84
- separate from the AI pipeline.
85
  """
86
  if not swi_path: return False, 0.0
87
  try:
88
- loader = LoadImaged(keys=["swi"])
89
- data = loader({"swi": swi_path})
90
- vol = data["swi"].numpy()
91
 
92
- # ROBUST NORMALIZATION (For Physics Only)
93
- # Use percentiles to handle outliers in raw hospital data
94
  p99 = np.percentile(vol, 99)
95
  if p99 > 0: vol = vol / p99
96
- vol = np.clip(vol, 0, 1)
97
 
98
- # Physics Rule: Signal Void < 0.05
99
- dark_mask = vol < 0.05
 
 
 
 
100
  labeled_array, num_features = ndimage.label(dark_mask)
101
- voxel_vol = 1.5**3
 
 
 
 
 
102
 
103
  for i in range(1, num_features+1):
104
- size = np.sum(labeled_array == i) * voxel_vol
105
- # Size Check: 2mL - 200mL
106
- if 2000 < size < 200000:
107
- return True, size
 
 
 
 
 
 
 
 
 
 
 
108
  return False, 0.0
 
109
  except Exception as e:
110
  print(f"Physics Check Failed: {e}")
111
  return False, 0.0
@@ -125,15 +139,17 @@ class GatekeeperService:
125
  response["reasoning"].append("CRITICAL: No FLAIR sequence found.")
126
  return response
127
 
128
- # 1. Physics Check
129
  if swi_path:
130
  is_bleed, size = self.physics_check_hemorrhage(swi_path)
131
  if is_bleed:
132
  response["status"] = "COMPLETED"
133
  response["triage_category"] = "HEMORRHAGE"
134
  response["next_agent"] = "REPORT_AGENT"
135
- response["reasoning"].append(f"Physics Rule: Signal void ({size:.0f}mm3) detected on SWI.")
136
  return response
 
 
137
 
138
  # 2. AI Prediction
139
  try:
@@ -144,21 +160,20 @@ class GatekeeperService:
144
  output = self.model(tensor_data)
145
  probs = torch.softmax(output, dim=1)
146
 
147
- # CONFIDENCE THRESHOLDING LOGIC
148
- # If "Ischemic" is the 2nd choice but very close to "Tumor",
149
- # we should prioritize Ischemic for safety (Stroke Alert).
150
- ischemic_idx = self.classes.index("ischemic")
151
- ischemic_prob = probs[0, ischemic_idx].item()
152
-
153
  pred_idx = torch.argmax(probs, dim=1).item()
154
  confidence = probs[0, pred_idx].item()
155
 
156
- # Override Logic: If Model is unsure (Tumor vs Ischemic) but Ischemic > 20%, alert Stroke.
157
- # It is safer to send a Tumor to the Stroke Vision Agent (which will find 0 volume)
158
- # than to send a Stroke home.
159
- if self.classes[pred_idx] == "tumor" and ischemic_prob > 0.20:
 
 
 
160
  diagnosis = "ischemic"
161
- response["reasoning"].append(f"AI Alert: Possible Stroke pattern ({ischemic_prob:.2%}). Prioritizing over Tumor signal.")
162
  else:
163
  diagnosis = self.classes[pred_idx]
164
 
@@ -184,7 +199,7 @@ class GatekeeperService:
184
  return response
185
 
186
  # ==========================================
187
- # 3. INITIALIZATION
188
  # ==========================================
189
  MODEL_FILE = "gatekeeper_hybrid_trained.pth"
190
  agent = GatekeeperService(MODEL_FILE)
@@ -192,9 +207,6 @@ agent = GatekeeperService(MODEL_FILE)
192
  def predict_mri_protocol(file_list):
193
  return agent.process_batch(file_list)
194
 
195
- # ==========================================
196
- # 4. GRADIO UI
197
- # ==========================================
198
  with gr.Blocks() as demo:
199
  gr.Markdown("# 🧠 Neuro-Triage: Gatekeeper Agent")
200
 
 
7
  import numpy as np
8
  import scipy.ndimage as ndimage
9
  import gradio as gr
10
+ import nibabel as nib # Added for direct raw loading
11
  from monai.transforms import (
12
  Compose, LoadImaged, EnsureChannelFirstd,
13
  Resized, ScaleIntensityd, ToTensord, Lambdad,
 
48
  else:
49
  print(f"⚠️ Model file '{model_path}' not found.")
50
 
51
+ # --- AI PIPELINE (PREPROCESSED) ---
 
52
  self.transforms = Compose([
53
  LoadImaged(keys=["image"]),
54
  EnsureChannelFirstd(keys=["image"]),
55
  Orientationd(keys=["image"], axcodes="RAS"),
 
56
  Spacingd(keys=["image"], pixdim=(1.5, 1.5, 1.5), mode=("bilinear")),
57
  Lambdad(keys=["image"], func=lambda x: x[0:1]),
 
 
58
  ScaleIntensityd(keys=["image"]),
 
59
  Resized(keys=["image"], spatial_size=(96, 96, 96)),
60
  ToTensord(keys=["image"])
61
  ])
 
65
  swi_path = None
66
  if not file_paths: return None, None
67
 
 
68
  for path in file_paths:
69
  filename = os.path.basename(path).lower()
70
  if "flair" in filename or "t2_flair" in filename:
 
75
 
76
  def physics_check_hemorrhage(self, swi_path):
77
  """
78
+ FIXED: Uses RAW nibabel loading to avoid interpolation artifacts.
 
79
  """
80
  if not swi_path: return False, 0.0
81
  try:
82
+ # 1. Load RAW Data (No Monai interpolation)
83
+ img_obj = nib.load(swi_path)
84
+ vol = img_obj.get_fdata()
85
 
86
+ # 2. Robust Normalization
87
+ # Map 0-99th percentile to 0-1
88
  p99 = np.percentile(vol, 99)
89
  if p99 > 0: vol = vol / p99
 
90
 
91
+ # 3. Physics Rule
92
+ # Since we made a "Black Hole", pixels are exactly 0.0 or close to it.
93
+ # We assume anything < 2% intensity is a void (Blood or Air).
94
+ dark_mask = vol < 0.02
95
+
96
+ # 4. Cluster Analysis
97
  labeled_array, num_features = ndimage.label(dark_mask)
98
+
99
+ # Use raw voxel size from header for accuracy
100
+ zooms = img_obj.header.get_zooms()
101
+ voxel_vol = zooms[0] * zooms[1] * zooms[2]
102
+
103
+ largest_bleed_size = 0
104
 
105
  for i in range(1, num_features+1):
106
+ # Count voxels in this cluster
107
+ voxel_count = np.sum(labeled_array == i)
108
+ size_mm3 = voxel_count * voxel_vol
109
+
110
+ # Logic:
111
+ # - Background Air is huge (> 500,000 mm3)
112
+ # - Noise is small (< 500 mm3)
113
+ # - Bleed is in between (500 mm3 - 300,000 mm3)
114
+ if 500 < size_mm3 < 300000:
115
+ if size_mm3 > largest_bleed_size:
116
+ largest_bleed_size = size_mm3
117
+
118
+ if largest_bleed_size > 0:
119
+ return True, largest_bleed_size
120
+
121
  return False, 0.0
122
+
123
  except Exception as e:
124
  print(f"Physics Check Failed: {e}")
125
  return False, 0.0
 
139
  response["reasoning"].append("CRITICAL: No FLAIR sequence found.")
140
  return response
141
 
142
+ # 1. Physics Check (Priority 1)
143
  if swi_path:
144
  is_bleed, size = self.physics_check_hemorrhage(swi_path)
145
  if is_bleed:
146
  response["status"] = "COMPLETED"
147
  response["triage_category"] = "HEMORRHAGE"
148
  response["next_agent"] = "REPORT_AGENT"
149
+ response["reasoning"].append(f"Physics Rule: Signal void ({size:.0f}mm3) detected on SWI. Thrombolysis Contraindicated.")
150
  return response
151
+ else:
152
+ response["reasoning"].append("Physics Rule: SWI scan clear (No active bleed detected).")
153
 
154
  # 2. AI Prediction
155
  try:
 
160
  output = self.model(tensor_data)
161
  probs = torch.softmax(output, dim=1)
162
 
163
+ # BIAS CORRECTION:
164
+ # If model is unsure between Tumor/Ischemic, prioritize Ischemic for safety.
 
 
 
 
165
  pred_idx = torch.argmax(probs, dim=1).item()
166
  confidence = probs[0, pred_idx].item()
167
 
168
+ # Get probabilities
169
+ p_ischemic = probs[0, self.classes.index("ischemic")].item()
170
+ p_tumor = probs[0, self.classes.index("tumor")].item()
171
+
172
+ # Override: If Tumor is predicted but Ischemic is > 20%, force Ischemic Alert.
173
+ # Reason: Better to send a Tumor to Vision Agent (Safety) than miss a Stroke.
174
+ if self.classes[pred_idx] == "tumor" and p_ischemic > 0.20:
175
  diagnosis = "ischemic"
176
+ response["reasoning"].append(f"AI Alert: Complex pattern. Prioritizing Stroke (Prob: {p_ischemic:.2%}) over Tumor to ensure safety.")
177
  else:
178
  diagnosis = self.classes[pred_idx]
179
 
 
199
  return response
200
 
201
  # ==========================================
202
+ # 3. INITIALIZATION & UI
203
  # ==========================================
204
  MODEL_FILE = "gatekeeper_hybrid_trained.pth"
205
  agent = GatekeeperService(MODEL_FILE)
 
207
  def predict_mri_protocol(file_list):
208
  return agent.process_batch(file_list)
209
 
 
 
 
210
  with gr.Blocks() as demo:
211
  gr.Markdown("# 🧠 Neuro-Triage: Gatekeeper Agent")
212