Spaces:
Starting
on
T4
Starting
on
T4
| def create_sam_mask(self, image, bbox_coords, mode): | |
| """ | |
| ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2 | |
| Restrukturierte Version mit klaren Blöcken pro Modus | |
| """ | |
| 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) | |
| # Bild für SAM vorbereiten | |
| image_np = np.array(image.convert("RGB")) | |
| # Immer nur eine BBox verwenden (SAM 2 erwartet genau 1) | |
| input_boxes = [[[x1, y1, x2, y2]]] | |
| # 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, | |
| 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) | |
| 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") | |
| 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(3): | |
| # Maske in Original-SAM-Größe (256x256) analysieren | |
| mask_256 = outputs.pred_masks[:, :, i, :, :] | |
| mask_np_256 = mask_256.sigmoid().squeeze().cpu().numpy() | |
| # Für Heuristik: Temporär auf Bildgröße skalieren für Flächenverhältnis und Schwerpunktposition | |
| temp_mask = F.interpolate( | |
| mask_256, | |
| size=(image.height, image.width), | |
| mode='bilinear', | |
| align_corners=False | |
| ).squeeze() | |
| mask_np_temp = temp_mask.sigmoid().cpu().numpy() | |
| # 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) | |
| 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) | |
| # 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) | |
| # Damit wird die Rohmaske für die UI-Anzeige gespeichert | |
| raw_mask_array = mask_array.copy() | |
| print("🌳 ENVIRONMENT-CHANGE POSTPROCESSING") | |
| # Originalbildgröße beibehalten | |
| 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ß) | |
| threshold = 0.5 | |
| mask_array = (mask_np > threshold).astype(np.uint8) * 255 | |
| mask_array = 255 - mask_array | |
| print(" ✅ Maske invertiert (Person schwarz, Hintergrund | |
| # 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 | |
| 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) | |
| 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) | |
| mask_array = mask_array ** 0.85 # Gamma-Korrektur | |
| 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 | |
| # ============================================================ | |
| # BLOCK 2: FOCUS_CHANGE | |
| # ============================================================ | |
| elif mode == "focus_change": | |
| print("-" * 60) | |
| print("🎯 MODUS: FOCUS_CHANGE (OPTIMIERT)") | |
| print("-" * 60) | |
| # Bild für SAM vorbereiten | |
| image_np = np.array(image.convert("RGB")) | |
| # NUR EINE BBOX UND NUR MITTELPUNKT (kein Gesichtspunkt) | |
| 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 | |
| input_labels = [[[1]]] # Positiver Prompt | |
| 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]}") | |
| # 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 (OHNE sie alle zu skalieren!) | |
| for i in range(3): | |
| # Maske in Original-SAM-Größe (256x256) analysieren | |
| mask_256 = outputs.pred_masks[:, :, i, :, :] | |
| mask_np_256 = mask_256.sigmoid().squeeze().cpu().numpy() | |
| # Für Heuristik: Temporär auf Bildgröße skalieren für Flächenverhältnis und Schwerpunktposition | |
| temp_mask = F.interpolate( | |
| mask_256, | |
| size=(image.height, image.width), | |
| mode='bilinear', | |
| align_corners=False | |
| ).squeeze() | |
| mask_np_temp = temp_mask.sigmoid().cpu().numpy() | |
| # 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 (wie bisher) | |
| mask_area_pixels = np.sum(mask_binary) | |
| # BBox-Überlappung | |
| 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 SKALIEREN -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.sigmoid().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() | |
| 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 (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) | |
| # 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) | |
| # 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. Größte Komponente behalten | |
| 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("👤 MODUS: FACE_ONLY_CHANGE") | |
| print("-" * 60) | |
| # ... existierende face_only_change Logik hier komplett ... | |
| # (wird aus dem Original übernommen, nicht verändert) | |
| # WICHTIG: Du musst den face_only_change Code hier einfügen | |
| # von Zeile ~252 bis ~650 aus dem Original | |
| # Beispiel-Struktur (vereinfacht): | |
| # Crop, Punkte setzen, spezielle Gesichtsheuristik etc. | |
| # Am Ende: | |
| mask = Image.new("L", (512, 512), 128) # Platzhalter | |
| raw_mask = mask.copy() | |
| return mask, raw_mask | |
| # ============================================================ | |
| # 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 |