Pavan Praneeth commited on
Commit
4b0207f
·
1 Parent(s): 1d68cc7

Add Gradio app for ISIC skin lesion segmentation

Browse files
Files changed (4) hide show
  1. README.md +29 -6
  2. app.py +139 -0
  3. model.py +74 -0
  4. requirements.txt +6 -0
README.md CHANGED
@@ -1,12 +1,35 @@
1
  ---
2
- title: Isic Segmentation
3
- emoji: 🐠
4
- colorFrom: purple
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.9.0
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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