Astridkraft commited on
Commit
421ffe3
·
verified ·
1 Parent(s): 4e659cc

Update controlnet_module.py

Browse files
Files changed (1) hide show
  1. controlnet_module.py +343 -200
controlnet_module.py CHANGED
@@ -102,6 +102,7 @@ class ControlNetProcessor:
102
  print(f"⚠️ Fehler beim Glätten der Maske: {e}")
103
  return mask_array
104
 
 
105
  def create_sam_mask(self, image, bbox_coords, mode):
106
  """
107
  ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2
@@ -114,7 +115,13 @@ class ControlNetProcessor:
114
  print(f"📐 Eingabebild-Größe: {image.size}")
115
  print(f"🎛️ Ausgewählter Modus: {mode}")
116
 
117
- # 1. SAM2 laden (falls noch nicht geschehen)
 
 
 
 
 
 
118
  if not self.sam_initialized:
119
  print("📥 SAM 2 ist noch nicht geladen, starte Lazy Loading...")
120
  self._lazy_load_sam()
@@ -136,7 +143,7 @@ class ControlNetProcessor:
136
  # ============================================================
137
  if mode == "face_only_change":
138
  print("-" * 60)
139
- print("👤 SPEZIALMODUS: NUR GESICHT - EMPFOHLENER WORKFLOW")
140
  print("-" * 60)
141
 
142
  # ============================================================
@@ -146,9 +153,9 @@ class ControlNetProcessor:
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
@@ -162,9 +169,9 @@ class ControlNetProcessor:
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
@@ -202,7 +209,7 @@ class ControlNetProcessor:
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
@@ -220,23 +227,36 @@ class ControlNetProcessor:
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:")
@@ -248,15 +268,37 @@ class ControlNetProcessor:
248
  print("-" * 60)
249
  print("🖼️ BILDAUFBEREITUNG FÜR SAM 2")
250
  image_np = np.array(image.convert("RGB"))
251
- input_boxes = [[[x1, y1, x2, y2]]]
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,
@@ -274,18 +316,14 @@ class ControlNetProcessor:
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, :, :]
@@ -305,11 +343,10 @@ class ControlNetProcessor:
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}")
@@ -319,58 +356,158 @@ class ControlNetProcessor:
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
 
@@ -378,186 +515,191 @@ class ControlNetProcessor:
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
 
395
  mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255
396
- unique_vals = np.unique(mask_array)
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")
473
-
474
- elif mode == "focus_change":
475
- print("🎯 MODUS: FOCUS ÄNDERN")
476
- # Arbeite auf der ORIGINAL-Maske (Person weiß, Hintergrund schwarz)
477
- mask_array = original_mask_array.copy()
478
- print(" Arbeite auf originaler Maske (Person weiß, Hintergrund schwarz)")
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")
516
  print(f" Maskengröße auf Ausschnitt: {temp_mask.size}")
517
 
518
- # Leere Maske in Originalbild-Größe erstellen
519
  final_mask = Image.new("L", original_image.size, 0)
520
  print(f" Leere Maske in Originalgröße: {final_mask.size}")
521
 
522
- # Die segmentierte Maske an der richtigen Position im Originalbild platzieren
523
  final_mask.paste(temp_mask, (crop_x1, crop_y1))
524
  print(f" Maskenposition im Original: ({crop_x1}, {crop_y1})")
525
 
526
- # Zurück zum mask_array konvertieren
527
  mask_array = np.array(final_mask)
528
  print(f" ✅ Maske zurück auf Originalgröße skaliert: {mask_array.shape}")
529
 
530
- # Originalbild wiederherstellen für eventuelle spätere Verwendung
531
  image = original_image
532
  print(f" 🔄 Bild-Referenz wieder auf Original gesetzt: {image.size}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
 
534
- # 9. Qualitätskontrolle und Statistik
535
  white_pixels = np.sum(mask_array > 127)
536
  total_pixels = mask_array.size
537
  white_ratio = white_pixels / total_pixels * 100
538
- black_pixels = total_pixels - white_pixels
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")
555
 
556
  print("#" * 80)
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
 
@@ -570,6 +712,7 @@ class ControlNetProcessor:
570
  traceback.print_exc()
571
  print("ℹ️ Fallback auf rechteckige Maske")
572
  return self._create_rectangular_mask(image, bbox_coords, mode)
 
573
 
574
  def _create_rectangular_mask(self, image, bbox_coords, mode):
575
  """Fallback: Erstellt rechteckige Maske"""
 
102
  print(f"⚠️ Fehler beim Glätten der Maske: {e}")
103
  return mask_array
104
 
105
+
106
  def create_sam_mask(self, image, bbox_coords, mode):
107
  """
108
  ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2
 
115
  print(f"📐 Eingabebild-Größe: {image.size}")
116
  print(f"🎛️ Ausgewählter Modus: {mode}")
117
 
118
+ # Variablen für alle Modi initialisieren
119
+ crop_size = None
120
+ crop_x1 = crop_y1 = crop_x2 = crop_y2 = None
121
+ original_image = image
122
+ best_score = 0.0
123
+
124
+ # 1. SAM2 laden
125
  if not self.sam_initialized:
126
  print("📥 SAM 2 ist noch nicht geladen, starte Lazy Loading...")
127
  self._lazy_load_sam()
 
143
  # ============================================================
144
  if mode == "face_only_change":
145
  print("-" * 60)
146
+ print("👤 SPEZIALMODUS: NUR GESICHT - ROBUSTER WORKFLOW")
147
  print("-" * 60)
148
 
149
  # ============================================================
 
153
  print(f"💾 Originalbild gesichert: {original_image.size}")
154
 
155
  # ============================================================
156
+ # SCHRITT 2: Crop = BBox × 2.5 (ERHÖHT für mehr Kontext)
157
  # ============================================================
158
+ print("✂️ SCHRITT 2: ERSTELLE QUADRATISCHEN AUSSCHNITT (BBox × 2.5)")
159
 
160
  # BBox-Zentrum berechnen
161
  bbox_center_x = (x1 + x2) // 2
 
169
  print(f" 📏 BBox Dimensionen: {bbox_width} × {bbox_height} px")
170
  print(f" 📐 Maximale BBox-Dimension: {bbox_max_dim} px")
171
 
172
+ # ERHÖHT: Crop-Größe berechnen (BBox × 2.5 für mehr Kontext)
173
+ crop_size = int(bbox_max_dim * 2.5)
174
+ print(f" 🎯 Ziel-Crop-Größe: {crop_size} × {crop_size} px (BBox × 2.5)")
175
 
176
  # Crop-Koordinaten berechnen (zentriert um BBox)
177
  crop_x1 = bbox_center_x - crop_size // 2
 
209
  print(f" ✅ Quadratischer Ausschnitt erstellt: {cropped_image.size}")
210
 
211
  # ============================================================
212
+ # SCHRITT 3: BBox-Koordinaten transformieren
213
  # ============================================================
214
  print("📐 SCHRITT 3: BBox-KOORDINATEN TRANSFORMIEREN")
215
  rel_x1 = x1 - crop_x1
 
227
  print(f" 📏 Relative BBox Größe: {rel_x2-rel_x1} × {rel_y2-rel_y1} px")
228
 
229
  # ============================================================
230
+ # SCHRITT 4: INTENSIVE BILDAUFBEREITUNG FÜR GESICHTSERKENNUNG
231
  # ============================================================
232
+ print("🔍 SCHRITT 4: ERWEITERTE BILDAUFBEREITUNG FÜR GESICHTSERKENNUNG")
233
+
234
+ # 1. Kontrast verstärken
235
  contrast_enhancer = ImageEnhance.Contrast(cropped_image)
236
+ enhanced_image = contrast_enhancer.enhance(1.8) # 80% mehr Kontrast
237
+
238
+ # 2. Schärfe erhöhen für bessere Kantenerkennung
239
+ sharpness_enhancer = ImageEnhance.Sharpness(enhanced_image)
240
+ enhanced_image = sharpness_enhancer.enhance(2.0) # 100% mehr Schärfe
241
+
242
+ # 3. Helligkeit anpassen
243
+ brightness_enhancer = ImageEnhance.Brightness(enhanced_image)
244
+ enhanced_image = brightness_enhancer.enhance(1.1) # 10% heller
245
 
246
+ print(f" ✅ Erweiterte Bildaufbereitung abgeschlossen")
247
+ print(f" • Kontrast: +80%")
248
+ print(f" • Schärfe: +100%")
249
+ print(f" • Helligkeit: +10%")
250
+
251
+ # Für SAM: Verwende aufbereiteten Ausschnitt
252
+ image = enhanced_image
253
  x1, y1, x2, y2 = rel_x1, rel_y1, rel_x2, rel_y2
254
 
255
+ print(" 🔄 SAM wird auf aufbereitetem Ausschnitt ausgeführt")
256
  print(f" 📊 SAM-Eingabegröße: {image.size}")
257
 
258
  # ============================================================
259
  # GEMEINSAME SAM-LOGIK FÜR ALLE MODI
 
260
  # ============================================================
261
  print("-" * 60)
262
  print(f"📦 BOUNDING BOX DETAILS FÜR SAM:")
 
268
  print("-" * 60)
269
  print("🖼️ BILDAUFBEREITUNG FÜR SAM 2")
270
  image_np = np.array(image.convert("RGB"))
 
 
 
271
 
272
  # ============================================================
273
+ # NEU: ERWEITERTE SAM-EINGABE FÜR GESICHTSMODUS
 
274
  # ============================================================
275
+ print("🎯 SCHRITT 4-5: ERWEITERTE SAM-PROMPTING")
276
+
277
+ bbox_width = x2 - x1
278
+ bbox_height = y2 - y1
279
+
280
+ # Für Gesichtsmodus: Verstärkte BBox-Prompts
281
+ if mode == "face_only_change":
282
+ # 1. Haupt-BBox (ursprüngliche Koordinaten)
283
+ input_boxes = [[[x1, y1, x2, y2]]]
284
+
285
+ # 2. ERWEITERTE BBox für Gesichtskontext (15% größer)
286
+ expand_factor = 0.15
287
+ expanded_x1 = max(0, int(x1 - bbox_width * expand_factor))
288
+ expanded_y1 = max(0, int(y1 - bbox_height * expand_factor))
289
+ expanded_x2 = min(image.width, int(x2 + bbox_width * expand_factor))
290
+ expanded_y2 = min(image.height, int(y2 + bbox_height * expand_factor))
291
+
292
+ input_boxes.append([[expanded_x1, expanded_y1, expanded_x2, expanded_y2]])
293
+
294
+ print(f" Haupt-BBox: [{x1}, {y1}, {x2}, {y2}]")
295
+ print(f" Erweiterte BBox: [{expanded_x1}, {expanded_y1}, {expanded_x2}, {expanded_y2}]")
296
+ print(f" Anzahl BBox-Prompts: {len(input_boxes)}")
297
+ else:
298
+ # Standard für andere Modi
299
+ input_boxes = [[[x1, y1, x2, y2]]]
300
+ print(f" Standard-BBox: [{x1}, {y1}, {x2}, {y2}]")
301
+
302
  print(" Verarbeite Bild mit SAM 2 Processor...")
303
  inputs = self.sam_processor(
304
  image_np,
 
316
  print(f"✅ Vorhersage abgeschlossen")
317
  print(f" Anzahl der Vorhersagemasken: {outputs.pred_masks.shape[2]}")
318
 
319
+ # 5. Maske extrahieren
320
+ print("📏 SCHRITT 6: MASKE EXTRAHIEREN")
321
 
 
 
 
322
  num_masks = outputs.pred_masks.shape[2]
323
  print(f" SAM lieferte {num_masks} verschiedene Masken")
324
 
325
  # Extrahiere alle Masken
326
  all_masks = []
 
327
 
328
  for i in range(num_masks):
329
  single_mask = outputs.pred_masks[:, :, i, :, :]
 
343
  print(f" Maske {i+1}: Größe={mask_area:,} Pixel, Max-Konfidenz={mask_np.max():.3f}")
344
 
345
  # ============================================================
346
+ # MODUS-SPEZIFISCHE HEURISTIK
347
  # ============================================================
348
+ print("🤔 SCHRITT 6: MASKENAUSWAHL MIT MODUS-SPEZIFISCHER HEURISTIK")
349
 
 
350
  bbox_center = ((x1 + x2) // 2, (y1 + y2) // 2)
351
  bbox_area = (x2 - x1) * (y2 - y1)
352
  print(f" Erwartetes BBox-Zentrum: {bbox_center}")
 
356
  best_score = -1
357
 
358
  for i, mask_np in enumerate(all_masks):
359
+ mask_max = mask_np.max()
360
+
361
+ # Grundlegende Filterung
362
+ if mask_max < 0.3:
363
+ print(f" ❌ Maske {i+1}: Zu niedrige Konfidenz ({mask_max:.3f}), überspringe")
364
+ continue
365
+
366
+ # Adaptiver Threshold
367
+ adaptive_threshold = max(0.3, mask_max * 0.7)
368
+ mask_binary = (mask_np > adaptive_threshold).astype(np.uint8)
369
 
370
  if np.sum(mask_binary) == 0:
371
+ print(f" ❌ Maske {i+1}: Keine Pixel nach Threshold {adaptive_threshold:.3f}")
372
  continue
373
 
374
+ mask_area_pixels = np.sum(mask_binary)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
+ # ============================================================
377
+ # SPEZIALHEURISTIK NUR FÜR GESICHTSMODUS
378
+ # ============================================================
379
+ if mode == "face_only_change":
380
+ print(f" 🔍 Analysiere Maske {i+1} mit GESICHTS-HEURISTIK")
381
+
382
+ # 1. FLÄCHENBASIERTE BEWERTUNG (40%)
383
+ area_ratio = mask_area_pixels / bbox_area
384
+ print(f" 📐 Flächen-Ratio: {area_ratio:.3f} ({mask_area_pixels:,} / {bbox_area:,} Pixel)")
385
+
386
+ # Optimale Kopfgröße: 80-120% der BBox
387
+ if area_ratio < 0.6:
388
+ print(f" ⚠️ Fläche zu klein für Kopf (<60% der BBox)")
389
+ area_score = area_ratio * 0.5 # Stark bestrafen
390
+ elif area_ratio > 1.5:
391
+ print(f" ⚠️ Fläche zu groß für Kopf (>150% der BBox)")
392
+ area_score = 2.0 - area_ratio # Linear bestrafen
393
+ elif 0.8 <= area_ratio <= 1.2:
394
+ area_score = 1.0 # Perfekte Größe
395
+ print(f" ✅ Perfekte Kopfgröße (80-120% der BBox)")
396
+ else:
397
+ # Sanfte Abweichung
398
+ area_score = 1.0 - abs(area_ratio - 1.0) * 0.5
399
+
400
+ # 2. KOMPAKTHEIT/SOLIDITÄT (30%)
401
+ labeled_mask = measure.label(mask_binary)
402
+ regions = measure.regionprops(labeled_mask)
403
+
404
+ if len(regions) == 0:
405
+ compactness_score = 0.1
406
+ print(f" ❌ Keine zusammenhängenden Regionen gefunden")
407
+ else:
408
+ # Größte Region finden (sollte der Kopf sein)
409
+ largest_region = max(regions, key=lambda r: r.area)
410
+
411
+ # Solidität = Fläche / konvexe Hüllenfläche
412
+ solidity = largest_region.solidity if hasattr(largest_region, 'solidity') else 0.7
413
+
414
+ # Exzentrizität (wie elliptisch) - Köpfe sind tendenziell elliptisch
415
+ eccentricity = largest_region.eccentricity if hasattr(largest_region, 'eccentricity') else 0.5
416
+
417
+ # Perfekt runde Formen (Kreis) sind 0, Linie wäre 1
418
+ # Köpfe haben typischerweise 0.5-0.8
419
+ if 0.4 <= eccentricity <= 0.9:
420
+ eccentricity_score = 1.0 - abs(eccentricity - 0.65) * 2
421
+ else:
422
+ eccentricity_score = 0.2
423
+
424
+ compactness_score = (solidity * 0.6 + eccentricity_score * 0.4)
425
+ print(f" 🎯 Kompaktheits-Analyse:")
426
+ print(f" • Solidität (Fläche/Konvex): {solidity:.3f}")
427
+ print(f" • Exzentrizität (Form): {eccentricity:.3f}")
428
+ print(f" • Kompaktheits-Score: {compactness_score:.3f}")
429
+
430
+ # 3. BBOX-ÜBERLAPPUNG (20%)
431
+ bbox_mask = np.zeros((image.height, image.width), dtype=np.uint8)
432
+ bbox_mask[y1:y2, x1:x2] = 1
433
+ overlap = np.sum(mask_binary & bbox_mask)
434
+ bbox_overlap_ratio = overlap / mask_area_pixels if mask_area_pixels > 0 else 0
435
+
436
+ # Für Kopf: Sollte großteils in BBox sein (mind. 70%)
437
+ if bbox_overlap_ratio >= 0.7:
438
+ bbox_score = 1.0
439
+ print(f" ✅ Hohe BBox-Überlappung: {bbox_overlap_ratio:.3f} ({overlap:,} Pixel)")
440
+ elif bbox_overlap_ratio >= 0.5:
441
+ bbox_score = bbox_overlap_ratio * 1.2
442
+ print(f" ⚠️ Mittlere BBox-Überlappung: {bbox_overlap_ratio:.3f}")
443
+ else:
444
+ bbox_score = bbox_overlap_ratio * 0.8
445
+ print(f" ❌ Geringe BBox-Überlappung: {bbox_overlap_ratio:.3f}")
446
+
447
+ # 4. SAM-KONFIDENZ (10%)
448
+ confidence_score = mask_max
449
+
450
+ # GESAMTSCORE für Gesicht
451
+ score = (
452
+ area_score * 0.4 + # 40% Flächenpassung
453
+ compactness_score * 0.3 + # 30% Kompaktheit
454
+ bbox_score * 0.2 + # 20% BBox-Überlappung
455
+ confidence_score * 0.1 # 10% Konfidenz
456
+ )
457
+
458
+ print(f" 📊 GESICHTS-SCORES für Maske {i+1}:")
459
+ print(f" • Flächen-Score: {area_score:.3f}")
460
+ print(f" • Kompaktheits-Score: {compactness_score:.3f}")
461
+ print(f" • BBox-Überlappungs-Score: {bbox_score:.3f}")
462
+ print(f" • Konfidenz-Score: {confidence_score:.3f}")
463
+ print(f" • GESAMTSCORE: {score:.3f}")
464
 
465
+ # ============================================================
466
+ # STANDARD-HEURISTIK FÜR ANDERE MODI
467
+ # ============================================================
468
+ else:
469
+ # Standard Heuristik für focus_change und environment_change
470
+ bbox_mask = np.zeros((image.height, image.width), dtype=np.uint8)
471
+ bbox_mask[y1:y2, x1:x2] = 1
472
+
473
+ overlap = np.sum(mask_binary & bbox_mask)
474
+ bbox_overlap_ratio = overlap / np.sum(bbox_mask) if np.sum(bbox_mask) > 0 else 0
475
+
476
+ # Schwerpunkt berechnen
477
+ y_coords, x_coords = np.where(mask_binary > 0)
478
+ if len(y_coords) > 0:
479
+ centroid_y = np.mean(y_coords)
480
+ centroid_x = np.mean(x_coords)
481
+ centroid_distance = np.sqrt((centroid_x - bbox_center[0])**2 + (centroid_y - bbox_center[1])**2)
482
+ normalized_distance = centroid_distance / max(image.width, image.height)
483
+ else:
484
+ normalized_distance = 1.0
485
+
486
+ # Flächen-Ratio
487
+ area_ratio = mask_area_pixels / bbox_area
488
+ area_score = 1.0 - min(abs(area_ratio - 1.0), 1.0)
489
+
490
+ # Konfidenz
491
+ confidence_score = mask_max
492
+
493
+ # Standard-Score
494
+ score = (
495
+ bbox_overlap_ratio * 0.4 +
496
+ (1.0 - normalized_distance) * 0.25 +
497
+ area_score * 0.25 +
498
+ confidence_score * 0.1
499
+ )
500
+
501
+ print(f" 📊 STANDARD-SCORES für Maske {i+1}:")
502
+ print(f" • BBox-Überlappung: {bbox_overlap_ratio:.3f}")
503
+ print(f" • Zentrums-Distanz: {centroid_distance if 'centroid_distance' in locals() else 'N/A'}")
504
+ print(f" • Flächen-Ratio: {area_ratio:.3f}")
505
+ print(f" • GESAMTSCORE: {score:.3f}")
506
 
507
  if score > best_score:
508
  best_score = score
509
  best_mask_idx = i
510
+ print(f" 🏆 Neue beste Maske: Nr. {i+1} mit Score {score:.3f}")
511
 
512
  print(f"✅ Beste Maske ausgewählt: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
513
 
 
515
  mask_np = all_masks[best_mask_idx]
516
 
517
  # ============================================================
518
+ # OPTIMIERTER THRESHOLD
519
  # ============================================================
520
  max_val = mask_np.max()
521
  print(f" 🔍 Maximaler SAM-Konfidenzwert der besten Maske: {max_val:.3f}")
522
 
523
+ if mode == "face_only_change":
524
+ # Spezieller Threshold für Gesichter
525
+ if max_val < 0.5:
526
+ dynamic_threshold = 0.25
527
+ print(f" ⚠️ SAM ist unsicher für Gesicht (max_val={max_val:.3f} < 0.5)")
528
+ elif max_val < 0.8:
529
+ dynamic_threshold = max_val * 0.65 # Mittlerer Threshold
530
+ print(f" ℹ️ SAM ist mäßig sicher für Gesicht (max_val={max_val:.3f})")
531
+ else:
532
+ dynamic_threshold = max_val * 0.75 # Hoher Threshold
533
+ print(f" ✅ SAM ist sicher für Gesicht (max_val={max_val:.3f} >= 0.8)")
534
+
535
+ print(f" 🎯 Gesichts-Threshold: {dynamic_threshold:.3f}")
536
  else:
537
+ # Standard Threshold
538
+ if max_val < 0.6:
539
+ dynamic_threshold = 0.3
540
+ print(f" ⚠️ SAM ist unsicher (max_val={max_val:.3f} < 0.6)")
541
+ else:
542
+ dynamic_threshold = max_val * 0.8
543
+ print(f" ✅ SAM ist sicher (max_val={max_val:.3f} >= 0.6)")
544
+
545
+ print(f" 🎯 Standard-Threshold: {dynamic_threshold:.3f}")
546
 
547
  mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255
 
 
548
 
549
  # ============================================================
550
+ # MODUS-SPEZIFISCHES POSTPROCESSING
551
  # ============================================================
552
+ print("🔧 SCHRITT 7: MODUS-SPEZIFISCHES POSTPROCESSING")
553
 
554
+ if mode == "face_only_change":
555
+ print("👤 GESICHTS-SPEZIFISCHES POSTPROCESSING")
556
+
557
+ # 1. Größte zusammenhängende Komponente finden (sollte der Kopf sein)
 
 
 
 
 
 
 
 
 
 
 
 
 
558
  labeled_array, num_features = ndimage.label(mask_array)
559
+
560
+ if num_features > 0:
561
+ print(f" 🔍 Gefundene Komponenten: {num_features}")
562
 
563
  sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
564
+ largest_component_idx = np.argmax(sizes) + 1
 
565
 
566
+ print(f" 👑 Größte Komponente: Nr. {largest_component_idx} mit {sizes[largest_component_idx-1]:,} Pixel")
 
567
 
568
+ # NUR die größte Komponente behalten (der Kopf)
569
+ mask_array = np.where(labeled_array == largest_component_idx, mask_array, 0)
570
+
571
+ # 2. FORMBASIERTE OPTIMIERUNG FÜR KOPF
572
+ print(" 🎯 Formbasierte Optimierung für Kopf")
573
+
574
+ # Hole die Region-Eigenschaften für die größte Komponente
575
+ labeled_single = np.where(labeled_array == largest_component_idx, 1, 0).astype(np.uint8)
576
+ regions = measure.regionprops(labeled_single)
577
+
578
+ if regions:
579
+ region = regions[0]
580
+
581
+ # Erweiterte Bounding Box für Kopf (etwas größer)
582
+ minr, minc, maxr, maxc = region.bbox
583
+ head_bbox_height = maxr - minr
584
+ head_bbox_width = maxc - minc
585
+
586
+ # Kopf sollte etwa 1.2-1.5 mal höher als breit sein
587
+ aspect_ratio = head_bbox_height / head_bbox_width if head_bbox_width > 0 else 1.0
588
+
589
+ print(f" 📏 Kopf-BBox: {head_bbox_width}×{head_bbox_height} (Ratio: {aspect_ratio:.2f})")
590
+
591
+ # Wenn der Kopf zu "flach" ist (z.B. nur Haare), vertikal erweitern
592
+ if aspect_ratio < 1.0 and head_bbox_height < bbox_height * 0.8:
593
+ print(f" ⬇️ Kopf zu flach, vertikal erweitern")
594
+ expand_y = int((bbox_height * 0.8 - head_bbox_height) / 2)
595
+ minr = max(0, minr - expand_y)
596
+ maxr = min(mask_array.shape[0], maxr + expand_y)
597
+
598
+ # Fülle den erweiterten Bereich
599
+ mask_array[minr:maxr, minc:maxc] = 255
600
+
601
+ # 3. MORPHOLOGISCHE OPERATIONEN FÜR SAUBEREN KOPF
602
+ print(" ⚙️ Morphologische Operationen für sauberen Kopf")
603
+
604
+ # Zuerst CLOSE, um kleine Löcher im Kopf zu füllen
605
+ kernel_close = np.ones((7, 7), np.uint8)
606
+ mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel_close, iterations=1)
607
+ print(" • MORPH_CLOSE (7x7) - Löcher im Kopf füllen")
608
+
609
+ # Dann OPEN, um kleine Ausreißer zu entfernen
610
+ kernel_open = np.ones((5, 5), np.uint8)
611
+ mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel_open, iterations=1)
612
+ print(" • MORPH_OPEN (5x5) - Rauschen entfernen")
613
+
614
+ # Sanfte Glättung der Kanten
615
+ mask_array = cv2.GaussianBlur(mask_array, (5, 5), 1.0)
616
+ mask_array = (mask_array > 127).astype(np.uint8) * 255
617
+ print(" • GaussianBlur + Re-Threshold - Glatte Kanten")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
 
619
+ # 4. MASKE ZURÜCK AUF ORIGINALGRÖSSE (nur für face_only_change)
 
 
620
  print("-" * 60)
621
  print("🔄 MASKE VOM AUSSCHNITT ZURÜCK AUF ORIGINALGRÖSSE")
622
 
 
623
  temp_mask = Image.fromarray(mask_array).convert("L")
624
  print(f" Maskengröße auf Ausschnitt: {temp_mask.size}")
625
 
 
626
  final_mask = Image.new("L", original_image.size, 0)
627
  print(f" Leere Maske in Originalgröße: {final_mask.size}")
628
 
 
629
  final_mask.paste(temp_mask, (crop_x1, crop_y1))
630
  print(f" Maskenposition im Original: ({crop_x1}, {crop_y1})")
631
 
 
632
  mask_array = np.array(final_mask)
633
  print(f" ✅ Maske zurück auf Originalgröße skaliert: {mask_array.shape}")
634
 
 
635
  image = original_image
636
  print(f" 🔄 Bild-Referenz wieder auf Original gesetzt: {image.size}")
637
+
638
+ elif mode == "focus_change":
639
+ print("🎯 FOCUS-CHANGE POSTPROCESSING")
640
+ mask_array = mask_array.copy()
641
+
642
+ # Größte weiße Komponente behalten (Person)
643
+ labeled_array, num_features = ndimage.label(mask_array)
644
+ if num_features > 1:
645
+ sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
646
+ largest_component = np.argmax(sizes) + 1
647
+ mask_array = np.where(labeled_array == largest_component, mask_array, 0)
648
+ print(f" ✅ Behalte größte Person-Komponente ({num_features} → 1 Komponente)")
649
+
650
+ # Maske leicht erweitern für bessere Abdeckung
651
+ kernel = np.ones((3,3), np.uint8)
652
+ mask_array = cv2.dilate(mask_array, kernel, iterations=1)
653
+ print(" ✅ Dilation für bessere Personenabdeckung")
654
+
655
+ elif mode == "environment_change":
656
+ print("🌳 ENVIRONMENT-CHANGE POSTPROCESSING")
657
+ mask_array = 255 - mask_array # Invertiere Maske
658
+ print(" ✅ Maske invertiert (Person schwarz, Hintergrund weiß)")
659
+
660
+ # Morphologische Operationen für saubere Umgebung
661
+ kernel = np.ones((5,5), np.uint8)
662
+ mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
663
+ print(" ✅ MORPH_CLOSE für zusammenhängende Umgebung")
664
 
665
+ # QUALITÄTSKONTROLLE
666
  white_pixels = np.sum(mask_array > 127)
667
  total_pixels = mask_array.size
668
  white_ratio = white_pixels / total_pixels * 100
 
 
669
 
670
  print("-" * 60)
671
  print("📊 MASKEN-STATISTIK (FINAL)")
672
  print(f" Weiße Pixel (Veränderungsbereich): {white_pixels:,} ({white_ratio:.1f}%)")
673
+ print(f" Schwarze Pixel (Erhaltungsbereich): {total_pixels-white_pixels:,} ({100-white_ratio:.1f}%)")
674
  print(f" Gesamtpixel: {total_pixels:,}")
675
 
676
  if mode == "face_only_change":
 
677
  original_face_area = original_bbox_size[0] * original_bbox_size[1]
678
  coverage_ratio = white_pixels / original_face_area if original_face_area > 0 else 0
679
+ print(f" 👤 GESICHTSABDECKUNG: {coverage_ratio:.1%} der ursprünglichen BBox")
680
+
681
+ # Warnungen basierend auf Abdeckung
682
+ if coverage_ratio < 0.7:
683
+ print(f" ⚠️ WARNUNG: Geringe Gesichtsabdeckung ({coverage_ratio:.1%})")
684
+ print(f" 💡 Tipp: BBox könnte zu groß sein oder SAM erkennt Gesicht nicht vollständig")
685
+ elif coverage_ratio > 1.3:
686
+ print(f" ⚠️ WARNUNG: Sehr hohe Gesichtsabdeckung ({coverage_ratio:.1%})")
687
+ print(f" 💡 Tipp: Maske könnte zu viel Hintergrund enthalten")
688
+ elif 0.8 <= coverage_ratio <= 1.2:
689
+ print(f" ✅ OPTIMALE Gesichtsabdeckung ({coverage_ratio:.1%})")
690
+
691
+ # Zurück zu PIL Image
692
  mask = Image.fromarray(mask_array).convert("L")
693
 
694
  print("#" * 80)
695
  print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
696
  print(f"📐 Finale Maskengröße: {mask.size}")
697
  print(f"🎛️ Verwendeter Modus: {mode}")
698
+
699
+ if mode == "face_only_change" and crop_size is not None:
700
+ print(f"👤 Bei face_only_change: Crop={crop_size}×{crop_size}px, Heuristik-Score={best_score:.3f}")
701
+ print(f"👤 Kopfabdeckung: {coverage_ratio:.1%} der BBox")
702
+
703
  print("#" * 80)
704
  return mask
705
 
 
712
  traceback.print_exc()
713
  print("ℹ️ Fallback auf rechteckige Maske")
714
  return self._create_rectangular_mask(image, bbox_coords, mode)
715
+
716
 
717
  def _create_rectangular_mask(self, image, bbox_coords, mode):
718
  """Fallback: Erstellt rechteckige Maske"""