| import gradio as gr |
| import numpy as np |
| from PIL import Image, ImageDraw, ImageFont |
| import cv2 |
| import os |
| import tempfile |
| from nudenet import NudeDetector |
|
|
| |
| DETECTION_MAX_DIM = 768 |
| PIXELS_PER_CM_ESTIMATE = 15 |
| MIN_CONFIDENCE = 0.45 |
|
|
| detector = NudeDetector(inference_resolution=640) |
|
|
| def resize_for_detection(img_pil, max_dim): |
| if max(img_pil.width, img_pil.height) <= max_dim: |
| return img_pil, 1.0 |
| ratio = max_dim / max(img_pil.width, img_pil.height) |
| new_size = (int(img_pil.width * ratio), int(img_pil.height * ratio)) |
| resized = img_pil.resize(new_size, Image.Resampling.LANCZOS) |
| scale = 1 / ratio |
| return resized, scale |
|
|
| def describe_breast_precise(crop_pil): |
| w, h = crop_pil.size |
| if w * h == 0: |
| return "Fehler: leeres Crop" |
| gray = cv2.cvtColor(np.array(crop_pil), cv2.COLOR_RGB2GRAY) |
| _, thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) |
| contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
| nipple_detected = any( |
| 40 < cv2.contourArea(c) < (w * h / 4) |
| and (p := cv2.arcLength(c, True)) > 0 |
| and (4 * np.pi * cv2.contourArea(c) / (p * p)) > 0.55 |
| for c in contours |
| ) |
| ratio = w / h |
| shape = "Breit" if ratio > 1.15 else "Hoch" if ratio < 0.85 else "Rund" |
| size = "klein" if w * h < 28000 else "mittel" if w * h < 75000 else "groß" if w * h < 140000 else "sehr groß" |
| w_cm = round(w / PIXELS_PER_CM_ESTIMATE, 1) |
| h_cm = round(h / PIXELS_PER_CM_ESTIMATE, 1) |
| return f"Brust: {shape}, {size}, Nippel: {'Ja' if nipple_detected else 'Nein'}, {w_cm}x{h_cm}cm" |
|
|
| def describe_vagina_precise(crop_pil): |
| w, h = crop_pil.size |
| if w * h == 0: |
| return "Fehler: leeres Crop" |
| gray = cv2.cvtColor(np.array(crop_pil), cv2.COLOR_RGB2GRAY) |
| hair_ratio = np.sum(cv2.inRange(gray, 35, 145) > 0) / (w * h) |
| shaved = "rasiert" if hair_ratio < 0.04 else "minimal" if hair_ratio < 0.13 else "Brazilian" if hair_ratio < 0.36 else "behaart" |
| ratio = w / h |
| area = w * h |
| if area < 18000: |
| form_desc = "Innie" |
| elif area > 65000 and ratio > 1.45: |
| form_desc = "Outie (Puff)" |
| elif ratio > 1.45: |
| form_desc = "Outie" |
| else: |
| form_desc = "Innie/Outie" |
| size = "winzig" if area < 18000 else "klein" if area < 38000 else "mittel" if area < 65000 else "groß" |
| w_cm = round(w / PIXELS_PER_CM_ESTIMATE, 1) |
| h_cm = round(h / PIXELS_PER_CM_ESTIMATE, 1) |
| return f"Vagina: {form_desc}, {size}, {shaved}, {w_cm}x{h_cm}cm" |
|
|
| def process_image(image_path): |
| try: |
| original_pil = Image.open(image_path).convert("RGB") |
| detection_pil, scale = resize_for_detection(original_pil, DETECTION_MAX_DIM) |
| detections = detector.detect(np.array(detection_pil)) |
|
|
| draw = ImageDraw.Draw(original_pil) |
| try: |
| font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20) |
| except Exception: |
| font = ImageFont.load_default() |
|
|
| results_text = [] |
|
|
| for det in detections: |
| label = det["class"] |
| score = det.get("score", 0) |
| if score < MIN_CONFIDENCE: |
| continue |
|
|
| if label not in ["FEMALE_BREAST_EXPOSED", "FEMALE_GENITALIA_EXPOSED"]: |
| continue |
|
|
| x, y, w, h = [int(v * scale) for v in det["box"]] |
| crop_pil = original_pil.crop((x, y, x + w, y + h)) |
|
|
| if label == "FEMALE_BREAST_EXPOSED": |
| desc = describe_breast_precise(crop_pil) |
| color = (255, 46, 130) |
| else: |
| desc = describe_vagina_precise(crop_pil) |
| color = (138, 43, 226) |
|
|
| draw.rectangle([x, y, x + w, y + h], outline=color, width=4) |
| text_pos = (x, y - 25 if y > 25 else y + h) |
| draw.text(text_pos, desc, fill=color, font=font) |
| results_text.append(desc) |
|
|
| if not results_text: |
| draw.text((10, 10), "Keine relevanten Bereiche erkannt.", fill=(255, 0, 0), font=font) |
|
|
| return original_pil |
|
|
| except Exception as e: |
| print(f"Fehler: {e}") |
| return None |
|
|
| def analyze_all(files): |
| if not files: |
| return [], [] |
|
|
| processed_images = [] |
| output_files = [] |
|
|
| output_dir = os.path.join(tempfile.gettempdir(), "gradio_analyzer_outputs") |
| os.makedirs(output_dir, exist_ok=True) |
|
|
| for f in files: |
| res = process_image(f.name) |
| if res is None: |
| continue |
|
|
| processed_images.append(res) |
|
|
| base_name = os.path.splitext(os.path.basename(f.name))[0] |
| out_path = os.path.join(output_dir, f"{base_name}_analyzed.png") |
| res.save(out_path) |
| output_files.append(out_path) |
|
|
| return processed_images, output_files |
|
|
| custom_css = """ |
| body { background: #0f0f1a; color: #e0e0ff; } |
| .gradio-container { max-width: 1000px !important; margin: auto; } |
| h1 { color: #ff2e82; text-align: center; } |
| """ |
|
|
| with gr.Blocks() as demo: |
| gr.Markdown("# 👙 Automatischer Nackt-Analyzer") |
| gr.Markdown("Lade Bilder hoch für eine automatische Analyse. Die Ergebnisse werden im Bild angezeigt und zusätzlich als Dateien bereitgestellt.") |
|
|
| with gr.Row(): |
| input_files = gr.File(file_count="multiple", label="Bilder hochladen") |
|
|
| with gr.Row(): |
| output_gallery = gr.Gallery( |
| label="Analyse-Ergebnisse", |
| columns=2, |
| height="auto" |
| ) |
|
|
| with gr.Row(): |
| output_downloads = gr.File( |
| label="Analysierte Bilder herunterladen", |
| file_count="multiple" |
| ) |
|
|
| input_files.change( |
| fn=analyze_all, |
| inputs=input_files, |
| outputs=[output_gallery, output_downloads] |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch( |
| css=custom_css, |
| theme=gr.themes.Soft(primary_hue="pink") |
| ) |