Update controlnet_module.py
Browse files- controlnet_module.py +247 -129
controlnet_module.py
CHANGED
|
@@ -127,65 +127,116 @@ class ControlNetProcessor:
|
|
| 127 |
|
| 128 |
# 2. Validiere BBox
|
| 129 |
x1, y1, x2, y2 = self._validate_bbox(image, bbox_coords)
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
# ============================================================
|
| 132 |
# SPEZIALBEHANDLUNG NUR FÜR face_only_change
|
| 133 |
# ============================================================
|
| 134 |
if mode == "face_only_change":
|
| 135 |
print("-" * 60)
|
| 136 |
-
print("👤 SPEZIALMODUS: NUR GESICHT -
|
| 137 |
print("-" * 60)
|
| 138 |
|
| 139 |
-
#
|
|
|
|
|
|
|
| 140 |
original_image = image
|
| 141 |
-
|
| 142 |
|
| 143 |
-
#
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
-
print(f"
|
| 154 |
-
print(f" 📏
|
| 155 |
-
print(f" 🔲 Ausschnitt-Bereich: [{crop_x1}, {crop_y1}, {crop_x2}, {crop_y2}]")
|
| 156 |
-
print(f" 📏 Ausschnitt-Größe: {crop_x2-crop_x1} × {crop_y2-crop_y1} px")
|
| 157 |
-
print(f" 📊 Puffer: {padding_x} × {padding_y} px")
|
| 158 |
|
| 159 |
# Bild ausschneiden
|
| 160 |
-
cropped_image =
|
| 161 |
-
print(f" ✅ Ausschnitt erstellt: {cropped_image.size}")
|
| 162 |
|
| 163 |
# ============================================================
|
| 164 |
-
#
|
| 165 |
# ============================================================
|
| 166 |
-
print("
|
| 167 |
-
contrast_enhancer = ImageEnhance.Contrast(cropped_image)
|
| 168 |
-
cropped_image = contrast_enhancer.enhance(1.5) # 50% mehr Kontrast
|
| 169 |
-
print(" ✅ Kontrast um 50% erhöht")
|
| 170 |
-
|
| 171 |
-
# BBox-Koordinaten relativ zum Ausschnitt neu berechnen
|
| 172 |
rel_x1 = x1 - crop_x1
|
| 173 |
rel_y1 = y1 - crop_y1
|
| 174 |
rel_x2 = x2 - crop_x1
|
| 175 |
rel_y2 = y2 - crop_y1
|
| 176 |
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
print(f" 📏 Relative BBox Größe: {rel_x2-rel_x1} × {rel_y2-rel_y1} px")
|
| 179 |
|
| 180 |
-
#
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
x1, y1, x2, y2 = rel_x1, rel_y1, rel_x2, rel_y2
|
| 183 |
|
| 184 |
-
print(" 🔄 SAM wird auf kontrastverstärktem Ausschnitt
|
|
|
|
| 185 |
|
| 186 |
# ============================================================
|
| 187 |
# GEMEINSAME SAM-LOGIK FÜR ALLE MODI
|
| 188 |
-
# (arbeitet auf `image` - bei face_only_change ist das der
|
| 189 |
# ============================================================
|
| 190 |
print("-" * 60)
|
| 191 |
print(f"📦 BOUNDING BOX DETAILS FÜR SAM:")
|
|
@@ -201,6 +252,11 @@ class ControlNetProcessor:
|
|
| 201 |
print(f" Konvertiere Bild zu NumPy Array: {image_np.shape}")
|
| 202 |
print(f" Erstelle Input Boxes: {input_boxes}")
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
print(" Verarbeite Bild mit SAM 2 Processor...")
|
| 205 |
inputs = self.sam_processor(
|
| 206 |
image_np,
|
|
@@ -216,40 +272,123 @@ class ControlNetProcessor:
|
|
| 216 |
print(" Führe Vorhersage durch...")
|
| 217 |
outputs = self.sam_model(**inputs)
|
| 218 |
print(f"✅ Vorhersage abgeschlossen")
|
|
|
|
| 219 |
|
| 220 |
# 5. Maske extrahieren und auf Originalgröße skalieren
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
align_corners=False
|
| 229 |
-
).squeeze()
|
| 230 |
-
print(f" Maske nach Interpolation: {final_mask.shape}")
|
| 231 |
-
|
| 232 |
-
# 6. In NumPy konvertieren und Schwellenwert anwenden
|
| 233 |
-
mask_np = final_mask.sigmoid().cpu().numpy()
|
| 234 |
-
print(f" Nach Sigmoid und CPU: {mask_np.shape}, Wertebereich: [{mask_np.min():.3f}, {mask_np.max():.3f}]")
|
| 235 |
|
| 236 |
# ============================================================
|
| 237 |
-
#
|
| 238 |
# ============================================================
|
| 239 |
max_val = mask_np.max()
|
| 240 |
-
print(f" 🔍 Maximaler SAM-Konfidenzwert: {max_val:.3f}")
|
| 241 |
|
| 242 |
-
# NEUE LOGIK: Unterscheidung basierend auf SAM-Konfidenz
|
| 243 |
if max_val < 0.6:
|
| 244 |
-
|
| 245 |
-
# Verwende festen, niedrigen Threshold
|
| 246 |
-
dynamic_threshold = 0.2 # Sehr niedrig für unsichere Vorhersagen
|
| 247 |
print(f" ⚠️ SAM ist unsicher (max_val={max_val:.3f} < 0.6)")
|
| 248 |
print(f" 🎯 Verwende festen niedrigen Threshold: {dynamic_threshold:.3f}")
|
| 249 |
else:
|
| 250 |
-
|
| 251 |
-
# Verwende prozentualen Threshold basierend auf Maximum
|
| 252 |
-
dynamic_threshold = max_val * 0.95
|
| 253 |
print(f" ✅ SAM ist sicher (max_val={max_val:.3f} >= 0.6)")
|
| 254 |
print(f" 🎯 Dynamischer Threshold: {dynamic_threshold:.3f} (80% von Maximum)")
|
| 255 |
|
|
@@ -258,71 +397,76 @@ class ControlNetProcessor:
|
|
| 258 |
print(f" Nach Threshold ({dynamic_threshold:.3f}): {mask_array.shape}, Unique Werte: {unique_vals}")
|
| 259 |
|
| 260 |
# ============================================================
|
| 261 |
-
#
|
| 262 |
# ============================================================
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
labeled_array, num_features = ndimage.label(mask_array)
|
| 265 |
if num_features > 1:
|
| 266 |
-
print(f" 🧹
|
| 267 |
|
| 268 |
sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
-
# Zähle, wie viele Komponenten die Mindestgröße erreichen
|
| 272 |
-
valid_components = 0
|
| 273 |
for i in range(1, num_features + 1):
|
| 274 |
-
if sizes[i-1]
|
| 275 |
-
valid_components += 1
|
| 276 |
-
else:
|
| 277 |
-
# Entferne kleine Komponenten (Rauschen)
|
| 278 |
mask_array = np.where(labeled_array == i, 0, mask_array)
|
| 279 |
-
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
-
#
|
| 283 |
original_mask_array = mask_array.copy() # Person weiß (255), Hintergrund schwarz (0)
|
| 284 |
inverted_mask_array = 255 - mask_array # Person schwarz (0), Hintergrund weiß (255)
|
| 285 |
|
| 286 |
print("-" * 60)
|
| 287 |
-
print(f"🔧
|
| 288 |
print(f" Original-Maske (Person weiß): {original_mask_array.shape}")
|
| 289 |
print(f" Invertierte Maske (Person schwarz): {inverted_mask_array.shape}")
|
| 290 |
|
| 291 |
-
#
|
| 292 |
if mode == "environment_change":
|
| 293 |
print("🌳 MODUS: UMWELT ÄNDERN")
|
| 294 |
# Arbeite auf der INVERTIERTEN Maske (Person schwarz, Hintergrund weiß)
|
| 295 |
mask_array = inverted_mask_array.copy()
|
| 296 |
print(" Arbeite auf invertierter Maske (Person schwarz, Hintergrund weiß)")
|
| 297 |
|
| 298 |
-
# Größte weiße Komponente finden (Hintergrund)
|
| 299 |
-
labeled_array, num_features = ndimage.label(mask_array)
|
| 300 |
-
print(f" Gefundene weiße Komponenten (Hintergrund): {num_features}")
|
| 301 |
-
|
| 302 |
-
# Nur wenn wir mehrere weiße Komponenten haben (z.B. Hintergrund durch Person geteilt)
|
| 303 |
-
if num_features > 1:
|
| 304 |
-
# Finde alle weißen Komponenten
|
| 305 |
-
sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
|
| 306 |
-
print(f" Größen der weißen Komponenten: {sizes}")
|
| 307 |
-
|
| 308 |
-
# Verbinde alle weißen Komponenten (Hintergrundteile)
|
| 309 |
-
for i in range(1, num_features + 1):
|
| 310 |
-
mask_array = np.where(labeled_array == i, 255, mask_array)
|
| 311 |
-
print(f" ✅ Verbinde {num_features} Hintergrund-Komponenten")
|
| 312 |
-
|
| 313 |
# Morphologische Operationen für saubere Umgebung
|
| 314 |
kernel = np.ones((5,5), np.uint8)
|
| 315 |
-
print(f" Wende MORPH_CLOSE an (Kernel 5x5) um schwarze Löcher zu füllen...")
|
| 316 |
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
|
| 317 |
-
print(f" Wende MORPH_OPEN an (Kernel 5x5) um kleine weiße Inseln zu entfernen...")
|
| 318 |
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel)
|
| 319 |
-
|
| 320 |
-
# Umgebung erweitern für besseren Personenschutz (2 Pixel)
|
| 321 |
-
print(f" Wende DILATE an (Kernel 2x2) für Personenschutz...")
|
| 322 |
mask_array = cv2.dilate(mask_array, np.ones((2,2), np.uint8), iterations=1)
|
| 323 |
-
|
| 324 |
-
# Leichte Unschärfe für natürlichere Übergänge
|
| 325 |
-
print(f" Wende GaussianBlur an (Kernel 3x3) für glatte Übergänge...")
|
| 326 |
mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0)
|
| 327 |
|
| 328 |
print(" ✅ Umwelt-Modus: Person geschützt, Hintergrund optimiert")
|
|
@@ -335,70 +479,37 @@ class ControlNetProcessor:
|
|
| 335 |
|
| 336 |
# Größte weiße Komponente behalten (Person)
|
| 337 |
labeled_array, num_features = ndimage.label(mask_array)
|
| 338 |
-
print(f" Gefundene weiße Komponenten (Person): {num_features}")
|
| 339 |
-
|
| 340 |
if num_features > 1:
|
| 341 |
sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
|
| 342 |
-
print(f" Größen der weißen Komponenten: {sizes}")
|
| 343 |
largest_component = np.argmax(sizes) + 1
|
| 344 |
mask_array = np.where(labeled_array == largest_component, mask_array, 0)
|
| 345 |
-
print(f" ✅ Behalte größte Person-Komponente ({num_features} Komponenten)")
|
| 346 |
|
| 347 |
# Maske leicht erweitern für bessere Abdeckung
|
| 348 |
kernel = np.ones((3,3), np.uint8)
|
| 349 |
-
print(f" Wende DILATE an (Kernel 3x3) für bessere Abdeckung...")
|
| 350 |
mask_array = cv2.dilate(mask_array, kernel, iterations=1)
|
| 351 |
-
|
| 352 |
-
# Morphologische Glättung
|
| 353 |
-
print(f" Wende MORPH_CLOSE an (Kernel 3x3) für glatte Kanten...")
|
| 354 |
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
|
| 355 |
|
| 356 |
print(" ✅ Focus-Modus: Person verändert, Hintergrund geschützt")
|
| 357 |
|
| 358 |
elif mode == "face_only_change":
|
| 359 |
-
print("👤 MODUS: NUR GESICHT ÄNDERN
|
| 360 |
# Arbeite auf der ORIGINAL-Maske (Person weiß, Hintergrund schwarz)
|
| 361 |
mask_array = original_mask_array.copy()
|
| 362 |
print(" Arbeite auf originaler Maske (Person weiß, Hintergrund schwarz)")
|
| 363 |
|
| 364 |
-
# Größte weiße Komponente behalten (Person)
|
| 365 |
-
labeled_array, num_features = ndimage.label(mask_array)
|
| 366 |
-
print(f" Gefundene weiße Komponenten auf AUSSCHNITT: {num_features}")
|
| 367 |
-
|
| 368 |
-
if num_features > 0:
|
| 369 |
-
sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
|
| 370 |
-
print(f" Größen der weißen Komponenten auf AUSSCHNITT: {sizes[:10]}...") # Nur erste 10 anzeigen
|
| 371 |
-
|
| 372 |
-
if num_features > 1:
|
| 373 |
-
# WICHTIG: Für Gesicht nehmen wir die GRÖSSTE Komponente im AUSSCHNITT
|
| 374 |
-
# (Im Ausschnitt sollte das das Gesicht sein, nicht der Hintergrund)
|
| 375 |
-
largest_component = np.argmax(sizes) + 1
|
| 376 |
-
mask_array = np.where(labeled_array == largest_component, mask_array, 0)
|
| 377 |
-
print(f" ✅ Behalte größte Komponente im Ausschnitt ({num_features} Komponenten)")
|
| 378 |
-
print(f" 📊 Größe der behaltenen Komponente: {sizes[largest_component-1]:,} Pixel")
|
| 379 |
-
else:
|
| 380 |
-
print(f" ℹ️ Nur eine Komponente gefunden, behalte diese")
|
| 381 |
-
|
| 382 |
# Starke Erosion für präzises Gesicht
|
| 383 |
kernel = np.ones((3,3), np.uint8)
|
| 384 |
-
print(f" Wende ERODE an (Kernel 3x3, 2 Iterationen) für präzises Gesicht...")
|
| 385 |
mask_array = cv2.erode(mask_array, kernel, iterations=2)
|
| 386 |
-
|
| 387 |
-
# Zusätzliche Präzisions-Erosion
|
| 388 |
-
print(f" Wende zusätzliche ERODE an (Kernel 2x2, 1 Iteration)...")
|
| 389 |
mask_array = cv2.erode(mask_array, np.ones((2,2), np.uint8), iterations=1)
|
| 390 |
-
|
| 391 |
-
# Sanfte Glättung der Kanten
|
| 392 |
-
print(f" Wende GaussianBlur an (Kernel 3x3) für glatte Kanten...")
|
| 393 |
mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0)
|
| 394 |
|
| 395 |
-
print(" ✅ Gesichts-Modus:
|
| 396 |
|
| 397 |
# ============================================================
|
| 398 |
-
# SPEZIALSCHRITT
|
| 399 |
# ============================================================
|
| 400 |
print("-" * 60)
|
| 401 |
-
print("🔄
|
| 402 |
|
| 403 |
# Temporäre Maske aus dem Array erstellen
|
| 404 |
temp_mask = Image.fromarray(mask_array).convert("L")
|
|
@@ -428,10 +539,16 @@ class ControlNetProcessor:
|
|
| 428 |
black_ratio = 100 - white_ratio
|
| 429 |
|
| 430 |
print("-" * 60)
|
| 431 |
-
print("📊 MASKEN-STATISTIK (
|
| 432 |
print(f" Weiße Pixel (Veränderungsbereich): {white_pixels:,} ({white_ratio:.1f}%)")
|
| 433 |
print(f" Schwarze Pixel (Erhaltungsbereich): {black_pixels:,} ({black_ratio:.1f}%)")
|
| 434 |
print(f" Gesamtpixel: {total_pixels:,}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
|
| 436 |
# 10. Zurück zu PIL Image
|
| 437 |
mask = Image.fromarray(mask_array).convert("L")
|
|
@@ -440,6 +557,7 @@ class ControlNetProcessor:
|
|
| 440 |
print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
|
| 441 |
print(f"📐 Finale Maskengröße: {mask.size}")
|
| 442 |
print(f"🎛️ Verwendeter Modus: {mode}")
|
|
|
|
| 443 |
print("#" * 80)
|
| 444 |
return mask
|
| 445 |
|
|
|
|
| 127 |
|
| 128 |
# 2. Validiere BBox
|
| 129 |
x1, y1, x2, y2 = self._validate_bbox(image, bbox_coords)
|
| 130 |
+
original_bbox = (x1, y1, x2, y2)
|
| 131 |
+
original_bbox_size = (x2 - x1, y2 - y1)
|
| 132 |
+
print(f"📏 Original-BBox Größe: {original_bbox_size[0]} × {original_bbox_size[1]} px")
|
| 133 |
|
| 134 |
# ============================================================
|
| 135 |
# SPEZIALBEHANDLUNG NUR FÜR face_only_change
|
| 136 |
# ============================================================
|
| 137 |
if mode == "face_only_change":
|
| 138 |
print("-" * 60)
|
| 139 |
+
print("👤 SPEZIALMODUS: NUR GESICHT - EMPFOHLENER WORKFLOW")
|
| 140 |
print("-" * 60)
|
| 141 |
|
| 142 |
+
# ============================================================
|
| 143 |
+
# SCHRITT 1: Originalbild sichern
|
| 144 |
+
# ============================================================
|
| 145 |
original_image = image
|
| 146 |
+
print(f"💾 Originalbild gesichert: {original_image.size}")
|
| 147 |
|
| 148 |
+
# ============================================================
|
| 149 |
+
# SCHRITT 2: Crop = BBox × 2.0 (einmal, sauber, quadratisch)
|
| 150 |
+
# ============================================================
|
| 151 |
+
print("✂️ SCHRITT 2: ERSTELLE QUADRATISCHEN AUSSCHNITT (BBox × 2.0)")
|
| 152 |
+
|
| 153 |
+
# BBox-Zentrum berechnen
|
| 154 |
+
bbox_center_x = (x1 + x2) // 2
|
| 155 |
+
bbox_center_y = (y1 + y2) // 2
|
| 156 |
+
print(f" 📍 BBox-Zentrum: ({bbox_center_x}, {bbox_center_y})")
|
| 157 |
+
|
| 158 |
+
# Größte Dimension der BBox finden
|
| 159 |
+
bbox_width = x2 - x1
|
| 160 |
+
bbox_height = y2 - y1
|
| 161 |
+
bbox_max_dim = max(bbox_width, bbox_height)
|
| 162 |
+
print(f" 📏 BBox Dimensionen: {bbox_width} × {bbox_height} px")
|
| 163 |
+
print(f" 📐 Maximale BBox-Dimension: {bbox_max_dim} px")
|
| 164 |
+
|
| 165 |
+
# Crop-Größe berechnen (BBox × 2.0)
|
| 166 |
+
crop_size = int(bbox_max_dim * 2.0)
|
| 167 |
+
print(f" 🎯 Ziel-Crop-Größe: {crop_size} × {crop_size} px (BBox × 2.0)")
|
| 168 |
+
|
| 169 |
+
# Crop-Koordinaten berechnen (zentriert um BBox)
|
| 170 |
+
crop_x1 = bbox_center_x - crop_size // 2
|
| 171 |
+
crop_y1 = bbox_center_y - crop_size // 2
|
| 172 |
+
crop_x2 = crop_x1 + crop_size
|
| 173 |
+
crop_y2 = crop_y1 + crop_size
|
| 174 |
+
|
| 175 |
+
# Sicherstellen, dass Crop innerhalb der Bildgrenzen bleibt
|
| 176 |
+
crop_x1 = max(0, crop_x1)
|
| 177 |
+
crop_y1 = max(0, crop_y1)
|
| 178 |
+
crop_x2 = min(original_image.width, crop_x2)
|
| 179 |
+
crop_y2 = min(original_image.height, crop_y2)
|
| 180 |
+
|
| 181 |
+
# Falls Crop zu klein ist, anpassen
|
| 182 |
+
actual_crop_width = crop_x2 - crop_x1
|
| 183 |
+
actual_crop_height = crop_y2 - crop_y1
|
| 184 |
+
|
| 185 |
+
if actual_crop_width < crop_size or actual_crop_height < crop_size:
|
| 186 |
+
# An Kanten anpassen
|
| 187 |
+
if crop_x1 == 0:
|
| 188 |
+
crop_x2 = min(original_image.width, crop_size)
|
| 189 |
+
elif crop_x2 == original_image.width:
|
| 190 |
+
crop_x1 = max(0, original_image.width - crop_size)
|
| 191 |
+
|
| 192 |
+
if crop_y1 == 0:
|
| 193 |
+
crop_y2 = min(original_image.height, crop_size)
|
| 194 |
+
elif crop_y2 == original_image.height:
|
| 195 |
+
crop_y1 = max(0, original_image.height - crop_size)
|
| 196 |
|
| 197 |
+
print(f" 🔲 Crop-Bereich: [{crop_x1}, {crop_y1}, {crop_x2}, {crop_y2}]")
|
| 198 |
+
print(f" 📏 Tatsächliche Crop-Größe: {crop_x2-crop_x1} × {crop_y2-crop_y1} px")
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
# Bild ausschneiden
|
| 201 |
+
cropped_image = original_image.crop((crop_x1, crop_y1, crop_x2, crop_y2))
|
| 202 |
+
print(f" ✅ Quadratischer Ausschnitt erstellt: {cropped_image.size}")
|
| 203 |
|
| 204 |
# ============================================================
|
| 205 |
+
# SCHRITT 3: BBox-Koordinaten im Crop-Koordinatensystem berechnen
|
| 206 |
# ============================================================
|
| 207 |
+
print("📐 SCHRITT 3: BBox-KOORDINATEN TRANSFORMIEREN")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
rel_x1 = x1 - crop_x1
|
| 209 |
rel_y1 = y1 - crop_y1
|
| 210 |
rel_x2 = x2 - crop_x1
|
| 211 |
rel_y2 = y2 - crop_y1
|
| 212 |
|
| 213 |
+
# Sicherstellen, dass BBox innerhalb des Crops liegt
|
| 214 |
+
rel_x1 = max(0, rel_x1)
|
| 215 |
+
rel_y1 = max(0, rel_y1)
|
| 216 |
+
rel_x2 = min(cropped_image.width, rel_x2)
|
| 217 |
+
rel_y2 = min(cropped_image.height, rel_y2)
|
| 218 |
+
|
| 219 |
+
print(f" 🎯 Relative BBox im Crop: [{rel_x1}, {rel_y1}, {rel_x2}, {rel_y2}]")
|
| 220 |
print(f" 📏 Relative BBox Größe: {rel_x2-rel_x1} × {rel_y2-rel_y1} px")
|
| 221 |
|
| 222 |
+
# ============================================================
|
| 223 |
+
# SCHRITT 4: Bildkontrast verstärken für bessere Segmentierung
|
| 224 |
+
# ============================================================
|
| 225 |
+
print("🔍 SCHRITT 4: KONTRASTVERSTÄRKUNG FÜR SAM")
|
| 226 |
+
contrast_enhancer = ImageEnhance.Contrast(cropped_image)
|
| 227 |
+
enhanced_cropped_image = contrast_enhancer.enhance(1.5) # 50% mehr Kontrast
|
| 228 |
+
print(f" ✅ Kontrast um 50% erhöht")
|
| 229 |
+
|
| 230 |
+
# Für SAM: Verwende kontrastverstärkten Ausschnitt und relative Koordinaten
|
| 231 |
+
image = enhanced_cropped_image
|
| 232 |
x1, y1, x2, y2 = rel_x1, rel_y1, rel_x2, rel_y2
|
| 233 |
|
| 234 |
+
print(" 🔄 SAM wird auf kontrastverstärktem Ausschnitt ausgeführt")
|
| 235 |
+
print(f" 📊 SAM-Eingabegröße: {image.size}")
|
| 236 |
|
| 237 |
# ============================================================
|
| 238 |
# GEMEINSAME SAM-LOGIK FÜR ALLE MODI
|
| 239 |
+
# (arbeitet auf `image` - bei face_only_change ist das der Crop)
|
| 240 |
# ============================================================
|
| 241 |
print("-" * 60)
|
| 242 |
print(f"📦 BOUNDING BOX DETAILS FÜR SAM:")
|
|
|
|
| 252 |
print(f" Konvertiere Bild zu NumPy Array: {image_np.shape}")
|
| 253 |
print(f" Erstelle Input Boxes: {input_boxes}")
|
| 254 |
|
| 255 |
+
# ============================================================
|
| 256 |
+
# SCHRITT 4-5: SAM mit Box-Prompt = ursprüngliche BBox
|
| 257 |
+
# (im Crop-Koordinatensystem bei face_only_change)
|
| 258 |
+
# ============================================================
|
| 259 |
+
print("🎯 SCHRITT 4-5: SAM MIT BOX-PROMPT")
|
| 260 |
print(" Verarbeite Bild mit SAM 2 Processor...")
|
| 261 |
inputs = self.sam_processor(
|
| 262 |
image_np,
|
|
|
|
| 272 |
print(" Führe Vorhersage durch...")
|
| 273 |
outputs = self.sam_model(**inputs)
|
| 274 |
print(f"✅ Vorhersage abgeschlossen")
|
| 275 |
+
print(f" Anzahl der Vorhersagemasken: {outputs.pred_masks.shape[2]}")
|
| 276 |
|
| 277 |
# 5. Maske extrahieren und auf Originalgröße skalieren
|
| 278 |
+
print("📏 SCHRITT 6: MASKE EXTRAHIEREN UND SKALIEREN")
|
| 279 |
+
|
| 280 |
+
# ============================================================
|
| 281 |
+
# SCHRITT 6: SAM liefert mehrere Masken
|
| 282 |
+
# ============================================================
|
| 283 |
+
num_masks = outputs.pred_masks.shape[2]
|
| 284 |
+
print(f" SAM lieferte {num_masks} verschiedene Masken")
|
| 285 |
+
|
| 286 |
+
# Extrahiere alle Masken
|
| 287 |
+
all_masks = []
|
| 288 |
+
mask_qualities = []
|
| 289 |
+
|
| 290 |
+
for i in range(num_masks):
|
| 291 |
+
single_mask = outputs.pred_masks[:, :, i, :, :]
|
| 292 |
+
resized_mask = F.interpolate(
|
| 293 |
+
single_mask,
|
| 294 |
+
size=(image.height, image.width),
|
| 295 |
+
mode='bilinear',
|
| 296 |
+
align_corners=False
|
| 297 |
+
).squeeze()
|
| 298 |
+
|
| 299 |
+
mask_np = resized_mask.sigmoid().cpu().numpy()
|
| 300 |
+
all_masks.append(mask_np)
|
| 301 |
+
|
| 302 |
+
# Basis-Statistiken für jede Maske
|
| 303 |
+
mask_binary = (mask_np > 0.5).astype(np.uint8)
|
| 304 |
+
mask_area = np.sum(mask_binary)
|
| 305 |
+
print(f" Maske {i+1}: Größe={mask_area:,} Pixel, Max-Konfidenz={mask_np.max():.3f}")
|
| 306 |
+
|
| 307 |
+
# ============================================================
|
| 308 |
+
# SCHRITT 6: Maskenauswahl per Heuristik
|
| 309 |
+
# ============================================================
|
| 310 |
+
print("🤔 SCHRITT 6: MASKENAUSWAHL MIT HEURISTIK")
|
| 311 |
+
|
| 312 |
+
# Erwartete BBox für Heuristik (in Pixel-Koordinaten)
|
| 313 |
+
bbox_center = ((x1 + x2) // 2, (y1 + y2) // 2)
|
| 314 |
+
bbox_area = (x2 - x1) * (y2 - y1)
|
| 315 |
+
print(f" Erwartetes BBox-Zentrum: {bbox_center}")
|
| 316 |
+
print(f" Erwartete BBox-Fläche: {bbox_area:,} Pixel")
|
| 317 |
+
|
| 318 |
+
best_mask_idx = 0
|
| 319 |
+
best_score = -1
|
| 320 |
+
|
| 321 |
+
for i, mask_np in enumerate(all_masks):
|
| 322 |
+
# Threshold für binäre Maske
|
| 323 |
+
mask_binary = (mask_np > 0.5).astype(np.uint8)
|
| 324 |
+
|
| 325 |
+
if np.sum(mask_binary) == 0:
|
| 326 |
+
print(f" ❌ Maske {i+1}: Keine Pixel, überspringe")
|
| 327 |
+
continue
|
| 328 |
+
|
| 329 |
+
# 1. Größte Überlappung mit BBox
|
| 330 |
+
# Erstelle binäre BBox-Maske
|
| 331 |
+
bbox_mask = np.zeros((image.height, image.width), dtype=np.uint8)
|
| 332 |
+
bbox_mask[y1:y2, x1:x2] = 1
|
| 333 |
+
|
| 334 |
+
overlap = np.sum(mask_binary & bbox_mask)
|
| 335 |
+
bbox_overlap_ratio = overlap / np.sum(bbox_mask) if np.sum(bbox_mask) > 0 else 0
|
| 336 |
+
|
| 337 |
+
# 2. Schwerpunkt nahe BBox-Zentrum
|
| 338 |
+
y_coords, x_coords = np.where(mask_binary > 0)
|
| 339 |
+
if len(y_coords) > 0:
|
| 340 |
+
centroid_y = np.mean(y_coords)
|
| 341 |
+
centroid_x = np.mean(x_coords)
|
| 342 |
+
centroid_distance = np.sqrt((centroid_x - bbox_center[0])**2 + (centroid_y - bbox_center[1])**2)
|
| 343 |
+
normalized_distance = centroid_distance / max(image.width, image.height)
|
| 344 |
+
else:
|
| 345 |
+
centroid_distance = float('inf')
|
| 346 |
+
normalized_distance = 1.0
|
| 347 |
+
|
| 348 |
+
# 3. Maskenfläche im erwarteten Bereich
|
| 349 |
+
mask_area = np.sum(mask_binary)
|
| 350 |
+
area_ratio = mask_area / bbox_area
|
| 351 |
+
area_score = 1.0 - min(abs(area_ratio - 1.0), 1.0) # 1.0 ist perfekt
|
| 352 |
+
|
| 353 |
+
# 4. SAM-Konfidenz
|
| 354 |
+
confidence_score = mask_np.max()
|
| 355 |
+
|
| 356 |
+
# Gesamtscore berechnen (Gewichtung anpassbar)
|
| 357 |
+
score = (
|
| 358 |
+
bbox_overlap_ratio * 0.4 + # 40% Überlappung mit BBox
|
| 359 |
+
(1.0 - normalized_distance) * 0.3 + # 30% Zentrumsnähe
|
| 360 |
+
area_score * 0.2 + # 20% Flächenübereinstimmung
|
| 361 |
+
confidence_score * 0.1 # 10% SAM-Konfidenz
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
print(f" 📊 Maske {i+1} Scores:")
|
| 365 |
+
print(f" • BBox-Überlappung: {bbox_overlap_ratio:.3f} ({overlap:,} Pixel)")
|
| 366 |
+
print(f" • Zentrums-Distanz: {centroid_distance:.1f} px (normalisiert: {normalized_distance:.3f})")
|
| 367 |
+
print(f" • Flächen-Ratio: {area_ratio:.3f} ({mask_area:,} Pixel)")
|
| 368 |
+
print(f" • Max-Konfidenz: {confidence_score:.3f}")
|
| 369 |
+
print(f" • GESAMTSCORE: {score:.3f}")
|
| 370 |
+
|
| 371 |
+
if score > best_score:
|
| 372 |
+
best_score = score
|
| 373 |
+
best_mask_idx = i
|
| 374 |
|
| 375 |
+
print(f"✅ Beste Maske ausgewählt: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
|
| 376 |
+
|
| 377 |
+
# Beste Maske verwenden
|
| 378 |
+
mask_np = all_masks[best_mask_idx]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
# ============================================================
|
| 381 |
+
# DYNAMISCHER THRESHOLD
|
| 382 |
# ============================================================
|
| 383 |
max_val = mask_np.max()
|
| 384 |
+
print(f" 🔍 Maximaler SAM-Konfidenzwert der besten Maske: {max_val:.3f}")
|
| 385 |
|
|
|
|
| 386 |
if max_val < 0.6:
|
| 387 |
+
dynamic_threshold = 0.2
|
|
|
|
|
|
|
| 388 |
print(f" ⚠️ SAM ist unsicher (max_val={max_val:.3f} < 0.6)")
|
| 389 |
print(f" 🎯 Verwende festen niedrigen Threshold: {dynamic_threshold:.3f}")
|
| 390 |
else:
|
| 391 |
+
dynamic_threshold = max_val * 0.8
|
|
|
|
|
|
|
| 392 |
print(f" ✅ SAM ist sicher (max_val={max_val:.3f} >= 0.6)")
|
| 393 |
print(f" 🎯 Dynamischer Threshold: {dynamic_threshold:.3f} (80% von Maximum)")
|
| 394 |
|
|
|
|
| 397 |
print(f" Nach Threshold ({dynamic_threshold:.3f}): {mask_array.shape}, Unique Werte: {unique_vals}")
|
| 398 |
|
| 399 |
# ============================================================
|
| 400 |
+
# SCHRITT 7: Postprocessing
|
| 401 |
# ============================================================
|
| 402 |
+
print("🔧 SCHRITT 7: POSTPROCESSING")
|
| 403 |
+
|
| 404 |
+
# a) Kleine Löcher füllen
|
| 405 |
+
if np.sum(mask_array > 0) > 0:
|
| 406 |
+
# Finde alle schwarze Regionen in der weißen Maske (Löcher)
|
| 407 |
+
mask_inverted = 255 - mask_array
|
| 408 |
+
labeled_holes, num_holes = ndimage.label(mask_inverted)
|
| 409 |
+
|
| 410 |
+
if num_holes > 1: # 1 ist der Hintergrund
|
| 411 |
+
print(f" 🔍 Gefundene Löcher: {num_holes - 1}")
|
| 412 |
+
|
| 413 |
+
# Fülle kleine Löcher
|
| 414 |
+
for i in range(2, num_holes + 1): # Beginne bei 2 (1 ist Hintergrund)
|
| 415 |
+
hole_size = np.sum(labeled_holes == i)
|
| 416 |
+
if hole_size < 500: # Kleine Löcher füllen
|
| 417 |
+
mask_array = np.where(labeled_holes == i, 255, mask_array)
|
| 418 |
+
print(f" • Loch {i} gefüllt ({hole_size} Pixel)")
|
| 419 |
+
|
| 420 |
+
# b) Kleine Komponenten entfernen
|
| 421 |
labeled_array, num_features = ndimage.label(mask_array)
|
| 422 |
if num_features > 1:
|
| 423 |
+
print(f" 🧹 Komponenten vor Filterung: {num_features}")
|
| 424 |
|
| 425 |
sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
|
| 426 |
+
total_mask_area = np.sum(mask_array > 0)
|
| 427 |
+
min_size = total_mask_area * 0.1 # 10% der Gesamtfläche
|
| 428 |
+
|
| 429 |
+
print(f" 📊 Gesamtmaskenfläche: {total_mask_area:,} Pixel")
|
| 430 |
+
print(f" 📏 Minimale Komponentengröße: {min_size:,.0f} Pixel")
|
| 431 |
|
|
|
|
|
|
|
| 432 |
for i in range(1, num_features + 1):
|
| 433 |
+
if sizes[i-1] < min_size:
|
|
|
|
|
|
|
|
|
|
| 434 |
mask_array = np.where(labeled_array == i, 0, mask_array)
|
| 435 |
+
print(f" • Komponente {i} entfernt ({sizes[i-1]:,} Pixel)")
|
| 436 |
+
|
| 437 |
+
# c) Ggf. leichte Erosion/Dilation
|
| 438 |
+
print(" ⚙️ Leichte morphologische Operationen...")
|
| 439 |
+
kernel = np.ones((3, 3), np.uint8)
|
| 440 |
+
|
| 441 |
+
# Leichte Erosion für saubere Kanten
|
| 442 |
+
mask_array = cv2.erode(mask_array, kernel, iterations=1)
|
| 443 |
+
print(" • Erosion (1 Iteration) angewendet")
|
| 444 |
+
|
| 445 |
+
# Leichte Dilation für glatte Übergänge
|
| 446 |
+
mask_array = cv2.dilate(mask_array, kernel, iterations=1)
|
| 447 |
+
print(" • Dilation (1 Iteration) angewendet")
|
| 448 |
|
| 449 |
+
# BEIDE MASKEN ERSTELLEN (vor Nachbearbeitung)
|
| 450 |
original_mask_array = mask_array.copy() # Person weiß (255), Hintergrund schwarz (0)
|
| 451 |
inverted_mask_array = 255 - mask_array # Person schwarz (0), Hintergrund weiß (255)
|
| 452 |
|
| 453 |
print("-" * 60)
|
| 454 |
+
print(f"🔧 MODUS-SPEZIFISCHE NACHBEARBEITUNG: {mode}")
|
| 455 |
print(f" Original-Maske (Person weiß): {original_mask_array.shape}")
|
| 456 |
print(f" Invertierte Maske (Person schwarz): {inverted_mask_array.shape}")
|
| 457 |
|
| 458 |
+
# MODUS-SPEZIFISCHE NACHBEARBEITUNG
|
| 459 |
if mode == "environment_change":
|
| 460 |
print("🌳 MODUS: UMWELT ÄNDERN")
|
| 461 |
# Arbeite auf der INVERTIERTEN Maske (Person schwarz, Hintergrund weiß)
|
| 462 |
mask_array = inverted_mask_array.copy()
|
| 463 |
print(" Arbeite auf invertierter Maske (Person schwarz, Hintergrund weiß)")
|
| 464 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
# Morphologische Operationen für saubere Umgebung
|
| 466 |
kernel = np.ones((5,5), np.uint8)
|
|
|
|
| 467 |
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
|
|
|
|
| 468 |
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel)
|
|
|
|
|
|
|
|
|
|
| 469 |
mask_array = cv2.dilate(mask_array, np.ones((2,2), np.uint8), iterations=1)
|
|
|
|
|
|
|
|
|
|
| 470 |
mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0)
|
| 471 |
|
| 472 |
print(" ✅ Umwelt-Modus: Person geschützt, Hintergrund optimiert")
|
|
|
|
| 479 |
|
| 480 |
# Größte weiße Komponente behalten (Person)
|
| 481 |
labeled_array, num_features = ndimage.label(mask_array)
|
|
|
|
|
|
|
| 482 |
if num_features > 1:
|
| 483 |
sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
|
|
|
|
| 484 |
largest_component = np.argmax(sizes) + 1
|
| 485 |
mask_array = np.where(labeled_array == largest_component, mask_array, 0)
|
|
|
|
| 486 |
|
| 487 |
# Maske leicht erweitern für bessere Abdeckung
|
| 488 |
kernel = np.ones((3,3), np.uint8)
|
|
|
|
| 489 |
mask_array = cv2.dilate(mask_array, kernel, iterations=1)
|
|
|
|
|
|
|
|
|
|
| 490 |
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
|
| 491 |
|
| 492 |
print(" ✅ Focus-Modus: Person verändert, Hintergrund geschützt")
|
| 493 |
|
| 494 |
elif mode == "face_only_change":
|
| 495 |
+
print("👤 MODUS: NUR GESICHT ÄNDERN")
|
| 496 |
# Arbeite auf der ORIGINAL-Maske (Person weiß, Hintergrund schwarz)
|
| 497 |
mask_array = original_mask_array.copy()
|
| 498 |
print(" Arbeite auf originaler Maske (Person weiß, Hintergrund schwarz)")
|
| 499 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
# Starke Erosion für präzises Gesicht
|
| 501 |
kernel = np.ones((3,3), np.uint8)
|
|
|
|
| 502 |
mask_array = cv2.erode(mask_array, kernel, iterations=2)
|
|
|
|
|
|
|
|
|
|
| 503 |
mask_array = cv2.erode(mask_array, np.ones((2,2), np.uint8), iterations=1)
|
|
|
|
|
|
|
|
|
|
| 504 |
mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0)
|
| 505 |
|
| 506 |
+
print(" ✅ Gesichts-Modus: Postprocessing auf Ausschnitt abgeschlossen")
|
| 507 |
|
| 508 |
# ============================================================
|
| 509 |
+
# SPEZIALSCHRITT: MASKE ZURÜCK AUF ORIGINALGRÖSSE BRINGEN
|
| 510 |
# ============================================================
|
| 511 |
print("-" * 60)
|
| 512 |
+
print("🔄 MASKE VOM AUSSCHNITT ZURÜCK AUF ORIGINALGRÖSSE")
|
| 513 |
|
| 514 |
# Temporäre Maske aus dem Array erstellen
|
| 515 |
temp_mask = Image.fromarray(mask_array).convert("L")
|
|
|
|
| 539 |
black_ratio = 100 - white_ratio
|
| 540 |
|
| 541 |
print("-" * 60)
|
| 542 |
+
print("📊 MASKEN-STATISTIK (FINAL)")
|
| 543 |
print(f" Weiße Pixel (Veränderungsbereich): {white_pixels:,} ({white_ratio:.1f}%)")
|
| 544 |
print(f" Schwarze Pixel (Erhaltungsbereich): {black_pixels:,} ({black_ratio:.1f}%)")
|
| 545 |
print(f" Gesamtpixel: {total_pixels:,}")
|
| 546 |
+
|
| 547 |
+
if mode == "face_only_change":
|
| 548 |
+
# Zusätzliche Statistik für Gesichtsmodus
|
| 549 |
+
original_face_area = original_bbox_size[0] * original_bbox_size[1]
|
| 550 |
+
coverage_ratio = white_pixels / original_face_area if original_face_area > 0 else 0
|
| 551 |
+
print(f" 👤 Gesichtsabdeckung: {coverage_ratio:.1%} der ursprünglichen BBox")
|
| 552 |
|
| 553 |
# 10. Zurück zu PIL Image
|
| 554 |
mask = Image.fromarray(mask_array).convert("L")
|
|
|
|
| 557 |
print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
|
| 558 |
print(f"📐 Finale Maskengröße: {mask.size}")
|
| 559 |
print(f"🎛️ Verwendeter Modus: {mode}")
|
| 560 |
+
print(f"👤 Bei face_only_change: Crop={crop_size}×{crop_size}px, Heuristik-Score={best_score:.3f}")
|
| 561 |
print("#" * 80)
|
| 562 |
return mask
|
| 563 |
|