Astridkraft's picture
Update app.py
735548b verified
raw
history blame
82.9 kB
import gradio as gr
from diffusers import StableDiffusionPipeline, StableDiffusionImg2ImgPipeline
from diffusers import StableDiffusionInpaintPipeline, AutoencoderKL
from diffusers import DPMSolverMultistepScheduler, PNDMScheduler
from controlnet_module import controlnet_processor
from diffusers import StableDiffusionControlNetInpaintPipeline, ControlNetModel
import torch
from PIL import Image, ImageDraw
import time
import os
import tempfile
import random
import re
from PIL import ImageFilter # Für GaussianBlur wird nur für SAM benötigt!
# Oben bei den anderen Imports:
import numpy as np # <-- HIER EINFÜGEN
from PIL import Image, ImageDraw, ImageFilter
# === OPTIMIERTE EINSTELLUNGEN ===
device = "cuda" if torch.cuda.is_available() else "cpu"
torch_dtype = torch.float16 if device == "cuda" else torch.float32
IMG_SIZE = 512
MAX_IMAGE_SIZE = 4096 # Maximale Bildgröße für Verarbeitung
print(f"Running on: {device}")
# === MODELLKONFIGURATION (NUR 2 MODELLE) ===
MODEL_CONFIGS = {
"runwayml/stable-diffusion-v1-5": {
"name": "🏠 Stable Diffusion 1.5 (Universal)",
"description": "Universal model, good all-rounder, reliable results",
"requires_vae": False,
"vae_model": "stabilityai/sd-vae-ft-mse",
"recommended_steps": 35,
"recommended_cfg": 7.5,
"supports_fp16": True
},
"SG161222/Realistic_Vision_V6.0_B1_noVAE": {
"name": "👤 Realistic Vision V6.0 (Portraits)",
"description": "Best for photorealistic faces, skin details, human portraits",
"requires_vae": True,
"vae_model": "stabilityai/sd-vae-ft-mse",
"recommended_steps": 40,
"recommended_cfg": 7.0,
"supports_fp16": False
}
}
# === SAFETENSORS KONFIGURATION ===
SAFETENSORS_MODELS = ["runwayml/stable-diffusion-v1-5"]
# Aktuell ausgewähltes Modell (wird vom User gesetzt)
current_model_id = "runwayml/stable-diffusion-v1-5"
# === AUTOMATISCHE NEGATIVE PROMPT GENERIERUNG ===
def auto_negative_prompt(positive_prompt):
"""Generiert automatisch negative Prompts basierend auf dem positiven Prompt"""
p = positive_prompt.lower()
negatives = []
# Personen / Portraits
if any(w in p for w in [
"person", "man", "woman", "face", "portrait", "team", "employee",
"people", "crowd", "character", "figure", "human", "child", "baby",
"girl", "boy", "lady", "gentleman", "fairy", "elf", "dwarf", "santa claus",
"mermaid", "angel", "demon", "witch", "wizard", "creature", "being",
"model", "actor", "actress", "celebrity", "avatar", "group"]):
negatives.append(
"blurry face, lowres face, deformed pupils, bad anatomy, malformed hands, extra fingers, uneven eyes, distorted face, "
"unrealistic skin, mutated, ugly, disfigured, poorly drawn face, "
"missing limbs, extra limbs, fused fingers, too many fingers, bad teeth, "
"mutated hands, long neck, extra wings, multiple wings,grainy face, noisy face, "
"compression artifacts, rendering artifacts, digital artifacts, overprocessed face, oversmoothed face "
)
# Business / Corporate
if any(w in p for w in ["office", "business", "team", "meeting", "corporate", "company", "workplace"]):
negatives.append(
"overexposed, oversaturated, harsh lighting, watermark, text, logo, brand"
)
# Produkt / CGI
if any(w in p for w in ["product", "packshot", "mockup", "render", "3d", "cgi", "packaging"]):
negatives.append(
"plastic texture, noisy, overly reflective surfaces, watermark, text, low poly"
)
# Landschaft / Umgebung
if any(w in p for w in ["landscape", "nature", "mountain", "forest", "outdoor", "beach", "sky"]):
negatives.append(
"blurry, oversaturated, unnatural colors, distorted horizon, floating objects"
)
# Logos / Symbole
if any(w in p for w in ["logo", "symbol", "icon", "typography", "badge", "emblem"]):
negatives.append(
"watermark, signature, username, text, writing, scribble, messy"
)
# Architektur / Gebäude
if any(w in p for w in ["building", "architecture", "house", "interior", "room", "facade"]):
negatives.append(
"deformed, distorted perspective, floating objects, collapsing structure"
)
# Basis negative Prompts für alle Fälle
base_negatives = "low quality, worst quality, blurry, jpeg artifacts, ugly, deformed"
if negatives:
return base_negatives + ", " + ", ".join(negatives)
else:
return base_negatives
# === HILFSFUNKTION: KOORDINATEN SORTIEREN ===
def sort_coordinates(x1, y1, x2, y2):
"""Sortiert Koordinaten, so dass x1 <= x2 und y1 <= y2"""
sorted_x1 = min(x1, x2)
sorted_x2 = max(x1, x2)
sorted_y1 = min(y1, y2)
sorted_y2 = max(y1, y2)
return sorted_x1, sorted_y1, sorted_x2, sorted_y2
# === GESICHTSMASKEN-FUNKTIONEN (ERWEITERT FÜR 3 MODI) ===
def create_face_mask(image, bbox_coords, mode):
"""
ERWEITERTE FUNKTION: Erzeugt Maske basierend auf 3 Modi
Weiße Bereiche werden VERÄNDERT, Schwarze bleiben ERHALTEN
Parameter:
- image: PIL Image
- bbox_coords: [x1, y1, x2, y2]
- mode: "environment_change", "focus_change", "face_only_change"
Returns:
- PIL Image (L-Modus, 0=schwarz=erhalten, 255=weiß=verändern)
"""
mask = Image.new("L", image.size, 0) # Start mit komplett schwarzer Maske (alles geschützt)
if bbox_coords and all(coord is not None for coord in bbox_coords):
# Sortiere Koordinaten
x1, y1, x2, y2 = sort_coordinates(*bbox_coords)
# Stelle sicher, dass Koordinaten innerhalb des Bildes liegen
x1 = max(0, min(x1, image.width-1))
y1 = max(0, min(y1, image.height-1))
x2 = max(0, min(x2, image.width-1))
y2 = max(0, min(y2, image.height-1))
draw = ImageDraw.Draw(mask)
if mode == "environment_change":
# MODUS 1: Umgebung ändern (Depth + Canny)
# Maske: Alles weiß AUSSER Bereich (schwarz)
draw.rectangle([0, 0, image.size[0], image.size[1]], fill=255) # Alles weiß = verändern
draw.rectangle([x1, y1, x2, y2], fill=0) # Bereich schwarz = geschützt (rechteckig)
print(f"🎯 MODUS: Umgebung ändern - Alles außer BBox wird verändert (BBox: {x1},{y1},{x2},{y2})")
elif mode == "focus_change":
# MODUS 2: Focus verändern (OpenPose + Canny)
# Maske: Nur innerhalb der Box weiß (Rest schwarz)
draw.rectangle([x1, y1, x2, y2], fill=255) # Nur Box weiß = verändern
print(f"🎯 MODUS: Focus verändern - Nur innerhalb der BBox wird verändert (BBox: {x1},{y1},{x2},{y2})")
elif mode == "face_only_change":
# MODUS 3: Ausschließlich Gesicht (Depth + Canny)
# Maske: Nur innerhalb der Box weiß (Rest schwarz) - wie focus_change
draw.rectangle([x1, y1, x2, y2], fill=255) # Nur Box weiß = verändern
print(f"🎯 MODUS: Ausschließlich Gesicht - Nur innerhalb der BBox wird verändert (BBox: {x1},{y1},{x2},{y2})")
return mask
# === KORREKTE GEMEINSAME PROPORTIONALE SKALIERUNG MIT PADDING ===
"""
SKALIERT BILD UND MASKE GEMEINSAM MIT GLEICHEN PROPORTIONEN (MIT PADDING)
Behält das Seitenverhältnis bei und fügt ggf. Padding hinzu
Parameter:
- image: PIL Image (RGB)
- mask: PIL Image (L-Modus, Maske)
- target_size: Zielgröße (Standard 512)
Returns:
- padded_image: skaliertes Bild mit Padding (RGB)
- padded_mask: skalierte Maske mit Padding (L)
- padding_info: Dictionary mit Skalierungsinfo für späteres Compositing
"""
# Herunterskalierung von Bild und BBox auf 512x512 für ControlnetInpaint-Pipeline
def scale_image_and_mask_together(image, mask, target_size=512, bbox_coords=None, mode=None):
if image is None or mask is None:
raise ValueError("Bild oder Maske ist None")
# MAX SIZE CHECK HINZUFÜGEN
if image.width > MAX_IMAGE_SIZE or image.height > MAX_IMAGE_SIZE:
raise ValueError(f"Bild zu groß! Maximal {MAX_IMAGE_SIZE}×{MAX_IMAGE_SIZE}")
if image.size != mask.size:
raise ValueError(f"Bild und Maske haben unterschiedliche Größen: {image.size} vs {mask.size}") #Stoppt Programm sofort mit Fehlermeldung!
#Variablen für Bildmaße
original_width, original_height = image.size
# Bestimme Skalierungsfaktor (längere Seite auf target_size)
scale = target_size / max(original_width, original_height)
new_width = int(original_width * scale)
new_height = int(original_height * scale)
print(f"📐 Gemeinsame Skalierung: {original_width}x{original_height}{new_width}x{new_height} (Skalierung: {scale:.4f})")
# Skaliere Bild und Maske getrennt voneinander aber proportional
scaled_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
scaled_mask = mask.resize((new_width, new_height), Image.Resampling.NEAREST)
# Auf Zielgröße padden (zentriert)
#Image.new("RGB", (target_size, target_size), (0, 0, 0)) erstellt ein neues, leeres, schwarzes Bild in der Ziel-Verarbeitungsgröße des Modells (512×512 für SD 1.5 oder 1024×1024 für SDXL)
# in das später das Bild eingefügt wird
padded_image = Image.new("RGB", (target_size, target_size), (0, 0, 0))
#Damit wird ein 512x512 Graustufenbild erstellt in das später die BBox eingefügt wird
padded_mask = Image.new("L", (target_size, target_size), 0)
# Zentrierte Position berechnen
# das ist der Padding-Bereich bei nicht quadratischen 512x512 Bildern damit daraus 512x512-Bilder werden
x_offset = (target_size - new_width) // 2
y_offset = (target_size - new_height) // 2
# mit Hilfe der Offsets kann das skalierte Bild mittig in das RGB-Schwarzbild eingefügt werden. Dadurch ergibt sich
# indirekt der Padding-Bereich.
padded_image.paste(scaled_image, (x_offset, y_offset))
# mit Hilfe der Offsets wird nun die herunterskalierte BBox (entweder als Rechteck oder als SAM-Maske)
# in das Graustufenbild eingefügt. Das Padding ergibt sich aus dem Graustufenbild!
padded_mask.paste(scaled_mask, (x_offset, y_offset))
# Hiermit wird die tranformierte BBox=BBox skaliert + Padding berechnet
scaled_bbox = None
if bbox_coords and all(c is not None for c in bbox_coords):
x1, y1, x2, y2 = bbox_coords
scaled_bbox = (
int(x1 * scale) + x_offset, # Einmalige, konsistente Berechnung
int(y1 * scale) + y_offset,
int(x2 * scale) + x_offset,
int(y2 * scale) + y_offset
)
print(f"📐 Skalierte BBox gespeichert: {scaled_bbox} (von {bbox_coords})")
# WICHTIG: Speichere alle Informationen für späteres Compositing
padding_info = {
'x_offset': x_offset,
'y_offset': y_offset,
'scaled_width': new_width,
'scaled_height': new_height,
'original_width': original_width,
'original_height': original_height,
'scale_factor': scale,
'target_size': target_size,
'original_bbox': bbox_coords,
'scaled_bbox': scaled_bbox,
'mode': mode
}
print(f"📦 Padding hinzugefügt: Offsets ({x_offset}, {y_offset})")
print(f"BBox gespeicher: {bbox_coords}, Modus:{mode}")
return padded_image, padded_mask, padding_info
# Composition Workflow nach Ausgabe ControlnetInpaint-Pipeline
def enhanced_composite_with_sam(original_image, inpaint_result, original_mask,
padding_info, bbox_coords, mode):
"""
COMPOSITING MIT SAM-MASKEN UND BBox-KOORDINATEN
Berücksichtigt die präzisen Kanten der SAM-Maske
"""
print(f"🎨 Verbessertes Compositing für Modus: {mode}")
# Extrahiere Padding-Info
x_offset = padding_info['x_offset']
y_offset = padding_info['y_offset']
scaled_width = padding_info['scaled_width']
scaled_height = padding_info['scaled_height']
scale_factor = padding_info['scale_factor']
original_width = padding_info['original_width']
original_height = padding_info['original_height']
# ==============================================
# FALL 1: Bild war bereits 512×512 (keine Skalierung)
# ==============================================
if scale_factor == 1.0 and x_offset == 0 and y_offset == 0:
print(f"✅ FALL 1: Bild 512×512 - kein Compositing nötig")
return inpaint_result
# ==============================================
# FALL 2 & 3: Bild wurde skaliert
# ==============================================
print(f"🔄 FALL 2/3: Bild skaliert - Compositing mit SAM-Maske")
# 1. PADDING ENTFERNEN von 512×512 Ergebnis
downscaled_result = inpaint_result.crop(
(x_offset, y_offset, x_offset + scaled_width, y_offset + scaled_height)
)
# 2. AUF ORIGINALGRÖßE SKALIEREN
final_image = original_image.copy()
if mode == "environment_change":
# ==============================================
# MODUS: UMWELT ÄNDERN (Objekt bleibt original)
# In dem Fall muß die BBox nicht berücksichtigt werden da Originalbild ausgeschnitten wird
# anhand der SAM-Maske
# ==============================================
print("🌳 Modus: Umwelt ändern mit SAM-Maske")
# Gesamtes bearbeitetes Bild hochskalieren
new_background = downscaled_result.resize(
(original_width, original_height),
Image.Resampling.LANCZOS
)
# Originalbild mit SAM-Maske einfügen
original_with_alpha = original_image.copy().convert("RGBA")
# Invertierte Maske für Objekterhalt
mask_inverted = Image.eval(original_mask, lambda x: 255 - x)
# Weiche Kanten für natürlichen Übergang
soft_mask = mask_inverted.filter(ImageFilter.GaussianBlur(3))
original_with_alpha.putalpha(soft_mask)
# Compositing
final_image = new_background.copy().convert("RGBA")
final_image.paste(original_with_alpha, (0, 0), original_with_alpha)
else:
# ==============================================
# MODUS: FOCUS oder GESICHT ÄNDERN
# Hier muß die BBox berücksichtigt werden da generiertes Bild ausgeschnitten wird
# ohne die BBox wird entlang der SAM-Maske geschnitten -> ungenau!
# ==============================================
mode_name = "Focus" if mode == "focus_change" else "Gesicht"
print(f"👤 Modus: {mode_name} ändern mit SAM-Maske")
if not bbox_coords or not all(c is not None for c in bbox_coords):
# Keine BBox: gesamtes Bild zurückgeben
final_image = downscaled_result.resize(
(original_width, original_height),
Image.Resampling.LANCZOS
)
return final_image.convert("RGB")
#BBox-Koordinaten korrekt transformieren
#Die BBox-Koordinaten müssen vom Originalbild nach 512x512 transformiert werden
bbox_scaled = (
int(bbox_coords[0] * scale_factor),
int(bbox_coords[1] * scale_factor),
int(bbox_coords[2] * scale_factor),
int(bbox_coords[3] * scale_factor)
)
#Mit den Padding-Offsets wird bei nicht quadratischen 512x512 Bildern
#das Padding hinzugefügt
bbox_in_512 = (
bbox_scaled[0] + x_offset,
bbox_scaled[1] + y_offset,
bbox_scaled[2] + x_offset,
bbox_scaled[3] + y_offset
)
if bbox_in_512[2] > bbox_in_512[0] and bbox_in_512[3] > bbox_in_512[1]:
# Bearbeiteten Bereich aus dem 512×512-Ergebnis ausschneiden
edited_region = inpaint_result.crop(bbox_in_512)
# Auf ORIGINAL-BBox-Größe skalieren
original_bbox_size = (bbox_coords[2] - bbox_coords[0],
bbox_coords[3] - bbox_coords[1])
edited_region_fullsize = edited_region.resize(
original_bbox_size,
Image.Resampling.LANCZOS
)
# SAM-Maske für den Bereich zuschneiden und weichzeichnen
mask_cropped = original_mask.crop(bbox_coords)
soft_mask = mask_cropped.filter(ImageFilter.GaussianBlur(3))
# Alpha-Compositing mit präziser SAM-Maske
edited_rgba = edited_region_fullsize.convert("RGBA")
mask_rgba = soft_mask.convert("L") # SAM-Maske als Alpha-Kanal
temp_image = Image.new("RGBA", original_bbox_size, (0, 0, 0, 0))
temp_image.paste(edited_rgba, (0, 0), mask_rgba)
final_image.paste(temp_image, (bbox_coords[0], bbox_coords[1]), temp_image)
print(f"✅ Korrektes Compositing abgeschlossen. Finale Größe: {final_image.size}")
return final_image.convert("RGB")
def auto_detect_face_area(image):
"""Optimierten Vorschlag für Gesichtsbereich ohne externe Bibliotheken"""
width, height = image.size
face_size = min(width, height) * 0.4
x1 = (width - face_size) / 2
y1 = (height - face_size) / 4
x2 = x1 + face_size
y2 = y1 + face_size * 1.2
# Sortiere Koordinaten und stelle sicher, dass sie innerhalb des Bildes liegen
x1 = max(0, int(min(x1, x2)))
y1 = max(0, int(min(y1, y2)))
x2 = min(width, int(max(x1, x2)))
y2 = min(height, int(max(y1, y2)))
print(f"Geschätzte Gesichtskoordinaten: [{x1}, {y1}, {x2}, {y2}] (Bild: {width}x{height})")
return [x1, y1, x2, y2]
# === PIPELINES ===
pipe_txt2img = None
current_pipe_model_id = None
pipe_img2img = None
pipe_img2img_pose = None
pipe_img2img_depth = None
#Das Laden des Modells bedeutet, die trainierten Gewichte (Parameter) von der Festplatte zu lesen und
#im Arbeitsspeicher (RAM) und idealerweise im Grafikspeicher (VRAM) zu halten, damit sie für Berechnungen schnell verfügbar sind.
#die Funktion load_txt2img() verwaltet zwei separate Pipeline-Instanzen (für SD 1.5 und Realistic Vision) und gibt je nach
#Modellauswahl (model_id) die entsprechende Instanz zurück.
def load_txt2img(model_id):
"""Lädt das Text-to-Image Modell basierend auf der Auswahl"""
global pipe_txt2img, current_pipe_model_id
if pipe_txt2img is not None and current_pipe_model_id == model_id:
print(f"✅ Modell {model_id} bereits geladen")
return pipe_txt2img
print(f"🔄 Lade Modell: {model_id}")
config = MODEL_CONFIGS.get(model_id, MODEL_CONFIGS["runwayml/stable-diffusion-v1-5"])
print(f"📋 Modell-Konfiguration: {config['name']}")
print(f"📝 Beschreibung: {config['description']}")
try:
# VAE-Handling basierend auf Modellkonfiguration (Realistic Vision hat kein VAE-der Autoencoder ist ein CNN)
vae = None
if config.get("requires_vae", False):
print(f"🔧 Lade externe VAE: {config['vae_model']}")
try:
vae = AutoencoderKL.from_pretrained(
config["vae_model"],
torch_dtype=torch_dtype
).to(device)
print("✅ VAE erfolgreich geladen")
except Exception as vae_error:
print(f"⚠️ Fehler beim Laden der VAE: {vae_error}")
print("ℹ️ Versuche ohne VAE weiter...")
vae = None
model_params = {
"torch_dtype": torch_dtype,
"safety_checker": None,
"requires_safety_checker": False,
"add_watermarker": False,
"cache_dir": "/tmp/models" # Für Hugging Face Spaces
}
# Die Modelle haben unterscgiedliche Gewichtsformate. Safetensors neu und schneller Zugriff!
if model_id == "SG161222/Realistic_Vision_V6.0_B1_noVAE":
model_params["allow_pickle"] = False # WICHTIG für PyTorch 2.6
model_params["use_safetensors"] = False
print("⚠️ Realistic Vision Modell - Nutzt .bin-Dateien.")
else:
model_params["allow_pickle"] = True
model_params["use_safetensors"] = True
print("✅ Verwende SafeTensors für sicheres Laden.")
if config.get("supports_fp16", False) and torch_dtype == torch.float16:
model_params["variant"] = "fp16"
print("ℹ️ Verwende FP16 Variante")
else:
print("ℹ️ Verwende Standard Variante (kein FP16)")
if vae is not None:
model_params["vae"] = vae
print(f"📥 Lade Hauptmodell von Hugging Face...")
pipe_txt2img = StableDiffusionPipeline.from_pretrained(
model_id,
**model_params
).to(device)
# Der Scheduler (z.B. DPM-Solver++ oder PNDM) ist der Algorithmus, der den Zeitplan für das schrittweise Entrauschen (Denoising)
# festlegt - er bestimmt, wie viele und welche Rauschschritte in welcher Reihenfolge abgearbeitet werden.
print("⚙️ Konfiguriere Scheduler...")
if pipe_txt2img.scheduler is None:
print("⚠️ Scheduler ist None, setze Standard-Scheduler")
pipe_txt2img.scheduler = PNDMScheduler.from_pretrained(
model_id,
subfolder="scheduler"
)
try:
if hasattr(pipe_txt2img.scheduler, 'config'):
scheduler_config = pipe_txt2img.scheduler.config
else:
scheduler_config = {
"beta_start": 0.00085,
"beta_end": 0.012,
"beta_schedule": "scaled_linear",
"num_train_timesteps": 1000,
"prediction_type": "epsilon",
"steps_offset": 1
}
print("⚠️ Keine Scheduler-Konfig gefunden, verwende Standard")
pipe_txt2img.scheduler = DPMSolverMultistepScheduler.from_config(
scheduler_config,
use_karras_sigmas=True,
algorithm_type="sde-dpmsolver++"
)
print("✅ DPM-Solver Multistep Scheduler konfiguriert")
except Exception as scheduler_error:
print(f"⚠️ Konnte DPM-Scheduler nicht setzen: {scheduler_error}")
print("ℹ️ Verwende Standard-Scheduler weiter")
pipe_txt2img.enable_attention_slicing()
print("✅ Attention Slicing aktiviert")
# Attention Slicing ist Aufteilung der Attention-Matrix auf die Heads -> späteres concat
if hasattr(pipe_txt2img, 'vae') and pipe_txt2img.vae is not None:
try:
pipe_txt2img.enable_vae_slicing()
if hasattr(pipe_txt2img.vae, 'enable_slicing'):
pipe_txt2img.vae.enable_slicing()
print("✅ VAE Slicing aktiviert")
except Exception as vae_slice_error:
print(f"⚠️ VAE Slicing nicht möglich: {vae_slice_error}")
current_pipe_model_id = model_id
print(f"✅ {config['name']} erfolgreich geladen")
print(f"📊 Modell-Dtype: {pipe_txt2img.dtype}")
print(f"📊 Scheduler: {type(pipe_txt2img.scheduler).__name__}")
print(f"⚙️ Empfohlene Einstellungen: Steps={config['recommended_steps']}, CFG={config['recommended_cfg']}")
return pipe_txt2img
except Exception as e:
print(f"❌ Fehler beim Laden von {model_id}: {str(e)[:200]}...")
import traceback
traceback.print_exc()
print("🔄 Fallback auf SD 1.5...")
try:
pipe_txt2img = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch_dtype,
use_safetensors=True,
).to(device)
pipe_txt2img.enable_attention_slicing()
current_pipe_model_id = "runwayml/stable-diffusion-v1-5"
print("✅ Fallback auf SD 1.5 erfolgreich")
return pipe_txt2img
except Exception as fallback_error:
print(f"❌ Auch Fallback fehlgeschlagen: {fallback_error}")
raise
def load_img2img(keep_environment=False):
global pipe_img2img_pose, pipe_img2img_depth
# Initialisiere globale Variablen, falls noch nicht geschehen
if 'pipe_img2img_pose' not in globals():
pipe_img2img_pose = None
if 'pipe_img2img_depth' not in globals():
pipe_img2img_depth = None
if keep_environment:
# ===== MODUS: Depth + Canny =====
if pipe_img2img_depth is None:
print("🔄 Lade Multi-ControlNet-Inpainting-Modell (Depth + Canny)...")
try:
# LADE BEIDE ControlNet-Modelle für Depth-Modus
controlnet_depth = ControlNetModel.from_pretrained(
"lllyasviel/sd-controlnet-depth",
torch_dtype=torch_dtype
)
controlnet_canny = ControlNetModel.from_pretrained(
"lllyasviel/sd-controlnet-canny",
torch_dtype=torch_dtype
)
# WICHTIG: Reihenfolge muss mit prepare_controlnet_maps übereinstimmen!
# [Depth, Canny]
pipe_img2img_depth = StableDiffusionControlNetInpaintPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=[controlnet_depth, controlnet_canny], # Depth zuerst!
torch_dtype=torch_dtype,
safety_checker=None,
requires_safety_checker=False,
cache_dir="/tmp/models",
use_safetensors=True
).to(device)
# Scheduler konfigurieren
pipe_img2img_depth.scheduler = DPMSolverMultistepScheduler.from_config(
pipe_img2img_depth.scheduler.config,
algorithm_type="sde-dpmsolver++",
use_karras_sigmas=True,
timestep_spacing="trailing"
)
# Optimierungen
pipe_img2img_depth.enable_attention_slicing()
print("✅ Multi-ControlNet-Inpainting-Pipeline geladen (Depth + Canny)")
except Exception as e:
print(f"❌ Fehler beim Laden der Depth+Canny Pipeline: {e}")
raise
return pipe_img2img_depth
else:
# ===== MODUS: OpenPose + Canny =====
if pipe_img2img_pose is None:
print("🔄 Lade Multi-ControlNet-Inpainting-Modell (OpenPose + Canny)...")
try:
# LADE BEIDE ControlNet-Modelle für Pose-Modus
controlnet_openpose = ControlNetModel.from_pretrained(
"lllyasviel/sd-controlnet-openpose",
torch_dtype=torch_dtype
)
controlnet_canny = ControlNetModel.from_pretrained(
"lllyasviel/sd-controlnet-canny",
torch_dtype=torch_dtype
)
# WICHTIG: Reihenfolge muss mit prepare_controlnet_maps übereinstimmen!
# [OpenPose, Canny]
pipe_img2img_pose = StableDiffusionControlNetInpaintPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=[controlnet_openpose, controlnet_canny], # OpenPose zuerst!
torch_dtype=torch_dtype,
safety_checker=None,
requires_safety_checker=False,
cache_dir="/tmp/models",
use_safetensors=True
).to(device)
# Scheduler konfigurieren
pipe_img2img_pose.scheduler = DPMSolverMultistepScheduler.from_config(
pipe_img2img_pose.scheduler.config,
algorithm_type="sde-dpmsolver++",
use_karras_sigmas=True,
timestep_spacing="trailing"
)
# Optimierungen
pipe_img2img_pose.enable_attention_slicing()
print("✅ Multi-ControlNet-Inpainting-Pipeline geladen (OpenPose + Canny)")
except Exception as e:
print(f"❌ Fehler beim Laden der OpenPose+Canny Pipeline: {e}")
raise
return pipe_img2img_pose
#Die Callback-Funktion wird von der Pipeline nach jedem Verarbeitungsschritt aufgerufen und erhält Informationen
#wie den aktuellen step und timestep. Diese nutzt der Progressbalken-Callback, um den Fortschritt zu berechnen und anzuzeigen.
# === CALLBACK-FUNKTIONEN FÜR FORTSCHRITT ===
class TextToImageProgressCallback:
def __init__(self, progress, total_steps):
self.progress = progress
self.total_steps = total_steps
self.current_step = 0
def __call__(self, pipe, step, timestep, callback_kwargs):
self.current_step = step + 1
progress_percent = (step / self.total_steps) * 100
self.progress(progress_percent / 100, desc="Generierung läuft...")
return callback_kwargs
class ImageToImageProgressCallback:
def __init__(self, progress, total_steps, strength):
self.progress = progress
self.total_steps = total_steps
self.current_step = 0
self.strength = strength
self.actual_total_steps = None
def __call__(self, pipe, step, timestep, callback_kwargs):
self.current_step = step + 1
if self.actual_total_steps is None:
self.actual_total_steps = int(self.total_steps * self.strength)
print(f"🎯 Steps: {self.total_steps} × {self.strength}{self.actual_total_steps} tatsächliche Denoising-Schritte")
progress_percent = (step / self.actual_total_steps) * 100
self.progress(progress_percent / 100, desc="Generierung läuft...")
return callback_kwargs
# === NEUE FUNKTIONEN FÜR DIE FEATURES (ANGEPASST FÜR 3 MODI) ===
def create_preview_image(image, bbox_coords, mode):
"""
NEUE FUNKTION: Erstellt Vorschau basierend auf 3 Modi mit farbigen Rahmen
Parameter:
- image: PIL Image
- bbox_coords: [x1, y1, x2, y2]
- mode: "environment_change", "focus_change", "face_only_change"
Returns:
- PIL Image mit farbigem Rahmen und Text
"""
if image is None:
return None
preview = image.copy()
draw = ImageDraw.Draw(preview)
# Farben basierend auf Modus
if mode == "environment_change":
border_color = (0, 255, 0, 180) # Grün für Umgebung
mode_text = "UMGEBUNG ÄNDERN (Bereich geschützt)"
box_color = (255, 255, 0, 200) # Gelb für geschützten Bereich
text_bg_color = (0, 128, 0, 160) # Dunkelgrün
elif mode == "focus_change":
border_color = (255, 165, 0, 180) # Orange für Focus
mode_text = "FOCUS VERÄNDERN (Bereich+Körper)"
box_color = (255, 0, 0, 200) # Rot für Veränderungsbereich
text_bg_color = (255, 140, 0, 160) # Dunkelorange
elif mode == "face_only_change":
border_color = (255, 0, 0, 180) # Rot für nur Gesicht
mode_text = "NUR BEREICH VERÄNDERN"
box_color = (255, 0, 0, 200) # Rot für Veränderungsbereich
text_bg_color = (128, 0, 0, 160) # Dunkelrot
else:
# Fallback
border_color = (128, 128, 128, 180)
mode_text = "UNBEKANNTER MODUS"
box_color = (128, 128, 128, 200)
text_bg_color = (64, 64, 64, 160)
# Skaliere Rahmendicke basierend auf Bildgröße (sonst bei großen Bildern ganz dünne Rahmen!)
border_width = max(8, image.width // 200) # Mindestens 8px, bei großen Bildern dicker
draw.rectangle([0, 0, preview.width-1, preview.height-1],
outline=border_color, width=border_width)
if bbox_coords and all(coord is not None for coord in bbox_coords):
# Sortiere Koordinaten
x1, y1, x2, y2 = sort_coordinates(*bbox_coords)
# Stelle sicher, dass die Koordinaten innerhalb des Bildes liegen
x1 = max(0, min(x1, preview.width-1))
y1 = max(0, min(y1, preview.height-1))
x2 = max(0, min(x2, preview.width-1))
y2 = max(0, min(y2, preview.height-1))
# Nur zeichnen, wenn die Bounding Box gültig ist
if x2 > x1 and y2 > y1:
# Skaliere Box-Rahmen basierend auf Bildgröße
box_width = max(3, image.width // 400)
draw.rectangle([x1, y1, x2, y2], outline=box_color, width=box_width)
text_color = (255, 255, 255)
# Text über der Bounding Box platzieren
text_y = max(0, y1 - 25)
text_bbox = draw.textbbox((x1, text_y), mode_text)
draw.rectangle([text_bbox[0]-5, text_bbox[1]-2, text_bbox[2]+5, text_bbox[3]+2],
fill=text_bg_color)
draw.text((x1, text_y), mode_text, fill=text_color)
return preview
def update_live_preview(image, bbox_x1, bbox_y1, bbox_x2, bbox_y2, mode):
"""
Aktualisiert die Live-Vorschau bei Koordinaten-Änderungen
NEU: Verwendet 3 Modi statt Boolean
"""
if image is None:
return None
# Sortiere die Koordinaten (Slider zeigen Originalkoordinaten)
bbox_coords = sort_coordinates(bbox_x1, bbox_y1, bbox_x2, bbox_y2)
return create_preview_image(image, bbox_coords, mode)
def process_image_upload(image):
"""Verarbeitet Bild-Upload -wenn kein Bild hochgeladen wird None zurückgegeben-> kein Absturz! und gibt Bild + Koordinaten zurück"""
if image is None:
return None, None, None, None, None
width, height = image.size
# Berechne Bounding-Box basierend auf der tatsächlichen Bildgröße
bbox = auto_detect_face_area(image)
# Sortiere die Koordinaten
bbox_x1, bbox_y1, bbox_x2, bbox_y2 = sort_coordinates(*bbox)
# Für die Vorschau verwende die Originalkoordinaten
preview = create_preview_image(image, [bbox_x1, bbox_y1, bbox_x2, bbox_y2], "environment_change")
# Slider-Werte SIND JETZT ORIGINALKOORDINATEN (keine 512-Skalierung!)
print(f"Bild {width}x{height} -> Slider-Originalwerte: [{bbox_x1}, {bbox_y1}, {bbox_x2}, {bbox_y2}]")
return preview, bbox_x1, bbox_y1, bbox_x2, bbox_y2
# === FUNKTION FÜR SLIDER-UPDATE ===
def update_slider_for_image(image):
"""Aktualisiert Slider-Maxima basierend auf Bildgröße bis 4096x4096"""
if image is None:
return (
gr.update(maximum=MAX_IMAGE_SIZE),
gr.update(maximum=MAX_IMAGE_SIZE),
gr.update(maximum=MAX_IMAGE_SIZE),
gr.update(maximum=MAX_IMAGE_SIZE)
)
width, height = image.size
# Setze Slider-Maxima auf Bildgröße (begrenzt auf MAX_IMAGE_SIZE für Stabilität)
max_width = min(width, MAX_IMAGE_SIZE)
max_height = min(height, MAX_IMAGE_SIZE)
print(f"Slider-Maxima gesetzt auf: {max_width}x{max_height}")
return (
gr.update(maximum=max_width),
gr.update(maximum=max_height),
gr.update(maximum=max_width),
gr.update(maximum=max_height)
)
def text_to_image(prompt, model_id, steps, guidance_scale, progress=gr.Progress()):
try:
if not prompt or not prompt.strip():
return None, "Bitte einen Prompt eingeben"
print("\n" + "="*80)
print(f"🚀 Starte Generierung mit Modell: {model_id}")
print("\n" + "="*80)
print(f"📝 Prompt: {prompt}")
# Automatische negative Prompts generieren
auto_negatives = auto_negative_prompt(prompt)
print(f"🤖 Automatisch generierte Negative Prompts: {auto_negatives}")
start_time = time.time()
# Liste von Qualitätswörtern/Gewichten, die auf Benutzereingaben prüfen
quality_keywords = ['masterpiece', 'best quality', 'high quality', 'highly detailed',
'exquisite', 'ultra detailed', 'professional',
'perfect', 'excellent', 'amazing', 'stunning', 'beautiful']
# Prüfe, ob der Benutzer bereits Qualitätswörter/Gewichte verwendet hat
user_has_quality_words = False
# Konvertiere Prompt zu Kleinbuchstaben für die Prüfung
prompt_lower = prompt.lower()
# Prüfe auf einfache Qualitätswörter
for keyword in quality_keywords:
if keyword in prompt_lower:
user_has_quality_words = True
print(f"✓ Benutzer verwendet bereits Qualitätswort: {keyword}")
break
# Prüfe auf Gewichte (z.B. (word:1.5), [word], etc.)
weight_patterns = [r'\([^)]+:\d+(\.\d+)?\)', r'\[[^\]]+\]']
for pattern in weight_patterns:
if re.search(pattern, prompt):
user_has_quality_words = True
print("✓ Benutzer verwendet bereits Gewichte im Prompt")
break
# Prompt basierend auf Prüfung anpassen
if not user_has_quality_words:
enhanced_prompt = f"masterpiece, best quality, {prompt}"
print(f"🔄 Verbesserter Prompt: {enhanced_prompt}")
else:
enhanced_prompt = prompt
print("✓ Benutzerprompt wird unverändert verwendet")
print(f"Finaler Prompt für Generation: {enhanced_prompt}")
progress(0, desc="Lade Modell...")
pipe = load_txt2img(model_id)
seed = random.randint(0, 2**32 - 1)
generator = torch.Generator(device=device).manual_seed(seed)
print(f"🌱 Seed: {seed}")
callback = TextToImageProgressCallback(progress, steps)
print(f"⚙️ Einstellungen: Steps={steps}, CFG={guidance_scale}")
image = pipe(
prompt=enhanced_prompt,
negative_prompt=auto_negatives,
height=512,
width=512,
num_inference_steps=int(steps),
guidance_scale=guidance_scale,
generator=generator,
callback_on_step_end=callback,
callback_on_step_end_tensor_inputs=[],
).images[0]
end_time = time.time()
duration = end_time - start_time
print(f"✅ Bild generiert in {duration:.2f} Sekunden")
config = MODEL_CONFIGS.get(model_id, MODEL_CONFIGS["runwayml/stable-diffusion-v1-5"])
status_msg = f"✅ Generiert mit {config['name']} in {duration:.1f}s"
return image, status_msg
except Exception as e:
error_msg = f"❌ Fehler: {str(e)}"
print(f"❌ Fehler in text_to_image: {e}")
import traceback
traceback.print_exc()
return None, error_msg
def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale,
mode, bbox_x1, bbox_y1, bbox_x2, bbox_y2,
progress=gr.Progress()):
"""
KORRIGIERTE HAUPTFUNKTION FÜR CONTROLNET-GESTEUERTES INPAINTING
"""
try:
if image is None:
return None, None, None, None, None
import time, random
start_time = time.time()
print("\n" + "="*80)
print(f"🚀 Img2Img Start → Modus: {mode}")
print("\n" + "="*80)
print(f"📊 Einstellungen: Strength: {strength}, Steps: {steps}, Guidance: {guidance_scale}")
print(f"📝 Prompt: {prompt}")
print(f"🚫 Negativ-Prompt: {neg_prompt}")
final_image = None # Variable wird initiiert!
# ===== AUTOMATISCHEN NEGATIV-PROMPT GENERIEREN =====
auto_negatives = auto_negative_prompt(prompt)
print(f"🤖 Automatisch generierter Negativ-Prompt: {auto_negatives}")
# ===== KOMBINIERE MANUELLEN UND AUTOMATISCHEN PROMPT =====
combined_negative_prompt = ""
if neg_prompt and neg_prompt.strip():
user_neg = neg_prompt.strip()
print(f"👤 Benutzer Negativ-Prompt: {user_neg}")
user_words = [word.strip().lower() for word in user_neg.split(",")]
auto_words = [word.strip().lower() for word in auto_negatives.split(",")]
combined_words = user_words.copy()
for auto_word in auto_words:
if auto_word and auto_word not in user_words:
combined_words.append(auto_word)
unique_words = []
seen_words = set()
for word in combined_words:
if word and word not in seen_words:
unique_words.append(word)
seen_words.add(word)
combined_negative_prompt = ", ".join(unique_words)
else:
combined_negative_prompt = auto_negatives
print(f"ℹ️ Kein manueller Negativ-Prompt, verwende nur automatischen: {combined_negative_prompt}")
print(f"✅ Finaler kombinierter Negativ-Prompt: {combined_negative_prompt}")
# ===== PROMPT-BOOSTER FÜR DREI MODI =====
if mode == "face_only_change":
face_boosters = "(perfect face:1.2), (symmetrical face:1.1), realistic shaded perfect face, "
if not any(keyword in prompt.lower() for keyword in
["perfect face", "symmetrical", "realistic face", "shaded face"]):
enhanced_prompt = face_boosters + prompt
print(f"👤 Gesichts-Booster hinzugefügt: {face_boosters}")
else:
enhanced_prompt = prompt
print(f"👤 Benutzer hat bereits Gesichts-Booster im Prompt")
elif mode == "focus_change":
focus_boosters = "(sharp focus:1.2), (detailed subject:1.1), (clear foreground:1.1), "
if not any(keyword in prompt.lower() for keyword in
["sharp focus", "detailed subject", "clear foreground", "well-defined"]):
enhanced_prompt = focus_boosters + prompt
print(f"🎯 Focus-Booster hinzugefügt: {focus_boosters}")
else:
enhanced_prompt = prompt
print(f"🎯 Benutzer hat bereits Focus-Booster im Prompt")
elif mode == "environment_change":
background_boosters = "complete scene, full background, entire environment, "
if not any(keyword in prompt.lower() for keyword in
["complete scene", "full background", "entire environment", "whole setting"]):
enhanced_prompt = background_boosters + prompt
print(f"🌳 Hintergrund-Booster hinzugefügt: {background_boosters}")
else:
enhanced_prompt = prompt
print(f"🌳 Benutzer hat bereits Hintergrund-Booster im Prompt")
else:
enhanced_prompt = prompt
print(f"🎯 Finaler Prompt für {mode}: {enhanced_prompt}")
progress(0, desc="Starte Generierung...")
####################################################################################################################################
# ===== OPTIMIERTE MODUS-SPEZIFISCHE EINSTELLUNGEN FÜR CONTROLNET =====
# Je radikaler die Veränderung, desto weniger ControlNet braucht man!
# Radikale Veränderung (Struktur komplett anders-Mensch/Auto):controlnet_strength = 0.3
# STRUKTURELLER WECHSEL (Anatomie ändert sich-Mensch/Tier): controlnet_strength = 0.4-0.5
# Detailveränderung (gleiche Struktur-Mensch/Hexe):controlnet_strength = 0.5-0.6
# Je spezifischer ich weiß, WAS genau gleich bleiben soll, desto gezielter wähle ich die entsprechende ControlNet-Map.
# Demnach ist ControlNet für den Erhalt der gewünschten Bildobjekte. Canny-erhält Kanten, Pose-Köperhalten, Depth-Tiefeninformationen
####################################################################################################################################
if mode == "focus_change":
# Optimale UI-Werte (für alle focus_change-Fälle):
# Strength: 0.55 – 0.6 → ideal: 0.58
# Guidance (CFG): 7.5 – 8 → ideal: 8
# Steps: 32 – 36
keep_environment = False
# adj_strength ist die Denoising-Stärke.0.1-0.3: Leichte Veränderung (behält Original)
# 0.4-0.6: Mittlere Veränderung, 0.7-0.9: Starke Veränderung
adj_strength = min(0.6, strength)
# CONTROLNET-STÄRKE=Anteil der Controlnet-Kontrolle die an Inpaint weitergegeben werden soll
# 0.3-0.5: Wenig Kontrolle → Inpaint hat mehr Freiheit, 0.6-0.8: Mittlere Kontrolle → Balance
# 0.9: Starke Kontrolle → Inpaint folgt streng ControlNet
controlnet_strength = 0.5 # Stärkere ControlNet-Kontrolle für Inpaint
# Konvertiert den gesamten Prompt in Kleinbuchstaben um ggf. bei den keywords zu mappen
prompt_lower = prompt.lower()
# Keyword-Gruppen
humanoid_keywords = [
"anime", "cartoon", "manga", "witch", "wizard", "sorcerer",
"alien", "elf", "fairy", "character", "fantasy", "superhero",
"cyborg", "robot", "android", "santa", "person", "woman", "man",
"girl", "boy", "child", "business", "suit", "professional",
"sports", "athlete", "runner", "dancer", "portrait", "face"
]
object_keywords = [
"car", "vehicle", "automobile", "chair", "table", "desk",
"statue", "sculpture", "monument", "lamp", "bottle", "vase",
"product", "object", "furniture", "device", "tool", "item",
"building", "house", "tree", "plant", "rock", "stone"
]
animal_keywords = [
"dog", "cat", "wolf", "lion", "tiger", "bear", "rabbit",
"horse", "bird", "animal", "creature", "beast", "monkey",
"elephant", "giraffe", "zebra", "deer", "fox", "pet"
]
# Standard-Ratio - Ratio entscheidet über Anatomie und Stilfreiheit
pose_ratio = 0.7 # 70%
canny_ratio = 0.3 # 30%
# Anpassung für Humanoid → Humanoid
if any(keyword in prompt_lower for keyword in humanoid_keywords):
adj_strength = 0.5 # wie stark entrauscht wird. Wenn Bereiche transparent oft nicht genug entrauscht. Strukturveränderung ist nicht sehr hoch-niedriger Wert!
controlnet_strength = 0.8 # controlnet_strength runter: Reduziert global und gleichmäßig den Einfluss beider Maps. Man kann auch beide Maps einzeln heruntersetzen. Ist das Gleiche!
pose_ratio = 0.60 # 95%Pose, 5%Canny - wenn Pose gehalten und kaum Detailveränderung-
canny_ratio = 0.10 # wenn Pose gehalten und mehr Detailveränderung -empfohlen 0.85/0.15, canny schlecht für Anime
print("👤 Humanoid → Humanoid → Ratio 85:15 (Pose:Canny)")
# Anpassung für Gegenstand → Gegenstand
elif any(keyword in prompt_lower for keyword in object_keywords):
adj_strength = min(0.7, strength * 1.15)
controlnet_strength = 0.5
pose_ratio = 0.10 # 10% Pose
canny_ratio = 0.90 # 90% Canny
print("📦 Gegenstand → Gegenstand → Ratio 25:75 (Pose:Canny)")
# Anpassung für Mensch → Tier
elif any(keyword in prompt_lower for keyword in animal_keywords):
adj_strength = min(0.6, strength * 1.1)
controlnet_strength = 0.5
pose_ratio = 0.5 # 50% Pose - empfohlen 0.45/0.55
canny_ratio = 0.5 # 50% Canny
print("🐾 Mensch → Tier → Ratio 50:50 (Pose:Canny)")
# CONDITIONING SCALE BERECHNEN (genau wie environment_change)
conditioning_scale = [
controlnet_strength * pose_ratio, # OpenPose
controlnet_strength * canny_ratio # Canny
]
print(f"🎯 MODUS: Focus verändern")
print(f" Strength: {adj_strength}, ControlNet: {controlnet_strength}")
print(f" OpenPose: {pose_ratio*100}%, Canny: {canny_ratio*100}%")
print(f" Conditioning Scale: [{conditioning_scale[0]:.3f}, {conditioning_scale[1]:.3f}]")
elif mode == "environment_change":
# optimale UI-Werte:
# Strength: 0.72 – 0.78 → ideal: 0.75
# guidance (CFG): 8.5 – 9.5 → ideal: 9
# Steps: 34 – 38 → ideal: 35
keep_environment = True
# Denoising: starke Neugenerierung
adj_strength = 0.75 # Leicht runter auf Realismus
# CONTROLNET-STÄRKE
controlnet_strength = 0.55 #Inpaint kann bei Neugenerierung nicht so viel Kontrolle vertragen
# Prompt-Umsetzung in Kleinbuchstaben:
prompt_lower = prompt.lower()
# Heuristik für Naturszenen vs. Innenräume
nature_keywords = ["beach", "forest", "mountain", "ocean", "sky", "field", "landscape", "nature", "outdoor", "desert", "snow", "arctic"]
interior_keywords = ["office", "room", "interior", "kitchen", "bedroom", "living room", "indoor", "wall", "furniture"]
# Standard: wird genutzt wenn Prompt nicht eines der obigen keywords beinhaltet
depth_ratio = 0.50 # 35%
canny_ratio = 0.12 # 10%
# Anpassung für Innenräume (mehr Kantenerhalt)
if any(keyword in prompt_lower for keyword in interior_keywords):
adj_strength = 0.75
controlnet_strength = 0.55
depth_ratio = 0.55
canny_ratio = 0.20
print("🏠 Innenraum erkannt → Ratio 80:20 (Depth:Canny)")
# Anpassung für Naturszenen (maximale Flexibilität) - die optimalen UI-Werte: strength:0,72 , steps: 35 , guidance: 9-9,5
elif any(keyword in prompt_lower for keyword in nature_keywords):
adj_strength = 0.75 #wie stark das bestehende Bild überschrieben wird im kompletten Denoising-Prozess-also Strukturveränderung
controlnet_strength = 0.3
depth_ratio = 0.15 #Depth-Wert hält Maßstab und Boden (Emi groß, Liege klein - Emi im Meer)
canny_ratio = 0.00 #erzwingt Kanten - hält dadurch an alter Umgebung fest - schlecht bei Umgebungswechsel
print("🌳 Naturszene erkannt → Ratio 95:5 (Depth:Canny)")
else:
print("🎯 Standard-Ratio 90:10 (Depth:Canny)")
# 4. CONDITIONING SCALE BERECHNEN
conditioning_scale = [
controlnet_strength * depth_ratio, # Depth-Gewichtung
controlnet_strength * canny_ratio # Canny-Gewichtung
]
print(f"🎯 OPTIMIERTER MODUS: Umgebung ändern")
print(f" Strength: {adj_strength}, ControlNet: {controlnet_strength}")
print(f" Depth: {depth_ratio*100}%, Canny: {canny_ratio*100}%")
print(f" Conditioning Scale: [{conditioning_scale[0]:.3f}, {conditioning_scale[1]:.3f}]")
else: # face_only_change
# Optimale UI-Werte (entscheidend)
# Strength: 0.60 – 0.65 → ideal: 0.62
# Guidance (CFG): 7.0 – 8.0 → ideal: 7.5
# Steps: 30 – 34 → ideal: 32
keep_environment = True
adj_strength = 0.6 #Struktur
# Controlnet-zurückhaltend
controlnet_strength = 0.4
# HIER FEHLEN DIE RATIOS - Controlnet gesteuertes Inpainting:
depth_ratio = 0.55 # Für Gesicht: Depth stärker
canny_ratio = 0.10 # Canny leichter hält an Kanten fest
# KRITISCH: conditioning_scale HIER definieren (wie bei environment_change)!
conditioning_scale = [
controlnet_strength * depth_ratio,
controlnet_strength * canny_ratio
]
print(f"👤 Gesichtsmodus: Depth {depth_ratio*100}%, Canny {canny_ratio*100}%")
print(f" Strength: {adj_strength:.2f}")
print(f" ControlNet: {controlnet_strength:.3f}")
print(f" Depth: {depth_ratio*100}%, Canny: {canny_ratio*100}%")
#################################################################################################
# ===== WICHTIG: VARIABLEN FÜR KOMPLETTEN WORKFLOW =====
original_mask = None
padding_info = None
scaled_image = None
scaled_mask = None
if bbox_x1 is not None and bbox_y1 is not None and bbox_x2 is not None and bbox_y2 is not None:
print(f"🎯 BBox Koordinaten erhalten: [{bbox_x1}, {bbox_y1}, {bbox_x2}, {bbox_y2}]")
# === WICHTIGE ÄNDERUNG: SAM 2 STATT create_face_mask ===
# 1. MASKE mit SAM 2 erzeugen (transparent für Benutzer)
processed_mask, raw_mask = controlnet_processor.create_sam_mask(
image=image,
bbox_coords=(bbox_x1, bbox_y1, bbox_x2, bbox_y2),
mode=mode
)
original_mask = processed_mask
# 2. BILD UND MASKE GEMEINSAM SKALIEREN (mit Padding)
scaled_image, scaled_mask, padding_info = scale_image_and_mask_together(
image.convert("RGB"), # Originalbild
original_mask, # SAM 2 Maske (oder Fallback)
target_size=IMG_SIZE,
bbox_coords=(bbox_x1, bbox_y1, bbox_x2, bbox_y2),
mode=mode
)
print(f"✅ Gemeinsame Skalierung abgeschlossen")
print(f" Original: {image.size} → Skaliert: {scaled_image.size}")
else:
# Keine BBox: Normales Img2Img (ohne Maske)
print(f"ℹ️ Keine BBox angegeben → normales Img2Img (ohne Maske)")
scaled_image = image.convert("RGB").resize((IMG_SIZE, IMG_SIZE), Image.Resampling.LANCZOS)
scaled_mask = Image.new("L", (IMG_SIZE, IMG_SIZE), 255) # Volle Maske
padding_info = None
progress(0.1, desc="ControlNet läuft...")
# ===== CONTROLNET: MAPS ERSTELLEN =====
print(f"📊 ControlNet Input Größe: {scaled_image.size}")
controlnet_maps, debug_maps = controlnet_processor.prepare_controlnet_maps(
image=scaled_image,
keep_environment=keep_environment
)
print(f"✅ ControlNet Maps erstellt: {len(controlnet_maps)} Maps")
progress(0.3, desc="ControlNet abgeschlossen – starte Inpaint...")
# ===== CONTROLNET-INPAINTING PIPELINE ===== Laden der Pipeline!
pipe = load_img2img(keep_environment=keep_environment)
# ===== SEED UND GENERATOR =====
adj_guidance = min(guidance_scale, 12.0)
seed = random.randint(0, 2**32 - 1)
generator = torch.Generator(device=device).manual_seed(seed)
print(f"🌱 Inpaint Seed: {seed}")
# ===== FORTSCHRITTS-CALLBACK =====
callback = ImageToImageProgressCallback(progress, int(steps), adj_strength)
# ===== CONTROLNET-GESTEUERTES INPAINTING DURCHFÜHREN =====
print(f"🔄 Führe ControlNet-gesteuertes Inpainting durch...")
result = pipe(
prompt=enhanced_prompt,
negative_prompt=combined_negative_prompt,
image=scaled_image,
mask_image=scaled_mask,
control_image=controlnet_maps,
controlnet_conditioning_scale=conditioning_scale, # DYNAMISCHE Liste
strength=adj_strength,
num_inference_steps=int(steps),
guidance_scale=adj_guidance,
generator=generator,
callback_on_step_end=callback,
callback_on_step_end_tensor_inputs=[],
)
print("✅ ControlNet-Inpainting abgeschlossen")
# ===== KORREKTES COMPOSITING =====
generated_image = result.images[0]
if original_mask is not None and padding_info is not None:
# KORREKTER WORKFLOW: Nur bearbeiteten Bereich in Originalbild einfügen
final_image = enhanced_composite_with_sam(
original_image=image.convert("RGB"),
inpaint_result=generated_image,
original_mask=original_mask,
padding_info=padding_info,
bbox_coords=(bbox_x1, bbox_y1, bbox_x2, bbox_y2),
mode=mode
)
print(f"✅ Korrektes Compositing durchgeführt")
else:
# Keine Maske: Einfach das generierte Bild zurückgeben
final_image = generated_image
mask_preview = Image.new("RGB", (512, 512), color="gray")
raw_sam_mask_display = Image.new("RGB", (512, 512), color="gray")
controlnet_map1 = Image.new("RGB", (512, 512), color="gray")
controlnet_map2 = Image.new("RGB", (512, 512), color="gray")
print(f"ℹ️ Keine Maske → Direkte Rückgabe des Bildes")
end_time = time.time()
duration = end_time - start_time
print(f"✅ Transformation abgeschlossen in {duration:.2f} Sekunden")
print(f"🎯 Verwendeter Modus: {mode}")
print(f"⚙️ ControlNet: {'Depth+Canny' if keep_environment else 'OpenPose+Canny'}")
print(f"📊 Finale Bildgröße: {final_image.size}")
# 1. Maske in RGB für die Anzeige konvertieren
mask_preview = original_mask.convert("RGB")
raw_sam_mask_display = raw_mask.convert("RGB")
if "pose" in debug_maps:
controlnet_map1 = debug_maps["pose"]
map1_label = "🎭 Pose Map"
else:
controlnet_map1 = debug_maps["depth"]
map1_label = "🏔️ Depth Map"
controlnet_map2 = debug_maps["canny"]
# Return 5 Werte:
return final_image, raw_sam_mask_display, mask_preview, controlnet_map1, controlnet_map2
except Exception as e:
print(f"❌ Fehler in img_to_image: {e}")
import traceback
traceback.print_exc()
# Fallback: Return das Originalbild oder ein leeres Bild
if image is not None:
fallback_image = image.copy()
else:
fallback_image = Image.new("RGB", (512, 512), color="gray")
return final_image, None, None, None, None
def update_bbox_from_image(image):
"""Aktualisiert die Bounding-Box-Koordinaten wenn ein Bild hochgeladen wird"""
if image is None:
return None, None, None, None
bbox = auto_detect_face_area(image)
return bbox[0], bbox[1], bbox[2], bbox[3]
def update_model_settings(model_id):
"""Aktualisiert die empfohlenen Einstellungen basierend auf Modellauswahl"""
config = MODEL_CONFIGS.get(model_id, MODEL_CONFIGS["runwayml/stable-diffusion-v1-5"])
return (
config["recommended_steps"], # steps
config["recommended_cfg"], # guidance_scale
f"📊 Empfohlene Einstellungen: {config['recommended_steps']} Steps, CFG {config['recommended_cfg']}"
)
def main_ui():
"""
HAUPT-UI (ANGEPASST FÜR 3 MODI)
"""
with gr.Blocks(
title="AI Image Generator",
theme=gr.themes.Base(),
css="""
/* ===== INFO-BOXEN über Textboxen ===== */
.info-box {
background: #f8fafc;
padding: 8px 12px;
border-radius: 6px;
border: 2px solid #e2e8f0;
margin-bottom: 6px;
font-size: 12px;
line-height: 1.3;
min-height: 50px !important;
height: 50px !important;
display: flex !important;
align-items: center;
justify-content: flex-start !important;
text-align: left;
padding-left: 15px;
overflow: hidden !important; /* KEIN Scroll */
border: none !important;
}
/* Linke Box (Prompt) - Blau */
.gr-column:first-child .info-box {
border-left: 4px solid #3b82f6;
background: #eff6ff;
}
/* Rechte Box (Negativ) - Rot */
.gr-column:last-child .info-box {
border-left: 4px solid #ef4444;
background: #fef2f2;
}
/* Code in Info-Boxen */
.info-box code {
background: white;
padding: 3px 3px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
border: 1px solid #e2e8f0;
display: inline-block;
margin: 3px 0;
}
/* ===== TEXTBOXEN ===== */
.prompt-box textarea {
min-height: 90px !important;
border-radius: 6px !important;
border: 2px solid #e2e8f0 !important;
padding: 10px !important;
font-size: 14px !important;
}
/* Focus-State */
.prompt-box textarea:focus {
border-color: #3b82f6 !important;
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1) !important;
}
/* Platzhalter */
.prompt-box textarea::placeholder {
color: #94a3b8 !important;
}
.clickable-file {
color: #1976d2;
cursor: pointer;
text-decoration: none;
font-family: 'Monaco', 'Consolas', monospace;
background: #e3f2fd;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #bbdefb;
}
.clickable-file:hover {
background: #bbdefb;
text-decoration: underline;
}
.model-info-box {
background: #e8f4fd;
padding: 12px;
border-radius: 6px;
margin: 10px 0;
border-left: 4px solid #2196f3;
font-size: 14px;
}
#generate-button {
background-color: #0080FF !important;
border: none !important;
margin: 20px auto !important;
display: block !important;
font-weight: 600;
width: 280px;
}
#generate-button:hover {
background-color: #0066CC !important;
}
.hint-box {
margin-top: 20px;
}
.custom-text {
font-size: 25px !important;
}
.image-upload .svelte-1p4f8co {
display: block !important;
}
.preview-box {
border: 2px dashed #ccc;
padding: 10px;
border-radius: 8px;
margin: 10px 0;
}
.mode-red {
border: 3px solid #ff4444 !important;
}
.mode-green {
border: 3px solid #44ff44 !important;
}
.coordinate-sliders {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
}
.gr-checkbox .wrap .text-gray {
font-size: 14px !important;
font-weight: 600 !important;
line-height: 1.4 !important;
}
.status-message {
padding: 10px;
border-radius: 5px;
margin: 10px 0;
text-align: center;
font-weight: 500;
}
.status-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.radio-group {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border: 2px solid #e9ecef;
}
.radio-item {
padding: 8px 12px;
margin: 5px 0;
border-radius: 4px;
transition: background 0.3s;
}
.radio-item:hover {
background: #e9ecef;
}
.radio-label {
font-weight: 600;
font-size: 14px;
}
.radio-description {
font-size: 12px;
color: #6c757d;
margin-left: 24px;
}
"""
) as demo:
with gr.Column(visible=True) as content_area:
with gr.Tab("Text zu Bild"):
gr.Markdown("## 🎨 Text zu Bild Generator")
with gr.Row():
with gr.Column(scale=2):
# Modellauswahl Dropdown (NUR 2 MODELLE)
model_dropdown = gr.Dropdown(
choices=[
(config["name"], model_id)
for model_id, config in MODEL_CONFIGS.items()
],
value="runwayml/stable-diffusion-v1-5",
label="📁 Modellauswahl",
info="🏠 Universal vs 👤 Portraits"
)
# Modellinformationen Box
model_info_box = gr.Markdown(
value="<div class='model-info-box'>"
"**🏠 Stable Diffusion 1.5 (Universal)**<br>"
"Universal model, good all-rounder, reliable results<br>"
"Empfohlene Einstellungen: 35 Steps, CFG 7.5"
"</div>",
label="Modellinformationen"
)
with gr.Column(scale=3):
txt_input = gr.Textbox(
placeholder="z.B. ultra realistic mountain landscape at sunrise, soft mist over the valley, detailed foliage, crisp textures, depth of field, sunlight rays through clouds, shot on medium format camera, 8k, HDR, hyper-detailed, natural lighting, masterpiece",
lines=3,
label="🎯 Prompt (Englisch)",
info="Beschreibe detailliert, was du sehen möchtest. Negative Prompts werden automatisch generiert."
)
with gr.Row():
with gr.Column():
txt_steps = gr.Slider(
minimum=10, maximum=100, value=35, step=1,
label="⚙️ Inferenz-Schritte",
info="Mehr Schritte = bessere Qualität, aber langsamer (20-50 empfohlen)"
)
with gr.Column():
txt_guidance = gr.Slider(
minimum=1.0, maximum=20.0, value=7.5, step=0.5,
label="🎛️ Prompt-Stärke (CFG Scale)",
info="Wie stark der Prompt befolgt wird (7-12 für gute Balance)"
)
# Status-Nachricht
status_output = gr.Markdown(
value="",
elem_classes="status-message"
)
generate_btn = gr.Button("🚀 Bild generieren", variant="primary", elem_id="generate-button")
with gr.Row():
txt_output = gr.Image(
label="🖼️ Generiertes Bild",
show_download_button=True,
type="pil",
height=400
)
# Event-Handler für Modelländerung
def update_model_info(model_id):
config = MODEL_CONFIGS.get(model_id, MODEL_CONFIGS["runwayml/stable-diffusion-v1-5"])
info_html = f"""
<div class='model-info-box'>
<strong>{config['name']}</strong><br>
{config['description']}<br>
<em>Empfohlene Einstellungen: {config['recommended_steps']} Steps, CFG {config['recommended_cfg']}</em>
</div>
"""
return info_html, config["recommended_steps"], config["recommended_cfg"]
model_dropdown.change(
fn=update_model_info,
inputs=[model_dropdown],
outputs=[model_info_box, txt_steps, txt_guidance]
)
generate_btn.click(
fn=text_to_image,
inputs=[txt_input, model_dropdown, txt_steps, txt_guidance],
outputs=[txt_output, status_output],
concurrency_limit=1
)
with gr.Tab("Bild zu Bild"):
gr.Markdown("## 🖼️ Bild zu Bild Transformation (3 MODI)")
with gr.Row():
with gr.Column():
img_input = gr.Image(
type="pil",
label="📤 Eingabebild",
height=300,
sources=["upload"],
elem_id="image-upload"
)
with gr.Column():
preview_output = gr.Image(
label="🎯 Live-Vorschau mit Maske",
height=300,
interactive=False,
show_download_button=False
)
# ===== NEUE RADIO-BUTTONS STATT CHECKBOX =====
with gr.Row():
with gr.Column():
gr.Markdown("### 🎛️ Transformations-Modus")
# NEU: 3 Radio-Buttons statt 1 Checkbox
mode_radio = gr.Radio(
choices=[
("🌳 Umgebung ändern", "environment_change"),
("🎯 Focus verändern", "focus_change"),
("👤 Ausschließlich Gesicht", "face_only_change")
],
value="environment_change", # Standardmodus
label="Wähle den Transformationsmodus:",
info="Steuert, welcher Teil des Bildes verändert wird",
elem_classes="radio-group"
)
# Detailierte Erklärungen
gr.Markdown("""
<div style="font-size: 12px; color: #666; margin-top: 10px;">
<strong>Modus-Erklärungen:</strong><br>
• <strong>🌳 Umgebung ändern:</strong> Ändert alles AUSSER dem markierten Bereich (Depth+Canny)<br>
• <strong>🎯 Focus verändern:</strong> Ändert markierten Bereich+Körper (OpenPose+Canny)<br>
• <strong>👤 Ausschließlich Gesicht:</strong> Ändert NUR den markierten Bereich (Depth+Canny)
</div>
""")
with gr.Row():
gr.Markdown("### 📐 Bildelementbereich anpassen")
# SLIDER MIT DYNAMISCHEM MAXIMUM (4096 für große Bilder)
with gr.Row():
with gr.Column():
bbox_x1 = gr.Slider(
label="← Links (x1)",
minimum=0, maximum=MAX_IMAGE_SIZE, value=100, step=1,
info="Linke Kante des Bildelementbereichs"
)
with gr.Column():
bbox_y1 = gr.Slider(
label="↑ Oben (y1)",
minimum=0, maximum=MAX_IMAGE_SIZE, value=100, step=1,
info="Obere Kante des Bildelementbereichs"
)
with gr.Row():
with gr.Column():
bbox_x2 = gr.Slider(
label="→ Rechts (x2)",
minimum=0, maximum=MAX_IMAGE_SIZE, value=300, step=1,
info="Rechte Kante des Bildelementbereichs"
)
with gr.Column():
bbox_y2 = gr.Slider(
label="↓ Unten (y2)",
minimum=0, maximum=MAX_IMAGE_SIZE, value=300, step=1,
info="Untere Kante des Bildelementbereichs"
)
with gr.Row():
with gr.Column():
# Info-Block über Positiv-Prompt
pos_info = gr.Markdown(
value="`[STIL-MOTIV], [UMGEBUNG], [VOR/HINTERGRUND], [DETAILS], [QUALITÄT], [BELEUCHTUNG]`",
elem_classes=["info-box"]
)
img_prompt = gr.Textbox(
placeholder="photorealistic coastal beach, keep person unchanged, high detailed, diffused light",
lines=2,
label="🎯 Transformations-Prompt (Englisch)",
#info="Was soll verändert werden? Sei spezifisch."
elem_classes=["prompt-box"]
)
with gr.Column():
# Info_Block über Negativ-Prompt
neg_info = gr.Markdown(
value="`[GESICHTER/ANATOMIE], [FEHLER], [QUALITÄT], [UNERWÜNSCHTES]`",
elem_classes=["info-box"]
)
img_neg_prompt = gr.Textbox(
placeholder="blurry face, deformed anatomy, ugly, extra limbs, poorly drawn hands",
lines=2,
label="🚫 Negativ-Prompt (Englisch)",
#info="Was soll vermieden werden? Unerwünschte Elemente auflisten."
elem_classes=["prompt-box"]
)
with gr.Row():
with gr.Column():
strength_slider = gr.Slider(
minimum=0.1, maximum=0.9, value=0.4, step=0.05,
label="💪 Veränderungs-Stärke (strength)",
info="0.1-0.3: Leichte Anpassungen, 0.4-0.6: Mittlere Veränderungen, 0.7-0.9: Starke Umgestaltung"
)
with gr.Column():
img_steps = gr.Slider(
minimum=10, maximum=45, value=35, step=1,
label="⚙️ Inferenz-Schritte",
info="Anzahl der Verarbeitungsschritte (25-45 für gute Ergebnisse)"
)
with gr.Column():
img_guidance = gr.Slider(
minimum=1.0, maximum=15.0, value=7.5, step=0.5,
label="🎛️ Prompt-Stärke (guidance)",
info="Einfluss des Prompts auf das Ergebnis (6-10 für natürliche Ergebnisse)"
)
with gr.Row():
gr.Markdown(
"### 📋 Hinweise:\n"
"• **🆕 3 Transformations-Modi** für präzise Kontrolle\n"
"• **🆕 Unterstützt Bilder bis 4096×4096 Pixel**\n"
"• **🆕 Automatische Bildelementerkennung** setzt Koordinaten beim Upload\n"
"• **🆕 Live-Vorschau** zeigt farbige Rahmen je nach Modus\n"
"• **🆕 Dynamische Koordinaten-Schieberegler** passen sich an Bildgröße an\n"
"• **ControlNet-Technologie** für konsistente Ergebnisse\n"
"• **Automatische Negative Prompts** für bessere Qualität\n"
"• **KORREKTER COMPOSITING-WORKFLOW** – nur bearbeiteter Bereich wird eingefügt\n"
"• **Ausgabe in Eingabebildgröße"
)
transform_btn = gr.Button("🔄 Bild transformieren", variant="primary")
with gr.Row():
img_output = gr.Image(
label="✨ Transformiertes Bild",
show_download_button=True,
type="pil",
height=400
)
with gr.Row():
sam_raw_mask_output = gr.Image(
label="🔍 SAM-Rohmaske (Vor Nachbearbeitung)",
type="pil",
height=300,
show_download_button=False
)
processed_mask_output = gr.Image(
label="🛠️ Nachbearbeitete Maske (Für Inpainting)",
type="pil",
height=300,
show_download_button=False
)
with gr.Row():
pose_map_output = gr.Image(
label="🎭 Pose/Depth Map",
type="pil",
height=300,
show_download_button=False
)
canny_map_output = gr.Image(
label="📐 Canny Edge Map",
type="pil",
height=300,
show_download_button=False
)
# EVENT-HANDLER FÜR DYNAMISCHE BILDGRÖßEN
img_input.upload(
fn=process_image_upload,
inputs=[img_input],
outputs=[preview_output, bbox_x1, bbox_y1, bbox_x2, bbox_y2]
).then(
fn=update_slider_for_image,
inputs=[img_input],
outputs=[bbox_x1, bbox_y1, bbox_x2, bbox_y2]
)
# NEUE Input-Liste mit mode_radio statt face_preserve
coordinate_inputs = [img_input, bbox_x1, bbox_y1, bbox_x2, bbox_y2, mode_radio]
# Live-Vorschau Updates für alle Steuerelemente
for slider in [bbox_x1, bbox_y1, bbox_x2, bbox_y2]:
slider.release(
fn=update_live_preview,
inputs=coordinate_inputs,
outputs=preview_output
)
# 2. PLATZHALTER-FUNKTION
def update_info(mode):
if mode == "environment_change":
return (
"`[STIL-MOTIV], [UMGEBUNG], [VOR/HINTERGRUND], [DETAILS], [QUALITÄT], [BELEUCHTUNG]`",
"`[GESICHTER/ANATOMIE], [FEHLER], [QUALITÄT], [UNERWÜNSCHTES]`"
)
elif mode == "focus_change":
return (
"`[GESICHTSBESCHREIBUNG], [KLEIDUNG], [POSITION], [DETAILS], [STIL]`",
"`[DEFORMIERT], [UNSCHÄRFE], [ANATOMIEFEHLER], [UNERWÜNSCHTES]`"
)
else: # face_only_change
return (
"`[HAARFARBE], [AUGEN], [GESICHTSAUSDRUCK], [DETAILS], [BELEUCHTUNG]`",
"`[UNREALISTISCH], [ASYMETRISCH], [FEHLER], [UNERWÜNSCHTES]`"
)
# 3. EVENT-HANDLER zur Änderung Textbox-Info- Verbindung Text-Box und Funktion
mode_radio.change(
fn=update_info,
inputs=[mode_radio],
outputs=[pos_info, neg_info]
)
# Mode-Radio-Button ändert auch Live-Vorschau
mode_radio.change(
fn=update_live_preview,
inputs=coordinate_inputs,
outputs=preview_output
)
# NEU: Transform-Button mit mode_radio statt face_preserve
transform_btn.click(
fn=img_to_image,
inputs=[
img_input, img_prompt, img_neg_prompt,
strength_slider, img_steps, img_guidance,
mode_radio, bbox_x1, bbox_y1, bbox_x2, bbox_y2
],
outputs=[img_output, sam_raw_mask_output, processed_mask_output, pose_map_output, canny_map_output],
concurrency_limit=1
)
return demo