Update app.py
Browse files
app.py
CHANGED
|
@@ -1,8 +1,3 @@
|
|
| 1 |
-
|
| 2 |
-
import sys
|
| 3 |
-
if 'google.colab' in sys.modules:
|
| 4 |
-
!pip install deepface nudenet opencv-python
|
| 5 |
-
|
| 6 |
from PIL import Image, ImageDraw, ImageFont
|
| 7 |
import numpy as np
|
| 8 |
import gradio as gr
|
|
@@ -14,8 +9,8 @@ import io
|
|
| 14 |
import tempfile
|
| 15 |
|
| 16 |
# --- Constants for image generation ---
|
| 17 |
-
FONT_SIZE_TITLE = 28
|
| 18 |
-
FONT_SIZE_DESC = 20
|
| 19 |
PADDING = 30
|
| 20 |
ITEM_SPACING = 50
|
| 21 |
BORDER_COLOR = (255, 20, 147)
|
|
@@ -26,6 +21,7 @@ ERROR_IMAGE_WIDTH = 800
|
|
| 26 |
ERROR_IMAGE_HEIGHT = 400
|
| 27 |
TARGET_MAX_DIMENSION = 400
|
| 28 |
PIXELS_PER_CM_ESTIMATE = 15
|
|
|
|
| 29 |
|
| 30 |
# Try to load a font, fall back to default if not available
|
| 31 |
try:
|
|
@@ -35,8 +31,8 @@ try:
|
|
| 35 |
except IOError:
|
| 36 |
print("Could not load arialbd.ttf, falling back to arial.ttf for FONT_DESC.")
|
| 37 |
FONT_DESC = ImageFont.truetype("arial.ttf", FONT_SIZE_DESC)
|
| 38 |
-
FONT_ERROR = ImageFont.truetype("arial.ttf",
|
| 39 |
-
FONT_LABEL_SIMPLE = ImageFont.truetype("arial.ttf",
|
| 40 |
except IOError:
|
| 41 |
print("Could not load arial.ttf, using default font.")
|
| 42 |
FONT_TITLE = ImageFont.load_default()
|
|
@@ -83,13 +79,13 @@ def estimate_vagina_size_cm(crop_img):
|
|
| 83 |
height_cm = round(height_px / PIXELS_PER_CM_ESTIMATE, 1)
|
| 84 |
return f"Breite: {width_cm} cm · Höhe: {height_cm} cm (geschätzt)"
|
| 85 |
|
| 86 |
-
def estimate_body_measurements(person_bbox, associated_nudenet_dets):
|
| 87 |
pb_x1, pb_y1, pb_x2, pb_y2 = person_bbox
|
| 88 |
person_width_px = pb_x2 - pb_x1
|
| 89 |
person_height_px = pb_y2 - pb_y1
|
| 90 |
|
| 91 |
if person_width_px <= 0 or person_height_px <= 0:
|
| 92 |
-
return "
|
| 93 |
|
| 94 |
person_width_cm = person_width_px / PIXELS_PER_CM_ESTIMATE
|
| 95 |
person_height_cm = person_height_px / PIXELS_PER_CM_ESTIMATE
|
|
@@ -98,16 +94,18 @@ def estimate_body_measurements(person_bbox, associated_nudenet_dets):
|
|
| 98 |
waist_cm = "?"
|
| 99 |
hip_cm = "?"
|
| 100 |
|
| 101 |
-
breast_detections = [
|
| 102 |
-
vagina_detections = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
if breast_detections:
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
bust_cm = round(bust_width_px * 1.7 / PIXELS_PER_CM_ESTIMATE, 1)
|
| 109 |
-
elif len(breast_detections) == 1:
|
| 110 |
-
bust_cm = round(breast_detections[0]['box'][2] * 1.5 / PIXELS_PER_CM_ESTIMATE, 1)
|
| 111 |
|
| 112 |
if vagina_detections:
|
| 113 |
vg_width_px = max([det['box'][2] for det in vagina_detections])
|
|
@@ -118,44 +116,43 @@ def estimate_body_measurements(person_bbox, associated_nudenet_dets):
|
|
| 118 |
if person_width_cm > 0:
|
| 119 |
waist_cm = round(person_width_cm * 0.8, 1)
|
| 120 |
|
| 121 |
-
return f"
|
| 122 |
|
| 123 |
def describe_breast_precise(crop):
|
| 124 |
w, h = crop.size
|
| 125 |
crop_np = np.array(crop)
|
| 126 |
gray = cv2.cvtColor(crop_np, cv2.COLOR_RGB2GRAY)
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
else: firmness = "deutlich hängend"
|
| 155 |
|
| 156 |
size_cm_estimate = estimate_breast_size_cm(crop)
|
| 157 |
|
| 158 |
-
return f"
|
| 159 |
|
| 160 |
def describe_vagina_precise(crop):
|
| 161 |
w, h = crop.size
|
|
@@ -172,17 +169,25 @@ def describe_vagina_precise(crop):
|
|
| 172 |
ratio_wh = w / h
|
| 173 |
labia_area = w * h
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
size_cm_estimate = estimate_vagina_size_cm(crop)
|
| 184 |
|
| 185 |
-
return f"Form: {form} ·
|
| 186 |
|
| 187 |
def estimate_age(crop_img):
|
| 188 |
try:
|
|
@@ -204,17 +209,32 @@ def estimate_age(crop_img):
|
|
| 204 |
# Initialize NudeDetector
|
| 205 |
detector = NudeDetector()
|
| 206 |
|
| 207 |
-
def
|
| 208 |
-
print(f"[INFO]
|
|
|
|
|
|
|
| 209 |
try:
|
| 210 |
if input_image_path is None:
|
| 211 |
-
print("[WARNING] No input image path provided.")
|
| 212 |
return create_error_image('Kein Bild ausgewählt.'), None
|
| 213 |
|
|
|
|
|
|
|
| 214 |
try:
|
| 215 |
original_img = Image.open(input_image_path).convert("RGB")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
original_img_np = np.array(original_img)
|
| 217 |
-
print(f"[INFO] Image opened successfully: {input_image_path}")
|
| 218 |
except Exception as e:
|
| 219 |
print(f"[ERROR] Failed to open image {input_image_path}: {e}")
|
| 220 |
return create_error_image(f'Fehler beim Öffnen des Bildes: {e}'), None
|
|
@@ -222,20 +242,35 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 222 |
current_filename = os.path.basename(input_image_path)
|
| 223 |
|
| 224 |
try:
|
| 225 |
-
nudenet_detections = detector.detect(
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
except Exception as e:
|
| 228 |
print(f"[ERROR] Error during NudeNet detection for {input_image_path}: {e}")
|
| 229 |
return create_error_image(f'Fehler bei der NudeNet-Erkennung: {e}'), None
|
| 230 |
|
| 231 |
persons_data = []
|
| 232 |
face_detections = []
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
| 239 |
|
| 240 |
if face_detections:
|
| 241 |
for face_det in face_detections:
|
|
@@ -266,14 +301,14 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 266 |
associated_nudenet_dets.append(nude_det)
|
| 267 |
|
| 268 |
if associated_nudenet_dets:
|
| 269 |
-
body_measurements_str = estimate_body_measurements(person_bbox, associated_nudenet_dets)
|
| 270 |
persons_data.append({
|
| 271 |
'person_bbox': person_bbox,
|
| 272 |
'nudenet_detections': associated_nudenet_dets,
|
| 273 |
'face_area': face_det['facial_area'],
|
| 274 |
'body_measurements': body_measurements_str
|
| 275 |
})
|
| 276 |
-
print(f"[INFO] {len(persons_data)} persons with associated NudeNet detections found.")
|
| 277 |
|
| 278 |
annotated_original_img = original_img.copy()
|
| 279 |
draw_annotated = ImageDraw.Draw(annotated_original_img)
|
|
@@ -283,18 +318,19 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 283 |
|
| 284 |
for i, person in enumerate(persons_data):
|
| 285 |
px1, py1, px2, py2 = person['person_bbox']
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
|
|
|
| 298 |
for det in person['nudenet_detections']:
|
| 299 |
det_class = det['class']
|
| 300 |
x1, y1, x2, y2 = det['box'][0], det['box'][1], det['box'][0] + det['box'][2], det['box'][1] + det['box'][3]
|
|
@@ -304,58 +340,38 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 304 |
|
| 305 |
crop = original_img.crop((x1, y1, x2, y2))
|
| 306 |
age_str = 'Alter unbekannt'
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
|
|
|
| 315 |
label_detailed = f"Person {i+1} Vagina"
|
| 316 |
-
desc = describe_vagina_precise(crop)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
current_image_crops_with_details.append((label_detailed, resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 318 |
processed_nudenet_ids.add(id(det))
|
| 319 |
-
elif 'BREAST_EXPOSED' in det_class:
|
| 320 |
-
person_breasts_for_merging.append({'bbox': (x1,y1,x2,y2), 'det': det, 'age_str': age_str})
|
| 321 |
-
|
| 322 |
-
if len(person_breasts_for_merging) >= 1:
|
| 323 |
-
if len(person_breasts_for_merging) >= 2:
|
| 324 |
-
x1_combined = min(b['bbox'][0] for b in person_breasts_for_merging)
|
| 325 |
-
y1_combined = min(b['bbox'][1] for b in person_breasts_for_merging)
|
| 326 |
-
x2_combined = max(b['bbox'][2] for b in person_breasts_for_merging)
|
| 327 |
-
y2_combined = max(b['bbox'][3] for b in person_breasts_for_merging)
|
| 328 |
-
|
| 329 |
-
pad_x = int((x2_combined - x1_combined) * 0.2)
|
| 330 |
-
pad_y = int((y2_combined - y1_combined) * 0.25)
|
| 331 |
-
x1_final = max(0, x1_combined - pad_x)
|
| 332 |
-
y1_final = max(0, y1_combined - pad_y)
|
| 333 |
-
x2_final = min(original_img.width, x2_combined + pad_x)
|
| 334 |
-
y2_final = min(original_img.height, y2_combined + pad_y)
|
| 335 |
-
|
| 336 |
-
if x2_final <= x1_final or y2_final <= y1_final:
|
| 337 |
-
print(f"[WARNING] Skipping invalid crop box for Merged Breast detection (person {i+1}) in {current_filename}: ({x1_final}, {y1_final}, {x2_final}, {y2_final})")
|
| 338 |
-
for b_data in person_breasts_for_merging:
|
| 339 |
-
crop = original_img.crop(b_data['bbox'])
|
| 340 |
-
desc = describe_breast_precise(crop) + f" · Alter: {b_data['age_str']}"
|
| 341 |
-
current_image_crops_with_details.append((f"Person {i+1} Brust (einzeln)", resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 342 |
-
processed_nudenet_ids.add(id(b_data['det']))
|
| 343 |
-
else:
|
| 344 |
-
merged_breast_crop = original_img.crop((x1_final, y1_final, x2_final, y2_final))
|
| 345 |
-
desc = describe_breast_precise(merged_breast_crop) + f" · Alter: {person_breasts_for_merging[0]['age_str']}"
|
| 346 |
-
current_image_crops_with_details.append((f"Person {i+1} Brüste (beide)", resize_crop(merged_breast_crop, TARGET_MAX_DIMENSION), desc))
|
| 347 |
-
for b_data in person_breasts_for_merging:
|
| 348 |
-
processed_nudenet_ids.add(id(b_data['det']))
|
| 349 |
-
else:
|
| 350 |
-
b_data = person_breasts_for_merging[0]
|
| 351 |
-
crop = original_img.crop(b_data['bbox'])
|
| 352 |
-
desc = describe_breast_precise(crop) + f" · Alter: {b_data['age_str']}"
|
| 353 |
-
current_image_crops_with_details.append((f"Person {i+1} Brust (einzeln)", resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 354 |
-
processed_nudenet_ids.add(id(b_data['det']))
|
| 355 |
|
| 356 |
unassociated_nudenet_detections = [det for det in nudenet_detections if id(det) not in processed_nudenet_ids]
|
| 357 |
|
| 358 |
-
global_breasts_for_merging = []
|
| 359 |
for det in unassociated_nudenet_detections:
|
| 360 |
det_class = det['class']
|
| 361 |
x1, y1, x2, y2 = det['box'][0], det['box'][1], det['box'][0] + det['box'][2], det['box'][1] + det['box'][3]
|
|
@@ -366,61 +382,45 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 366 |
bbox_label_global = FONT_LABEL_SIMPLE.getbbox(label_global_simple)
|
| 367 |
text_width_global = bbox_label_global[2] - bbox_label_global[0]
|
| 368 |
text_height_global = bbox_label_global[3] - bbox_label_global[1]
|
| 369 |
-
draw_annotated.rectangle([(x1, y1 - text_height_global - 2), (x1 + text_width_global, y1)], fill=
|
| 370 |
draw_annotated.text((x1, y1 - text_height_global - 2), label_global_simple, font=FONT_LABEL_SIMPLE, fill=BACKGROUND_COLOR)
|
| 371 |
|
| 372 |
crop = original_img.crop((x1, y1, x2, y2))
|
| 373 |
age_str = 'Alter unbekannt'
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
| 378 |
|
| 379 |
-
if 'FEMALE_GENITALIA_EXPOSED' in det_class:
|
| 380 |
label_detailed = "Global Vagina"
|
| 381 |
-
desc = describe_vagina_precise(crop)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
current_image_crops_with_details.append((label_detailed, resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 383 |
-
elif 'BREAST_EXPOSED' in det_class:
|
| 384 |
-
global_breasts_for_merging.append({'bbox': (x1,y1,x2,y2), 'det': det, 'age_str': age_str})
|
| 385 |
-
|
| 386 |
-
if len(global_breasts_for_merging) >= 1:
|
| 387 |
-
if len(global_breasts_for_merging) >= 2:
|
| 388 |
-
x1_combined = min(b['bbox'][0] for b in global_breasts_for_merging)
|
| 389 |
-
y1_combined = min(b['bbox'][1] for b in global_breasts_for_merging)
|
| 390 |
-
y2_combined = max(b['bbox'][3] for b in global_breasts_for_merging)
|
| 391 |
-
x2_combined = max(b['bbox'][2] for b in global_breasts_for_merging)
|
| 392 |
-
|
| 393 |
-
pad_x = int((x2_combined - x1_combined) * 0.2)
|
| 394 |
-
pad_y = int((y2_combined - y1_combined) * 0.25)
|
| 395 |
-
x1_final = max(0, x1_combined - pad_x)
|
| 396 |
-
y1_final = max(0, y1_combined - pad_y)
|
| 397 |
-
x2_final = min(original_img.width, x2_combined + pad_x)
|
| 398 |
-
y2_final = min(original_img.height, y2_combined + pad_y)
|
| 399 |
-
|
| 400 |
-
if x2_final <= x1_final or y2_final <= y1_final:
|
| 401 |
-
print(f"[WARNING] Skipping invalid crop box for Merged Global Breast detection in {current_filename}: ({x1_final}, {y1_final}, {x2_final}, {y2_final})")
|
| 402 |
-
for b_data in global_breasts_for_merging:
|
| 403 |
-
crop = original_img.crop(b_data['bbox'])
|
| 404 |
-
desc = describe_breast_precise(crop) + f" · Alter: {b_data['age_str']}"
|
| 405 |
-
current_image_crops_with_details.append((f"Global Brust (einzeln)", resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 406 |
-
else:
|
| 407 |
-
merged_breast_crop = original_img.crop((x1_final, y1_final, x2_final, y2_final))
|
| 408 |
-
desc = describe_breast_precise(merged_breast_crop) + f" · Alter: {global_breasts_for_merging[0]['age_str']}"
|
| 409 |
-
current_image_crops_with_details.append((f"Global Brüste (beide)", resize_crop(merged_breast_crop, TARGET_MAX_DIMENSION), desc))
|
| 410 |
-
else:
|
| 411 |
-
b_data = global_breasts_for_merging[0]
|
| 412 |
-
crop = original_img.crop(b_data['bbox'])
|
| 413 |
-
desc = describe_breast_precise(crop) + f" · Alter: {b_data['age_str']}"
|
| 414 |
-
current_image_crops_with_details.append((f"Global Brust (einzeln)", resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 415 |
|
| 416 |
-
if not
|
| 417 |
-
text = "Keine
|
| 418 |
original_img_width, original_img_height = original_img.size
|
| 419 |
bbox_text = FONT_TITLE.getbbox(text)
|
| 420 |
text_width = bbox_text[2] - bbox_text[0]
|
| 421 |
text_height = bbox_text[3] - bbox_text[1]
|
| 422 |
draw_annotated.text(((original_img_width - text_width) / 2, original_img_height - text_height - PADDING), text, font=FONT_TITLE, fill=TEXT_COLOR)
|
| 423 |
-
print("[INFO] No
|
| 424 |
|
| 425 |
if not current_image_crops_with_details:
|
| 426 |
composite_crops_img = create_error_image("Keine detailreichen Bereiche für Crops gefunden.")
|
|
@@ -429,7 +429,7 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 429 |
total_height_crops = PADDING
|
| 430 |
|
| 431 |
for title, crop_img, desc in current_image_crops_with_details:
|
| 432 |
-
if crop_img.width == 1 and crop_img.height == 1:
|
| 433 |
bbox_title = FONT_TITLE.getbbox(title)
|
| 434 |
bbox_desc = FONT_DESC.getbbox(desc)
|
| 435 |
text_title_height = bbox_title[3] - bbox_title[1]
|
|
@@ -459,7 +459,7 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 459 |
current_y_offset_crops = PADDING
|
| 460 |
|
| 461 |
for title, crop_img, desc in current_image_crops_with_details:
|
| 462 |
-
if crop_img.width == 1 and crop_img.height == 1:
|
| 463 |
bbox_title = FONT_TITLE.getbbox(title)
|
| 464 |
text_title_width = bbox_title[2] - bbox_title[0]
|
| 465 |
text_title_height = bbox_title[3] - bbox_title[1]
|
|
@@ -500,7 +500,14 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 500 |
current_y_offset_crops += text_desc_height + ITEM_SPACING
|
| 501 |
|
| 502 |
separator_text_original = "Originalbild mit Markierungen"
|
| 503 |
-
separator_text_crops = "Detaillierte
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
|
| 505 |
bbox_separator_original = FONT_TITLE.getbbox(separator_text_original)
|
| 506 |
bbox_separator_crops = FONT_TITLE.getbbox(separator_text_crops)
|
|
@@ -538,7 +545,6 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 538 |
|
| 539 |
print("[INFO] Combined image generated successfully.")
|
| 540 |
|
| 541 |
-
# Save the combined image as a PDF to a temporary file
|
| 542 |
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_pdf:
|
| 543 |
combined_img.save(tmp_pdf.name, "PDF")
|
| 544 |
pdf_path = tmp_pdf.name
|
|
@@ -546,18 +552,45 @@ def analyze_image_with_gradio(input_image_path):
|
|
| 546 |
return combined_img, pdf_path
|
| 547 |
|
| 548 |
except Exception as e:
|
| 549 |
-
print(f"[CRITICAL ERROR] An unexpected error occurred during image analysis: {e}")
|
| 550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
|
| 552 |
interface = gr.Interface(
|
| 553 |
-
fn=
|
| 554 |
-
inputs=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
outputs=[
|
| 556 |
-
gr.
|
| 557 |
-
gr.File(label="
|
| 558 |
],
|
| 559 |
-
title="🔞
|
| 560 |
-
description="Laden Sie ein
|
| 561 |
)
|
| 562 |
|
| 563 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from PIL import Image, ImageDraw, ImageFont
|
| 2 |
import numpy as np
|
| 3 |
import gradio as gr
|
|
|
|
| 9 |
import tempfile
|
| 10 |
|
| 11 |
# --- Constants for image generation ---
|
| 12 |
+
FONT_SIZE_TITLE = 36 # Increased from 28
|
| 13 |
+
FONT_SIZE_DESC = 24 # Increased from 20
|
| 14 |
PADDING = 30
|
| 15 |
ITEM_SPACING = 50
|
| 16 |
BORDER_COLOR = (255, 20, 147)
|
|
|
|
| 21 |
ERROR_IMAGE_HEIGHT = 400
|
| 22 |
TARGET_MAX_DIMENSION = 400
|
| 23 |
PIXELS_PER_CM_ESTIMATE = 15
|
| 24 |
+
MAX_PROCESSING_DIMENSION = 1024 # New constant for initial image resizing
|
| 25 |
|
| 26 |
# Try to load a font, fall back to default if not available
|
| 27 |
try:
|
|
|
|
| 31 |
except IOError:
|
| 32 |
print("Could not load arialbd.ttf, falling back to arial.ttf for FONT_DESC.")
|
| 33 |
FONT_DESC = ImageFont.truetype("arial.ttf", FONT_SIZE_DESC)
|
| 34 |
+
FONT_ERROR = ImageFont.truetype("arial.ttf", 30) # Increased from 24
|
| 35 |
+
FONT_LABEL_SIMPLE = ImageFont.truetype("arial.ttf", 18) # Increased from 14
|
| 36 |
except IOError:
|
| 37 |
print("Could not load arial.ttf, using default font.")
|
| 38 |
FONT_TITLE = ImageFont.load_default()
|
|
|
|
| 79 |
height_cm = round(height_px / PIXELS_PER_CM_ESTIMATE, 1)
|
| 80 |
return f"Breite: {width_cm} cm · Höhe: {height_cm} cm (geschätzt)"
|
| 81 |
|
| 82 |
+
def estimate_body_measurements(person_bbox, associated_nudenet_dets, analysis_mode):
|
| 83 |
pb_x1, pb_y1, pb_x2, pb_y2 = person_bbox
|
| 84 |
person_width_px = pb_x2 - pb_x1
|
| 85 |
person_height_px = pb_y2 - pb_y1
|
| 86 |
|
| 87 |
if person_width_px <= 0 or person_height_px <= 0:
|
| 88 |
+
return "Körpermaße nicht schätzbar (ungültiger Personen-BBox)"
|
| 89 |
|
| 90 |
person_width_cm = person_width_px / PIXELS_PER_CM_ESTIMATE
|
| 91 |
person_height_cm = person_height_px / PIXELS_PER_CM_ESTIMATE
|
|
|
|
| 94 |
waist_cm = "?"
|
| 95 |
hip_cm = "?"
|
| 96 |
|
| 97 |
+
breast_detections = []
|
| 98 |
+
vagina_detections = []
|
| 99 |
+
|
| 100 |
+
if analysis_mode in ['Komplett (Brüste + Vagina + geschätztes Alter)', 'Brüste (Nur Brüste)']:
|
| 101 |
+
breast_detections = [det for det in associated_nudenet_dets if 'FEMALE_BREAST_EXPOSED' in det['class']]
|
| 102 |
+
if analysis_mode in ['Komplett (Brüste + Vagina + geschätztes Alter)', 'Vagina (Nur Vagina)']:
|
| 103 |
+
vagina_detections = [det for det in associated_nudenet_dets if 'FEMALE_GENITALIA_EXPOSED' in det['class']]
|
| 104 |
|
| 105 |
if breast_detections:
|
| 106 |
+
# Simple estimate: average breast width
|
| 107 |
+
b_width_px = sum([det['box'][2] for det in breast_detections]) / len(breast_detections)
|
| 108 |
+
bust_cm = round(b_width_px * 2.5 / PIXELS_PER_CM_ESTIMATE, 1) # A rough multiplier for bust circumference
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
if vagina_detections:
|
| 111 |
vg_width_px = max([det['box'][2] for det in vagina_detections])
|
|
|
|
| 116 |
if person_width_cm > 0:
|
| 117 |
waist_cm = round(person_width_cm * 0.8, 1)
|
| 118 |
|
| 119 |
+
return f"Maße (geschätzt, unsicher): Büste: {bust_cm} cm · Taille: {waist_cm} cm · Hüfte: {hip_cm} cm"
|
| 120 |
|
| 121 |
def describe_breast_precise(crop):
|
| 122 |
w, h = crop.size
|
| 123 |
crop_np = np.array(crop)
|
| 124 |
gray = cv2.cvtColor(crop_np, cv2.COLOR_RGB2GRAY)
|
| 125 |
|
| 126 |
+
# Calculate nipple area/prominence (very rough estimation)
|
| 127 |
+
# This is a very rough estimation and needs more advanced computer vision
|
| 128 |
+
# For now, let's just make a simple placeholder based on contrast/shape
|
| 129 |
+
# Find contours, look for circular/elliptical shapes
|
| 130 |
+
_, thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 131 |
+
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
| 132 |
+
nipple_detected = False
|
| 133 |
+
for contour in contours:
|
| 134 |
+
area = cv2.contourArea(contour)
|
| 135 |
+
if area > 50 and area < (w * h / 5): # Filter by reasonable size
|
| 136 |
+
perimeter = cv2.arcLength(contour, True)
|
| 137 |
+
if perimeter > 0:
|
| 138 |
+
circularity = 4 * np.pi * area / (perimeter * perimeter)
|
| 139 |
+
if circularity > 0.6: # Check for somewhat circular shapes
|
| 140 |
+
nipple_detected = True
|
| 141 |
+
break
|
| 142 |
+
|
| 143 |
+
nipple_prominence = "Sichtbar" if nipple_detected else "Nicht hervorstehend"
|
| 144 |
+
|
| 145 |
+
# Estimate shape (round, conical, bell, etc.)
|
| 146 |
+
ratio_wh = w / h
|
| 147 |
+
if ratio_wh > 1.1: shape = "Breit / Horizontal"
|
| 148 |
+
elif ratio_wh < 0.9: shape = "Hoch / Vertikal"
|
| 149 |
+
else: shape = "Rund / Ausgewogen"
|
| 150 |
+
|
| 151 |
+
size = "klein" if w*h < 30000 else "mittel" if w*h < 80000 else "groß" if w*h < 150000 else "sehr groß"
|
|
|
|
| 152 |
|
| 153 |
size_cm_estimate = estimate_breast_size_cm(crop)
|
| 154 |
|
| 155 |
+
return f"Form: {shape} · Größe: {size} · Nippel: {nipple_prominence} · Maße: {size_cm_estimate}"
|
| 156 |
|
| 157 |
def describe_vagina_precise(crop):
|
| 158 |
w, h = crop.size
|
|
|
|
| 169 |
ratio_wh = w / h
|
| 170 |
labia_area = w * h
|
| 171 |
|
| 172 |
+
# More detailed form descriptions
|
| 173 |
+
if labia_area < 20000: # Very small overall area
|
| 174 |
+
if ratio_wh > 1.0: form = "Mini Outie (Schmetterlingsform)"
|
| 175 |
+
else: form = "Sehr kleine Innie (Barbie-Form)"
|
| 176 |
+
elif ratio_wh < 0.75: # Taller than wide
|
| 177 |
+
form = "Lange Innie (Vertikale Form)"
|
| 178 |
+
elif ratio_wh > 1.5: # Very wide
|
| 179 |
+
if labia_area > 70000: form = "Extrem ausgeprägtes Outie (Puff-Form)"
|
| 180 |
+
else: form = "Breites Outie (Offene Form)"
|
| 181 |
+
elif ratio_wh > 1.1: # Moderately wide (more typical Outie)
|
| 182 |
+
form = "Klassisches Outie (Labien sichtbar)"
|
| 183 |
+
else: # Ratio between 0.75 and 1.1, and not tiny area (more typical Innie)
|
| 184 |
+
form = "Klassische Innie (Geschlossene Form)"
|
| 185 |
+
|
| 186 |
+
size = "winzig" if labia_area < 20000 else "klein" if labia_area < 40000 else "mittel" if labia_area < 70000 else "groß & voll"
|
| 187 |
|
| 188 |
size_cm_estimate = estimate_vagina_size_cm(crop)
|
| 189 |
|
| 190 |
+
return f"Form: {form} · Größe: {size} · Behaart: {shaved} · Maße: {size_cm_estimate}"
|
| 191 |
|
| 192 |
def estimate_age(crop_img):
|
| 193 |
try:
|
|
|
|
| 209 |
# Initialize NudeDetector
|
| 210 |
detector = NudeDetector()
|
| 211 |
|
| 212 |
+
def _analyze_single_image_and_generate_outputs(analysis_mode, input_image_path):
|
| 213 |
+
print(f"[INFO] _analyze_single_image_and_generate_outputs called with mode: {analysis_mode}, path: {input_image_path}")
|
| 214 |
+
|
| 215 |
+
temp_img_path = None
|
| 216 |
try:
|
| 217 |
if input_image_path is None:
|
| 218 |
+
print("[WARNING] No input image path provided to single analyzer.")
|
| 219 |
return create_error_image('Kein Bild ausgewählt.'), None
|
| 220 |
|
| 221 |
+
processing_img_for_detection_path = input_image_path
|
| 222 |
+
|
| 223 |
try:
|
| 224 |
original_img = Image.open(input_image_path).convert("RGB")
|
| 225 |
+
if max(original_img.width, original_img.height) > MAX_PROCESSING_DIMENSION:
|
| 226 |
+
original_img = resize_crop(original_img, MAX_PROCESSING_DIMENSION)
|
| 227 |
+
print(f"[INFO] Image resized to {original_img.width}x{original_img.height} for faster processing.")
|
| 228 |
+
|
| 229 |
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_file:
|
| 230 |
+
original_img.save(tmp_file.name, "JPEG")
|
| 231 |
+
temp_img_path = tmp_file.name
|
| 232 |
+
processing_img_for_detection_path = temp_img_path
|
| 233 |
+
else:
|
| 234 |
+
processing_img_for_detection_path = input_image_path
|
| 235 |
+
|
| 236 |
original_img_np = np.array(original_img)
|
| 237 |
+
print(f"[INFO] Image opened and potentially resized successfully: {input_image_path}")
|
| 238 |
except Exception as e:
|
| 239 |
print(f"[ERROR] Failed to open image {input_image_path}: {e}")
|
| 240 |
return create_error_image(f'Fehler beim Öffnen des Bildes: {e}'), None
|
|
|
|
| 242 |
current_filename = os.path.basename(input_image_path)
|
| 243 |
|
| 244 |
try:
|
| 245 |
+
nudenet_detections = detector.detect(processing_img_for_detection_path)
|
| 246 |
+
|
| 247 |
+
# Filter NudeNet detections based on analysis_mode
|
| 248 |
+
allowed_classes = []
|
| 249 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 250 |
+
allowed_classes = ['FEMALE_GENITALIA_EXPOSED', 'ANUS_EXPOSED', 'FEMALE_BREAST_EXPOSED', 'BUTTOCKS_EXPOSED']
|
| 251 |
+
elif analysis_mode == 'Brüste (Nur Brüste)':
|
| 252 |
+
allowed_classes = ['FEMALE_BREAST_EXPOSED', 'ANUS_EXPOSED', 'BUTTOCKS_EXPOSED'] # Include other body parts for general detection context
|
| 253 |
+
elif analysis_mode == 'Vagina (Nur Vagina)':
|
| 254 |
+
allowed_classes = ['FEMALE_GENITALIA_EXPOSED', 'ANUS_EXPOSED', 'BUTTOCKS_EXPOSED'] # Include other body parts for general detection context
|
| 255 |
+
|
| 256 |
+
if allowed_classes:
|
| 257 |
+
nudenet_detections = [det for det in nudenet_detections if det['class'] in allowed_classes]
|
| 258 |
+
|
| 259 |
+
print(f"[INFO] NudeNet detected {len(nudenet_detections)} objects for mode '{analysis_mode}'.")
|
| 260 |
except Exception as e:
|
| 261 |
print(f"[ERROR] Error during NudeNet detection for {input_image_path}: {e}")
|
| 262 |
return create_error_image(f'Fehler bei der NudeNet-Erkennung: {e}'), None
|
| 263 |
|
| 264 |
persons_data = []
|
| 265 |
face_detections = []
|
| 266 |
+
# Face detection for age estimation is always attempted if mode includes age
|
| 267 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 268 |
+
try:
|
| 269 |
+
face_results = DeepFace.extract_faces(original_img_np, detector_backend='retinaface', enforce_detection=False)
|
| 270 |
+
face_detections = [f for f in face_results if f['confidence'] > 0.9]
|
| 271 |
+
print(f"[INFO] DeepFace detected {len(face_detections)} faces.")
|
| 272 |
+
except Exception as e:
|
| 273 |
+
print(f"[WARNING] Error during DeepFace face detection: {e}. Proceeding without person association or age estimation.")
|
| 274 |
|
| 275 |
if face_detections:
|
| 276 |
for face_det in face_detections:
|
|
|
|
| 301 |
associated_nudenet_dets.append(nude_det)
|
| 302 |
|
| 303 |
if associated_nudenet_dets:
|
| 304 |
+
body_measurements_str = estimate_body_measurements(person_bbox, associated_nudenet_dets, analysis_mode)
|
| 305 |
persons_data.append({
|
| 306 |
'person_bbox': person_bbox,
|
| 307 |
'nudenet_detections': associated_nudenet_dets,
|
| 308 |
'face_area': face_det['facial_area'],
|
| 309 |
'body_measurements': body_measurements_str
|
| 310 |
})
|
| 311 |
+
print(f"[INFO] {len(persons_data)} persons with associated NudeNet detections found for mode '{analysis_mode}'.")
|
| 312 |
|
| 313 |
annotated_original_img = original_img.copy()
|
| 314 |
draw_annotated = ImageDraw.Draw(annotated_original_img)
|
|
|
|
| 318 |
|
| 319 |
for i, person in enumerate(persons_data):
|
| 320 |
px1, py1, px2, py2 = person['person_bbox']
|
| 321 |
+
# Draw person bounding box if there are associated detections
|
| 322 |
+
if any(det['class'] in allowed_classes for det in person['nudenet_detections']):
|
| 323 |
+
draw_annotated.rectangle([(px1, py1), (px2, py2)], outline=(0, 255, 0), width=BORDER_WIDTH)
|
| 324 |
+
label_person = f"Person {i+1}"
|
| 325 |
+
bbox_label_person = FONT_LABEL_SIMPLE.getbbox(label_person)
|
| 326 |
+
text_width_person = bbox_label_person[2] - bbox_label_person[0]
|
| 327 |
+
text_height_person = bbox_label_person[3] - bbox_label_person[1]
|
| 328 |
+
draw_annotated.rectangle([(px1, py1 - text_height_person - 2), (px1 + text_width_person, py1)], fill=(0, 255, 0))
|
| 329 |
+
draw_annotated.text((px1, py1 - text_height_person - 2), label_person, font=FONT_LABEL_SIMPLE, fill=BACKGROUND_COLOR)
|
| 330 |
+
|
| 331 |
+
if person['body_measurements'] and person['body_measurements'] != "Körpermaße nicht schätzbar (ungültiger Personen-BBox)":
|
| 332 |
+
current_image_crops_with_details.append((f"Person {i+1} Körpermaße", Image.new('RGB', (1,1), (0,0,0)), person['body_measurements']))
|
| 333 |
+
|
| 334 |
for det in person['nudenet_detections']:
|
| 335 |
det_class = det['class']
|
| 336 |
x1, y1, x2, y2 = det['box'][0], det['box'][1], det['box'][0] + det['box'][2], det['box'][1] + det['box'][3]
|
|
|
|
| 340 |
|
| 341 |
crop = original_img.crop((x1, y1, x2, y2))
|
| 342 |
age_str = 'Alter unbekannt'
|
| 343 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 344 |
+
try:
|
| 345 |
+
face_x, face_y, face_w, face_h = person['face_area']['x'], person['face_area']['y'], person['face_area']['w'], person['face_area']['h']
|
| 346 |
+
face_crop_for_age = original_img.crop((face_x, face_y, face_x + face_w, face_y + face_h))
|
| 347 |
+
age_str = estimate_age(face_crop_for_age)
|
| 348 |
+
except Exception as e:
|
| 349 |
+
print(f"[ERROR] Error estimating age for person {i+1} crop: {e}")
|
| 350 |
+
|
| 351 |
+
if 'FEMALE_GENITALIA_EXPOSED' in det_class and analysis_mode in ['Komplett (Brüste + Vagina + geschätztes Alter)', 'Vagina (Nur Vagina)']:
|
| 352 |
label_detailed = f"Person {i+1} Vagina"
|
| 353 |
+
desc = describe_vagina_precise(crop)
|
| 354 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 355 |
+
desc += f" · Alter: {age_str}"
|
| 356 |
+
current_image_crops_with_details.append((label_detailed, resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 357 |
+
processed_nudenet_ids.add(id(det))
|
| 358 |
+
elif 'FEMALE_BREAST_EXPOSED' in det_class and analysis_mode in ['Komplett (Brüste + Vagina + geschätztes Alter)', 'Brüste (Nur Brüste)']:
|
| 359 |
+
label_detailed = f"Person {i+1} Brust"
|
| 360 |
+
desc = describe_breast_precise(crop)
|
| 361 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 362 |
+
desc += f" · Alter: {age_str}"
|
| 363 |
+
current_image_crops_with_details.append((label_detailed, resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 364 |
+
processed_nudenet_ids.add(id(det))
|
| 365 |
+
elif ('ANUS_EXPOSED' in det_class or 'BUTTOCKS_EXPOSED' in det_class) and analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 366 |
+
label_detailed = f"Person {i+1} {det_class.replace('_EXPOSED', '').replace('_', ' ').title()}"
|
| 367 |
+
desc = f"Erkannt: {det_class.replace('_EXPOSED', '').replace('_', ' ').title()}"
|
| 368 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 369 |
+
desc += f" · Alter: {age_str}"
|
| 370 |
current_image_crops_with_details.append((label_detailed, resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 371 |
processed_nudenet_ids.add(id(det))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
|
| 373 |
unassociated_nudenet_detections = [det for det in nudenet_detections if id(det) not in processed_nudenet_ids]
|
| 374 |
|
|
|
|
| 375 |
for det in unassociated_nudenet_detections:
|
| 376 |
det_class = det['class']
|
| 377 |
x1, y1, x2, y2 = det['box'][0], det['box'][1], det['box'][0] + det['box'][2], det['box'][1] + det['box'][3]
|
|
|
|
| 382 |
bbox_label_global = FONT_LABEL_SIMPLE.getbbox(label_global_simple)
|
| 383 |
text_width_global = bbox_label_global[2] - bbox_label_global[0]
|
| 384 |
text_height_global = bbox_label_global[3] - bbox_label_global[1]
|
| 385 |
+
draw_annotated.rectangle([(x1, y1 - text_height_global - 2), (x1 + text_width_global, y1)], fill=TEXT_COLOR)
|
| 386 |
draw_annotated.text((x1, y1 - text_height_global - 2), label_global_simple, font=FONT_LABEL_SIMPLE, fill=BACKGROUND_COLOR)
|
| 387 |
|
| 388 |
crop = original_img.crop((x1, y1, x2, y2))
|
| 389 |
age_str = 'Alter unbekannt'
|
| 390 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 391 |
+
try:
|
| 392 |
+
resized_crop_for_age = resize_crop(crop, TARGET_MAX_DIMENSION)
|
| 393 |
+
age_str = estimate_age(resized_crop_for_age)
|
| 394 |
+
except Exception as e:
|
| 395 |
+
print(f"[ERROR] Error estimating age for global crop: {e}")
|
| 396 |
|
| 397 |
+
if 'FEMALE_GENITALIA_EXPOSED' in det_class and analysis_mode in ['Komplett (Brüste + Vagina + geschätztes Alter)', 'Vagina (Nur Vagina)']:
|
| 398 |
label_detailed = "Global Vagina"
|
| 399 |
+
desc = describe_vagina_precise(crop)
|
| 400 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 401 |
+
desc += f" · Alter: {age_str}"
|
| 402 |
+
current_image_crops_with_details.append((label_detailed, resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 403 |
+
elif 'FEMALE_BREAST_EXPOSED' in det_class and analysis_mode in ['Komplett (Brüste + Vagina + geschätztes Alter)', 'Brüste (Nur Brüste)']:
|
| 404 |
+
label_detailed = "Global Brust"
|
| 405 |
+
desc = describe_breast_precise(crop)
|
| 406 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 407 |
+
desc += f" · Alter: {age_str}"
|
| 408 |
+
current_image_crops_with_details.append((label_detailed, resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
| 409 |
+
elif ('ANUS_EXPOSED' in det_class or 'BUTTOCKS_EXPOSED' in det_class) and analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 410 |
+
label_detailed = f"Global {det_class.replace('_EXPOSED', '').replace('_', ' ').title()}"
|
| 411 |
+
desc = f"Erkannt: {det_class.replace('_EXPOSED', '').replace('_', ' ').title()}"
|
| 412 |
+
if analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 413 |
+
desc += f" · Alter: {age_str}"
|
| 414 |
current_image_crops_with_details.append((label_detailed, resize_crop(crop, TARGET_MAX_DIMENSION), desc))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
|
| 416 |
+
if not current_image_crops_with_details:
|
| 417 |
+
text = "Keine relevanten Bereiche für den ausgewählten Analysemodus erkannt."
|
| 418 |
original_img_width, original_img_height = original_img.size
|
| 419 |
bbox_text = FONT_TITLE.getbbox(text)
|
| 420 |
text_width = bbox_text[2] - bbox_text[0]
|
| 421 |
text_height = bbox_text[3] - bbox_text[1]
|
| 422 |
draw_annotated.text(((original_img_width - text_width) / 2, original_img_height - text_height - PADDING), text, font=FONT_TITLE, fill=TEXT_COLOR)
|
| 423 |
+
print(f"[INFO] No relevant detections for mode '{analysis_mode}', adding message to original image.")
|
| 424 |
|
| 425 |
if not current_image_crops_with_details:
|
| 426 |
composite_crops_img = create_error_image("Keine detailreichen Bereiche für Crops gefunden.")
|
|
|
|
| 429 |
total_height_crops = PADDING
|
| 430 |
|
| 431 |
for title, crop_img, desc in current_image_crops_with_details:
|
| 432 |
+
if crop_img.width == 1 and crop_img.height == 1: # This is for body measurements
|
| 433 |
bbox_title = FONT_TITLE.getbbox(title)
|
| 434 |
bbox_desc = FONT_DESC.getbbox(desc)
|
| 435 |
text_title_height = bbox_title[3] - bbox_title[1]
|
|
|
|
| 459 |
current_y_offset_crops = PADDING
|
| 460 |
|
| 461 |
for title, crop_img, desc in current_image_crops_with_details:
|
| 462 |
+
if crop_img.width == 1 and crop_img.height == 1: # This is for body measurements
|
| 463 |
bbox_title = FONT_TITLE.getbbox(title)
|
| 464 |
text_title_width = bbox_title[2] - bbox_title[0]
|
| 465 |
text_title_height = bbox_title[3] - bbox_title[1]
|
|
|
|
| 500 |
current_y_offset_crops += text_desc_height + ITEM_SPACING
|
| 501 |
|
| 502 |
separator_text_original = "Originalbild mit Markierungen"
|
| 503 |
+
separator_text_crops = "Detaillierte Analysen"
|
| 504 |
+
|
| 505 |
+
if analysis_mode == 'Brüste (Nur Brüste)':
|
| 506 |
+
separator_text_crops = "Detaillierte Brust-Analysen"
|
| 507 |
+
elif analysis_mode == 'Vagina (Nur Vagina)':
|
| 508 |
+
separator_text_crops = "Detaillierte Vagina-Analysen"
|
| 509 |
+
elif analysis_mode == 'Komplett (Brüste + Vagina + geschätztes Alter)':
|
| 510 |
+
separator_text_crops = "Detaillierte Gesamt-Analysen"
|
| 511 |
|
| 512 |
bbox_separator_original = FONT_TITLE.getbbox(separator_text_original)
|
| 513 |
bbox_separator_crops = FONT_TITLE.getbbox(separator_text_crops)
|
|
|
|
| 545 |
|
| 546 |
print("[INFO] Combined image generated successfully.")
|
| 547 |
|
|
|
|
| 548 |
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_pdf:
|
| 549 |
combined_img.save(tmp_pdf.name, "PDF")
|
| 550 |
pdf_path = tmp_pdf.name
|
|
|
|
| 552 |
return combined_img, pdf_path
|
| 553 |
|
| 554 |
except Exception as e:
|
| 555 |
+
print(f"[CRITICAL ERROR] An unexpected error occurred during single image analysis: {e}")
|
| 556 |
+
if temp_img_path and os.path.exists(temp_img_path):
|
| 557 |
+
os.remove(temp_img_path)
|
| 558 |
+
return create_error_image(f'Unerwarteter Fehler bei der Bildanalyse für {os.path.basename(input_image_path)}: {e}'), None
|
| 559 |
+
finally:
|
| 560 |
+
if temp_img_path and os.path.exists(temp_img_path):
|
| 561 |
+
os.remove(temp_img_path)
|
| 562 |
+
|
| 563 |
+
def analyze_images_for_gallery(analysis_mode, input_image_paths):
|
| 564 |
+
print(f"[INFO] analyze_images_for_gallery called with mode: {analysis_mode} and {len(input_image_paths) if input_image_paths else 0} paths.")
|
| 565 |
+
all_combined_images = []
|
| 566 |
+
all_pdf_paths = []
|
| 567 |
+
|
| 568 |
+
if not input_image_paths:
|
| 569 |
+
return [create_error_image('Keine Bilder ausgewählt.')], [None]
|
| 570 |
+
|
| 571 |
+
for input_image_path in input_image_paths:
|
| 572 |
+
combined_img, pdf_path = _analyze_single_image_and_generate_outputs(analysis_mode, input_image_path)
|
| 573 |
+
all_combined_images.append(combined_img)
|
| 574 |
+
all_pdf_paths.append(pdf_path)
|
| 575 |
+
|
| 576 |
+
return all_combined_images, all_pdf_paths
|
| 577 |
|
| 578 |
interface = gr.Interface(
|
| 579 |
+
fn=analyze_images_for_gallery,
|
| 580 |
+
inputs=[
|
| 581 |
+
gr.Radio(
|
| 582 |
+
choices=['Komplett (Brüste + Vagina + geschätztes Alter)', 'Brüste (Nur Brüste)', 'Vagina (Nur Vagina)'],
|
| 583 |
+
value='Komplett (Brüste + Vagina + geschätztes Alter)',
|
| 584 |
+
label='Analyse Modus'
|
| 585 |
+
),
|
| 586 |
+
gr.File(file_count="multiple", type="filepath", label="Bilder hochladen (mehrere Dateien erlaubt)")
|
| 587 |
+
],
|
| 588 |
outputs=[
|
| 589 |
+
gr.Gallery(label="Analysierte Kompositbilder"),
|
| 590 |
+
gr.File(file_count="multiple", label="PDFs herunterladen")
|
| 591 |
],
|
| 592 |
+
title="🔞 Nacktheits-Analysator",
|
| 593 |
+
description="Laden Sie ein oder mehrere Bilder hoch, um eine detaillierte Analyse basierend auf dem ausgewählten Modus zu erhalten. Das Ergebnis ist ein Kompositbild mit Originalbild und detaillierten Crops, pro Person gruppiert. Sie können auch eine PDF-Version herunterladen."
|
| 594 |
)
|
| 595 |
|
| 596 |
if __name__ == "__main__":
|