From 57f0e4f8f7ccd53d900381421ac426ec74dbf37a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 06:23:11 +0000 Subject: [PATCH 1/3] feat: enhance face detection with GPU support, privacy mode, and gallery - Added GPU support (CUDA) to the YOLOv12 detector if available. - Implemented `blur_faces` method in `YOLOv12FaceDetector` for privacy-conscious detections. - Implemented `get_face_crops` method to extract and gallery detected faces. - Updated `/api/detect-image` to support an optional `blur` parameter and return base64 face crops. - Added a Dark Mode toggle to the web UI with persistent theme settings. - Added a "Privacy Mode (Blur Faces)" toggle and a "Face Gallery" section to the UI. - Cleaned up the repository of temporary test and mock files. Co-authored-by: RevDra <200167038+RevDra@users.noreply.github.com> --- src/face_detection_yolov12.py | 58 +++++++++++++++++++++- src/web_app.py | 20 ++++++-- web/templates/index.html | 91 ++++++++++++++++++++++++++++++++--- 3 files changed, 157 insertions(+), 12 deletions(-) diff --git a/src/face_detection_yolov12.py b/src/face_detection_yolov12.py index f2c9ba2..59e4e41 100644 --- a/src/face_detection_yolov12.py +++ b/src/face_detection_yolov12.py @@ -30,8 +30,9 @@ def __init__(self, model_path): try: from ultralytics import YOLO - self.yolo = YOLO(model_path) - print(f"Loaded YOLOv12 model from {model_path}") + self.device = "cuda" if torch.cuda.is_available() else "cpu" + self.yolo = YOLO(model_path).to(self.device) + print(f"Loaded YOLOv12 model from {model_path} on {self.device}") except ImportError: raise ImportError("ultralytics package not found. pip install ultralytics") except Exception as e: @@ -136,6 +137,59 @@ def draw_faces(self, image, detections, color=(0, 255, 0), thickness=2, show_con ) return result + def blur_faces(self, image, detections, blur_factor=30): + """Apply Gaussian blur to detected faces for privacy""" + result = image.copy() + h, w = image.shape[:2] + + for det in detections: + x1, y1, x2, y2 = det["x1"], det["y1"], det["x2"], det["y2"] + + # Ensure coordinates are within image boundaries + x1, y1 = max(0, x1), max(0, y1) + x2, y2 = min(w, x2), min(h, y2) + + if x2 > x1 and y2 > y1: + # Extract the face ROI from the ORIGINAL image to avoid cumulative blur + # but here we work on result which is a copy. + face_roi = image[y1:y2, x1:x2] + + # Ensure kernel size is odd and based on face size + kw = (x2 - x1) // 2 + kh = (y2 - y1) // 2 + + # Minimum kernel size for noticeable blur + kw = max(25, kw if kw % 2 != 0 else kw + 1) + kh = max(25, kh if kh % 2 != 0 else kh + 1) + + # Use BORDER_DEFAULT (reflection) for better visual result + blurred_face = cv2.GaussianBlur(face_roi, (kw, kh), blur_factor) + result[y1:y2, x1:x2] = blurred_face + + return result + + def get_face_crops(self, image, detections, padding=0.1): + """Extract cropped face images from detection results""" + crops = [] + h, w = image.shape[:2] + + for det in detections: + x1, y1, x2, y2 = det["x1"], det["y1"], det["x2"], det["y2"] + fw, fh = x2 - x1, y2 - y1 + + # Add padding + px = int(fw * padding) + py = int(fh * padding) + + cx1, cy1 = max(0, x1 - px), max(0, y1 - py) + cx2, cy2 = min(w, x2 + px), min(h, y2 + py) + + if cx2 > cx1 and cy2 > cy1: + crop = image[cy1:cy2, cx1:cx2] + crops.append(crop) + + return crops + # --- GUI Class (Webcam) --- class WebcamFaceDetectionGUI: diff --git a/src/web_app.py b/src/web_app.py index f9fa948..45d6ab2 100644 --- a/src/web_app.py +++ b/src/web_app.py @@ -148,8 +148,10 @@ def detect_image(): if not allowed_file(file.filename) or not is_image(file.filename): return jsonify({"error": "Only image files allowed"}), 400 - # Get model selection + # Get parameters model = request.form.get("model", "yolov12l-face.pt") + blur = request.form.get("blur") == "true" + if model not in ALLOWED_MODELS: app.logger.info(f"Invalid model '{model}' requested. Fallback to default.") model = "yolov12l-face.pt" @@ -171,12 +173,23 @@ def detect_image(): # Use standard detection for uploaded files detections = detector.detect_faces(image, conf_threshold=0.32) - # Draw detections - result_image = detector.draw_faces(image, detections, show_confidence=True) + # Process image: Draw or Blur + if blur: + result_image = detector.blur_faces(image, detections) + else: + result_image = detector.draw_faces(image, detections, show_confidence=True) if result_image is None: return jsonify({"error": "Failed to process image"}), 500 + # Extract crops for gallery + crops_base64 = [] + if len(detections) > 0: + crops = detector.get_face_crops(image, detections) + for crop in crops: + _, buffer = cv2.imencode(".jpg", crop) + crops_base64.append(base64.b64encode(buffer).decode()) + # Convert result to base64 for display _, buffer = cv2.imencode(".jpg", result_image) img_base64 = base64.b64encode(buffer).decode() @@ -197,6 +210,7 @@ def detect_image(): } for i, det in enumerate(detections) ], + "crops": crops_base64 }, } diff --git a/web/templates/index.html b/web/templates/index.html index 84ca4cd..ec38c7a 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -6,9 +6,18 @@