Spaces:
Sleeping
Sleeping
| 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.8 | |
| 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() | |
| 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: | |
| print(f" ⚠️ Bildgröße angepasst: {image.size} → {original_image.size}") | |
| temp_mask = Image.fromarray(mask_array).convert("L") | |
| temp_mask = temp_mask.resize(original_image.size, Image.Resampling.NEAREST) | |
| mask_array = np.array(temp_mask) | |
| print(f" ✅ Maske auf Originalgröße skaliert: {mask_array.shape}") | |
| # Maske invertieren (Person wird schwarz, Hintergrund weiß) | |
| mask_array = 255 - mask_array | |
| print(" ✅ Maske invertiert (Person schwarz, Hintergrund weiß)") | |
| # Weiße Punkte in der Person (schwarz) entfernen | |
| print("🧹 Entferne weiße Punkte in der Person...") | |
| kernel_open = np.ones((3, 3), np.uint8) | |
| mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel_open, iterations=3) | |
| print(" ✅ MORPH_OPEN entfernt weiße Punkte in der Person") | |
| # DEBUG nach MORPH_OPEN | |
| print(f" Nach MORPH_OPEN - Weiße Pixel: {np.sum(mask_array > 127)}") | |
| # Morphologische Operationen für saubere Umgebung - entfernt schwarze Pixel aus Umgebung | |
| print("🔧 Verbessere Umgebungsmaske...") | |
| kernel_close = np.ones((5, 5), np.uint8) | |
| mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel_close) | |
| print(" ✅ MORPH_CLOSE für zusammenhängende Umgebung") | |
| # DEBUG nach MORPH_CLOSE | |
| print(f" Nach MORPH_CLOSE - Weiße Pixel: {np.sum(mask_array > 127)}") | |
| # Weiche Ränder für bessere Integration der Person | |
| print("🌈 Erstelle weiche Übergänge...") | |
| mask_array = cv2.GaussianBlur(mask_array, (9, 9), 2.0) #2.0 bestimmt wie stark die Unschärfe ist | |
| print(" ✅ Gaussian Blur für weiche Übergänge") | |
| # DEBUG nach Gaussian Blur | |
| print(f" Nach Gaussian Blur - Min/Max: {mask_array.min()}/{mask_array.max()}") | |
| print(f" Nach Gaussian Blur - dtype: {mask_array.dtype}") | |
| # Gamma-Korrektur für präzisere Ränder | |
| print("🎛️ Wende Gamma-Korrektur an...") | |
| mask_array = mask_array.astype(np.float32) / 255.0 | |
| print(f" Konvertiert zu Float32: Min={mask_array.min():.3f}, Max={mask_array.max():.3f}") | |
| mask_array = np.clip(mask_array, 0.0, 1.0) #begrenzt alle Werte auf 0 und 1 | |
| mask_array = mask_array ** 0.85 # Gamma-Korrektur Werte > 0.5 werden abgedunkelt, <0.5 aufgehellt-erzeugt natürliche Maskenübergänge | |
| print(f" Nach Gamma 0.85: Min={mask_array.min():.3f}, Max={mask_array.max():.3f}") | |
| mask_array = (mask_array * 255).astype(np.uint8) | |
| print(" ✅ Gamma-Korrektur (0.85) gegen milchige Ränder") | |
| # FINALE QUALITÄTSKONTROLLE | |
| print("-" * 60) | |
| print("📊 FINALE MASKEN-STATISTIK (ENVIRONMENT_CHANGE)") | |
| white_pixels = np.sum(mask_array > 127) | |
| black_pixels = np.sum(mask_array <= 127) | |
| total_pixels = mask_array.size | |
| white_ratio = white_pixels / total_pixels * 100 | |
| black_ratio = black_pixels / total_pixels * 100 | |
| print(f" Weiße Pixel (HINTERGRUND - Veränderung): {white_pixels:,} ({white_ratio:.1f}%)") | |
| print(f" Schwarze Pixel (PERSON - Erhaltung): {black_pixels:,} ({black_ratio:.1f}%)") | |
| print(f" Gesamtpixel: {total_pixels:,}") | |
| # Warnungen basierend auf Verhältnis | |
| if white_ratio < 30: | |
| print(f" ⚠️ WARNUNG: Sehr wenig Hintergrund ({white_ratio:.1f}%)") | |
| print(f" ℹ️ Das könnte bedeuten, dass die Person zu groß segmentiert wurde") | |
| elif white_ratio > 90: | |
| print(f" ⚠️ WARNUNG: Sehr viel Hintergrund ({white_ratio:.1f}%)") | |
| print(f" ℹ️ Das könnte bedeuten, dass die Person zu klein segmentiert wurde") | |
| elif 50 <= white_ratio <= 80: | |
| print(f" ✅ OPTIMALES Verhältnis ({white_ratio:.1f}%)") | |
| else: | |
| print(f" ℹ️ Normales Verhältnis ({white_ratio:.1f}%)") | |
| # Zurück zu PIL Image | |
| mask = Image.fromarray(mask_array).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. In app.py wird mask immer auf 512 skaliert. | |
| # ============================================================ | |
| # BLOCK 2: FOCUS_CHANGE | |
| # ============================================================ | |
| elif mode == "focus_change": | |
| print("-" * 60) | |
| print("🎯 MODUS: FOCUS_CHANGE (OPTIMIERT)") | |
| print("-" * 60) | |
| # 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, :, :] | |
| 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}") | |
| # NUR DIE BESTE MASKE AUF 512x512 HERUNTERSKALIEREN -Für Inpaint | |
| best_mask_256 = outputs.pred_masks[:, :, best_mask_idx, :, :] | |
| resized_mask = F.interpolate( | |
| best_mask_256, | |
| size=(512, 512), # DIREKT AUF CONTROLNET-ZIELGRÖßE | |
| mode='bilinear', | |
| align_corners=False | |
| ).squeeze() | |
| mask_np = resized_mask.cpu().numpy() | |
| print(f" 🔄 Beste Maske skaliert auf 512×512 für ControlNet") | |
| # ============================================================ | |
| # 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.7: # Schlechte Maskenqualität | |
| dynamic_threshold = 0.05 # SEHR NIEDRIG für maximale Abdeckung | |
| print(f" ⚠️ Masken-Score niedrig ({best_score:.3f}). " | |
| f"Threshold=0.05 für maximale Abdeckung") | |
| else: | |
| dynamic_threshold = max(0.15, mask_max * 0.3) # Moderater Threshold | |
| print(f" ✅ Gute Maske. Threshold={dynamic_threshold:.3f}") | |
| # Binärmaske erstellen (512x512) | |
| 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) | |
| # BBox auf 512x512 skalieren für Fallback | |
| 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) | |
| cv2.rectangle(mask_array, (fb_x1, fb_y1), (fb_x2, fb_y2), 255, -1) #weiße Rechteckbox | |
| # Damit wird die Rohmaske für die UI-Anzeige gespeichert | |
| raw_mask_array = mask_array.copy() | |
| # FOCUS_CHANGE POSTPROCESSING (angepasst für 512x512) | |
| print("🔧 FOCUS_CHANGE POSTPROCESSING (auf 512×512)") | |
| 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) | |
| # 5. Auf Originalgröße für Rückgabe (falls benötigt) | |
| mask_512 = Image.fromarray(mask_array).convert("L") | |
| raw_mask = Image.fromarray(raw_mask_array).convert("L") | |
| # Finale Maske für ControlNet ist 512x512 | |
| mask = mask_512 | |
| print(f"✅ FOCUS_CHANGE Maske erstellt: {mask.size}") | |
| return mask, raw_mask | |
| # ============================================================ | |
| # 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") | |
| #============ | |
| #Doppelte Berechnung: CROP und Original damit Heuristik | |
| # auf Original berechnet werden kann und Weiterverarbeitung auf Crop | |
| #============== | |
| # Masken speichern in den Arrays | |
| all_masks_crop = [] | |
| all_masks_original = [] | |
| for i in range(num_masks): | |
| single_mask = outputs.pred_masks[:, :, i, :, :] | |
| #Für Heuristik SAM-Masken auf Original-Bildgröße | |
| resized_mask_original = F.interpolate( | |
| single_mask, | |
| size=(original_image.height, original_image.width), | |
| mode='bilinear', | |
| align_corners=False | |
| ).squeeze() | |
| mask_np_original = resized_mask_original.sigmoid().cpu().numpy() | |
| all_masks_original.append(mask_np_original) | |
| # 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_crop = resized_mask_crop.sigmoid().cpu().numpy() | |
| all_masks_crop.append(mask_np_crop) | |
| # Debug-Info | |
| mask_binary_crop = (mask_np_crop > 0.5).astype(np.uint8) | |
| mask_binary_original = (mask_np_original > 0.5).astype(np.uint8) | |
| print(f" Maske {i+1}: Crop={np.sum(mask_binary_crop):,}px, " | |
| f"Original={np.sum(mask_binary_original):,}px") | |
| # ============================================================ | |
| # HEURISTIK | |
| # ============================================================ | |
| print("🤔 SCHRITT 6: MASKENAUSWAHL MIT MODUS-SPEZIFISCHER HEURISTIK") | |
| bbox_center = ((original_bbox[0] + original_bbox[2]) // 2, | |
| (original_bbox[1] + original_bbox[3]) // 2) | |
| bbox_area = (original_bbox[2] - original_bbox[0]) * (original_bbox[3] - original_bbox[1]) | |
| best_mask_idx = 0 | |
| best_score = -1 | |
| for i, mask_np in enumerate(all_masks_original): | |
| 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 | |
| mask_area_pixels = np.sum(mask_binary) | |
| # ============================================================ | |
| # SPEZIALHEURISTIK | |
| # ============================================================ | |
| print(f" 🔍 Analysiere Maske {i+1} mit GESICHTS-HEURISTIK") | |
| # 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((original_image.height, original_image.width), dtype=np.uint8) | |
| bbox_mask[original_bbox[1]:original_bbox[3], original_bbox[0]:original_bbox[2]] = 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 Gesicht | |
| 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 | |
| # ============================================================ | |
| # 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 | |
| # ============================================================ | |
| print("👤 GESICHTS-SPEZIFISCHES POSTPROCESSING") | |
| # 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") | |
| # ============================================================ | |
| # KRITISCH: MASKE IMMER ZURÜCK AUF ORIGINALGRÖSSE (auch bei Fallback!) | |
| # ============================================================ | |
| print("-" * 60) | |
| print("🔄 MASKE IMMER ZURÜCK AUF ORIGINALGRÖSSE TRANSFORMIEREN") | |
| # WICHTIG: Immer die richtigen Crop-Koordinaten verwenden | |
| temp_mask = Image.fromarray(mask_array).convert("L") | |
| print(f" Maskengröße auf Ausschnitt: {temp_mask.size}") | |
| # Maske auf ORIGINALBILDGRÖSSE bringen | |
| final_mask = Image.new("L", original_image.size, 0) | |
| print(f" Leere Maske in Originalgröße: {final_mask.size}") | |
| # Immer die gespeicherten Crop-Koordinaten verwenden | |
| if 'crop_x1' in locals() and 'crop_y1' in locals(): | |
| final_mask.paste(temp_mask, (crop_x1, crop_y1)) | |
| print(f" Maskenposition im Original: ({crop_x1}, {crop_y1})") | |
| else: | |
| # Fallback: Zentrieren | |
| x_offset = (original_image.width - temp_mask.width) // 2 | |
| y_offset = (original_image.height - temp_mask.height) // 2 | |
| final_mask.paste(temp_mask, (x_offset, y_offset)) | |
| print(f" ⚠️ Keine Crop-Koordinaten, zentriert: ({x_offset}, {y_offset})") | |
| mask_array = np.array(final_mask) | |
| print(f" ✅ Maske zurück auf Originalgröße skaliert: {mask_array.shape}") | |
| # Bild-Referenz zurücksetzen | |
| image = original_image | |
| print(f" 🔄 Bild-Referenz wieder auf Original gesetzt: {image.size}") | |
| # ============================================================ | |
| # ABSCHLIESSENDE STATISTIK | |
| # ============================================================ | |
| print("📊 FINALE MASKEN-STATISTIK") | |
| # Weiße Pixel zählen | |
| white_pixels = np.sum(mask_array > 0) | |
| total_pixels = mask_array.size | |
| white_ratio = white_pixels / total_pixels * 100 | |
| # 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:,}") | |
| # 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%})") | |
| # Zurück zu PIL Image | |
| mask = Image.fromarray(mask_array).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(f"👤 Crop={crop_size}×{crop_size}px, Heuristik-Score={best_score:.3f}") | |
| print(f"👤 Kopfabdeckung: {coverage_ratio:.1%} der BBox") | |
| print(f"🔍 DEBUG FINALE MASKE:") | |
| print(f" mask_array Min/Max: {mask_array.min()}/{mask_array.max()}, Typ: {mask_array.dtype}") | |
| print(f" Weiße Pixel final: {np.sum(mask_array > 0)}") | |
| print("#" * 80) | |
| return mask, raw_mask #in app.py wird mask immer auf t12 skaliert | |
| # ============================================================ | |
| # 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 |