| import os |
| from huggingface_hub import login |
|
|
| login(token=os.getenv("HF_TOKEN")) |
|
|
| import gradio as gr |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| import torchvision.models as models |
| import torchvision.transforms as transforms |
| import numpy as np |
| from PIL import Image |
| import json |
| import os |
| import pydicom |
| from pydicom.uid import ImplicitVRLittleEndian |
| from huggingface_hub import snapshot_download |
|
|
| |
| |
| |
|
|
| MODEL_REPO = "InfoBayAI/resnet18-ct-pathology-classifier" |
|
|
| SAVE_DIR = snapshot_download(repo_id=MODEL_REPO) |
|
|
| print("Model downloaded to:", SAVE_DIR) |
|
|
| with open(os.path.join(SAVE_DIR, "config.json"), "r") as f: |
| config = json.load(f) |
|
|
| with open(os.path.join(SAVE_DIR, "labels.json"), "r") as f: |
| label_names_raw = json.load(f) |
|
|
| |
| label_names = {int(k): v for k, v in label_names_raw.items()} |
|
|
| IMG_SIZE = config["img_size"] |
| NUM_CLASSES = config["num_classes"] |
|
|
| |
| |
| |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
|
|
| |
| |
| |
| |
| model = models.resnet18(pretrained=False) |
|
|
| |
| model.fc = nn.Sequential( |
| nn.Linear(model.fc.in_features, 256), |
| nn.ReLU(), |
| nn.Dropout(0.4), |
| nn.Linear(256, NUM_CLASSES) |
| ) |
|
|
| model.load_state_dict( |
| torch.load(os.path.join(SAVE_DIR, "pytorch_model.bin"), map_location=device) |
| ) |
| model.to(device) |
| model.eval() |
|
|
| print("โ
CT Pathology Model Loaded Successfully") |
|
|
| |
| |
| |
| |
| val_transform = transforms.Compose([ |
| transforms.Resize((IMG_SIZE, IMG_SIZE)), |
| transforms.ToTensor(), |
| ]) |
|
|
| |
| |
| |
| |
| def load_image_from_pil(pil_image): |
| """Gradio passes a PIL image directly โ just convert to RGB.""" |
| return pil_image.convert("RGB") |
|
|
|
|
| def load_dicom(path): |
| """Load a DICOM file from disk path and return a PIL RGB image.""" |
| try: |
| dcm = pydicom.dcmread(path, force=True) |
|
|
| if not hasattr(dcm, "file_meta") or dcm.file_meta is None: |
| dcm.file_meta = pydicom.dataset.FileMetaDataset() |
|
|
| if not hasattr(dcm.file_meta, "TransferSyntaxUID"): |
| dcm.file_meta.TransferSyntaxUID = ImplicitVRLittleEndian |
|
|
| try: |
| dcm.decompress() |
| except: |
| pass |
|
|
| img = dcm.pixel_array.astype(np.float32) |
|
|
| if img.max() == img.min(): |
| return None |
|
|
| img = (img - img.min()) / (img.max() - img.min()) |
| img = (img * 255).astype(np.uint8) |
|
|
| return Image.fromarray(img).convert("RGB") |
|
|
| except Exception as e: |
| print(f"โ DICOM load failed | {e}") |
| return None |
|
|
| |
| |
| |
| def predict_ct(image): |
| if image is None: |
| return "โ ๏ธ Please upload a CT scan image.", None |
|
|
| img = load_image_from_pil(image) |
|
|
| tensor = val_transform(img).unsqueeze(0).to(device) |
|
|
| with torch.no_grad(): |
| output = model(tensor) |
| probs = F.softmax(output, dim=1) |
|
|
| probs_np = probs.cpu().numpy()[0] |
|
|
| sorted_indices = np.argsort(probs_np)[::-1] |
|
|
| top_label = label_names[sorted_indices[0]] |
| top_conf = probs_np[sorted_indices[0]] * 100 |
|
|
| formatted_probs = "\n".join([ |
| f" {label_names[i]:<25} โ {probs_np[i]*100:.2f}%" |
| for i in sorted_indices |
| ]) |
|
|
| result_text = f""" |
| ๐ซ CT Scan Pathology Classification |
| |
| ๐ Prediction : {top_label} |
| ๐ Confidence : {top_conf:.2f}% |
| |
| ๐ All Class Probabilities: |
| {formatted_probs} |
| |
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ |
| โ ๏ธ AI-assisted output โ NOT a medical diagnosis. |
| Always consult a qualified radiologist. |
| """ |
|
|
| probs_dict = { |
| label_names[i]: float(probs_np[i]) |
| for i in range(NUM_CLASSES) |
| } |
|
|
| return result_text, probs_dict |
|
|
| |
| |
| |
| def create_interface(): |
| with gr.Blocks( |
| theme=gr.themes.Soft(), |
| css=""" |
| .gradio-container { |
| max-width: 1600px !important; |
| margin: auto; |
| } |
| .gr-image { |
| min-height: 500px !important; |
| } |
| textarea { |
| font-size: 15px !important; |
| font-family: monospace !important; |
| } |
| h1, h2, h3 { |
| text-align: center; |
| } |
| """ |
| ) as interface: |
|
|
| gr.Markdown("# ๐ซ CT Scan Pathology Classifier") |
| gr.Markdown( |
| "### Detects: **Hydropneumothorax ยท Brain Gliosis ยท Liver Cirrhosis ยท Liver Abscess**" |
| ) |
| gr.Markdown("---") |
|
|
| with gr.Row(equal_height=True): |
|
|
| |
| with gr.Column(scale=1.2): |
| image_input = gr.Image( |
| type="pil", |
| label="๐ค Upload CT Scan (JPG / PNG / BMP)", |
| height=500, |
| sources=["upload"] |
| ) |
|
|
| with gr.Row(): |
| predict_btn = gr.Button("๐ Analyze CT Scan", variant="primary") |
| clear_btn = gr.Button("๐งน Clear") |
|
|
| |
| with gr.Column(scale=1.8): |
| output_text = gr.Textbox( |
| label="๐ Classification Result", |
| lines=18 |
| ) |
| output_chart = gr.Label( |
| label="๐ Confidence Breakdown" |
| ) |
|
|
| gr.Markdown("---") |
| gr.Markdown( |
| "๐ก **Tips:** Use axial CT slices for best results. " |
| "Convert DICOM to PNG/JPG before uploading if needed." |
| ) |
|
|
| |
| predict_btn.click( |
| fn=predict_ct, |
| inputs=image_input, |
| outputs=[output_text, output_chart] |
| ) |
|
|
| clear_btn.click( |
| fn=lambda: (None, "", None), |
| inputs=[], |
| outputs=[image_input, output_text, output_chart] |
| ) |
|
|
| return interface |
|
|
| |
| |
| |
| if __name__ == "__main__": |
| app = create_interface() |
| app.launch(share=True) |