Astridkraft commited on
Commit
4e659cc
·
verified ·
1 Parent(s): 735c1b0

Update controlnet_module.py

Browse files
Files changed (1) hide show
  1. controlnet_module.py +247 -129
controlnet_module.py CHANGED
@@ -127,65 +127,116 @@ class ControlNetProcessor:
127
 
128
  # 2. Validiere BBox
129
  x1, y1, x2, y2 = self._validate_bbox(image, bbox_coords)
 
 
 
130
 
131
  # ============================================================
132
  # SPEZIALBEHANDLUNG NUR FÜR face_only_change
133
  # ============================================================
134
  if mode == "face_only_change":
135
  print("-" * 60)
136
- print("👤 SPEZIALMODUS: NUR GESICHT - ERSTELLE FOKUSIERTEN AUSSCHNITT")
137
  print("-" * 60)
138
 
139
- # Originalbild und Koordinaten sichern
 
 
140
  original_image = image
141
- original_bbox = (x1, y1, x2, y2)
142
 
143
- # Puffer um die BBox berechnen (20% der BBox-Größe, mindestens 50px)
144
- padding_x = max(50, int((x2 - x1) * 0.2))
145
- padding_y = max(50, int((y2 - y1) * 0.2))
146
-
147
- # Ausschnitt-Koordinaten berechnen (innerhalb der Bildgrenzen)
148
- crop_x1 = max(0, x1 - padding_x)
149
- crop_y1 = max(0, y1 - padding_y)
150
- crop_x2 = min(image.width, x2 + padding_x)
151
- crop_y2 = min(image.height, y2 + padding_y)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
- print(f" 📐 Original-BBox: [{x1}, {y1}, {x2}, {y2}]")
154
- print(f" 📏 Original-BBox Größe: {x2-x1} × {y2-y1} px")
155
- print(f" 🔲 Ausschnitt-Bereich: [{crop_x1}, {crop_y1}, {crop_x2}, {crop_y2}]")
156
- print(f" 📏 Ausschnitt-Größe: {crop_x2-crop_x1} × {crop_y2-crop_y1} px")
157
- print(f" 📊 Puffer: {padding_x} × {padding_y} px")
158
 
159
  # Bild ausschneiden
160
- cropped_image = image.crop((crop_x1, crop_y1, crop_x2, crop_y2))
161
- print(f" ✅ Ausschnitt erstellt: {cropped_image.size}")
162
 
163
  # ============================================================
164
- # NEU: KONTRASTVERSTÄRKUNG FÜR BESSERE SAM-ERKENNUNG
165
  # ============================================================
166
- print(" 🔍 Wende Kontrastverstärkung an für bessere Segmentierung...")
167
- contrast_enhancer = ImageEnhance.Contrast(cropped_image)
168
- cropped_image = contrast_enhancer.enhance(1.5) # 50% mehr Kontrast
169
- print(" ✅ Kontrast um 50% erhöht")
170
-
171
- # BBox-Koordinaten relativ zum Ausschnitt neu berechnen
172
  rel_x1 = x1 - crop_x1
173
  rel_y1 = y1 - crop_y1
174
  rel_x2 = x2 - crop_x1
175
  rel_y2 = y2 - crop_y1
176
 
177
- print(f" 🎯 Relative BBox im Ausschnitt: [{rel_x1}, {rel_y1}, {rel_x2}, {rel_y2}]")
 
 
 
 
 
 
178
  print(f" 📏 Relative BBox Größe: {rel_x2-rel_x1} × {rel_y2-rel_y1} px")
179
 
180
- # Für SAM: Verwende Ausschnitt und relative Koordinaten
181
- image = cropped_image
 
 
 
 
 
 
 
 
182
  x1, y1, x2, y2 = rel_x1, rel_y1, rel_x2, rel_y2
183
 
184
- print(" 🔄 SAM wird auf kontrastverstärktem Ausschnitt (nicht Vollbild) ausgeführt")
 
185
 
186
  # ============================================================
187
  # GEMEINSAME SAM-LOGIK FÜR ALLE MODI
188
- # (arbeitet auf `image` - bei face_only_change ist das der Ausschnitt)
189
  # ============================================================
190
  print("-" * 60)
191
  print(f"📦 BOUNDING BOX DETAILS FÜR SAM:")
@@ -201,6 +252,11 @@ class ControlNetProcessor:
201
  print(f" Konvertiere Bild zu NumPy Array: {image_np.shape}")
202
  print(f" Erstelle Input Boxes: {input_boxes}")
203
 
 
 
 
 
 
204
  print(" Verarbeite Bild mit SAM 2 Processor...")
205
  inputs = self.sam_processor(
206
  image_np,
@@ -216,40 +272,123 @@ class ControlNetProcessor:
216
  print(" Führe Vorhersage durch...")
217
  outputs = self.sam_model(**inputs)
218
  print(f"✅ Vorhersage abgeschlossen")
 
219
 
220
  # 5. Maske extrahieren und auf Originalgröße skalieren
221
- single_mask = outputs.pred_masks[:, :, 0, :, :]
222
- print(f" Rohmaske Shape vor Interpolation: {single_mask.shape}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
- final_mask = F.interpolate(
225
- single_mask,
226
- size=(image.height, image.width),
227
- mode='bilinear',
228
- align_corners=False
229
- ).squeeze()
230
- print(f" Maske nach Interpolation: {final_mask.shape}")
231
-
232
- # 6. In NumPy konvertieren und Schwellenwert anwenden
233
- mask_np = final_mask.sigmoid().cpu().numpy()
234
- print(f" Nach Sigmoid und CPU: {mask_np.shape}, Wertebereich: [{mask_np.min():.3f}, {mask_np.max():.3f}]")
235
 
236
  # ============================================================
237
- # KRITISCH: DYNAMISCHER THRESHOLD FÜR UNSICHERE SAM-VORHERSAGEN
238
  # ============================================================
239
  max_val = mask_np.max()
240
- print(f" 🔍 Maximaler SAM-Konfidenzwert: {max_val:.3f}")
241
 
242
- # NEUE LOGIK: Unterscheidung basierend auf SAM-Konfidenz
243
  if max_val < 0.6:
244
- # Fall: SAM ist unsicher (wie in Ihrem Log: max_val=0.505)
245
- # Verwende festen, niedrigen Threshold
246
- dynamic_threshold = 0.2 # Sehr niedrig für unsichere Vorhersagen
247
  print(f" ⚠️ SAM ist unsicher (max_val={max_val:.3f} < 0.6)")
248
  print(f" 🎯 Verwende festen niedrigen Threshold: {dynamic_threshold:.3f}")
249
  else:
250
- # Fall: SAM ist sicher
251
- # Verwende prozentualen Threshold basierend auf Maximum
252
- dynamic_threshold = max_val * 0.95
253
  print(f" ✅ SAM ist sicher (max_val={max_val:.3f} >= 0.6)")
254
  print(f" 🎯 Dynamischer Threshold: {dynamic_threshold:.3f} (80% von Maximum)")
255
 
@@ -258,71 +397,76 @@ class ControlNetProcessor:
258
  print(f" Nach Threshold ({dynamic_threshold:.3f}): {mask_array.shape}, Unique Werte: {unique_vals}")
259
 
260
  # ============================================================
261
- # NEU: VORFILTERUNG FÜR KLEINE KOMPONENTEN (RAUSCHEN ENTFERNEN)
262
  # ============================================================
263
- if np.sum(mask_array > 0) > 0: # Nur wenn weiße Pixel existieren
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  labeled_array, num_features = ndimage.label(mask_array)
265
  if num_features > 1:
266
- print(f" 🧹 Vorfilterung: Gefundene Komponenten vor Filterung: {num_features}")
267
 
268
  sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
269
- min_size = 1000 # Minimale Größe für eine sinnvolle Komponente
 
 
 
 
270
 
271
- # Zähle, wie viele Komponenten die Mindestgröße erreichen
272
- valid_components = 0
273
  for i in range(1, num_features + 1):
274
- if sizes[i-1] >= min_size:
275
- valid_components += 1
276
- else:
277
- # Entferne kleine Komponenten (Rauschen)
278
  mask_array = np.where(labeled_array == i, 0, mask_array)
279
-
280
- print(f" ✅ Entferne kleine Komponenten (<{min_size}px): {num_features} → {valid_components} Komponenten")
 
 
 
 
 
 
 
 
 
 
 
281
 
282
- # 7. BEIDE MASKEN ERSTELLEN (vor Nachbearbeitung)
283
  original_mask_array = mask_array.copy() # Person weiß (255), Hintergrund schwarz (0)
284
  inverted_mask_array = 255 - mask_array # Person schwarz (0), Hintergrund weiß (255)
285
 
286
  print("-" * 60)
287
- print(f"🔧 STARTE NACHBEARBEITUNG FÜR MODUS: {mode}")
288
  print(f" Original-Maske (Person weiß): {original_mask_array.shape}")
289
  print(f" Invertierte Maske (Person schwarz): {inverted_mask_array.shape}")
290
 
291
- # 8. MODUS-SPEZIFISCHE NACHBEARBEITUNG
292
  if mode == "environment_change":
293
  print("🌳 MODUS: UMWELT ÄNDERN")
294
  # Arbeite auf der INVERTIERTEN Maske (Person schwarz, Hintergrund weiß)
295
  mask_array = inverted_mask_array.copy()
296
  print(" Arbeite auf invertierter Maske (Person schwarz, Hintergrund weiß)")
297
 
298
- # Größte weiße Komponente finden (Hintergrund)
299
- labeled_array, num_features = ndimage.label(mask_array)
300
- print(f" Gefundene weiße Komponenten (Hintergrund): {num_features}")
301
-
302
- # Nur wenn wir mehrere weiße Komponenten haben (z.B. Hintergrund durch Person geteilt)
303
- if num_features > 1:
304
- # Finde alle weißen Komponenten
305
- sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
306
- print(f" Größen der weißen Komponenten: {sizes}")
307
-
308
- # Verbinde alle weißen Komponenten (Hintergrundteile)
309
- for i in range(1, num_features + 1):
310
- mask_array = np.where(labeled_array == i, 255, mask_array)
311
- print(f" ✅ Verbinde {num_features} Hintergrund-Komponenten")
312
-
313
  # Morphologische Operationen für saubere Umgebung
314
  kernel = np.ones((5,5), np.uint8)
315
- print(f" Wende MORPH_CLOSE an (Kernel 5x5) um schwarze Löcher zu füllen...")
316
  mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
317
- print(f" Wende MORPH_OPEN an (Kernel 5x5) um kleine weiße Inseln zu entfernen...")
318
  mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel)
319
-
320
- # Umgebung erweitern für besseren Personenschutz (2 Pixel)
321
- print(f" Wende DILATE an (Kernel 2x2) für Personenschutz...")
322
  mask_array = cv2.dilate(mask_array, np.ones((2,2), np.uint8), iterations=1)
323
-
324
- # Leichte Unschärfe für natürlichere Übergänge
325
- print(f" Wende GaussianBlur an (Kernel 3x3) für glatte Übergänge...")
326
  mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0)
327
 
328
  print(" ✅ Umwelt-Modus: Person geschützt, Hintergrund optimiert")
@@ -335,70 +479,37 @@ class ControlNetProcessor:
335
 
336
  # Größte weiße Komponente behalten (Person)
337
  labeled_array, num_features = ndimage.label(mask_array)
338
- print(f" Gefundene weiße Komponenten (Person): {num_features}")
339
-
340
  if num_features > 1:
341
  sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
342
- print(f" Größen der weißen Komponenten: {sizes}")
343
  largest_component = np.argmax(sizes) + 1
344
  mask_array = np.where(labeled_array == largest_component, mask_array, 0)
345
- print(f" ✅ Behalte größte Person-Komponente ({num_features} Komponenten)")
346
 
347
  # Maske leicht erweitern für bessere Abdeckung
348
  kernel = np.ones((3,3), np.uint8)
349
- print(f" Wende DILATE an (Kernel 3x3) für bessere Abdeckung...")
350
  mask_array = cv2.dilate(mask_array, kernel, iterations=1)
351
-
352
- # Morphologische Glättung
353
- print(f" Wende MORPH_CLOSE an (Kernel 3x3) für glatte Kanten...")
354
  mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
355
 
356
  print(" ✅ Focus-Modus: Person verändert, Hintergrund geschützt")
357
 
358
  elif mode == "face_only_change":
359
- print("👤 MODUS: NUR GESICHT ÄNDERN (AUF AUSSCHNITT)")
360
  # Arbeite auf der ORIGINAL-Maske (Person weiß, Hintergrund schwarz)
361
  mask_array = original_mask_array.copy()
362
  print(" Arbeite auf originaler Maske (Person weiß, Hintergrund schwarz)")
363
 
364
- # Größte weiße Komponente behalten (Person)
365
- labeled_array, num_features = ndimage.label(mask_array)
366
- print(f" Gefundene weiße Komponenten auf AUSSCHNITT: {num_features}")
367
-
368
- if num_features > 0:
369
- sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
370
- print(f" Größen der weißen Komponenten auf AUSSCHNITT: {sizes[:10]}...") # Nur erste 10 anzeigen
371
-
372
- if num_features > 1:
373
- # WICHTIG: Für Gesicht nehmen wir die GRÖSSTE Komponente im AUSSCHNITT
374
- # (Im Ausschnitt sollte das das Gesicht sein, nicht der Hintergrund)
375
- largest_component = np.argmax(sizes) + 1
376
- mask_array = np.where(labeled_array == largest_component, mask_array, 0)
377
- print(f" ✅ Behalte größte Komponente im Ausschnitt ({num_features} Komponenten)")
378
- print(f" 📊 Größe der behaltenen Komponente: {sizes[largest_component-1]:,} Pixel")
379
- else:
380
- print(f" ℹ️ Nur eine Komponente gefunden, behalte diese")
381
-
382
  # Starke Erosion für präzises Gesicht
383
  kernel = np.ones((3,3), np.uint8)
384
- print(f" Wende ERODE an (Kernel 3x3, 2 Iterationen) für präzises Gesicht...")
385
  mask_array = cv2.erode(mask_array, kernel, iterations=2)
386
-
387
- # Zusätzliche Präzisions-Erosion
388
- print(f" Wende zusätzliche ERODE an (Kernel 2x2, 1 Iteration)...")
389
  mask_array = cv2.erode(mask_array, np.ones((2,2), np.uint8), iterations=1)
390
-
391
- # Sanfte Glättung der Kanten
392
- print(f" Wende GaussianBlur an (Kernel 3x3) für glatte Kanten...")
393
  mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0)
394
 
395
- print(" ✅ Gesichts-Modus: Nachbearbeitung auf Ausschnitt abgeschlossen")
396
 
397
  # ============================================================
398
- # SPEZIALSCHRITT NUR FÜR face_only_change: MASKE ZURÜCKSKALIEREN
399
  # ============================================================
400
  print("-" * 60)
401
- print("🔄 SPEZIALSCHRITT: MASKE VOM AUSSCHNITT ZURÜCK AUF ORIGINALGRÖSSE")
402
 
403
  # Temporäre Maske aus dem Array erstellen
404
  temp_mask = Image.fromarray(mask_array).convert("L")
@@ -428,10 +539,16 @@ class ControlNetProcessor:
428
  black_ratio = 100 - white_ratio
429
 
430
  print("-" * 60)
431
- print("📊 MASKEN-STATISTIK (NACHBEARBEITET)")
432
  print(f" Weiße Pixel (Veränderungsbereich): {white_pixels:,} ({white_ratio:.1f}%)")
433
  print(f" Schwarze Pixel (Erhaltungsbereich): {black_pixels:,} ({black_ratio:.1f}%)")
434
  print(f" Gesamtpixel: {total_pixels:,}")
 
 
 
 
 
 
435
 
436
  # 10. Zurück zu PIL Image
437
  mask = Image.fromarray(mask_array).convert("L")
@@ -440,6 +557,7 @@ class ControlNetProcessor:
440
  print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
441
  print(f"📐 Finale Maskengröße: {mask.size}")
442
  print(f"🎛️ Verwendeter Modus: {mode}")
 
443
  print("#" * 80)
444
  return mask
445
 
 
127
 
128
  # 2. Validiere BBox
129
  x1, y1, x2, y2 = self._validate_bbox(image, bbox_coords)
130
+ original_bbox = (x1, y1, x2, y2)
131
+ original_bbox_size = (x2 - x1, y2 - y1)
132
+ print(f"📏 Original-BBox Größe: {original_bbox_size[0]} × {original_bbox_size[1]} px")
133
 
134
  # ============================================================
135
  # SPEZIALBEHANDLUNG NUR FÜR face_only_change
136
  # ============================================================
137
  if mode == "face_only_change":
138
  print("-" * 60)
139
+ print("👤 SPEZIALMODUS: NUR GESICHT - EMPFOHLENER WORKFLOW")
140
  print("-" * 60)
141
 
142
+ # ============================================================
143
+ # SCHRITT 1: Originalbild sichern
144
+ # ============================================================
145
  original_image = image
146
+ print(f"💾 Originalbild gesichert: {original_image.size}")
147
 
148
+ # ============================================================
149
+ # SCHRITT 2: Crop = BBox × 2.0 (einmal, sauber, quadratisch)
150
+ # ============================================================
151
+ print("✂️ SCHRITT 2: ERSTELLE QUADRATISCHEN AUSSCHNITT (BBox × 2.0)")
152
+
153
+ # BBox-Zentrum berechnen
154
+ bbox_center_x = (x1 + x2) // 2
155
+ bbox_center_y = (y1 + y2) // 2
156
+ print(f" 📍 BBox-Zentrum: ({bbox_center_x}, {bbox_center_y})")
157
+
158
+ # Größte Dimension der BBox finden
159
+ bbox_width = x2 - x1
160
+ bbox_height = y2 - y1
161
+ bbox_max_dim = max(bbox_width, bbox_height)
162
+ print(f" 📏 BBox Dimensionen: {bbox_width} × {bbox_height} px")
163
+ print(f" 📐 Maximale BBox-Dimension: {bbox_max_dim} px")
164
+
165
+ # Crop-Größe berechnen (BBox × 2.0)
166
+ crop_size = int(bbox_max_dim * 2.0)
167
+ print(f" 🎯 Ziel-Crop-Größe: {crop_size} × {crop_size} px (BBox × 2.0)")
168
+
169
+ # Crop-Koordinaten berechnen (zentriert um BBox)
170
+ crop_x1 = bbox_center_x - crop_size // 2
171
+ crop_y1 = bbox_center_y - crop_size // 2
172
+ crop_x2 = crop_x1 + crop_size
173
+ crop_y2 = crop_y1 + crop_size
174
+
175
+ # Sicherstellen, dass Crop innerhalb der Bildgrenzen bleibt
176
+ crop_x1 = max(0, crop_x1)
177
+ crop_y1 = max(0, crop_y1)
178
+ crop_x2 = min(original_image.width, crop_x2)
179
+ crop_y2 = min(original_image.height, crop_y2)
180
+
181
+ # Falls Crop zu klein ist, anpassen
182
+ actual_crop_width = crop_x2 - crop_x1
183
+ actual_crop_height = crop_y2 - crop_y1
184
+
185
+ if actual_crop_width < crop_size or actual_crop_height < crop_size:
186
+ # An Kanten anpassen
187
+ if crop_x1 == 0:
188
+ crop_x2 = min(original_image.width, crop_size)
189
+ elif crop_x2 == original_image.width:
190
+ crop_x1 = max(0, original_image.width - crop_size)
191
+
192
+ if crop_y1 == 0:
193
+ crop_y2 = min(original_image.height, crop_size)
194
+ elif crop_y2 == original_image.height:
195
+ crop_y1 = max(0, original_image.height - crop_size)
196
 
197
+ print(f" 🔲 Crop-Bereich: [{crop_x1}, {crop_y1}, {crop_x2}, {crop_y2}]")
198
+ print(f" 📏 Tatsächliche Crop-Größe: {crop_x2-crop_x1} × {crop_y2-crop_y1} px")
 
 
 
199
 
200
  # Bild ausschneiden
201
+ cropped_image = original_image.crop((crop_x1, crop_y1, crop_x2, crop_y2))
202
+ print(f" ✅ Quadratischer Ausschnitt erstellt: {cropped_image.size}")
203
 
204
  # ============================================================
205
+ # SCHRITT 3: BBox-Koordinaten im Crop-Koordinatensystem berechnen
206
  # ============================================================
207
+ print("📐 SCHRITT 3: BBox-KOORDINATEN TRANSFORMIEREN")
 
 
 
 
 
208
  rel_x1 = x1 - crop_x1
209
  rel_y1 = y1 - crop_y1
210
  rel_x2 = x2 - crop_x1
211
  rel_y2 = y2 - crop_y1
212
 
213
+ # Sicherstellen, dass BBox innerhalb des Crops liegt
214
+ rel_x1 = max(0, rel_x1)
215
+ rel_y1 = max(0, rel_y1)
216
+ rel_x2 = min(cropped_image.width, rel_x2)
217
+ rel_y2 = min(cropped_image.height, rel_y2)
218
+
219
+ print(f" 🎯 Relative BBox im Crop: [{rel_x1}, {rel_y1}, {rel_x2}, {rel_y2}]")
220
  print(f" 📏 Relative BBox Größe: {rel_x2-rel_x1} × {rel_y2-rel_y1} px")
221
 
222
+ # ============================================================
223
+ # SCHRITT 4: Bildkontrast verstärken für bessere Segmentierung
224
+ # ============================================================
225
+ print("🔍 SCHRITT 4: KONTRASTVERSTÄRKUNG FÜR SAM")
226
+ contrast_enhancer = ImageEnhance.Contrast(cropped_image)
227
+ enhanced_cropped_image = contrast_enhancer.enhance(1.5) # 50% mehr Kontrast
228
+ print(f" ✅ Kontrast um 50% erhöht")
229
+
230
+ # Für SAM: Verwende kontrastverstärkten Ausschnitt und relative Koordinaten
231
+ image = enhanced_cropped_image
232
  x1, y1, x2, y2 = rel_x1, rel_y1, rel_x2, rel_y2
233
 
234
+ print(" 🔄 SAM wird auf kontrastverstärktem Ausschnitt ausgeführt")
235
+ print(f" 📊 SAM-Eingabegröße: {image.size}")
236
 
237
  # ============================================================
238
  # GEMEINSAME SAM-LOGIK FÜR ALLE MODI
239
+ # (arbeitet auf `image` - bei face_only_change ist das der Crop)
240
  # ============================================================
241
  print("-" * 60)
242
  print(f"📦 BOUNDING BOX DETAILS FÜR SAM:")
 
252
  print(f" Konvertiere Bild zu NumPy Array: {image_np.shape}")
253
  print(f" Erstelle Input Boxes: {input_boxes}")
254
 
255
+ # ============================================================
256
+ # SCHRITT 4-5: SAM mit Box-Prompt = ursprüngliche BBox
257
+ # (im Crop-Koordinatensystem bei face_only_change)
258
+ # ============================================================
259
+ print("🎯 SCHRITT 4-5: SAM MIT BOX-PROMPT")
260
  print(" Verarbeite Bild mit SAM 2 Processor...")
261
  inputs = self.sam_processor(
262
  image_np,
 
272
  print(" Führe Vorhersage durch...")
273
  outputs = self.sam_model(**inputs)
274
  print(f"✅ Vorhersage abgeschlossen")
275
+ print(f" Anzahl der Vorhersagemasken: {outputs.pred_masks.shape[2]}")
276
 
277
  # 5. Maske extrahieren und auf Originalgröße skalieren
278
+ print("📏 SCHRITT 6: MASKE EXTRAHIEREN UND SKALIEREN")
279
+
280
+ # ============================================================
281
+ # SCHRITT 6: SAM liefert mehrere Masken
282
+ # ============================================================
283
+ num_masks = outputs.pred_masks.shape[2]
284
+ print(f" SAM lieferte {num_masks} verschiedene Masken")
285
+
286
+ # Extrahiere alle Masken
287
+ all_masks = []
288
+ mask_qualities = []
289
+
290
+ for i in range(num_masks):
291
+ single_mask = outputs.pred_masks[:, :, i, :, :]
292
+ resized_mask = F.interpolate(
293
+ single_mask,
294
+ size=(image.height, image.width),
295
+ mode='bilinear',
296
+ align_corners=False
297
+ ).squeeze()
298
+
299
+ mask_np = resized_mask.sigmoid().cpu().numpy()
300
+ all_masks.append(mask_np)
301
+
302
+ # Basis-Statistiken für jede Maske
303
+ mask_binary = (mask_np > 0.5).astype(np.uint8)
304
+ mask_area = np.sum(mask_binary)
305
+ print(f" Maske {i+1}: Größe={mask_area:,} Pixel, Max-Konfidenz={mask_np.max():.3f}")
306
+
307
+ # ============================================================
308
+ # SCHRITT 6: Maskenauswahl per Heuristik
309
+ # ============================================================
310
+ print("🤔 SCHRITT 6: MASKENAUSWAHL MIT HEURISTIK")
311
+
312
+ # Erwartete BBox für Heuristik (in Pixel-Koordinaten)
313
+ bbox_center = ((x1 + x2) // 2, (y1 + y2) // 2)
314
+ bbox_area = (x2 - x1) * (y2 - y1)
315
+ print(f" Erwartetes BBox-Zentrum: {bbox_center}")
316
+ print(f" Erwartete BBox-Fläche: {bbox_area:,} Pixel")
317
+
318
+ best_mask_idx = 0
319
+ best_score = -1
320
+
321
+ for i, mask_np in enumerate(all_masks):
322
+ # Threshold für binäre Maske
323
+ mask_binary = (mask_np > 0.5).astype(np.uint8)
324
+
325
+ if np.sum(mask_binary) == 0:
326
+ print(f" ❌ Maske {i+1}: Keine Pixel, überspringe")
327
+ continue
328
+
329
+ # 1. Größte Überlappung mit BBox
330
+ # Erstelle binäre BBox-Maske
331
+ bbox_mask = np.zeros((image.height, image.width), dtype=np.uint8)
332
+ bbox_mask[y1:y2, x1:x2] = 1
333
+
334
+ overlap = np.sum(mask_binary & bbox_mask)
335
+ bbox_overlap_ratio = overlap / np.sum(bbox_mask) if np.sum(bbox_mask) > 0 else 0
336
+
337
+ # 2. Schwerpunkt nahe BBox-Zentrum
338
+ y_coords, x_coords = np.where(mask_binary > 0)
339
+ if len(y_coords) > 0:
340
+ centroid_y = np.mean(y_coords)
341
+ centroid_x = np.mean(x_coords)
342
+ centroid_distance = np.sqrt((centroid_x - bbox_center[0])**2 + (centroid_y - bbox_center[1])**2)
343
+ normalized_distance = centroid_distance / max(image.width, image.height)
344
+ else:
345
+ centroid_distance = float('inf')
346
+ normalized_distance = 1.0
347
+
348
+ # 3. Maskenfläche im erwarteten Bereich
349
+ mask_area = np.sum(mask_binary)
350
+ area_ratio = mask_area / bbox_area
351
+ area_score = 1.0 - min(abs(area_ratio - 1.0), 1.0) # 1.0 ist perfekt
352
+
353
+ # 4. SAM-Konfidenz
354
+ confidence_score = mask_np.max()
355
+
356
+ # Gesamtscore berechnen (Gewichtung anpassbar)
357
+ score = (
358
+ bbox_overlap_ratio * 0.4 + # 40% Überlappung mit BBox
359
+ (1.0 - normalized_distance) * 0.3 + # 30% Zentrumsnähe
360
+ area_score * 0.2 + # 20% Flächenübereinstimmung
361
+ confidence_score * 0.1 # 10% SAM-Konfidenz
362
+ )
363
+
364
+ print(f" 📊 Maske {i+1} Scores:")
365
+ print(f" • BBox-Überlappung: {bbox_overlap_ratio:.3f} ({overlap:,} Pixel)")
366
+ print(f" • Zentrums-Distanz: {centroid_distance:.1f} px (normalisiert: {normalized_distance:.3f})")
367
+ print(f" • Flächen-Ratio: {area_ratio:.3f} ({mask_area:,} Pixel)")
368
+ print(f" • Max-Konfidenz: {confidence_score:.3f}")
369
+ print(f" • GESAMTSCORE: {score:.3f}")
370
+
371
+ if score > best_score:
372
+ best_score = score
373
+ best_mask_idx = i
374
 
375
+ print(f"✅ Beste Maske ausgewählt: Nr. {best_mask_idx+1} mit Score {best_score:.3f}")
376
+
377
+ # Beste Maske verwenden
378
+ mask_np = all_masks[best_mask_idx]
 
 
 
 
 
 
 
379
 
380
  # ============================================================
381
+ # DYNAMISCHER THRESHOLD
382
  # ============================================================
383
  max_val = mask_np.max()
384
+ print(f" 🔍 Maximaler SAM-Konfidenzwert der besten Maske: {max_val:.3f}")
385
 
 
386
  if max_val < 0.6:
387
+ dynamic_threshold = 0.2
 
 
388
  print(f" ⚠️ SAM ist unsicher (max_val={max_val:.3f} < 0.6)")
389
  print(f" 🎯 Verwende festen niedrigen Threshold: {dynamic_threshold:.3f}")
390
  else:
391
+ dynamic_threshold = max_val * 0.8
 
 
392
  print(f" ✅ SAM ist sicher (max_val={max_val:.3f} >= 0.6)")
393
  print(f" 🎯 Dynamischer Threshold: {dynamic_threshold:.3f} (80% von Maximum)")
394
 
 
397
  print(f" Nach Threshold ({dynamic_threshold:.3f}): {mask_array.shape}, Unique Werte: {unique_vals}")
398
 
399
  # ============================================================
400
+ # SCHRITT 7: Postprocessing
401
  # ============================================================
402
+ print("🔧 SCHRITT 7: POSTPROCESSING")
403
+
404
+ # a) Kleine Löcher füllen
405
+ if np.sum(mask_array > 0) > 0:
406
+ # Finde alle schwarze Regionen in der weißen Maske (Löcher)
407
+ mask_inverted = 255 - mask_array
408
+ labeled_holes, num_holes = ndimage.label(mask_inverted)
409
+
410
+ if num_holes > 1: # 1 ist der Hintergrund
411
+ print(f" 🔍 Gefundene Löcher: {num_holes - 1}")
412
+
413
+ # Fülle kleine Löcher
414
+ for i in range(2, num_holes + 1): # Beginne bei 2 (1 ist Hintergrund)
415
+ hole_size = np.sum(labeled_holes == i)
416
+ if hole_size < 500: # Kleine Löcher füllen
417
+ mask_array = np.where(labeled_holes == i, 255, mask_array)
418
+ print(f" • Loch {i} gefüllt ({hole_size} Pixel)")
419
+
420
+ # b) Kleine Komponenten entfernen
421
  labeled_array, num_features = ndimage.label(mask_array)
422
  if num_features > 1:
423
+ print(f" 🧹 Komponenten vor Filterung: {num_features}")
424
 
425
  sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
426
+ total_mask_area = np.sum(mask_array > 0)
427
+ min_size = total_mask_area * 0.1 # 10% der Gesamtfläche
428
+
429
+ print(f" 📊 Gesamtmaskenfläche: {total_mask_area:,} Pixel")
430
+ print(f" 📏 Minimale Komponentengröße: {min_size:,.0f} Pixel")
431
 
 
 
432
  for i in range(1, num_features + 1):
433
+ if sizes[i-1] < min_size:
 
 
 
434
  mask_array = np.where(labeled_array == i, 0, mask_array)
435
+ print(f" • Komponente {i} entfernt ({sizes[i-1]:,} Pixel)")
436
+
437
+ # c) Ggf. leichte Erosion/Dilation
438
+ print(" ⚙️ Leichte morphologische Operationen...")
439
+ kernel = np.ones((3, 3), np.uint8)
440
+
441
+ # Leichte Erosion für saubere Kanten
442
+ mask_array = cv2.erode(mask_array, kernel, iterations=1)
443
+ print(" • Erosion (1 Iteration) angewendet")
444
+
445
+ # Leichte Dilation für glatte Übergänge
446
+ mask_array = cv2.dilate(mask_array, kernel, iterations=1)
447
+ print(" • Dilation (1 Iteration) angewendet")
448
 
449
+ # BEIDE MASKEN ERSTELLEN (vor Nachbearbeitung)
450
  original_mask_array = mask_array.copy() # Person weiß (255), Hintergrund schwarz (0)
451
  inverted_mask_array = 255 - mask_array # Person schwarz (0), Hintergrund weiß (255)
452
 
453
  print("-" * 60)
454
+ print(f"🔧 MODUS-SPEZIFISCHE NACHBEARBEITUNG: {mode}")
455
  print(f" Original-Maske (Person weiß): {original_mask_array.shape}")
456
  print(f" Invertierte Maske (Person schwarz): {inverted_mask_array.shape}")
457
 
458
+ # MODUS-SPEZIFISCHE NACHBEARBEITUNG
459
  if mode == "environment_change":
460
  print("🌳 MODUS: UMWELT ÄNDERN")
461
  # Arbeite auf der INVERTIERTEN Maske (Person schwarz, Hintergrund weiß)
462
  mask_array = inverted_mask_array.copy()
463
  print(" Arbeite auf invertierter Maske (Person schwarz, Hintergrund weiß)")
464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  # Morphologische Operationen für saubere Umgebung
466
  kernel = np.ones((5,5), np.uint8)
 
467
  mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
 
468
  mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_OPEN, kernel)
 
 
 
469
  mask_array = cv2.dilate(mask_array, np.ones((2,2), np.uint8), iterations=1)
 
 
 
470
  mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0)
471
 
472
  print(" ✅ Umwelt-Modus: Person geschützt, Hintergrund optimiert")
 
479
 
480
  # Größte weiße Komponente behalten (Person)
481
  labeled_array, num_features = ndimage.label(mask_array)
 
 
482
  if num_features > 1:
483
  sizes = ndimage.sum(mask_array, labeled_array, range(1, num_features + 1))
 
484
  largest_component = np.argmax(sizes) + 1
485
  mask_array = np.where(labeled_array == largest_component, mask_array, 0)
 
486
 
487
  # Maske leicht erweitern für bessere Abdeckung
488
  kernel = np.ones((3,3), np.uint8)
 
489
  mask_array = cv2.dilate(mask_array, kernel, iterations=1)
 
 
 
490
  mask_array = cv2.morphologyEx(mask_array, cv2.MORPH_CLOSE, kernel)
491
 
492
  print(" ✅ Focus-Modus: Person verändert, Hintergrund geschützt")
493
 
494
  elif mode == "face_only_change":
495
+ print("👤 MODUS: NUR GESICHT ÄNDERN")
496
  # Arbeite auf der ORIGINAL-Maske (Person weiß, Hintergrund schwarz)
497
  mask_array = original_mask_array.copy()
498
  print(" Arbeite auf originaler Maske (Person weiß, Hintergrund schwarz)")
499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  # Starke Erosion für präzises Gesicht
501
  kernel = np.ones((3,3), np.uint8)
 
502
  mask_array = cv2.erode(mask_array, kernel, iterations=2)
 
 
 
503
  mask_array = cv2.erode(mask_array, np.ones((2,2), np.uint8), iterations=1)
 
 
 
504
  mask_array = cv2.GaussianBlur(mask_array, (3, 3), 0)
505
 
506
+ print(" ✅ Gesichts-Modus: Postprocessing auf Ausschnitt abgeschlossen")
507
 
508
  # ============================================================
509
+ # SPEZIALSCHRITT: MASKE ZURÜCK AUF ORIGINALGRÖSSE BRINGEN
510
  # ============================================================
511
  print("-" * 60)
512
+ print("🔄 MASKE VOM AUSSCHNITT ZURÜCK AUF ORIGINALGRÖSSE")
513
 
514
  # Temporäre Maske aus dem Array erstellen
515
  temp_mask = Image.fromarray(mask_array).convert("L")
 
539
  black_ratio = 100 - white_ratio
540
 
541
  print("-" * 60)
542
+ print("📊 MASKEN-STATISTIK (FINAL)")
543
  print(f" Weiße Pixel (Veränderungsbereich): {white_pixels:,} ({white_ratio:.1f}%)")
544
  print(f" Schwarze Pixel (Erhaltungsbereich): {black_pixels:,} ({black_ratio:.1f}%)")
545
  print(f" Gesamtpixel: {total_pixels:,}")
546
+
547
+ if mode == "face_only_change":
548
+ # Zusätzliche Statistik für Gesichtsmodus
549
+ original_face_area = original_bbox_size[0] * original_bbox_size[1]
550
+ coverage_ratio = white_pixels / original_face_area if original_face_area > 0 else 0
551
+ print(f" 👤 Gesichtsabdeckung: {coverage_ratio:.1%} der ursprünglichen BBox")
552
 
553
  # 10. Zurück zu PIL Image
554
  mask = Image.fromarray(mask_array).convert("L")
 
557
  print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
558
  print(f"📐 Finale Maskengröße: {mask.size}")
559
  print(f"🎛️ Verwendeter Modus: {mode}")
560
+ print(f"👤 Bei face_only_change: Crop={crop_size}×{crop_size}px, Heuristik-Score={best_score:.3f}")
561
  print("#" * 80)
562
  return mask
563