Face-blurring / app.py
seawolf2357's picture
Update app.py
a4c6a53 verified
import gradio as gr
import cv2
import numpy as np
import tempfile
import os
from pathlib import Path
from typing import Optional, Tuple
import torch
from PIL import Image
# ==============================
# Model loader
# ==============================
def load_model(model_path: str = "yolov8-face-hf.pt", device: Optional[str] = None):
from ultralytics import YOLO
if device is None:
if torch.cuda.is_available():
device = "cuda"
elif torch.backends.mps.is_available():
device = "mps"
else:
device = "cpu"
model = YOLO(model_path)
model.to(device)
return model, device
# Load model globally
model, device = load_model()
# ==============================
# Helper functions
# ==============================
def _ensure_odd(x: int) -> int:
return x if x % 2 == 1 else x + 1
def _choose_writer_size(w: int, h: int) -> Tuple[int, int]:
return (w if w % 2 == 0 else w - 1, h if h % 2 == 0 else h - 1)
def _apply_anonymization(face_roi: np.ndarray, mode: str, blur_kernel: int, mosaic: int = 15) -> np.ndarray:
if face_roi.size == 0:
return face_roi
if mode == "Gaussian Blur":
k = _ensure_odd(max(blur_kernel, 15))
return cv2.GaussianBlur(face_roi, (k, k), 0)
else:
m = max(2, mosaic)
h, w = face_roi.shape[:2]
face_small = cv2.resize(face_roi, (max(1, w // m), max(1, h // m)), interpolation=cv2.INTER_LINEAR)
return cv2.resize(face_small, (w, h), interpolation=cv2.INTER_NEAREST)
def blur_faces_image(image_bgr, conf, iou, expand_ratio, mode, blur_kernel, mosaic):
h, w = image_bgr.shape[:2]
face_count = 0
with torch.no_grad():
results = model.predict(image_bgr, conf=conf, iou=iou, verbose=False, device=device)
for r in results:
boxes = r.boxes.xyxy.cpu().numpy() if hasattr(r.boxes, "xyxy") else []
face_count = len(boxes)
for x1, y1, x2, y2 in boxes:
x1, y1, x2, y2 = map(int, [x1, y1, x2, y2])
if expand_ratio > 0:
bw = x2 - x1
bh = y2 - y1
dx = int(bw * expand_ratio)
dy = int(bh * expand_ratio)
x1 -= dx; y1 -= dy; x2 += dx; y2 += dy
x1 = max(0, min(w, x1))
x2 = max(0, min(w, x2))
y1 = max(0, min(h, y1))
y2 = max(0, min(h, y2))
if x2 <= x1 or y2 <= y1:
continue
roi = image_bgr[y1:y2, x1:x2]
image_bgr[y1:y2, x1:x2] = _apply_anonymization(roi, mode, blur_kernel, mosaic)
return image_bgr, face_count
def blur_faces_video(input_path, conf, iou, expand_ratio, mode, blur_kernel, mosaic, progress=gr.Progress()):
from moviepy.editor import VideoFileClip
cap = cv2.VideoCapture(input_path)
if not cap.isOpened():
raise IOError("Cannot open video")
in_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
in_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 0
out_w, out_h = _choose_writer_size(in_w, in_h)
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
temp_video_path = tempfile.NamedTemporaryFile(delete=False, suffix="_temp.mp4").name
output_path = tempfile.NamedTemporaryFile(delete=False, suffix="_blurred.mp4").name
out = cv2.VideoWriter(temp_video_path, fourcc, fps, (out_w, out_h))
idx = 0
total_faces = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
frame = cv2.resize(frame, (out_w, out_h))
with torch.no_grad():
results = model.predict(frame, conf=conf, iou=iou, verbose=False, device=device)
h, w = frame.shape[:2]
r0 = results[0] if len(results) else None
boxes = r0.boxes.xyxy if (r0 and hasattr(r0, "boxes")) else []
total_faces += len(boxes)
for b in boxes:
x1, y1, x2, y2 = map(int, b)
if expand_ratio > 0:
bw = x2 - x1
bh = y2 - y1
dx = int(bw * expand_ratio)
dy = int(bh * expand_ratio)
x1 -= dx; y1 -= dy; x2 += dx; y2 += dy
x1 = max(0, min(w, x1))
x2 = max(0, min(w, x2))
y1 = max(0, min(h, y1))
y2 = max(0, min(h, y2))
if x2 <= x1 or y2 <= y1:
continue
roi = frame[y1:y2, x1:x2]
frame[y1:y2, x1:x2] = _apply_anonymization(roi, mode, blur_kernel, mosaic)
out.write(frame)
idx += 1
if frames > 0:
progress(idx / frames, desc=f"Processing frame {idx}/{frames}")
finally:
cap.release()
out.release()
try:
progress(0.95, desc="Merging audio...")
original = VideoFileClip(input_path)
processed = VideoFileClip(temp_video_path).set_audio(original.audio)
processed.write_videofile(
output_path,
codec="libx264",
audio_codec="aac",
threads=1,
logger=None
)
original.close()
processed.close()
return output_path, total_faces, frames
except Exception as e:
print("Audio merging failed:", e)
return temp_video_path, total_faces, frames
# ==============================
# Main Processing Functions
# ==============================
def process_image(image, conf, iou, expand_ratio, mode_choice, blur_intensity, mosaic_size):
if image is None:
return None, "⚠️ Please upload an image first!"
# Convert PIL to BGR
image_bgr = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
h, w = image_bgr.shape[:2]
# Determine blur settings
if mode_choice == "Gaussian Blur":
blur_kernel = blur_intensity
mosaic = 15
else:
blur_kernel = 51
mosaic = mosaic_size
# Process
result_bgr, face_count = blur_faces_image(
image_bgr.copy(), conf, iou, expand_ratio,
mode_choice, blur_kernel, mosaic
)
# Convert back to RGB
result_rgb = cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB)
result_pil = Image.fromarray(result_rgb)
# Generate log
info_log = f"""βœ… IMAGE PROCESSING COMPLETE!
{'=' * 50}
πŸ–ΌοΈ Image Info:
β€’ Size: {w} x {h} pixels
β€’ Format: RGB
{'=' * 50}
πŸ” Detection Settings:
β€’ Confidence: {conf}
β€’ IoU Threshold: {iou}
β€’ Box Expansion: {expand_ratio}
{'=' * 50}
🎨 Blur Settings:
β€’ Style: {mode_choice}
β€’ Intensity: {blur_intensity if mode_choice == "Gaussian Blur" else mosaic_size}
{'=' * 50}
πŸ‘€ Results:
β€’ Faces Detected: {face_count}
β€’ Faces Blurred: {face_count}
{'=' * 50}
πŸ’Ύ Ready to download!"""
return result_pil, info_log
def process_video(video, conf, iou, expand_ratio, mode_choice, blur_intensity, mosaic_size, progress=gr.Progress()):
if video is None:
return None, "⚠️ Please upload a video first!"
# Determine blur settings
if mode_choice == "Gaussian Blur":
blur_kernel = blur_intensity
mosaic = 15
else:
blur_kernel = 51
mosaic = mosaic_size
try:
output_path, total_faces, total_frames = blur_faces_video(
video, conf, iou, expand_ratio,
mode_choice, blur_kernel, mosaic, progress
)
info_log = f"""βœ… VIDEO PROCESSING COMPLETE!
{'=' * 50}
πŸŽ₯ Video Info:
β€’ Total Frames: {total_frames}
β€’ Output Path: {os.path.basename(output_path)}
{'=' * 50}
πŸ” Detection Settings:
β€’ Confidence: {conf}
β€’ IoU Threshold: {iou}
β€’ Box Expansion: {expand_ratio}
{'=' * 50}
🎨 Blur Settings:
β€’ Style: {mode_choice}
β€’ Intensity: {blur_intensity if mode_choice == "Gaussian Blur" else mosaic_size}
{'=' * 50}
πŸ‘€ Results:
β€’ Total Faces Detected: {total_faces}
β€’ Frames Processed: {total_frames}
{'=' * 50}
πŸ’Ύ Ready to download!"""
return output_path, info_log
except Exception as e:
return None, f"❌ Error: {str(e)}"
# ============================================
# 🎨 Comic Classic Theme - Toon Playground
# ============================================
css = """
/* ===== 🎨 Google Fonts Import ===== */
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
/* ===== 🎨 Comic Classic λ°°κ²½ - λΉˆν‹°μ§€ 페이퍼 + λ„νŠΈ νŒ¨ν„΄ ===== */
.gradio-container {
background-color: #FEF9C3 !important;
background-image:
radial-gradient(#1F2937 1px, transparent 1px) !important;
background-size: 20px 20px !important;
min-height: 100vh !important;
font-family: 'Comic Neue', cursive, sans-serif !important;
}
/* ===== ν—ˆκΉ…νŽ˜μ΄μŠ€ 상단 μš”μ†Œ μˆ¨κΉ€ ===== */
.huggingface-space-header,
#space-header,
.space-header,
[class*="space-header"],
.svelte-1ed2p3z,
.space-header-badge,
.header-badge,
[data-testid="space-header"],
.svelte-kqij2n,
.svelte-1ax1toq,
.embed-container > div:first-child {
display: none !important;
visibility: hidden !important;
height: 0 !important;
width: 0 !important;
overflow: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* ===== Footer μ™„μ „ μˆ¨κΉ€ ===== */
footer,
.footer,
.gradio-container footer,
.built-with,
[class*="footer"],
.gradio-footer,
.main-footer,
div[class*="footer"],
.show-api,
.built-with-gradio,
a[href*="gradio.app"],
a[href*="huggingface.co/spaces"] {
display: none !important;
visibility: hidden !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
/* ===== 메인 μ»¨ν…Œμ΄λ„ˆ ===== */
#col-container {
max-width: 1400px;
margin: 0 auto;
}
/* ===== 🎨 헀더 타이틀 - μ½”λ―Ή μŠ€νƒ€μΌ ===== */
.header-text h1 {
font-family: 'Bangers', cursive !important;
color: #1F2937 !important;
font-size: 3.5rem !important;
font-weight: 400 !important;
text-align: center !important;
margin-bottom: 0.5rem !important;
text-shadow:
4px 4px 0px #FACC15,
6px 6px 0px #1F2937 !important;
letter-spacing: 3px !important;
-webkit-text-stroke: 2px #1F2937 !important;
}
/* ===== 🎨 μ„œλΈŒνƒ€μ΄ν‹€ ===== */
.subtitle {
text-align: center !important;
font-family: 'Comic Neue', cursive !important;
font-size: 1.2rem !important;
color: #1F2937 !important;
margin-bottom: 1.5rem !important;
font-weight: 700 !important;
}
/* ===== 🎨 Stats μΉ΄λ“œ ===== */
.stats-row {
display: flex !important;
justify-content: center !important;
gap: 1rem !important;
margin: 1.5rem 0 !important;
flex-wrap: wrap !important;
}
.stat-card {
background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%) !important;
border: 3px solid #1F2937 !important;
border-radius: 12px !important;
padding: 1rem 1.5rem !important;
text-align: center !important;
box-shadow: 4px 4px 0px #1F2937 !important;
min-width: 120px !important;
}
.stat-card .emoji {
font-size: 2rem !important;
display: block !important;
margin-bottom: 0.3rem !important;
}
.stat-card .label {
color: #FFFFFF !important;
font-family: 'Comic Neue', cursive !important;
font-weight: 700 !important;
font-size: 0.9rem !important;
}
/* ===== 🎨 μΉ΄λ“œ/νŒ¨λ„ - λ§Œν™” ν”„λ ˆμž„ μŠ€νƒ€μΌ ===== */
.gr-panel,
.gr-box,
.gr-form,
.block,
.gr-group {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 6px 6px 0px #1F2937 !important;
transition: all 0.2s ease !important;
}
.gr-panel:hover,
.block:hover {
transform: translate(-2px, -2px) !important;
box-shadow: 8px 8px 0px #1F2937 !important;
}
/* ===== 🎨 νƒ­ μŠ€νƒ€μΌ ===== */
.gr-tabs {
border: 3px solid #1F2937 !important;
border-radius: 12px !important;
overflow: hidden !important;
box-shadow: 6px 6px 0px #1F2937 !important;
}
.gr-tab-nav {
background: #FACC15 !important;
border-bottom: 3px solid #1F2937 !important;
}
.gr-tab-nav button {
font-family: 'Bangers', cursive !important;
font-size: 1.2rem !important;
letter-spacing: 1px !important;
color: #1F2937 !important;
padding: 12px 24px !important;
border: none !important;
background: transparent !important;
transition: all 0.2s ease !important;
}
.gr-tab-nav button:hover {
background: #FDE68A !important;
}
.gr-tab-nav button.selected {
background: #3B82F6 !important;
color: #FFFFFF !important;
text-shadow: 1px 1px 0px #1F2937 !important;
}
/* ===== 🎨 μž…λ ₯ ν•„λ“œ ===== */
textarea,
input[type="text"],
input[type="number"] {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
color: #1F2937 !important;
font-family: 'Comic Neue', cursive !important;
font-size: 1rem !important;
font-weight: 700 !important;
transition: all 0.2s ease !important;
}
textarea:focus,
input[type="text"]:focus,
input[type="number"]:focus {
border-color: #3B82F6 !important;
box-shadow: 4px 4px 0px #3B82F6 !important;
outline: none !important;
}
/* ===== 🎨 λ“œλ‘­λ‹€μš΄ μŠ€νƒ€μΌ ===== */
[data-testid="dropdown"] {
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 3px 3px 0px #1F2937 !important;
}
[data-testid="dropdown"] .wrap {
background: #FFFFFF !important;
}
[data-testid="dropdown"] .wrap-inner {
background: #FFFFFF !important;
}
[data-testid="dropdown"] .secondary-wrap {
background: #FFFFFF !important;
}
[data-testid="dropdown"] input {
color: #1F2937 !important;
font-family: 'Comic Neue', cursive !important;
font-weight: 700 !important;
}
[data-testid="dropdown"] .options {
background: #FFFFFF !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 4px 4px 0px #1F2937 !important;
}
[data-testid="dropdown"] .item {
color: #1F2937 !important;
font-family: 'Comic Neue', cursive !important;
font-weight: 700 !important;
}
[data-testid="dropdown"] .item:hover {
background: #FACC15 !important;
}
[data-testid="dropdown"] .item.selected {
background: #3B82F6 !important;
color: #FFFFFF !important;
}
/* ===== 🎨 Primary λ²„νŠΌ ===== */
.gr-button-primary,
button.primary,
.gr-button.primary,
.process-btn {
background: #3B82F6 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
color: #FFFFFF !important;
font-family: 'Bangers', cursive !important;
font-weight: 400 !important;
font-size: 1.3rem !important;
letter-spacing: 2px !important;
padding: 14px 28px !important;
box-shadow: 5px 5px 0px #1F2937 !important;
transition: all 0.1s ease !important;
text-shadow: 1px 1px 0px #1F2937 !important;
}
.gr-button-primary:hover,
button.primary:hover,
.gr-button.primary:hover,
.process-btn:hover {
background: #2563EB !important;
transform: translate(-2px, -2px) !important;
box-shadow: 7px 7px 0px #1F2937 !important;
}
.gr-button-primary:active,
button.primary:active,
.gr-button.primary:active,
.process-btn:active {
transform: translate(3px, 3px) !important;
box-shadow: 2px 2px 0px #1F2937 !important;
}
/* ===== 🎨 둜그 좜λ ₯ μ˜μ—­ ===== */
.info-log textarea {
background: #1F2937 !important;
color: #10B981 !important;
font-family: 'Courier New', monospace !important;
font-size: 0.9rem !important;
font-weight: 400 !important;
border: 3px solid #10B981 !important;
border-radius: 8px !important;
box-shadow: 4px 4px 0px #10B981 !important;
}
/* ===== 🎨 이미지/λΉ„λ””μ˜€ μ˜μ—­ ===== */
.gr-image,
.gr-video,
.image-container,
.video-container {
border: 4px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 8px 8px 0px #1F2937 !important;
overflow: hidden !important;
background: #FFFFFF !important;
}
/* ===== 🎨 μŠ¬λΌμ΄λ” μŠ€νƒ€μΌ ===== */
input[type="range"] {
accent-color: #3B82F6 !important;
}
.gr-slider {
background: #FFFFFF !important;
}
/* ===== 🎨 μ•„μ½”λ””μ–Έ ===== */
.gr-accordion {
background: #FACC15 !important;
border: 3px solid #1F2937 !important;
border-radius: 8px !important;
box-shadow: 4px 4px 0px #1F2937 !important;
}
.gr-accordion-header {
color: #1F2937 !important;
font-family: 'Comic Neue', cursive !important;
font-weight: 700 !important;
font-size: 1.1rem !important;
}
/* ===== 🎨 라벨 μŠ€νƒ€μΌ ===== */
label,
.gr-input-label,
.gr-block-label {
color: #1F2937 !important;
font-family: 'Comic Neue', cursive !important;
font-weight: 700 !important;
font-size: 1rem !important;
}
/* ===== 🎨 ν”„λ‘œκ·Έλ ˆμŠ€ λ°” ===== */
.progress-bar,
.gr-progress-bar {
background: #3B82F6 !important;
border: 2px solid #1F2937 !important;
border-radius: 4px !important;
}
/* ===== 🎨 μŠ€ν¬λ‘€λ°” ===== */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: #FEF9C3;
border: 2px solid #1F2937;
}
::-webkit-scrollbar-thumb {
background: #3B82F6;
border: 2px solid #1F2937;
border-radius: 0px;
}
::-webkit-scrollbar-thumb:hover {
background: #EF4444;
}
/* ===== 🎨 선택 ν•˜μ΄λΌμ΄νŠΈ ===== */
::selection {
background: #FACC15;
color: #1F2937;
}
/* ===== 🎨 링크 μŠ€νƒ€μΌ ===== */
a {
color: #3B82F6 !important;
text-decoration: none !important;
font-weight: 700 !important;
}
a:hover {
color: #EF4444 !important;
}
/* ===== 🎨 Row/Column 간격 ===== */
.gr-row {
gap: 1.5rem !important;
}
.gr-column {
gap: 1rem !important;
}
/* ===== λ°˜μ‘ν˜• μ‘°μ • ===== */
@media (max-width: 768px) {
.header-text h1 {
font-size: 2.2rem !important;
text-shadow:
3px 3px 0px #FACC15,
4px 4px 0px #1F2937 !important;
}
.gr-button-primary,
button.primary {
padding: 12px 20px !important;
font-size: 1.1rem !important;
}
.gr-panel,
.block {
box-shadow: 4px 4px 0px #1F2937 !important;
}
.stat-card {
min-width: 100px !important;
padding: 0.8rem 1rem !important;
}
}
/* ===== 🎨 닀크λͺ¨λ“œ λΉ„ν™œμ„±ν™” ===== */
@media (prefers-color-scheme: dark) {
.gradio-container {
background-color: #FEF9C3 !important;
}
}
"""
# ============================================
# Build the Gradio Interface
# ============================================
with gr.Blocks(fill_height=True, title="Ansim Blur - Face Privacy Protection") as demo:
# HOME Badge
gr.HTML("""
<div style="text-align: center; margin: 20px 0 10px 0;">
<a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
<img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
</a>
<a href="https://discord.gg/openfreeai" target="_blank" style="text-decoration: none; margin-left: 10px;">
<img src="https://img.shields.io/static/v1?label=Discord&message=OpenFree%20AI&color=5865F2&labelColor=1F2937&logo=discord&logoColor=white&style=for-the-badge" alt="Discord">
</a>
</div>
""")
# Header Title
gr.Markdown(
"""
# πŸ”’ ANSIM BLUR - FACE PRIVACY πŸ›‘οΈ
""",
elem_classes="header-text"
)
gr.Markdown(
"""
<p class="subtitle">🎭 Advanced AI-Powered Face Detection & Privacy Protection! ✨</p>
""",
)
# Stats Cards
gr.HTML("""
<div class="stats-row">
<div class="stat-card">
<span class="emoji">πŸ–ΌοΈ</span>
<span class="label">Image Support</span>
</div>
<div class="stat-card">
<span class="emoji">πŸŽ₯</span>
<span class="label">Video Processing</span>
</div>
<div class="stat-card">
<span class="emoji">⚑</span>
<span class="label">Real-time AI</span>
</div>
<div class="stat-card">
<span class="emoji">πŸ›‘οΈ</span>
<span class="label">Privacy First</span>
</div>
</div>
""")
# Device Info
gr.Markdown(f"""
<p style="text-align: center; font-family: 'Comic Neue', cursive; font-weight: 700; color: #1F2937; margin: 1rem 0;">
πŸ–₯️ Running on: <span style="color: #3B82F6;">{device.upper()}</span>
</p>
""")
# Main Tabs
with gr.Tabs():
# ===== IMAGE TAB =====
with gr.Tab("πŸ“Έ Image Processing"):
with gr.Row(equal_height=False):
# Left Column - Input & Settings
with gr.Column(scale=1, min_width=400):
input_image = gr.Image(
label="πŸ–ΌοΈ Upload Image",
type="pil",
height=350
)
with gr.Accordion("βš™οΈ Detection Settings", open=True):
conf_img = gr.Slider(
minimum=0.05,
maximum=0.9,
value=0.25,
step=0.01,
label="🎯 Confidence Threshold"
)
iou_img = gr.Slider(
minimum=0.1,
maximum=0.9,
value=0.45,
step=0.01,
label="πŸ“ NMS IoU"
)
expand_img = gr.Slider(
minimum=0.0,
maximum=0.5,
value=0.05,
step=0.01,
label="πŸ”² Box Expansion"
)
with gr.Accordion("🎨 Blur Settings", open=True):
mode_img = gr.Dropdown(
choices=["Gaussian Blur", "Mosaic Effect"],
value="Gaussian Blur",
label="πŸ–ŒοΈ Style"
)
blur_intensity_img = gr.Slider(
minimum=15,
maximum=151,
value=51,
step=2,
label="πŸ’¨ Blur Intensity"
)
mosaic_size_img = gr.Slider(
minimum=5,
maximum=40,
value=15,
step=1,
label="🧩 Mosaic Size"
)
process_img_btn = gr.Button(
"πŸ” PROCESS IMAGE! 🎭",
variant="primary",
size="lg",
elem_classes="process-btn"
)
# Right Column - Output
with gr.Column(scale=1, min_width=400):
output_image = gr.Image(
label="πŸ–ΌοΈ Processed Result",
type="pil",
height=350
)
with gr.Accordion("πŸ“œ Processing Log", open=True):
info_log_img = gr.Textbox(
label="",
placeholder="Upload an image and click process...",
lines=12,
max_lines=18,
interactive=False,
elem_classes="info-log"
)
# ===== VIDEO TAB =====
with gr.Tab("🎬 Video Processing"):
with gr.Row(equal_height=False):
# Left Column - Input & Settings
with gr.Column(scale=1, min_width=400):
input_video = gr.Video(
label="πŸŽ₯ Upload Video",
height=350
)
with gr.Accordion("βš™οΈ Detection Settings", open=True):
conf_vid = gr.Slider(
minimum=0.05,
maximum=0.9,
value=0.25,
step=0.01,
label="🎯 Confidence Threshold"
)
iou_vid = gr.Slider(
minimum=0.1,
maximum=0.9,
value=0.45,
step=0.01,
label="πŸ“ NMS IoU"
)
expand_vid = gr.Slider(
minimum=0.0,
maximum=0.5,
value=0.05,
step=0.01,
label="πŸ”² Box Expansion"
)
with gr.Accordion("🎨 Blur Settings", open=True):
mode_vid = gr.Dropdown(
choices=["Gaussian Blur", "Mosaic Effect"],
value="Gaussian Blur",
label="πŸ–ŒοΈ Style"
)
blur_intensity_vid = gr.Slider(
minimum=15,
maximum=151,
value=51,
step=2,
label="πŸ’¨ Blur Intensity"
)
mosaic_size_vid = gr.Slider(
minimum=5,
maximum=40,
value=15,
step=1,
label="🧩 Mosaic Size"
)
process_vid_btn = gr.Button(
"🎬 PROCESS VIDEO! πŸ›‘οΈ",
variant="primary",
size="lg",
elem_classes="process-btn"
)
# Right Column - Output
with gr.Column(scale=1, min_width=400):
output_video = gr.Video(
label="πŸŽ₯ Processed Result",
height=350
)
with gr.Accordion("πŸ“œ Processing Log", open=True):
info_log_vid = gr.Textbox(
label="",
placeholder="Upload a video and click process...",
lines=12,
max_lines=18,
interactive=False,
elem_classes="info-log"
)
# Instructions
gr.Markdown(
"""
<div style="background: linear-gradient(135deg, #EFF6FF 0%, #DBEAFE 100%); border: 3px solid #3B82F6; border-radius: 12px; padding: 1.5rem; box-shadow: 6px 6px 0px #1F2937; margin-top: 2rem;">
<h3 style="font-family: 'Bangers', cursive; color: #1F2937; font-size: 1.3rem; margin-bottom: 0.5rem;">πŸ“ HOW TO USE</h3>
<ol style="font-family: 'Comic Neue', cursive; color: #1F2937; font-weight: 700;">
<li>Upload an image or video containing faces</li>
<li>Adjust detection settings (confidence, IoU, expansion)</li>
<li>Choose blur style (Gaussian or Mosaic)</li>
<li>Click the Process button and wait for results</li>
<li>Download your privacy-protected media!</li>
</ol>
</div>
<div style="background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%); border: 3px solid #F59E0B; border-radius: 12px; padding: 1.5rem; box-shadow: 6px 6px 0px #1F2937; margin-top: 1rem;">
<h3 style="font-family: 'Bangers', cursive; color: #1F2937; font-size: 1.3rem; margin-bottom: 0.5rem;">πŸ’‘ TIPS</h3>
<ul style="font-family: 'Comic Neue', cursive; color: #1F2937; font-weight: 700;">
<li>Lower confidence = more faces detected (may include false positives)</li>
<li>Higher blur intensity = stronger privacy protection</li>
<li>Mosaic effect works better for artistic results</li>
<li>Video processing may take time depending on length</li>
</ul>
</div>
"""
)
# Event Handlers
process_img_btn.click(
fn=process_image,
inputs=[
input_image,
conf_img,
iou_img,
expand_img,
mode_img,
blur_intensity_img,
mosaic_size_img
],
outputs=[output_image, info_log_img]
)
process_vid_btn.click(
fn=process_video,
inputs=[
input_video,
conf_vid,
iou_vid,
expand_vid,
mode_vid,
blur_intensity_vid,
mosaic_size_vid
],
outputs=[output_video, info_log_vid]
)
if __name__ == "__main__":
demo.launch(css=css, ssr_mode=False)