Stable-ControlNet-GPU / controlnet_module.py
Astridkraft's picture
Update controlnet_module.py
9a70785 verified
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
from skimage import measure, morphology
# === 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 create_sam_mask(self, image, bbox_coords, mode):
"""
ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2
"""
try:
print("#" * 80)
print("# 🎯 STARTE SAM 2 SEGMENTIERUNG")
print("#" * 80)
print(f"📐 Eingabebild-Größe: {image.size}")
print(f"🎛️ Ausgewählter Modus: {mode}")
# ============================================================
# VORBEREITUNG FÜR ALLE MODI
# ============================================================
original_image = image
# 1. SAM2 laden
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)
# 2. Validiere BBox
x1, y1, x2, y2 = self._validate_bbox(image, bbox_coords)
original_bbox = (x1, y1, x2, y2)
print(f"📏 Original-BBox Größe: {x2-x1} × {y2-y1} px")
# ============================================================
# BLOCK 1: ENVIRONMENT_CHANGE
# ============================================================
if mode == "environment_change":
print("-" * 60)
print("🌳 MODUS: ENVIRONMENT_CHANGE")
print("-" * 60)
# Der Prozessor von SAM erwartet ein NumPy-Array kein PIL
image_np = np.array(image.convert("RGB"))
# Packt die BBox-Koordinaten in eine 3D-Liste
input_boxes = [[[x1, y1, x2, y2]]]
# Aufruf des SAM-Prozessors mit Originalbild in Form NumPy-Array und BBox.Der Processor verarbeitet Bild und BBox
# in die für SAM erforderlichen Tensoren und speichert sie in inputs.
inputs = self.sam_processor(
image_np,
input_boxes=input_boxes,
return_tensors="pt"
).to(self.device) # Ohne .to(self.device) werden die Tensoren standardmäßig im CPU-RAM erzeugt und gespeichert! Da GPU-Fehler!
print(f" - 'input_boxes' Shape: {inputs['input_boxes'].shape}")
# SAM2 Vorhersage
print("-" * 60)
print("🧠 SAM 2 INFERENZ (Vorhersage)")
with torch.no_grad():
print(" Führe Vorhersage durch...")
outputs = self.sam_model(**inputs) #führt die Segmentierung mit SAM aus
print(f"✅ Vorhersage abgeschlossen")
print(f" Anzahl der Vorhersagemasken: {outputs.pred_masks.shape[2]}")
num_masks = outputs.pred_masks.shape[2]
print(f" SAM lieferte {num_masks} verschiedene Masken")
# Sammlung aller Masken in all_masks
all_masks = []
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() #wandelt Modellausgaben in Wahrscheinlichkeiten und bewegt Daten von GPU nach CPU
all_masks.append(mask_np) #fügt die aktuelle Maske der Liste all_masks hinzu
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")
print("🤔 HEURISTIK: Beste Maske auswählen")
best_mask_idx = 0
best_score = -1
# Alle 3 Masken analysieren (OHNE sie alle zu skalieren!)
for i in range(num_masks):
mask_np_temp = all_masks[i] #verwende Maske auf Original-Bildgröße
# Adaptive Vor-Filterung (prüft ob Maske überhaupt gültig ist)
mask_max = mask_np_temp.max()
if mask_max < 0.3:
continue # Maske überspringen
adaptive_threshold = max(0.3, mask_max * 0.7)
mask_binary = (mask_np_temp > adaptive_threshold).astype(np.uint8)
# wenn nur schwarze Pixel (keine Segmentierung) nimm die nächste Maske
if np.sum(mask_binary) == 0:
print(f" ❌ Maske {i+1}: Keine Pixel nach adaptive_threshold {adaptive_threshold:.3f}")
continue
# Heuristik-Berechnung
mask_area_pixels = np.sum(mask_binary)
#Berechnung von Überlappung SAM-Maske und ursprünglicher BBox
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
# Schwerpunkt berechnen
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:
normalized_distance = 1.0
# Flächen-Ratio
area_ratio = mask_area_pixels / bbox_area
area_score = 1.0 - min(abs(area_ratio - 1.0), 1.0)
# Konfidenz
confidence_score = mask_max
# Standard-Score
score = (
bbox_overlap_ratio * 0.4 +
(1.0 - normalized_distance) * 0.25 +
area_score * 0.25 +
confidence_score * 0.1
)
print(f" 📊 STANDARD-SCORES für Maske {i+1}:")
print(f" • BBox-Überlappung: {bbox_overlap_ratio:.3f}")
print(f" • Zentrums-Distanz: {centroid_distance if 'centroid_distance' in locals() else 'N/A'}")
print(f" • Flächen-Ratio: {area_ratio:.3f}")
print(f" • GESAMTSCORE: {score:.3f}")
if score > best_score:
best_score = score
best_mask_idx = i
print(f" 🏆 Neue beste Maske: Nr. {i+1} mit Score {score:.3f}")
print(f"✅ Beste Maske ausgewählt: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
# Beste Maske verwenden - mask_np beste Maske
mask_np = all_masks[best_mask_idx]
max_val = mask_np.max()
print(f" 🔍 Maximaler SAM-Konfidenzwert der besten Maske: {max_val:.3f}")
if max_val < 0.6:
dynamic_threshold = 0.3
print(f" ⚠️ SAM ist unsicher (max_val={max_val:.3f} < 0.6)")
else:
dynamic_threshold = max_val * 0.85
print(f" ✅ SAM ist sicher (max_val={max_val:.3f} >= 0.6)")
# Binärmaske erstellen (256x256)
mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255
# Fallback bei leerer Maske, der höchste Wert ist 0 also schwarz
if mask_array.max() == 0:
print(" ⚠️ Maske leer, erstelle rechteckige Fallback-Maske")
mask_array = np.zeros((512, 512), dtype=np.uint8) * 255 # weiße 512x512-Maske
# Skaliere BBox auf 512x512
scale_x = 512 / image.width
scale_y = 512 / image.height
fb_x1 = int(x1 * scale_x)
fb_y1 = int(y1 * scale_y)
fb_x2 = int(x2 * scale_x)
fb_y2 = int(y2 * scale_y)
# Schwarzes Rechteck für Person bzw. BBox
cv2.rectangle(mask_array, (fb_x1, fb_y1), (fb_x2, fb_y2), 0, -1)
# Damit wird die Rohmaske für die UI-Anzeige gespeichert
raw_mask_array = mask_array.copy()
##########################################################
# POSTPROCESSING
##########################################################
print("🌳 ENVIRONMENT-CHANGE POSTPROCESSING")
# Konvertierung zu PIL, hochskalieren auf Originalgröße (korrekte Überlagerung mit O-Bild),
# Konvertierung NumPy für weitere Verarbeitung da mathematisch korrekter als PIL.
if image.size != original_image.size: #Vergleich SAM-Maskengröße und Original-Bildgröße
print(f" ⚠️ Bildgröße angepasst: {image.size}{original_image.size}")
temp_mask = Image.fromarray(mask_array).convert("L") #wandelt NumPy-Array in PIL-Bild
temp_mask = temp_mask.resize(original_image.size, Image.Resampling.NEAREST) #skaliert auf Originalgröße
mask_array = np.array(temp_mask) #np. heißt mache aus PIL-Image wieder numPy-Array
print(f" ✅ Maske auf Originalgröße skaliert: {mask_array.shape}")
working_mask = mask_array.copy() # Person = Weiß, Hintergrund = Schwarz - working_mask muß vor Nutzung definiert werden!
print(f"working_mask shape: {working_mask.shape}")
# DILATE auf der weißen Person - daduch wird Person etwas vergrößert
kernel_dilate = np.ones((5, 5), np.uint8)
working_mask = cv2.dilate(working_mask, kernel_dilate, iterations=1)
print(f" ✅ Dilate (5x5) - Person leicht erweitert")
# MORPH_CLOSE auf dem schwarzen Hintergrund (feine Löcher)- kleiner Kernel filigrane Heranarbeitung an Person,
# es werden aber auch nur kleine Löcher in Umgebung von weiß nach schwarz geändert!
kernel_close_small = np.ones((3, 3), np.uint8)
working_mask = cv2.morphologyEx(working_mask, cv2.MORPH_CLOSE, kernel_close_small, iterations=1)
print(f" ✅ MORPH_CLOSE (3x3) - Feine Löcher im Hintergrund geschlossen")
# KONTURENFILTER auf der weißen Person - arbeitet filigraner als MORPH-CLOSE
# Finde Konturen (nur äußere)
contours, _ = cv2.findContours(working_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
# Finde die größte Kontur (sollte die Person sein)
largest_contour = max(contours, key=cv2.contourArea)
# Erstelle eine saubere Maske mit nur der größten Kontur
clean_mask = np.zeros_like(working_mask)
cv2.drawContours(clean_mask, [largest_contour], -1, 255, -1)
# Optional: Kleine weiße Punkte IN der Person entfernen
# Dazu invertieren wir temporär, um "Löcher" (schwarze Pixel) in der Person zu finden
temp_inverted = 255 - clean_mask
hole_contours, _ = cv2.findContours(temp_inverted, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
for hole in hole_contours:
area = cv2.contourArea(hole)
if area < 100: # Sehr kleine Löcher füllen
cv2.drawContours(clean_mask, [hole], -1, 255, -1)
working_mask = clean_mask
print(f" ✅ Konturenfilter - Größte Kontur behalten, {len(contours)-1} kleine entfernt")
# Gaussian-BLUR für weiche Kanten
working_mask = cv2.GaussianBlur(working_mask, (5, 5), 1.2)
print(f" ✅ Gaussian Blur (5x5, sigma=1.2) für weiche Kanten")
# GAMMA-Korrektur für präzisere Ränder
working_mask_float = working_mask.astype(np.float32) / 255.0
working_mask_float = np.clip(working_mask_float, 0.0, 1.0)
working_mask_float = working_mask_float ** 0.85 # Gamma 0.85
working_mask = (working_mask_float * 255).astype(np.uint8)
print(f" ✅ Gamma-Korrektur (0.85) gegen milchige Ränder")
# Erst binäre Maske erzeugen und dann invertieren
binary_mask = (working_mask > 128).astype(np.uint8) * 255
final_mask = 255 - binary_mask
print(f" ✅ Finale Invertierung für environment_change")
# Qualitätskontrolle - Debug
white_pixels = np.sum(final_mask > 127)
black_pixels = np.sum(final_mask <= 127)
total_pixels = final_mask.size
print(f" 📊 FINALE MASKE:")
print(f" • Weiße Pixel (Hintergrund): {white_pixels:,} ({white_pixels/total_pixels*100:.1f}%)")
print(f" • Schwarze Pixel (Person): {black_pixels:,} ({black_pixels/total_pixels*100:.1f}%)")
# Zurück zu PIL Image
mask = Image.fromarray(final_mask).convert("L")
raw_mask = Image.fromarray(raw_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("#" * 80)
return mask, raw_mask
# in mask steht die invertierte nachbearbeitete Maske, in raw_mask die Rohmaske.Wichtig: mask (SAM-Maske)
# muß immer in Originalgröße zurück!
# ============================================================
# BLOCK 2: FOCUS_CHANGE
# ============================================================
elif mode == "focus_change":
print("-" * 60)
print("🎯 MODUS: FOCUS_CHANGE (OPTIMIERT)")
print("-" * 60)
# Originalgröße speichern
# original_size = image.size
# print(f"💾 Originalgröße gespeichert: {original_size}")
# Konvertierung O-Bild in NumPy-Array für SAM
image_np = np.array(image.convert("RGB"))
# Packt die BBox-Koordinaten in eine 3D-Liste
input_boxes = [[[x1, y1, x2, y2]]]
# Nur Mittelpunkt als positiver Prompt
center_x = (x1 + x2) // 2
center_y = (y1 + y2) // 2
input_points = [[[[center_x, center_y]]]] # NUR EIN PUNKT in 4D-Liste
input_labels = [[[1]]] # Markiert Punkt als Positiver Prompt also der Bereich muß segmentiert werden
print(f" 🎯 SAM-Prompt: BBox [{x1},{y1},{x2},{y2}]")
print(f" 👁️ Punkt: Nur Mitte ({center_x},{center_y})")
# SAM Inputs vorbereiten
inputs = self.sam_processor(
image_np,
input_boxes=input_boxes,
input_points=input_points,
input_labels=input_labels,
return_tensors="pt"
).to(self.device)
# SAM Vorhersage (alle 3 Masken)
print("🧠 SAM 2 INFERENZ (3 Masken-Varianten)")
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]}")
num_masks = outputs.pred_masks.shape[2]
# Sammlung aller Masken in all_masks
all_masks = []
for i in range(num_masks):
single_mask = outputs.pred_masks[:, :, i, :, :]
# Interpolation auf Originalgröße
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) #fügt die aktuelle Maske der Liste all_masks hinzu
# BBox-Information für Heuristik
bbox_center = ((x1 + x2) // 2, (y1 + y2) // 2)
bbox_area = (x2 - x1) * (y2 - y1)
print("🤔 HEURISTIK: Beste Maske auswählen")
best_mask_idx = 0
best_score = -1
# Alle 3 Masken analysieren
for i in range(num_masks):
# Maske in Original-Bildgröße -vorher interpolate- analysieren
mask_np_temp = all_masks[i]
# Adaptive Vor-Filterung (prüft ob Maske überhaupt gültig ist)
mask_max = mask_np_temp.max()
if mask_max < 0.3:
continue # Maske überspringen
adaptive_threshold = max(0.3, mask_max * 0.7)
mask_binary = (mask_np_temp > adaptive_threshold).astype(np.uint8)
# wenn nur schwarze Pixel (keine Segmentierung) nimm die nächste Maske
if np.sum(mask_binary) == 0:
continue
# Heuristik-Berechnung
mask_area_pixels = np.sum(mask_binary) # zählt alle weißen Pixel in der Binärmaske
# Berechnet wie gut die SAM-Maske mit der ursprünglichen BBox überlappt
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
# Schwerpunkt
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:
normalized_distance = 1.0
# Flächen-Ratio
area_ratio = mask_area_pixels / bbox_area
area_score = 1.0 - min(abs(area_ratio - 1.0), 1.0)
# FOCUS_CHANGE spezifischer Score
score = (
bbox_overlap_ratio * 0.4 + # 40% BBox-Überlappung
(1.0 - normalized_distance) * 0.25 + # 25% Zentrumsnähe
area_score * 0.25 + # 25% Flächenpassung
mask_max * 0.1 # 10% SAM-Konfidenz
)
print(f" Maske {i+1}: Score={score:.3f}, "
f"Überlappung={bbox_overlap_ratio:.3f}, "
f"Fläche={mask_area_pixels:,}px")
if score > best_score:
best_score = score
best_mask_idx = i
print(f"✅ Beste Maske: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
best_mask_original = all_masks[best_mask_idx]
mask_np = best_mask_original
print(f" ✅ Beste Maske in Originalgröße: {image.width}×{image.height}")
# ============================================================
# DYNAMISCHER THRESHOLD
# SAM gibt nur Wahrscheinlichkeiten aus!
# Nachdem das Modell eine Maske für eine Person vorhersagt (wo jeder Pixel einen Wert zwischen 0 und 1 hat,
# wie "wahrscheinlich gehört dieser Pixel zur Person"), wird diese Maske binarisiert (0 oder 1), indem alle
# Pixel unter 0.05 auf 0 gesetzt werden, alle darüber auf 1.
# ============================================================
mask_max = mask_np.max() #höchster Wahrscheinlichkeitswert in SAM-Maske
if best_score < 0.6: # Schlechte Maskenqualität
dynamic_threshold = 0.15 # SEHR NIEDRIG für maximale Abdeckung
print(f" ⚠️ Masken-Score niedrig ({best_score:.3f}). "
f"Threshold=0.15 für bessere Präzision")
elif best_score < 0.8:
dynamic_threshold = max(0.25, mask_max * 0.5) # Vorher 0.15/0.3 - JETZT 0.25/0.5
print(f" ℹ️ Mittlere Maskenqualität. Threshold={dynamic_threshold:.3f}")
else:
dynamic_threshold = max(0.35, mask_max * 0.7) # sehr hoher Threshold für gute Masken
print(f" ✅ Excellente Maske. Threshold={dynamic_threshold:.3f}")
# Binärmaske erstellen
mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255
# Fallback bei leerer Maske, der höchste Wert ist 0 also schwarz
if mask_array.max() == 0:
print(" ⚠️ Maske leer, erstelle rechteckige Fallback-Maske")
mask_array = np.zeros((image.height, image.width), dtype=np.uint8)
# BBox auf 512x512 skalieren für Fallback
cv2.rectangle(mask_array, (x1, y1), (x2, y2), 255, -1) #weiße Box
# Damit wird die Rohmaske für die UI-Anzeige gespeichert
raw_mask_array = mask_array.copy()
print(f"🔧 FOCUS_CHANGE POSTPROCESSING (auf {image.width}×{image.height})")
###################################################
# POSTPROCESSING (Originalgröße)
###################################################
print("🔧 FOCUS_CHANGE POSTPROCESSING (Originalgröße)")
print(f" mask_array - Min/Max: {mask_array.min()}/{mask_array.max()}")
print(f" mask_array - Weiße Pixel: {np.sum(mask_array > 0)}")
print(f" mask_array - Shape: {mask_array.shape}")
print(f" mask_array - dtype: {mask_array.dtype}")
# 1. Findet und behält nur die größte zusammenhängende Komponente der Maske
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)
print(f" ✅ Größte Komponente behalten ({num_features}→1)")
# 2. Morphologische Operationen
kernel_close = np.ones((5, 5), np.uint8)
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel_close, iterations=2)
kernel_dilate = np.ones((15, 15), np.uint8)
mask_array = cv2.dilate(mask_array, kernel_dilate, iterations=1)
# 3. Weiche Übergänge mittlerer Blur für natürliche Übergänge
mask_array = cv2.GaussianBlur(mask_array, (9, 9), 2.0)
# 4. Gamma-Korrektur
mask_array_float = mask_array.astype(np.float32) / 255.0
mask_array_float = np.clip(mask_array_float, 0.0, 1.0)
mask_array_float = mask_array_float ** 0.85
mask_array = (mask_array_float * 255).astype(np.uint8)
# Konvertierung von NumPy-Array auf PIL-Image
mask_original = Image.fromarray(mask_array).convert("L")
raw_mask = Image.fromarray(raw_mask_array).convert("L")
# Finale Maske für ControlNet
mask = mask_original
print(f"✅ FOCUS_CHANGE Maske erstellt: {mask.size}")
return mask, raw_mask
#Wichtig: mask (SAM-Maske) muß in Originalgröße zurück sonst Probleme in Funktion create_mask_and_sam_together
#Und raw_mask auch in Originalgröße sonst Anzeige-Problem UI
# ============================================================
# BLOCK 3: FACE_ONLY_CHANGE
# ============================================================
elif mode == "face_only_change":
print("-" * 60)
print("👤 SPEZIALMODUS: NUR GESICHT - ROBUSTER WORKFLOW")
print("-" * 60)
# ============================================================
# Originalbild sichern
# Andere Vorgehensweise da SAM bei kleinen Köpfen sonst keine Chance hat!
# Bild ausschneiden auf eine vergrößerte quadratische Box - Crops
# ============================================================
original_image = image
print(f"💾 Originalbild gesichert: {original_image.size}")
original_bbox = (x1, y1, x2, y2) # <-- DAS FEHLT
print(f"💾 Original-BBox gespeichert: {original_bbox}")
# ============================================================
# Crop = BBox × 2.5 (ERHÖHT für mehr Kontext)
# ============================================================
print("✂️ SCHRITT 2: ERSTELLE QUADRATISCHEN AUSSCHNITT (BBox × 2.5)")
# 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.5)
crop_size = int(bbox_max_dim * 2.5)
print(f" 🎯 Ziel-Crop-Größe: {crop_size} × {crop_size} px (BBox × 2.5)")
# 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)
# ITERATIVE ANPASSUNG für bessere Crop-Größe
max_iterations = 3
print(f" 🔄 Iterative Crop-Anpassung (max. {max_iterations} Versuche)")
for iteration in range(max_iterations):
actual_crop_width = crop_x2 - crop_x1
actual_crop_height = crop_y2 - crop_y1
# Prüfen ob Crop groß genug ist
if actual_crop_width >= crop_size and actual_crop_height >= crop_size:
print(f" ✅ Crop-Größe OK nach {iteration} Iteration(en): {actual_crop_width}×{actual_crop_height} px")
break
print(f" 🔄 Iteration {iteration+1}: Crop zu klein ({actual_crop_width}×{actual_crop_height})")
# BREITE anpassen (falls nötig)
if actual_crop_width < crop_size:
if crop_x1 == 0: # Am linken Rand
crop_x2 = min(original_image.width, crop_x1 + crop_size)
print(f" ← Breite angepasst (linker Rand): crop_x2 = {crop_x2}")
elif crop_x2 == original_image.width: # Am rechten Rand
crop_x1 = max(0, crop_x2 - crop_size)
print(f" → Breite angepasst (rechter Rand): crop_x1 = {crop_x1}")
else:
# Nicht am Rand - zentriert erweitern
missing_width = crop_size - actual_crop_width
expand_left = missing_width // 2
expand_right = missing_width - expand_left
crop_x1 = max(0, crop_x1 - expand_left)
crop_x2 = min(original_image.width, crop_x2 + expand_right)
print(f" ↔ Zentriert erweitert um {missing_width}px")
# HÖHE anpassen (falls nötig)
if actual_crop_height < crop_size:
if crop_y1 == 0: # Am oberen Rand
crop_y2 = min(original_image.height, crop_y1 + crop_size)
print(f" ↑ Höhe angepasst (oberer Rand): crop_y2 = {crop_y2}")
elif crop_y2 == original_image.height: # Am unteren Rand
crop_y1 = max(0, crop_y2 - crop_size)
print(f" ↓ Höhe angepasst (unterer Rand): crop_y1 = {crop_y1}")
else:
# Nicht am Rand - zentriert erweitern
missing_height = crop_size - actual_crop_height
expand_top = missing_height // 2
expand_bottom = missing_height - expand_top
crop_y1 = max(0, crop_y1 - expand_top)
crop_y2 = min(original_image.height, crop_y2 + expand_bottom)
print(f" ↕ Zentriert erweitert um {missing_height}px")
# Sicherstellen, dass innerhalb der Bildgrenzen
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)
# Letzte Iteration erreicht?
if iteration == max_iterations - 1:
actual_crop_width = crop_x2 - crop_x1
actual_crop_height = crop_y2 - crop_y1
print(f" ⚠️ Max. Iterationen erreicht. Finaler Crop: {actual_crop_width}×{actual_crop_height} px")
# Warnung wenn immer noch zu klein
if actual_crop_width < crop_size or actual_crop_height < crop_size:
min_acceptable = int(bbox_max_dim * 1.8) # Mindestens 1.8× BBox
if actual_crop_width < min_acceptable or actual_crop_height < min_acceptable:
print(f" 🚨 KRITISCH: Crop immer noch zu klein ({actual_crop_width}×{actual_crop_height})")
print(f" 🚨 SAM könnte Probleme haben!")
print(f" 🔲 Finaler Crop-Bereich: [{crop_x1}, {crop_y1}, {crop_x2}, {crop_y2}]")
print(f" 📏 Finale Crop-Größe: {crop_x2-crop_x1} × {crop_y2-crop_y1} px")
# Bild ausschneiden- 2,5 mal so groß und quadratisch wie BBox
cropped_image = original_image.crop((crop_x1, crop_y1, crop_x2, crop_y2))
print(f" ✅ Quadratischer Ausschnitt erstellt: {cropped_image.size}")
# ============================================================
# BBox-Koordinaten transformieren
# ============================================================
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")
# ============================================================
# INTENSIVE BILDAUFBEREITUNG FÜR GESICHTSERKENNUNG
# ============================================================
print("🔍 SCHRITT 4: ERWEITERTE BILDAUFBEREITUNG FÜR GESICHTSERKENNUNG")
# 1. Kontrast verstärken
contrast_enhancer = ImageEnhance.Contrast(cropped_image)
enhanced_image = contrast_enhancer.enhance(1.8) # 80% mehr Kontrast
# 2. Schärfe erhöhen für bessere Kantenerkennung
sharpness_enhancer = ImageEnhance.Sharpness(enhanced_image)
enhanced_image = sharpness_enhancer.enhance(2.0) # 100% mehr Schärfe
# 3. Helligkeit anpassen
brightness_enhancer = ImageEnhance.Brightness(enhanced_image)
enhanced_image = brightness_enhancer.enhance(1.1) # 10% heller
print(f" ✅ Erweiterte Bildaufbereitung abgeschlossen")
print(f" • Kontrast: +80%")
print(f" • Schärfe: +100%")
print(f" • Helligkeit: +10%")
# Für SAM: Verwende aufbereiteten Ausschnitt
image = enhanced_image
x1, y1, x2, y2 = rel_x1, rel_y1, rel_x2, rel_y2
print(" 🔄 SAM wird auf aufbereitetem Ausschnitt ausgeführt")
print(f" 📊 SAM-Eingabegröße: {image.size}")
# ============================================================
# SAM-AUSFÜHRUNG
# ============================================================
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")
# Vorbereitung für SAM2 - WICHTIG: NUR EINE BBOX
print("-" * 60)
print("🖼️ BILDAUFBEREITUNG FÜR SAM 2")
# SAM erwartet NumPy-Array, kein PIL
image_np = np.array(image.convert("RGB"))
# Immer nur eine BBox verwenden (SAM 2 erwartet genau 1)
input_boxes = [[[x1, y1, x2, y2]]]
# Punkt in der BBox-Mitte (zur Ünterstützung von SAM damit BBox nicht zu dicht um Kopf gezogen werden muß!)
center_x = (x1 + x2) // 2
center_y = (y1 + y2) // 2
# Punkt im Gesicht (30% höher vom Mittelpunkt)(auch für größere BBox)
bbox_height = y2 - y1
face_offset = int(bbox_height * 0.3)
face_x = center_x
face_y = center_y - face_offset
face_y = max(y1 + 10, min(face_y, y2 - 10)) # In BBox halten
# BEIDE Punkte kombinieren
input_points = [[[[center_x, center_y], [face_x, face_y]]]] # ZWEI Punkte
input_labels = [[[1, 1]]] # Beide sind positive Prompts
print(f" 🎯 SAM-Prompt: BBox [{x1},{y1},{x2},{y2}]")
print(f" 👁️ Punkte: Mitte ({center_x},{center_y}), Gesicht ({face_x},{face_y})")
# Aufruf des SAM-Prozessors mit den Variablen. Der Processor verpackt diese Rohdaten
# in die für das SAM-Modell erforderlichen Tensoren und speichert sie in inputs.
inputs = self.sam_processor(
image_np,
input_boxes=input_boxes,
input_points=input_points, # ZWEI Punkte
input_labels=input_labels, # Zwei Labels
return_tensors="pt"
).to(self.device) # Ohne .to(self.device) werden die Tensoren standardmäßig im CPU-RAM erzeugt und gespeichert! Da GPU-Fehler!
print(f"✅ Processor-Ausgabe: Dictionary mit {len(inputs)} Schlüsseln: {list(inputs.keys())}")
print(f" - 'pixel_values' Shape: {inputs['pixel_values'].shape}")
print(f" - 'input_boxes' Shape: {inputs['input_boxes'].shape}")
if 'input_points' in inputs:
print(f" - 'input_points' Shape: {inputs['input_points'].shape}")
# 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
print("📏 SCHRITT 6: MASKE EXTRAHIEREN")
num_masks = outputs.pred_masks.shape[2]
print(f" SAM lieferte {num_masks} verschiedene Masken")
#========================
# Heuristik und Postprocessing auf Crop
#=========================
# Masken speichern in Array
all_masks_crop = [] #Weiterverarbeitung in Crop-Größe
for i in range(num_masks):
single_mask = outputs.pred_masks[:, :, i, :, :]
# 2. FÜR VERARBEITUNG: Auf CROP-GRÖSSE interpolieren
resized_mask_crop = F.interpolate(
single_mask,
size=(image.height, image.width), # CROP-Größe!
mode='bilinear',
align_corners=False
).squeeze()
mask_np = resized_mask_crop.sigmoid().cpu().numpy()
all_masks_crop.append(mask_np)
# Debug-Info
mask_binary = (mask_np > 0.5).astype(np.uint8)
print(f" Maske {i+1}: {np.sum(mask_binary):,}px (Crop-Größe)")
# ============================================================
# HEURISTIK (zur Bestimmung der besten Maske)
# ============================================================
print("🤔 HEURISTIK AUF CROP-GRÖSSE BERECHNEN")
# BBox-Information für Heuristik (IN CROP-KOORDINATEN!)
bbox_center = ((x1 + x2) // 2, (y1 + y2) // 2)
bbox_area = (x2 - x1) * (y2 - y1)
print(f" CROP-BBox: [{x1}, {y1}, {x2}, {y2}]")
print(f" CROP-BBox-Größe: {x2-x1}×{y2-y1}px = {bbox_area:,}px²")
print(f" CROP-BBox-Zentrum: {bbox_center}")
print(f" Crop-Bildgröße: {image.width}×{image.height}")
best_mask_idx = 0
best_score = -1
for i, mask_np in enumerate(all_masks_crop):
mask_max = mask_np.max()
# Grundlegende Filterung
if mask_max < 0.3:
print(f" ❌ Maske {i+1}: Zu niedrige Konfidenz ({mask_max:.3f}), überspringe")
continue
# Adaptiver Threshold
adaptive_threshold = max(0.3, mask_max * 0.7)
mask_binary = (mask_np > adaptive_threshold).astype(np.uint8)
if np.sum(mask_binary) == 0:
print(f" ❌ Maske {i+1}: Keine Pixel nach Threshold {adaptive_threshold:.3f}")
continue
#Maskenfläche in Pixeln (Crop-Grösse)
mask_area_pixels = np.sum(mask_binary)
# ============================================================
# SPEZIALHEURISTIK
# ============================================================
print(f" 🔍 Analysiere Maske {i+1} auf Crop-Größe")
# 1. FLÄCHENBASIERTE BEWERTUNG (40%)
area_ratio = mask_area_pixels / bbox_area
print(f" 📐 Flächen-Ratio: {area_ratio:.3f} ({mask_area_pixels:,} / {bbox_area:,} Pixel)")
# Optimale Kopfgröße: 80-120% der BBox
if area_ratio < 0.6:
print(f" ⚠️ Fläche zu klein für Kopf (<60% der BBox)")
area_score = area_ratio * 0.5 # Stark bestrafen
elif area_ratio > 1.5:
print(f" ⚠️ Fläche zu groß für Kopf (>150% der BBox)")
area_score = 2.0 - area_ratio # Linear bestrafen
elif 0.8 <= area_ratio <= 1.2:
area_score = 1.0 # Perfekte Größe
print(f" ✅ Perfekte Kopfgröße (80-120% der BBox)")
else:
# Sanfte Abweichung
area_score = 1.0 - abs(area_ratio - 1.0) * 0.5
# 2. KOMPAKTHEIT/SOLIDITÄT (30%)
labeled_mask = measure.label(mask_binary)
regions = measure.regionprops(labeled_mask)
if len(regions) == 0:
compactness_score = 0.1
print(f" ❌ Keine zusammenhängenden Regionen gefunden")
else:
# Größte Region finden (sollte der Kopf sein)
largest_region = max(regions, key=lambda r: r.area)
# Solidität = Fläche / konvexe Hüllenfläche
solidity = largest_region.solidity if hasattr(largest_region, 'solidity') else 0.7
# Exzentrizität (wie elliptisch) - Köpfe sind tendenziell elliptisch
eccentricity = largest_region.eccentricity if hasattr(largest_region, 'eccentricity') else 0.5
# Perfekt runde Formen (Kreis) sind 0, Linie wäre 1
# Köpfe haben typischerweise 0.5-0.8
if 0.4 <= eccentricity <= 0.9:
eccentricity_score = 1.0 - abs(eccentricity - 0.65) * 2
else:
eccentricity_score = 0.2
compactness_score = (solidity * 0.6 + eccentricity_score * 0.4)
print(f" 🎯 Kompaktheits-Analyse:")
print(f" • Solidität (Fläche/Konvex): {solidity:.3f}")
print(f" • Exzentrizität (Form): {eccentricity:.3f}")
print(f" • Kompaktheits-Score: {compactness_score:.3f}")
# 3. BBOX-ÜBERLAPPUNG (20%)
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 / mask_area_pixels if mask_area_pixels > 0 else 0
print(f" 📍 BBox-Überlappung: {overlap:,} von {mask_area_pixels:,} Pixeln ({bbox_overlap_ratio:.1%})")
# Für Kopf: Sollte großteils in BBox sein (mind. 70%)
if bbox_overlap_ratio >= 0.7:
bbox_score = 1.0
print(f" ✅ Hohe BBox-Überlappung: {bbox_overlap_ratio:.3f} ({overlap:,} Pixel)")
elif bbox_overlap_ratio >= 0.5:
bbox_score = bbox_overlap_ratio * 1.2
print(f" ⚠️ Mittlere BBox-Überlappung: {bbox_overlap_ratio:.3f}")
else:
bbox_score = bbox_overlap_ratio * 0.8
print(f" ❌ Geringe BBox-Überlappung: {bbox_overlap_ratio:.3f}")
# SAM-KONFIDENZ (10%)
confidence_score = mask_max
# GESAMTSCORE für Kopf
score = (
area_score * 0.4 + # 40% Flächenpassung
compactness_score * 0.3 + # 30% Kompaktheit
bbox_score * 0.2 + # 20% BBox-Überlappung
confidence_score * 0.1 # 10% Konfidenz
)
print(f" 📊 GESICHTS-SCORES für Maske {i+1}:")
print(f" • Flächen-Score: {area_score:.3f}")
print(f" • Kompaktheits-Score: {compactness_score:.3f}")
print(f" • BBox-Überlappungs-Score: {bbox_score:.3f}")
print(f" • Konfidenz-Score: {confidence_score:.3f}")
print(f" • GESAMTSCORE: {score:.3f}")
if score > best_score:
best_score = score
best_mask_idx = i
print(f" 🏆 Neue beste Maske: Nr. {i+1} mit Score {score:.3f}")
print(f"✅ Beste Maske ausgewählt: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
# Beste Maske verwenden
mask_np = all_masks_crop[best_mask_idx]
max_val = mask_np.max()
print(f"🔍 Maximaler SAM-Konfidenzwert der besten Maske: {max_val:.3f}")
# ============================================================
# THRESHOLD-BESTIMMUNG Wahrscheinlichkeiten -> Binär (0,1)
# ============================================================
# Spezieller Threshold für Gesichter
if max_val < 0.5:
dynamic_threshold = 0.25
print(f" ⚠️ SAM ist unsicher für Gesicht (max_val={max_val:.3f} < 0.5)")
elif max_val < 0.8:
dynamic_threshold = max_val * 0.65 # Mittlerer Threshold
print(f" ℹ️ SAM ist mäßig sicher für Gesicht (max_val={max_val:.3f})")
else:
dynamic_threshold = max_val * 0.75 # Hoher Threshold
print(f" ✅ SAM ist sicher für Gesicht (max_val={max_val:.3f} >= 0.8)")
print(f" 🎯 Gesichts-Threshold: {dynamic_threshold:.3f}")
# Binärmaske erstellen
print("🐛 DEBUG THRESHOLD:")
print(f" mask_np Min/Max: {mask_np.min():.3f}/{mask_np.max():.3f}")
print(f" dynamic_threshold: {dynamic_threshold:.3f}")
mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255
print(f"🚨 DEBUG BINÄRMASKE:")
print(f" mask_array Min/Max: {mask_array.min()}/{mask_array.max()}")
print(f" Weiße Pixel in mask_array: {np.sum(mask_array > 0)}")
print(f" Anteil weiße Pixel: {np.sum(mask_array > 0) / mask_array.size:.1%}")
# Fallback wenn Maske leer
if mask_array.max() == 0:
print("⚠️ KRITISCH: Binärmaske ist leer! Erzwinge Testmaske (BBox).")
print(f" 🚨 BBox für Fallback: x1={x1}, y1={y1}, x2={x2}, y2={y2}")
test_mask = np.zeros((image.height, image.width), dtype=np.uint8)
cv2.rectangle(test_mask, (x1, y1), (x2, y2), 255, -1)
mask_array = test_mask
print(f"🐛 DEBUG ERZWUNGENE MASKE: Weiße Pixel: {np.sum(mask_array > 0)}")
# Rohmaske speichern
raw_mask_array = mask_array.copy()
# ============================================================
# POSTPROCESSING auf Crop-Größe
# ============================================================
print("👤 POSTPROCESSING AUF CROP-GRÖSSE")
# 1. Größte zusammenhängende Komponente finden
labeled_array, num_features = ndimage.label(mask_array)
if num_features > 0:
print(f" 🔍 Gefundene Komponenten: {num_features}")
sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
largest_component_idx = np.argmax(sizes) + 1
print(f" 👑 Größte Komponente: Nr. {largest_component_idx} mit {sizes[largest_component_idx-1]:,} Pixel")
# NUR die größte Komponente behalten (der Kopf)
mask_array = np.where(labeled_array == largest_component_idx, mask_array, 0)
# MORPHOLOGISCHE OPERATIONEN FÜR SAUBEREN KOPF
print(" ⚙️ Morphologische Operationen für sauberen Kopf")
# Zuerst CLOSE, um kleine Löcher im Kopf zu füllen
kernel_close = np.ones((7, 7), np.uint8)
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel_close, iterations=1)
print(" • MORPH_CLOSE (7x7) - Löcher im Kopf füllen")
# Dann OPEN, um kleine Ausreißer zu entfernen
kernel_open = np.ones((5, 5), np.uint8)
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel_open, iterations=1)
print(" • MORPH_OPEN (5x5) - Rauschen entfernen")
# LEICHTER DILATE FÜR MEHR ABDECKUNG (wichtig für Gesicht!)
print(" 🔲 Leichter Dilate für natürliche Abdeckung")
kernel_dilate = np.ones((21,21), np.uint8) # Größerer Kernel für Gesicht
mask_array = cv2.dilate(mask_array, kernel_dilate, iterations=1)
# WEICHER GAUSSIAN BLUR FÜR NATÜRLICHE ÜBERGÄNGE
print(" 🔷 Gaussian Blur für weiche Hautübergänge (15x15, sigma=3.0)")
mask_array = cv2.GaussianBlur(mask_array, (31,31), 6.0)
# GAMMA-KORREKTUR FÜR GLATTE, NATÜRLICHE KANTEN
print(" 🎨 Gamma-Korrektur (0.7) für glatte Übergänge")
mask_array_float = mask_array.astype(np.float32) / 255.0
mask_array_float = np.clip(mask_array_float, 0.0, 1.0)
mask_array_float = mask_array_float ** 0.7 # Stärkeres Gamma für weichere Kanten
mask_array = (mask_array_float * 255).astype(np.uint8)
# NOCH EIN WEICHER BLUR FÜR EXTRA-GLÄTTE KANTEN
print(" 💫 Finaler weicher Blur (9x9, sigma=1.5)")
mask_array = cv2.GaussianBlur(mask_array, (19,19), 3.0)
# SICHERSTELLEN, DASS MASKE NICHT ZU DÜNN IST (speziell für Gesicht!)
print(" 📏 Prüfe Maskendichte...")
white_pixels = np.sum(mask_array > 128)
bbox_area = (x2 - x1) * (y2 - y1)
coverage_ratio = white_pixels / bbox_area if bbox_area > 0 else 0
print(f" 📊 Aktuelle Abdeckung: {white_pixels:,}px / {bbox_area:,}px = {coverage_ratio:.1%}")
# Wenn Maske zu dünn (unter 90% der BBox), weiter erweitern
if coverage_ratio < 0.9:
print(f" ⚠️ Maske zu dünn für Gesicht (<90%)")
print(f" 📈 Zusätzlicher Dilate...")
kernel_extra = np.ones((35, 35), np.uint8)
mask_array = cv2.dilate(mask_array, kernel_extra, iterations=1)
# Nochmal weichzeichnen
mask_array = cv2.GaussianBlur(mask_array, (11, 11), 2.0)
# ============================================================
# Maske und Rohmaske auf Originalgröße transformieren
# ============================================================
print("🔄 MASKE AUF ORIGINALGRÖSSE TRANSFORMIEREN")
# 1. Maske in Crop-Größe wird konveriert von NumPy nach PIL
mask_crop_pil = Image.fromarray(mask_array).convert("L")
# Leere Maske in Originalgröße
mask_original = Image.new("L", original_image.size, 0)
# Crop-Maske an richtiger Position in leerem Originalbild einfügen
# da Hauptprogramm Originalgröße erwartet.
mask_original.paste(mask_crop_pil, (crop_x1, crop_y1))
# 2. Rohmaske ebenfalls transformieren
raw_mask_crop_pil = Image.fromarray(raw_mask_array).convert("L")
raw_mask_original = Image.new("L", original_image.size, 0)
raw_mask_original.paste(raw_mask_crop_pil, (crop_x1, crop_y1))
# ============================================================
# ABSCHLIESSENDE STATISTIK
# ============================================================
print("📊 FINALE MASKEN-STATISTIK")
# Nach den Dilate-Operationen:
#expanded_pixels = np.sum(mask_array > 128) - current_white
#print(f" 📈 Maske um {expanded_pixels:,} Pixel erweitert")
#print(f" 📏 Neue Kanten: ~{25//2}px von Original-Maske entfernt")
# Weiße Pixel zählen
final_white = np.sum(mask_array > 128)
final_coverage = final_white / bbox_area if bbox_area > 0 else 0
final_array = np.array(mask_original) # ✅ Original-Größe
white_pixels = np.sum(final_array > 0)
total_pixels = final_array.size
white_ratio = white_pixels / total_pixels * 100 if total_pixels >0 else 0
# Original-BBox Fläche (vor Crop)
original_bbox_width = original_bbox[2] - original_bbox[0]
original_bbox_height = original_bbox[3] - original_bbox[1]
original_face_area = original_bbox_width * original_bbox_height
coverage_ratio = white_pixels / original_face_area if original_face_area > 0 else 0
print(f" 👤 GESICHTSABDECKUNG: {coverage_ratio:.1%} der ursprünglichen BBox")
print(f" Weiße Pixel (Veränderungsbereich): {white_pixels:,} ({white_ratio:.1f}%)")
print(f" Schwarze Pixel (Erhaltungsbereich): {total_pixels-white_pixels:,} ({100-white_ratio:.1f}%)")
print(f" Gesamtpixel: {total_pixels:,}")
print(f" • Weiße Pixel: {final_white:,}")
print(f" • BBox-Fläche: {bbox_area:,}")
print(f" • Abdeckung: {final_coverage:.1%}")
print(f" • Empfohlen: >90% für natürliches Gesicht")
# Warnungen basierend auf Abdeckung
if coverage_ratio < 0.7:
print(f" ⚠️ WARNUNG: Geringe Gesichtsabdeckung ({coverage_ratio:.1%})")
elif coverage_ratio > 1.3:
print(f" ⚠️ WARNUNG: Sehr hohe Gesichtsabdeckung ({coverage_ratio:.1%})")
elif 0.8 <= coverage_ratio <= 1.2:
print(f" ✅ OPTIMALE Gesichtsabdeckung ({coverage_ratio:.1%})")
print("#" * 80)
print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
print(f"📐 Finale Maskengröße: {mask_original.size}")
print(f"🎛️ Verwendeter Modus: {mode}")
print(f"👤 Crop={crop_size}×{crop_size}px, Heuristik-Score={best_score:.3f}")
print(f"👤 Kopfabdeckung: {coverage_ratio:.1%} der BBox")
print("#" * 80)
return mask_original, raw_mask_original
#Maske und Raw_mask muß in Originalgröße zurück!
# ============================================================
# UNBEKANNTER MODUS
# ============================================================
else:
print(f"❌ Unbekannter Modus: {mode}")
return self._create_rectangular_mask(image, bbox_coords, "focus_change")
except Exception as e:
print("❌" * 40)
print("❌ FEHLER IN SAM 2 SEGMENTIERUNG")
print(f"Fehler: {str(e)[:200]}")
print("❌" * 40)
import traceback
traceback.print_exc()
# Fallback
fallback_mask = self._create_rectangular_mask(original_image, original_bbox, mode)
if fallback_mask.size != original_image.size:
print(f" ⚠️ Fallback-Maske angepasst: {fallback_mask.size}{original_image.size}")
fallback_mask = fallback_mask.resize(original_image.size, Image.Resampling.NEAREST)
return fallback_mask, fallback_mask
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 und allen Gelenkpunkten")
pose_image = detector(
image,
include_body=True,
include_hand=True, # 🔥 Hände einschließen (21 Punkte pro Hand)
include_face=True, # 🔥 Gesicht einschließen (70 Punkte)
hand_and_face=True, # 🔥 Beide gleichzeitig
include_foot=True,
detect_resolution=896, # 🔥 Höhere Detektionsauflösung für Details
image_resolution=512, # Ausgabegröße
return_pil=True
)
print("✅ Detaillierte Pose-Map erstellt")
print(f" 🔥 137 Gelenkpunkte (statt nur 25)")
print(f" 🔥 Enthält: Körper (25) + Hände (42) + Gesicht (70)")
print(f" 🔥 Detektionsauflösung: 768px für mehr Details")
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]
extra_maps = {"depth": depth_map, "canny": canny_map} # NEU
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]
extra_maps = {"pose": pose_map, "canny": 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, extra_maps
# 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)