Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,8 +5,7 @@ from PIL import Image, ImageDraw
|
|
| 5 |
import time
|
| 6 |
import os
|
| 7 |
import tempfile
|
| 8 |
-
import
|
| 9 |
-
import numpy as np
|
| 10 |
|
| 11 |
# === OPTIMIERTE EINSTELLUNGEN ===
|
| 12 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
@@ -15,45 +14,38 @@ IMG_SIZE = 512
|
|
| 15 |
|
| 16 |
print(f"Running on: {device}")
|
| 17 |
|
| 18 |
-
# ===
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
|
| 26 |
-
face_cascade = cv2.CascadeClassifier(CASCADE_PATH)
|
| 27 |
-
faces = face_cascade.detectMultiScale(gray, 1.2, 6)
|
| 28 |
-
|
| 29 |
-
if len(faces) == 0:
|
| 30 |
-
print("⚠️ Kein Gesicht erkannt – keine Maske angewendet.")
|
| 31 |
-
return None
|
| 32 |
-
|
| 33 |
-
# Nimm das größte erkannte Gesicht
|
| 34 |
-
(x, y, w, h) = sorted(faces, key=lambda f: f[2]*f[3], reverse=True)[0]
|
| 35 |
-
|
| 36 |
-
# Erweitere den Bereich leicht, um Haare / Stirn einzuschließen
|
| 37 |
-
pad = int(h * 0.25)
|
| 38 |
-
bbox = (max(0, x - pad), max(0, y - pad),
|
| 39 |
-
min(cv_image.shape[1], x + w + pad),
|
| 40 |
-
min(cv_image.shape[0], y + h + pad))
|
| 41 |
-
|
| 42 |
-
print(f"✅ Gesicht erkannt: {bbox}")
|
| 43 |
-
return bbox
|
| 44 |
-
|
| 45 |
-
except Exception as e:
|
| 46 |
-
print(f"❌ Fehler bei Gesichtserkennung: {e}")
|
| 47 |
-
return None
|
| 48 |
-
|
| 49 |
-
def create_face_mask(image, bbox):
|
| 50 |
-
"""Erzeugt eine runde Gesichtsmaske aus Koordinaten."""
|
| 51 |
-
mask = Image.new("L", image.size, 0)
|
| 52 |
-
if bbox is not None:
|
| 53 |
draw = ImageDraw.Draw(mask)
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
return mask
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
# === PIPELINES ===
|
| 58 |
pipe_txt2img = None
|
| 59 |
pipe_img2img = None
|
|
@@ -70,10 +62,8 @@ def load_txt2img():
|
|
| 70 |
requires_safety_checker=False
|
| 71 |
).to(device)
|
| 72 |
|
| 73 |
-
# DPMSolver für Text-to-Image
|
| 74 |
from diffusers import DPMSolverMultistepScheduler
|
| 75 |
pipe_txt2img.scheduler = DPMSolverMultistepScheduler.from_config(pipe_txt2img.scheduler.config)
|
| 76 |
-
|
| 77 |
pipe_txt2img.enable_attention_slicing()
|
| 78 |
return pipe_txt2img
|
| 79 |
|
|
@@ -89,21 +79,16 @@ def load_img2img():
|
|
| 89 |
requires_safety_checker=False
|
| 90 |
).to(device)
|
| 91 |
|
| 92 |
-
# --- OPTIMIERTER SAMPLER ---
|
| 93 |
from diffusers import DPMSolverMultistepScheduler
|
| 94 |
pipe_img2img.scheduler = DPMSolverMultistepScheduler.from_config(
|
| 95 |
pipe_img2img.scheduler.config,
|
| 96 |
algorithm_type="sde-dpmsolver++",
|
| 97 |
use_karras_sigmas=True,
|
| 98 |
-
# wichtig: linear_multistep statt linspace ergibt natürlichere Übergänge
|
| 99 |
timestep_spacing="trailing"
|
| 100 |
)
|
| 101 |
|
| 102 |
-
# Effiziente Speicherverwaltung
|
| 103 |
pipe_img2img.enable_attention_slicing()
|
| 104 |
pipe_img2img.enable_vae_tiling()
|
| 105 |
-
|
| 106 |
-
# Optional: für stabilere Farberhaltung
|
| 107 |
pipe_img2img.vae_slicing = True
|
| 108 |
|
| 109 |
return pipe_img2img
|
|
@@ -115,13 +100,14 @@ def text_to_image(prompt, steps, guidance_scale):
|
|
| 115 |
return None
|
| 116 |
|
| 117 |
print(f"Starting generation for: {prompt}")
|
| 118 |
-
print(f"Parameters - Steps: {steps}, Guidance Scale: {guidance_scale}")
|
| 119 |
start_time = time.time()
|
| 120 |
|
| 121 |
pipe = load_txt2img()
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
|
|
|
|
|
|
|
| 125 |
|
| 126 |
image = pipe(
|
| 127 |
prompt=prompt,
|
|
@@ -135,7 +121,7 @@ def text_to_image(prompt, steps, guidance_scale):
|
|
| 135 |
end_time = time.time()
|
| 136 |
print(f"✅ Bild generiert in {end_time - start_time:.2f} Sekunden")
|
| 137 |
|
| 138 |
-
return image
|
| 139 |
|
| 140 |
except Exception as e:
|
| 141 |
print(f"❌ Fehler: {e}")
|
|
@@ -143,46 +129,62 @@ def text_to_image(prompt, steps, guidance_scale):
|
|
| 143 |
traceback.print_exc()
|
| 144 |
return None
|
| 145 |
|
| 146 |
-
def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, face_preserve):
|
| 147 |
try:
|
| 148 |
if image is None:
|
| 149 |
return None
|
| 150 |
|
| 151 |
print(f"🧩 Img2Img Start → Strength: {strength}, Steps: {steps}, Guidance: {guidance_scale}")
|
| 152 |
print(f"Prompt: {prompt}")
|
| 153 |
-
print(f"Negative: {neg_prompt}")
|
| 154 |
print(f"Gesicht beibehalten: {face_preserve}")
|
| 155 |
start_time = time.time()
|
| 156 |
|
| 157 |
pipe = load_img2img()
|
| 158 |
-
|
| 159 |
-
# --- PREPROCESSING ---
|
| 160 |
img_resized = image.convert("RGB").resize((IMG_SIZE, IMG_SIZE))
|
| 161 |
|
| 162 |
# --- PARAMETER-TUNING ---
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
generator = torch.Generator(device=device).manual_seed(
|
|
|
|
| 169 |
|
| 170 |
-
# ---
|
| 171 |
mask = None
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
if mask:
|
| 176 |
-
print("✅
|
| 177 |
-
|
| 178 |
-
|
|
|
|
| 179 |
|
| 180 |
# --- PIPELINE-AUFRUF ---
|
| 181 |
result = pipe(
|
| 182 |
prompt=prompt,
|
| 183 |
negative_prompt=neg_prompt,
|
| 184 |
image=img_resized,
|
| 185 |
-
mask_image=mask, #
|
| 186 |
strength=adj_strength,
|
| 187 |
num_inference_steps=int(steps),
|
| 188 |
guidance_scale=adj_guidance,
|
|
@@ -192,27 +194,20 @@ def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, fac
|
|
| 192 |
end_time = time.time()
|
| 193 |
print(f"✅ Bild transformiert in {end_time - start_time:.2f} Sekunden")
|
| 194 |
|
| 195 |
-
generated_image = result.images[0]
|
| 196 |
-
|
| 197 |
-
#
|
| 198 |
try:
|
| 199 |
-
# Erstelle Temp-Verzeichnis falls nicht vorhanden
|
| 200 |
temp_dir = "/tmp/gradio_fallback"
|
| 201 |
os.makedirs(temp_dir, exist_ok=True)
|
| 202 |
-
|
| 203 |
-
# Speichere Bild temporär
|
| 204 |
temp_path = os.path.join(temp_dir, f"generated_{int(time.time())}.png")
|
| 205 |
generated_image.save(temp_path, "PNG")
|
| 206 |
-
print(f"💾 Bild temporär gespeichert: {temp_path}")
|
| 207 |
-
|
| 208 |
-
# Lade Bild wieder für konsistente Rückgabe
|
| 209 |
saved_image = Image.open(temp_path)
|
| 210 |
-
|
| 211 |
except Exception as temp_error:
|
| 212 |
print(f"⚠️ Temp-Speicherung fehlgeschlagen: {temp_error}")
|
| 213 |
-
saved_image = generated_image
|
| 214 |
|
| 215 |
-
return saved_image
|
| 216 |
|
| 217 |
except Exception as e:
|
| 218 |
print(f"❌ Fehler: {e}")
|
|
@@ -220,6 +215,14 @@ def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, fac
|
|
| 220 |
traceback.print_exc()
|
| 221 |
return None
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
# === UI ===
|
| 224 |
with gr.Blocks() as app:
|
| 225 |
gr.Markdown("# 🎨 AI Bild Generator")
|
|
@@ -246,13 +249,6 @@ with gr.Blocks() as app:
|
|
| 246 |
label="Guidance Scale (Prompt-Treue vs. Verzerrung)"
|
| 247 |
)
|
| 248 |
|
| 249 |
-
with gr.Row():
|
| 250 |
-
gr.Markdown(
|
| 251 |
-
"**Parameter-Erklärung:** "
|
| 252 |
-
"• **Steps:** Mehr = bessere Qualität, aber langsamer "
|
| 253 |
-
"• **Guidance:** Niedrig = weniger Verzerrung, Hoch = mehr KI-Fantasie "
|
| 254 |
-
)
|
| 255 |
-
|
| 256 |
generate_btn = gr.Button("🎨 Bild generieren", variant="primary")
|
| 257 |
txt_output = gr.Image(
|
| 258 |
label="Generiertes Bild",
|
|
@@ -279,15 +275,15 @@ with gr.Blocks() as app:
|
|
| 279 |
with gr.Row():
|
| 280 |
with gr.Column():
|
| 281 |
img_prompt = gr.Textbox(
|
| 282 |
-
placeholder="background
|
| 283 |
lines=2,
|
| 284 |
label="Transformations-Prompt (Englisch)"
|
| 285 |
)
|
| 286 |
with gr.Column():
|
| 287 |
img_neg_prompt = gr.Textbox(
|
| 288 |
-
placeholder="blurry, deformed, ugly, bad anatomy
|
| 289 |
lines=2,
|
| 290 |
-
label="Negativ-Prompt
|
| 291 |
)
|
| 292 |
|
| 293 |
with gr.Row():
|
|
@@ -299,30 +295,37 @@ with gr.Blocks() as app:
|
|
| 299 |
with gr.Column():
|
| 300 |
img_steps = gr.Slider(
|
| 301 |
minimum=10, maximum=100, value=35, step=1,
|
| 302 |
-
label="Steps
|
| 303 |
)
|
| 304 |
with gr.Column():
|
| 305 |
img_guidance = gr.Slider(
|
| 306 |
minimum=1.0, maximum=20.0, value=7.5, step=0.5,
|
| 307 |
-
label="Guidance Scale
|
| 308 |
)
|
| 309 |
|
| 310 |
-
#
|
| 311 |
with gr.Row():
|
| 312 |
face_preserve = gr.Checkbox(
|
| 313 |
-
label="👤 Gesicht
|
| 314 |
value=True,
|
| 315 |
-
info="
|
| 316 |
)
|
| 317 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
with gr.Row():
|
| 319 |
gr.Markdown(
|
| 320 |
-
"**
|
| 321 |
-
"• **
|
| 322 |
-
"• **
|
| 323 |
-
"• **
|
| 324 |
-
"• **Negativ-Prompt:** Beschreibt was NICHT im Bild sein soll "
|
| 325 |
-
"• **Gesicht beibehalten:** Automatische Gesichtserkennung schützt Porträts "
|
| 326 |
)
|
| 327 |
|
| 328 |
transform_btn = gr.Button("🔄 Bild transformieren", variant="primary")
|
|
@@ -333,9 +336,20 @@ with gr.Blocks() as app:
|
|
| 333 |
show_download_button=True
|
| 334 |
)
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
transform_btn.click(
|
| 337 |
fn=img_to_image,
|
| 338 |
-
inputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
outputs=img_output,
|
| 340 |
concurrency_limit=1
|
| 341 |
)
|
|
|
|
| 5 |
import time
|
| 6 |
import os
|
| 7 |
import tempfile
|
| 8 |
+
import random
|
|
|
|
| 9 |
|
| 10 |
# === OPTIMIERTE EINSTELLUNGEN ===
|
| 11 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
| 14 |
|
| 15 |
print(f"Running on: {device}")
|
| 16 |
|
| 17 |
+
# === GESICHTSMASKEN-FUNKTIONEN ===
|
| 18 |
+
def create_face_mask(image, bbox_coords):
|
| 19 |
+
"""Erzeugt eine Gesichtsmaske - WEIßE Bereiche werden VERÄNDERT, SCHWARZE BLEIBEN"""
|
| 20 |
+
mask = Image.new("L", image.size, 0) # Start mit komplett schwarzer Maske (alles geschützt)
|
| 21 |
+
|
| 22 |
+
if bbox_coords and all(coord is not None for coord in bbox_coords):
|
| 23 |
+
x1, y1, x2, y2 = bbox_coords
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
draw = ImageDraw.Draw(mask)
|
| 25 |
+
|
| 26 |
+
if face_preserve:
|
| 27 |
+
# GESICHTSERHALTUNG: Maske um das Gesicht herum zeichnen
|
| 28 |
+
# Das Gesicht bleibt schwarz (geschützt), der Rest wird weiß (verändert)
|
| 29 |
+
draw.rectangle([0, 0, image.size[0], image.size[1]], fill=255) # Alles weiß = alles verändern
|
| 30 |
+
draw.ellipse([x1, y1, x2, y2], fill=0) # Gesicht schwarz = geschützt
|
| 31 |
+
print("✅ Gesicht wird GESCHÜTZT - Umgebung wird verändert")
|
| 32 |
+
else:
|
| 33 |
+
# NUR GESICHT VERÄNDERN: Nur das Gesicht wird weiß (verändert), Rest schwarz (geschützt)
|
| 34 |
+
draw.ellipse([x1, y1, x2, y2], fill=255) # Gesicht weiß = verändern
|
| 35 |
+
print("✅ Nur Gesicht wird verändert - Umgebung bleibt erhalten")
|
| 36 |
+
|
| 37 |
return mask
|
| 38 |
|
| 39 |
+
def auto_detect_face_area(image):
|
| 40 |
+
"""Vorschlag für Gesichtsbereich"""
|
| 41 |
+
width, height = image.size
|
| 42 |
+
face_size = min(width, height) * 0.4
|
| 43 |
+
x1 = (width - face_size) / 2
|
| 44 |
+
y1 = (height - face_size) / 3
|
| 45 |
+
x2 = x1 + face_size
|
| 46 |
+
y2 = y1 + face_size
|
| 47 |
+
return [int(x1), int(y1), int(x2), int(y2)]
|
| 48 |
+
|
| 49 |
# === PIPELINES ===
|
| 50 |
pipe_txt2img = None
|
| 51 |
pipe_img2img = None
|
|
|
|
| 62 |
requires_safety_checker=False
|
| 63 |
).to(device)
|
| 64 |
|
|
|
|
| 65 |
from diffusers import DPMSolverMultistepScheduler
|
| 66 |
pipe_txt2img.scheduler = DPMSolverMultistepScheduler.from_config(pipe_txt2img.scheduler.config)
|
|
|
|
| 67 |
pipe_txt2img.enable_attention_slicing()
|
| 68 |
return pipe_txt2img
|
| 69 |
|
|
|
|
| 79 |
requires_safety_checker=False
|
| 80 |
).to(device)
|
| 81 |
|
|
|
|
| 82 |
from diffusers import DPMSolverMultistepScheduler
|
| 83 |
pipe_img2img.scheduler = DPMSolverMultistepScheduler.from_config(
|
| 84 |
pipe_img2img.scheduler.config,
|
| 85 |
algorithm_type="sde-dpmsolver++",
|
| 86 |
use_karras_sigmas=True,
|
|
|
|
| 87 |
timestep_spacing="trailing"
|
| 88 |
)
|
| 89 |
|
|
|
|
| 90 |
pipe_img2img.enable_attention_slicing()
|
| 91 |
pipe_img2img.enable_vae_tiling()
|
|
|
|
|
|
|
| 92 |
pipe_img2img.vae_slicing = True
|
| 93 |
|
| 94 |
return pipe_img2img
|
|
|
|
| 100 |
return None
|
| 101 |
|
| 102 |
print(f"Starting generation for: {prompt}")
|
|
|
|
| 103 |
start_time = time.time()
|
| 104 |
|
| 105 |
pipe = load_txt2img()
|
| 106 |
|
| 107 |
+
# ZUFÄLLIGER SEED für Variation
|
| 108 |
+
seed = random.randint(0, 2**32 - 1)
|
| 109 |
+
generator = torch.Generator(device=device).manual_seed(seed)
|
| 110 |
+
print(f"🎲 Using seed: {seed}")
|
| 111 |
|
| 112 |
image = pipe(
|
| 113 |
prompt=prompt,
|
|
|
|
| 121 |
end_time = time.time()
|
| 122 |
print(f"✅ Bild generiert in {end_time - start_time:.2f} Sekunden")
|
| 123 |
|
| 124 |
+
return image
|
| 125 |
|
| 126 |
except Exception as e:
|
| 127 |
print(f"❌ Fehler: {e}")
|
|
|
|
| 129 |
traceback.print_exc()
|
| 130 |
return None
|
| 131 |
|
| 132 |
+
def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, face_preserve, bbox_x1, bbox_y1, bbox_x2, bbox_y2):
|
| 133 |
try:
|
| 134 |
if image is None:
|
| 135 |
return None
|
| 136 |
|
| 137 |
print(f"🧩 Img2Img Start → Strength: {strength}, Steps: {steps}, Guidance: {guidance_scale}")
|
| 138 |
print(f"Prompt: {prompt}")
|
|
|
|
| 139 |
print(f"Gesicht beibehalten: {face_preserve}")
|
| 140 |
start_time = time.time()
|
| 141 |
|
| 142 |
pipe = load_img2img()
|
|
|
|
|
|
|
| 143 |
img_resized = image.convert("RGB").resize((IMG_SIZE, IMG_SIZE))
|
| 144 |
|
| 145 |
# --- PARAMETER-TUNING ---
|
| 146 |
+
adj_strength = min(0.85, strength * 1.3)
|
| 147 |
+
adj_guidance = min(guidance_scale, 7.0)
|
| 148 |
+
|
| 149 |
+
# ZUFÄLLIGER SEED für Variation
|
| 150 |
+
seed = random.randint(0, 2**32 - 1)
|
| 151 |
+
generator = torch.Generator(device=device).manual_seed(seed)
|
| 152 |
+
print(f"🎲 Using seed: {seed}")
|
| 153 |
|
| 154 |
+
# --- GESICHTSMASKE ---
|
| 155 |
mask = None
|
| 156 |
+
bbox_coords = None
|
| 157 |
+
|
| 158 |
+
if bbox_x1 is not None and bbox_y1 is not None and bbox_x2 is not None and bbox_y2 is not None:
|
| 159 |
+
# Skaliere Koordinaten auf die neue Bildgröße
|
| 160 |
+
orig_width, orig_height = image.size
|
| 161 |
+
scale_x = IMG_SIZE / orig_width
|
| 162 |
+
scale_y = IMG_SIZE / orig_height
|
| 163 |
+
|
| 164 |
+
scaled_coords = [
|
| 165 |
+
int(bbox_x1 * scale_x),
|
| 166 |
+
int(bbox_y1 * scale_y),
|
| 167 |
+
int(bbox_x2 * scale_x),
|
| 168 |
+
int(bbox_y2 * scale_y)
|
| 169 |
+
]
|
| 170 |
+
bbox_coords = scaled_coords
|
| 171 |
+
print(f"📐 Skalierte Koordinaten: {scaled_coords}")
|
| 172 |
+
|
| 173 |
+
# Maskenlogik basierend auf face_preserve
|
| 174 |
+
if bbox_coords:
|
| 175 |
+
mask = create_face_mask(img_resized, bbox_coords)
|
| 176 |
if mask:
|
| 177 |
+
print("✅ Maske erfolgreich erstellt")
|
| 178 |
+
else:
|
| 179 |
+
print("⚠️ Keine gültigen Koordinaten - keine Maske angewendet")
|
| 180 |
+
mask = None
|
| 181 |
|
| 182 |
# --- PIPELINE-AUFRUF ---
|
| 183 |
result = pipe(
|
| 184 |
prompt=prompt,
|
| 185 |
negative_prompt=neg_prompt,
|
| 186 |
image=img_resized,
|
| 187 |
+
mask_image=mask, # None = gesamtes Bild verändern
|
| 188 |
strength=adj_strength,
|
| 189 |
num_inference_steps=int(steps),
|
| 190 |
guidance_scale=adj_guidance,
|
|
|
|
| 194 |
end_time = time.time()
|
| 195 |
print(f"✅ Bild transformiert in {end_time - start_time:.2f} Sekunden")
|
| 196 |
|
| 197 |
+
generated_image = result.images[0]
|
| 198 |
+
|
| 199 |
+
# Temp-Speicherung
|
| 200 |
try:
|
|
|
|
| 201 |
temp_dir = "/tmp/gradio_fallback"
|
| 202 |
os.makedirs(temp_dir, exist_ok=True)
|
|
|
|
|
|
|
| 203 |
temp_path = os.path.join(temp_dir, f"generated_{int(time.time())}.png")
|
| 204 |
generated_image.save(temp_path, "PNG")
|
|
|
|
|
|
|
|
|
|
| 205 |
saved_image = Image.open(temp_path)
|
|
|
|
| 206 |
except Exception as temp_error:
|
| 207 |
print(f"⚠️ Temp-Speicherung fehlgeschlagen: {temp_error}")
|
| 208 |
+
saved_image = generated_image
|
| 209 |
|
| 210 |
+
return saved_image
|
| 211 |
|
| 212 |
except Exception as e:
|
| 213 |
print(f"❌ Fehler: {e}")
|
|
|
|
| 215 |
traceback.print_exc()
|
| 216 |
return None
|
| 217 |
|
| 218 |
+
def update_bbox_from_image(image):
|
| 219 |
+
"""Aktualisiert die Bounding-Box-Koordinaten wenn ein Bild hochgeladen wird"""
|
| 220 |
+
if image is None:
|
| 221 |
+
return None, None, None, None
|
| 222 |
+
|
| 223 |
+
bbox = auto_detect_face_area(image)
|
| 224 |
+
return bbox[0], bbox[1], bbox[2], bbox[3]
|
| 225 |
+
|
| 226 |
# === UI ===
|
| 227 |
with gr.Blocks() as app:
|
| 228 |
gr.Markdown("# 🎨 AI Bild Generator")
|
|
|
|
| 249 |
label="Guidance Scale (Prompt-Treue vs. Verzerrung)"
|
| 250 |
)
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
generate_btn = gr.Button("🎨 Bild generieren", variant="primary")
|
| 253 |
txt_output = gr.Image(
|
| 254 |
label="Generiertes Bild",
|
|
|
|
| 275 |
with gr.Row():
|
| 276 |
with gr.Column():
|
| 277 |
img_prompt = gr.Textbox(
|
| 278 |
+
placeholder="change background to forest, keep face unchanged",
|
| 279 |
lines=2,
|
| 280 |
label="Transformations-Prompt (Englisch)"
|
| 281 |
)
|
| 282 |
with gr.Column():
|
| 283 |
img_neg_prompt = gr.Textbox(
|
| 284 |
+
placeholder="blurry, deformed, ugly, bad anatomy",
|
| 285 |
lines=2,
|
| 286 |
+
label="Negativ-Prompt"
|
| 287 |
)
|
| 288 |
|
| 289 |
with gr.Row():
|
|
|
|
| 295 |
with gr.Column():
|
| 296 |
img_steps = gr.Slider(
|
| 297 |
minimum=10, maximum=100, value=35, step=1,
|
| 298 |
+
label="Steps"
|
| 299 |
)
|
| 300 |
with gr.Column():
|
| 301 |
img_guidance = gr.Slider(
|
| 302 |
minimum=1.0, maximum=20.0, value=7.5, step=0.5,
|
| 303 |
+
label="Guidance Scale"
|
| 304 |
)
|
| 305 |
|
| 306 |
+
# GESICHTSOPTIONEN
|
| 307 |
with gr.Row():
|
| 308 |
face_preserve = gr.Checkbox(
|
| 309 |
+
label="👤 Gesicht beibehalten (Umgebung verändern)",
|
| 310 |
value=True,
|
| 311 |
+
info="Gesicht bleibt erhalten, Hintergrund wird verändert"
|
| 312 |
)
|
| 313 |
|
| 314 |
+
with gr.Row():
|
| 315 |
+
gr.Markdown("**Gesichtsbereich definieren (x1, y1, x2, y2):**")
|
| 316 |
+
|
| 317 |
+
with gr.Row():
|
| 318 |
+
bbox_x1 = gr.Number(label="x1 (links)", value=100, precision=0)
|
| 319 |
+
bbox_y1 = gr.Number(label="y1 (oben)", value=100, precision=0)
|
| 320 |
+
bbox_x2 = gr.Number(label="x2 (rechts)", value=300, precision=0)
|
| 321 |
+
bbox_y2 = gr.Number(label="y2 (unten)", value=300, precision=0)
|
| 322 |
+
|
| 323 |
with gr.Row():
|
| 324 |
gr.Markdown(
|
| 325 |
+
"**Anleitung:** "
|
| 326 |
+
"• **Gesicht beibehalten** = Gesicht bleibt, Hintergrund ändert sich "
|
| 327 |
+
"• **Nicht aktiviert** = Nur Gesicht ändert sich, Hintergrund bleibt "
|
| 328 |
+
"• **Koordinaten anpassen** um den genauen Bereich zu definieren "
|
|
|
|
|
|
|
| 329 |
)
|
| 330 |
|
| 331 |
transform_btn = gr.Button("🔄 Bild transformieren", variant="primary")
|
|
|
|
| 336 |
show_download_button=True
|
| 337 |
)
|
| 338 |
|
| 339 |
+
# Event-Handler für Bild-Upload
|
| 340 |
+
img_input.change(
|
| 341 |
+
fn=update_bbox_from_image,
|
| 342 |
+
inputs=[img_input],
|
| 343 |
+
outputs=[bbox_x1, bbox_y1, bbox_x2, bbox_y2]
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
transform_btn.click(
|
| 347 |
fn=img_to_image,
|
| 348 |
+
inputs=[
|
| 349 |
+
img_input, img_prompt, img_neg_prompt,
|
| 350 |
+
strength_slider, img_steps, img_guidance,
|
| 351 |
+
face_preserve, bbox_x1, bbox_y1, bbox_x2, bbox_y2
|
| 352 |
+
],
|
| 353 |
outputs=img_output,
|
| 354 |
concurrency_limit=1
|
| 355 |
)
|