ab2207 commited on
Commit
7d22b09
Β·
verified Β·
1 Parent(s): f878059

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +308 -127
app.py CHANGED
@@ -4,11 +4,12 @@ Face Privacy Tool (Gradio UI with YOLOv8 Segmentation & Video Support)
4
 
5
  # --- Standard Libraries ---
6
  import logging
7
- from abc import ABC, abstractmethod
8
- from dataclasses import dataclass, field
9
- from typing import Any, Dict, List, Tuple
10
  import tempfile
11
  import os
 
 
 
12
 
13
  # --- Computer Vision & UI Libraries ---
14
  import cv2
@@ -20,14 +21,38 @@ from ultralytics import YOLO
20
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
21
  logger = logging.getLogger(__name__)
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  # ====================================================
24
  # CONFIGURATION DATA CLASSES
25
  # ====================================================
26
  @dataclass
27
  class BlurConfig:
28
- type: str = "gaussian"
29
- intensity: float = 1.5
30
- pixel_size: int = 30
 
31
  solid_color: Tuple[int, int, int] = (0, 0, 0)
32
  adaptive_blur: bool = True
33
  min_kernel: int = 15
@@ -35,11 +60,13 @@ class BlurConfig:
35
 
36
  @dataclass
37
  class DetectionConfig:
38
- min_confidence: float = 0.4
39
- model_path: str = "yolov8n-seg.pt" # YOLOv8 segmentation model
 
40
 
41
  @dataclass
42
  class AppConfig:
 
43
  blur: BlurConfig = field(default_factory=BlurConfig)
44
  detection: DetectionConfig = field(default_factory=DetectionConfig)
45
  scaling_factor: float = 1.2
@@ -47,91 +74,122 @@ class AppConfig:
47
  face_margin: int = 15
48
 
49
  # ====================================================
50
- # BLUR EFFECTS
51
  # ====================================================
52
  class BlurEffect(ABC):
 
53
  def __init__(self, config: BlurConfig):
54
  self.config = config
55
 
56
  @abstractmethod
57
  def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
 
58
  pass
59
 
60
  class GaussianBlur(BlurEffect):
 
61
  def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
62
  x, y, w, h = roi
63
  face_roi = image[y:y+h, x:x+w]
64
- if face_roi.size == 0:
65
- return image
66
  if self.config.adaptive_blur:
67
  min_dim = min(w, h)
68
- kernel_val = int(min_dim * 0.25 * self.config.intensity)
 
69
  kernel_val = max(self.config.min_kernel, min(kernel_val, self.config.max_kernel))
70
  else:
71
- kernel_val = 45
72
- kernel_val = kernel_val | 1
 
73
  blurred_roi = cv2.GaussianBlur(face_roi, (kernel_val, kernel_val), 0)
74
  image[y:y+h, x:x+w] = blurred_roi
75
  return image
76
 
77
  class PixelateBlur(BlurEffect):
 
78
  def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
79
  x, y, w, h = roi
80
  face_roi = image[y:y+h, x:x+w]
81
- if face_roi.size == 0:
82
- return image
83
- h_roi, w_roi, _ = face_roi.shape
84
  pixel_size = self.config.pixel_size
85
- if pixel_size <= 0:
86
- return image
87
  small = cv2.resize(face_roi, (max(1, w_roi // pixel_size), max(1, h_roi // pixel_size)), interpolation=cv2.INTER_LINEAR)
88
  pixelated = cv2.resize(small, (w_roi, h_roi), interpolation=cv2.INTER_NEAREST)
89
  image[y:y+h, x:x+w] = pixelated
90
  return image
91
 
92
  class SolidColorBlur(BlurEffect):
 
93
  def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
94
  x, y, w, h = roi
95
  cv2.rectangle(image, (x, y), (x+w, y+h), self.config.solid_color, -1)
96
  return image
97
 
98
  def get_blur_effect(config: BlurConfig) -> BlurEffect:
99
- if config.type == "gaussian":
100
- return GaussianBlur(config)
101
- if config.type == "pixelate":
102
- return PixelateBlur(config)
103
- if config.type == "solid":
104
- return SolidColorBlur(config)
105
- raise ValueError(f"Unknown blur type: {config.type}")
106
 
107
  # ====================================================
108
- # YOLOv8 DETECTOR
109
  # ====================================================
110
- class YOLOv8Detector:
 
111
  def __init__(self, config: DetectionConfig):
112
- self.model = YOLO(config.model_path)
113
- self.min_conf = config.min_confidence
114
-
115
- def detect_faces(self, image: np.ndarray) -> List[Dict[str, Any]]:
116
- """Detect faces/objects with YOLOv8 segmentation"""
117
- results = self.model(image, conf=self.min_conf)
 
 
 
 
 
 
118
  faces = []
 
 
119
  for r in results:
 
120
  for box in r.boxes:
121
- x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
122
- faces.append({"x": x1, "y": y1, "width": x2 - x1, "height": y2 - y1})
123
- return faces
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  # ====================================================
126
- # MAIN APPLICATION
127
  # ====================================================
128
  class FacePrivacyApp:
129
- def __init__(self, config: AppConfig):
130
  self.config = config
131
  self.blur_effect = get_blur_effect(config.blur)
132
- self.detector = YOLOv8Detector(config.detection)
133
 
134
  def _expand_bbox(self, bbox: Dict[str, Any], img_shape: Tuple[int, int]) -> Tuple[int, int, int, int]:
 
135
  h_img, w_img = img_shape
136
  new_w = int(bbox["width"] * self.config.scaling_factor)
137
  new_h = int(bbox["height"] * self.config.scaling_factor)
@@ -143,113 +201,236 @@ class FacePrivacyApp:
143
  h = min(h_img - y, new_h + self.config.forehead_margin)
144
  return x, y, w, h
145
 
146
- def process_image(self, image: np.ndarray) -> np.ndarray:
 
147
  writable_image = image.copy()
148
- faces = self.detector.detect_faces(writable_image)
149
  for face in faces:
150
  expanded_roi = self._expand_bbox(face, writable_image.shape[:2])
151
  writable_image = self.blur_effect.apply(writable_image, expanded_roi)
152
  return writable_image
153
 
154
  # ====================================================
155
- # VIDEO PROCESSING FUNCTION
156
  # ====================================================
157
- def process_video_fn(video_file, blur_type, blur_amount, blur_size):
158
- if video_file is None:
159
- return None
160
-
161
  app_config = AppConfig(
162
  scaling_factor=blur_size,
163
  blur=BlurConfig(type=blur_type, intensity=blur_amount, pixel_size=int(blur_amount))
164
  )
165
- app = FacePrivacyApp(app_config)
166
-
167
- cap = cv2.VideoCapture(video_file.name)
168
- if not cap.isOpened():
169
- logger.warning(f"Cannot open video: {video_file.name}")
170
- return None
171
-
172
- out_fd, out_path = tempfile.mkstemp(suffix=".mp4")
173
- os.close(out_fd)
174
-
175
- fourcc = cv2.VideoWriter_fourcc(*'mp4v')
176
- fps = cap.get(cv2.CAP_PROP_FPS)
177
- width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
178
- height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
179
- out_vid = cv2.VideoWriter(out_path, fourcc, fps, (width, height))
180
-
181
- while True:
182
- ret, frame = cap.read()
183
- if not ret:
184
- break
185
- processed_frame = app.process_image(frame)
186
- out_vid.write(processed_frame)
187
-
188
- cap.release()
189
- out_vid.release()
190
- return out_path
191
-
192
- # ====================================================
193
- # GRADIO INTERFACE FUNCTIONS
194
- # ====================================================
195
- def process_single_image_fn(media, blur_type, blur_amount, blur_size):
196
- if media is None:
197
- return None
198
- app_config = AppConfig(
199
- scaling_factor=blur_size,
200
- blur=BlurConfig(type=blur_type, intensity=blur_amount, pixel_size=int(blur_amount))
201
- )
202
- app = FacePrivacyApp(app_config)
203
- return app.process_image(media)
204
-
205
- def update_amount_slider_visibility(blur_type):
206
- return gr.update(visible=(blur_type != "solid"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  # ====================================================
209
- # BUILD GRADIO APP
210
  # ====================================================
211
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
212
- gr.Markdown("# Face Privacy Tool (YOLOv8 Segmentation & Video)")
 
213
 
214
  with gr.Row():
215
  with gr.Column(scale=1):
216
- gr.Markdown("### βš™οΈ Settings")
217
- blur_type = gr.Radio(["gaussian", "pixelate", "solid"], value="pixelate", label="Blur Type")
218
- blur_amount = gr.Slider(1, 50, step=1, value=25, label="Blur Amount")
219
- blur_size = gr.Slider(1.0, 2.0, step=0.05, value=1.2, label="Blur Size (Expansion)")
 
 
 
220
 
221
  with gr.Column(scale=2):
222
  with gr.Tabs():
223
- with gr.TabItem("Single Image"):
224
- image_input = gr.Image(sources=["upload"], type="numpy", label="Upload Image")
225
- image_output = gr.Image(type="numpy", label="Blurred Image")
226
- single_image_button = gr.Button("Apply Blur to Single Image", variant="primary")
227
- single_image_button.click(
228
- fn=process_single_image_fn,
229
- inputs=[image_input, blur_type, blur_amount, blur_size],
230
- outputs=image_output
231
- )
232
-
233
- with gr.TabItem("Video Upload"):
234
- video_input = gr.File(file_types=[".mp4", ".mov", ".avi"], label="Upload Video")
235
- video_output = gr.Video(label="Processed Video")
236
- process_video_button = gr.Button("Process Video", variant="primary")
237
- process_video_button.click(
238
- fn=process_video_fn,
239
- inputs=[video_input, blur_type, blur_amount, blur_size],
240
- outputs=video_output
241
- )
242
-
243
- with gr.TabItem("Webcam"):
244
- webcam_input = gr.Image(sources=["webcam"], type="numpy", streaming=True, label="Live Webcam")
245
- webcam_output = gr.Image(type="numpy", label="Processed Feed")
246
- webcam_input.stream(
247
- fn=process_single_image_fn,
248
- inputs=[webcam_input, blur_type, blur_amount, blur_size],
249
- outputs=webcam_output
250
- )
251
-
252
- blur_type.change(fn=update_amount_slider_visibility, inputs=blur_type, outputs=blur_amount)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
 
 
 
254
  if __name__ == "__main__":
255
- demo.launch()
 
 
 
 
 
 
 
 
4
 
5
  # --- Standard Libraries ---
6
  import logging
7
+ import atexit
 
 
8
  import tempfile
9
  import os
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Tuple, Optional
13
 
14
  # --- Computer Vision & UI Libraries ---
15
  import cv2
 
21
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
22
  logger = logging.getLogger(__name__)
23
 
24
+ # ====================================================
25
+ # TEMPORARY FILE CLEANUP
26
+ # ====================================================
27
+ TEMP_FILES = []
28
+
29
+ def cleanup_temp_files():
30
+ """Clean up any temporary files created during the session on exit."""
31
+ for f in TEMP_FILES:
32
+ try:
33
+ if os.path.exists(f):
34
+ os.remove(f)
35
+ logger.info(f"πŸ—‘οΈ Cleaned up temporary file: {f}")
36
+ except Exception as e:
37
+ logger.warning(f"⚠️ Failed to delete temporary file {f}: {e}")
38
+
39
+ atexit.register(cleanup_temp_files)
40
+
41
+ def create_temp_file(suffix=".mp4") -> str:
42
+ """Creates a temporary file and registers it for cleanup."""
43
+ path = tempfile.mktemp(suffix=suffix)
44
+ TEMP_FILES.append(path)
45
+ return path
46
+
47
  # ====================================================
48
  # CONFIGURATION DATA CLASSES
49
  # ====================================================
50
  @dataclass
51
  class BlurConfig:
52
+ """Configuration for blur effects."""
53
+ type: str = "pixelate"
54
+ intensity: float = 25.0
55
+ pixel_size: int = 25
56
  solid_color: Tuple[int, int, int] = (0, 0, 0)
57
  adaptive_blur: bool = True
58
  min_kernel: int = 15
 
60
 
61
  @dataclass
62
  class DetectionConfig:
63
+ """Configuration for the face detector."""
64
+ min_confidence: float = 0.5
65
+ model_path: str = "yolov8n-face.pt"
66
 
67
  @dataclass
68
  class AppConfig:
69
+ """Main application configuration."""
70
  blur: BlurConfig = field(default_factory=BlurConfig)
71
  detection: DetectionConfig = field(default_factory=DetectionConfig)
72
  scaling_factor: float = 1.2
 
74
  face_margin: int = 15
75
 
76
  # ====================================================
77
+ # BLUR EFFECTS (STRATEGY PATTERN)
78
  # ====================================================
79
  class BlurEffect(ABC):
80
+ """Abstract base class for blur effects."""
81
  def __init__(self, config: BlurConfig):
82
  self.config = config
83
 
84
  @abstractmethod
85
  def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
86
+ """Apply the blur effect to the region of interest (ROI)."""
87
  pass
88
 
89
  class GaussianBlur(BlurEffect):
90
+ """Gaussian blur with adaptive kernel sizing for a natural look."""
91
  def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
92
  x, y, w, h = roi
93
  face_roi = image[y:y+h, x:x+w]
94
+ if face_roi.size == 0: return image
95
+
96
  if self.config.adaptive_blur:
97
  min_dim = min(w, h)
98
+ # Intensity now directly maps to kernel size percentage
99
+ kernel_val = int(min_dim * (self.config.intensity / 100.0))
100
  kernel_val = max(self.config.min_kernel, min(kernel_val, self.config.max_kernel))
101
  else:
102
+ kernel_val = int(self.config.intensity)
103
+
104
+ kernel_val = kernel_val | 1 # Ensure kernel size is odd
105
  blurred_roi = cv2.GaussianBlur(face_roi, (kernel_val, kernel_val), 0)
106
  image[y:y+h, x:x+w] = blurred_roi
107
  return image
108
 
109
  class PixelateBlur(BlurEffect):
110
+ """Pixelation effect for a retro/digital privacy look."""
111
  def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
112
  x, y, w, h = roi
113
  face_roi = image[y:y+h, x:x+w]
114
+ if face_roi.size == 0: return image
115
+
116
+ h_roi, w_roi = face_roi.shape[:2]
117
  pixel_size = self.config.pixel_size
118
+ if pixel_size <= 0: return image
119
+
120
  small = cv2.resize(face_roi, (max(1, w_roi // pixel_size), max(1, h_roi // pixel_size)), interpolation=cv2.INTER_LINEAR)
121
  pixelated = cv2.resize(small, (w_roi, h_roi), interpolation=cv2.INTER_NEAREST)
122
  image[y:y+h, x:x+w] = pixelated
123
  return image
124
 
125
  class SolidColorBlur(BlurEffect):
126
+ """Solid color rectangle overlay for complete redaction."""
127
  def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
128
  x, y, w, h = roi
129
  cv2.rectangle(image, (x, y), (x+w, y+h), self.config.solid_color, -1)
130
  return image
131
 
132
  def get_blur_effect(config: BlurConfig) -> BlurEffect:
133
+ """Factory function to create a blur effect instance."""
134
+ blur_effects = {"gaussian": GaussianBlur, "pixelate": PixelateBlur, "solid": SolidColorBlur}
135
+ blur_class = blur_effects.get(config.type)
136
+ if not blur_class: raise ValueError(f"Unknown blur type: {config.type}")
137
+ return blur_class(config)
 
 
138
 
139
  # ====================================================
140
+ # YOLOv8 FACE DETECTOR (SINGLETON)
141
  # ====================================================
142
+ class YOLOv8FaceDetector:
143
+ """Unified face detector using YOLOv8-Face model."""
144
  def __init__(self, config: DetectionConfig):
145
+ try:
146
+ logger.info(f"Attempting to load YOLOv8-Face model: {config.model_path}")
147
+ self.model = YOLO(config.model_path)
148
+ self.min_conf = config.min_confidence
149
+ logger.info("βœ… Model loaded successfully.")
150
+ except Exception as e:
151
+ logger.error(f"❌ Failed to load model: {e}")
152
+ raise RuntimeError(f"Model loading failed. Ensure '{config.model_path}' is available.") from e
153
+
154
+ def detect_faces(self, image: np.ndarray, conf_threshold: float, return_annotated: bool = False) -> Tuple[List[Dict[str, Any]], Optional[np.ndarray]]:
155
+ """Detects faces in an image with a given confidence threshold."""
156
+ results = self.model(image, conf=conf_threshold, verbose=False)
157
  faces = []
158
+ annotated_image = image.copy() if return_annotated else None
159
+
160
  for r in results:
161
+ if r.boxes is None: continue
162
  for box in r.boxes:
163
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
164
+ confidence = float(box.conf[0])
165
+ faces.append({"x": x1, "y": y1, "width": x2 - x1, "height": y2 - y1, "confidence": confidence})
166
+ if return_annotated:
167
+ cv2.rectangle(annotated_image, (x1, y1), (x2, y2), (0, 255, 0), 3)
168
+ label = f"Face: {confidence:.2%}"
169
+ (w, h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
170
+ cv2.rectangle(annotated_image, (x1, y1 - h - 10), (x1 + w, y1), (0, 255, 0), -1)
171
+ cv2.putText(annotated_image, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
172
+ return faces, annotated_image
173
+
174
+ GLOBAL_DETECTOR: Optional[YOLOv8FaceDetector] = None
175
+ def get_global_detector() -> YOLOv8FaceDetector:
176
+ """Initializes and returns the global singleton detector instance."""
177
+ global GLOBAL_DETECTOR
178
+ if GLOBAL_DETECTOR is None:
179
+ GLOBAL_DETECTOR = YOLOv8FaceDetector(DetectionConfig())
180
+ return GLOBAL_DETECTOR
181
 
182
  # ====================================================
183
+ # CORE APPLICATION LOGIC
184
  # ====================================================
185
  class FacePrivacyApp:
186
+ def __init__(self, config: AppConfig, detector: YOLOv8FaceDetector):
187
  self.config = config
188
  self.blur_effect = get_blur_effect(config.blur)
189
+ self.detector = detector
190
 
191
  def _expand_bbox(self, bbox: Dict[str, Any], img_shape: Tuple[int, int]) -> Tuple[int, int, int, int]:
192
+ """Expands a bounding box to include margins for better coverage."""
193
  h_img, w_img = img_shape
194
  new_w = int(bbox["width"] * self.config.scaling_factor)
195
  new_h = int(bbox["height"] * self.config.scaling_factor)
 
201
  h = min(h_img - y, new_h + self.config.forehead_margin)
202
  return x, y, w, h
203
 
204
+ def process_image(self, image: np.ndarray, conf_threshold: float) -> np.ndarray:
205
+ """Applies blur to all detected faces in an image."""
206
  writable_image = image.copy()
207
+ faces, _ = self.detector.detect_faces(writable_image, conf_threshold, return_annotated=False)
208
  for face in faces:
209
  expanded_roi = self._expand_bbox(face, writable_image.shape[:2])
210
  writable_image = self.blur_effect.apply(writable_image, expanded_roi)
211
  return writable_image
212
 
213
  # ====================================================
214
+ # GRADIO HANDLER FUNCTIONS
215
  # ====================================================
216
+ def get_app_instance(blur_type: str, blur_amount: float, blur_size: float) -> FacePrivacyApp:
217
+ """Creates a FacePrivacyApp instance from UI settings."""
218
+ detector = get_global_detector()
 
219
  app_config = AppConfig(
220
  scaling_factor=blur_size,
221
  blur=BlurConfig(type=blur_type, intensity=blur_amount, pixel_size=int(blur_amount))
222
  )
223
+ return FacePrivacyApp(app_config, detector)
224
+
225
+ def process_media(media, blur_type, blur_amount, blur_size, confidence):
226
+ if media is None: return None
227
+ try:
228
+ app = get_app_instance(blur_type, blur_amount, blur_size)
229
+ return app.process_image(media, confidence)
230
+ except Exception as e:
231
+ logger.error(f"Image processing error: {e}")
232
+ gr.Warning(f"An error occurred: {e}")
233
+ return media
234
+
235
+ def process_video(video_file, blur_type, blur_amount, blur_size, confidence, progress=gr.Progress()):
236
+ if video_file is None: return None, "⚠️ No video provided."
237
+ try:
238
+ app = get_app_instance(blur_type, blur_amount, blur_size)
239
+ cap = cv2.VideoCapture(video_file.name)
240
+ if not cap.isOpened(): return None, "❌ Cannot open video file."
241
+
242
+ out_path = create_temp_file()
243
+ fourcc = cv2.VideoWriter_fourcc(*'avc1') # H.264 for browser compatibility
244
+ fps, w, h = cap.get(cv2.CAP_PROP_FPS), int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
245
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
246
+ out_vid = cv2.VideoWriter(out_path, fourcc, fps, (w, h))
247
+ if not out_vid.isOpened():
248
+ logger.warning("H.264 codec failed, falling back to mp4v.")
249
+ out_vid = cv2.VideoWriter(out_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
250
+
251
+ frame_num = 0
252
+ while cap.isOpened():
253
+ ret, frame = cap.read()
254
+ if not ret: break
255
+ frame_num += 1
256
+ progress(frame_num / max(total_frames, 1), desc=f"Processing frame {frame_num}/{total_frames}")
257
+ processed_frame = app.process_image(frame, confidence)
258
+ out_vid.write(processed_frame)
259
+ cap.release()
260
+ out_vid.release()
261
+ return out_path, f"βœ… Processed {frame_num} frames."
262
+ except Exception as e:
263
+ logger.error(f"Video processing error: {e}")
264
+ gr.Error(f"Video processing failed: {e}")
265
+ return None, f"❌ Error: {e}"
266
+
267
+ def detect_faces_image(image, confidence):
268
+ if image is None: return None, "⚠️ No image provided."
269
+ try:
270
+ detector = get_global_detector()
271
+ faces, annotated_image = detector.detect_faces(image, confidence, return_annotated=True)
272
+ if faces:
273
+ result = f"βœ… **{len(faces)} face(s) detected!**\n\n" + "\n".join([f"- Face {i+1}: Confidence {f['confidence']:.2%}" for i, f in enumerate(faces)])
274
+ else:
275
+ result = "❌ **No faces detected.**"
276
+ return annotated_image, result
277
+ except Exception as e:
278
+ logger.error(f"Image detection error: {e}")
279
+ gr.Warning(f"An error occurred: {e}")
280
+ return image, f"❌ Error: {e}"
281
+
282
+ def detect_faces_video(video_file, confidence, progress=gr.Progress()):
283
+ if video_file is None: return None, "⚠️ No video provided."
284
+ try:
285
+ detector = get_global_detector()
286
+ cap = cv2.VideoCapture(video_file.name)
287
+ if not cap.isOpened(): return None, "❌ Cannot open video file."
288
+
289
+ out_path = create_temp_file()
290
+ fourcc = cv2.VideoWriter_fourcc(*'avc1')
291
+ fps, w, h = cap.get(cv2.CAP_PROP_FPS), int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
292
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
293
+ out_vid = cv2.VideoWriter(out_path, fourcc, fps, (w,h))
294
+ if not out_vid.isOpened(): out_vid = cv2.VideoWriter(out_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w,h))
295
+
296
+ frame_num, frames_with_faces, all_confidences = 0, 0, []
297
+ while cap.isOpened():
298
+ ret, frame = cap.read()
299
+ if not ret: break
300
+ frame_num += 1
301
+ progress(frame_num / max(total_frames, 1), desc=f"Analyzing frame {frame_num}/{total_frames}")
302
+ faces, annotated_frame = detector.detect_faces(frame, confidence, return_annotated=True)
303
+ if faces:
304
+ frames_with_faces += 1
305
+ all_confidences.extend([f["confidence"] for f in faces])
306
+ out_vid.write(annotated_frame)
307
+ cap.release()
308
+ out_vid.release()
309
+
310
+ if frames_with_faces > 0:
311
+ avg_conf = sum(all_confidences) / len(all_confidences)
312
+ result = (f"βœ… **Faces detected in {frames_with_faces}/{frame_num} frames!**\n"
313
+ f"πŸ“ˆ Average Confidence: **{avg_conf:.2%}**\n"
314
+ f"🎯 Max Confidence: **{max(all_confidences):.2%}**")
315
+ else:
316
+ result = f"❌ **No faces detected in {frame_num} frames.**"
317
+ return out_path, result
318
+ except Exception as e:
319
+ logger.error(f"Video detection error: {e}")
320
+ gr.Error(f"Video detection failed: {e}")
321
+ return None, f"❌ Error: {e}"
322
+
323
+ def detect_faces_webcam(image, confidence):
324
+ if image is None: return None
325
+ try:
326
+ detector = get_global_detector()
327
+ _, annotated_image = detector.detect_faces(image, confidence, return_annotated=True)
328
+ return annotated_image
329
+ except Exception as e:
330
+ logger.error(f"Webcam detection error: {e}")
331
+ return image
332
 
333
  # ====================================================
334
+ # GRADIO UI
335
  # ====================================================
336
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), title="Face Privacy Tool") as demo:
337
+ gr.Markdown("# πŸ” Face Privacy Tool")
338
+ gr.Markdown("AI-powered face detection and privacy protection using **YOLOv8-Face**. Obscure faces in images, videos, and live webcam feeds, or use the detection mode to verify their presence.")
339
 
340
  with gr.Row():
341
  with gr.Column(scale=1):
342
+ gr.Markdown("### βš™οΈ Global Settings")
343
+ with gr.Accordion("Privacy Settings", open=True):
344
+ blur_type = gr.Radio(["gaussian", "pixelate", "solid"], value="pixelate", label="Blur Type", info="Choose how to obscure faces.")
345
+ blur_amount = gr.Slider(1, 100, step=1, value=25, label="Blur Intensity/Size", info="Higher = more obscured.")
346
+ blur_size = gr.Slider(1.0, 2.0, step=0.05, value=1.2, label="Coverage Area", info="Expand blur beyond face boundary.")
347
+ with gr.Accordion("Detection Settings", open=True):
348
+ detection_confidence = gr.Slider(0.1, 0.9, step=0.05, value=0.5, label="Detection Threshold", info="Lower = more sensitive, but more false positives.")
349
 
350
  with gr.Column(scale=2):
351
  with gr.Tabs():
352
+ with gr.TabItem("πŸ”’ Privacy Mode (Blur Faces)"):
353
+ gr.Markdown("### Apply privacy protection to your media.")
354
+ with gr.Tabs():
355
+ with gr.TabItem("πŸ“· Image"):
356
+ with gr.Row():
357
+ img_in_blur = gr.Image(sources=["upload", "clipboard"], type="numpy", label="Input Image")
358
+ img_out_blur = gr.Image(type="numpy", label="Protected Image")
359
+ with gr.Row():
360
+ blur_img_btn = gr.Button("Apply Privacy Blur", variant="primary", scale=3)
361
+ gr.ClearButton([img_in_blur, img_out_blur], scale=1)
362
+ gr.Examples(examples=[["./examples/group_photo.jpg"], ["./examples/single_person.jpg"]], inputs=img_in_blur)
363
+
364
+ with gr.TabItem("πŸŽ₯ Video"):
365
+ with gr.Row():
366
+ vid_in_blur = gr.File(file_types=[".mp4", ".mov", ".avi"], label="Input Video")
367
+ with gr.Column():
368
+ vid_out_blur = gr.Video(label="Protected Video")
369
+ vid_status_blur = gr.Markdown("")
370
+ with gr.Row():
371
+ blur_vid_btn = gr.Button("Process Video", variant="primary", scale=3)
372
+ gr.ClearButton([vid_in_blur, vid_out_blur, vid_status_blur], scale=1)
373
+
374
+ with gr.TabItem("πŸ“Ή Webcam"):
375
+ gr.Markdown("**Real-time privacy protection from your webcam feed.**")
376
+ with gr.Row():
377
+ web_in_blur = gr.Image(sources=["webcam"], type="numpy", streaming=True, label="Live Webcam")
378
+ web_out_blur = gr.Image(type="numpy", label="Protected Feed")
379
+
380
+ with gr.TabItem("πŸ” Detection Mode (Check for Faces)"):
381
+ gr.Markdown("### Verify if your media contains human faces.")
382
+ with gr.Tabs():
383
+ with gr.TabItem("πŸ“· Image"):
384
+ with gr.Row():
385
+ img_in_detect = gr.Image(sources=["upload", "clipboard"], type="numpy", label="Input Image")
386
+ with gr.Column():
387
+ img_out_detect = gr.Image(type="numpy", label="Detection Result")
388
+ img_status_detect = gr.Markdown("_Upload an image to start._")
389
+ with gr.Row():
390
+ detect_img_btn = gr.Button("Detect Faces", variant="primary", scale=3)
391
+ gr.ClearButton([img_in_detect, img_out_detect, img_status_detect], scale=1)
392
+ gr.Examples(examples=[["./examples/group_photo.jpg"], ["./examples/no_face.jpg"]], inputs=img_in_detect)
393
+
394
+ with gr.TabItem("πŸŽ₯ Video"):
395
+ with gr.Row():
396
+ vid_in_detect = gr.File(file_types=[".mp4", ".mov", ".avi"], label="Input Video")
397
+ with gr.Column():
398
+ vid_out_detect = gr.Video(label="Annotated Video")
399
+ vid_status_detect = gr.Markdown("_Upload a video to start._")
400
+ with gr.Row():
401
+ detect_vid_btn = gr.Button("Analyze Video for Faces", variant="primary", scale=3)
402
+ gr.ClearButton([vid_in_detect, vid_out_detect, vid_status_detect], scale=1)
403
+
404
+ with gr.TabItem("πŸ“Ή Webcam"):
405
+ gr.Markdown("**Live face detection from your webcam feed.**")
406
+ with gr.Row():
407
+ web_in_detect = gr.Image(sources=["webcam"], type="numpy", streaming=True, label="Live Feed")
408
+ web_out_detect = gr.Image(type="numpy", label="Detection Result")
409
+
410
+ # --- Event Handlers ---
411
+ # Privacy Mode
412
+ blur_img_btn.click(process_media, inputs=[img_in_blur, blur_type, blur_amount, blur_size, detection_confidence], outputs=img_out_blur)
413
+ blur_vid_btn.click(process_video, inputs=[vid_in_blur, blur_type, blur_amount, blur_size, detection_confidence], outputs=[vid_out_blur, vid_status_blur])
414
+ web_in_blur.stream(process_media, inputs=[web_in_blur, blur_type, blur_amount, blur_size, detection_confidence], outputs=web_out_blur)
415
+
416
+ # Detection Mode
417
+ detect_img_btn.click(detect_faces_image, inputs=[img_in_detect, detection_confidence], outputs=[img_out_detect, img_status_detect])
418
+ detect_vid_btn.click(detect_faces_video, inputs=[vid_in_detect, detection_confidence], outputs=[vid_out_detect, vid_status_detect])
419
+ web_in_detect.stream(detect_faces_webcam, inputs=[web_in_detect, detection_confidence], outputs=web_out_detect)
420
+
421
+ # Footer
422
+ gr.Markdown("---")
423
+ gr.Markdown("πŸ€– **Powered by YOLOv8-Face** | Built with Gradio & OpenCV | ⚑ Optimized for performance with a global model singleton.")
424
 
425
+ # ====================================================
426
+ # MAIN ENTRY POINT
427
+ # ====================================================
428
  if __name__ == "__main__":
429
+ logger.info("πŸš€ Initializing Face Privacy Tool...")
430
+ try:
431
+ get_global_detector()
432
+ logger.info("βœ… Systems ready. Launching Gradio interface...")
433
+ demo.launch()
434
+ except Exception as e:
435
+ logger.error(f"❌ Startup failed: {e}")
436
+ logger.info("πŸ’‘ Make sure 'yolov8n-face.pt' is available in the current directory or will be downloaded automatically by ultralytics.")