import torch from diffusers import StableDiffusionControlNetPipeline, ControlNetModel from controlnet_aux import OpenposeDetector from PIL import Image, ImageFilter, ImageEnhance # NEU: ImageEnhance für Kontrast import random import cv2 import numpy as np import gradio as gr import torch.nn.functional as F from transformers import Sam2Model, Sam2Processor from scipy import ndimage # === CONTROLNET FORTSCHRITTS-CALLBACK (Für Gradio-UI) === class ControlNetProgressCallback: def __init__(self, progress, total_steps): self.progress = progress self.total_steps = total_steps self.current_step = 0 def __call__(self, pipe, step_index, timestep, callback_kwargs): self.current_step = step_index + 1 progress_percentage = self.current_step / self.total_steps if self.progress is not None: self.progress(progress_percentage, desc=f"ControlNet: Schritt {self.current_step}/{self.total_steps}") print(f"ControlNet Fortschritt: {self.current_step}/{self.total_steps} ({progress_percentage:.1%})") return callback_kwargs class ControlNetProcessor: def __init__(self, device="cuda", torch_dtype=torch.float32): self.device = device self.torch_dtype = torch_dtype self.pose_detector = None self.midas_model = None self.midas_transform = None self.sam_processor = None self.sam_model = None self.sam_initialized = False def _lazy_load_sam(self): """Lazy Loading von SAM 2 über 🤗 Transformers API""" if self.sam_initialized: return True try: print("#" * 80) print("# 🔄 LADE SAM 2 (Segment Anything Model 2)") print("#" * 80) model_id = "facebook/sam2-hiera-tiny" print(f"📥 Modell-ID: {model_id}") print(f"📥 Lade Processor...") self.sam_processor = Sam2Processor.from_pretrained(model_id) print(f"📥 Lade Modell...") self.sam_model = Sam2Model.from_pretrained(model_id, torch_dtype=torch.float32).to(self.device) self.sam_model.eval() self.sam_initialized = True print("✅ SAM 2 erfolgreich geladen (via Transformers)") return True except Exception as e: print(f"❌ FEHLER beim Laden von SAM 2: {str(e)[:200]}") self.sam_initialized = True return False def _validate_bbox(self, image, bbox_coords): """Validiert und korrigiert BBox-Koordinaten""" width, height = image.size if isinstance(bbox_coords, (list, tuple)) and len(bbox_coords) == 4: x1, y1, x2, y2 = bbox_coords else: x1, y1, x2, y2 = bbox_coords x1, x2 = min(x1, x2), max(x1, x2) y1, y2 = min(y1, y2), max(y1, y2) x1 = max(0, min(x1, width - 1)) y1 = max(0, min(y1, height - 1)) x2 = max(0, min(x2, width - 1)) y2 = max(0, min(y2, height - 1)) if x2 - x1 < 10 or y2 - y1 < 10: size = min(width, height) * 0.3 x1 = max(0, width/2 - size/2) y1 = max(0, height/2 - size/2) x2 = min(width, width/2 + size/2) y2 = min(height, height/2 + size/2) return int(x1), int(y1), int(x2), int(y2) def _smooth_mask(self, mask_array, blur_radius=3): """Glättet die Maske für bessere Übergänge""" try: if blur_radius > 0: mask_array = cv2.medianBlur(mask_array, blur_radius*2+1) return mask_array except Exception as e: print(f"⚠️ Fehler beim Glätten der Maske: {e}") return mask_array def create_sam_mask(self, image, bbox_coords, mode): """ ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2 Sonderbehandlung für face_only_change: Arbeitet auf Bildausschnitt """ try: print("#" * 80) print("# 🎯 STARTE SAM 2 SEGMENTIERUNG") print("#" * 80) print(f"📐 Eingabebild-Größe: {image.size}") print(f"🎛️ Ausgewählter Modus: {mode}") # 1. SAM2 laden (falls noch nicht geschehen) if not self.sam_initialized: print("📥 SAM 2 ist noch nicht geladen, starte Lazy Loading...") self._lazy_load_sam() if self.sam_model is None or self.sam_processor is None: print("⚠️ SAM 2 Model nicht verfügbar, verwende Fallback") return self._create_rectangular_mask(image, bbox_coords, mode) else: print("✅ SAM 2 Modell ist geladen und bereit") # 2. Validiere BBox x1, y1, x2, y2 = self._validate_bbox(image, bbox_coords) original_bbox = (x1, y1, x2, y2) original_bbox_size = (x2 - x1, y2 - y1) print(f"📏 Original-BBox Größe: {original_bbox_size[0]} × {original_bbox_size[1]} px") # ============================================================ # SPEZIALBEHANDLUNG NUR FÜR face_only_change # ============================================================ if mode == "face_only_change": print("-" * 60) print("👤 SPEZIALMODUS: NUR GESICHT - EMPFOHLENER WORKFLOW") print("-" * 60) # ============================================================ # SCHRITT 1: Originalbild sichern # ============================================================ original_image = image print(f"💾 Originalbild gesichert: {original_image.size}") # ============================================================ # SCHRITT 2: Crop = BBox × 2.0 (einmal, sauber, quadratisch) # ============================================================ print("✂️ SCHRITT 2: ERSTELLE QUADRATISCHEN AUSSCHNITT (BBox × 2.0)") # BBox-Zentrum berechnen bbox_center_x = (x1 + x2) // 2 bbox_center_y = (y1 + y2) // 2 print(f" 📍 BBox-Zentrum: ({bbox_center_x}, {bbox_center_y})") # Größte Dimension der BBox finden bbox_width = x2 - x1 bbox_height = y2 - y1 bbox_max_dim = max(bbox_width, bbox_height) print(f" 📏 BBox Dimensionen: {bbox_width} × {bbox_height} px") print(f" 📐 Maximale BBox-Dimension: {bbox_max_dim} px") # Crop-Größe berechnen (BBox × 2.0) crop_size = int(bbox_max_dim * 2.0) print(f" 🎯 Ziel-Crop-Größe: {crop_size} × {crop_size} px (BBox × 2.0)") # Crop-Koordinaten berechnen (zentriert um BBox) crop_x1 = bbox_center_x - crop_size // 2 crop_y1 = bbox_center_y - crop_size // 2 crop_x2 = crop_x1 + crop_size crop_y2 = crop_y1 + crop_size # Sicherstellen, dass Crop innerhalb der Bildgrenzen bleibt crop_x1 = max(0, crop_x1) crop_y1 = max(0, crop_y1) crop_x2 = min(original_image.width, crop_x2) crop_y2 = min(original_image.height, crop_y2) # Falls Crop zu klein ist, anpassen actual_crop_width = crop_x2 - crop_x1 actual_crop_height = crop_y2 - crop_y1 if actual_crop_width < crop_size or actual_crop_height < crop_size: # An Kanten anpassen if crop_x1 == 0: crop_x2 = min(original_image.width, crop_size) elif crop_x2 == original_image.width: crop_x1 = max(0, original_image.width - crop_size) if crop_y1 == 0: crop_y2 = min(original_image.height, crop_size) elif crop_y2 == original_image.height: crop_y1 = max(0, original_image.height - crop_size) print(f" 🔲 Crop-Bereich: [{crop_x1}, {crop_y1}, {crop_x2}, {crop_y2}]") print(f" 📏 Tatsächliche Crop-Größe: {crop_x2-crop_x1} × {crop_y2-crop_y1} px") # Bild ausschneiden cropped_image = original_image.crop((crop_x1, crop_y1, crop_x2, crop_y2)) print(f" ✅ Quadratischer Ausschnitt erstellt: {cropped_image.size}") # ============================================================ # SCHRITT 3: BBox-Koordinaten im Crop-Koordinatensystem berechnen # ============================================================ print("📐 SCHRITT 3: BBox-KOORDINATEN TRANSFORMIEREN") rel_x1 = x1 - crop_x1 rel_y1 = y1 - crop_y1 rel_x2 = x2 - crop_x1 rel_y2 = y2 - crop_y1 # Sicherstellen, dass BBox innerhalb des Crops liegt rel_x1 = max(0, rel_x1) rel_y1 = max(0, rel_y1) rel_x2 = min(cropped_image.width, rel_x2) rel_y2 = min(cropped_image.height, rel_y2) print(f" 🎯 Relative BBox im Crop: [{rel_x1}, {rel_y1}, {rel_x2}, {rel_y2}]") print(f" 📏 Relative BBox Größe: {rel_x2-rel_x1} × {rel_y2-rel_y1} px") # ============================================================ # SCHRITT 4: Bildkontrast verstärken für bessere Segmentierung # ============================================================ print("🔍 SCHRITT 4: KONTRASTVERSTÄRKUNG FÜR SAM") contrast_enhancer = ImageEnhance.Contrast(cropped_image) enhanced_cropped_image = contrast_enhancer.enhance(1.5) # 50% mehr Kontrast print(f" ✅ Kontrast um 50% erhöht") # Für SAM: Verwende kontrastverstärkten Ausschnitt und relative Koordinaten image = enhanced_cropped_image x1, y1, x2, y2 = rel_x1, rel_y1, rel_x2, rel_y2 print(" 🔄 SAM wird auf kontrastverstärktem Ausschnitt ausgeführt") print(f" 📊 SAM-Eingabegröße: {image.size}") # ============================================================ # GEMEINSAME SAM-LOGIK FÜR ALLE MODI # (arbeitet auf `image` - bei face_only_change ist das der Crop) # ============================================================ print("-" * 60) print(f"📦 BOUNDING BOX DETAILS FÜR SAM:") print(f" Bild-Größe für SAM: {image.size}") print(f" BBox Koordinaten: [{x1}, {y1}, {x2}, {y2}]") print(f" BBox Dimensionen: {x2-x1}px × {y2-y1}px") # 3. Vorbereitung für SAM2 print("-" * 60) print("🖼️ BILDAUFBEREITUNG FÜR SAM 2") image_np = np.array(image.convert("RGB")) input_boxes = [[[x1, y1, x2, y2]]] print(f" Konvertiere Bild zu NumPy Array: {image_np.shape}") print(f" Erstelle Input Boxes: {input_boxes}") # ============================================================ # SCHRITT 4-5: SAM mit Box-Prompt = ursprüngliche BBox # (im Crop-Koordinatensystem bei face_only_change) # ============================================================ print("🎯 SCHRITT 4-5: SAM MIT BOX-PROMPT") print(" Verarbeite Bild mit SAM 2 Processor...") inputs = self.sam_processor( image_np, input_boxes=input_boxes, return_tensors="pt" ).to(self.device) print(f"✅ Processor-Ausgabe: {len(inputs)} Elemente") # 4. SAM2 Vorhersage print("-" * 60) print("🧠 SAM 2 INFERENZ (Vorhersage)") with torch.no_grad(): print(" Führe Vorhersage durch...") outputs = self.sam_model(**inputs) print(f"✅ Vorhersage abgeschlossen") print(f" Anzahl der Vorhersagemasken: {outputs.pred_masks.shape[2]}") # 5. Maske extrahieren und auf Originalgröße skalieren print("📏 SCHRITT 6: MASKE EXTRAHIEREN UND SKALIEREN") # ============================================================ # SCHRITT 6: SAM liefert mehrere Masken # ============================================================ num_masks = outputs.pred_masks.shape[2] print(f" SAM lieferte {num_masks} verschiedene Masken") # Extrahiere alle Masken all_masks = [] mask_qualities = [] for i in range(num_masks): single_mask = outputs.pred_masks[:, :, i, :, :] resized_mask = F.interpolate( single_mask, size=(image.height, image.width), mode='bilinear', align_corners=False ).squeeze() mask_np = resized_mask.sigmoid().cpu().numpy() all_masks.append(mask_np) # Basis-Statistiken für jede Maske mask_binary = (mask_np > 0.5).astype(np.uint8) mask_area = np.sum(mask_binary) print(f" Maske {i+1}: Größe={mask_area:,} Pixel, Max-Konfidenz={mask_np.max():.3f}") # ============================================================ # SCHRITT 6: Maskenauswahl per Heuristik # ============================================================ print("🤔 SCHRITT 6: MASKENAUSWAHL MIT HEURISTIK") # Erwartete BBox für Heuristik (in Pixel-Koordinaten) bbox_center = ((x1 + x2) // 2, (y1 + y2) // 2) bbox_area = (x2 - x1) * (y2 - y1) print(f" Erwartetes BBox-Zentrum: {bbox_center}") print(f" Erwartete BBox-Fläche: {bbox_area:,} Pixel") best_mask_idx = 0 best_score = -1 for i, mask_np in enumerate(all_masks): # Threshold für binäre Maske mask_binary = (mask_np > 0.5).astype(np.uint8) if np.sum(mask_binary) == 0: print(f" ❌ Maske {i+1}: Keine Pixel, überspringe") continue # 1. Größte Überlappung mit BBox # Erstelle binäre BBox-Maske bbox_mask = np.zeros((image.height, image.width), dtype=np.uint8) bbox_mask[y1:y2, x1:x2] = 1 overlap = np.sum(mask_binary & bbox_mask) bbox_overlap_ratio = overlap / np.sum(bbox_mask) if np.sum(bbox_mask) > 0 else 0 # 2. Schwerpunkt nahe BBox-Zentrum y_coords, x_coords = np.where(mask_binary > 0) if len(y_coords) > 0: centroid_y = np.mean(y_coords) centroid_x = np.mean(x_coords) centroid_distance = np.sqrt((centroid_x - bbox_center[0])**2 + (centroid_y - bbox_center[1])**2) normalized_distance = centroid_distance / max(image.width, image.height) else: centroid_distance = float('inf') normalized_distance = 1.0 # 3. Maskenfläche im erwarteten Bereich mask_area = np.sum(mask_binary) area_ratio = mask_area / bbox_area area_score = 1.0 - min(abs(area_ratio - 1.0), 1.0) # 1.0 ist perfekt # 4. SAM-Konfidenz confidence_score = mask_np.max() # Gesamtscore berechnen (Gewichtung anpassbar) score = ( bbox_overlap_ratio * 0.4 + # 40% Überlappung mit BBox (1.0 - normalized_distance) * 0.3 + # 30% Zentrumsnähe area_score * 0.2 + # 20% Flächenübereinstimmung confidence_score * 0.1 # 10% SAM-Konfidenz ) print(f" 📊 Maske {i+1} Scores:") print(f" • BBox-Überlappung: {bbox_overlap_ratio:.3f} ({overlap:,} Pixel)") print(f" • Zentrums-Distanz: {centroid_distance:.1f} px (normalisiert: {normalized_distance:.3f})") print(f" • Flächen-Ratio: {area_ratio:.3f} ({mask_area:,} Pixel)") print(f" • Max-Konfidenz: {confidence_score:.3f}") print(f" • GESAMTSCORE: {score:.3f}") if score > best_score: best_score = score best_mask_idx = i print(f"✅ Beste Maske ausgewählt: Nr. {best_mask_idx+1} mit Score {best_score:.3f}") # Beste Maske verwenden mask_np = all_masks[best_mask_idx] # ============================================================ # DYNAMISCHER THRESHOLD # ============================================================ max_val = mask_np.max() print(f" 🔍 Maximaler SAM-Konfidenzwert der besten Maske: {max_val:.3f}") if max_val < 0.6: dynamic_threshold = 0.2 print(f" ⚠️ SAM ist unsicher (max_val={max_val:.3f} < 0.6)") print(f" 🎯 Verwende festen niedrigen Threshold: {dynamic_threshold:.3f}") else: dynamic_threshold = max_val * 0.8 print(f" ✅ SAM ist sicher (max_val={max_val:.3f} >= 0.6)") print(f" 🎯 Dynamischer Threshold: {dynamic_threshold:.3f} (80% von Maximum)") mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255 unique_vals = np.unique(mask_array) print(f" Nach Threshold ({dynamic_threshold:.3f}): {mask_array.shape}, Unique Werte: {unique_vals}") # ============================================================ # SCHRITT 7: Postprocessing # ============================================================ print("🔧 SCHRITT 7: POSTPROCESSING") # a) Kleine Löcher füllen if np.sum(mask_array > 0) > 0: # Finde alle schwarze Regionen in der weißen Maske (Löcher) mask_inverted = 255 - mask_array labeled_holes, num_holes = ndimage.label(mask_inverted) if num_holes > 1: # 1 ist der Hintergrund print(f" 🔍 Gefundene Löcher: {num_holes - 1}") # Fülle kleine Löcher for i in range(2, num_holes + 1): # Beginne bei 2 (1 ist Hintergrund) hole_size = np.sum(labeled_holes == i) if hole_size < 500: # Kleine Löcher füllen mask_array = np.where(labeled_holes == i, 255, mask_array) print(f" • Loch {i} gefüllt ({hole_size} Pixel)") # b) Kleine Komponenten entfernen labeled_array, num_features = ndimage.label(mask_array) if num_features > 1: print(f" 🧹 Komponenten vor Filterung: {num_features}") sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1)) total_mask_area = np.sum(mask_array > 0) min_size = total_mask_area * 0.1 # 10% der Gesamtfläche print(f" 📊 Gesamtmaskenfläche: {total_mask_area:,} Pixel") print(f" 📏 Minimale Komponentengröße: {min_size:,.0f} Pixel") for i in range(1, num_features + 1): if sizes[i-1] < min_size: mask_array = np.where(labeled_array == i, 0, mask_array) print(f" • Komponente {i} entfernt ({sizes[i-1]:,} Pixel)") # c) Ggf. leichte Erosion/Dilation print(" ⚙️ Leichte morphologische Operationen...") kernel = np.ones((3, 3), np.uint8) # Leichte Erosion für saubere Kanten mask_array = cv2.erode(mask_array, kernel, iterations=1) print(" • Erosion (1 Iteration) angewendet") # Leichte Dilation für glatte Übergänge mask_array = cv2.dilate(mask_array, kernel, iterations=1) print(" • Dilation (1 Iteration) angewendet") # BEIDE MASKEN ERSTELLEN (vor Nachbearbeitung) original_mask_array = mask_array.copy() # Person weiß (255), Hintergrund schwarz (0) inverted_mask_array = 255 - mask_array # Person schwarz (0), Hintergrund weiß (255) print("-" * 60) print(f"🔧 MODUS-SPEZIFISCHE NACHBEARBEITUNG: {mode}") print(f" Original-Maske (Person weiß): {original_mask_array.shape}") print(f" Invertierte Maske (Person schwarz): {inverted_mask_array.shape}") # MODUS-SPEZIFISCHE NACHBEARBEITUNG if mode == "environment_change": print("🌳 MODUS: UMWELT ÄNDERN") # Arbeite auf der INVERTIERTEN Maske (Person schwarz, Hintergrund weiß) mask_array = inverted_mask_array.copy() print(" Arbeite auf invertierter Maske (Person schwarz, Hintergrund weiß)") # Morphologische Operationen für saubere Umgebung kernel = np.ones((5,5), np.uint8) mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel) mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel) mask_array = cv2.dilate(mask_array, np.ones((2,2), np.uint8), iterations=1) mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0) print(" ✅ Umwelt-Modus: Person geschützt, Hintergrund optimiert") elif mode == "focus_change": print("🎯 MODUS: FOCUS ÄNDERN") # Arbeite auf der ORIGINAL-Maske (Person weiß, Hintergrund schwarz) mask_array = original_mask_array.copy() print(" Arbeite auf originaler Maske (Person weiß, Hintergrund schwarz)") # Größte weiße Komponente behalten (Person) labeled_array, num_features = ndimage.label(mask_array) if num_features > 1: sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1)) largest_component = np.argmax(sizes) + 1 mask_array = np.where(labeled_array == largest_component, mask_array, 0) # Maske leicht erweitern für bessere Abdeckung kernel = np.ones((3,3), np.uint8) mask_array = cv2.dilate(mask_array, kernel, iterations=1) mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel) print(" ✅ Focus-Modus: Person verändert, Hintergrund geschützt") elif mode == "face_only_change": print("👤 MODUS: NUR GESICHT ÄNDERN") # Arbeite auf der ORIGINAL-Maske (Person weiß, Hintergrund schwarz) mask_array = original_mask_array.copy() print(" Arbeite auf originaler Maske (Person weiß, Hintergrund schwarz)") # Starke Erosion für präzises Gesicht kernel = np.ones((3,3), np.uint8) mask_array = cv2.erode(mask_array, kernel, iterations=2) mask_array = cv2.erode(mask_array, np.ones((2,2), np.uint8), iterations=1) mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0) print(" ✅ Gesichts-Modus: Postprocessing auf Ausschnitt abgeschlossen") # ============================================================ # SPEZIALSCHRITT: MASKE ZURÜCK AUF ORIGINALGRÖSSE BRINGEN # ============================================================ print("-" * 60) print("🔄 MASKE VOM AUSSCHNITT ZURÜCK AUF ORIGINALGRÖSSE") # Temporäre Maske aus dem Array erstellen temp_mask = Image.fromarray(mask_array).convert("L") print(f" Maskengröße auf Ausschnitt: {temp_mask.size}") # Leere Maske in Originalbild-Größe erstellen final_mask = Image.new("L", original_image.size, 0) print(f" Leere Maske in Originalgröße: {final_mask.size}") # Die segmentierte Maske an der richtigen Position im Originalbild platzieren final_mask.paste(temp_mask, (crop_x1, crop_y1)) print(f" Maskenposition im Original: ({crop_x1}, {crop_y1})") # Zurück zum mask_array konvertieren mask_array = np.array(final_mask) print(f" ✅ Maske zurück auf Originalgröße skaliert: {mask_array.shape}") # Originalbild wiederherstellen für eventuelle spätere Verwendung image = original_image print(f" 🔄 Bild-Referenz wieder auf Original gesetzt: {image.size}") # 9. Qualitätskontrolle und Statistik white_pixels = np.sum(mask_array > 127) total_pixels = mask_array.size white_ratio = white_pixels / total_pixels * 100 black_pixels = total_pixels - white_pixels black_ratio = 100 - white_ratio print("-" * 60) print("📊 MASKEN-STATISTIK (FINAL)") print(f" Weiße Pixel (Veränderungsbereich): {white_pixels:,} ({white_ratio:.1f}%)") print(f" Schwarze Pixel (Erhaltungsbereich): {black_pixels:,} ({black_ratio:.1f}%)") print(f" Gesamtpixel: {total_pixels:,}") if mode == "face_only_change": # Zusätzliche Statistik für Gesichtsmodus original_face_area = original_bbox_size[0] * original_bbox_size[1] coverage_ratio = white_pixels / original_face_area if original_face_area > 0 else 0 print(f" 👤 Gesichtsabdeckung: {coverage_ratio:.1%} der ursprünglichen BBox") # 10. Zurück zu PIL Image mask = Image.fromarray(mask_array).convert("L") print("#" * 80) print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN") print(f"📐 Finale Maskengröße: {mask.size}") print(f"🎛️ Verwendeter Modus: {mode}") print(f"👤 Bei face_only_change: Crop={crop_size}×{crop_size}px, Heuristik-Score={best_score:.3f}") print("#" * 80) return mask except Exception as e: print("❌" * 40) print("❌ FEHLER IN SAM 2 SEGMENTIERUNG") print("❌" * 40) print(f"Fehler: {str(e)[:200]}") import traceback traceback.print_exc() print("ℹ️ Fallback auf rechteckige Maske") return self._create_rectangular_mask(image, bbox_coords, mode) def _create_rectangular_mask(self, image, bbox_coords, mode): """Fallback: Erstellt rechteckige Maske""" print("#" * 80) print("# ⚠️ FALLBACK: ERSTELLE RECHTECKIGE MASKE") print("#" * 80) from PIL import ImageDraw mask = Image.new("L", image.size, 0) print(f"📐 Erstelle leere Maske: {mask.size}") if bbox_coords and all(coord is not None for coord in bbox_coords): x1, y1, x2, y2 = self._validate_bbox(image, bbox_coords) draw = ImageDraw.Draw(mask) if mode == "environment_change": draw.rectangle([0, 0, image.size[0], image.size[1]], fill=255) draw.rectangle([x1, y1, x2, y2], fill=0) print(f" Modus: Umgebung ändern - BBox geschützt: [{x1}, {y1}, {x2}, {y2}]") else: draw.rectangle([x1, y1, x2, y2], fill=255) print(f" Modus: Focus/Gesicht ändern - BBox verändert: [{x1}, {y1}, {x2}, {y2}]") print("✅ Rechteckige Maske erstellt") return mask def load_pose_detector(self): """Lädt nur den Pose-Detector""" if self.pose_detector is None: print("#" * 80) print("# 📥 LADE POSE DETECTOR") print("#" * 80) try: self.pose_detector = OpenposeDetector.from_pretrained("lllyasviel/ControlNet") print("✅ Pose-Detector geladen") except Exception as e: print(f"⚠️ Pose-Detector konnte nicht geladen werden: {e}") return self.pose_detector def load_midas_model(self): """Lädt MiDaS Model für Depth Maps""" if self.midas_model is None: print("#" * 80) print("# 📥 LADE MIDAS MODELL FÜR DEPTH MAPS") print("#" * 80) try: import torchvision.transforms as T self.midas_model = torch.hub.load( "intel-isl/MiDaS", "DPT_Hybrid", trust_repo=True ) self.midas_model.to(self.device) self.midas_model.eval() self.midas_transform = T.Compose([ T.Resize(384), T.ToTensor(), T.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), ]) print("✅ MiDaS Modell erfolgreich geladen") except Exception as e: print(f"❌ MiDaS konnte nicht geladen werden: {e}") print("ℹ️ Verwende Fallback-Methode") self.midas_model = None return self.midas_model def extract_pose_simple(self, image): """Einfache Pose-Extraktion ohne komplexe Abhängigkeiten""" print("#" * 80) print("# ⚠️ ERSTELLE EINFACHE POSE-MAP (FALLBACK)") print("#" * 80) try: img_array = np.array(image.convert("RGB")) edges = cv2.Canny(img_array, 100, 200) pose_image = Image.fromarray(edges).convert("RGB") print("⚠️ Verwende Kanten-basierte Pose-Approximation") return pose_image except Exception as e: print(f"Fehler bei einfacher Pose-Extraktion: {e}") return image.convert("RGB").resize((512, 512)) def extract_pose(self, image): """Extrahiert Pose-Map aus Bild mit Fallback""" print("#" * 80) print("# 🕺 ERSTELLE POSE-MAP") print("#" * 80) try: detector = self.load_pose_detector() if detector is None: print("⚠️ Kein Pose-Detector verfügbar, verwende Fallback") return self.extract_pose_simple(image) print(" Extrahiere Pose mit OpenPose...") pose_image = detector(image, hand_and_face=True) print("✅ Pose-Map erfolgreich erstellt") return pose_image except Exception as e: print(f"Fehler bei Pose-Extraktion: {e}") return self.extract_pose_simple(image) def extract_canny_edges(self, image): """Extrahiert Canny Edges für Umgebungserhaltung""" print("#" * 80) print("# 🎨 ERSTELLE CANNY EDGE MAP") print("#" * 80) try: img_array = np.array(image.convert("RGB")) gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) edges = cv2.Canny(gray, 100, 200) edges_rgb = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB) edges_image = Image.fromarray(edges_rgb) print("✅ Canny Edge Map erstellt") return edges_image except Exception as e: print(f"Fehler bei Canny Edge Extraction: {e}") return image.convert("RGB").resize((512, 512)) def extract_depth_map(self, image): """ Extrahiert Depth Map mit MiDaS (Fallback auf Filter) """ print("#" * 80) print("# 🏔️ ERSTELLE DEPTH MAP") print("#" * 80) try: midas = self.load_midas_model() if midas is not None: print("🎯 Verwende MiDaS für Depth Map...") import torchvision.transforms as T img_transformed = self.midas_transform(image).unsqueeze(0).to(self.device) with torch.no_grad(): print(" Führe MiDaS Inferenz durch...") prediction = midas(img_transformed) prediction = torch.nn.functional.interpolate( prediction.unsqueeze(1), size=image.size[::-1], mode="bicubic", align_corners=False, ).squeeze() depth_np = prediction.cpu().numpy() depth_min, depth_max = depth_np.min(), depth_np.max() print(f" Tiefenwerte: Min={depth_min:.3f}, Max={depth_max:.3f}") if depth_max > depth_min: depth_np = (depth_np - depth_min) / (depth_max - depth_min) depth_np = (depth_np * 255).astype(np.uint8) depth_image = Image.fromarray(depth_np).convert("RGB") print("✅ MiDaS Depth Map erfolgreich erstellt") return depth_image else: raise Exception("MiDaS nicht geladen") except Exception as e: print(f"⚠️ MiDaS Fehler: {e}. Verwende Fallback...") try: img_array = np.array(image.convert("RGB")) gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) depth_map = cv2.GaussianBlur(gray, (5, 5), 0) depth_rgb = cv2.cvtColor(depth_map, cv2.COLOR_GRAY2RGB) depth_image = Image.fromarray(depth_rgb) print("✅ Fallback Depth Map erstellt") return depth_image except Exception as fallback_error: print(f"❌ Auch Fallback fehlgeschlagen: {fallback_error}") return image.convert("RGB").resize((512, 512)) def prepare_controlnet_maps(self, image, keep_environment=False): """ ERSTELLT NUR CONDITIONING-MAPS, generiert KEIN Bild. """ print("#" * 80) print("# 🎯 STARTE CONTROLNET CONDITIONING-MAP ERSTELLUNG") print("#" * 80) print(f"📐 Eingabebild-Größe: {image.size}") print(f"🎛️ Modus: {'Depth + Canny' if keep_environment else 'OpenPose + Canny'}") if keep_environment: print(" Modus: Depth + Canny") print(" Schritt 1/2: Extrahiere Depth Map...") depth_map = self.extract_depth_map(image) print(" Schritt 2/2: Extrahiere Canny Edges...") canny_map = self.extract_canny_edges(image) conditioning_images = [depth_map, canny_map] else: print(" Modus: OpenPose + Canny") print(" Schritt 1/2: Extrahiere Pose Map...") pose_map = self.extract_pose(image) print(" Schritt 2/2: Extrahiere Canny Edges...") canny_map = self.extract_canny_edges(image) conditioning_images = [pose_map, canny_map] print("-" * 60) print(f"✅ {len(conditioning_images)} CONDITIONING-MAPS ERSTELLT") for i, img in enumerate(conditioning_images): print(f" Map {i+1}: {img.size}, Modus: {img.mode}") print("#" * 80) return conditioning_images # Globale Instanz device = "cuda" if torch.cuda.is_available() else "cpu" torch_dtype = torch.float16 if device == "cuda" else torch.float32 controlnet_processor = ControlNetProcessor(device=device, torch_dtype=torch_dtype)