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("""
""") # 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("""π₯οΈ 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( """