Upload app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 4 |
+
import cv2
|
| 5 |
+
from nudenet import NudeDetector
|
| 6 |
+
|
| 7 |
+
# --- Konstanten ---
|
| 8 |
+
DETECTION_MAX_DIM = 768
|
| 9 |
+
PIXELS_PER_CM_ESTIMATE = 15
|
| 10 |
+
MIN_CONFIDENCE = 0.45
|
| 11 |
+
|
| 12 |
+
# --- NudeNet Detector ---
|
| 13 |
+
detector = NudeDetector(inference_resolution=640)
|
| 14 |
+
|
| 15 |
+
# --- Hilfsfunktionen ---
|
| 16 |
+
def resize_for_detection(img_pil, max_dim):
|
| 17 |
+
if max(img_pil.width, img_pil.height) <= max_dim:
|
| 18 |
+
return img_pil, 1.0
|
| 19 |
+
ratio = max_dim / max(img_pil.width, img_pil.height)
|
| 20 |
+
new_size = (int(img_pil.width * ratio), int(img_pil.height * ratio))
|
| 21 |
+
resized = img_pil.resize(new_size, Image.Resampling.LANCZOS)
|
| 22 |
+
scale = 1 / ratio
|
| 23 |
+
return resized, scale
|
| 24 |
+
|
| 25 |
+
def describe_breast_precise(crop_pil):
|
| 26 |
+
w,h = crop_pil.size
|
| 27 |
+
if w*h == 0:
|
| 28 |
+
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 (p:=cv2.arcLength(c,True))>0 and
|
| 34 |
+
(4*np.pi*cv2.contourArea(c)/(p*p))>0.55
|
| 35 |
+
for c in contours
|
| 36 |
+
)
|
| 37 |
+
ratio = w/h
|
| 38 |
+
shape = "Breit" if ratio>1.15 else "Hoch" if ratio<0.85 else "Rund"
|
| 39 |
+
size = "klein" if w*h<28000 else "mittel" if w*h<75000 else "groß" if w*h<140000 else "sehr groß"
|
| 40 |
+
w_cm = round(w/PIXELS_PER_CM_ESTIMATE,1)
|
| 41 |
+
h_cm = round(h/PIXELS_PER_CM_ESTIMATE,1)
|
| 42 |
+
return f"Brust: {shape}, {size}, Nippel: {'Ja' if nipple_detected else 'Nein'}, {w_cm}x{h_cm}cm"
|
| 43 |
+
|
| 44 |
+
def describe_vagina_precise(crop_pil):
|
| 45 |
+
w,h = crop_pil.size
|
| 46 |
+
if w*h == 0:
|
| 47 |
+
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) # <--- korrigiert
|
| 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 |
+
# --- Bildverarbeitung ---
|
| 67 |
+
def process_image(image):
|
| 68 |
+
try:
|
| 69 |
+
original_pil = Image.fromarray(image).convert("RGB") if isinstance(image,np.ndarray) else image.convert("RGB")
|
| 70 |
+
detection_pil, scale = resize_for_detection(original_pil, DETECTION_MAX_DIM)
|
| 71 |
+
detections = detector.detect(np.array(detection_pil))
|
| 72 |
+
draw = ImageDraw.Draw(original_pil)
|
| 73 |
+
font = ImageFont.load_default()
|
| 74 |
+
results_text = []
|
| 75 |
+
|
| 76 |
+
for det in detections:
|
| 77 |
+
label = det["class"]
|
| 78 |
+
score = det.get("score",0)
|
| 79 |
+
if score < MIN_CONFIDENCE:
|
| 80 |
+
continue
|
| 81 |
+
if label not in ["FEMALE_BREAST_EXPOSED","FEMALE_GENITALIA_EXPOSED"]:
|
| 82 |
+
continue
|
| 83 |
+
x,y,w,h = [int(v*scale) for v in det["box"]]
|
| 84 |
+
crop_pil = original_pil.crop((x,y,x+w,y+h))
|
| 85 |
+
if label=="FEMALE_BREAST_EXPOSED":
|
| 86 |
+
desc = describe_breast_precise(crop_pil)
|
| 87 |
+
color = (255,46,130)
|
| 88 |
+
else:
|
| 89 |
+
desc = describe_vagina_precise(crop_pil)
|
| 90 |
+
color = (138,43,226)
|
| 91 |
+
draw.rectangle([x,y,x+w,y+h],outline=color,width=4)
|
| 92 |
+
text_pos = (x,y-15 if y>15 else y+h)
|
| 93 |
+
draw.text(text_pos,desc,fill=color,font=font)
|
| 94 |
+
results_text.append(desc)
|
| 95 |
+
|
| 96 |
+
if not results_text:
|
| 97 |
+
draw.text((10,10),"Keine relevanten Bereiche erkannt.",fill=(255,0,0),font=font)
|
| 98 |
+
|
| 99 |
+
return np.array(original_pil)
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
print(f"Fehler: {e}")
|
| 103 |
+
return None
|
| 104 |
+
|
| 105 |
+
# --- Gradio App ---
|
| 106 |
+
css = """
|
| 107 |
+
body { background: #0f0f1a; color: #e0e0ff; }
|
| 108 |
+
.gradio-container { max-width: 900px !important; margin: auto; }
|
| 109 |
+
h1 { color: #ff2e82; text-align: center; }
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
with gr.Blocks(css=css) as demo:
|
| 113 |
+
gr.Markdown("# 👙 Automatischer Nackt-Analyzer")
|
| 114 |
+
gr.Markdown("Lade ein Bild hoch und erhalte direkt das analysierte Bild mit Annotationen.")
|
| 115 |
+
input_image = gr.Image(type="numpy", label="Bild hochladen")
|
| 116 |
+
output_image = gr.Image(label="Analyse-Ergebnis")
|
| 117 |
+
input_image.change(fn=process_image, inputs=input_image, outputs=output_image)
|
| 118 |
+
|
| 119 |
+
demo.launch()
|