multimodalart HF Staff commited on
Commit
32a2d49
·
verified ·
1 Parent(s): 7059126

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/example_1.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/example_3.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/example_5.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,42 @@
1
  ---
2
  title: Stable Layers
3
- emoji: 🐨
4
- colorFrom: pink
5
- colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.22.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Stable Layers
3
+ emoji: 🎨
4
+ colorFrom: yellow
5
+ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.22.0
 
8
  app_file: app.py
9
  pinned: false
10
+ short_description: Decompose images into editable RGBA layers
11
+ python_version: "3.12"
12
+ models:
13
+ - Qwen/Qwen-Image-Layered
14
+ - StabilityLabs/Stable-Layers
15
  ---
16
 
17
+ # Stable Layers — Image Decomposition
18
+
19
+ Decompose any image into a stack of editable RGBA layers (background + objects) using
20
+ [StabilityLabs/Stable-Layers](https://huggingface.co/StabilityLabs/Stable-Layers), a LoRA
21
+ adapter fine-tuned on the [Qwen/Qwen-Image-Layered](https://huggingface.co/Qwen/Qwen-Image-Layered)
22
+ base pipeline.
23
+
24
+ ## How it works
25
+
26
+ Upload an image and the model decomposes it into ordered, back-to-front RGBA layers
27
+ suitable for compositing and editing. Layer 0 is the background (inpainted behind the
28
+ removed objects); higher-index layers sit on top.
29
+
30
+ **Inference settings** (locked to the model card's recommended recipe):
31
+ - Heun sampler (2nd order) — 50 steps
32
+ - CFG 1.0 (disabled)
33
+ - 640px max dimension
34
+ - 4 layers by default
35
+
36
+ The transparent RGBA toggle controls whether layers are saved with real alpha (for
37
+ compositing in an editor) or composited onto white (for easy eyeballing).
38
+
39
+ ## Credits
40
+
41
+ Example images sourced from the [Qwen-Image-Layered demo Space](https://huggingface.co/spaces/Qwen/Qwen-Image-Layered)
42
+ (Apache 2.0 license).
app.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces # MUST be first
2
+ import os
3
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
4
+
5
+ import tempfile
6
+ import zipfile
7
+ import random
8
+ import numpy as np
9
+ import torch
10
+ import gradio as gr
11
+ from PIL import Image
12
+ from pathlib import Path
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Constants — from decompose.py (reference inference script)
16
+ # ---------------------------------------------------------------------------
17
+ BASE_MODEL = "Qwen/Qwen-Image-Layered"
18
+ LORA_REPO = "StabilityLabs/Stable-Layers"
19
+ LORA_SUBFOLDER = "model" # adapter_config.json + adapter_model.safetensors live under model/
20
+
21
+ PROMPT_TEMPLATE = (
22
+ "system\nDescribe the image by detailing the color, shape, size, "
23
+ "texture, quantity, text, spatial relationships of the objects and background:"
24
+ "\nuser\n{}\nassistant\n"
25
+ )
26
+ PROMPT_TEMPLATE_DROP_IDX = 34
27
+ DEFAULT_PROMPT = "a clean, well composed image"
28
+ VAE_SCALE_FACTOR = 8
29
+ MAX_SEED = np.iinfo(np.int32).max
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Latent helpers — verbatim from decompose.py
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def rgb_to_rgba(image):
36
+ b, _, h, w = image.shape
37
+ alpha = torch.ones(b, 1, h, w, device=image.device, dtype=image.dtype)
38
+ return torch.cat([image, alpha], dim=1)
39
+
40
+
41
+ def normalize_latents(latents, vae):
42
+ mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
43
+ std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
44
+ return (latents - mean) / std
45
+
46
+
47
+ def denormalize_latents(latents, vae):
48
+ mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
49
+ inv_std = (1.0 / torch.tensor(vae.config.latents_std)).view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
50
+ return latents / inv_std + mean
51
+
52
+
53
+ def pack_latents(latents, batch_size, num_channels, height, width, num_frames):
54
+ latents = latents.view(batch_size, num_frames, num_channels, height // 2, 2, width // 2, 2)
55
+ latents = latents.permute(0, 1, 3, 5, 2, 4, 6)
56
+ return latents.reshape(batch_size, num_frames * (height // 2) * (width // 2), num_channels * 4)
57
+
58
+
59
+ def unpack_latents(latents, height, width, num_layers, vae_scale_factor=VAE_SCALE_FACTOR):
60
+ batch_size, _, channels = latents.shape
61
+ frames = num_layers + 1
62
+ h = 2 * (int(height) // (vae_scale_factor * 2))
63
+ w = 2 * (int(width) // (vae_scale_factor * 2))
64
+ latents = latents.view(batch_size, frames, h // 2, w // 2, channels // 4, 2, 2)
65
+ latents = latents.permute(0, 1, 4, 2, 5, 3, 6)
66
+ latents = latents.reshape(batch_size, frames, channels // 4, h, w)
67
+ return latents.permute(0, 2, 1, 3, 4)
68
+
69
+
70
+ @torch.no_grad()
71
+ def encode_condition_image(vae, image):
72
+ """RGB image tensor in [-1,1], (B,3,H,W) -> packed condition latents."""
73
+ b = image.shape[0]
74
+ vae_input_ch = getattr(vae.config, "input_channels", 3)
75
+ if vae_input_ch == 4 and image.shape[1] == 3:
76
+ image = rgb_to_rgba(image)
77
+ elif vae_input_ch == 3 and image.shape[1] == 4:
78
+ image = image[:, :3]
79
+
80
+ image_5d = image.unsqueeze(2).to(dtype=vae.dtype)
81
+ dist = vae.encode(image_5d)
82
+ if hasattr(dist, "latent_dist"):
83
+ latents = dist.latent_dist.mode()
84
+ elif hasattr(dist, "mode"):
85
+ latents = dist.mode()
86
+ else:
87
+ latents = dist
88
+
89
+ latents = normalize_latents(latents, vae)
90
+ z_dim, lh, lw = latents.shape[1], latents.shape[3], latents.shape[4]
91
+ latents = latents.permute(0, 2, 1, 3, 4)
92
+ return pack_latents(latents, b, z_dim, lh, lw, 1).to(dtype=torch.bfloat16)
93
+
94
+
95
+ @torch.no_grad()
96
+ def decode_layers(vae, latents, height, width, num_layers):
97
+ b = latents.shape[0]
98
+ unpacked = unpack_latents(latents, height, width, num_layers)
99
+ unpacked = denormalize_latents(unpacked, vae)
100
+ layer_latents = unpacked[:, :, 1:].permute(0, 2, 1, 3, 4) # drop base frame
101
+ _, _, c, h, w = layer_latents.shape
102
+ decoded = vae.decode(layer_latents.reshape(b * num_layers, c, 1, h, w).to(dtype=vae.dtype),
103
+ return_dict=False)[0]
104
+ decoded = decoded.squeeze(2).float().clamp(-1.0, 1.0)
105
+ _, c_out, h_out, w_out = decoded.shape
106
+ return decoded.reshape(b, num_layers, c_out, h_out, w_out)
107
+
108
+
109
+ def composite_layers(layers):
110
+ """Porter-Duff 'over', back-to-front."""
111
+ if layers.shape[2] == 3:
112
+ return layers.mean(dim=1)
113
+ result = torch.zeros_like(layers[:, 0, :3])
114
+ for i in range(layers.shape[1]):
115
+ rgb = layers[:, i, :3]
116
+ alpha = (layers[:, i, 3:4] + 1.0) / 2.0
117
+ result = rgb * alpha + result * (1.0 - alpha)
118
+ return result
119
+
120
+
121
+ def tensor_to_pil(t, mode="RGB"):
122
+ arr = ((t.clamp(-1, 1) + 1) / 2 * 255).byte().permute(1, 2, 0).cpu().numpy()
123
+ return Image.fromarray(arr, mode=mode)
124
+
125
+
126
+ def layer_to_pil_white_bg(layer):
127
+ if layer.shape[0] == 4:
128
+ rgb, alpha = layer[:3], (layer[3:4] + 1.0) / 2.0
129
+ return tensor_to_pil(rgb * alpha + torch.ones_like(rgb) * (1.0 - alpha))
130
+ return tensor_to_pil(layer)
131
+
132
+
133
+ def layer_to_pil_rgba(layer):
134
+ layer = layer.clamp(-1, 1).float()
135
+ rgba = torch.cat([(layer[:3] + 1) / 2, (layer[3:4] + 1) / 2], dim=0)
136
+ arr = (rgba * 255).byte().permute(1, 2, 0).cpu().numpy()
137
+ return Image.fromarray(arr, mode="RGBA")
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Denoising — Heun (2nd order) — verbatim from decompose.py
142
+ # ---------------------------------------------------------------------------
143
+
144
+ def transformer_forward(transformer, hidden_states, timestep, encoder_hidden_states,
145
+ img_shapes, encoder_hidden_states_mask=None, additional_t_cond=None):
146
+ return transformer(
147
+ hidden_states=hidden_states,
148
+ timestep=timestep / 1000,
149
+ encoder_hidden_states=encoder_hidden_states,
150
+ encoder_hidden_states_mask=encoder_hidden_states_mask,
151
+ img_shapes=img_shapes,
152
+ guidance=None,
153
+ additional_t_cond=additional_t_cond,
154
+ return_dict=False,
155
+ )[0]
156
+
157
+
158
+ def build_img_shapes(num_layers, packed_h, packed_w):
159
+ return [[*[(1, packed_h, packed_w) for _ in range(num_layers + 1)], (1, packed_h, packed_w)]]
160
+
161
+
162
+ @torch.no_grad()
163
+ def denoise(transformer, scheduler, latents, timesteps, prompt_embeds, prompt_mask,
164
+ img_shapes, condition_latents, guidance_scale,
165
+ neg_embeds=None, neg_mask=None, additional_t_cond=None):
166
+ """Heun's method (2nd order) over the scheduler's sigmas."""
167
+ gen_seq_len = latents.shape[1]
168
+ do_cfg = guidance_scale > 1.0 and neg_embeds is not None
169
+
170
+ def velocity(lat, t_val):
171
+ ts = t_val.expand(1).to(lat.dtype)
172
+ model_input = lat if condition_latents is None else torch.cat([lat, condition_latents], dim=1)
173
+ v = transformer_forward(transformer, model_input, ts, prompt_embeds,
174
+ img_shapes, prompt_mask, additional_t_cond)[:, :gen_seq_len]
175
+ if do_cfg:
176
+ v_neg = transformer_forward(transformer, model_input, ts, neg_embeds,
177
+ img_shapes, neg_mask, additional_t_cond)[:, :gen_seq_len]
178
+ v = v_neg + guidance_scale * (v - v_neg)
179
+ return v
180
+
181
+ sigmas = scheduler.sigmas
182
+ for i, t in enumerate(timesteps):
183
+ sigma = sigmas[i]
184
+ sigma_next = sigmas[i + 1] if i + 1 < len(sigmas) else torch.tensor(0.0, device=t.device)
185
+ dt = sigma_next - sigma
186
+
187
+ v1 = velocity(latents, t)
188
+ latents_mid = latents + dt * v1
189
+ if sigma_next > 0: # Heun corrector
190
+ v2 = velocity(latents_mid, sigma_next * 1000)
191
+ latents = latents + dt * 0.5 * (v1 + v2)
192
+ else:
193
+ latents = latents_mid
194
+ return latents
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Resize helper — from decompose.py
199
+ # ---------------------------------------------------------------------------
200
+
201
+ def compute_aspect_resize(orig_w, orig_h, max_size):
202
+ """Keep aspect ratio, max dim = max_size, both dims divisible by 16."""
203
+ scale = max_size / max(orig_w, orig_h)
204
+ return (max(int(round(orig_w * scale / 16)) * 16, 16),
205
+ max(int(round(orig_h * scale / 16)) * 16, 16))
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Model loading at module scope (ZeroGPU rule)
210
+ # ---------------------------------------------------------------------------
211
+
212
+ print("Loading base model: " + BASE_MODEL)
213
+ from diffusers import DiffusionPipeline
214
+ from peft import PeftModel
215
+
216
+ pipe = DiffusionPipeline.from_pretrained(
217
+ BASE_MODEL, torch_dtype=torch.bfloat16,
218
+ trust_remote_code=True,
219
+ )
220
+ transformer = pipe.transformer.to("cuda").eval()
221
+ vae = pipe.vae.to("cuda").eval()
222
+ scheduler = pipe.scheduler
223
+ tokenizer = getattr(pipe, "tokenizer", None)
224
+ text_encoder = getattr(pipe, "text_encoder", None)
225
+ del pipe
226
+ transformer.requires_grad_(False)
227
+ vae.requires_grad_(False)
228
+
229
+ print("Loading LoRA: " + LORA_REPO + "/" + LORA_SUBFOLDER)
230
+ transformer = PeftModel.from_pretrained(transformer, LORA_REPO, subfolder=LORA_SUBFOLDER)
231
+ transformer.eval()
232
+
233
+ in_channels = getattr(transformer.config, "in_channels", 64)
234
+ additional_t_cond = torch.zeros(1, dtype=torch.long, device="cuda")
235
+
236
+ if text_encoder is None or tokenizer is None:
237
+ raise RuntimeError("base model has no text encoder/tokenizer")
238
+ text_encoder = text_encoder.to("cuda").eval()
239
+ text_encoder.requires_grad_(False)
240
+
241
+ import inspect
242
+ supports_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters)
243
+
244
+
245
+ @torch.no_grad()
246
+ def encode_prompt(text):
247
+ tokens = tokenizer([PROMPT_TEMPLATE.format(text)], padding=True,
248
+ return_tensors="pt").to(text_encoder.device)
249
+ out = text_encoder(input_ids=tokens.input_ids,
250
+ attention_mask=tokens.attention_mask,
251
+ output_hidden_states=True)
252
+ hidden = out.hidden_states[-1]
253
+ mask_bool = tokens.attention_mask.bool()
254
+ lengths = mask_bool.sum(dim=1)
255
+ chunks = torch.split(hidden[mask_bool], lengths.tolist(), dim=0)
256
+ chunks = [c[PROMPT_TEMPLATE_DROP_IDX:] for c in chunks] # drop system prefix
257
+ max_len = max(c.size(0) for c in chunks)
258
+ embeds = torch.stack([torch.cat([c, c.new_zeros(max_len - c.size(0), c.size(1))]) for c in chunks])
259
+ mask = torch.stack([
260
+ torch.cat([torch.ones(c.size(0), dtype=torch.long, device=c.device),
261
+ torch.zeros(max_len - c.size(0), dtype=torch.long, device=c.device)])
262
+ for c in chunks
263
+ ])
264
+ return embeds.to(dtype=torch.bfloat16, device="cuda"), mask.to("cuda")
265
+
266
+
267
+ # Pre-encode default prompt and negative prompt at module scope
268
+ prompt_embeds, prompt_mask = encode_prompt(DEFAULT_PROMPT)
269
+ neg_embeds, neg_mask = encode_prompt("")
270
+
271
+ print("Model loaded successfully.")
272
+
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # Inference
276
+ # ---------------------------------------------------------------------------
277
+
278
+ @spaces.GPU(duration=300)
279
+ def decompose(input_image, num_layers=4, transparent=True, seed=42,
280
+ randomize_seed=False, prompt="", progress=gr.Progress(track_tqdm=True)):
281
+ """Decompose an image into editable RGBA layers using Stable Layers.
282
+
283
+ Args:
284
+ input_image: Source image to decompose into layers.
285
+ num_layers: Number of layers to decompose into (2-6 recommended).
286
+ transparent: If True, output RGBA layers with real alpha; if False, composite onto white.
287
+ seed: Random seed for reproducibility.
288
+ randomize_seed: If True, pick a random seed each run.
289
+ prompt: Optional text prompt describing the image (default works well).
290
+ """
291
+ if input_image is None:
292
+ raise gr.Error("Please upload an image first.")
293
+
294
+ if randomize_seed:
295
+ seed = random.randint(0, MAX_SEED)
296
+ seed = int(seed)
297
+
298
+ if isinstance(input_image, np.ndarray):
299
+ pil_image = Image.fromarray(input_image)
300
+ else:
301
+ pil_image = input_image
302
+ image = pil_image.convert("RGB")
303
+
304
+ tw, th = compute_aspect_resize(*image.size, 640)
305
+ source = image.resize((tw, th), Image.LANCZOS)
306
+
307
+ packed_h = (th // VAE_SCALE_FACTOR) // 2
308
+ packed_w = (tw // VAE_SCALE_FACTOR) // 2
309
+
310
+ img_t = torch.from_numpy(np.array(source)).permute(2, 0, 1).float() / 127.5 - 1.0
311
+ img_t = img_t.unsqueeze(0).to("cuda")
312
+ condition_latents = encode_condition_image(vae, img_t)
313
+
314
+ # dynamic-shift mu from the condition sequence length
315
+ mu = (condition_latents.shape[1] / (256 * 256 / 16 / 16)) ** 0.5
316
+ if supports_sigmas:
317
+ scheduler.set_timesteps(sigmas=np.linspace(1.0, 0, 50 + 1)[:-1],
318
+ device="cuda", mu=mu)
319
+ else:
320
+ scheduler.set_timesteps(num_inference_steps=50, device="cuda")
321
+
322
+ img_shapes = build_img_shapes(num_layers, packed_h, packed_w)
323
+ seq_len = (num_layers + 1) * packed_h * packed_w
324
+
325
+ gen = torch.Generator(device="cuda").manual_seed(seed)
326
+ latents = torch.randn(1, seq_len, in_channels, device="cuda",
327
+ dtype=torch.bfloat16, generator=gen)
328
+
329
+ # Use custom prompt if provided, else use pre-encoded default
330
+ if prompt and prompt.strip():
331
+ p_embeds, p_mask = encode_prompt(prompt.strip())
332
+ else:
333
+ p_embeds, p_mask = prompt_embeds, prompt_mask
334
+
335
+ latents = denoise(transformer, scheduler, latents, scheduler.timesteps,
336
+ p_embeds, p_mask, img_shapes, condition_latents,
337
+ 1.0, neg_embeds, neg_mask, additional_t_cond)
338
+
339
+ decoded = decode_layers(vae, latents, th, tw, num_layers)
340
+ composite = composite_layers(decoded)
341
+
342
+ # Build output gallery: [composite] + [layer_0..N-1]
343
+ output_images = []
344
+ composite_pil = tensor_to_pil(composite[0])
345
+ output_images.append(("composite", composite_pil))
346
+
347
+ for li in range(num_layers):
348
+ layer = decoded[0, li]
349
+ pil = layer_to_pil_rgba(layer) if transparent else layer_to_pil_white_bg(layer)
350
+ label = "background" if li == 0 else f"layer {li}"
351
+ output_images.append((label, pil))
352
+
353
+ # Create a ZIP file for download
354
+ temp_files = []
355
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
356
+ with zipfile.ZipFile(tmp.name, 'w', zipfile.ZIP_DEFLATED) as zipf:
357
+ composite_pil.save(tmp.name.replace('.zip', '_composite.png'))
358
+ zipf.write(tmp.name.replace('.zip', '_composite.png'), "composite.png")
359
+ for i, (label, pil) in enumerate(output_images[1:]):
360
+ fname = f"layer_{i}.png"
361
+ pil_path = tmp.name.replace('.zip', f'_{fname}')
362
+ pil.save(pil_path)
363
+ zipf.write(pil_path, fname)
364
+ temp_files.append(pil_path)
365
+ temp_files.append(tmp.name.replace('.zip', '_composite.png'))
366
+ zip_path = tmp.name
367
+
368
+ # Clean up temp files
369
+ for tf in temp_files:
370
+ try:
371
+ os.unlink(tf)
372
+ except Exception:
373
+ pass
374
+
375
+ # Return gallery as list of (image, caption) tuples + zip file + seed
376
+ gallery_items = [(pil, label) for label, pil in output_images]
377
+ return gallery_items, zip_path, seed
378
+
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # Gradio UI
382
+ # ---------------------------------------------------------------------------
383
+
384
+ CSS = """
385
+ #col-container { max-width: 1200px; margin: 0 auto; }
386
+ .dark .gradio-container { color: var(--body-text-color); }
387
+ """
388
+
389
+ with gr.Blocks(title="Stable Layers — Image Decomposition", css=CSS) as demo:
390
+ with gr.Column(elem_id="col-container"):
391
+ gr.Markdown(
392
+ "# 🎨 Stable Layers\n"
393
+ "Decompose any image into editable RGBA layers using "
394
+ "[StabilityLabs/Stable-Layers](https://huggingface.co/StabilityLabs/Stable-Layers), "
395
+ "a LoRA adapter on [Qwen/Qwen-Image-Layered](https://huggingface.co/Qwen/Qwen-Image-Layered)."
396
+ )
397
+ gr.Markdown(
398
+ "Upload an image and the model decomposes it into **background + object layers** "
399
+ "(back-to-front, Porter-Duff 'over' compositing). "
400
+ "Each layer is an editable RGBA image you can download as a ZIP."
401
+ )
402
+
403
+ with gr.Row():
404
+ with gr.Column(scale=1):
405
+ input_image = gr.Image(
406
+ label="Input Image",
407
+ type="pil",
408
+ image_mode="RGB",
409
+ )
410
+ num_layers = gr.Slider(
411
+ label="Number of layers",
412
+ minimum=2,
413
+ maximum=6,
414
+ step=1,
415
+ value=4,
416
+ )
417
+ transparent = gr.Checkbox(
418
+ label="Transparent RGBA layers (real alpha for compositing)",
419
+ value=True,
420
+ )
421
+ run_button = gr.Button("Decompose!", variant="primary", size="lg")
422
+
423
+ with gr.Accordion("Advanced Settings", open=False):
424
+ prompt = gr.Textbox(
425
+ label="Prompt (optional — default works well)",
426
+ placeholder="a clean, well composed image",
427
+ value="",
428
+ lines=2,
429
+ )
430
+ seed = gr.Slider(
431
+ label="Seed",
432
+ minimum=0,
433
+ maximum=MAX_SEED,
434
+ step=1,
435
+ value=42,
436
+ )
437
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=False)
438
+ gr.Markdown(
439
+ "_Heun sampler · 50 steps · CFG 1.0 · 640px are locked to the "
440
+ "recommended settings from the model card._"
441
+ )
442
+
443
+ with gr.Column(scale=2):
444
+ gallery = gr.Gallery(
445
+ label="Decomposed Layers",
446
+ columns=3,
447
+ height="auto",
448
+ format="png",
449
+ )
450
+ zip_file = gr.File(label="Download all layers (ZIP)")
451
+ used_seed = gr.Number(label="Seed used", precision=0, interactive=False)
452
+
453
+ gr.Examples(
454
+ examples=[
455
+ ["assets/example_1.png", 4, True],
456
+ ["assets/example_3.png", 4, True],
457
+ ["assets/example_5.png", 4, True],
458
+ ],
459
+ inputs=[input_image, num_layers, transparent],
460
+ outputs=[gallery, zip_file, used_seed],
461
+ fn=decompose,
462
+ cache_examples=True,
463
+ cache_mode="lazy",
464
+ )
465
+
466
+ run_button.click(
467
+ fn=decompose,
468
+ inputs=[input_image, num_layers, transparent, seed, randomize_seed, prompt],
469
+ outputs=[gallery, zip_file, used_seed],
470
+ )
471
+
472
+ if __name__ == "__main__":
473
+ demo.launch(theme=gr.themes.Citrus(), mcp_server=True)
assets/example_1.png ADDED

Git LFS Details

  • SHA256: 8e46c75427751d043b9cab2f8d887954a4c1b3b1439d8a40b318eb0e0688d700
  • Pointer size: 132 Bytes
  • Size of remote file: 1.48 MB
assets/example_3.png ADDED

Git LFS Details

  • SHA256: 75a6e1b27b07be60668a5b834fc23d32eb648babf5f91f10e7af10bdd1aae419
  • Pointer size: 132 Bytes
  • Size of remote file: 1.55 MB
assets/example_5.png ADDED

Git LFS Details

  • SHA256: 35b197ebacd22326dbc65e30e3c35c3e1e7c3baab1858a86187aa45018464faa
  • Pointer size: 132 Bytes
  • Size of remote file: 2.07 MB
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/huggingface/diffusers
2
+ transformers
3
+ accelerate
4
+ peft
5
+ safetensors
6
+ sentencepiece
7
+ torchvision
8
+ numpy