mqadir commited on
Commit
0b1229a
·
verified ·
1 Parent(s): c3001e8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -89
app.py CHANGED
@@ -7,9 +7,11 @@ import nibabel as nib
7
  import traceback
8
  import tempfile
9
  import torch
 
 
 
 
10
 
11
- # GLOBAL SETUP & MODEL INITIALIZATION
12
- # Set dummy environment variables to prevent nnU-Net path errors in HF Spaces
13
  os.environ['nnUNet_raw'] = './tmp/nnUNet_raw'
14
  os.environ['nnUNet_results'] = './model_weights'
15
  os.environ['nnUNet_preprocessed'] = './tmp/nnUNet_preprocessed'
@@ -19,158 +21,187 @@ os.makedirs('./tmp/nnUNet_preprocessed', exist_ok=True)
19
  from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
20
  from batchgenerators.utilities.file_and_folder_operations import join
21
 
22
- # Dynamically assign device (prevents crashes if deployed on a CPU space)
23
  device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
24
- print(f"Initializing nnU-Net on device: {device}")
25
-
26
  try:
27
  predictor = nnUNetPredictor(
28
- tile_step_size=0.5,
29
- use_gaussian=True,
30
- use_mirroring=True,
31
- perform_everything_on_device=True,
32
- device=device,
33
- verbose=False,
34
- verbose_preprocessing=False,
35
- allow_tqdm=True
36
  )
37
-
38
- # UPDATE THIS PATH: Ensure this matches the folder structure in your HF Space repository
39
  model_folder = './model_weights/Dataset998_PanTSTen/nnUNetTrainer__nnUNetPlans__3d_fullres'
40
-
41
  predictor.initialize_from_trained_model_folder(
42
- model_folder,
43
- use_folds=('all',),
44
- checkpoint_name='checkpoint_best.pth',
45
  )
46
- print("nnU-Net loaded successfully.")
47
  except Exception as e:
48
  predictor = None
49
- print(f"Warning: nnU-Net failed to load. Ensure 'model_weights' folder exists. Error: {e}")
50
 
51
  try:
52
  risk_model = joblib.load("model.joblib")
53
  except FileNotFoundError:
54
  risk_model = None
55
 
56
- # MAIN PROCESSING PIPELINE
57
 
58
- def process_scan(nifti_file, no_chemo, pd_surgery):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  if nifti_file is None:
60
- return gr.update(value="**Error:** Please upload a NIfTI file.", visible=True), *[gr.update(value="")]*4, gr.update(value="")
61
 
62
  try:
63
- # Use Python's tempfile to handle temporary folders automatically
64
  with tempfile.TemporaryDirectory() as temp_in_dir, tempfile.TemporaryDirectory() as temp_out_dir:
65
-
66
- # File Validation & Renaming
67
  original_path = nifti_file.name
68
  base_name = os.path.basename(original_path)
69
 
70
- # Enforce nnU-Net's _0000 naming convention
71
  if not base_name.endswith("_0000.nii.gz") and base_name.endswith(".nii.gz"):
72
  new_base = base_name.replace(".nii.gz", "_0000.nii.gz")
73
- elif not base_name.endswith("_0000.nii") and base_name.endswith(".nii"):
74
- new_base = base_name.replace(".nii", "_0000.nii")
75
  else:
76
  new_base = base_name
77
 
78
- # Copy the single uploaded file into the temporary input directory
79
  input_file_path = os.path.join(temp_in_dir, new_base)
80
  shutil.copy(original_path, input_file_path)
81
 
82
- # Segmentation (nnU-Net)
83
  if predictor is None:
84
- raise RuntimeError("nnU-Net predictor was not initialized properly at startup.")
85
 
86
- # Predict from the temporary input folder to the temporary output folder
87
  predictor.predict_from_files(
88
- temp_in_dir,
89
- temp_out_dir,
90
- save_probabilities=False,
91
- overwrite=False,
92
- num_processes_preprocessing=2,
93
- num_processes_segmentation_export=2,
94
- folder_with_segs_from_prev_stage=None,
95
- num_parts=1,
96
- part_id=0
97
  )
98
 
99
  output_base = new_base.replace("_0000", "")
100
  output_mask_path = os.path.join(temp_out_dir, output_base)
101
 
102
  if not os.path.exists(output_mask_path):
103
- raise FileNotFoundError("nnU-Net failed to generate the output mask.")
104
-
105
- # --- Step 3: Feature Extraction ---
106
- # TODO: Add your custom extraction logic here
107
- # mask_img = nib.load(output_mask_path)
108
- # mask_data = mask_img.get_fdata()
109
- # mac, vev, aev, ttv = extract_features(mask_data, original_scan_data)
110
-
111
- # Dummy values for UI demonstration
112
- mac, vev, aev, ttv = 18.4, 62.0, 4.2, 1.8
113
 
114
- # Risk Prediction
115
- if risk_model is not None:
116
- features = np.array([[mac, vev, aev, ttv, int(no_chemo), int(pd_surgery)]])
117
- prob_r1 = risk_model.predict_proba(features)[0][1]
118
- else:
119
- prob_r1 = 0.72 # Dummy value
120
-
121
- # Format Output
122
- risk_percentage = prob_r1 * 100
123
- risk_color = "red" if risk_percentage > 50 else "green"
124
- risk_display = f"<h2 style='color: {risk_color}; margin-top: 0;'>Predicted probability of R1 resection: {risk_percentage:.1f}%</h2>"
125
 
126
- return (
127
- gr.update(value="", visible=False),
128
- f"{mac:.1f}", f"{vev:.1f}", f"{aev:.1f}", f"{ttv:.1f}",
129
- risk_display
130
- )
131
 
 
 
 
 
 
 
 
 
132
  except Exception as e:
133
- error_msg = f"**Pipeline Error:** {str(e)}\n\n```\n{traceback.format_exc()}\n```"
134
  return gr.update(value=error_msg, visible=True), *[gr.update(value="")]*4, gr.update(value="")
135
 
136
- # GRADIO UI LAYOUT
137
 
 
138
 
139
  with gr.Blocks() as demo:
140
  gr.Markdown("# Preoperative R1 Resection Risk Predictor")
141
 
142
  with gr.Row():
 
143
  with gr.Column(scale=1):
144
- file_input = gr.File(label="Drop your NIfTI file here", file_types=[".nii.gz", ".nii"], file_count="single")
145
 
146
- gr.Markdown("### Clinical Metadata")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  no_chemo = gr.Checkbox(label="Absence of Neoadjuvant Chemo (1/0)")
148
  pd_surgery = gr.Checkbox(label="Patient underwent PD (Pancreaticoduodenectomy) (1/0)")
149
 
150
- analyze_btn = gr.Button("Run Analysis", variant="primary")
151
-
152
  with gr.Column(scale=2):
153
  error_box = gr.Markdown(visible=False)
154
 
155
  gr.Markdown("### Segmentation Metrics")
156
  with gr.Row():
157
- mac_out = gr.Textbox(label="MAC", interactive=False)
158
- vev_out = gr.Textbox(label="VEV", interactive=False)
159
- aev_out = gr.Textbox(label="AEV", interactive=False)
160
- ttv_out = gr.Textbox(label="TTV", interactive=False)
161
 
162
  gr.Markdown("### Risk Assessment")
163
- risk_out = gr.HTML(value="<i>Run analysis to see predicted risk...</i>")
164
 
165
- analyze_btn.click(
166
- fn=process_scan,
167
- inputs=[file_input, no_chemo, pd_surgery],
168
- outputs=[error_box, mac_out, vev_out, aev_out, ttv_out, risk_out]
 
 
 
 
 
 
 
 
 
169
  )
170
 
171
  if __name__ == "__main__":
172
- demo.launch(
173
- server_name="0.0.0.0",
174
- server_port=7860,
175
- theme=gr.themes.Base()
176
- )
 
7
  import traceback
8
  import tempfile
9
  import torch
10
+ from scipy.ndimage import distance_transform_edt
11
+
12
+
13
+ # SET ENV VARIABLES *BEFORE* IMPORTING NNUNET
14
 
 
 
15
  os.environ['nnUNet_raw'] = './tmp/nnUNet_raw'
16
  os.environ['nnUNet_results'] = './model_weights'
17
  os.environ['nnUNet_preprocessed'] = './tmp/nnUNet_preprocessed'
 
21
  from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
22
  from batchgenerators.utilities.file_and_folder_operations import join
23
 
24
+ # Model Initializations
25
  device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 
 
26
  try:
27
  predictor = nnUNetPredictor(
28
+ tile_step_size=0.5, use_gaussian=True, use_mirroring=True,
29
+ perform_everything_on_device=True, device=device,
30
+ verbose=False, verbose_preprocessing=False, allow_tqdm=True
 
 
 
 
 
31
  )
 
 
32
  model_folder = './model_weights/Dataset998_PanTSTen/nnUNetTrainer__nnUNetPlans__3d_fullres'
 
33
  predictor.initialize_from_trained_model_folder(
34
+ model_folder, use_folds=('all',), checkpoint_name='checkpoint_best.pth',
 
 
35
  )
 
36
  except Exception as e:
37
  predictor = None
38
+ print(f"Warning: nnU-Net failed to load. {e}")
39
 
40
  try:
41
  risk_model = joblib.load("model.joblib")
42
  except FileNotFoundError:
43
  risk_model = None
44
 
 
45
 
46
+
47
+ # FEATURE EXTRACTION
48
+
49
+ def extract_features(seg_path, high_risk_threshold_mm=5.0):
50
+ """Calculates metrics directly from a NIfTI mask."""
51
+ seg_img = nib.load(seg_path)
52
+ seg_data = seg_img.get_fdata()
53
+ spacing = seg_img.header.get_zooms()[:3]
54
+ voxel_volume_mm3 = spacing[0] * spacing[1] * spacing[2]
55
+
56
+ tumor_mask = (seg_data == 10)
57
+ artery_mask = np.isin(seg_data, [2, 8])
58
+ veins_mask = (seg_data == 9)
59
+
60
+ if not np.any(tumor_mask):
61
+ return 50.0, 0.0, 0.0, 0.0
62
+
63
+ ttv_mm3 = np.sum(tumor_mask) * voxel_volume_mm3
64
+
65
+ dist_artery = distance_transform_edt(~artery_mask, sampling=spacing) if np.any(artery_mask) else np.full(seg_data.shape, 50.0)
66
+ dist_veins = distance_transform_edt(~veins_mask, sampling=spacing) if np.any(veins_mask) else np.full(seg_data.shape, 50.0)
67
+
68
+ tumor_artery_distances = dist_artery[tumor_mask]
69
+ tumor_vein_distances = dist_veins[tumor_mask]
70
+
71
+ mac = np.min(tumor_artery_distances)
72
+ aev_mm3 = np.sum(tumor_artery_distances <= high_risk_threshold_mm) * voxel_volume_mm3
73
+ vev_mm3 = np.sum(tumor_vein_distances <= high_risk_threshold_mm) * voxel_volume_mm3
74
+
75
+ return mac, vev_mm3, aev_mm3, ttv_mm3
76
+
77
+ def calculate_risk_and_format(mask_path, no_chemo, pd_surgery):
78
+ """Runs extraction, ML prediction, and formats UI output."""
79
+ mac, vev, aev, ttv = extract_features(mask_path)
80
+
81
+ if risk_model is not None:
82
+ features = np.array([[mac, vev, aev, ttv, int(no_chemo), int(pd_surgery)]])
83
+ prob_r1 = risk_model.predict_proba(features)[0][1]
84
+ else:
85
+ prob_r1 = 0.72 # Fallback dummy
86
+
87
+ risk_percentage = prob_r1 * 100
88
+ risk_color = "red" if risk_percentage > 50 else "green"
89
+ risk_display = f"<h2 style='color: {risk_color}; margin-top: 0;'>Predicted probability of R1 resection: {risk_percentage:.1f}%</h2>"
90
+
91
+ return (gr.update(value="", visible=False), f"{mac:.1f}", f"{vev:.1f}", f"{aev:.1f}", f"{ttv:.1f}", risk_display)
92
+
93
+
94
+ # PIPELINE HANDLERS
95
+
96
+ def process_raw_ct(nifti_file, no_chemo, pd_surgery):
97
+ """Panel 1: Runs heavy nnU-Net segmentation first."""
98
  if nifti_file is None:
99
+ return gr.update(value="**Error:** Please upload a Raw CT NIfTI file.", visible=True), *[gr.update(value="")]*4, gr.update(value="")
100
 
101
  try:
 
102
  with tempfile.TemporaryDirectory() as temp_in_dir, tempfile.TemporaryDirectory() as temp_out_dir:
 
 
103
  original_path = nifti_file.name
104
  base_name = os.path.basename(original_path)
105
 
106
+ # Ensure _0000 suffix
107
  if not base_name.endswith("_0000.nii.gz") and base_name.endswith(".nii.gz"):
108
  new_base = base_name.replace(".nii.gz", "_0000.nii.gz")
 
 
109
  else:
110
  new_base = base_name
111
 
 
112
  input_file_path = os.path.join(temp_in_dir, new_base)
113
  shutil.copy(original_path, input_file_path)
114
 
 
115
  if predictor is None:
116
+ raise RuntimeError("nnU-Net is not initialized.")
117
 
118
+ # Run Segmentation
119
  predictor.predict_from_files(
120
+ temp_in_dir, temp_out_dir, save_probabilities=False, overwrite=False,
121
+ num_processes_preprocessing=2, num_processes_segmentation_export=2,
122
+ folder_with_segs_from_prev_stage=None, num_parts=1, part_id=0
 
 
 
 
 
 
123
  )
124
 
125
  output_base = new_base.replace("_0000", "")
126
  output_mask_path = os.path.join(temp_out_dir, output_base)
127
 
128
  if not os.path.exists(output_mask_path):
129
+ raise FileNotFoundError("nnU-Net failed to generate mask.")
 
 
 
 
 
 
 
 
 
130
 
131
+ return calculate_risk_and_format(output_mask_path, no_chemo, pd_surgery)
 
 
 
 
 
 
 
 
 
 
132
 
133
+ except Exception as e:
134
+ error_msg = f"**nnU-Net Pipeline Error:** {str(e)}\n\n```\n{traceback.format_exc()}\n```"
135
+ return gr.update(value=error_msg, visible=True), *[gr.update(value="")]*4, gr.update(value="")
 
 
136
 
137
+ def process_presegmented_mask(mask_file, no_chemo, pd_surgery):
138
+ """Panel 2: Skips nnU-Net, processes mask directly."""
139
+ if mask_file is None:
140
+ return gr.update(value="**Error:** Please upload a pre-segmented Mask NIfTI file.", visible=True), *[gr.update(value="")]*4, gr.update(value="")
141
+
142
+ try:
143
+ # Directly pass the uploaded mask to the extraction logic
144
+ return calculate_risk_and_format(mask_file.name, no_chemo, pd_surgery)
145
  except Exception as e:
146
+ error_msg = f"**Fast Pipeline Error:** {str(e)}\n\n```\n{traceback.format_exc()}\n```"
147
  return gr.update(value=error_msg, visible=True), *[gr.update(value="")]*4, gr.update(value="")
148
 
 
149
 
150
+ # GRADIO UI LAYOUT
151
 
152
  with gr.Blocks() as demo:
153
  gr.Markdown("# Preoperative R1 Resection Risk Predictor")
154
 
155
  with gr.Row():
156
+ # LEFT COLUMN: Inputs (Tabs + Clinical Data)
157
  with gr.Column(scale=1):
 
158
 
159
+ with gr.Tabs():
160
+ # TAB 1: Slow / Full Pipeline
161
+ with gr.TabItem("Raw CT (Runs nnU-Net)"):
162
+ gr.Markdown("*Note: Segmentation may take >1 hour on CPU.*")
163
+ file_input_ct = gr.File(label="Upload Raw CT Scan (.nii.gz)", file_count="single")
164
+ analyze_ct_btn = gr.Button("Segment & Analyze", variant="primary")
165
+
166
+ # TAB 2: Fast Pipeline
167
+ with gr.TabItem("Pre-segmented Mask (Fast)"):
168
+ gr.Markdown("*Bypass AI segmentation. Upload your own mask.*")
169
+ file_input_mask = gr.File(label="Upload Segmentation Mask (.nii.gz)", file_count="single")
170
+ analyze_mask_btn = gr.Button("Analyze Mask", variant="primary")
171
+
172
+ gr.Markdown("---")
173
+ gr.Markdown("### Clinical Metadata (Required)")
174
  no_chemo = gr.Checkbox(label="Absence of Neoadjuvant Chemo (1/0)")
175
  pd_surgery = gr.Checkbox(label="Patient underwent PD (Pancreaticoduodenectomy) (1/0)")
176
 
177
+ # RIGHT COLUMN: Shared Outputs
 
178
  with gr.Column(scale=2):
179
  error_box = gr.Markdown(visible=False)
180
 
181
  gr.Markdown("### Segmentation Metrics")
182
  with gr.Row():
183
+ mac_out = gr.Textbox(label="MAC (mm)", interactive=False)
184
+ vev_out = gr.Textbox(label="VEV (mm³)", interactive=False)
185
+ aev_out = gr.Textbox(label="AEV (mm³)", interactive=False)
186
+ ttv_out = gr.Textbox(label="TTV (mm³)", interactive=False)
187
 
188
  gr.Markdown("### Risk Assessment")
189
+ risk_out = gr.HTML(value="<i>Upload a file and run analysis...</i>")
190
 
191
+ # Connect buttons to their respective functions, but update the SAME output boxes
192
+ outputs_list = [error_box, mac_out, vev_out, aev_out, ttv_out, risk_out]
193
+
194
+ analyze_ct_btn.click(
195
+ fn=process_raw_ct,
196
+ inputs=[file_input_ct, no_chemo, pd_surgery],
197
+ outputs=outputs_list
198
+ )
199
+
200
+ analyze_mask_btn.click(
201
+ fn=process_presegmented_mask,
202
+ inputs=[file_input_mask, no_chemo, pd_surgery],
203
+ outputs=outputs_list
204
  )
205
 
206
  if __name__ == "__main__":
207
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True)