Galaxydude2 commited on
Commit
ea4fc2e
·
verified ·
1 Parent(s): 1c760f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -182
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", 24)
39
- FONT_LABEL_SIMPLE = ImageFont.truetype("arial.ttf", 14)
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 "KörpermaÑe nicht schätzbar (ungültiger Personen-BBox)"
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 = [det for det in associated_nudenet_dets if 'BREAST_EXPOSED' in det['class']]
102
- vagina_detections = [det for det in associated_nudenet_dets if 'FEMALE_GENITALIA_EXPOSED' in det['class']]
 
 
 
 
 
103
 
104
  if breast_detections:
105
- total_breast_width_px = sum([det['box'][2] for det in breast_detections])
106
- if len(breast_detections) == 2:
107
- bust_width_px = (breast_detections[0]['box'][2] + breast_detections[1]['box'][2]) / 2
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"MaÑe (geschätzt, unsicher): Büste: {bust_cm} cm · Taille: {waist_cm} cm · Hüfte: {hip_cm} cm"
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
- area = w * h
129
- if area < 35000: cup = "A"
130
- elif area < 60000: cup = "B"
131
- elif area < 95000: cup = "C"
132
- elif area < 140000: cup = "D"
133
- elif area < 200000: cup = "E/F"
134
- else: cup = "H+ (massiv)"
135
-
136
- hsv = cv2.cvtColor(crop_np, cv2.COLOR_RGB2HSV)
137
- lower_nipple = np.array([0, 50, 20])
138
- upper_nipple = np.array([20, 255, 140])
139
- mask = cv2.inRange(hsv, lower_nipple, upper_nipple)
140
- contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
141
- areola_px = max([cv2.contourArea(c) for c in contours], default=0)
142
- if areola_px < 800: nipple = "sehr klein (XS)"
143
- elif areola_px < 2000: nipple = "klein"
144
- elif areola_px < 4500: nipple = "mittel"
145
- elif areola_px < 8000: nipple = "groÑ"
146
- else: nipple = "sehr groÑ (XXL)"
147
-
148
- edges = cv2.Canny(gray, 50, 100)
149
- lower_third = edges[int(h*0.66):, :]
150
- sag_ratio = np.sum(lower_third) / np.sum(edges) if np.sum(edges) > 0 else 0
151
- if sag_ratio < 0.15: firmness = "extrem perky & straff"
152
- elif sag_ratio < 0.28: firmness = "fest & jugendlich"
153
- elif sag_ratio < 0.45: firmness = "natürlich weich"
154
- else: firmness = "deutlich hängend"
155
 
156
  size_cm_estimate = estimate_breast_size_cm(crop)
157
 
158
- return f"Körbchen: {cup} · Areola/Nippel: {nipple} · Festigkeit: {firmness} · MaÑe: {size_cm_estimate}"
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
- if ratio_wh > 1.4 and labia_area > 65000: form = "Puff Outie (dick & vorstehend)"
176
- elif ratio_wh > 1.2: form = "Outie (Labien deutlich sichtbar)"
177
- elif ratio_wh < 0.75: form = "lange Innie (fast geschlossen)"
178
- elif labia_area < 25000: form = "sehr kleine Barbie (Mini-Innie)"
179
- else: form = "klassische Innie / Barbie"
180
-
181
- size = "winzig" if labia_area < 20000 else "klein" if labia_area < 40000 else "mittel" if labia_area < 70000 else "groÑ & voll"
 
 
 
 
 
 
 
 
182
 
183
  size_cm_estimate = estimate_vagina_size_cm(crop)
184
 
185
- return f"Form: {form} · GröÑe: {size} · Behaart: {shaved} · MaÑe: {size_cm_estimate}"
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 analyze_image_with_gradio(input_image_path):
208
- print(f"[INFO] analyze_image_with_gradio called with path: {input_image_path}")
 
 
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(input_image_path)
226
- print(f"[INFO] NudeNet detected {len(nudenet_detections)} objects.")
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- try:
234
- face_results = DeepFace.extract_faces(original_img_np, detector_backend='retinaface', enforce_detection=False)
235
- face_detections = [f for f in face_results if f['confidence'] > 0.9]
236
- print(f"[INFO] DeepFace detected {len(face_detections)} faces.")
237
- except Exception as e:
238
- print(f"[WARNING] Error during DeepFace face detection: {e}. Proceeding without person association.")
 
 
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
- draw_annotated.rectangle([(px1, py1), (px2, py2)], outline=(0, 255, 0), width=BORDER_WIDTH)
287
- label_person = f"Person {i+1}"
288
- bbox_label_person = FONT_LABEL_SIMPLE.getbbox(label_person)
289
- text_width_person = bbox_label_person[2] - bbox_label_person[0]
290
- text_height_person = bbox_label_person[3] - bbox_label_person[1]
291
- draw_annotated.rectangle([(px1, py1 - text_height_person - 2), (px1 + text_width_person, py1)], fill=(0, 255, 0))
292
- draw_annotated.text((px1, py1 - text_height_person - 2), label_person, font=FONT_LABEL_SIMPLE, fill=BACKGROUND_COLOR)
293
-
294
- if person['body_measurements'] and person['body_measurements'] != "KörpermaÑe nicht schätzbar (ungültiger Personen-BBox)":
295
- current_image_crops_with_details.append((f"Person {i+1} KörpermaÑe", Image.new('RGB', (1,1), (0,0,0)), person['body_measurements']))
296
-
297
- person_breasts_for_merging = []
 
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
- try:
308
- face_x, face_y, face_w, face_h = person['face_area']['x'], person['face_area']['y'], person['face_area']['w'], person['face_area']['h']
309
- face_crop_for_age = original_img.crop((face_x, face_y, face_x + face_w, face_y + face_h))
310
- age_str = estimate_age(face_crop_for_age)
311
- except Exception as e:
312
- print(f"[ERROR] Error estimating age for person {i+1} crop: {e}")
313
-
314
- if 'FEMALE_GENITALIA_EXPOSED' in det_class:
 
315
  label_detailed = f"Person {i+1} Vagina"
316
- desc = describe_vagina_precise(crop) + f" · Alter: {age_str}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=(255, 255, 0))
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
- try:
375
- age_str = estimate_age(crop)
376
- except Exception as e:
377
- print(f"[ERROR] Error estimating age for global crop: {e}")
 
 
378
 
379
- if 'FEMALE_GENITALIA_EXPOSED' in det_class:
380
  label_detailed = "Global Vagina"
381
- desc = describe_vagina_precise(crop) + f" · Alter: {age_str}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 persons_data and not (len(global_breasts_for_merging) > 0 or any('FEMALE_GENITALIA_EXPOSED' in d['class'] for d in unassociated_nudenet_detections)):
417
- text = "Keine Personen oder relevante Körperteile 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("[INFO] No persons or relevant NudeNet detections, 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,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 Bereichsanalysen"
 
 
 
 
 
 
 
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
- return create_error_image(f'Unerwarteter Fehler bei der Bildanalyse: {e}'), None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
  interface = gr.Interface(
553
- fn=analyze_image_with_gradio,
554
- inputs=gr.Image(type="filepath", label="Bild hochladen"),
 
 
 
 
 
 
 
555
  outputs=[
556
- gr.Image(type="pil", label="Analysiertes Kompositbild"),
557
- gr.File(label="PDF herunterladen")
558
  ],
559
- title="🔞 Final Analyzer",
560
- description="Laden Sie ein Bild hoch, um eine detaillierte Analyse von Brüsten und Vagina (falls erkannt) inklusive Alters-Schätzung zu erhalten. Das Ergebnis ist ein Kompositbild mit Originalbild und detaillierten Crops, pro Person gruppiert. Sie können auch eine PDF-Version herunterladen."
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__":