Astridkraft commited on
Commit
1356e3f
·
verified ·
1 Parent(s): 5f4ab72

Update controlnet_module.py

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