Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +3 -0
- README.md +34 -5
- app.py +473 -0
- assets/example_1.png +3 -0
- assets/example_3.png +3 -0
- assets/example_5.png +3 -0
- requirements.txt +8 -0
.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:
|
| 5 |
-
colorTo:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
assets/example_3.png
ADDED
|
Git LFS Details
|
assets/example_5.png
ADDED
|
Git LFS Details
|
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
|