ab2207 commited on
Commit
0c0bbf3
Β·
verified Β·
1 Parent(s): ee6efb9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +366 -459
app.py CHANGED
@@ -1,5 +1,6 @@
1
  """
2
- Face Privacy Tool (Gradio UI with YOLOv8 Segmentation & Video Support)
 
3
  """
4
 
5
  # --- Standard Libraries ---
@@ -7,9 +8,12 @@ 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
@@ -17,29 +21,183 @@ import numpy as np
17
  import gradio as gr
18
  from ultralytics import YOLO
19
 
 
 
 
 
 
 
 
 
20
  # --- Configure Logging ---
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
@@ -54,564 +212,313 @@ SENSITIVITY_MAP = {
54
  }
55
 
56
  def get_confidence_from_sensitivity(sensitivity: str) -> float:
57
- """Converts user-friendly sensitivity text to numerical confidence threshold."""
58
  return SENSITIVITY_MAP.get(sensitivity, 0.5)
59
 
60
  # ====================================================
61
- # CONFIGURATION DATA CLASSES
62
  # ====================================================
63
- @dataclass
64
- class BlurConfig:
65
- """Configuration for blur effects."""
66
- type: str = "pixelate"
67
- intensity: float = 25.0
68
- pixel_size: int = 25
69
- solid_color: Tuple[int, int, int] = (0, 0, 0)
70
- adaptive_blur: bool = True
71
- min_kernel: int = 15
72
- max_kernel: int = 95
73
-
74
  @dataclass
75
  class DetectionConfig:
76
- """Configuration for the face detector."""
77
  min_confidence: float = 0.5
78
  model_path: str = "yolov8n-face.pt"
79
 
80
- @dataclass
81
- class AppConfig:
82
- """Main application configuration."""
83
- blur: BlurConfig = field(default_factory=BlurConfig)
84
- detection: DetectionConfig = field(default_factory=DetectionConfig)
85
- scaling_factor: float = 1.2
86
- forehead_margin: int = 20
87
- face_margin: int = 15
88
-
89
- # ====================================================
90
- # BLUR EFFECTS (STRATEGY PATTERN)
91
- # ====================================================
92
- class BlurEffect(ABC):
93
- """Abstract base class for blur effects."""
94
- def __init__(self, config: BlurConfig):
95
- self.config = config
96
-
97
- @abstractmethod
98
- def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
99
- """Apply the blur effect to the region of interest (ROI)."""
100
- pass
101
-
102
- class GaussianBlur(BlurEffect):
103
- """Gaussian blur with adaptive kernel sizing for a natural look."""
104
- def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
105
- x, y, w, h = roi
106
- face_roi = image[y:y+h, x:x+w]
107
- if face_roi.size == 0:
108
- return image
109
-
110
- if self.config.adaptive_blur:
111
- min_dim = min(w, h)
112
- kernel_val = int(min_dim * (self.config.intensity / 100.0))
113
- kernel_val = max(self.config.min_kernel, min(kernel_val, self.config.max_kernel))
114
- else:
115
- kernel_val = int(self.config.intensity)
116
-
117
- kernel_val = kernel_val | 1 # Ensure kernel size is odd
118
- blurred_roi = cv2.GaussianBlur(face_roi, (kernel_val, kernel_val), 0)
119
- image[y:y+h, x:x+w] = blurred_roi
120
- return image
121
-
122
- class PixelateBlur(BlurEffect):
123
- """Pixelation effect for a retro/digital privacy look."""
124
- def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
125
- x, y, w, h = roi
126
- face_roi = image[y:y+h, x:x+w]
127
- if face_roi.size == 0:
128
- return image
129
-
130
- h_roi, w_roi = face_roi.shape[:2]
131
- pixel_size = self.config.pixel_size
132
- if pixel_size <= 0:
133
- return image
134
-
135
- small = cv2.resize(face_roi, (max(1, w_roi // pixel_size), max(1, h_roi // pixel_size)), interpolation=cv2.INTER_LINEAR)
136
- pixelated = cv2.resize(small, (w_roi, h_roi), interpolation=cv2.INTER_NEAREST)
137
- image[y:y+h, x:x+w] = pixelated
138
- return image
139
-
140
- class SolidColorBlur(BlurEffect):
141
- """Solid color rectangle overlay for complete redaction."""
142
- def apply(self, image: np.ndarray, roi: Tuple[int, int, int, int]) -> np.ndarray:
143
- x, y, w, h = roi
144
- cv2.rectangle(image, (x, y), (x+w, y+h), self.config.solid_color, -1)
145
- return image
146
-
147
- def get_blur_effect(config: BlurConfig) -> BlurEffect:
148
- """Factory function to create a blur effect instance."""
149
- blur_effects = {"gaussian": GaussianBlur, "pixelate": PixelateBlur, "solid": SolidColorBlur}
150
- blur_class = blur_effects.get(config.type)
151
- if not blur_class:
152
- raise ValueError(f"Unknown blur type: {config.type}")
153
- return blur_class(config)
154
-
155
  # ====================================================
156
- # YOLOv8 FACE DETECTOR (SINGLETON)
157
  # ====================================================
158
  class YOLOv8FaceDetector:
159
- """Unified face detector using YOLOv8-Face model."""
160
  def __init__(self, config: DetectionConfig):
161
  try:
162
- logger.info(f"Attempting to load YOLOv8-Face model: {config.model_path}")
163
  self.model = YOLO(config.model_path)
164
  self.min_conf = config.min_confidence
165
- logger.info("βœ… Model loaded successfully.")
166
  except Exception as e:
167
- logger.error(f"❌ Failed to load model: {e}")
168
- raise RuntimeError(f"Model loading failed. Ensure '{config.model_path}' is available.") from e
169
-
170
- def detect_faces(self, image: np.ndarray, conf_threshold: float, return_annotated: bool = False) -> Tuple[List[Dict[str, Any]], Optional[np.ndarray]]:
171
- """Detects faces in an image with a given confidence threshold."""
 
 
 
 
172
  results = self.model(image, conf=conf_threshold, verbose=False)
173
  faces = []
174
- annotated_image = image.copy() if return_annotated else None
 
 
175
 
176
  for r in results:
177
- if r.boxes is None:
178
  continue
179
  for box in r.boxes:
180
  x1, y1, x2, y2 = map(int, box.xyxy[0])
181
  confidence = float(box.conf[0])
182
- faces.append({"x": x1, "y": y1, "width": x2 - x1, "height": y2 - y1, "confidence": confidence})
183
 
184
- if return_annotated:
185
- # Draw bounding box
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  cv2.rectangle(annotated_image, (x1, y1), (x2, y2), (0, 255, 0), 3)
187
-
188
- # Simplified label - just "Face" without percentage
189
  label = "Face"
190
  (w, h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
191
  cv2.rectangle(annotated_image, (x1, y1 - h - 10), (x1 + w, y1), (0, 255, 0), -1)
192
- cv2.putText(annotated_image, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
 
 
 
193
 
194
  return faces, annotated_image
195
 
196
  GLOBAL_DETECTOR: Optional[YOLOv8FaceDetector] = None
197
 
198
  def get_global_detector() -> YOLOv8FaceDetector:
199
- """Initializes and returns the global singleton detector instance."""
200
  global GLOBAL_DETECTOR
201
  if GLOBAL_DETECTOR is None:
202
  GLOBAL_DETECTOR = YOLOv8FaceDetector(DetectionConfig())
203
  return GLOBAL_DETECTOR
204
 
205
  # ====================================================
206
- # CORE APPLICATION LOGIC
207
- # ====================================================
208
- class FacePrivacyApp:
209
- def __init__(self, config: AppConfig, detector: YOLOv8FaceDetector):
210
- self.config = config
211
- self.blur_effect = get_blur_effect(config.blur)
212
- self.detector = detector
213
-
214
- def _expand_bbox(self, bbox: Dict[str, Any], img_shape: Tuple[int, int]) -> Tuple[int, int, int, int]:
215
- """Expands a bounding box to include margins for better coverage."""
216
- h_img, w_img = img_shape
217
- new_w = int(bbox["width"] * self.config.scaling_factor)
218
- new_h = int(bbox["height"] * self.config.scaling_factor)
219
- x_offset = (new_w - bbox["width"]) // 2
220
- y_offset = (new_h - bbox["height"]) // 2
221
- x = max(0, bbox["x"] - x_offset - self.config.face_margin)
222
- y = max(0, bbox["y"] - y_offset - self.config.forehead_margin)
223
- w = min(w_img - x, new_w + 2 * self.config.face_margin)
224
- h = min(h_img - y, new_h + self.config.forehead_margin)
225
- return x, y, w, h
226
-
227
- def process_image(self, image: np.ndarray, conf_threshold: float) -> np.ndarray:
228
- """Applies blur to all detected faces in an image."""
229
- writable_image = image.copy()
230
- faces, _ = self.detector.detect_faces(writable_image, conf_threshold, return_annotated=False)
231
- for face in faces:
232
- expanded_roi = self._expand_bbox(face, writable_image.shape[:2])
233
- writable_image = self.blur_effect.apply(writable_image, expanded_roi)
234
- return writable_image
235
-
236
- # ====================================================
237
- # GRADIO HANDLER FUNCTIONS
238
  # ====================================================
239
- def get_app_instance(blur_type: str, blur_amount: float, blur_size: float) -> FacePrivacyApp:
240
- """Creates a FacePrivacyApp instance from UI settings."""
241
- detector = get_global_detector()
242
- app_config = AppConfig(
243
- scaling_factor=blur_size,
244
- blur=BlurConfig(type=blur_type, intensity=blur_amount, pixel_size=int(blur_amount))
245
- )
246
- return FacePrivacyApp(app_config, detector)
247
-
248
- def process_media(media, blur_type, blur_amount, blur_size, sensitivity):
249
- """Process single image with blur effect."""
250
- if media is None:
251
- return None
252
- try:
253
- # Convert sensitivity to confidence threshold
254
- confidence = get_confidence_from_sensitivity(sensitivity)
255
- app = get_app_instance(blur_type, blur_amount, blur_size)
256
- return app.process_image(media, confidence)
257
- except Exception as e:
258
- logger.error(f"Image processing error: {e}")
259
- gr.Warning(f"An error occurred: {e}")
260
- return media
261
-
262
- def process_video(video_file, blur_type, blur_amount, blur_size, sensitivity, progress=gr.Progress()):
263
- """Process video with blur effect."""
264
- if video_file is None:
265
- return None, "⚠️ No video provided."
266
- try:
267
- # Convert sensitivity to confidence threshold
268
- confidence = get_confidence_from_sensitivity(sensitivity)
269
- app = get_app_instance(blur_type, blur_amount, blur_size)
270
- cap = cv2.VideoCapture(video_file.name)
271
- if not cap.isOpened():
272
- return None, "❌ Cannot open video file."
273
-
274
- out_path = create_temp_file()
275
- fourcc = cv2.VideoWriter_fourcc(*'avc1')
276
- 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))
277
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
278
- out_vid = cv2.VideoWriter(out_path, fourcc, fps, (w, h))
279
-
280
- if not out_vid.isOpened():
281
- logger.warning("H.264 codec failed, falling back to mp4v.")
282
- out_vid = cv2.VideoWriter(out_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
283
-
284
- frame_num = 0
285
- while cap.isOpened():
286
- ret, frame = cap.read()
287
- if not ret:
288
- break
289
- frame_num += 1
290
- progress(frame_num / max(total_frames, 1), desc=f"Processing frame {frame_num}/{total_frames}")
291
- processed_frame = app.process_image(frame, confidence)
292
- out_vid.write(processed_frame)
293
-
294
- cap.release()
295
- out_vid.release()
296
- return out_path, f"βœ… Processed {frame_num} frames."
297
- except Exception as e:
298
- logger.error(f"Video processing error: {e}")
299
- gr.Error(f"Video processing failed: {e}")
300
- return None, f"❌ Error: {e}"
301
-
302
- def detect_faces_image(image, sensitivity):
303
- """Detect faces in single image."""
304
- if image is None:
305
- return None, "⚠️ No image provided."
306
  try:
307
- # Convert sensitivity to confidence threshold
308
  confidence = get_confidence_from_sensitivity(sensitivity)
309
  detector = get_global_detector()
310
- faces, annotated_image = detector.detect_faces(image, confidence, return_annotated=True)
311
 
312
- # Simplified result - just show count
313
  if faces:
314
- result = f"βœ… **{len(faces)} face(s) detected!**"
 
 
315
  else:
316
- result = "❌ **No faces detected.**"
317
 
318
- return annotated_image, result
319
  except Exception as e:
320
- logger.error(f"Image detection error: {e}")
321
- gr.Warning(f"An error occurred: {e}")
322
  return image, f"❌ Error: {e}"
323
 
324
- def detect_faces_video(video_file, sensitivity, progress=gr.Progress()):
325
- """Detect faces in video."""
326
- if video_file is None:
327
- return None, "⚠️ No video provided."
328
- try:
329
- # Convert sensitivity to confidence threshold
330
- confidence = get_confidence_from_sensitivity(sensitivity)
331
- detector = get_global_detector()
332
- cap = cv2.VideoCapture(video_file.name)
333
- if not cap.isOpened():
334
- return None, "❌ Cannot open video file."
335
-
336
- out_path = create_temp_file()
337
- fourcc = cv2.VideoWriter_fourcc(*'avc1')
338
- 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))
339
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
340
- out_vid = cv2.VideoWriter(out_path, fourcc, fps, (w, h))
341
-
342
- if not out_vid.isOpened():
343
- out_vid = cv2.VideoWriter(out_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
344
-
345
- frame_num, frames_with_faces = 0, 0
346
- while cap.isOpened():
347
- ret, frame = cap.read()
348
- if not ret:
349
- break
350
- frame_num += 1
351
- progress(frame_num / max(total_frames, 1), desc=f"Analyzing frame {frame_num}/{total_frames}")
352
- faces, annotated_frame = detector.detect_faces(frame, confidence, return_annotated=True)
353
- if faces:
354
- frames_with_faces += 1
355
- out_vid.write(annotated_frame)
356
-
357
- cap.release()
358
- out_vid.release()
359
-
360
- # Simplified result - just show frame count
361
- if frames_with_faces > 0:
362
- result = f"βœ… **Faces detected in {frames_with_faces}/{frame_num} frames!**"
363
- else:
364
- result = f"❌ **No faces detected in {frame_num} frames.**"
365
-
366
- return out_path, result
367
- except Exception as e:
368
- logger.error(f"Video detection error: {e}")
369
- gr.Error(f"Video detection failed: {e}")
370
- return None, f"❌ Error: {e}"
371
-
372
- def detect_faces_webcam(image, sensitivity):
373
- """Detect faces in webcam stream."""
374
- if image is None:
375
  return None
 
376
  try:
377
- # Convert sensitivity to confidence threshold
378
  confidence = get_confidence_from_sensitivity(sensitivity)
379
  detector = get_global_detector()
380
- _, annotated_image = detector.detect_faces(image, confidence, return_annotated=True)
381
- return annotated_image
382
  except Exception as e:
383
- logger.error(f"Webcam detection error: {e}")
384
  return image
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  # ====================================================
387
  # GRADIO UI
388
  # ====================================================
389
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), title="Face Privacy Tool") as demo:
390
- gr.Markdown("# πŸ” Face Privacy Tool")
391
- 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.")
 
 
 
392
 
393
  with gr.Row():
394
- # ========== SETTINGS SIDEBAR (WRAPPED IN FRAME) ==========
395
  with gr.Column(scale=1):
396
- with gr.Column(scale=1, variant="panel"):
397
- gr.Markdown("### βš™οΈ Global Settings")
398
-
399
- with gr.Accordion("Privacy Settings", open=True):
400
- blur_type = gr.Radio(
401
- ["gaussian", "pixelate", "solid"],
402
- value="pixelate",
403
- label="Blur Type",
404
- info="Choose how to obscure faces."
405
- )
406
- blur_amount = gr.Slider(
407
- 1, 100,
408
- step=1,
409
- value=15,
410
- label="Blur Intensity/Size",
411
- info="Higher = more obscured."
412
- )
413
- blur_size = gr.Slider(
414
- 1.0, 2.0,
415
- step=0.05,
416
- value=1.1,
417
- label="Coverage Area",
418
- info="Expand blur beyond face boundary."
419
- )
420
 
421
  with gr.Accordion("Detection Settings", open=True):
422
- # Changed from Slider to Radio for sensitivity
423
  detection_sensitivity = gr.Radio(
424
  choices=list(SENSITIVITY_MAP.keys()),
425
  value="Balanced (Default)",
426
- label="Detection Sensitivity",
427
- info="How strict the face detection should be"
428
  )
429
 
430
- # ========== MAIN CONTENT AREA ==========
431
  with gr.Column(scale=2):
432
  with gr.Tabs():
433
- # ========== PRIVACY MODE ==========
434
- with gr.TabItem("πŸ”’ Privacy Mode (Blur Faces)"):
435
- gr.Markdown("### Apply privacy protection to your media.")
436
 
437
  with gr.Tabs():
438
- # Image Tab
439
  with gr.TabItem("πŸ“· Image"):
440
  with gr.Row():
441
- img_in_blur = gr.Image(
442
- sources=["upload", "clipboard"],
443
- type="numpy",
444
  label="Input Image",
445
- height=500,
446
- )
447
- img_out_blur = gr.Image(
448
- type="numpy",
449
- label="Protected Image",
450
- height=500,
451
- )
452
- with gr.Row():
453
- blur_img_btn = gr.Button("Apply Privacy Blur", variant="primary", scale=3)
454
- gr.ClearButton([img_in_blur, img_out_blur], scale=1)
455
- gr.Examples(
456
- examples=[
457
- ["./examples/single_face.jpg"],
458
- ["./examples/two_faces.png"],
459
- ["./examples/group_photo.png"],
460
- ["./examples/group2.webp"],
461
- ],
462
- inputs=img_in_blur,
463
- label="Click an example to try"
464
- )
465
-
466
- # Video Tab
467
- with gr.TabItem("πŸŽ₯ Video"):
468
- with gr.Row():
469
- vid_in_blur = gr.File(
470
- file_types=[".mp4", ".mov", ".avi"],
471
- label="Input Video"
472
  )
473
  with gr.Column():
474
- vid_out_blur = gr.Video(
475
- label="Protected Video",
 
476
  height=500
477
  )
478
- vid_status_blur = gr.Markdown("")
 
479
  with gr.Row():
480
- blur_vid_btn = gr.Button("Process Video", variant="primary", scale=3)
481
- gr.ClearButton([vid_in_blur, vid_out_blur, vid_status_blur], scale=1)
482
-
483
- # Webcam Tab
484
  with gr.TabItem("πŸ“Ή Webcam"):
485
- gr.Markdown("**Real-time privacy protection from your webcam feed.**")
486
  with gr.Row():
487
- web_in_blur = gr.Image(
488
- sources=["webcam"],
489
- type="numpy",
490
- streaming=True,
491
  label="Live Webcam",
492
- height=500,
493
  )
494
- web_out_blur = gr.Image(
495
- type="numpy",
496
- label="Protected Feed",
497
- height=500,
498
  )
499
-
500
- # ========== DETECTION MODE ==========
501
- with gr.TabItem("πŸ” Detection Mode (Check for Faces)"):
502
- gr.Markdown("### Verify if your media contains human faces.")
503
 
504
- with gr.Tabs():
505
- # Image Detection Tab
506
- with gr.TabItem("πŸ“· Image"):
507
- with gr.Row():
508
- img_in_detect = gr.Image(
509
- sources=["upload", "clipboard"],
510
- type="numpy",
511
- label="Input Image",
512
- height=500,
513
- )
514
- with gr.Column():
515
- img_out_detect = gr.Image(
516
- type="numpy",
517
- label="Detection Result",
518
- height=500,
519
- )
520
- img_status_detect = gr.Markdown("_Upload an image to start._")
521
-
522
- with gr.Row():
523
- detect_img_btn = gr.Button("Detect Faces", variant="primary", scale=3)
524
- gr.ClearButton([img_in_detect, img_out_detect, img_status_detect], scale=1)
525
- gr.Examples(
526
- examples=[
527
- ["./examples/single_face.jpg"],
528
- ["./examples/two_faces.png"],
529
- ["./examples/group_photo.png"],
530
- ["./examples/group2.webp"]
531
- ],
532
- inputs=img_in_detect,
533
- label="Click an example to try"
534
  )
535
-
536
- # Video Detection Tab
537
- with gr.TabItem("πŸŽ₯ Video"):
538
- with gr.Row():
539
- vid_in_detect = gr.File(
540
- file_types=[".mp4", ".mov", ".avi"],
541
- label="Input Video"
542
- )
543
- with gr.Column():
544
- vid_out_detect = gr.Video(
545
- label="Annotated Video",
546
- height=500
547
- )
548
- vid_status_detect = gr.Markdown("_Upload a video to start._")
549
- with gr.Row():
550
- detect_vid_btn = gr.Button("Analyze Video for Faces", variant="primary", scale=3)
551
- gr.ClearButton([vid_in_detect, vid_out_detect, vid_status_detect], scale=1)
552
-
553
- # Webcam Detection Tab
554
- with gr.TabItem("πŸ“Ή Webcam"):
555
- gr.Markdown("**Live face detection from your webcam feed.**")
556
- with gr.Row():
557
- web_in_detect = gr.Image(
558
- sources=["webcam"],
559
- type="numpy",
560
- streaming=True,
561
- label="Live Feed",
562
- height=500,
563
- )
564
- web_out_detect = gr.Image(
565
- type="numpy",
566
- label="Detection Result",
567
- height=500,
568
- )
569
 
570
  # ========== EVENT HANDLERS ==========
571
- # Privacy Mode (updated to use detection_sensitivity instead of detection_confidence)
572
- blur_img_btn.click(
573
- process_media,
574
- inputs=[img_in_blur, blur_type, blur_amount, blur_size, detection_sensitivity],
575
- outputs=img_out_blur
576
- )
577
- blur_vid_btn.click(
578
- process_video,
579
- inputs=[vid_in_blur, blur_type, blur_amount, blur_size, detection_sensitivity],
580
- outputs=[vid_out_blur, vid_status_blur]
581
  )
582
- web_in_blur.stream(
583
- process_media,
584
- inputs=[web_in_blur, blur_type, blur_amount, blur_size, detection_sensitivity],
585
- outputs=web_out_blur
 
586
  )
587
-
588
- # Detection Mode (updated to use detection_sensitivity instead of detection_confidence)
589
- detect_img_btn.click(
590
- detect_faces_image,
591
- inputs=[img_in_detect, detection_sensitivity],
592
- outputs=[img_out_detect, img_status_detect]
593
  )
594
- detect_vid_btn.click(
595
- detect_faces_video,
596
- inputs=[vid_in_detect, detection_sensitivity],
597
- outputs=[vid_out_detect, vid_status_detect]
 
598
  )
599
- web_in_detect.stream(
600
- detect_faces_webcam,
601
- inputs=[web_in_detect, detection_sensitivity],
602
- outputs=web_out_detect
603
  )
 
 
 
604
 
605
  # ====================================================
606
- # MAIN ENTRY POINT
607
  # ====================================================
608
  if __name__ == "__main__":
609
- logger.info("πŸš€ Initializing Face Privacy Tool...")
610
  try:
611
  get_global_detector()
612
- logger.info("βœ… Systems ready. Launching Gradio interface...")
 
613
  demo.launch()
614
  except Exception as e:
615
- logger.error(f"❌ Startup failed: {e}")
616
- logger.info("πŸ’‘ Make sure 'yolov8n-face.pt' is available in the current directory or will be downloaded automatically by ultralytics.")
617
-
 
1
  """
2
+ Face Recognition Tool (YOLO + DeepFace)
3
+ Extended from Face Privacy Tool to add face recognition capabilities
4
  """
5
 
6
  # --- Standard Libraries ---
 
8
  import atexit
9
  import tempfile
10
  import os
11
+ import json
12
+ import pickle
13
  from abc import ABC, abstractmethod
14
  from dataclasses import dataclass, field
15
  from typing import Any, Dict, List, Tuple, Optional
16
+ from pathlib import Path
17
 
18
  # --- Computer Vision & UI Libraries ---
19
  import cv2
 
21
  import gradio as gr
22
  from ultralytics import YOLO
23
 
24
+ # --- Face Recognition Libraries ---
25
+ try:
26
+ from deepface import DeepFace
27
+ DEEPFACE_AVAILABLE = True
28
+ except ImportError:
29
+ DEEPFACE_AVAILABLE = False
30
+ logging.warning("DeepFace not installed. Install with: pip install deepface")
31
+
32
  # --- Configure Logging ---
33
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
34
  logger = logging.getLogger(__name__)
35
 
36
+ # ====================================================
37
+ # FACE DATABASE MANAGER
38
+ # ====================================================
39
+ class FaceDatabase:
40
+ """Manages known faces and their embeddings."""
41
+
42
+ def __init__(self, db_path: str = "face_database"):
43
+ self.db_path = Path(db_path)
44
+ self.db_path.mkdir(exist_ok=True)
45
+ self.embeddings_file = self.db_path / "embeddings.pkl"
46
+ self.known_faces = {} # {person_id: {"name": str, "embedding": np.array, "image_path": str}}
47
+ self.load_database()
48
+
49
+ def load_database(self):
50
+ """Load all known faces and their embeddings."""
51
+ if self.embeddings_file.exists():
52
+ try:
53
+ with open(self.embeddings_file, 'rb') as f:
54
+ self.known_faces = pickle.load(f)
55
+ logger.info(f"βœ… Loaded {len(self.known_faces)} known faces from database")
56
+ except Exception as e:
57
+ logger.error(f"Failed to load embeddings: {e}")
58
+ self.known_faces = {}
59
+ else:
60
+ logger.info("No existing database found. Starting fresh.")
61
+
62
+ def save_database(self):
63
+ """Save embeddings to disk."""
64
+ try:
65
+ with open(self.embeddings_file, 'wb') as f:
66
+ pickle.dump(self.known_faces, f)
67
+ logger.info("βœ… Database saved successfully")
68
+ except Exception as e:
69
+ logger.error(f"Failed to save database: {e}")
70
+
71
+ def add_person(self, person_id: str, name: str, image: np.ndarray) -> Tuple[bool, str]:
72
+ """Add a new person to the database."""
73
+ if not DEEPFACE_AVAILABLE:
74
+ return False, "❌ DeepFace not installed"
75
+
76
+ try:
77
+ # Generate embedding
78
+ embedding_objs = DeepFace.represent(
79
+ img_path=image,
80
+ model_name="Facenet512",
81
+ enforce_detection=True
82
+ )
83
+
84
+ if not embedding_objs:
85
+ return False, "❌ No face detected in image"
86
+
87
+ embedding = np.array(embedding_objs[0]["embedding"])
88
+
89
+ # Save image
90
+ person_dir = self.db_path / person_id
91
+ person_dir.mkdir(exist_ok=True)
92
+ image_path = person_dir / "face.jpg"
93
+ cv2.imwrite(str(image_path), cv2.cvtColor(image, cv2.COLOR_RGB2BGR))
94
+
95
+ # Store in database
96
+ self.known_faces[person_id] = {
97
+ "name": name,
98
+ "embedding": embedding,
99
+ "image_path": str(image_path)
100
+ }
101
+
102
+ self.save_database()
103
+ return True, f"βœ… Added {name} (ID: {person_id}) to database"
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error adding person: {e}")
107
+ return False, f"❌ Error: {str(e)}"
108
+
109
+ def remove_person(self, person_id: str) -> Tuple[bool, str]:
110
+ """Remove a person from the database."""
111
+ if person_id in self.known_faces:
112
+ del self.known_faces[person_id]
113
+ self.save_database()
114
+ return True, f"βœ… Removed person with ID: {person_id}"
115
+ return False, f"❌ Person ID not found: {person_id}"
116
+
117
+ def recognize_face(self, face_image: np.ndarray, threshold: float = 0.6) -> Dict[str, Any]:
118
+ """
119
+ Recognize a face by comparing with known embeddings.
120
+ Returns: {"match": bool, "person_id": str, "name": str, "distance": float}
121
+ """
122
+ if not DEEPFACE_AVAILABLE or len(self.known_faces) == 0:
123
+ return {"match": False, "person_id": "unknown", "name": "Unknown", "distance": 999}
124
+
125
+ try:
126
+ # Generate embedding for input face
127
+ embedding_objs = DeepFace.represent(
128
+ img_path=face_image,
129
+ model_name="Facenet512",
130
+ enforce_detection=False # We already detected it with YOLO
131
+ )
132
+
133
+ if not embedding_objs:
134
+ return {"match": False, "person_id": "unknown", "name": "Unknown", "distance": 999}
135
+
136
+ query_embedding = np.array(embedding_objs[0]["embedding"])
137
+
138
+ # Find best match
139
+ best_match = None
140
+ best_distance = float('inf')
141
+
142
+ for person_id, data in self.known_faces.items():
143
+ # Cosine distance
144
+ distance = np.linalg.norm(query_embedding - data["embedding"])
145
+
146
+ if distance < best_distance:
147
+ best_distance = distance
148
+ best_match = {
149
+ "match": distance < threshold,
150
+ "person_id": person_id,
151
+ "name": data["name"],
152
+ "distance": distance
153
+ }
154
+
155
+ if best_match and best_match["match"]:
156
+ return best_match
157
+ else:
158
+ return {"match": False, "person_id": "unknown", "name": "Unknown", "distance": best_distance}
159
+
160
+ except Exception as e:
161
+ logger.error(f"Recognition error: {e}")
162
+ return {"match": False, "person_id": "unknown", "name": "Unknown", "distance": 999}
163
+
164
+ def list_all_people(self) -> str:
165
+ """Return a formatted list of all known people."""
166
+ if not self.known_faces:
167
+ return "πŸ“­ Database is empty"
168
+
169
+ result = "### πŸ‘₯ Known People:\n\n"
170
+ for person_id, data in self.known_faces.items():
171
+ result += f"- **{data['name']}** (ID: `{person_id}`)\n"
172
+ return result
173
+
174
+ # Global database instance
175
+ FACE_DB: Optional[FaceDatabase] = None
176
+
177
+ def get_face_database() -> FaceDatabase:
178
+ """Get or create the global face database instance."""
179
+ global FACE_DB
180
+ if FACE_DB is None:
181
+ FACE_DB = FaceDatabase()
182
+ return FACE_DB
183
+
184
  # ====================================================
185
  # TEMPORARY FILE CLEANUP
186
  # ====================================================
187
  TEMP_FILES = []
188
 
189
  def cleanup_temp_files():
 
190
  for f in TEMP_FILES:
191
  try:
192
  if os.path.exists(f):
193
  os.remove(f)
194
+ logger.info(f"πŸ—‘οΈ Cleaned up: {f}")
195
  except Exception as e:
196
+ logger.warning(f"⚠️ Failed to delete {f}: {e}")
197
 
198
  atexit.register(cleanup_temp_files)
199
 
200
  def create_temp_file(suffix=".mp4") -> str:
 
201
  path = tempfile.mktemp(suffix=suffix)
202
  TEMP_FILES.append(path)
203
  return path
 
212
  }
213
 
214
  def get_confidence_from_sensitivity(sensitivity: str) -> float:
 
215
  return SENSITIVITY_MAP.get(sensitivity, 0.5)
216
 
217
  # ====================================================
218
+ # CONFIGURATION
219
  # ====================================================
 
 
 
 
 
 
 
 
 
 
 
220
  @dataclass
221
  class DetectionConfig:
 
222
  min_confidence: float = 0.5
223
  model_path: str = "yolov8n-face.pt"
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  # ====================================================
226
+ # YOLO FACE DETECTOR
227
  # ====================================================
228
  class YOLOv8FaceDetector:
 
229
  def __init__(self, config: DetectionConfig):
230
  try:
231
+ logger.info(f"Loading model: {config.model_path}")
232
  self.model = YOLO(config.model_path)
233
  self.min_conf = config.min_confidence
234
+ logger.info("βœ… Model loaded")
235
  except Exception as e:
236
+ logger.error(f"❌ Model loading failed: {e}")
237
+ raise RuntimeError(f"Cannot load model '{config.model_path}'") from e
238
+
239
+ def detect_faces(self, image: np.ndarray, conf_threshold: float,
240
+ recognize: bool = False) -> Tuple[List[Dict[str, Any]], np.ndarray]:
241
+ """
242
+ Detect faces and optionally recognize them.
243
+ Returns: (faces_list, annotated_image)
244
+ """
245
  results = self.model(image, conf=conf_threshold, verbose=False)
246
  faces = []
247
+ annotated_image = image.copy()
248
+
249
+ face_db = get_face_database() if recognize else None
250
 
251
  for r in results:
252
+ if r.boxes is None:
253
  continue
254
  for box in r.boxes:
255
  x1, y1, x2, y2 = map(int, box.xyxy[0])
256
  confidence = float(box.conf[0])
 
257
 
258
+ face_info = {
259
+ "x": x1, "y": y1,
260
+ "width": x2 - x1,
261
+ "height": y2 - y1,
262
+ "confidence": confidence
263
+ }
264
+
265
+ # Face recognition
266
+ if recognize and face_db and DEEPFACE_AVAILABLE:
267
+ # Crop face region
268
+ face_crop = image[y1:y2, x1:x2]
269
+ if face_crop.size > 0:
270
+ recognition_result = face_db.recognize_face(face_crop)
271
+ face_info.update(recognition_result)
272
+
273
+ # Draw bounding box (green if known, red if unknown)
274
+ color = (0, 255, 0) if recognition_result["match"] else (0, 0, 255)
275
+ cv2.rectangle(annotated_image, (x1, y1), (x2, y2), color, 3)
276
+
277
+ # Draw label with ID and name
278
+ if recognition_result["match"]:
279
+ label = f"{recognition_result['name']} ({recognition_result['person_id']})"
280
+ else:
281
+ label = "Unknown"
282
+
283
+ # Text background
284
+ (w, h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
285
+ cv2.rectangle(annotated_image, (x1, y1 - h - 10), (x1 + w, y1), color, -1)
286
+ cv2.putText(annotated_image, label, (x1, y1 - 5),
287
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
288
+ else:
289
+ # Simple detection without recognition
290
  cv2.rectangle(annotated_image, (x1, y1), (x2, y2), (0, 255, 0), 3)
 
 
291
  label = "Face"
292
  (w, h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
293
  cv2.rectangle(annotated_image, (x1, y1 - h - 10), (x1 + w, y1), (0, 255, 0), -1)
294
+ cv2.putText(annotated_image, label, (x1, y1 - 5),
295
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
296
+
297
+ faces.append(face_info)
298
 
299
  return faces, annotated_image
300
 
301
  GLOBAL_DETECTOR: Optional[YOLOv8FaceDetector] = None
302
 
303
  def get_global_detector() -> YOLOv8FaceDetector:
 
304
  global GLOBAL_DETECTOR
305
  if GLOBAL_DETECTOR is None:
306
  GLOBAL_DETECTOR = YOLOv8FaceDetector(DetectionConfig())
307
  return GLOBAL_DETECTOR
308
 
309
  # ====================================================
310
+ # GRADIO HANDLERS - FACE RECOGNITION
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  # ====================================================
312
+ def recognize_faces_image(image, sensitivity):
313
+ """Recognize faces in a single image."""
314
+ if image is None:
315
+ return None, "⚠️ No image provided"
316
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  try:
 
318
  confidence = get_confidence_from_sensitivity(sensitivity)
319
  detector = get_global_detector()
320
+ faces, annotated = detector.detect_faces(image, confidence, recognize=True)
321
 
 
322
  if faces:
323
+ known = sum(1 for f in faces if f.get("match", False))
324
+ unknown = len(faces) - known
325
+ result = f"βœ… **Detected {len(faces)} face(s):** {known} known, {unknown} unknown"
326
  else:
327
+ result = "❌ **No faces detected**"
328
 
329
+ return annotated, result
330
  except Exception as e:
331
+ logger.error(f"Recognition error: {e}")
 
332
  return image, f"❌ Error: {e}"
333
 
334
+ def recognize_faces_webcam(image, sensitivity):
335
+ """Recognize faces in webcam stream."""
336
+ if image is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  return None
338
+
339
  try:
 
340
  confidence = get_confidence_from_sensitivity(sensitivity)
341
  detector = get_global_detector()
342
+ _, annotated = detector.detect_faces(image, confidence, recognize=True)
343
+ return annotated
344
  except Exception as e:
345
+ logger.error(f"Webcam recognition error: {e}")
346
  return image
347
 
348
+ # ====================================================
349
+ # DATABASE MANAGEMENT HANDLERS
350
+ # ====================================================
351
+ def add_person_to_db(person_id, name, image):
352
+ """Add a new person to the face database."""
353
+ if not person_id or not name:
354
+ return None, "⚠️ Please provide both ID and Name"
355
+
356
+ if image is None:
357
+ return None, "⚠️ Please upload a face image"
358
+
359
+ db = get_face_database()
360
+ success, message = db.add_person(person_id, name, image)
361
+
362
+ if success:
363
+ return db.list_all_people(), message
364
+ else:
365
+ return db.list_all_people(), message
366
+
367
+ def remove_person_from_db(person_id):
368
+ """Remove a person from the database."""
369
+ if not person_id:
370
+ return None, "⚠️ Please provide a person ID"
371
+
372
+ db = get_face_database()
373
+ success, message = db.remove_person(person_id)
374
+ return db.list_all_people(), message
375
+
376
+ def refresh_database_list():
377
+ """Refresh the list of known people."""
378
+ db = get_face_database()
379
+ return db.list_all_people()
380
+
381
  # ====================================================
382
  # GRADIO UI
383
  # ====================================================
384
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue"), title="Face Recognition Tool") as demo:
385
+ gr.Markdown("# 🎭 Face Recognition Tool")
386
+ gr.Markdown("AI-powered face detection and recognition using YOLOv8 + DeepFace. Detect faces and identify known individuals.")
387
+
388
+ if not DEEPFACE_AVAILABLE:
389
+ gr.Markdown("⚠️ **Warning:** DeepFace not installed. Install with: `pip install deepface`")
390
 
391
  with gr.Row():
392
+ # ========== SETTINGS SIDEBAR ==========
393
  with gr.Column(scale=1):
394
+ with gr.Column(variant="panel"):
395
+ gr.Markdown("### βš™οΈ Settings")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
397
  with gr.Accordion("Detection Settings", open=True):
 
398
  detection_sensitivity = gr.Radio(
399
  choices=list(SENSITIVITY_MAP.keys()),
400
  value="Balanced (Default)",
401
+ label="Detection Sensitivity"
 
402
  )
403
 
404
+ # ========== MAIN CONTENT ==========
405
  with gr.Column(scale=2):
406
  with gr.Tabs():
407
+ # ========== FACE RECOGNITION TAB ==========
408
+ with gr.TabItem("🎭 Face Recognition"):
409
+ gr.Markdown("### Identify known faces in your images")
410
 
411
  with gr.Tabs():
412
+ # Image Recognition
413
  with gr.TabItem("πŸ“· Image"):
414
  with gr.Row():
415
+ img_in_recog = gr.Image(
416
+ sources=["upload", "clipboard"],
417
+ type="numpy",
418
  label="Input Image",
419
+ height=500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  )
421
  with gr.Column():
422
+ img_out_recog = gr.Image(
423
+ type="numpy",
424
+ label="Recognition Result",
425
  height=500
426
  )
427
+ img_status_recog = gr.Markdown("_Upload an image to start._")
428
+
429
  with gr.Row():
430
+ recog_img_btn = gr.Button("Recognize Faces", variant="primary", scale=3)
431
+ gr.ClearButton([img_in_recog, img_out_recog, img_status_recog], scale=1)
432
+
433
+ # Webcam Recognition
434
  with gr.TabItem("πŸ“Ή Webcam"):
435
+ gr.Markdown("**Real-time face recognition from webcam**")
436
  with gr.Row():
437
+ web_in_recog = gr.Image(
438
+ sources=["webcam"],
439
+ type="numpy",
440
+ streaming=True,
441
  label="Live Webcam",
442
+ height=500
443
  )
444
+ web_out_recog = gr.Image(
445
+ type="numpy",
446
+ label="Recognition Feed",
447
+ height=500
448
  )
449
+
450
+ # ========== DATABASE MANAGEMENT TAB ==========
451
+ with gr.TabItem("πŸ—„οΈ Manage Database"):
452
+ gr.Markdown("### Add or remove known people")
453
 
454
+ with gr.Row():
455
+ with gr.Column():
456
+ gr.Markdown("#### βž• Add New Person")
457
+ add_person_id = gr.Textbox(label="Person ID", placeholder="e.g., EMP001")
458
+ add_person_name = gr.Textbox(label="Name", placeholder="e.g., John Doe")
459
+ add_person_image = gr.Image(
460
+ sources=["upload", "webcam"],
461
+ type="numpy",
462
+ label="Face Image"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  )
464
+ add_person_btn = gr.Button("Add Person", variant="primary")
465
+ add_status = gr.Markdown("")
466
+
467
+ with gr.Column():
468
+ gr.Markdown("#### βž– Remove Person")
469
+ remove_person_id = gr.Textbox(label="Person ID to Remove", placeholder="e.g., EMP001")
470
+ remove_person_btn = gr.Button("Remove Person", variant="stop")
471
+ remove_status = gr.Markdown("")
472
+
473
+ gr.Markdown("---")
474
+ with gr.Row():
475
+ refresh_btn = gr.Button("πŸ”„ Refresh List", scale=1)
476
+ database_list = gr.Markdown(value="Loading...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
  # ========== EVENT HANDLERS ==========
479
+ # Recognition
480
+ recog_img_btn.click(
481
+ recognize_faces_image,
482
+ inputs=[img_in_recog, detection_sensitivity],
483
+ outputs=[img_out_recog, img_status_recog]
 
 
 
 
 
484
  )
485
+
486
+ web_in_recog.stream(
487
+ recognize_faces_webcam,
488
+ inputs=[web_in_recog, detection_sensitivity],
489
+ outputs=web_out_recog
490
  )
491
+
492
+ # Database Management
493
+ add_person_btn.click(
494
+ add_person_to_db,
495
+ inputs=[add_person_id, add_person_name, add_person_image],
496
+ outputs=[database_list, add_status]
497
  )
498
+
499
+ remove_person_btn.click(
500
+ remove_person_from_db,
501
+ inputs=[remove_person_id],
502
+ outputs=[database_list, remove_status]
503
  )
504
+
505
+ refresh_btn.click(
506
+ refresh_database_list,
507
+ outputs=database_list
508
  )
509
+
510
+ # Load database list on startup
511
+ demo.load(refresh_database_list, outputs=database_list)
512
 
513
  # ====================================================
514
+ # MAIN
515
  # ====================================================
516
  if __name__ == "__main__":
517
+ logger.info("πŸš€ Starting Face Recognition Tool...")
518
  try:
519
  get_global_detector()
520
+ get_face_database()
521
+ logger.info("βœ… Systems ready. Launching...")
522
  demo.launch()
523
  except Exception as e:
524
+ logger.error(f"❌ Startup failed: {e}")