Spaces:
Sleeping
Sleeping
Pavan Praneeth commited on
Commit ·
4b0207f
1
Parent(s): 1d68cc7
Add Gradio app for ISIC skin lesion segmentation
Browse files
README.md
CHANGED
|
@@ -1,12 +1,35 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ISIC Skin Lesion Segmentation
|
| 3 |
+
emoji: 🔬
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.44.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# ISIC 2018 Skin Lesion Segmentation — U-Net
|
| 13 |
+
|
| 14 |
+
Upload a dermoscopy image to get an instant binary segmentation mask from a trained U-Net.
|
| 15 |
+
|
| 16 |
+
## Results
|
| 17 |
+
|
| 18 |
+
| Metric | Test Set |
|
| 19 |
+
|--------|----------|
|
| 20 |
+
| Dice | **0.9301 ± 0.0621** |
|
| 21 |
+
| IoU | **0.8744 ± 0.0891** |
|
| 22 |
+
|
| 23 |
+
Trained on **ISIC 2018 Task 1** (568 images, 70/15/15 train/val/test split).
|
| 24 |
+
Best checkpoint: epoch **45**, val Dice **0.9207**.
|
| 25 |
+
|
| 26 |
+
## Model Architecture
|
| 27 |
+
|
| 28 |
+
Classic **U-Net** with skip connections.
|
| 29 |
+
Channel progression: 3 → 64 → 128 → 256 → 512 → 1024 → 512 → 256 → 128 → 64 → 1
|
| 30 |
+
|
| 31 |
+
## Usage
|
| 32 |
+
|
| 33 |
+
1. Upload any dermoscopy / skin lesion image
|
| 34 |
+
2. Click **Segment 🔍**
|
| 35 |
+
3. View the predicted binary mask and overlay
|
app.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py
|
| 3 |
+
------
|
| 4 |
+
Gradio demo for ISIC 2018 Skin Lesion Segmentation using a trained U-Net.
|
| 5 |
+
|
| 6 |
+
Hosted on Hugging Face Spaces.
|
| 7 |
+
Model weights are downloaded from the HF Hub model repo on first run.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
import gradio as gr
|
| 14 |
+
from PIL import Image
|
| 15 |
+
from huggingface_hub import hf_hub_download
|
| 16 |
+
|
| 17 |
+
# ---------------------------------------------------------------------------
|
| 18 |
+
# Constants
|
| 19 |
+
# ---------------------------------------------------------------------------
|
| 20 |
+
MODEL_REPO = "pavanpraneeth/isic-unet"
|
| 21 |
+
MODEL_FILE = "best_model.pth"
|
| 22 |
+
IMAGE_SIZE = 256
|
| 23 |
+
THRESHOLD = 0.5
|
| 24 |
+
IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
|
| 25 |
+
IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
|
| 26 |
+
|
| 27 |
+
DEVICE = (
|
| 28 |
+
torch.device("cuda") if torch.cuda.is_available()
|
| 29 |
+
else torch.device("mps") if torch.backends.mps.is_available()
|
| 30 |
+
else torch.device("cpu")
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
# Load model (once at startup)
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
from model import UNet # model.py is alongside app.py in the Space repo
|
| 37 |
+
|
| 38 |
+
def load_model() -> torch.nn.Module:
|
| 39 |
+
ckpt_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
|
| 40 |
+
model = UNet(in_channels=3, out_channels=1)
|
| 41 |
+
state = torch.load(ckpt_path, map_location=DEVICE)
|
| 42 |
+
model.load_state_dict(state["model_state_dict"])
|
| 43 |
+
model.eval().to(DEVICE)
|
| 44 |
+
print(f"[app] Model loaded from {MODEL_REPO} on {DEVICE}")
|
| 45 |
+
return model
|
| 46 |
+
|
| 47 |
+
MODEL = load_model()
|
| 48 |
+
|
| 49 |
+
# ---------------------------------------------------------------------------
|
| 50 |
+
# Preprocessing / postprocessing helpers
|
| 51 |
+
# ---------------------------------------------------------------------------
|
| 52 |
+
|
| 53 |
+
def preprocess(img: np.ndarray) -> torch.Tensor:
|
| 54 |
+
"""Resize, normalise (ImageNet), convert to tensor."""
|
| 55 |
+
pil = Image.fromarray(img).convert("RGB").resize((IMAGE_SIZE, IMAGE_SIZE))
|
| 56 |
+
arr = np.array(pil, dtype=np.float32) / 255.0
|
| 57 |
+
arr = (arr - IMAGENET_MEAN) / IMAGENET_STD # (H, W, 3)
|
| 58 |
+
tensor = torch.from_numpy(arr.transpose(2, 0, 1)) # (3, H, W)
|
| 59 |
+
return tensor.unsqueeze(0).to(DEVICE) # (1, 3, H, W)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def postprocess_mask(pred: torch.Tensor) -> np.ndarray:
|
| 63 |
+
"""Convert raw sigmoid output → uint8 mask image (0 or 255)."""
|
| 64 |
+
mask = (pred.squeeze().cpu().numpy() > THRESHOLD).astype(np.uint8) * 255
|
| 65 |
+
return mask
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def make_overlay(original_rgb: np.ndarray, mask: np.ndarray) -> np.ndarray:
|
| 69 |
+
"""Overlay mask boundary on original image in red."""
|
| 70 |
+
h, w = mask.shape
|
| 71 |
+
orig_resized = np.array(
|
| 72 |
+
Image.fromarray(original_rgb).resize((w, h))
|
| 73 |
+
).copy()
|
| 74 |
+
|
| 75 |
+
# Draw red where mask == 255
|
| 76 |
+
overlay = orig_resized.copy()
|
| 77 |
+
overlay[mask > 0] = (
|
| 78 |
+
overlay[mask > 0] * 0.4 + np.array([255, 0, 0]) * 0.6
|
| 79 |
+
).astype(np.uint8)
|
| 80 |
+
return overlay
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# ---------------------------------------------------------------------------
|
| 84 |
+
# Inference function (called by Gradio)
|
| 85 |
+
# ---------------------------------------------------------------------------
|
| 86 |
+
|
| 87 |
+
def segment(image: np.ndarray):
|
| 88 |
+
"""Run inference and return (mask_image, overlay_image)."""
|
| 89 |
+
if image is None:
|
| 90 |
+
return None, None
|
| 91 |
+
|
| 92 |
+
tensor = preprocess(image)
|
| 93 |
+
with torch.no_grad():
|
| 94 |
+
pred = MODEL(tensor) # (1, 1, 256, 256)
|
| 95 |
+
|
| 96 |
+
mask = postprocess_mask(pred) # (256, 256) uint8
|
| 97 |
+
overlay = make_overlay(image, mask)
|
| 98 |
+
|
| 99 |
+
mask_rgb = np.stack([mask, mask, mask], axis=-1) # grey → RGB for display
|
| 100 |
+
return mask_rgb, overlay
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# ---------------------------------------------------------------------------
|
| 104 |
+
# Gradio UI
|
| 105 |
+
# ---------------------------------------------------------------------------
|
| 106 |
+
|
| 107 |
+
DESCRIPTION = """
|
| 108 |
+
## 🔬 ISIC 2018 Skin Lesion Segmentation
|
| 109 |
+
|
| 110 |
+
Upload a dermoscopy image to get an instant binary segmentation mask from a trained **U-Net**.
|
| 111 |
+
|
| 112 |
+
| Metric | Test Set Score |
|
| 113 |
+
|--------|---------------|
|
| 114 |
+
| Dice | **0.9301 ± 0.0621** |
|
| 115 |
+
| IoU | **0.8744 ± 0.0891** |
|
| 116 |
+
|
| 117 |
+
*Trained on ISIC 2018 Task 1 (568 images, 70/15/15 split).*
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="ISIC Skin Lesion Segmentation") as demo:
|
| 121 |
+
gr.Markdown(DESCRIPTION)
|
| 122 |
+
|
| 123 |
+
with gr.Row():
|
| 124 |
+
with gr.Column():
|
| 125 |
+
inp = gr.Image(label="Input Image", type="numpy")
|
| 126 |
+
btn = gr.Button("Segment 🔍", variant="primary")
|
| 127 |
+
with gr.Column():
|
| 128 |
+
out_mask = gr.Image(label="Predicted Mask")
|
| 129 |
+
out_overlay = gr.Image(label="Overlay on Original")
|
| 130 |
+
|
| 131 |
+
btn.click(fn=segment, inputs=inp, outputs=[out_mask, out_overlay])
|
| 132 |
+
|
| 133 |
+
gr.Examples(
|
| 134 |
+
examples=[],
|
| 135 |
+
inputs=inp,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
if __name__ == "__main__":
|
| 139 |
+
demo.launch()
|
model.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
model.py — Self-contained UNet definition for HF Spaces.
|
| 3 |
+
Copied from the training project (model.py).
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn as nn
|
| 8 |
+
from typing import List
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class DoubleConv(nn.Module):
|
| 12 |
+
def __init__(self, in_channels: int, out_channels: int) -> None:
|
| 13 |
+
super().__init__()
|
| 14 |
+
self.block = nn.Sequential(
|
| 15 |
+
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False),
|
| 16 |
+
nn.BatchNorm2d(out_channels),
|
| 17 |
+
nn.ReLU(inplace=True),
|
| 18 |
+
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False),
|
| 19 |
+
nn.BatchNorm2d(out_channels),
|
| 20 |
+
nn.ReLU(inplace=True),
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 24 |
+
return self.block(x)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class UNet(nn.Module):
|
| 28 |
+
def __init__(
|
| 29 |
+
self,
|
| 30 |
+
in_channels: int = 3,
|
| 31 |
+
out_channels: int = 1,
|
| 32 |
+
features: List[int] = [64, 128, 256, 512],
|
| 33 |
+
) -> None:
|
| 34 |
+
super().__init__()
|
| 35 |
+
|
| 36 |
+
self.encoders = nn.ModuleList()
|
| 37 |
+
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
|
| 38 |
+
ch = in_channels
|
| 39 |
+
for feat in features:
|
| 40 |
+
self.encoders.append(DoubleConv(ch, feat))
|
| 41 |
+
ch = feat
|
| 42 |
+
|
| 43 |
+
self.bottleneck = DoubleConv(features[-1], features[-1] * 2)
|
| 44 |
+
ch = features[-1] * 2
|
| 45 |
+
|
| 46 |
+
self.up_convs = nn.ModuleList()
|
| 47 |
+
self.decoders = nn.ModuleList()
|
| 48 |
+
for feat in reversed(features):
|
| 49 |
+
self.up_convs.append(nn.ConvTranspose2d(ch, feat, kernel_size=2, stride=2))
|
| 50 |
+
self.decoders.append(DoubleConv(feat * 2, feat))
|
| 51 |
+
ch = feat
|
| 52 |
+
|
| 53 |
+
self.output_conv = nn.Conv2d(features[0], out_channels, kernel_size=1)
|
| 54 |
+
self.sigmoid = nn.Sigmoid()
|
| 55 |
+
|
| 56 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 57 |
+
skip_connections: List[torch.Tensor] = []
|
| 58 |
+
|
| 59 |
+
for encoder in self.encoders:
|
| 60 |
+
x = encoder(x)
|
| 61 |
+
skip_connections.append(x)
|
| 62 |
+
x = self.pool(x)
|
| 63 |
+
|
| 64 |
+
x = self.bottleneck(x)
|
| 65 |
+
|
| 66 |
+
skip_connections = skip_connections[::-1]
|
| 67 |
+
for up_conv, decoder, skip in zip(self.up_convs, self.decoders, skip_connections):
|
| 68 |
+
x = up_conv(x)
|
| 69 |
+
if x.shape != skip.shape:
|
| 70 |
+
x = nn.functional.interpolate(x, size=skip.shape[2:], mode="bilinear", align_corners=False)
|
| 71 |
+
x = torch.cat([skip, x], dim=1)
|
| 72 |
+
x = decoder(x)
|
| 73 |
+
|
| 74 |
+
return self.sigmoid(self.output_conv(x))
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch==2.2.2
|
| 2 |
+
torchvision==0.17.2
|
| 3 |
+
gradio==4.44.0
|
| 4 |
+
huggingface_hub==0.23.4
|
| 5 |
+
Pillow
|
| 6 |
+
numpy
|