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("""
HOME Discord
""") # Header Title gr.Markdown( """ # πŸ”’ ANSIM BLUR - FACE PRIVACY πŸ›‘οΈ """, elem_classes="header-text" ) gr.Markdown( """

🎭 Advanced AI-Powered Face Detection & Privacy Protection! ✨

""", ) # Stats Cards gr.HTML("""
πŸ–ΌοΈ Image Support
πŸŽ₯ Video Processing
⚑ Real-time AI
πŸ›‘οΈ Privacy First
""") # Device Info gr.Markdown(f"""

πŸ–₯️ Running on: {device.upper()}

""") # 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( """

πŸ“ HOW TO USE

  1. Upload an image or video containing faces
  2. Adjust detection settings (confidence, IoU, expansion)
  3. Choose blur style (Gaussian or Mosaic)
  4. Click the Process button and wait for results
  5. Download your privacy-protected media!

πŸ’‘ TIPS

""" ) # 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)