Astridkraft commited on
Commit
f82108c
·
verified ·
1 Parent(s): 26b394c

Update sam_module.py

Browse files
Files changed (1) hide show
  1. sam_module.py +1110 -0
sam_module.py CHANGED
@@ -1,3 +1,1113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  def create_sam_mask(self, image, bbox_coords, mode):
2
  """
3
  ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2
 
1
+ def create_sam_mask(self, image, bbox_coords, mode):
2
+ """
3
+ ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2
4
+ """
5
+ try:
6
+ print("#" * 80)
7
+ print("# 🎯 STARTE SAM 2 SEGMENTIERUNG")
8
+ print("#" * 80)
9
+ print(f"📐 Eingabebild-Größe: {image.size}")
10
+ print(f"🎛️ Ausgewählter Modus: {mode}")
11
+
12
+ # ============================================================
13
+ # VORBEREITUNG FÜR ALLE MODI
14
+ # ============================================================
15
+ original_image = image
16
+
17
+ # 1. SAM2 laden
18
+ if not self.sam_initialized:
19
+ print("📥 SAM 2 ist noch nicht geladen, starte Lazy Loading...")
20
+ self._lazy_load_sam()
21
+
22
+ if self.sam_model is None or self.sam_processor is None:
23
+ print("⚠️ SAM 2 Model nicht verfügbar, verwende Fallback")
24
+ return self._create_rectangular_mask(image, bbox_coords, mode)
25
+
26
+ # 2. Validiere BBox
27
+ x1, y1, x2, y2 = self._validate_bbox(image, bbox_coords)
28
+ original_bbox = (x1, y1, x2, y2)
29
+ print(f"📏 Original-BBox Größe: {x2-x1} × {y2-y1} px")
30
+
31
+ # ============================================================
32
+ # BLOCK 1: ENVIRONMENT_CHANGE
33
+ # ============================================================
34
+ if mode == "environment_change":
35
+ print("-" * 60)
36
+ print("🌳 MODUS: ENVIRONMENT_CHANGE")
37
+ print("-" * 60)
38
+
39
+ # Der Prozessor von SAM erwartet ein NumPy-Array kein PIL
40
+ image_np = np.array(image.convert("RGB"))
41
+
42
+ # Packt die BBox-Koordinaten in eine 3D-Liste
43
+ input_boxes = [[[x1, y1, x2, y2]]]
44
+
45
+ # Aufruf des SAM-Prozessors mit Originalbild in Form NumPy-Array und BBox.Der Processor verarbeitet Bild und BBox
46
+ # in die für SAM erforderlichen Tensoren und speichert sie in inputs.
47
+ inputs = self.sam_processor(
48
+ image_np,
49
+ input_boxes=input_boxes,
50
+ return_tensors="pt"
51
+ ).to(self.device) # Ohne .to(self.device) werden die Tensoren standardmäßig im CPU-RAM erzeugt und gespeichert! Da GPU-Fehler!
52
+
53
+ print(f" - 'input_boxes' Shape: {inputs['input_boxes'].shape}")
54
+
55
+ # SAM2 Vorhersage
56
+ print("-" * 60)
57
+ print("🧠 SAM 2 INFERENZ (Vorhersage)")
58
+ with torch.no_grad():
59
+ print(" Führe Vorhersage durch...")
60
+ outputs = self.sam_model(**inputs) #führt die Segmentierung mit SAM aus
61
+ print(f"✅ Vorhersage abgeschlossen")
62
+ print(f" Anzahl der Vorhersagemasken: {outputs.pred_masks.shape[2]}")
63
+
64
+ num_masks = outputs.pred_masks.shape[2]
65
+ print(f" SAM lieferte {num_masks} verschiedene Masken")
66
+
67
+ # Sammlung aller Masken in all_masks
68
+ all_masks = []
69
+
70
+ for i in range(num_masks):
71
+ single_mask = outputs.pred_masks[:, :, i, :, :]
72
+ resized_mask = F.interpolate(
73
+ single_mask,
74
+ size=(image.height, image.width),
75
+ mode='bilinear',
76
+ align_corners=False
77
+ ).squeeze()
78
+
79
+ mask_np = resized_mask.sigmoid().cpu().numpy() #wandelt Modellausgaben in Wahrscheinlichkeiten und bewegt Daten von GPU nach CPU
80
+ all_masks.append(mask_np) #fügt die aktuelle Maske der Liste all_masks hinzu
81
+
82
+
83
+ bbox_center = ((x1 + x2) // 2, (y1 + y2) // 2)
84
+ bbox_area = (x2 - x1) * (y2 - y1)
85
+ print(f" Erwartetes BBox-Zentrum: {bbox_center}")
86
+ print(f" Erwartete BBox-Fläche: {bbox_area:,} Pixel")
87
+
88
+ print("🤔 HEURISTIK: Beste Maske auswählen")
89
+ best_mask_idx = 0
90
+ best_score = -1
91
+
92
+ # Alle 3 Masken analysieren (OHNE sie alle zu skalieren!)
93
+ for i in range(num_masks):
94
+ mask_np_temp = all_masks[i] #verwende Maske auf Original-Bildgröße
95
+
96
+ # Adaptive Vor-Filterung (prüft ob Maske überhaupt gültig ist)
97
+ mask_max = mask_np_temp.max()
98
+ if mask_max < 0.3:
99
+ continue # Maske überspringen
100
+
101
+ adaptive_threshold = max(0.3, mask_max * 0.7)
102
+ mask_binary = (mask_np_temp > adaptive_threshold).astype(np.uint8)
103
+
104
+ # wenn nur schwarze Pixel (keine Segmentierung) nimm die nächste Maske
105
+ if np.sum(mask_binary) == 0:
106
+ print(f" ❌ Maske {i+1}: Keine Pixel nach adaptive_threshold {adaptive_threshold:.3f}")
107
+ continue
108
+
109
+ # Heuristik-Berechnung
110
+ mask_area_pixels = np.sum(mask_binary)
111
+
112
+ #Berechnung von Überlappung SAM-Maske und ursprünglicher BBox
113
+ bbox_mask = np.zeros((image.height, image.width), dtype=np.uint8)
114
+ bbox_mask[y1:y2, x1:x2] = 1
115
+
116
+ overlap = np.sum(mask_binary & bbox_mask)
117
+ bbox_overlap_ratio = overlap / np.sum(bbox_mask) if np.sum(bbox_mask) > 0 else 0
118
+
119
+ # Schwerpunkt berechnen
120
+ y_coords, x_coords = np.where(mask_binary > 0)
121
+ if len(y_coords) > 0:
122
+ centroid_y = np.mean(y_coords)
123
+ centroid_x = np.mean(x_coords)
124
+ centroid_distance = np.sqrt((centroid_x - bbox_center[0])**2 + (centroid_y - bbox_center[1])**2)
125
+ normalized_distance = centroid_distance / max(image.width, image.height)
126
+ else:
127
+ normalized_distance = 1.0
128
+
129
+ # Flächen-Ratio
130
+ area_ratio = mask_area_pixels / bbox_area
131
+ area_score = 1.0 - min(abs(area_ratio - 1.0), 1.0)
132
+
133
+ # Konfidenz
134
+ confidence_score = mask_max
135
+
136
+ # Standard-Score
137
+ score = (
138
+ bbox_overlap_ratio * 0.4 +
139
+ (1.0 - normalized_distance) * 0.25 +
140
+ area_score * 0.25 +
141
+ confidence_score * 0.1
142
+ )
143
+
144
+ print(f" 📊 STANDARD-SCORES für Maske {i+1}:")
145
+ print(f" • BBox-Überlappung: {bbox_overlap_ratio:.3f}")
146
+ print(f" • Zentrums-Distanz: {centroid_distance if 'centroid_distance' in locals() else 'N/A'}")
147
+ print(f" • Flächen-Ratio: {area_ratio:.3f}")
148
+ print(f" • GESAMTSCORE: {score:.3f}")
149
+
150
+ if score > best_score:
151
+ best_score = score
152
+ best_mask_idx = i
153
+ print(f" 🏆 Neue beste Maske: Nr. {i+1} mit Score {score:.3f}")
154
+
155
+ print(f"✅ Beste Maske ausgewählt: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
156
+
157
+ # Beste Maske verwenden - mask_np beste Maske
158
+ mask_np = all_masks[best_mask_idx]
159
+
160
+ max_val = mask_np.max()
161
+ print(f" 🔍 Maximaler SAM-Konfidenzwert der besten Maske: {max_val:.3f}")
162
+
163
+ if max_val < 0.6:
164
+ dynamic_threshold = 0.3
165
+ print(f" ⚠️ SAM ist unsicher (max_val={max_val:.3f} < 0.6)")
166
+ else:
167
+ dynamic_threshold = max_val * 0.8
168
+ print(f" ✅ SAM ist sicher (max_val={max_val:.3f} >= 0.6)")
169
+
170
+ # Binärmaske erstellen (256x256)
171
+ mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255
172
+
173
+ # Fallback bei leerer Maske, der höchste Wert ist 0 also schwarz
174
+ if mask_array.max() == 0:
175
+ print(" ⚠️ Maske leer, erstelle rechteckige Fallback-Maske")
176
+ mask_array = np.zeros((512, 512), dtype=np.uint8) * 255 # weiße 512x512-Maske
177
+
178
+ # Skaliere BBox auf 512x512
179
+ scale_x = 512 / image.width
180
+ scale_y = 512 / image.height
181
+ fb_x1 = int(x1 * scale_x)
182
+ fb_y1 = int(y1 * scale_y)
183
+ fb_x2 = int(x2 * scale_x)
184
+ fb_y2 = int(y2 * scale_y)
185
+
186
+ # Schwarzes Rechteck für Person bzw. BBox
187
+ cv2.rectangle(mask_array, (fb_x1, fb_y1), (fb_x2, fb_y2), 0, -1)
188
+
189
+ # Damit wird die Rohmaske für die UI-Anzeige gespeichert
190
+ raw_mask_array = mask_array.copy()
191
+
192
+ print("🌳 ENVIRONMENT-CHANGE POSTPROCESSING")
193
+
194
+ # Konvertierung zu PIL, hochskalieren auf Originalgröße (korrekte Überlagerung mit O-Bild),
195
+ # Konvertierung NumPy für weitere Verarbeitung da mathematisch korrekter als PIL.
196
+ if image.size != original_image.size:
197
+ print(f" ⚠️ Bildgröße angepasst: {image.size} → {original_image.size}")
198
+ temp_mask = Image.fromarray(mask_array).convert("L")
199
+ temp_mask = temp_mask.resize(original_image.size, Image.Resampling.NEAREST)
200
+ mask_array = np.array(temp_mask)
201
+ print(f" ✅ Maske auf Originalgröße skaliert: {mask_array.shape}")
202
+
203
+ # Maske invertieren (Person wird schwarz, Hintergrund weiß)
204
+ mask_array = 255 - mask_array
205
+ print(" ✅ Maske invertiert (Person schwarz, Hintergrund weiß)")
206
+
207
+ # Weiße Punkte in der Person (schwarz) entfernen
208
+ print("🧹 Entferne weiße Punkte in der Person...")
209
+ kernel_open = np.ones((3, 3), np.uint8)
210
+ mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel_open, iterations=3)
211
+ print(" ✅ MORPH_OPEN entfernt weiße Punkte in der Person")
212
+
213
+ # DEBUG nach MORPH_OPEN
214
+ print(f" Nach MORPH_OPEN - Weiße Pixel: {np.sum(mask_array > 127)}")
215
+
216
+ # Morphologische Operationen für saubere Umgebung - entfernt schwarze Pixel aus Umgebung
217
+ print("🔧 Verbessere Umgebungsmaske...")
218
+ kernel_close = np.ones((5, 5), np.uint8)
219
+ mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel_close)
220
+ print(" ✅ MORPH_CLOSE für zusammenhängende Umgebung")
221
+
222
+ # DEBUG nach MORPH_CLOSE
223
+ print(f" Nach MORPH_CLOSE - Weiße Pixel: {np.sum(mask_array > 127)}")
224
+
225
+ # Weiche Ränder für bessere Integration der Person
226
+ print("🌈 Erstelle weiche Übergänge...")
227
+ mask_array = cv2.GaussianBlur(mask_array, (9, 9), 2.0) #2.0 bestimmt wie stark die Unschärfe ist
228
+ print(" ✅ Gaussian Blur für weiche Übergänge")
229
+
230
+ # DEBUG nach Gaussian Blur
231
+ print(f" Nach Gaussian Blur - Min/Max: {mask_array.min()}/{mask_array.max()}")
232
+ print(f" Nach Gaussian Blur - dtype: {mask_array.dtype}")
233
+
234
+ # Gamma-Korrektur für präzisere Ränder
235
+ print("🎛️ Wende Gamma-Korrektur an...")
236
+ mask_array = mask_array.astype(np.float32) / 255.0
237
+ print(f" Konvertiert zu Float32: Min={mask_array.min():.3f}, Max={mask_array.max():.3f}")
238
+
239
+ mask_array = np.clip(mask_array, 0.0, 1.0) #begrenzt alle Werte auf 0 und 1
240
+ mask_array = mask_array ** 0.85 # Gamma-Korrektur Werte > 0.5 werden abgedunkelt, <0.5 aufgehellt-erzeugt natürliche Maskenübergänge
241
+ print(f" Nach Gamma 0.85: Min={mask_array.min():.3f}, Max={mask_array.max():.3f}")
242
+
243
+ mask_array = (mask_array * 255).astype(np.uint8)
244
+ print(" ✅ Gamma-Korrektur (0.85) gegen milchige Ränder")
245
+
246
+ # FINALE QUALITÄTSKONTROLLE
247
+ print("-" * 60)
248
+ print("📊 FINALE MASKEN-STATISTIK (ENVIRONMENT_CHANGE)")
249
+
250
+ white_pixels = np.sum(mask_array > 127)
251
+ black_pixels = np.sum(mask_array <= 127)
252
+ total_pixels = mask_array.size
253
+
254
+ white_ratio = white_pixels / total_pixels * 100
255
+ black_ratio = black_pixels / total_pixels * 100
256
+
257
+ print(f" Weiße Pixel (HINTERGRUND - Veränderung): {white_pixels:,} ({white_ratio:.1f}%)")
258
+ print(f" Schwarze Pixel (PERSON - Erhaltung): {black_pixels:,} ({black_ratio:.1f}%)")
259
+ print(f" Gesamtpixel: {total_pixels:,}")
260
+
261
+ # Warnungen basierend auf Verhältnis
262
+ if white_ratio < 30:
263
+ print(f" ⚠️ WARNUNG: Sehr wenig Hintergrund ({white_ratio:.1f}%)")
264
+ print(f" ℹ️ Das könnte bedeuten, dass die Person zu groß segmentiert wurde")
265
+ elif white_ratio > 90:
266
+ print(f" ⚠️ WARNUNG: Sehr viel Hintergrund ({white_ratio:.1f}%)")
267
+ print(f" ℹ️ Das könnte bedeuten, dass die Person zu klein segmentiert wurde")
268
+ elif 50 <= white_ratio <= 80:
269
+ print(f" ✅ OPTIMALES Verhältnis ({white_ratio:.1f}%)")
270
+ else:
271
+ print(f" ℹ️ Normales Verhältnis ({white_ratio:.1f}%)")
272
+
273
+ # Zurück zu PIL Image
274
+ mask = Image.fromarray(mask_array).convert("L")
275
+ raw_mask = Image.fromarray(raw_mask_array).convert("L")
276
+
277
+ print("#" * 80)
278
+ print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
279
+ print(f"📐 Finale Maskengröße: {mask.size}")
280
+ print(f"🎛️ Verwendeter Modus: {mode}")
281
+ print("#" * 80)
282
+
283
+ 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.
284
+
285
+ # ============================================================
286
+ # BLOCK 2: FOCUS_CHANGE
287
+ # ============================================================
288
+ elif mode == "focus_change":
289
+ print("-" * 60)
290
+ print("🎯 MODUS: FOCUS_CHANGE (OPTIMIERT)")
291
+ print("-" * 60)
292
+
293
+ # Konvertierung O-Bild in NumPy-Array für SAM
294
+ image_np = np.array(image.convert("RGB"))
295
+
296
+ # Packt die BBox-Koordinaten in eine 3D-Liste
297
+ input_boxes = [[[x1, y1, x2, y2]]]
298
+
299
+ # Nur Mittelpunkt als positiver Prompt
300
+ center_x = (x1 + x2) // 2
301
+ center_y = (y1 + y2) // 2
302
+ input_points = [[[[center_x, center_y]]]] # NUR EIN PUNKT in 4D-Liste
303
+ input_labels = [[[1]]] # Markiert Punkt als Positiver Prompt also der Bereich muß segmentiert werden
304
+
305
+ print(f" 🎯 SAM-Prompt: BBox [{x1},{y1},{x2},{y2}]")
306
+ print(f" 👁️ Punkt: Nur Mitte ({center_x},{center_y})")
307
+
308
+ # SAM Inputs vorbereiten
309
+ inputs = self.sam_processor(
310
+ image_np,
311
+ input_boxes=input_boxes,
312
+ input_points=input_points,
313
+ input_labels=input_labels,
314
+ return_tensors="pt"
315
+ ).to(self.device)
316
+
317
+ # SAM Vorhersage (alle 3 Masken)
318
+ print("🧠 SAM 2 INFERENZ (3 Masken-Varianten)")
319
+ with torch.no_grad():
320
+ print(" Führe Vorhersage durch...")
321
+ outputs = self.sam_model(**inputs)
322
+ print(f"✅ Vorhersage abgeschlossen")
323
+ print(f" Anzahl der Vorhersagemasken: {outputs.pred_masks.shape[2]}")
324
+
325
+ num_masks = outputs.pred_masks.shape[2]
326
+
327
+
328
+ # Sammlung aller Masken in all_masks
329
+ all_masks = []
330
+
331
+ for i in range(num_masks):
332
+ single_mask = outputs.pred_masks[:, :, i, :, :]
333
+ resized_mask = F.interpolate(
334
+ single_mask,
335
+ size=(image.height, image.width),
336
+ mode='bilinear',
337
+ align_corners=False
338
+ ).squeeze()
339
+
340
+ mask_np = resized_mask.sigmoid().cpu().numpy()
341
+ all_masks.append(mask_np) #fügt die aktuelle Maske der Liste all_masks hinzu
342
+
343
+
344
+ # BBox-Information für Heuristik
345
+ bbox_center = ((x1 + x2) // 2, (y1 + y2) // 2)
346
+ bbox_area = (x2 - x1) * (y2 - y1)
347
+
348
+ print("🤔 HEURISTIK: Beste Maske auswählen")
349
+ best_mask_idx = 0
350
+ best_score = -1
351
+
352
+ # Alle 3 Masken analysieren
353
+ for i in range(num_masks):
354
+ # Maske in Original-Bildgröße -vorher interpolate- analysieren
355
+
356
+ mask_np_temp = all_masks[i]
357
+
358
+ # Adaptive Vor-Filterung (prüft ob Maske überhaupt gültig ist)
359
+ mask_max = mask_np_temp.max()
360
+ if mask_max < 0.3:
361
+ continue # Maske überspringen
362
+
363
+ adaptive_threshold = max(0.3, mask_max * 0.7)
364
+ mask_binary = (mask_np_temp > adaptive_threshold).astype(np.uint8)
365
+
366
+ # wenn nur schwarze Pixel (keine Segmentierung) nimm die nächste Maske
367
+ if np.sum(mask_binary) == 0:
368
+ continue
369
+
370
+ # Heuristik-Berechnung
371
+ mask_area_pixels = np.sum(mask_binary) # zählt alle weißen Pixel in der Binärmaske
372
+
373
+ # Berechnet wie gut die SAM-Maske mit der ursprünglichen BBox überlappt
374
+ bbox_mask = np.zeros((image.height, image.width), dtype=np.uint8)
375
+ bbox_mask[y1:y2, x1:x2] = 1
376
+ overlap = np.sum(mask_binary & bbox_mask)
377
+ bbox_overlap_ratio = overlap / np.sum(bbox_mask) if np.sum(bbox_mask) > 0 else 0
378
+
379
+ # Schwerpunkt
380
+ y_coords, x_coords = np.where(mask_binary > 0)
381
+ if len(y_coords) > 0:
382
+ centroid_y = np.mean(y_coords)
383
+ centroid_x = np.mean(x_coords)
384
+ centroid_distance = np.sqrt((centroid_x - bbox_center[0])**2 +
385
+ (centroid_y - bbox_center[1])**2)
386
+ normalized_distance = centroid_distance / max(image.width, image.height)
387
+ else:
388
+ normalized_distance = 1.0
389
+
390
+ # Flächen-Ratio
391
+ area_ratio = mask_area_pixels / bbox_area
392
+ area_score = 1.0 - min(abs(area_ratio - 1.0), 1.0)
393
+
394
+ # FOCUS_CHANGE spezifischer Score
395
+ score = (
396
+ bbox_overlap_ratio * 0.4 + # 40% BBox-Überlappung
397
+ (1.0 - normalized_distance) * 0.25 + # 25% Zentrumsnähe
398
+ area_score * 0.25 + # 25% Flächenpassung
399
+ mask_max * 0.1 # 10% SAM-Konfidenz
400
+ )
401
+
402
+ print(f" Maske {i+1}: Score={score:.3f}, "
403
+ f"Überlappung={bbox_overlap_ratio:.3f}, "
404
+ f"Fläche={mask_area_pixels:,}px")
405
+
406
+ if score > best_score:
407
+ best_score = score
408
+ best_mask_idx = i
409
+
410
+ print(f"✅ Beste Maske: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
411
+
412
+ # NUR DIE BESTE MASKE AUF 512x512 HERUNTERSKALIEREN -Für Inpaint
413
+ best_mask_256 = outputs.pred_masks[:, :, best_mask_idx, :, :]
414
+ resized_mask = F.interpolate(
415
+ best_mask_256,
416
+ size=(512, 512), # DIREKT AUF CONTROLNET-ZIELGRÖßE
417
+ mode='bilinear',
418
+ align_corners=False
419
+ ).squeeze()
420
+
421
+ mask_np = resized_mask.cpu().numpy()
422
+ print(f" 🔄 Beste Maske skaliert auf 512×512 für ControlNet")
423
+
424
+ # ============================================================
425
+ # DYNAMISCHER THRESHOLD
426
+ # SAM gibt nur Wahrscheinlichkeiten aus!
427
+ # Nachdem das Modell eine Maske für eine Person vorhersagt (wo jeder Pixel einen Wert zwischen 0 und 1 hat,
428
+ # wie "wahrscheinlich gehört dieser Pixel zur Person"), wird diese Maske binarisiert (0 oder 1), indem alle
429
+ # Pixel unter 0.05 auf 0 gesetzt werden, alle darüber auf 1.
430
+ # ============================================================
431
+ mask_max = mask_np.max() #höchster Wahrscheinlichkeitswert in SAM-Maske
432
+ if best_score < 0.7: # Schlechte Maskenqualität
433
+ dynamic_threshold = 0.05 # SEHR NIEDRIG für maximale Abdeckung
434
+ print(f" ⚠️ Masken-Score niedrig ({best_score:.3f}). "
435
+ f"Threshold=0.05 für maximale Abdeckung")
436
+ else:
437
+ dynamic_threshold = max(0.15, mask_max * 0.3) # Moderater Threshold
438
+ print(f" ✅ Gute Maske. Threshold={dynamic_threshold:.3f}")
439
+
440
+ # Binärmaske erstellen (512x512)
441
+ mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255
442
+
443
+ # Fallback bei leerer Maske, der höchste Wert ist 0 also schwarz
444
+ if mask_array.max() == 0:
445
+ print(" ⚠️ Maske leer, erstelle rechteckige Fallback-Maske")
446
+ mask_array = np.zeros((512, 512), dtype=np.uint8)
447
+ # BBox auf 512x512 skalieren für Fallback
448
+ scale_x = 512 / image.width
449
+ scale_y = 512 / image.height
450
+ fb_x1 = int(x1 * scale_x)
451
+ fb_y1 = int(y1 * scale_y)
452
+ fb_x2 = int(x2 * scale_x)
453
+ fb_y2 = int(y2 * scale_y)
454
+ cv2.rectangle(mask_array, (fb_x1, fb_y1), (fb_x2, fb_y2), 255, -1) #weiße Rechteckbox
455
+
456
+ # Damit wird die Rohmaske für die UI-Anzeige gespeichert
457
+ raw_mask_array = mask_array.copy()
458
+
459
+ # FOCUS_CHANGE POSTPROCESSING (angepasst für 512x512)
460
+ print("🔧 FOCUS_CHANGE POSTPROCESSING (auf 512×512)")
461
+ print(f" mask_array - Min/Max: {mask_array.min()}/{mask_array.max()}")
462
+ print(f" mask_array - Weiße Pixel: {np.sum(mask_array > 0)}")
463
+ print(f" mask_array - Shape: {mask_array.shape}")
464
+ print(f" mask_array - dtype: {mask_array.dtype}")
465
+
466
+ # 1. Findet und behält nur die größte zusammenhängende Komponente der Maske
467
+ labeled_array, num_features = ndimage.label(mask_array)
468
+ if num_features > 1:
469
+ sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
470
+ largest_component = np.argmax(sizes) + 1
471
+ mask_array = np.where(labeled_array == largest_component, mask_array, 0)
472
+ print(f" ✅ Größte Komponente behalten ({num_features}→1)")
473
+
474
+ # 2. Morphologische Operationen
475
+ kernel_close = np.ones((5, 5), np.uint8)
476
+ mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel_close, iterations=2)
477
+
478
+ kernel_dilate = np.ones((15, 15), np.uint8)
479
+ mask_array = cv2.dilate(mask_array, kernel_dilate, iterations=1)
480
+
481
+ # 3. Weiche Übergänge mittlerer Blur für natürliche Übergänge
482
+ mask_array = cv2.GaussianBlur(mask_array, (9, 9), 2.0)
483
+
484
+ # 4. Gamma-Korrektur
485
+ mask_array_float = mask_array.astype(np.float32) / 255.0
486
+ mask_array_float = np.clip(mask_array_float, 0.0, 1.0)
487
+ mask_array_float = mask_array_float ** 0.85
488
+ mask_array = (mask_array_float * 255).astype(np.uint8)
489
+
490
+ # 5. Auf Originalgröße für Rückgabe (falls benötigt)
491
+ mask_512 = Image.fromarray(mask_array).convert("L")
492
+ raw_mask = Image.fromarray(raw_mask_array).convert("L")
493
+
494
+ # Finale Maske für ControlNet ist 512x512
495
+ mask = mask_512
496
+
497
+ print(f"✅ FOCUS_CHANGE Maske erstellt: {mask.size}")
498
+ return mask, raw_mask
499
+
500
+ # ============================================================
501
+ # BLOCK 3: FACE_ONLY_CHANGE
502
+ # ============================================================
503
+ elif mode == "face_only_change":
504
+ print("-" * 60)
505
+ print("👤 SPEZIALMODUS: NUR GESICHT - ROBUSTER WORKFLOW")
506
+ print("-" * 60)
507
+
508
+ # ============================================================
509
+ # Originalbild sichern
510
+ # Andere Vorgehensweise da SAM bei kleinen Köpfen sonst keine Chance hat!
511
+ # Bild ausschneiden auf eine vergrößerte quadratische Box - Crops
512
+ # ============================================================
513
+ original_image = image
514
+ print(f"💾 Originalbild gesichert: {original_image.size}")
515
+ original_bbox = (x1, y1, x2, y2) # <-- DAS FEHLT
516
+ print(f"💾 Original-BBox gespeichert: {original_bbox}")
517
+
518
+ # ============================================================
519
+ # Crop = BBox × 2.5 (ERHÖHT für mehr Kontext)
520
+ # ============================================================
521
+ print("✂️ SCHRITT 2: ERSTELLE QUADRATISCHEN AUSSCHNITT (BBox × 2.5)")
522
+
523
+ # BBox-Zentrum berechnen
524
+ bbox_center_x = (x1 + x2) // 2
525
+ bbox_center_y = (y1 + y2) // 2
526
+ print(f" 📍 BBox-Zentrum: ({bbox_center_x}, {bbox_center_y})")
527
+
528
+ # Größte Dimension der BBox finden
529
+ bbox_width = x2 - x1
530
+ bbox_height = y2 - y1
531
+ bbox_max_dim = max(bbox_width, bbox_height)
532
+ print(f" 📏 BBox Dimensionen: {bbox_width} × {bbox_height} px")
533
+ print(f" 📐 Maximale BBox-Dimension: {bbox_max_dim} px")
534
+
535
+ # Crop-Größe berechnen (BBox × 2.5)
536
+ crop_size = int(bbox_max_dim * 2.5)
537
+ print(f" 🎯 Ziel-Crop-Größe: {crop_size} × {crop_size} px (BBox × 2.5)")
538
+
539
+ # Crop-Koordinaten berechnen (zentriert um BBox)
540
+ crop_x1 = bbox_center_x - crop_size // 2
541
+ crop_y1 = bbox_center_y - crop_size // 2
542
+ crop_x2 = crop_x1 + crop_size
543
+ crop_y2 = crop_y1 + crop_size
544
+
545
+ # Sicherstellen, dass Crop innerhalb der Bildgrenzen bleibt
546
+ crop_x1 = max(0, crop_x1)
547
+ crop_y1 = max(0, crop_y1)
548
+ crop_x2 = min(original_image.width, crop_x2)
549
+ crop_y2 = min(original_image.height, crop_y2)
550
+
551
+
552
+ # ITERATIVE ANPASSUNG für bessere Crop-Größe
553
+ max_iterations = 3
554
+ print(f" 🔄 Iterative Crop-Anpassung (max. {max_iterations} Versuche)")
555
+
556
+ for iteration in range(max_iterations):
557
+ actual_crop_width = crop_x2 - crop_x1
558
+ actual_crop_height = crop_y2 - crop_y1
559
+
560
+ # Prüfen ob Crop groß genug ist
561
+ if actual_crop_width >= crop_size and actual_crop_height >= crop_size:
562
+ print(f" ✅ Crop-Größe OK nach {iteration} Iteration(en): {actual_crop_width}×{actual_crop_height} px")
563
+ break
564
+
565
+ print(f" 🔄 Iteration {iteration+1}: Crop zu klein ({actual_crop_width}×{actual_crop_height})")
566
+
567
+ # BREITE anpassen (falls nötig)
568
+ if actual_crop_width < crop_size:
569
+ if crop_x1 == 0: # Am linken Rand
570
+ crop_x2 = min(original_image.width, crop_x1 + crop_size)
571
+ print(f" ← Breite angepasst (linker Rand): crop_x2 = {crop_x2}")
572
+ elif crop_x2 == original_image.width: # Am rechten Rand
573
+ crop_x1 = max(0, crop_x2 - crop_size)
574
+ print(f" → Breite angepasst (rechter Rand): crop_x1 = {crop_x1}")
575
+ else:
576
+ # Nicht am Rand - zentriert erweitern
577
+ missing_width = crop_size - actual_crop_width
578
+ expand_left = missing_width // 2
579
+ expand_right = missing_width - expand_left
580
+
581
+ crop_x1 = max(0, crop_x1 - expand_left)
582
+ crop_x2 = min(original_image.width, crop_x2 + expand_right)
583
+ print(f" ↔ Zentriert erweitert um {missing_width}px")
584
+
585
+ # HÖHE anpassen (falls nötig)
586
+ if actual_crop_height < crop_size:
587
+ if crop_y1 == 0: # Am oberen Rand
588
+ crop_y2 = min(original_image.height, crop_y1 + crop_size)
589
+ print(f" ↑ Höhe angepasst (oberer Rand): crop_y2 = {crop_y2}")
590
+ elif crop_y2 == original_image.height: # Am unteren Rand
591
+ crop_y1 = max(0, crop_y2 - crop_size)
592
+ print(f" ↓ Höhe angepasst (unterer Rand): crop_y1 = {crop_y1}")
593
+ else:
594
+ # Nicht am Rand - zentriert erweitern
595
+ missing_height = crop_size - actual_crop_height
596
+ expand_top = missing_height // 2
597
+ expand_bottom = missing_height - expand_top
598
+
599
+ crop_y1 = max(0, crop_y1 - expand_top)
600
+ crop_y2 = min(original_image.height, crop_y2 + expand_bottom)
601
+ print(f" ↕ Zentriert erweitert um {missing_height}px")
602
+
603
+ # Sicherstellen, dass innerhalb der Bildgrenzen
604
+ crop_x1 = max(0, crop_x1)
605
+ crop_y1 = max(0, crop_y1)
606
+ crop_x2 = min(original_image.width, crop_x2)
607
+ crop_y2 = min(original_image.height, crop_y2)
608
+
609
+ # Letzte Iteration erreicht?
610
+ if iteration == max_iterations - 1:
611
+ actual_crop_width = crop_x2 - crop_x1
612
+ actual_crop_height = crop_y2 - crop_y1
613
+ print(f" ⚠️ Max. Iterationen erreicht. Finaler Crop: {actual_crop_width}×{actual_crop_height} px")
614
+
615
+ # Warnung wenn immer noch zu klein
616
+ if actual_crop_width < crop_size or actual_crop_height < crop_size:
617
+ min_acceptable = int(bbox_max_dim * 1.8) # Mindestens 1.8× BBox
618
+ if actual_crop_width < min_acceptable or actual_crop_height < min_acceptable:
619
+ print(f" 🚨 KRITISCH: Crop immer noch zu klein ({actual_crop_width}×{actual_crop_height})")
620
+ print(f" 🚨 SAM könnte Probleme haben!")
621
+
622
+ print(f" 🔲 Finaler Crop-Bereich: [{crop_x1}, {crop_y1}, {crop_x2}, {crop_y2}]")
623
+ print(f" 📏 Finale Crop-Größe: {crop_x2-crop_x1} × {crop_y2-crop_y1} px")
624
+
625
+
626
+ # Bild ausschneiden- 2,5 mal so groß und quadratisch wie BBox
627
+ cropped_image = original_image.crop((crop_x1, crop_y1, crop_x2, crop_y2))
628
+ print(f" ✅ Quadratischer Ausschnitt erstellt: {cropped_image.size}")
629
+
630
+ # ============================================================
631
+ # BBox-Koordinaten transformieren
632
+ # ============================================================
633
+ print("📐 SCHRITT 3: BBox-KOORDINATEN TRANSFORMIEREN")
634
+ rel_x1 = x1 - crop_x1
635
+ rel_y1 = y1 - crop_y1
636
+ rel_x2 = x2 - crop_x1
637
+ rel_y2 = y2 - crop_y1
638
+
639
+ # Sicherstellen, dass BBox innerhalb des Crops liegt
640
+ rel_x1 = max(0, rel_x1)
641
+ rel_y1 = max(0, rel_y1)
642
+ rel_x2 = min(cropped_image.width, rel_x2)
643
+ rel_y2 = min(cropped_image.height, rel_y2)
644
+
645
+ print(f" 🎯 Relative BBox im Crop: [{rel_x1}, {rel_y1}, {rel_x2}, {rel_y2}]")
646
+ print(f" 📏 Relative BBox Größe: {rel_x2-rel_x1} × {rel_y2-rel_y1} px")
647
+
648
+ # ============================================================
649
+ # INTENSIVE BILDAUFBEREITUNG FÜR GESICHTSERKENNUNG
650
+ # ============================================================
651
+ print("🔍 SCHRITT 4: ERWEITERTE BILDAUFBEREITUNG FÜR GESICHTSERKENNUNG")
652
+
653
+ # 1. Kontrast verstärken
654
+ contrast_enhancer = ImageEnhance.Contrast(cropped_image)
655
+ enhanced_image = contrast_enhancer.enhance(1.8) # 80% mehr Kontrast
656
+
657
+ # 2. Schärfe erhöhen für bessere Kantenerkennung
658
+ sharpness_enhancer = ImageEnhance.Sharpness(enhanced_image)
659
+ enhanced_image = sharpness_enhancer.enhance(2.0) # 100% mehr Schärfe
660
+
661
+ # 3. Helligkeit anpassen
662
+ brightness_enhancer = ImageEnhance.Brightness(enhanced_image)
663
+ enhanced_image = brightness_enhancer.enhance(1.1) # 10% heller
664
+
665
+ print(f" ✅ Erweiterte Bildaufbereitung abgeschlossen")
666
+ print(f" • Kontrast: +80%")
667
+ print(f" • Schärfe: +100%")
668
+ print(f" • Helligkeit: +10%")
669
+
670
+ # Für SAM: Verwende aufbereiteten Ausschnitt
671
+ image = enhanced_image
672
+ x1, y1, x2, y2 = rel_x1, rel_y1, rel_x2, rel_y2
673
+
674
+ print(" 🔄 SAM wird auf aufbereitetem Ausschnitt ausgeführt")
675
+ print(f" 📊 SAM-Eingabegröße: {image.size}")
676
+
677
+ # ============================================================
678
+ # SAM-AUSFÜHRUNG
679
+ # ============================================================
680
+ print("-" * 60)
681
+ print(f"📦 BOUNDING BOX DETAILS FÜR SAM:")
682
+ print(f" Bild-Größe für SAM: {image.size}")
683
+ print(f" BBox Koordinaten: [{x1}, {y1}, {x2}, {y2}]")
684
+ print(f" BBox Dimensionen: {x2-x1}px × {y2-y1}px")
685
+
686
+ # Vorbereitung für SAM2 - WICHTIG: NUR EINE BBOX
687
+ print("-" * 60)
688
+ print("🖼️ BILDAUFBEREITUNG FÜR SAM 2")
689
+ # SAM erwartet NumPy-Array, kein PIL
690
+ image_np = np.array(image.convert("RGB"))
691
+
692
+ # Immer nur eine BBox verwenden (SAM 2 erwartet genau 1)
693
+ input_boxes = [[[x1, y1, x2, y2]]]
694
+
695
+ # Punkt in der BBox-Mitte (zur Ünterstützung von SAM damit BBox nicht zu dicht um Kopf gezogen werden muß!)
696
+ center_x = (x1 + x2) // 2
697
+ center_y = (y1 + y2) // 2
698
+
699
+ # Punkt im Gesicht (30% höher vom Mittelpunkt)(auch für größere BBox)
700
+ bbox_height = y2 - y1
701
+ face_offset = int(bbox_height * 0.3)
702
+ face_x = center_x
703
+ face_y = center_y - face_offset
704
+ face_y = max(y1 + 10, min(face_y, y2 - 10)) # In BBox halten
705
+
706
+ # BEIDE Punkte kombinieren
707
+ input_points = [[[[center_x, center_y], [face_x, face_y]]]] # ZWEI Punkte
708
+ input_labels = [[[1, 1]]] # Beide sind positive Prompts
709
+
710
+ print(f" 🎯 SAM-Prompt: BBox [{x1},{y1},{x2},{y2}]")
711
+ print(f" 👁️ Punkte: Mitte ({center_x},{center_y}), Gesicht ({face_x},{face_y})")
712
+
713
+ # Aufruf des SAM-Prozessors mit den Variablen. Der Processor verpackt diese Rohdaten
714
+ # in die für das SAM-Modell erforderlichen Tensoren und speichert sie in inputs.
715
+ inputs = self.sam_processor(
716
+ image_np,
717
+ input_boxes=input_boxes,
718
+ input_points=input_points, # ZWEI Punkte
719
+ input_labels=input_labels, # Zwei Labels
720
+ return_tensors="pt"
721
+ ).to(self.device) # Ohne .to(self.device) werden die Tensoren standardmäßig im CPU-RAM erzeugt und gespeichert! Da GPU-Fehler!
722
+
723
+ print(f"✅ Processor-Ausgabe: Dictionary mit {len(inputs)} Schlüsseln: {list(inputs.keys())}")
724
+ print(f" - 'pixel_values' Shape: {inputs['pixel_values'].shape}")
725
+ print(f" - 'input_boxes' Shape: {inputs['input_boxes'].shape}")
726
+ if 'input_points' in inputs:
727
+ print(f" - 'input_points' Shape: {inputs['input_points'].shape}")
728
+
729
+ # 4. SAM2 Vorhersage
730
+ print("-" * 60)
731
+ print("🧠 SAM 2 INFERENZ (Vorhersage)")
732
+ with torch.no_grad():
733
+ print(" Führe Vorhersage durch...")
734
+ outputs = self.sam_model(**inputs)
735
+ print(f"✅ Vorhersage abgeschlossen")
736
+ print(f" Anzahl der Vorhersagemasken: {outputs.pred_masks.shape[2]}")
737
+
738
+ # 5. Maske extrahieren
739
+ print("📏 SCHRITT 6: MASKE EXTRAHIEREN")
740
+
741
+ num_masks = outputs.pred_masks.shape[2]
742
+ print(f" SAM lieferte {num_masks} verschiedene Masken")
743
+
744
+ #============
745
+ #Doppelte Berechnung: CROP und Original damit Heuristik
746
+ # auf Original berechnet werden kann und Weiterverarbeitung auf Crop
747
+ #==============
748
+
749
+ # Masken speichern in den Arrays
750
+ all_masks_crop = []
751
+ all_masks_original = []
752
+
753
+ for i in range(num_masks):
754
+ single_mask = outputs.pred_masks[:, :, i, :, :]
755
+ #Für Heuristik SAM-Masken auf Original-Bildgröße
756
+ resized_mask_original = F.interpolate(
757
+ single_mask,
758
+ size=(original_image.height, original_image.width),
759
+ mode='bilinear',
760
+ align_corners=False
761
+ ).squeeze()
762
+
763
+ mask_np_original = resized_mask_original.sigmoid().cpu().numpy()
764
+ all_masks_original.append(mask_np_original)
765
+
766
+ # 2. FÜR VERARBEITUNG: Auf CROP-GRÖSSE interpolieren
767
+ resized_mask_crop = F.interpolate(
768
+ single_mask,
769
+ size=(image.height, image.width), # CROP-Größe!
770
+ mode='bilinear',
771
+ align_corners=False
772
+ ).squeeze()
773
+ mask_np_crop = resized_mask_crop.sigmoid().cpu().numpy()
774
+ all_masks_crop.append(mask_np_crop)
775
+
776
+ # Debug-Info
777
+ mask_binary_crop = (mask_np_crop > 0.5).astype(np.uint8)
778
+ mask_binary_original = (mask_np_original > 0.5).astype(np.uint8)
779
+ print(f" Maske {i+1}: Crop={np.sum(mask_binary_crop):,}px, "
780
+ f"Original={np.sum(mask_binary_original):,}px")
781
+
782
+
783
+ # ============================================================
784
+ # HEURISTIK
785
+ # ============================================================
786
+ print("🤔 SCHRITT 6: MASKENAUSWAHL MIT MODUS-SPEZIFISCHER HEURISTIK")
787
+
788
+ bbox_center = ((original_bbox[0] + original_bbox[2]) // 2,
789
+ (original_bbox[1] + original_bbox[3]) // 2)
790
+ bbox_area = (original_bbox[2] - original_bbox[0]) * (original_bbox[3] - original_bbox[1])
791
+
792
+
793
+ best_mask_idx = 0
794
+ best_score = -1
795
+
796
+ for i, mask_np in enumerate(all_masks_original):
797
+ mask_max = mask_np.max()
798
+
799
+ # Grundlegende Filterung
800
+ if mask_max < 0.3:
801
+ print(f" ❌ Maske {i+1}: Zu niedrige Konfidenz ({mask_max:.3f}), überspringe")
802
+ continue
803
+
804
+ # Adaptiver Threshold
805
+ adaptive_threshold = max(0.3, mask_max * 0.7)
806
+ mask_binary = (mask_np > adaptive_threshold).astype(np.uint8)
807
+
808
+ if np.sum(mask_binary) == 0:
809
+ print(f" ❌ Maske {i+1}: Keine Pixel nach Threshold {adaptive_threshold:.3f}")
810
+ continue
811
+
812
+ mask_area_pixels = np.sum(mask_binary)
813
+
814
+ # ============================================================
815
+ # SPEZIALHEURISTIK
816
+ # ============================================================
817
+
818
+ print(f" 🔍 Analysiere Maske {i+1} mit GESICHTS-HEURISTIK")
819
+
820
+ # 1. FLÄCHENBASIERTE BEWERTUNG (40%)
821
+ area_ratio = mask_area_pixels / bbox_area
822
+ print(f" 📐 Flächen-Ratio: {area_ratio:.3f} ({mask_area_pixels:,} / {bbox_area:,} Pixel)")
823
+
824
+ # Optimale Kopfgröße: 80-120% der BBox
825
+ if area_ratio < 0.6:
826
+ print(f" ⚠️ Fläche zu klein für Kopf (<60% der BBox)")
827
+ area_score = area_ratio * 0.5 # Stark bestrafen
828
+ elif area_ratio > 1.5:
829
+ print(f" ⚠️ Fläche zu groß für Kopf (>150% der BBox)")
830
+ area_score = 2.0 - area_ratio # Linear bestrafen
831
+ elif 0.8 <= area_ratio <= 1.2:
832
+ area_score = 1.0 # Perfekte Größe
833
+ print(f" ✅ Perfekte Kopfgröße (80-120% der BBox)")
834
+ else:
835
+ # Sanfte Abweichung
836
+ area_score = 1.0 - abs(area_ratio - 1.0) * 0.5
837
+
838
+ # 2. KOMPAKTHEIT/SOLIDITÄT (30%)
839
+ labeled_mask = measure.label(mask_binary)
840
+ regions = measure.regionprops(labeled_mask)
841
+
842
+ if len(regions) == 0:
843
+ compactness_score = 0.1
844
+ print(f" ❌ Keine zusammenhängenden Regionen gefunden")
845
+ else:
846
+ # Größte Region finden (sollte der Kopf sein)
847
+ largest_region = max(regions, key=lambda r: r.area)
848
+
849
+ # Solidität = Fläche / konvexe Hüllenfläche
850
+ solidity = largest_region.solidity if hasattr(largest_region, 'solidity') else 0.7
851
+
852
+ # Exzentrizität (wie elliptisch) - Köpfe sind tendenziell elliptisch
853
+ eccentricity = largest_region.eccentricity if hasattr(largest_region, 'eccentricity') else 0.5
854
+
855
+ # Perfekt ronde Formen (Kreis) sind 0, Linie wäre 1
856
+ # Köpfe haben typischerweise 0.5-0.8
857
+ if 0.4 <= eccentricity <= 0.9:
858
+ eccentricity_score = 1.0 - abs(eccentricity - 0.65) * 2
859
+ else:
860
+ eccentricity_score = 0.2
861
+
862
+ compactness_score = (solidity * 0.6 + eccentricity_score * 0.4)
863
+ print(f" 🎯 Kompaktheits-Analyse:")
864
+ print(f" • Solidität (Fläche/Konvex): {solidity:.3f}")
865
+ print(f" • Exzentrizität (Form): {eccentricity:.3f}")
866
+ print(f" • Kompaktheits-Score: {compactness_score:.3f}")
867
+
868
+ # 3. BBOX-ÜBERLAPPUNG (20%)
869
+ bbox_mask = np.zeros((original_image.height, original_image.width), dtype=np.uint8)
870
+
871
+ bbox_mask[original_bbox[1]:original_bbox[3], original_bbox[0]:original_bbox[2]] = 1
872
+
873
+ overlap = np.sum(mask_binary & bbox_mask)
874
+
875
+ bbox_overlap_ratio = overlap / mask_area_pixels if mask_area_pixels > 0 else 0
876
+
877
+ print(f" 📍 BBox-Überlappung: {overlap:,} von {mask_area_pixels:,} Pixeln ({bbox_overlap_ratio:.1%})")
878
+
879
+
880
+ # Für Kopf: Sollte großteils in BBox sein (mind. 70%)
881
+ if bbox_overlap_ratio >= 0.7:
882
+ bbox_score = 1.0
883
+ print(f" ✅ Hohe BBox-Überlappung: {bbox_overlap_ratio:.3f} ({overlap:,} Pixel)")
884
+ elif bbox_overlap_ratio >= 0.5:
885
+ bbox_score = bbox_overlap_ratio * 1.2
886
+ print(f" ⚠️ Mittlere BBox-Überlappung: {bbox_overlap_ratio:.3f}")
887
+ else:
888
+ bbox_score = bbox_overlap_ratio * 0.8
889
+ print(f" ❌ Geringe BBox-Überlappung: {bbox_overlap_ratio:.3f}")
890
+
891
+ # SAM-KONFIDENZ (10%)
892
+ confidence_score = mask_max
893
+
894
+ # GESAMTSCORE für Gesicht
895
+ score = (
896
+ area_score * 0.4 + # 40% Flächenpassung
897
+ compactness_score * 0.3 + # 30% Kompaktheit
898
+ bbox_score * 0.2 + # 20% BBox-Überlappung
899
+ confidence_score * 0.1 # 10% Konfidenz
900
+ )
901
+
902
+ print(f" 📊 GESICHTS-SCORES für Maske {i+1}:")
903
+ print(f" • Flächen-Score: {area_score:.3f}")
904
+ print(f" • Kompaktheits-Score: {compactness_score:.3f}")
905
+ print(f" • BBox-Überlappungs-Score: {bbox_score:.3f}")
906
+ print(f" • Konfidenz-Score: {confidence_score:.3f}")
907
+ print(f" • GESAMTSCORE: {score:.3f}")
908
+
909
+ if score > best_score:
910
+ best_score = score
911
+ best_mask_idx = i
912
+ print(f" 🏆 Neue beste Maske: Nr. {i+1} mit Score {score:.3f}")
913
+
914
+ print(f"✅ Beste Maske ausgewählt: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
915
+
916
+ # Beste Maske verwenden
917
+ mask_np = all_masks_crop[best_mask_idx]
918
+ max_val = mask_np.max()
919
+ print(f"🔍 Maximaler SAM-Konfidenzwert der besten Maske: {max_val:.3f}")
920
+
921
+ # ============================================================
922
+ # THRESHOLD-BESTIMMUNG
923
+ # ============================================================
924
+ # Spezieller Threshold für Gesichter
925
+ if max_val < 0.5:
926
+ dynamic_threshold = 0.25
927
+ print(f" ⚠️ SAM ist unsicher für Gesicht (max_val={max_val:.3f} < 0.5)")
928
+ elif max_val < 0.8:
929
+ dynamic_threshold = max_val * 0.65 # Mittlerer Threshold
930
+ print(f" ℹ️ SAM ist mäßig sicher für Gesicht (max_val={max_val:.3f})")
931
+ else:
932
+ dynamic_threshold = max_val * 0.75 # Hoher Threshold
933
+ print(f" ✅ SAM ist sicher für Gesicht (max_val={max_val:.3f} >= 0.8)")
934
+
935
+ print(f" 🎯 Gesichts-Threshold: {dynamic_threshold:.3f}")
936
+
937
+ # Binärmaske erstellen
938
+ print("🐛 DEBUG THRESHOLD:")
939
+ print(f" mask_np Min/Max: {mask_np.min():.3f}/{mask_np.max():.3f}")
940
+ print(f" dynamic_threshold: {dynamic_threshold:.3f}")
941
+
942
+ mask_array = (mask_np > dynamic_threshold).astype(np.uint8) * 255
943
+
944
+ print(f"🚨 DEBUG BINÄRMASKE:")
945
+ print(f" mask_array Min/Max: {mask_array.min()}/{mask_array.max()}")
946
+ print(f" Weiße Pixel in mask_array: {np.sum(mask_array > 0)}")
947
+ print(f" Anteil weiße Pixel: {np.sum(mask_array > 0) / mask_array.size:.1%}")
948
+
949
+ # Fallback wenn Maske leer
950
+ if mask_array.max() == 0:
951
+ print("⚠️ KRITISCH: Binärmaske ist leer! Erzwinge Testmaske (BBox).")
952
+ print(f" 🚨 BBox für Fallback: x1={x1}, y1={y1}, x2={x2}, y2={y2}")
953
+
954
+ test_mask = np.zeros((image.height, image.width), dtype=np.uint8)
955
+ cv2.rectangle(test_mask, (x1, y1), (x2, y2), 255, -1)
956
+
957
+ mask_array = test_mask
958
+ print(f"🐛 DEBUG ERZWUNGENE MASKE: Weiße Pixel: {np.sum(mask_array > 0)}")
959
+
960
+ # Rohmaske speichern
961
+ raw_mask_array = mask_array.copy()
962
+
963
+ # ============================================================
964
+ # POSTPROCESSING
965
+ # ============================================================
966
+
967
+ print("👤 GESICHTS-SPEZIFISCHES POSTPROCESSING")
968
+
969
+ # 1. Größte zusammenhängende Komponente finden
970
+ labeled_array, num_features = ndimage.label(mask_array)
971
+
972
+ if num_features > 0:
973
+ print(f" 🔍 Gefundene Komponenten: {num_features}")
974
+
975
+ sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
976
+ largest_component_idx = np.argmax(sizes) + 1
977
+
978
+ print(f" 👑 Größte Komponente: Nr. {largest_component_idx} mit {sizes[largest_component_idx-1]:,} Pixel")
979
+
980
+ # NUR die größte Komponente behalten (der Kopf)
981
+ mask_array = np.where(labeled_array == largest_component_idx, mask_array, 0)
982
+
983
+ # MORPHOLOGISCHE OPERATIONEN FÜR SAUBEREN KOPF
984
+ print(" ⚙️ Morphologische Operationen für sauberen Kopf")
985
+
986
+ # Zuerst CLOSE, um kleine Löcher im Kopf zu füllen
987
+ kernel_close = np.ones((7, 7), np.uint8)
988
+ mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel_close, iterations=1)
989
+ print(" • MORPH_CLOSE (7x7) - Löcher im Kopf füllen")
990
+
991
+ # Dann OPEN, um kleine Ausreißer zu entfernen
992
+ kernel_open = np.ones((5, 5), np.uint8)
993
+ mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel_open, iterations=1)
994
+ print(" • MORPH_OPEN (5x5) - Rauschen entfernen")
995
+
996
+ # ============================================================
997
+ # KRITISCH: MASKE IMMER ZURÜCK AUF ORIGINALGRÖSSE (auch bei Fallback!)
998
+ # ============================================================
999
+ print("-" * 60)
1000
+ print("🔄 MASKE IMMER ZURÜCK AUF ORIGINALGRÖSSE TRANSFORMIEREN")
1001
+
1002
+ # WICHTIG: Immer die richtigen Crop-Koordinaten verwenden
1003
+ temp_mask = Image.fromarray(mask_array).convert("L")
1004
+ print(f" Maskengröße auf Ausschnitt: {temp_mask.size}")
1005
+
1006
+ # Maske auf ORIGINALBILDGRÖSSE bringen
1007
+ final_mask = Image.new("L", original_image.size, 0)
1008
+ print(f" Leere Maske in Originalgröße: {final_mask.size}")
1009
+
1010
+ # Immer die gespeicherten Crop-Koordinaten verwenden
1011
+ if 'crop_x1' in locals() and 'crop_y1' in locals():
1012
+ final_mask.paste(temp_mask, (crop_x1, crop_y1))
1013
+ print(f" Maskenposition im Original: ({crop_x1}, {crop_y1})")
1014
+ else:
1015
+ # Fallback: Zentrieren
1016
+ x_offset = (original_image.width - temp_mask.width) // 2
1017
+ y_offset = (original_image.height - temp_mask.height) // 2
1018
+ final_mask.paste(temp_mask, (x_offset, y_offset))
1019
+ print(f" ⚠️ Keine Crop-Koordinaten, zentriert: ({x_offset}, {y_offset})")
1020
+
1021
+ mask_array = np.array(final_mask)
1022
+ print(f" ✅ Maske zurück auf Originalgröße skaliert: {mask_array.shape}")
1023
+
1024
+ # Bild-Referenz zurücksetzen
1025
+ image = original_image
1026
+ print(f" 🔄 Bild-Referenz wieder auf Original gesetzt: {image.size}")
1027
+
1028
+ # ============================================================
1029
+ # ABSCHLIESSENDE STATISTIK
1030
+ # ============================================================
1031
+
1032
+ print("📊 FINALE MASKEN-STATISTIK")
1033
+
1034
+ # Weiße Pixel zählen
1035
+ white_pixels = np.sum(mask_array > 0)
1036
+ total_pixels = mask_array.size
1037
+ white_ratio = white_pixels / total_pixels * 100
1038
+
1039
+ # Original-BBox Fläche (vor Crop)
1040
+ original_bbox_width = original_bbox[2] - original_bbox[0]
1041
+ original_bbox_height = original_bbox[3] - original_bbox[1]
1042
+ original_face_area = original_bbox_width * original_bbox_height
1043
+ coverage_ratio = white_pixels / original_face_area if original_face_area > 0 else 0
1044
+ print(f" 👤 GESICHTSABDECKUNG: {coverage_ratio:.1%} der ursprünglichen BBox")
1045
+
1046
+ print(f" Weiße Pixel (Veränderungsbereich): {white_pixels:,} ({white_ratio:.1f}%)")
1047
+ print(f" Schwarze Pixel (Erhaltungsbereich): {total_pixels-white_pixels:,} ({100-white_ratio:.1f}%)")
1048
+ print(f" Gesamtpixel: {total_pixels:,}")
1049
+
1050
+ # Warnungen basierend auf Abdeckung
1051
+ if coverage_ratio < 0.7:
1052
+ print(f" ⚠️ WARNUNG: Geringe Gesichtsabdeckung ({coverage_ratio:.1%})")
1053
+ elif coverage_ratio > 1.3:
1054
+ print(f" ⚠️ WARNUNG: Sehr hohe Gesichtsabdeckung ({coverage_ratio:.1%})")
1055
+ elif 0.8 <= coverage_ratio <= 1.2:
1056
+ print(f" ✅ OPTIMALE Gesichtsabdeckung ({coverage_ratio:.1%})")
1057
+
1058
+ # Zurück zu PIL Image
1059
+ mask = Image.fromarray(mask_array).convert("L")
1060
+ raw_mask = Image.fromarray(raw_mask_array).convert("L")
1061
+
1062
+ print("#" * 80)
1063
+ print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
1064
+ print(f"📐 Finale Maskengröße: {mask.size}")
1065
+ print(f"🎛️ Verwendeter Modus: {mode}")
1066
+
1067
+ print(f"👤 Crop={crop_size}×{crop_size}px, Heuristik-Score={best_score:.3f}")
1068
+ print(f"👤 Kopfabdeckung: {coverage_ratio:.1%} der BBox")
1069
+
1070
+ print(f"🔍 DEBUG FINALE MASKE:")
1071
+ print(f" mask_array Min/Max: {mask_array.min()}/{mask_array.max()}, Typ: {mask_array.dtype}")
1072
+ print(f" Weiße Pixel final: {np.sum(mask_array > 0)}")
1073
+
1074
+ print("#" * 80)
1075
+
1076
+ return mask, raw_mask #in app.py wird mask immer auf t12 skaliert
1077
+
1078
+ # ============================================================
1079
+ # UNBEKANNTER MODUS
1080
+ # ============================================================
1081
+ else:
1082
+ print(f"❌ Unbekannter Modus: {mode}")
1083
+ return self._create_rectangular_mask(image, bbox_coords, "focus_change")
1084
+
1085
+ except Exception as e:
1086
+ print("❌" * 40)
1087
+ print("❌ FEHLER IN SAM 2 SEGMENTIERUNG")
1088
+ print(f"Fehler: {str(e)[:200]}")
1089
+ print("❌" * 40)
1090
+ import traceback
1091
+ traceback.print_exc()
1092
+
1093
+ # Fallback
1094
+ fallback_mask = self._create_rectangular_mask(original_image, original_bbox, mode)
1095
+ if fallback_mask.size != original_image.size:
1096
+ print(f" ⚠️ Fallback-Maske angepasst: {fallback_mask.size} → {original_image.size}")
1097
+ fallback_mask = fallback_mask.resize(original_image.size, Image.Resampling.NEAREST)
1098
+
1099
+ return fallback_mask, fallback_mask
1100
+
1101
+
1102
+
1103
+
1104
+
1105
+
1106
+
1107
+
1108
+
1109
+
1110
+
1111
  def create_sam_mask(self, image, bbox_coords, mode):
1112
  """
1113
  ERWEITERTE Funktion: Erstellt präzise Maske mit SAM 2