mqadir's picture
Update app.py
24ccd6a verified
Raw History Blame Contribute Delete
10.2 kB
import gradio as gr
import os
import shutil
import joblib
import numpy as np
import nibabel as nib
import traceback
import tempfile
import torch
from scipy.ndimage import distance_transform_edt
# SET ENV VARIABLES *BEFORE* IMPORTING NNUNET
os.environ['nnUNet_raw'] = './tmp/nnUNet_raw'
os.environ['nnUNet_results'] = './model_weights'
os.environ['nnUNet_preprocessed'] = './tmp/nnUNet_preprocessed'
os.makedirs('./tmp/nnUNet_raw', exist_ok=True)
os.makedirs('./tmp/nnUNet_preprocessed', exist_ok=True)
from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
from batchgenerators.utilities.file_and_folder_operations import join
# Model Initializations
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
try:
predictor = nnUNetPredictor(
tile_step_size=0.5, use_gaussian=True, use_mirroring=True,
perform_everything_on_device=True, device=device,
verbose=False, verbose_preprocessing=False, allow_tqdm=True
)
model_folder = './model_weights/Dataset998_PanTSTen/nnUNetTrainer__nnUNetPlans__3d_fullres'
predictor.initialize_from_trained_model_folder(
model_folder, use_folds=('all',), checkpoint_name='checkpoint_best.pth',
)
except Exception as e:
predictor = None
print(f"Warning: nnU-Net failed to load. {e}")
try:
risk_model = joblib.load("model.joblib")
except FileNotFoundError:
risk_model = None
# FEATURE EXTRACTION
def extract_features(seg_path, high_risk_threshold_mm=5.0):
"""Calculates metrics directly from a NIfTI mask."""
seg_img = nib.load(seg_path)
seg_data = seg_img.get_fdata()
spacing = seg_img.header.get_zooms()[:3]
voxel_volume_mm3 = spacing[0] * spacing[1] * spacing[2]
tumor_mask = (seg_data == 10)
artery_mask = np.isin(seg_data, [2, 8])
veins_mask = (seg_data == 9)
if not np.any(tumor_mask):
return 50.0, 0.0, 0.0, 0.0
ttv_mm3 = np.sum(tumor_mask) * voxel_volume_mm3
dist_artery = distance_transform_edt(~artery_mask, sampling=spacing) if np.any(artery_mask) else np.full(seg_data.shape, 50.0)
dist_veins = distance_transform_edt(~veins_mask, sampling=spacing) if np.any(veins_mask) else np.full(seg_data.shape, 50.0)
tumor_artery_distances = dist_artery[tumor_mask]
tumor_vein_distances = dist_veins[tumor_mask]
mac = np.min(tumor_artery_distances)
aev_mm3 = np.sum(tumor_artery_distances <= high_risk_threshold_mm) * voxel_volume_mm3
vev_mm3 = np.sum(tumor_vein_distances <= high_risk_threshold_mm) * voxel_volume_mm3
return mac, vev_mm3, aev_mm3, ttv_mm3
def calculate_risk_and_format(mask_path, no_chemo, pd_surgery):
"""Runs extraction, ML prediction, and formats UI output."""
# 1. Extract raw metrics
mac, vev, aev, ttv = extract_features(mask_path)
# Convert textboxes safely
try:
chemo_val = int(no_chemo)
pd_val = int(pd_surgery)
except ValueError:
error_msg = "**Error:** Clinical metadata fields must be numbers (1 or 0)."
return (gr.update(value=error_msg, visible=True), "", "", "", "", "")
# 2. Hardcode the Means and Stds from your training data
# Extracted from your means/stds arrays based on their index positions
TTV_MEAN, TTV_STD = 8980.23827, 8281.19811
MAC_MEAN, MAC_STD = 8.53567157, 8.92353600
AEV_MEAN, AEV_STD = 155.784393, 367.152035
VEV_MEAN, VEV_STD = 576.025386, 559.944824
# 3. Apply Z-score Normalization
scaled_ttv = (ttv - TTV_MEAN) / TTV_STD
scaled_mac = (mac - MAC_MEAN) / MAC_STD
scaled_aev = (aev - AEV_MEAN) / AEV_STD
scaled_vev = (vev - VEV_MEAN) / VEV_STD
# 4. Predict Risk
if risk_model is not None:
# STRICT ORDER MATCHING your column_order:
# ['Chemo_0', 'Surgery_1', 'Total_Tumor_Volume_mm3_arterial', 'Min_Distance_mm_arterial', 'High_Risk_Volume_mm3_5mm_arterial', 'High_Risk_Volume_mm3_5mm_venous']
features = np.array([[
chemo_val, # Chemo_0
pd_val, # Surgery_1
scaled_ttv, # Total_Tumor_Volume_mm3_arterial
scaled_mac, # Min_Distance_mm_arterial
scaled_aev, # High_Risk_Volume_mm3_5mm_arterial
scaled_vev # High_Risk_Volume_mm3_5mm_venous
]])
prob_r1 = risk_model.predict_proba(features)[0][1]
else:
prob_r1 = 0.72 # Fallback dummy
# 5. Format Outputs
risk_percentage = prob_r1
risk_color = "#C55A11" if risk_percentage > 0.5 else "#B89036"
risk_display = f"<h2 style='color: {risk_color}; margin-top: 0;'>Predicted probability of R+ resection: {risk_percentage:.2f}</h2>"
# Notice we still display the RAW un-normalized values to the user in the UI,
# but the model calculates risk using the SCALED values.
return (
gr.update(value="", visible=False),
f"{mac:.1f}",
f"{vev:.1f}",
f"{aev:.1f}",
f"{ttv:.1f}",
risk_display
)
# PIPELINE HANDLERS
def process_raw_ct(nifti_file, no_chemo, pd_surgery):
"""Panel 1: Runs heavy nnU-Net segmentation first."""
if nifti_file is None:
return gr.update(value="**Error:** Please upload a Raw CT NIfTI file.", visible=True), *[gr.update(value="")]*4, gr.update(value="")
try:
with tempfile.TemporaryDirectory() as temp_in_dir, tempfile.TemporaryDirectory() as temp_out_dir:
original_path = nifti_file.name
base_name = os.path.basename(original_path)
# Ensure _0000 suffix
if not base_name.endswith("_0000.nii.gz") and base_name.endswith(".nii.gz"):
new_base = base_name.replace(".nii.gz", "_0000.nii.gz")
else:
new_base = base_name
input_file_path = os.path.join(temp_in_dir, new_base)
shutil.copy(original_path, input_file_path)
if predictor is None:
raise RuntimeError("nnU-Net is not initialized.")
# Run Segmentation
predictor.predict_from_files(
temp_in_dir, temp_out_dir, save_probabilities=False, overwrite=False,
num_processes_preprocessing=2, num_processes_segmentation_export=2,
folder_with_segs_from_prev_stage=None, num_parts=1, part_id=0
)
output_base = new_base.replace("_0000", "")
output_mask_path = os.path.join(temp_out_dir, output_base)
if not os.path.exists(output_mask_path):
raise FileNotFoundError("nnU-Net failed to generate mask.")
return calculate_risk_and_format(output_mask_path, no_chemo, pd_surgery)
except Exception as e:
error_msg = f"**nnU-Net Pipeline Error:** {str(e)}\n\n```\n{traceback.format_exc()}\n```"
return gr.update(value=error_msg, visible=True), *[gr.update(value="")]*4, gr.update(value="")
def process_presegmented_mask(mask_file, no_chemo, pd_surgery):
"""Panel 2: Skips nnU-Net, processes mask directly."""
if mask_file is None:
return gr.update(value="**Error:** Please upload a pre-segmented Mask NIfTI file.", visible=True), *[gr.update(value="")]*4, gr.update(value="")
try:
# Directly pass the uploaded mask to the extraction logic
return calculate_risk_and_format(mask_file.name, no_chemo, pd_surgery)
except Exception as e:
error_msg = f"**Fast Pipeline Error:** {str(e)}\n\n```\n{traceback.format_exc()}\n```"
return gr.update(value=error_msg, visible=True), *[gr.update(value="")]*4, gr.update(value="")
# GRADIO UI LAYOUT
with gr.Blocks() as demo:
gr.Markdown("# Preoperative PDAC R+ Resection Risk Predictor")
with gr.Row():
# LEFT COLUMN: Inputs (Tabs + Clinical Data)
with gr.Column(scale=1):
with gr.Tabs():
# TAB 1: Slow / Full Pipeline
with gr.TabItem("Raw CT (Runs nnU-Net)"):
gr.Markdown("*Note: Segmentation may take >1 hour on CPU.*")
file_input_ct = gr.File(label="Upload Raw CT Scan (.nii.gz)", file_count="single")
analyze_ct_btn = gr.Button("Segment & Analyze", variant="primary")
# TAB 2: Fast Pipeline
with gr.TabItem("Pre-segmented Mask (Fast)"):
gr.Markdown("*Bypass AI segmentation. Upload your own mask.*")
file_input_mask = gr.File(label="Upload Segmentation Mask (.nii.gz)", file_count="single")
analyze_mask_btn = gr.Button("Analyze Mask", variant="primary")
gr.Markdown("---")
gr.Markdown("### Clinical Metadata (Required)")
no_chemo = gr.Checkbox(label="Absence of Neoadjuvant Chemo (1/0)")
pd_surgery = gr.Checkbox(label="Patient underwent PD (Pancreaticoduodenectomy) (1/0)")
# RIGHT COLUMN: Shared Outputs
with gr.Column(scale=2):
error_box = gr.Markdown(visible=False)
gr.Markdown("### Segmentation Metrics")
with gr.Row():
mac_out = gr.Textbox(label="Minimum Arterial Clearance (mm)", interactive=False)
vev_out = gr.Textbox(label="Venous Encasement Volume (mm³)", interactive=False)
aev_out = gr.Textbox(label="Arterial Encasement Volume (mm³)", interactive=False)
ttv_out = gr.Textbox(label="Total Tumor Volume (mm³)", interactive=False)
gr.Markdown("### Risk Assessment")
risk_out = gr.HTML(value="<i>Upload a file and run analysis...</i>")
# Connect buttons to their respective functions, but update the SAME output boxes
outputs_list = [error_box, mac_out, vev_out, aev_out, ttv_out, risk_out]
analyze_ct_btn.click(
fn=process_raw_ct,
inputs=[file_input_ct, no_chemo, pd_surgery],
outputs=outputs_list
)
analyze_mask_btn.click(
fn=process_presegmented_mask,
inputs=[file_input_mask, no_chemo, pd_surgery],
outputs=outputs_list
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, share=True)