| import streamlit as st |
| import torch |
| import numpy as np |
| import cv2 |
| from PIL import Image |
| from transformers import AutoModelForImageClassification, AutoImageProcessor |
| from torchvision import transforms |
| from pytorch_grad_cam import GradCAM |
| from pytorch_grad_cam.utils.image import show_cam_on_image |
| from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget |
| import torch.nn.functional as F |
|
|
| |
| |
| |
| st.set_page_config( |
| page_title="RSNA Intracranial Hemorrhage AI", |
| page_icon="🧠", |
| layout="wide" |
| ) |
|
|
| st.title("🧠 AI-Assisted Intracranial Hemorrhage Detection") |
| st.markdown(""" |
| **Workflow:** `Input CT Scan` $\\rightarrow$ `ResNet50 Inference` $\\rightarrow$ `Grad-CAM XAI` $\\rightarrow$ `Radiologist Review` |
| """) |
|
|
| |
| |
| |
| MODEL_ID = "dongqinggeng/rsna" |
|
|
| @st.cache_resource |
| def load_model_and_processor(): |
| try: |
| |
| model = AutoModelForImageClassification.from_pretrained(MODEL_ID) |
| model.eval() |
| |
| |
| try: |
| processor = AutoImageProcessor.from_pretrained(MODEL_ID) |
| except: |
| processor = None |
| |
| return model, processor |
| except Exception as e: |
| st.error(f"Error loading model from Hugging Face: {e}") |
| return None, None |
|
|
| model, processor = load_model_and_processor() |
|
|
| |
| LABELS = ['epidural', 'intraparenchymal', 'intraventricular', 'subarachnoid', 'subdural', 'any'] |
| id2label = {i: label for i, label in enumerate(LABELS)} |
|
|
| |
| |
| |
|
|
| |
| class HuggingFaceModelWrapper(torch.nn.Module): |
| def __init__(self, model): |
| super(HuggingFaceModelWrapper, self).__init__() |
| self.model = model |
| def forward(self, x): |
| return self.model(x).logits |
|
|
| |
| def process_image(image): |
| |
| transform = transforms.Compose([ |
| transforms.Resize((224, 224)), |
| transforms.ToTensor(), |
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) |
| ]) |
| |
| |
| image = image.convert("RGB") |
| input_tensor = transform(image).unsqueeze(0) |
| return input_tensor, image |
|
|
| |
| def generate_gradcam(model, input_tensor, target_layer): |
| cam = GradCAM(model=HuggingFaceModelWrapper(model), target_layers=[target_layer]) |
| |
| grayscale_cam = cam(input_tensor=input_tensor, targets=None) |
| return grayscale_cam[0, :] |
|
|
| |
| |
| |
|
|
| |
| st.sidebar.header("1. Input Data") |
| uploaded_file = st.sidebar.file_uploader("Upload a CT Slice (PNG/JPG/DICOM)", type=["png", "jpg", "jpeg"]) |
|
|
| |
| if st.sidebar.button("Load Demo Image (Simulated)"): |
| |
| st.sidebar.info("Please upload a real file to test.") |
|
|
| |
| col1, col2 = st.columns([1, 1.5]) |
|
|
| if uploaded_file is not None and model is not None: |
| |
| image_pil = Image.open(uploaded_file) |
| |
| with col1: |
| st.subheader("Original CT Scan") |
| st.image(image_pil, use_container_width=True, caption="Input Slice") |
|
|
| |
| with st.spinner("Running AI Model & Generating Explanations..."): |
| |
| input_tensor, image_rgb_pil = process_image(image_pil) |
| |
| |
| with torch.no_grad(): |
| outputs = model(input_tensor) |
| probs = torch.sigmoid(outputs.logits).cpu().numpy()[0] |
| |
| |
| |
| |
| specific_probs = probs[:5] |
| top_idx = np.argmax(specific_probs) |
| top_label = LABELS[top_idx] |
| top_prob = specific_probs[top_idx] |
| any_prob = probs[5] |
|
|
| |
| |
| target_layer = model.resnet.encoder.stages[-1].layers[-1] |
| cam_mask = generate_gradcam(model, input_tensor, target_layer) |
| |
| |
| |
| img_np = np.array(image_rgb_pil) |
| img_np = img_np.astype(np.float32) / 255.0 |
| |
| visualization = show_cam_on_image(img_np, cam_mask, use_rgb=True) |
|
|
| |
| with col2: |
| st.subheader("XAI Output (Grad-CAM)") |
| st.image(visualization, use_container_width=True, caption=f"Model Focus Area (Red = High Attention)") |
| |
| |
| st.divider() |
| st.header("📝 AI Analysis Report") |
| |
| |
| if any_prob > 0.5: |
| status_color = "red" |
| status_text = "Hemorrhage Detected" |
| confidence_text = f"High Confidence ({any_prob:.2%})" |
| |
| review_text = ( |
| f"**AI Findings:** The model detected specific features consistent with **{top_label} hemorrhage** " |
| f"(Probability: {top_prob:.2%}).\n\n" |
| f"**XAI Localization:** The Grad-CAM heatmap highlights a region of interest. " |
| f"Please verify if this corresponds to a hyperdense area in the brain parenchyma or extra-axial space." |
| ) |
| else: |
| status_color = "green" |
| status_text = "No Hemorrhage Detected" |
| confidence_text = f"({1-any_prob:.2%} sure)" |
| review_text = "**AI Findings:** No significant signs of intracranial hemorrhage were detected. Heatmap shows diffuse or non-specific activation." |
|
|
| |
| m1, m2, m3 = st.columns(3) |
| m1.metric("Overall Prediction", status_text, delta=confidence_text, delta_color="inverse" if any_prob > 0.5 else "normal") |
| m2.metric("Primary Subtype", top_label.capitalize() if any_prob > 0.3 else "N/A", f"{top_prob:.2%}") |
| |
| st.markdown(f""" |
| > **Radiologist Review Note:** > {review_text} |
| """) |
| |
| |
| st.subheader("Detailed Class Probabilities") |
| st.bar_chart({label: prob for label, prob in zip(LABELS, probs)}) |
|
|
| else: |
| |
| st.info("👈 Please upload a CT image from the sidebar to start the analysis.") |
| st.markdown("### How to interpret the heatmap?") |
| st.markdown(""" |
| * **Red Areas**: Regions that contributed most to the AI's decision (High Importance). |
| * **Blue Areas**: Regions the AI ignored. |
| * *Note: If the heatmap highlights the skull or background, the prediction might be an artifact.* |
| """) |