import gradio as gr import numpy as np from PIL import Image, ImageDraw, ImageFont import cv2 import os import tempfile from nudenet import NudeDetector # --- Konstanten --- 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") )