Update controlnet_module.py
Browse files- 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 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
#
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
# ============================================================
|
| 150 |
-
# SCHRITT 1: Originalbild sichern
|
| 151 |
-
# ============================================================
|
| 152 |
-
original_image = image
|
| 153 |
-
print(f"💾 Originalbild gesichert: {original_image.size}")
|
| 154 |
|
| 155 |
-
#
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
#
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
-
#
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
crop_y2 = min(original_image.height, crop_y2)
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 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 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
-
#
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
#
|
| 212 |
-
|
| 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 |
-
#
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
| 225 |
|
| 226 |
-
print(f"
|
| 227 |
-
|
|
|
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
-
#
|
| 235 |
-
|
| 236 |
-
|
|
|
|
| 237 |
|
| 238 |
-
|
| 239 |
-
sharpness_enhancer = ImageEnhance.Sharpness(enhanced_image)
|
| 240 |
-
enhanced_image = sharpness_enhancer.enhance(2.0) # 100% mehr Schärfe
|
| 241 |
|
| 242 |
-
#
|
| 243 |
-
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
-
#
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
# ============================================================
|
| 259 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 276 |
center_x = (x1 + x2) // 2
|
| 277 |
center_y = (y1 + y2) // 2
|
| 278 |
|
| 279 |
-
#
|
| 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 |
-
#
|
| 327 |
-
|
|
|
|
| 328 |
|
| 329 |
for i in range(num_masks):
|
| 330 |
single_mask = outputs.pred_masks[:, :, i, :, :]
|
| 331 |
-
|
|
|
|
| 332 |
single_mask,
|
| 333 |
-
size=(
|
| 334 |
mode='bilinear',
|
| 335 |
align_corners=False
|
| 336 |
).squeeze()
|
| 337 |
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
#
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
# ============================================================
|
| 347 |
-
#
|
| 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(
|
| 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 |
-
#
|
| 379 |
# ============================================================
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 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 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 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 |
-
|
| 463 |
-
|
| 464 |
-
|
| 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 =
|
| 517 |
-
|
|
|
|
|
|
|
| 518 |
# ============================================================
|
| 519 |
-
#
|
| 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 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 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 |
-
|
| 588 |
-
|
|
|
|
|
|
|
| 589 |
|
|
|
|
| 590 |
|
| 591 |
-
|
| 592 |
-
print("
|
| 593 |
-
print("
|
| 594 |
-
print(f"
|
| 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 |
-
#
|
| 600 |
if mask_array.max() == 0:
|
| 601 |
-
print("
|
| 602 |
-
print("
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
print("
|
| 609 |
|
| 610 |
-
#
|
| 611 |
raw_mask_array = mask_array.copy()
|
| 612 |
|
| 613 |
-
|
| 614 |
# ============================================================
|
| 615 |
-
#
|
| 616 |
# ============================================================
|
| 617 |
-
|
| 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 |
-
|
| 707 |
-
|
| 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 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 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 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 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 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 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 |
-
#
|
| 802 |
-
|
| 803 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 805 |
print("#" * 80)
|
| 806 |
print(f"✅ SAM 2 SEGMENTIERUNG ABGESCHLOSSEN")
|
| 807 |
-
print(f"📐 Finale Maskengröße: {
|
| 808 |
print(f"🎛️ Verwendeter Modus: {mode}")
|
| 809 |
-
|
| 810 |
-
|
| 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 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"""
|