Update controlnet_module.py
Browse files- controlnet_module.py +53 -78
controlnet_module.py
CHANGED
|
@@ -103,11 +103,10 @@ class ControlNetProcessor:
|
|
| 103 |
print(f"⚠️ Fehler beim Glätten der Maske: {e}")
|
| 104 |
return mask_array
|
| 105 |
|
| 106 |
-
|
| 107 |
def create_sam_mask(self, image, bbox_coords, mode):
|
| 108 |
"""
|
| 109 |
ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2
|
| 110 |
-
|
| 111 |
"""
|
| 112 |
try:
|
| 113 |
print("#" * 80)
|
|
@@ -170,7 +169,7 @@ class ControlNetProcessor:
|
|
| 170 |
print(f" 📏 BBox Dimensionen: {bbox_width} × {bbox_height} px")
|
| 171 |
print(f" 📐 Maximale BBox-Dimension: {bbox_max_dim} px")
|
| 172 |
|
| 173 |
-
#
|
| 174 |
crop_size = int(bbox_max_dim * 2.5)
|
| 175 |
print(f" 🎯 Ziel-Crop-Größe: {crop_size} × {crop_size} px (BBox × 2.5)")
|
| 176 |
|
|
@@ -265,41 +264,18 @@ class ControlNetProcessor:
|
|
| 265 |
print(f" BBox Koordinaten: [{x1}, {y1}, {x2}, {y2}]")
|
| 266 |
print(f" BBox Dimensionen: {x2-x1}px × {y2-y1}px")
|
| 267 |
|
| 268 |
-
# 3. Vorbereitung für SAM2
|
| 269 |
print("-" * 60)
|
| 270 |
print("🖼️ BILDAUFBEREITUNG FÜR SAM 2")
|
| 271 |
image_np = np.array(image.convert("RGB"))
|
| 272 |
|
| 273 |
-
#
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
print("
|
| 277 |
-
|
| 278 |
-
bbox_width = x2 - x1
|
| 279 |
-
bbox_height = y2 - y1
|
| 280 |
-
|
| 281 |
-
# Für Gesichtsmodus: Verstärkte BBox-Prompts
|
| 282 |
-
if mode == "face_only_change":
|
| 283 |
-
# 1. Haupt-BBox (ursprüngliche Koordinaten)
|
| 284 |
-
input_boxes = [[[x1, y1, x2, y2]]]
|
| 285 |
-
|
| 286 |
-
# 2. ERWEITERTE BBox für Gesichtskontext (15% größer)
|
| 287 |
-
expand_factor = 0.15
|
| 288 |
-
expanded_x1 = max(0, int(x1 - bbox_width * expand_factor))
|
| 289 |
-
expanded_y1 = max(0, int(y1 - bbox_height * expand_factor))
|
| 290 |
-
expanded_x2 = min(image.width, int(x2 + bbox_width * expand_factor))
|
| 291 |
-
expanded_y2 = min(image.height, int(y2 + bbox_height * expand_factor))
|
| 292 |
-
|
| 293 |
-
input_boxes.append([[expanded_x1, expanded_y1, expanded_x2, expanded_y2]])
|
| 294 |
-
|
| 295 |
-
print(f" Haupt-BBox: [{x1}, {y1}, {x2}, {y2}]")
|
| 296 |
-
print(f" Erweiterte BBox: [{expanded_x1}, {expanded_y1}, {expanded_x2}, {expanded_y2}]")
|
| 297 |
-
print(f" Anzahl BBox-Prompts: {len(input_boxes)}")
|
| 298 |
-
else:
|
| 299 |
-
# Standard für andere Modi
|
| 300 |
-
input_boxes = [[[x1, y1, x2, y2]]]
|
| 301 |
-
print(f" Standard-BBox: [{x1}, {y1}, {x2}, {y2}]")
|
| 302 |
|
|
|
|
| 303 |
print(" Verarbeite Bild mit SAM 2 Processor...")
|
| 304 |
inputs = self.sam_processor(
|
| 305 |
image_np,
|
|
@@ -555,7 +531,7 @@ class ControlNetProcessor:
|
|
| 555 |
if mode == "face_only_change":
|
| 556 |
print("👤 GESICHTS-SPEZIFISCHES POSTPROCESSING")
|
| 557 |
|
| 558 |
-
# 1. Größte zusammenhängende Komponente finden
|
| 559 |
labeled_array, num_features = ndimage.label(mask_array)
|
| 560 |
|
| 561 |
if num_features > 0:
|
|
@@ -569,37 +545,7 @@ class ControlNetProcessor:
|
|
| 569 |
# NUR die größte Komponente behalten (der Kopf)
|
| 570 |
mask_array = np.where(labeled_array == largest_component_idx, mask_array, 0)
|
| 571 |
|
| 572 |
-
# 2.
|
| 573 |
-
print(" 🎯 Formbasierte Optimierung für Kopf")
|
| 574 |
-
|
| 575 |
-
# Hole die Region-Eigenschaften für die größte Komponente
|
| 576 |
-
labeled_single = np.where(labeled_array == largest_component_idx, 1, 0).astype(np.uint8)
|
| 577 |
-
regions = measure.regionprops(labeled_single)
|
| 578 |
-
|
| 579 |
-
if regions:
|
| 580 |
-
region = regions[0]
|
| 581 |
-
|
| 582 |
-
# Erweiterte Bounding Box für Kopf (etwas größer)
|
| 583 |
-
minr, minc, maxr, maxc = region.bbox
|
| 584 |
-
head_bbox_height = maxr - minr
|
| 585 |
-
head_bbox_width = maxc - minc
|
| 586 |
-
|
| 587 |
-
# Kopf sollte etwa 1.2-1.5 mal höher als breit sein
|
| 588 |
-
aspect_ratio = head_bbox_height / head_bbox_width if head_bbox_width > 0 else 1.0
|
| 589 |
-
|
| 590 |
-
print(f" 📏 Kopf-BBox: {head_bbox_width}×{head_bbox_height} (Ratio: {aspect_ratio:.2f})")
|
| 591 |
-
|
| 592 |
-
# Wenn der Kopf zu "flach" ist (z.B. nur Haare), vertikal erweitern
|
| 593 |
-
if aspect_ratio < 1.0 and head_bbox_height < bbox_height * 0.8:
|
| 594 |
-
print(f" ⬇️ Kopf zu flach, vertikal erweitern")
|
| 595 |
-
expand_y = int((bbox_height * 0.8 - head_bbox_height) / 2)
|
| 596 |
-
minr = max(0, minr - expand_y)
|
| 597 |
-
maxr = min(mask_array.shape[0], maxr + expand_y)
|
| 598 |
-
|
| 599 |
-
# Fülle den erweiterten Bereich
|
| 600 |
-
mask_array[minr:maxr, minc:maxc] = 255
|
| 601 |
-
|
| 602 |
-
# 3. MORPHOLOGISCHE OPERATIONEN FÜR SAUBEREN KOPF
|
| 603 |
print(" ⚙️ Morphologische Operationen für sauberen Kopf")
|
| 604 |
|
| 605 |
# Zuerst CLOSE, um kleine Löcher im Kopf zu füllen
|
|
@@ -611,33 +557,49 @@ class ControlNetProcessor:
|
|
| 611 |
kernel_open = np.ones((5, 5), np.uint8)
|
| 612 |
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel_open, iterations=1)
|
| 613 |
print(" • MORPH_OPEN (5x5) - Rauschen entfernen")
|
| 614 |
-
|
| 615 |
-
# Sanfte Glättung der Kanten
|
| 616 |
-
mask_array = cv2.GaussianBlur(mask_array, (5, 5), 1.0)
|
| 617 |
-
mask_array = (mask_array > 127).astype(np.uint8) * 255
|
| 618 |
-
print(" • GaussianBlur + Re-Threshold - Glatte Kanten")
|
| 619 |
|
| 620 |
-
#
|
|
|
|
|
|
|
| 621 |
print("-" * 60)
|
| 622 |
-
print("🔄 MASKE
|
| 623 |
|
|
|
|
| 624 |
temp_mask = Image.fromarray(mask_array).convert("L")
|
| 625 |
print(f" Maskengröße auf Ausschnitt: {temp_mask.size}")
|
| 626 |
|
|
|
|
| 627 |
final_mask = Image.new("L", original_image.size, 0)
|
| 628 |
print(f" Leere Maske in Originalgröße: {final_mask.size}")
|
| 629 |
|
| 630 |
-
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
mask_array = np.array(final_mask)
|
| 634 |
print(f" ✅ Maske zurück auf Originalgröße skaliert: {mask_array.shape}")
|
| 635 |
|
|
|
|
| 636 |
image = original_image
|
| 637 |
print(f" 🔄 Bild-Referenz wieder auf Original gesetzt: {image.size}")
|
| 638 |
|
| 639 |
elif mode == "focus_change":
|
| 640 |
print("🎯 FOCUS-CHANGE POSTPROCESSING")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
mask_array = mask_array.copy()
|
| 642 |
|
| 643 |
# Größte weiße Komponente behalten (Person)
|
|
@@ -655,6 +617,13 @@ class ControlNetProcessor:
|
|
| 655 |
|
| 656 |
elif mode == "environment_change":
|
| 657 |
print("🌳 ENVIRONMENT-CHANGE POSTPROCESSING")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
mask_array = 255 - mask_array # Invertiere Maske
|
| 659 |
print(" ✅ Maske invertiert (Person schwarz, Hintergrund weiß)")
|
| 660 |
|
|
@@ -682,10 +651,8 @@ class ControlNetProcessor:
|
|
| 682 |
# Warnungen basierend auf Abdeckung
|
| 683 |
if coverage_ratio < 0.7:
|
| 684 |
print(f" ⚠️ WARNUNG: Geringe Gesichtsabdeckung ({coverage_ratio:.1%})")
|
| 685 |
-
print(f" 💡 Tipp: BBox könnte zu groß sein oder SAM erkennt Gesicht nicht vollständig")
|
| 686 |
elif coverage_ratio > 1.3:
|
| 687 |
print(f" ⚠️ WARNUNG: Sehr hohe Gesichtsabdeckung ({coverage_ratio:.1%})")
|
| 688 |
-
print(f" 💡 Tipp: Maske könnte zu viel Hintergrund enthalten")
|
| 689 |
elif 0.8 <= coverage_ratio <= 1.2:
|
| 690 |
print(f" ✅ OPTIMALE Gesichtsabdeckung ({coverage_ratio:.1%})")
|
| 691 |
|
|
@@ -711,9 +678,17 @@ class ControlNetProcessor:
|
|
| 711 |
print(f"Fehler: {str(e)[:200]}")
|
| 712 |
import traceback
|
| 713 |
traceback.print_exc()
|
|
|
|
|
|
|
| 714 |
print("ℹ️ Fallback auf rechteckige Maske")
|
| 715 |
-
|
| 716 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
|
| 718 |
def _create_rectangular_mask(self, image, bbox_coords, mode):
|
| 719 |
"""Fallback: Erstellt rechteckige Maske"""
|
|
|
|
| 103 |
print(f"⚠️ Fehler beim Glätten der Maske: {e}")
|
| 104 |
return mask_array
|
| 105 |
|
|
|
|
| 106 |
def create_sam_mask(self, image, bbox_coords, mode):
|
| 107 |
"""
|
| 108 |
ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2
|
| 109 |
+
Korrigierte Version für face_only_change mit einzelner BBox
|
| 110 |
"""
|
| 111 |
try:
|
| 112 |
print("#" * 80)
|
|
|
|
| 169 |
print(f" 📏 BBox Dimensionen: {bbox_width} × {bbox_height} px")
|
| 170 |
print(f" 📐 Maximale BBox-Dimension: {bbox_max_dim} px")
|
| 171 |
|
| 172 |
+
# Crop-Größe berechnen (BBox × 2.5)
|
| 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 |
|
|
|
|
| 264 |
print(f" BBox Koordinaten: [{x1}, {y1}, {x2}, {y2}]")
|
| 265 |
print(f" BBox Dimensionen: {x2-x1}px × {y2-y1}px")
|
| 266 |
|
| 267 |
+
# 3. Vorbereitung für SAM2 - WICHTIG: NUR EINE BBOX
|
| 268 |
print("-" * 60)
|
| 269 |
print("🖼️ BILDAUFBEREITUNG FÜR SAM 2")
|
| 270 |
image_np = np.array(image.convert("RGB"))
|
| 271 |
|
| 272 |
+
# KORREKTUR: Immer nur eine BBox verwenden (SAM 2 erwartet genau 1)
|
| 273 |
+
input_boxes = [[[x1, y1, x2, y2]]]
|
| 274 |
+
print(f" Konvertiere Bild zu NumPy Array: {image_np.shape}")
|
| 275 |
+
print(f" Erstelle EINZIGE Input Box: {input_boxes}")
|
| 276 |
+
print(" ℹ️ SAM 2 erwartet genau eine BBox pro Vorhersage")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
+
print("🎯 SCHRITT 4-5: SAM MIT BOX-PROMPT")
|
| 279 |
print(" Verarbeite Bild mit SAM 2 Processor...")
|
| 280 |
inputs = self.sam_processor(
|
| 281 |
image_np,
|
|
|
|
| 531 |
if mode == "face_only_change":
|
| 532 |
print("👤 GESICHTS-SPEZIFISCHES POSTPROCESSING")
|
| 533 |
|
| 534 |
+
# 1. Größte zusammenhängende Komponente finden
|
| 535 |
labeled_array, num_features = ndimage.label(mask_array)
|
| 536 |
|
| 537 |
if num_features > 0:
|
|
|
|
| 545 |
# NUR die größte Komponente behalten (der Kopf)
|
| 546 |
mask_array = np.where(labeled_array == largest_component_idx, mask_array, 0)
|
| 547 |
|
| 548 |
+
# 2. MORPHOLOGISCHE OPERATIONEN FÜR SAUBEREN KOPF
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
print(" ⚙️ Morphologische Operationen für sauberen Kopf")
|
| 550 |
|
| 551 |
# Zuerst CLOSE, um kleine Löcher im Kopf zu füllen
|
|
|
|
| 557 |
kernel_open = np.ones((5, 5), np.uint8)
|
| 558 |
mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel_open, iterations=1)
|
| 559 |
print(" • MORPH_OPEN (5x5) - Rauschen entfernen")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
+
# ============================================================
|
| 562 |
+
# KRITISCH: MASKE IMMER ZURÜCK AUF ORIGINALGRÖSSE (auch bei Fallback!)
|
| 563 |
+
# ============================================================
|
| 564 |
print("-" * 60)
|
| 565 |
+
print("🔄 MASKE IMMER ZURÜCK AUF ORIGINALGRÖSSE TRANSFORMIEREN")
|
| 566 |
|
| 567 |
+
# WICHTIG: Immer die richtigen Crop-Koordinaten verwenden
|
| 568 |
temp_mask = Image.fromarray(mask_array).convert("L")
|
| 569 |
print(f" Maskengröße auf Ausschnitt: {temp_mask.size}")
|
| 570 |
|
| 571 |
+
# Maske auf ORIGINALBILDGRÖSSE bringen
|
| 572 |
final_mask = Image.new("L", original_image.size, 0)
|
| 573 |
print(f" Leere Maske in Originalgröße: {final_mask.size}")
|
| 574 |
|
| 575 |
+
# Immer die gespeicherten Crop-Koordinaten verwenden
|
| 576 |
+
if crop_x1 is not None and crop_y1 is not None:
|
| 577 |
+
final_mask.paste(temp_mask, (crop_x1, crop_y1))
|
| 578 |
+
print(f" Maskenposition im Original: ({crop_x1}, {crop_y1})")
|
| 579 |
+
else:
|
| 580 |
+
# Fallback: Zentrieren
|
| 581 |
+
x_offset = (original_image.width - temp_mask.width) // 2
|
| 582 |
+
y_offset = (original_image.height - temp_mask.height) // 2
|
| 583 |
+
final_mask.paste(temp_mask, (x_offset, y_offset))
|
| 584 |
+
print(f" ⚠️ Keine Crop-Koordinaten, zentriert: ({x_offset}, {y_offset})")
|
| 585 |
|
| 586 |
mask_array = np.array(final_mask)
|
| 587 |
print(f" ✅ Maske zurück auf Originalgröße skaliert: {mask_array.shape}")
|
| 588 |
|
| 589 |
+
# Bild-Referenz zurücksetzen
|
| 590 |
image = original_image
|
| 591 |
print(f" 🔄 Bild-Referenz wieder auf Original gesetzt: {image.size}")
|
| 592 |
|
| 593 |
elif mode == "focus_change":
|
| 594 |
print("🎯 FOCUS-CHANGE POSTPROCESSING")
|
| 595 |
+
|
| 596 |
+
# Für focus_change: Originalbildgröße beibehalten
|
| 597 |
+
if image.size != original_image.size:
|
| 598 |
+
print(f" ⚠️ Bildgröße angepasst: {image.size} → {original_image.size}")
|
| 599 |
+
temp_mask = Image.fromarray(mask_array).convert("L")
|
| 600 |
+
temp_mask = temp_mask.resize(original_image.size, Image.Resampling.NEAREST)
|
| 601 |
+
mask_array = np.array(temp_mask)
|
| 602 |
+
|
| 603 |
mask_array = mask_array.copy()
|
| 604 |
|
| 605 |
# Größte weiße Komponente behalten (Person)
|
|
|
|
| 617 |
|
| 618 |
elif mode == "environment_change":
|
| 619 |
print("🌳 ENVIRONMENT-CHANGE POSTPROCESSING")
|
| 620 |
+
|
| 621 |
+
# Für environment_change: Originalbildgröße beibehalten
|
| 622 |
+
if image.size != original_image.size:
|
| 623 |
+
temp_mask = Image.fromarray(mask_array).convert("L")
|
| 624 |
+
temp_mask = temp_mask.resize(original_image.size, Image.Resampling.NEAREST)
|
| 625 |
+
mask_array = np.array(temp_mask)
|
| 626 |
+
|
| 627 |
mask_array = 255 - mask_array # Invertiere Maske
|
| 628 |
print(" ✅ Maske invertiert (Person schwarz, Hintergrund weiß)")
|
| 629 |
|
|
|
|
| 651 |
# Warnungen basierend auf Abdeckung
|
| 652 |
if coverage_ratio < 0.7:
|
| 653 |
print(f" ⚠️ WARNUNG: Geringe Gesichtsabdeckung ({coverage_ratio:.1%})")
|
|
|
|
| 654 |
elif coverage_ratio > 1.3:
|
| 655 |
print(f" ⚠️ WARNUNG: Sehr hohe Gesichtsabdeckung ({coverage_ratio:.1%})")
|
|
|
|
| 656 |
elif 0.8 <= coverage_ratio <= 1.2:
|
| 657 |
print(f" ✅ OPTIMALE Gesichtsabdeckung ({coverage_ratio:.1%})")
|
| 658 |
|
|
|
|
| 678 |
print(f"Fehler: {str(e)[:200]}")
|
| 679 |
import traceback
|
| 680 |
traceback.print_exc()
|
| 681 |
+
|
| 682 |
+
# WICHTIG: Im Fallback immer die richtige Größe zurückgeben
|
| 683 |
print("ℹ️ Fallback auf rechteckige Maske")
|
| 684 |
+
fallback_mask = self._create_rectangular_mask(original_image, original_bbox, mode)
|
| 685 |
+
|
| 686 |
+
# Sicherstellen, dass die Maske die richtige Größe hat
|
| 687 |
+
if fallback_mask.size != original_image.size:
|
| 688 |
+
print(f" ⚠️ Fallback-Maske angepasst: {fallback_mask.size} → {original_image.size}")
|
| 689 |
+
fallback_mask = fallback_mask.resize(original_image.size, Image.Resampling.NEAREST)
|
| 690 |
+
|
| 691 |
+
return fallback_mask
|
| 692 |
|
| 693 |
def _create_rectangular_mask(self, image, bbox_coords, mode):
|
| 694 |
"""Fallback: Erstellt rechteckige Maske"""
|