Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,20 +1,22 @@
|
|
| 1 |
-
# app.py
|
| 2 |
# Neurale netwerken als tekenaars — ControlNet (scribble) demo
|
| 3 |
-
#
|
| 4 |
-
#
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
import os
|
| 7 |
import random
|
| 8 |
import numpy as np
|
| 9 |
-
from typing import Optional
|
| 10 |
|
| 11 |
-
from PIL import Image,
|
| 12 |
|
| 13 |
import gradio as gr
|
| 14 |
|
| 15 |
import torch
|
| 16 |
from diffusers import ControlNetModel, StableDiffusionControlNetPipeline
|
| 17 |
-
from diffusers.utils import load_image
|
| 18 |
|
| 19 |
|
| 20 |
MODEL_BASE = os.environ.get("SD_BASE_MODEL", "runwayml/stable-diffusion-v1-5")
|
|
@@ -23,7 +25,6 @@ MODEL_CN = os.environ.get("CN_SCRIBBLE_MODEL", "lllyasviel/sd-controlnet-scribbl
|
|
| 23 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 24 |
DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
|
| 25 |
|
| 26 |
-
|
| 27 |
pipe: Optional[StableDiffusionControlNetPipeline] = None
|
| 28 |
|
| 29 |
|
|
@@ -36,7 +37,7 @@ def _lazy_load_pipeline():
|
|
| 36 |
MODEL_BASE,
|
| 37 |
controlnet=controlnet,
|
| 38 |
torch_dtype=DTYPE,
|
| 39 |
-
safety_checker=None,
|
| 40 |
)
|
| 41 |
if DEVICE == "cuda":
|
| 42 |
pipe.enable_model_cpu_offload()
|
|
@@ -45,78 +46,86 @@ def _lazy_load_pipeline():
|
|
| 45 |
return pipe
|
| 46 |
|
| 47 |
|
| 48 |
-
def
|
| 49 |
-
"""
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
|
| 59 |
img = Image.alpha_composite(bg, img)
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
bin_img = (arr < thr).astype(np.uint8) * 0 # donkere pixels → 0 (zwart)
|
| 67 |
-
# We willen echte lijnen behouden; maak binaire inversie (zwart op wit)
|
| 68 |
-
# Gebruik originele gray met punt-bij-punt: onder drempel → zwart, anders wit
|
| 69 |
-
mask = arr < thr
|
| 70 |
out = np.full((*arr.shape, 3), 255, dtype=np.uint8)
|
| 71 |
-
out[
|
| 72 |
-
|
| 73 |
scribble = Image.fromarray(out, mode="RGB")
|
| 74 |
|
| 75 |
-
#
|
| 76 |
max_side = max(scribble.width, scribble.height)
|
| 77 |
sq = Image.new("RGB", (max_side, max_side), (255, 255, 255))
|
| 78 |
ox = (max_side - scribble.width) // 2
|
| 79 |
oy = (max_side - scribble.height) // 2
|
| 80 |
sq.paste(scribble, (ox, oy))
|
| 81 |
|
| 82 |
-
# Resize naar 768
|
| 83 |
-
|
| 84 |
-
sq = sq.resize((target, target), Image.BICUBIC)
|
| 85 |
-
return sq
|
| 86 |
|
| 87 |
|
| 88 |
def run(
|
| 89 |
-
drawing
|
| 90 |
prompt: str,
|
| 91 |
negative_prompt: str,
|
| 92 |
guidance_scale: float = 8.0,
|
| 93 |
controlnet_conditioning_scale: float = 1.2,
|
| 94 |
-
num_inference_steps: int =
|
| 95 |
seed: int = -1,
|
| 96 |
-
strength_fill: float = 1.0,
|
| 97 |
):
|
| 98 |
if drawing is None:
|
| 99 |
raise gr.Error("Teken eerst iets links of laad een schetsbestand.")
|
| 100 |
-
if not prompt.strip():
|
| 101 |
-
prompt = "clean line-art, detailed sketch, shaded, high quality, coherent completion"
|
| 102 |
|
| 103 |
cn_image = rgba_to_scribble(drawing)
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
gen = torch.Generator(device=DEVICE).manual_seed(seed)
|
| 111 |
|
| 112 |
p = _lazy_load_pipeline()
|
| 113 |
-
|
| 114 |
full_prompt = (
|
| 115 |
"black pencil sketch, clean lines, coherent shape completion, subtle shading, "
|
| 116 |
"consistent with the input scribble, " + prompt
|
| 117 |
)
|
| 118 |
-
|
| 119 |
-
image = p(
|
| 120 |
prompt=full_prompt,
|
| 121 |
negative_prompt=negative_prompt or "text, watermark, extra limbs, low quality, distorted face, nsfw",
|
| 122 |
image=cn_image,
|
|
@@ -126,37 +135,52 @@ def run(
|
|
| 126 |
num_inference_steps=int(num_inference_steps),
|
| 127 |
guidance_scale=float(guidance_scale),
|
| 128 |
controlnet_conditioning_scale=float(controlnet_conditioning_scale),
|
| 129 |
-
)
|
|
|
|
| 130 |
|
| 131 |
-
#
|
| 132 |
pad = 16
|
| 133 |
combo = Image.new("RGB", (cn_image.width * 2 + pad, cn_image.height), (245, 245, 245))
|
| 134 |
combo.paste(cn_image, (0, 0))
|
| 135 |
combo.paste(image, (cn_image.width + pad, 0))
|
| 136 |
|
| 137 |
-
return image, cn_image, combo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
|
| 140 |
CSS = """
|
| 141 |
-
#combo img { border-radius: 10px; }
|
| 142 |
.gradio-container { max-width: 1100px !important; }
|
|
|
|
| 143 |
"""
|
| 144 |
|
| 145 |
-
with gr.Blocks(title="🖌️ Neurale netwerken als tekenaars", css=CSS) as demo:
|
| 146 |
gr.Markdown(
|
| 147 |
"""
|
| 148 |
# 🖌️ Neurale netwerken als tekenaars
|
| 149 |
-
Teken links
|
| 150 |
-
|
| 151 |
-
**Tip:** duidelijke contouren werken het best. Probeer bijvoorbeeld: *“een vliegende vis met vleugels, potloodschets”*.
|
| 152 |
"""
|
| 153 |
)
|
| 154 |
|
| 155 |
with gr.Row():
|
| 156 |
with gr.Column():
|
| 157 |
-
canvas = gr.
|
| 158 |
-
label="Tekening (schets) —
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
| 160 |
height=480,
|
| 161 |
)
|
| 162 |
|
|
@@ -175,30 +199,34 @@ Teken links een schets. Rechts **maakt een AI** (Stable Diffusion + ControlNet *
|
|
| 175 |
with gr.Accordion("Geavanceerd", open=False):
|
| 176 |
guidance_scale = gr.Slider(4.0, 15.0, value=8.0, step=0.5, label="Guidance scale")
|
| 177 |
cn_strength = gr.Slider(0.1, 2.0, value=1.2, step=0.05, label="ControlNet conditioning scale")
|
| 178 |
-
steps = gr.Slider(10, 40, value=
|
| 179 |
seed = gr.Number(value=-1, precision=0, label="Seed (-1 = random)")
|
| 180 |
|
| 181 |
-
|
|
|
|
|
|
|
| 182 |
|
| 183 |
with gr.Column():
|
| 184 |
out_img = gr.Image(label="Vervolledigde tekening", interactive=False)
|
| 185 |
cn_preview = gr.Image(label="Scribble (input voor AI)", interactive=False)
|
| 186 |
combo = gr.Image(label="Vergelijking (links: input, rechts: AI)", elem_id="combo")
|
| 187 |
-
meta = gr.Markdown()
|
| 188 |
|
| 189 |
run_btn.click(
|
| 190 |
run,
|
| 191 |
inputs=[canvas, prompt, negative_prompt, guidance_scale, cn_strength, steps, seed],
|
| 192 |
-
outputs=[out_img, cn_preview, combo
|
| 193 |
)
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
| 202 |
)
|
| 203 |
|
| 204 |
if __name__ == "__main__":
|
|
|
|
| 1 |
+
# app.py (v2)
|
| 2 |
# Neurale netwerken als tekenaars — ControlNet (scribble) demo
|
| 3 |
+
# Fixes:
|
| 4 |
+
# - Gebruik gr.Image(tool="sketch", type="pil") i.p.v. Sketchpad (betere compatibiliteit met Gradio 4)
|
| 5 |
+
# - Robuuste conversie van numpy/PIL naar RGBA
|
| 6 |
+
# - Testknop "Voorbeeld-schets" om pipeline te verifiëren
|
| 7 |
+
# - Kleinere default-steps voor snellere eerste run
|
| 8 |
|
| 9 |
import os
|
| 10 |
import random
|
| 11 |
import numpy as np
|
| 12 |
+
from typing import Optional
|
| 13 |
|
| 14 |
+
from PIL import Image, ImageDraw
|
| 15 |
|
| 16 |
import gradio as gr
|
| 17 |
|
| 18 |
import torch
|
| 19 |
from diffusers import ControlNetModel, StableDiffusionControlNetPipeline
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
MODEL_BASE = os.environ.get("SD_BASE_MODEL", "runwayml/stable-diffusion-v1-5")
|
|
|
|
| 25 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 26 |
DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
|
| 27 |
|
|
|
|
| 28 |
pipe: Optional[StableDiffusionControlNetPipeline] = None
|
| 29 |
|
| 30 |
|
|
|
|
| 37 |
MODEL_BASE,
|
| 38 |
controlnet=controlnet,
|
| 39 |
torch_dtype=DTYPE,
|
| 40 |
+
safety_checker=None,
|
| 41 |
)
|
| 42 |
if DEVICE == "cuda":
|
| 43 |
pipe.enable_model_cpu_offload()
|
|
|
|
| 46 |
return pipe
|
| 47 |
|
| 48 |
|
| 49 |
+
def to_rgba(img):
|
| 50 |
+
"""Accepteer PIL of numpy; geef RGBA terug."""
|
| 51 |
+
if img is None:
|
| 52 |
+
return None
|
| 53 |
+
if isinstance(img, np.ndarray):
|
| 54 |
+
# Gradio kan np.uint8 HxWxC teruggeven
|
| 55 |
+
if img.ndim == 2:
|
| 56 |
+
pil = Image.fromarray(img).convert("RGBA")
|
| 57 |
+
else:
|
| 58 |
+
pil = Image.fromarray(img.astype(np.uint8))
|
| 59 |
+
if pil.mode != "RGBA":
|
| 60 |
+
pil = pil.convert("RGBA")
|
| 61 |
+
return pil
|
| 62 |
+
if isinstance(img, Image.Image):
|
| 63 |
+
return img.convert("RGBA")
|
| 64 |
+
# Defensief: probeer te openen als pad/bytes
|
| 65 |
+
try:
|
| 66 |
+
return Image.open(img).convert("RGBA")
|
| 67 |
+
except Exception:
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def rgba_to_scribble(img: Image.Image) -> Image.Image:
|
| 72 |
+
"""Zet RGBA schets om naar zwart-op-wit scribble, vierkant 768x768."""
|
| 73 |
+
img = to_rgba(img)
|
| 74 |
+
if img is None:
|
| 75 |
+
return None
|
| 76 |
+
|
| 77 |
+
# Witte achtergrond + compositing (zodat transparant wit wordt)
|
| 78 |
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
|
| 79 |
img = Image.alpha_composite(bg, img)
|
| 80 |
|
| 81 |
+
# Grijswaarden en drempel
|
| 82 |
+
arr = np.array(img.convert("L"))
|
| 83 |
+
thr = max(25, int(arr.mean() * 0.9))
|
| 84 |
+
# Donkerder dan thr -> lijn
|
| 85 |
+
lines = arr < thr
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
out = np.full((*arr.shape, 3), 255, dtype=np.uint8)
|
| 87 |
+
out[lines] = (0, 0, 0)
|
|
|
|
| 88 |
scribble = Image.fromarray(out, mode="RGB")
|
| 89 |
|
| 90 |
+
# Vierkant maken met padding
|
| 91 |
max_side = max(scribble.width, scribble.height)
|
| 92 |
sq = Image.new("RGB", (max_side, max_side), (255, 255, 255))
|
| 93 |
ox = (max_side - scribble.width) // 2
|
| 94 |
oy = (max_side - scribble.height) // 2
|
| 95 |
sq.paste(scribble, (ox, oy))
|
| 96 |
|
| 97 |
+
# Resize naar 768 (kwaliteit) — maak 512 als je CPU gebruikt
|
| 98 |
+
return sq.resize((768, 768), Image.BICUBIC)
|
|
|
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
def run(
|
| 102 |
+
drawing,
|
| 103 |
prompt: str,
|
| 104 |
negative_prompt: str,
|
| 105 |
guidance_scale: float = 8.0,
|
| 106 |
controlnet_conditioning_scale: float = 1.2,
|
| 107 |
+
num_inference_steps: int = 18,
|
| 108 |
seed: int = -1,
|
|
|
|
| 109 |
):
|
| 110 |
if drawing is None:
|
| 111 |
raise gr.Error("Teken eerst iets links of laad een schetsbestand.")
|
|
|
|
|
|
|
| 112 |
|
| 113 |
cn_image = rgba_to_scribble(drawing)
|
| 114 |
+
if cn_image is None:
|
| 115 |
+
raise gr.Error("Kon de schets niet lezen. Probeer opnieuw te tekenen of upload een PNG/JPG.")
|
| 116 |
|
| 117 |
+
if not prompt.strip():
|
| 118 |
+
prompt = "clean pencil sketch, coherent completion, subtle shading, high quality"
|
| 119 |
+
gen = torch.Generator(device=DEVICE).manual_seed(
|
| 120 |
+
int(seed) if (seed is not None and int(seed) >= 0) else random.randint(0, 2**31 - 1)
|
| 121 |
+
)
|
|
|
|
| 122 |
|
| 123 |
p = _lazy_load_pipeline()
|
|
|
|
| 124 |
full_prompt = (
|
| 125 |
"black pencil sketch, clean lines, coherent shape completion, subtle shading, "
|
| 126 |
"consistent with the input scribble, " + prompt
|
| 127 |
)
|
| 128 |
+
result = p(
|
|
|
|
| 129 |
prompt=full_prompt,
|
| 130 |
negative_prompt=negative_prompt or "text, watermark, extra limbs, low quality, distorted face, nsfw",
|
| 131 |
image=cn_image,
|
|
|
|
| 135 |
num_inference_steps=int(num_inference_steps),
|
| 136 |
guidance_scale=float(guidance_scale),
|
| 137 |
controlnet_conditioning_scale=float(controlnet_conditioning_scale),
|
| 138 |
+
)
|
| 139 |
+
image = result.images[0]
|
| 140 |
|
| 141 |
+
# Combo (input vs output) naast elkaar
|
| 142 |
pad = 16
|
| 143 |
combo = Image.new("RGB", (cn_image.width * 2 + pad, cn_image.height), (245, 245, 245))
|
| 144 |
combo.paste(cn_image, (0, 0))
|
| 145 |
combo.paste(image, (cn_image.width + pad, 0))
|
| 146 |
|
| 147 |
+
return image, cn_image, combo
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def make_example_scribble() -> Image.Image:
|
| 151 |
+
"""Genereer een eenvoudige voorbeeldschets (appelvorm)."""
|
| 152 |
+
w, h = 400, 400
|
| 153 |
+
img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
| 154 |
+
d = ImageDraw.Draw(img)
|
| 155 |
+
d.line([(120, 320), (100, 240), (110, 180), (160, 140), (240, 140),
|
| 156 |
+
(290, 180), (300, 240), (280, 320), (200, 340), (120, 320)], fill=(0, 0, 0, 255), width=14)
|
| 157 |
+
d.line([(200, 140), (210, 110)], fill=(0, 0, 0, 255), width=12)
|
| 158 |
+
d.line([(210, 110), (250, 120), (270, 150)], fill=(0, 0, 0, 255), width=10)
|
| 159 |
+
return img
|
| 160 |
|
| 161 |
|
| 162 |
CSS = """
|
|
|
|
| 163 |
.gradio-container { max-width: 1100px !important; }
|
| 164 |
+
#combo img { border-radius: 10px; }
|
| 165 |
"""
|
| 166 |
|
| 167 |
+
with gr.Blocks(title="🖌️ Neurale netwerken als tekenaars (v2)", css=CSS) as demo:
|
| 168 |
gr.Markdown(
|
| 169 |
"""
|
| 170 |
# 🖌️ Neurale netwerken als tekenaars
|
| 171 |
+
Teken links (of klik op **Voorbeeld-schets**) en laat AI de tekening aanvullen
|
| 172 |
+
met **Stable Diffusion + ControlNet (scribble)**.
|
|
|
|
| 173 |
"""
|
| 174 |
)
|
| 175 |
|
| 176 |
with gr.Row():
|
| 177 |
with gr.Column():
|
| 178 |
+
canvas = gr.Image(
|
| 179 |
+
label="Tekening (schets) — gebruik de penseel-tool",
|
| 180 |
+
sources=["upload", "clipboard"],
|
| 181 |
+
type="pil",
|
| 182 |
+
image_mode="RGBA",
|
| 183 |
+
tool="sketch",
|
| 184 |
height=480,
|
| 185 |
)
|
| 186 |
|
|
|
|
| 199 |
with gr.Accordion("Geavanceerd", open=False):
|
| 200 |
guidance_scale = gr.Slider(4.0, 15.0, value=8.0, step=0.5, label="Guidance scale")
|
| 201 |
cn_strength = gr.Slider(0.1, 2.0, value=1.2, step=0.05, label="ControlNet conditioning scale")
|
| 202 |
+
steps = gr.Slider(10, 40, value=18, step=1, label="Aantal diffusion-steps")
|
| 203 |
seed = gr.Number(value=-1, precision=0, label="Seed (-1 = random)")
|
| 204 |
|
| 205 |
+
with gr.Row():
|
| 206 |
+
run_btn = gr.Button("Vervolledig ✨", variant="primary")
|
| 207 |
+
example_btn = gr.Button("Voorbeeld-schets", variant="secondary")
|
| 208 |
|
| 209 |
with gr.Column():
|
| 210 |
out_img = gr.Image(label="Vervolledigde tekening", interactive=False)
|
| 211 |
cn_preview = gr.Image(label="Scribble (input voor AI)", interactive=False)
|
| 212 |
combo = gr.Image(label="Vergelijking (links: input, rechts: AI)", elem_id="combo")
|
|
|
|
| 213 |
|
| 214 |
run_btn.click(
|
| 215 |
run,
|
| 216 |
inputs=[canvas, prompt, negative_prompt, guidance_scale, cn_strength, steps, seed],
|
| 217 |
+
outputs=[out_img, cn_preview, combo],
|
| 218 |
)
|
| 219 |
|
| 220 |
+
# Voorbeeld-schets vullen en direct runnen
|
| 221 |
+
def fill_and_run(p, n, gs, cs, st, sd):
|
| 222 |
+
img = make_example_scribble()
|
| 223 |
+
out, scrib, both = run(img, p, n, gs, cs, st, sd)
|
| 224 |
+
return img, out, scrib, both
|
| 225 |
+
|
| 226 |
+
example_btn.click(
|
| 227 |
+
fill_and_run,
|
| 228 |
+
inputs=[prompt, negative_prompt, guidance_scale, cn_strength, steps, seed],
|
| 229 |
+
outputs=[canvas, out_img, cn_preview, combo],
|
| 230 |
)
|
| 231 |
|
| 232 |
if __name__ == "__main__":
|