Spaces:
Sleeping
Sleeping
| from PIL import Image | |
| import numpy as np | |
| import gradio as gr | |
| import os | |
| import cv2 | |
| from nudenet import NudeDetector | |
| import concurrent.futures | |
| # --- Konstanten --- | |
| DETECTION_MAX_DIM = 768 | |
| PIXELS_PER_CM_ESTIMATE = 15 | |
| MIN_CONFIDENCE = 0.45 | |
| 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/Horizontal" if ratio > 1.15 else "Hoch/Vertikal" if ratio < 0.85 else "Rund/Ausgewogen" | |
| 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"Form: {shape} · Größe: {size} · Nippel: {'Sichtbar' if nipple_detected else 'Flach'} · {w_cm}×{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 = "komplett rasiert" if hair_ratio < 0.04 else "minimal/Strip" if hair_ratio < 0.13 else "Brazilian" if hair_ratio < 0.36 else "voll behaart" | |
| ratio = w / h | |
| area = w * h | |
| if area < 18000: | |
| form_desc = "Winzige Innie" if 0.75 <= ratio <= 1.25 else "Vertikal" if ratio < 0.75 else "Horizontal" | |
| prom = "sehr unauffällig" | |
| elif area > 65000 and ratio > 1.45: | |
| form_desc = "Extrem Outie (Puff)" | |
| prom = "sehr stark" | |
| elif ratio > 1.45: | |
| form_desc = "Breites Outie" | |
| prom = "stark" | |
| elif ratio < 0.75: | |
| form_desc = "Lange Innie" | |
| prom = "mittel" | |
| elif ratio > 1.1: | |
| form_desc = "Klassisches Outie" | |
| prom = "sichtbar" | |
| else: | |
| form_desc = "Ausgewogene Innie/Outie" | |
| prom = "normal" | |
| visibility = "Innere Labien sichtbar" if "Outie" in form_desc or prom in ["stark", "sehr stark", "sichtbar"] else "Innere Labien verdeckt" | |
| size = "winzig" if area < 18000 else "klein" if area < 38000 else "mittel" if area < 65000 else "groß & voll" | |
| w_cm = round(w / PIXELS_PER_CM_ESTIMATE, 1) | |
| h_cm = round(h / PIXELS_PER_CM_ESTIMATE, 1) | |
| return f"Form: {form_desc} · Größe: {size} · Prominenz: {prom} · Sichtbarkeit: {visibility} · Behaart: {shaved} · {w_cm}×{h_cm} cm" | |
| detector = NudeDetector(inference_resolution=640) | |
| def process_single_image(mode: str, path: str): | |
| try: | |
| original_pil = Image.open(path).convert("RGB") | |
| detection_pil, scale = resize_for_detection(original_pil, DETECTION_MAX_DIM) | |
| detections = detector.detect(np.array(detection_pil)) | |
| target_class = "FEMALE_BREAST_EXPOSED" if mode == "Brüste" else "FEMALE_GENITALIA_EXPOSED" | |
| relevant = [d for d in detections if d["class"] == target_class and d.get("score", 0) >= MIN_CONFIDENCE] | |
| filename = os.path.basename(path) | |
| result = f"**{filename}** — {mode}\n\n" | |
| if not relevant: | |
| result += "❌ Keine relevanten Bereiche erkannt.\n\n" | |
| return result | |
| result += f"✅ **{len(relevant)}** {mode.lower()} gefunden\n\n" | |
| for i, det in enumerate(relevant, 1): | |
| x, y, w, h = [int(v * scale) for v in det["box"]] | |
| crop_pil = original_pil.crop((x, y, x + w, y + h)) | |
| desc = describe_breast_precise(crop_pil) if mode == "Brüste" else describe_vagina_precise(crop_pil) | |
| result += f"**{mode} {i}**\n{desc}\n\n" | |
| return result | |
| except Exception as e: | |
| return f"**{os.path.basename(path)}** — Fehler: {str(e)}\n\n" | |
| def analyze_text(mode: str, image_paths): | |
| if not image_paths: | |
| return "**Keine Bilder hochgeladen.**" | |
| max_workers = min(6, len(image_paths), os.cpu_count() or 4) | |
| with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: | |
| futures = [executor.submit(process_single_image, mode, p) for p in image_paths] | |
| results = [f.result() for f in concurrent.futures.as_completed(futures)] | |
| return "\n---\n\n".join(results) | |
| custom_css = """ | |
| body { background: #0f0f1a; color: #e0e0ff; font-family: system-ui, sans-serif; } | |
| .gradio-container { max-width: 1100px !important; margin: auto; } | |
| h1 { color: #ff2e82; text-shadow: 0 0 25px #ff2e8260; } | |
| button { background: linear-gradient(45deg, #ff2e82, #c71585) !important; border: none !important; font-weight: 600; } | |
| .markdown { font-size: 15.5px; line-height: 1.65; } | |
| """ | |
| interface = gr.Interface( | |
| fn=analyze_text, | |
| inputs=[ | |
| gr.Radio(choices=["Brüste", "Vagina"], value="Brüste", label="Suchmodus"), | |
| gr.File(file_count="multiple", type="filepath", label="Bilder hochladen") | |
| ], | |
| outputs=gr.Markdown(label="Text-Ergebnisse"), | |
| title="Nackt-Analyzer – Brüste / Vagina", | |
| description="Schnelle textbasierte Analyse (optimiert für Gradio 6+)" | |
| ) | |
| if __name__ == "__main__": | |
| interface.launch( | |
| share=True, | |
| theme=gr.themes.Soft(primary_hue="pink", secondary_hue="purple"), | |
| css=custom_css | |
| ) |