Spaces:
Running
Running
| 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) |