| import os |
| import urllib.request |
| import numpy as np |
| import cv2 |
| from pathlib import Path |
| from tensorflow.keras.applications.mobilenet_v2 import preprocess_input |
|
|
| BASE_DIR = Path(__file__).resolve().parent.parent |
| FACE_DETECTOR_DIR = BASE_DIR / "models" / "face_detector" |
|
|
| FACE_DETECTOR_FILES = { |
| "deploy.prototxt": ( |
| "https://raw.githubusercontent.com/opencv/opencv/master/" |
| "samples/dnn/face_detector/deploy.prototxt" |
| ), |
| "res10_300x300_ssd_iter_140000.caffemodel": ( |
| "https://raw.githubusercontent.com/opencv/opencv_3rdparty/" |
| "dnn_samples_face_detector_20170830/" |
| "res10_300x300_ssd_iter_140000.caffemodel" |
| ), |
| } |
|
|
| CLASSES = ["with_mask", "without_mask"] |
|
|
|
|
| def download_face_detector(dest_dir: Path = FACE_DETECTOR_DIR) -> None: |
| dest_dir.mkdir(parents=True, exist_ok=True) |
| for filename, url in FACE_DETECTOR_FILES.items(): |
| dest = dest_dir / filename |
| if dest.exists(): |
| print(f"[OK] {filename} already exists, skipping download.") |
| continue |
| print(f"[DOWNLOADING] {filename} ...") |
| urllib.request.urlretrieve(url, dest) |
| print(f"[DONE] Saved to {dest}") |
|
|
|
|
| def load_face_detector(model_dir: Path = FACE_DETECTOR_DIR): |
| prototxt = str(model_dir / "deploy.prototxt") |
| caffemodel = str(model_dir / "res10_300x300_ssd_iter_140000.caffemodel") |
| if not os.path.exists(prototxt) or not os.path.exists(caffemodel): |
| raise FileNotFoundError( |
| "Face detector model files not found. " |
| "Run: python src/utils.py --download-face-detector" |
| ) |
| return cv2.dnn.readNet(prototxt, caffemodel) |
|
|
|
|
| def detect_faces(frame: np.ndarray, net, confidence_threshold: float = 0.5): |
| """ |
| Returns list of (startX, startY, endX, endY) for each face found. |
| Adds 5% padding around each detected face region. |
| """ |
| h, w = frame.shape[:2] |
| blob = cv2.dnn.blobFromImage( |
| cv2.resize(frame, (300, 300)), 1.0, (300, 300), |
| (104.0, 177.0, 123.0) |
| ) |
| net.setInput(blob) |
| detections = net.forward() |
|
|
| faces = [] |
| for i in range(detections.shape[2]): |
| confidence = detections[0, 0, i, 2] |
| if confidence < confidence_threshold: |
| continue |
| box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) |
| startX, startY, endX, endY = box.astype(int) |
|
|
| |
| pad_x = int((endX - startX) * 0.05) |
| pad_y = int((endY - startY) * 0.05) |
| startX = max(0, startX - pad_x) |
| startY = max(0, startY - pad_y) |
| endX = min(w, endX + pad_x) |
| endY = min(h, endY + pad_y) |
|
|
| faces.append((startX, startY, endX, endY)) |
| return faces |
|
|
|
|
| def preprocess_face(face_roi: np.ndarray, target_size: tuple = (224, 224)) -> np.ndarray: |
| face = cv2.resize(face_roi, target_size) |
| face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB) |
| face = preprocess_input(face.astype("float32")) |
| return np.expand_dims(face, axis=0) |
|
|
|
|
| def get_label_color(label: str) -> tuple: |
| return (0, 255, 0) if label == "with_mask" else (0, 0, 255) |
|
|
|
|
| def load_dataset_paths(dataset_dir: Path): |
| """ |
| Returns (image_paths, labels) lists from a folder structured as: |
| dataset_dir/ |
| with_mask/ |
| without_mask/ |
| """ |
| image_paths, labels = [], [] |
| for class_name in CLASSES: |
| class_dir = dataset_dir / class_name |
| if not class_dir.exists(): |
| print(f"[WARNING] Directory not found: {class_dir}") |
| continue |
| for ext in ("*.jpg", "*.jpeg", "*.png"): |
| for img_path in class_dir.glob(ext): |
| image_paths.append(str(img_path)) |
| labels.append(class_name) |
| return image_paths, labels |
|
|
|
|
| if __name__ == "__main__": |
| import argparse |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--download-face-detector", action="store_true") |
| args = parser.parse_args() |
|
|
| if args.download_face_detector: |
| download_face_detector() |
|
|