g-loubna commited on
Commit
c5bce9d
·
0 Parent(s):

Space: download weights from model repo

Browse files
Files changed (6) hide show
  1. .gitignore +1 -0
  2. app.py +289 -0
  3. inference.py +42 -0
  4. model.py +11 -0
  5. requirements.txt +8 -0
  6. style.css +391 -0
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ venv/ pycache/ *.pyc *.log .DS_Store
app.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import torch
4
+ from pathlib import Path
5
+ from PIL import Image
6
+ from inference import load_model, predict
7
+
8
+ # -------- Hub / Weights Configuration --------
9
+ HUB_REPO_ID = "g-loubna/bridge-unetpp" # Hugging Face model repo (change if you renamed it)
10
+ WEIGHTS_FILENAME = "MILESTONE_090_ACHIEVED_iou_0.9077.pth"
11
+ WEIGHTS_PATH = Path(WEIGHTS_FILENAME)
12
+
13
+ # Try to fetch weights from Hub if not present locally
14
+ # (Requires 'huggingface-hub' in requirements.txt)
15
+ try:
16
+ if not WEIGHTS_PATH.exists():
17
+ print(f"Weights file {WEIGHTS_FILENAME} not found locally. Downloading from {HUB_REPO_ID} ...")
18
+ from huggingface_hub import hf_hub_download
19
+ hf_hub_download(
20
+ repo_id=HUB_REPO_ID,
21
+ filename=WEIGHTS_FILENAME,
22
+ local_dir=".", # place file in current working directory
23
+ local_dir_use_symlinks=False # make a real copy (Spaces friendly)
24
+ )
25
+ if WEIGHTS_PATH.exists():
26
+ print("Download complete.")
27
+ else:
28
+ print("Download attempted but file still not found.")
29
+ except Exception as dl_err:
30
+ print(f"WARNING: Could not download weights automatically: {dl_err}")
31
+
32
+ # ---------------- Runtime / Device ----------------
33
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
34
+
35
+ CLASS_INFO = [
36
+ {"id": 0, "name": "background", "color": (0, 0, 0)},
37
+ {"id": 1, "name": "beton", "color": (0, 114, 189)},
38
+ {"id": 2, "name": "steel", "color": (200, 30, 30)},
39
+ ]
40
+ COLOR_MAP = np.array([c["color"] for c in CLASS_INFO], dtype=np.uint8)
41
+
42
+ # ---------------- Load Model (defensive) ----------------
43
+ model_load_error = None
44
+ model = None
45
+ try:
46
+ if WEIGHTS_PATH.exists():
47
+ model = load_model(str(WEIGHTS_PATH), DEVICE)
48
+ else:
49
+ model_load_error = f"Weight file {WEIGHTS_FILENAME} not found after download attempt."
50
+ except Exception as e:
51
+ model_load_error = f"Model failed to load: {e}"
52
+
53
+ # ---------------- Utility Functions ----------------
54
+ def resize_mask_to_original(mask_np_small: np.ndarray, original_shape):
55
+ H, W = original_shape[:2]
56
+ if mask_np_small.shape[:2] == (H, W):
57
+ return mask_np_small
58
+ pil_small = Image.fromarray(mask_np_small.astype(np.uint8))
59
+ pil_big = pil_small.resize((W, H), resample=Image.NEAREST)
60
+ return np.array(pil_big)
61
+
62
+ def overlay_mask(original_np: np.ndarray, mask_np: np.ndarray, alpha: float = 0.5):
63
+ color_mask = COLOR_MAP[mask_np]
64
+ blended = (1 - alpha) * original_np.astype(np.float32) + alpha * color_mask
65
+ return blended.clip(0, 255).astype(np.uint8)
66
+
67
+ def compute_class_stats(mask_np: np.ndarray):
68
+ total = mask_np.size
69
+ counts = np.bincount(mask_np.flatten(), minlength=len(COLOR_MAP))
70
+ stats = []
71
+ for info in CLASS_INFO:
72
+ cid = info["id"]
73
+ count = int(counts[cid]) if cid < len(counts) else 0
74
+ pct = (count / total * 100.0) if total else 0.0
75
+ stats.append({**info, "count": count, "pct": pct})
76
+ return stats
77
+
78
+ def build_legend_html(stats):
79
+ rows = []
80
+ for s in stats:
81
+ r, g, b = s["color"]
82
+ rows.append(f"""
83
+ <div class="legend-item" aria-label="Class {s['name']}">
84
+ <div class="legend-color" style="--c: rgb({r},{g},{b});"></div>
85
+ <div class="legend-meta">
86
+ <div class="legend-name">{s['id']}: {s['name']}</div>
87
+ <div class="legend-stats">
88
+ <span class="legend-count">{s['count']} px</span>
89
+ <span class="legend-pct">{s['pct']:.2f}%</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ """)
94
+ return f"""
95
+ <div class="legend-wrapper" id="legend-wrapper">
96
+ <div class="legend-header">
97
+ <span>Segmentation Legend</span>
98
+ <button onclick="toggleLegend()" class="legend-toggle-btn" aria-label="Collapse legend">⤢</button>
99
+ </div>
100
+ <div id="legend-body" class="legend-body expanded">
101
+ {''.join(rows)}
102
+ </div>
103
+ </div>
104
+ """
105
+
106
+ def raw_mask_download(mask_np: np.ndarray):
107
+ from io import BytesIO
108
+ import base64
109
+ img = Image.fromarray(mask_np.astype(np.uint8))
110
+ bio = BytesIO()
111
+ img.save(bio, format="PNG")
112
+ bio.seek(0)
113
+ return "data:image/png;base64," + base64.b64encode(bio.read()).decode()
114
+
115
+ def make_colored_mask_rgba(mask_np: np.ndarray, bg_opacity: float):
116
+ """
117
+ Return an RGBA image where background class (0) has adjustable opacity.
118
+ bg_opacity in [0,1].
119
+ """
120
+ rgb = COLOR_MAP[mask_np] # (H,W,3)
121
+ H, W = mask_np.shape
122
+ alpha_channel = np.full((H, W), 255, dtype=np.uint8)
123
+ alpha_channel[mask_np == 0] = int(bg_opacity * 255)
124
+ rgba = np.dstack([rgb, alpha_channel]).astype(np.uint8)
125
+ return Image.fromarray(rgba, mode="RGBA")
126
+
127
+ def run_segmentation(image, view_mode, alpha, show_colored, return_small, bg_opacity):
128
+ if model is None:
129
+ return (None, None, "<p class='legend-empty'>Model not loaded.</p>",
130
+ f"<span style='color:#ff8080'>{model_load_error or 'Model error.'}</span>")
131
+ if image is None:
132
+ return (None, None, "<p class='legend-empty'>No image yet.</p>",
133
+ "<span style='opacity:0.6'>No mask.</span>")
134
+
135
+ pred_mask = predict(image, model, DEVICE)
136
+ mask_small = pred_mask.numpy()
137
+
138
+ H, W = image.shape[:2]
139
+ if return_small:
140
+ mask_np = mask_small
141
+ if view_mode == "Overlay":
142
+ pil_orig = Image.fromarray(image.astype(np.uint8))
143
+ base_img = np.array(pil_orig.resize(mask_small.shape[::-1], resample=Image.BILINEAR))
144
+ else:
145
+ base_img = image
146
+ else:
147
+ mask_np = resize_mask_to_original(mask_small, (H, W))
148
+ base_img = image
149
+
150
+ if view_mode == "Colored Mask":
151
+ out_img = make_colored_mask_rgba(mask_np, bg_opacity)
152
+ elif view_mode == "Overlay":
153
+ blended = overlay_mask(base_img, mask_np, alpha=alpha)
154
+ out_img = Image.fromarray(blended)
155
+ else: # Raw Class Indices
156
+ max_id = len(COLOR_MAP) - 1
157
+ norm = (mask_np / max_id * 255).astype(np.uint8)
158
+ gray_rgb = np.stack([norm, norm, norm], axis=-1)
159
+ out_img = Image.fromarray(gray_rgb)
160
+
161
+ if show_colored:
162
+ colored_only = make_colored_mask_rgba(mask_np, bg_opacity)
163
+ else:
164
+ colored_only = None
165
+
166
+ stats = compute_class_stats(mask_np)
167
+ legend_html = build_legend_html(stats)
168
+ download_link = raw_mask_download(mask_np)
169
+ download_html = f"<a class='download-anchor' href='{download_link}' download='raw_mask.png'>Download Raw Mask (PNG)</a>"
170
+
171
+ return out_img, colored_only, legend_html, download_html
172
+
173
+ def clear_outputs():
174
+ return None, None, "<p class='legend-empty'>Cleared.</p>", "<div id='download-link'>Cleared.</div>"
175
+
176
+ # ---------------- Load CSS ----------------
177
+ css_path = Path(__file__).parent / "style.css"
178
+ css_text = css_path.read_text(encoding="utf-8")
179
+
180
+ # ---------------- Interface Layout ----------------
181
+ with gr.Blocks(css=css_text, title="Hey Inspector • Drone Bridge Image Segmentation") as demo:
182
+ gr.HTML("""
183
+ <div class="hero-banner floating">
184
+ <h1 class="hero-title">Hey Inspector • Drone Bridge Image Segmentation</h1>
185
+ </div>
186
+ """)
187
+ if model_load_error:
188
+ gr.HTML(f"<div style='color:#ff4d4d; font-weight:600; margin-bottom:10px;'>{model_load_error}</div>")
189
+
190
+ gr.HTML("<p class='intro-tagline'>Upload an image and choose how you want to visualize the segmentation.</p>")
191
+
192
+ with gr.Row():
193
+ with gr.Column(scale=5, elem_classes="panel glass left-panel"):
194
+ input_image = gr.Image(
195
+ label="Input Image",
196
+ type="numpy",
197
+ image_mode="RGB",
198
+ sources=["upload", "clipboard", "webcam"]
199
+ )
200
+ view_mode = gr.Radio(
201
+ ["Colored Mask", "Overlay", "Raw Class Indices"],
202
+ value="Colored Mask",
203
+ label="View Mode",
204
+ elem_id="view-mode-radio"
205
+ )
206
+ alpha = gr.Slider(
207
+ 0.0, 1.0, value=0.5, step=0.05,
208
+ label="Overlay Opacity",
209
+ elem_id="alpha-slider"
210
+ )
211
+ bg_opacity = gr.Slider(
212
+ 0.0, 1.0, value=1.0, step=0.05,
213
+ label="Background Opacity (Colored Mask)",
214
+ elem_id="bg-opacity-slider"
215
+ )
216
+ show_colored = gr.Checkbox(value=True, label="Show 'Colored Mask (Always)' panel")
217
+ return_small = gr.Checkbox(value=False, label="Return downsized (256x256) mask instead of original size")
218
+ with gr.Row():
219
+ run_btn = gr.Button("Run Segmentation", elem_id="run-btn", variant="primary")
220
+ clear_btn = gr.Button("Clear", elem_id="clear-btn")
221
+
222
+ with gr.Column(scale=7, elem_classes="panel glass right-panel"):
223
+ gr.Markdown("#### Results")
224
+ output_image = gr.Image(label="Result View", type="pil")
225
+ color_mask_output = gr.Image(label="Colored Mask (Always)", type="pil")
226
+ legend_html = gr.HTML("<p class='legend-empty'>Legend will appear here after segmentation.</p>")
227
+ download_html = gr.HTML("<div id='download-link'>No mask yet.</div>")
228
+
229
+ gr.Markdown("""
230
+ **Tips**
231
+ - Background Opacity affects only Colored Mask outputs (main and the 'always' panel).
232
+ - Set it to 0 to hide background and emphasize target classes.
233
+ - Overlay mode ignores the background opacity slider (uses original image + colored mask).
234
+ - Raw Class Indices is a grayscale class map.
235
+ """)
236
+
237
+ gr.HTML("""
238
+ <script>
239
+ function toggleLegend(){
240
+ const b = document.getElementById('legend-body');
241
+ if(b){ b.classList.toggle('collapsed'); }
242
+ }
243
+ function syncAlphaVisibility(){
244
+ const radios = document.querySelectorAll("#view-mode-radio input");
245
+ let mode = "Colored Mask";
246
+ radios.forEach(r => { if(r.checked) mode = r.value; });
247
+ const overlayWrap = document.querySelector("#alpha-slider")?.closest(".gr-form");
248
+ const overlayRange = document.querySelector("#alpha-slider input[type=range]");
249
+ const bgWrap = document.querySelector("#bg-opacity-slider")?.closest(".gr-form");
250
+ if(overlayRange){
251
+ if(mode === "Overlay"){
252
+ overlayRange.disabled = false;
253
+ if(overlayWrap) overlayWrap.style.opacity = "1";
254
+ } else {
255
+ overlayRange.disabled = true;
256
+ if(overlayWrap) overlayWrap.style.opacity = "0.35";
257
+ }
258
+ }
259
+ const bgRange = document.querySelector("#bg-opacity-slider input[type=range]");
260
+ if(bgRange){
261
+ if(mode === "Colored Mask"){
262
+ bgRange.disabled = false;
263
+ if(bgWrap) bgWrap.style.opacity = "1";
264
+ } else {
265
+ bgRange.disabled = true;
266
+ if(bgWrap) bgWrap.style.opacity = "0.35";
267
+ }
268
+ }
269
+ }
270
+ document.addEventListener("change", e => {
271
+ if(e.target && e.target.closest("#view-mode-radio")) syncAlphaVisibility();
272
+ });
273
+ window.addEventListener("load", syncAlphaVisibility);
274
+ </script>
275
+ """)
276
+
277
+ run_btn.click(
278
+ fn=run_segmentation,
279
+ inputs=[input_image, view_mode, alpha, show_colored, return_small, bg_opacity],
280
+ outputs=[output_image, color_mask_output, legend_html, download_html]
281
+ )
282
+ clear_btn.click(
283
+ fn=clear_outputs,
284
+ inputs=None,
285
+ outputs=[output_image, color_mask_output, legend_html, download_html]
286
+ )
287
+
288
+ if __name__ == "__main__":
289
+ demo.launch()
inference.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from collections import OrderedDict
3
+ import numpy as np
4
+ from PIL import Image
5
+ import torchvision.transforms as transforms
6
+ from model import get_model
7
+
8
+ _preprocess = transforms.Compose([
9
+ transforms.Resize((256, 256)),
10
+ transforms.ToTensor(),
11
+ transforms.Normalize(
12
+ mean=[0.485, 0.456, 0.406],
13
+ std=[0.229, 0.224, 0.225]
14
+ )
15
+ ])
16
+
17
+ def load_model(weights_path: str, device: torch.device):
18
+ checkpoint = torch.load(weights_path, map_location=device, weights_only=False)
19
+ if "model_state_dict" not in checkpoint:
20
+ raise KeyError("model_state_dict not found in checkpoint")
21
+ state_dict = checkpoint["model_state_dict"]
22
+ new_state_dict = OrderedDict()
23
+ for k, v in state_dict.items():
24
+ new_state_dict[k.replace("module.", "")] = v
25
+ model = get_model()
26
+ model.load_state_dict(new_state_dict)
27
+ model.to(device)
28
+ model.eval()
29
+ return model
30
+
31
+ def preprocess(image):
32
+ if isinstance(image, np.ndarray):
33
+ image = Image.fromarray(image)
34
+ return _preprocess(image).unsqueeze(0)
35
+
36
+ def predict(image, model, device):
37
+ model.eval()
38
+ with torch.no_grad():
39
+ tensor = preprocess(image).to(device)
40
+ output = model(tensor)
41
+ pred = torch.argmax(output, dim=1).squeeze(0).cpu()
42
+ return pred
model.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import segmentation_models_pytorch as smp
2
+
3
+ def get_model():
4
+ model = smp.UnetPlusPlus(
5
+ encoder_name="resnext101_32x4d",
6
+ encoder_weights=None, # using your own trained weights
7
+ in_channels=3,
8
+ classes=3,
9
+ activation=None
10
+ )
11
+ return model
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ torchvision
3
+ timm
4
+ segmentation-models-pytorch
5
+ gradio
6
+ Pillow
7
+ numpy
8
+ huggingface-hub
style.css ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Light mauve / blue gentle theme */
2
+
3
+ /* Core variables */
4
+ :root {
5
+ --bg-gradient: linear-gradient(135deg, #e3e9ff 0%, #d4dafc 35%, #c7d3fa 65%, #c0cef5 100%);
6
+ --panel-bg: rgba(255,255,255,0.55);
7
+ --panel-border: rgba(160,170,200,0.55);
8
+ --text-color: #1d2b3a;
9
+ --accent: #1d6fd4;
10
+ --accent-hover: #3d8af0;
11
+ --legend-bg: rgba(255,255,255,0.70);
12
+ --legend-border: rgba(140,160,190,0.55);
13
+ --scrollbar-bg: #d0d9f2;
14
+ --scrollbar-thumb: #9ab4e6;
15
+ --scrollbar-thumb-hover: #799dd9;
16
+ --radius: 18px;
17
+ --transition: 0.28s cubic-bezier(.4,.14,.3,1);
18
+ --font-stack: 'Inter','Segoe UI',system-ui,sans-serif;
19
+ }
20
+
21
+ body, .gradio-container {
22
+ background: var(--bg-gradient) !important;
23
+ font-family: var(--font-stack);
24
+ color: var(--text-color);
25
+ min-height: 100vh;
26
+ margin: 0;
27
+ padding-bottom: 40px;
28
+ transition: background 0.6s ease, color 0.4s ease;
29
+ -webkit-font-smoothing: antialiased;
30
+ }
31
+
32
+ /* Floating hero */
33
+ .hero-banner {
34
+ position: relative;
35
+ width: 100%;
36
+ border-radius: 24px;
37
+ margin: 18px 0 30px 0;
38
+ background: linear-gradient(125deg, #6289ff, #7d9dff 40%, #8bb1ff 75%);
39
+ box-shadow: 0 14px 40px -10px rgba(60,85,140,0.45), 0 4px 18px -6px rgba(60,85,140,0.35);
40
+ padding: 34px 30px;
41
+ overflow: hidden;
42
+ }
43
+
44
+ .hero-banner:before,
45
+ .hero-banner:after {
46
+ content:"";
47
+ position:absolute;
48
+ width:240px;
49
+ height:240px;
50
+ background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.55), transparent 70%);
51
+ top:-60px;
52
+ left:-60px;
53
+ filter: blur(10px);
54
+ opacity: 0.55;
55
+ pointer-events:none;
56
+ }
57
+ .hero-banner:after {
58
+ top:auto;
59
+ left:auto;
60
+ bottom:-70px;
61
+ right:-40px;
62
+ background: radial-gradient(circle at 70% 70%, rgba(255,255,255,0.45), transparent 65%);
63
+ opacity:0.45;
64
+ }
65
+
66
+ .floating {
67
+ animation: floatY 6.5s ease-in-out infinite;
68
+ }
69
+
70
+ @keyframes floatY {
71
+ 0% { transform: translateY(0px); }
72
+ 50% { transform: translateY(-10px); }
73
+ 100% { transform: translateY(0px); }
74
+ }
75
+
76
+ .hero-title {
77
+ font-size: 2rem;
78
+ font-weight: 700;
79
+ letter-spacing: 1px;
80
+ color: #ffffff;
81
+ margin: 0;
82
+ line-height: 1.15;
83
+ text-shadow: 0 4px 18px rgba(0,0,0,0.35);
84
+ position: relative;
85
+ z-index: 2;
86
+ }
87
+
88
+ /* Tagline forced white */
89
+ .intro-tagline {
90
+ font-size: 1rem;
91
+ font-weight: 500;
92
+ letter-spacing: 0.5px;
93
+ margin: 0 6px 24px 6px;
94
+ color: #ffffff !important;
95
+ text-shadow: 0 2px 8px rgba(0,0,0,0.35);
96
+ background: rgba(255,255,255,0.10);
97
+ padding: 10px 16px;
98
+ border-radius: 14px;
99
+ display: inline-block;
100
+ backdrop-filter: blur(6px);
101
+ box-shadow: 0 6px 24px -10px rgba(60,85,140,0.50);
102
+ }
103
+
104
+ /* Panels */
105
+ .panel.glass {
106
+ background: var(--panel-bg) !important;
107
+ border: 1px solid var(--panel-border) !important;
108
+ backdrop-filter: blur(14px) saturate(160%);
109
+ border-radius: var(--radius) !important;
110
+ padding: 20px !important;
111
+ box-shadow: 0 10px 30px -12px rgba(70,80,120,0.45);
112
+ position: relative;
113
+ overflow: hidden;
114
+ }
115
+
116
+ .panel.glass:before {
117
+ content:"";
118
+ position:absolute;
119
+ inset:0;
120
+ background:
121
+ radial-gradient(circle at 80% 10%, rgba(255,255,255,0.35), transparent 55%),
122
+ radial-gradient(circle at 15% 85%, rgba(255,255,255,0.25), transparent 60%);
123
+ pointer-events:none;
124
+ opacity:0.55;
125
+ }
126
+
127
+ #view-mode-radio label {
128
+ background: rgba(255,255,255,0.55);
129
+ border: 1px solid rgba(140,160,190,0.6);
130
+ border-radius: 12px;
131
+ padding: 7px 12px;
132
+ cursor: pointer;
133
+ transition: var(--transition);
134
+ font-weight: 500;
135
+ font-size: 0.85rem;
136
+ color: #2d3f55;
137
+ box-shadow: 0 2px 8px -4px rgba(80,100,140,0.35);
138
+ }
139
+ #view-mode-radio label:hover {
140
+ background: rgba(255,255,255,0.75);
141
+ }
142
+ #view-mode-radio input:checked + label {
143
+ background: linear-gradient(135deg, var(--accent), var(--accent-hover));
144
+ border-color: rgba(255,255,255,0.8);
145
+ color: #fff;
146
+ box-shadow: 0 4px 15px -6px rgba(30,70,130,0.65);
147
+ }
148
+
149
+ #alpha-slider input[type=range] {
150
+ accent-color: var(--accent);
151
+ }
152
+
153
+ #run-btn, #clear-btn {
154
+ font-weight: 600;
155
+ border-radius: 16px !important;
156
+ padding: 12px 20px !important;
157
+ letter-spacing: 0.6px;
158
+ transition: var(--transition);
159
+ border: none !important;
160
+ font-size: 0.9rem;
161
+ }
162
+
163
+ #run-btn {
164
+ background: linear-gradient(135deg, var(--accent), var(--accent-hover)) !important;
165
+ color: #fff !important;
166
+ box-shadow: 0 8px 24px -10px rgba(30,70,130,0.65);
167
+ }
168
+ #run-btn:hover {
169
+ transform: translateY(-4px);
170
+ box-shadow: 0 12px 30px -10px rgba(30,70,130,0.75);
171
+ }
172
+
173
+ #clear-btn {
174
+ background: rgba(200,60,60,0.15) !important;
175
+ color: #9d2e2e !important;
176
+ border: 1px solid rgba(200,60,60,0.35) !important;
177
+ box-shadow: 0 4px 16px -10px rgba(200,60,60,0.55);
178
+ }
179
+ #clear-btn:hover {
180
+ background: rgba(200,60,60,0.30) !important;
181
+ color: #6a1212 !important;
182
+ transform: translateY(-2px);
183
+ }
184
+
185
+ .download-anchor {
186
+ font-size: 0.95rem;
187
+ font-weight: 600;
188
+ text-decoration: none;
189
+ color: var(--accent);
190
+ display: inline-block;
191
+ margin-top: 10px;
192
+ transition: var(--transition);
193
+ letter-spacing: 0.5px;
194
+ }
195
+ .download-anchor:hover {
196
+ color: var(--accent-hover);
197
+ text-shadow: 0 0 6px rgba(120,170,255,0.55);
198
+ }
199
+
200
+ /* Legend */
201
+ .legend-wrapper {
202
+ margin-top: 14px;
203
+ background: var(--legend-bg);
204
+ border: 1px solid var(--legend-border);
205
+ border-radius: 16px;
206
+ padding: 14px 16px 12px;
207
+ position: relative;
208
+ overflow: hidden;
209
+ backdrop-filter: blur(12px);
210
+ animation: fadeIn 0.5s ease;
211
+ box-shadow: 0 10px 28px -14px rgba(70,80,120,0.55);
212
+ }
213
+
214
+ .legend-header {
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: space-between;
218
+ font-weight: 600;
219
+ letter-spacing: 0.6px;
220
+ color: var(--text-color);
221
+ margin-bottom: 6px;
222
+ }
223
+
224
+ .legend-toggle-btn {
225
+ background: rgba(255,255,255,0.55);
226
+ border: 1px solid rgba(130,150,180,0.55);
227
+ padding: 4px 11px;
228
+ cursor: pointer;
229
+ border-radius: 10px;
230
+ font-size: 0.78rem;
231
+ color: #2d3f55;
232
+ transition: var(--transition);
233
+ box-shadow: 0 3px 12px -6px rgba(60,80,120,0.35);
234
+ }
235
+ .legend-toggle-btn:hover {
236
+ background: rgba(255,255,255,0.8);
237
+ transform: translateY(-2px);
238
+ }
239
+
240
+ .legend-body {
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: 10px;
244
+ max-height: 240px;
245
+ overflow-y: auto;
246
+ padding-right: 4px;
247
+ transition: max-height 0.5s ease;
248
+ }
249
+
250
+ .legend-body.collapsed {
251
+ max-height: 0;
252
+ overflow: hidden;
253
+ padding: 0;
254
+ margin: 0;
255
+ }
256
+
257
+ .legend-item {
258
+ display: flex;
259
+ gap: 14px;
260
+ align-items: center;
261
+ background: rgba(255,255,255,0.55);
262
+ padding: 8px 12px;
263
+ border-radius: 14px;
264
+ border: 1px solid rgba(130,150,180,0.45);
265
+ position: relative;
266
+ backdrop-filter: blur(6px);
267
+ transition: var(--transition);
268
+ box-shadow: 0 4px 16px -10px rgba(60,80,120,0.4);
269
+ }
270
+
271
+ .legend-item:hover {
272
+ background: rgba(255,255,255,0.75);
273
+ transform: translateY(-3px);
274
+ }
275
+
276
+ .legend-color {
277
+ width: 42px;
278
+ height: 42px;
279
+ border-radius: 11px;
280
+ background: var(--c);
281
+ box-shadow: 0 4px 18px -8px var(--c);
282
+ border: 2px solid rgba(255,255,255,0.7);
283
+ position: relative;
284
+ }
285
+
286
+ .legend-color:after {
287
+ content:"";
288
+ position:absolute;
289
+ inset:0;
290
+ border-radius: 9px;
291
+ background: linear-gradient(140deg, rgba(255,255,255,0.40), transparent 70%);
292
+ mix-blend-mode: overlay;
293
+ }
294
+
295
+ .legend-meta {
296
+ display: flex;
297
+ flex-direction: column;
298
+ font-size: 0.78rem;
299
+ line-height: 1.1rem;
300
+ letter-spacing: 0.4px;
301
+ }
302
+
303
+ .legend-name {
304
+ font-weight: 600;
305
+ font-size: 0.9rem;
306
+ text-transform: uppercase;
307
+ color:#2e3f58;
308
+ }
309
+
310
+ .legend-stats {
311
+ display: flex;
312
+ gap: 12px;
313
+ font-size: 0.68rem;
314
+ opacity: 0.85;
315
+ color:#415671;
316
+ }
317
+
318
+ .legend-count { color: #9b6c10; }
319
+ .legend-pct { color: #115f9d; }
320
+
321
+ .legend-empty {
322
+ opacity: 0.65;
323
+ font-style: italic;
324
+ font-size: 0.9rem;
325
+ padding: 4px;
326
+ color:#2d3f55;
327
+ }
328
+
329
+ /* Scrollbars */
330
+ .legend-body::-webkit-scrollbar {
331
+ width: 9px;
332
+ }
333
+ .legend-body::-webkit-scrollbar-track {
334
+ background: var(--scrollbar-bg);
335
+ border-radius: 10px;
336
+ }
337
+ .legend-body::-webkit-scrollbar-thumb {
338
+ background: var(--scrollbar-thumb);
339
+ border-radius: 10px;
340
+ border: 1px solid rgba(255,255,255,0.4);
341
+ }
342
+ .legend-body::-webkit-scrollbar-thumb:hover {
343
+ background: var(--scrollbar-thumb-hover);
344
+ }
345
+
346
+ /* Images */
347
+ .gradio-image img, .gradio-image canvas {
348
+ border-radius: 16px !important;
349
+ box-shadow: 0 10px 32px -14px rgba(70,80,120,0.55);
350
+ }
351
+
352
+ /* Animations */
353
+ @keyframes fadeIn {
354
+ from { opacity: 0; transform: translateY(8px); }
355
+ to { opacity: 1; transform: translateY(0); }
356
+ }
357
+
358
+ /* Disabled slider style */
359
+ #alpha-slider input[disabled] {
360
+ filter: grayscale(75%);
361
+ cursor: not-allowed;
362
+ opacity: 0.6;
363
+ }
364
+
365
+ /* Markdown headings */
366
+ .markdown-body h4, .markdown-body h3, .markdown-body h2 {
367
+ color: #2d3f55;
368
+ }
369
+
370
+ /* Links */
371
+ a { color: var(--accent); }
372
+ a:hover { color: var(--accent-hover); text-decoration: underline; }
373
+
374
+ /* Responsive */
375
+ @media (max-width: 980px) {
376
+ .hero-banner { padding: 28px 22px; }
377
+ .hero-title { font-size: 1.55rem; }
378
+ #run-btn, #clear-btn { width: 100%; }
379
+ .legend-body { max-height: 200px; }
380
+ }
381
+
382
+ #bg-opacity-slider input[type=range] {
383
+ accent-color: var(--accent);
384
+ }
385
+
386
+ /* Disabled range (already present for alpha; ensure both look consistent) */
387
+ #bg-opacity-slider input[disabled] {
388
+ filter: grayscale(75%);
389
+ cursor: not-allowed;
390
+ opacity: 0.55;
391
+ }