Upload 2 files
Browse files- app_3.py +149 -0
- requirements_3.txt +6 -0
app_3.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 4 |
+
import cv2
|
| 5 |
+
import os
|
| 6 |
+
from nudenet import NudeDetector
|
| 7 |
+
import concurrent.futures
|
| 8 |
+
|
| 9 |
+
# --- Konstanten ---
|
| 10 |
+
DETECTION_MAX_DIM = 768
|
| 11 |
+
PIXELS_PER_CM_ESTIMATE = 15
|
| 12 |
+
MIN_CONFIDENCE = 0.45
|
| 13 |
+
|
| 14 |
+
# Initialisiere den Detektor
|
| 15 |
+
detector = NudeDetector(inference_resolution=640)
|
| 16 |
+
|
| 17 |
+
def resize_for_detection(img_pil, max_dim):
|
| 18 |
+
if max(img_pil.width, img_pil.height) <= max_dim:
|
| 19 |
+
return img_pil, 1.0
|
| 20 |
+
ratio = max_dim / max(img_pil.width, img_pil.height)
|
| 21 |
+
new_size = (int(img_pil.width * ratio), int(img_pil.height * ratio))
|
| 22 |
+
resized = img_pil.resize(new_size, Image.Resampling.LANCZOS)
|
| 23 |
+
scale = 1 / ratio
|
| 24 |
+
return resized, scale
|
| 25 |
+
|
| 26 |
+
def describe_breast_precise(crop_pil):
|
| 27 |
+
w, h = crop_pil.size
|
| 28 |
+
if w * h == 0: return "Fehler: leeres Crop"
|
| 29 |
+
gray = cv2.cvtColor(np.array(crop_pil), cv2.COLOR_RGB2GRAY)
|
| 30 |
+
_, thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 31 |
+
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 32 |
+
nipple_detected = any(
|
| 33 |
+
40 < cv2.contourArea(c) < (w * h / 4) and
|
| 34 |
+
(p := cv2.arcLength(c, True)) > 0 and
|
| 35 |
+
(4 * np.pi * cv2.contourArea(c) / (p * p)) > 0.55
|
| 36 |
+
for c in contours
|
| 37 |
+
)
|
| 38 |
+
ratio = w / h
|
| 39 |
+
shape = "Breit" if ratio > 1.15 else "Hoch" if ratio < 0.85 else "Rund"
|
| 40 |
+
size = "klein" if w*h < 28000 else "mittel" if w*h < 75000 else "groß" if w*h < 140000 else "sehr groß"
|
| 41 |
+
w_cm = round(w / PIXELS_PER_CM_ESTIMATE, 1)
|
| 42 |
+
h_cm = round(h / PIXELS_PER_CM_ESTIMATE, 1)
|
| 43 |
+
return f"Brust: {shape}, {size}, Nippel: {'Ja' if nipple_detected else 'Nein'}, {w_cm}x{h_cm}cm"
|
| 44 |
+
|
| 45 |
+
def describe_vagina_precise(crop_pil):
|
| 46 |
+
w, h = crop_pil.size
|
| 47 |
+
if w * h == 0: return "Fehler: leeres Crop"
|
| 48 |
+
gray = cv2.cvtColor(np.array(crop_pil), cv2.COLOR_RGB2GRAY)
|
| 49 |
+
hair_ratio = np.sum(cv2.inRange(gray, 35, 145) > 0) / (w * h)
|
| 50 |
+
shaved = "rasiert" if hair_ratio < 0.04 else "minimal" if hair_ratio < 0.13 else "Brazilian" if hair_ratio < 0.36 else "behaart"
|
| 51 |
+
ratio = w / h
|
| 52 |
+
area = w * h
|
| 53 |
+
if area < 18000:
|
| 54 |
+
form_desc = "Innie"
|
| 55 |
+
elif area > 65000 and ratio > 1.45:
|
| 56 |
+
form_desc = "Outie (Puff)"
|
| 57 |
+
elif ratio > 1.45:
|
| 58 |
+
form_desc = "Outie"
|
| 59 |
+
else:
|
| 60 |
+
form_desc = "Innie/Outie"
|
| 61 |
+
size = "winzig" if area < 18000 else "klein" if area < 38000 else "mittel" if area < 65000 else "groß"
|
| 62 |
+
w_cm = round(w / PIXELS_PER_CM_ESTIMATE, 1)
|
| 63 |
+
h_cm = round(h / PIXELS_PER_CM_ESTIMATE, 1)
|
| 64 |
+
return f"Vagina: {form_desc}, {size}, {shaved}, {w_cm}x{h_cm}cm"
|
| 65 |
+
|
| 66 |
+
def process_image(image_path):
|
| 67 |
+
try:
|
| 68 |
+
original_pil = Image.open(image_path).convert("RGB")
|
| 69 |
+
detection_pil, scale = resize_for_detection(original_pil, DETECTION_MAX_DIM)
|
| 70 |
+
detections = detector.detect(np.array(detection_pil))
|
| 71 |
+
|
| 72 |
+
draw = ImageDraw.Draw(original_pil)
|
| 73 |
+
# Versuche eine Schriftart zu laden, sonst Standard
|
| 74 |
+
try:
|
| 75 |
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20)
|
| 76 |
+
except:
|
| 77 |
+
font = ImageFont.load_default()
|
| 78 |
+
|
| 79 |
+
results_text = []
|
| 80 |
+
|
| 81 |
+
for det in detections:
|
| 82 |
+
label = det["class"]
|
| 83 |
+
score = det.get("score", 0)
|
| 84 |
+
if score < MIN_CONFIDENCE:
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
if label not in ["FEMALE_BREAST_EXPOSED", "FEMALE_GENITALIA_EXPOSED"]:
|
| 88 |
+
continue
|
| 89 |
+
|
| 90 |
+
x, y, w, h = [int(v * scale) for v in det["box"]]
|
| 91 |
+
crop_pil = original_pil.crop((x, y, x + w, y + h))
|
| 92 |
+
|
| 93 |
+
if label == "FEMALE_BREAST_EXPOSED":
|
| 94 |
+
desc = describe_breast_precise(crop_pil)
|
| 95 |
+
color = (255, 46, 130) # Pink
|
| 96 |
+
else:
|
| 97 |
+
desc = describe_vagina_precise(crop_pil)
|
| 98 |
+
color = (138, 43, 226) # Purple
|
| 99 |
+
|
| 100 |
+
# Zeichne Box
|
| 101 |
+
draw.rectangle([x, y, x + w, y + h], outline=color, width=4)
|
| 102 |
+
# Zeichne Text-Hintergrund
|
| 103 |
+
text_pos = (x, y - 25 if y > 25 else y + h)
|
| 104 |
+
draw.text(text_pos, desc, fill=color, font=font)
|
| 105 |
+
results_text.append(desc)
|
| 106 |
+
|
| 107 |
+
if not results_text:
|
| 108 |
+
# Wenn nichts gefunden wurde, gib das Originalbild mit Hinweis zurück
|
| 109 |
+
draw.text((10, 10), "Keine relevanten Bereiche erkannt.", fill=(255, 0, 0), font=font)
|
| 110 |
+
return original_pil
|
| 111 |
+
|
| 112 |
+
return original_pil
|
| 113 |
+
except Exception as e:
|
| 114 |
+
print(f"Fehler: {e}")
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
def analyze_all(files):
|
| 118 |
+
if not files:
|
| 119 |
+
return None
|
| 120 |
+
|
| 121 |
+
processed_images = []
|
| 122 |
+
for f in files:
|
| 123 |
+
res = process_image(f.name)
|
| 124 |
+
if res:
|
| 125 |
+
processed_images.append(res)
|
| 126 |
+
|
| 127 |
+
return processed_images
|
| 128 |
+
|
| 129 |
+
custom_css = """
|
| 130 |
+
body { background: #0f0f1a; color: #e0e0ff; }
|
| 131 |
+
.gradio-container { max-width: 1000px !important; margin: auto; }
|
| 132 |
+
h1 { color: #ff2e82; text-align: center; }
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
with gr.Blocks(css=custom_css, theme=gr.themes.Soft(primary_hue="pink")) as demo:
|
| 136 |
+
gr.Markdown("# 👙 Automatischer Nackt-Analyzer")
|
| 137 |
+
gr.Markdown("Lade Bilder hoch für eine automatische Analyse von Brüsten und Vagina. Die Ergebnisse werden direkt im Bild angezeigt.")
|
| 138 |
+
|
| 139 |
+
with gr.Row():
|
| 140 |
+
input_files = gr.File(file_count="multiple", label="Bilder hochladen")
|
| 141 |
+
|
| 142 |
+
with gr.Row():
|
| 143 |
+
output_gallery = gr.Gallery(label="Analyse-Ergebnisse", columns=2, height="auto", show_download_button=True)
|
| 144 |
+
|
| 145 |
+
# Automatischer Trigger bei Upload
|
| 146 |
+
input_files.change(fn=analyze_all, inputs=input_files, outputs=output_gallery)
|
| 147 |
+
|
| 148 |
+
if __name__ == "__main__":
|
| 149 |
+
demo.launch()
|
requirements_3.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=6.9.0
|
| 2 |
+
nudenet>=3.4.2
|
| 3 |
+
opencv-python-headless>=4.11.0
|
| 4 |
+
pillow>=11.1.0
|
| 5 |
+
numpy>=2.2.0
|
| 6 |
+
onnxruntime>=1.20.0
|