Spaces:
Build error
Build error
| """ | |
| Face Mask Detection and Inpainting Module. | |
| Detects if a person is wearing a face mask and removes it via inpainting | |
| to reveal the underlying face for recognition. | |
| Mask detection uses a heuristic based on lower-face analysis. | |
| Inpainting uses OpenCV's TELEA algorithm on the masked region. | |
| """ | |
| from typing import Tuple, Optional | |
| import numpy as np | |
| import cv2 | |
| class MaskProcessor: | |
| """ | |
| Detects face masks and performs inpainting to reveal the face. | |
| Uses color/texture analysis in the lower half of the face to detect masks. | |
| Inpaints using cv2.INPAINT_TELEA for CPU-friendly operation. | |
| """ | |
| def __init__(self, mask_threshold: float = 0.5): | |
| """ | |
| Initialize the mask processor. | |
| Args: | |
| mask_threshold: Confidence threshold for mask detection. | |
| """ | |
| self.mask_threshold = mask_threshold | |
| # Skin color ranges in HSV for lower-face analysis | |
| self.lower_skin = np.array([0, 20, 70], dtype=np.uint8) | |
| self.upper_skin = np.array([20, 150, 255], dtype=np.uint8) | |
| # Mask color ranges (blue/green surgical masks, black cloth masks) | |
| self.mask_colors = { | |
| 'blue': (np.array([90, 50, 50], dtype=np.uint8), np.array([120, 255, 255], dtype=np.uint8)), | |
| 'green': (np.array([40, 50, 50], dtype=np.uint8), np.array([80, 255, 255], dtype=np.uint8)), | |
| 'white': (np.array([0, 0, 180], dtype=np.uint8), np.array([180, 30, 255], dtype=np.uint8)), | |
| 'black': (np.array([0, 0, 0], dtype=np.uint8), np.array([180, 255, 60], dtype=np.uint8)), | |
| } | |
| def detect_mask(self, face_crop: np.ndarray) -> Tuple[bool, float]: | |
| """ | |
| Detect if a face crop contains a mask. | |
| Analyzes the lower half of the face for mask-like features. | |
| Uses color segmentation and texture analysis. | |
| Args: | |
| face_crop: Cropped face image (BGR). | |
| Returns: | |
| Tuple of (is_masked: bool, confidence: float). | |
| """ | |
| if face_crop is None or face_crop.size == 0: | |
| return False, 0.0 | |
| h, w = face_crop.shape[:2] | |
| if h < 20 or w < 20: | |
| return False, 0.0 | |
| # Extract lower half of face (where masks are worn) | |
| lower_half = face_crop[h // 2:, :, :] | |
| # Convert to HSV for color analysis | |
| hsv = cv2.cvtColor(lower_half, cv2.COLOR_BGR2HSV) | |
| # Check for skin pixels in lower half | |
| skin_mask = cv2.inRange(hsv, self.lower_skin, self.upper_skin) | |
| skin_ratio = np.sum(skin_mask > 0) / skin_mask.size | |
| # Check for mask color pixels | |
| mask_pixel_ratio = 0.0 | |
| for color_name, (lower, upper) in self.mask_colors.items(): | |
| color_mask = cv2.inRange(hsv, lower, upper) | |
| ratio = np.sum(color_mask > 0) / color_mask.size | |
| mask_pixel_ratio = max(mask_pixel_ratio, ratio) | |
| # Edge detection: masks have fewer edges than skin (smooth surface) | |
| gray = cv2.cvtColor(lower_half, cv2.COLOR_BGR2GRAY) | |
| edges = cv2.Canny(gray, 50, 150) | |
| edge_density = np.sum(edges > 0) / edges.size | |
| # Decision logic: | |
| # Mask is likely if: low skin ratio + high mask color ratio + low edge density | |
| is_masked = False | |
| confidence = 0.0 | |
| if skin_ratio < 0.2 and mask_pixel_ratio > 0.15: | |
| # Strong mask color match | |
| is_masked = True | |
| confidence = min(1.0, mask_pixel_ratio * 2.0) | |
| elif skin_ratio < 0.15 and edge_density < 0.05: | |
| # Low skin and low edges -> likely mask | |
| is_masked = True | |
| confidence = 0.7 | |
| elif skin_ratio < 0.3 and mask_pixel_ratio > 0.1: | |
| # Moderate evidence | |
| is_masked = True | |
| confidence = 0.6 | |
| elif edge_density < 0.02 and mask_pixel_ratio > 0.05: | |
| # Very smooth surface + some mask color | |
| is_masked = True | |
| confidence = 0.55 | |
| else: | |
| # Likely no mask - skin visible | |
| confidence = max(0, min(0.5, skin_ratio)) | |
| return is_masked, float(confidence) | |
| def remove_mask( | |
| self, | |
| face_crop: np.ndarray, | |
| mask_region: Optional[np.ndarray] = None | |
| ) -> np.ndarray: | |
| """ | |
| Remove mask from face via inpainting. | |
| Creates a mask of the lower half of the face and inpaints | |
| using OpenCV's TELEA algorithm for smooth results. | |
| Args: | |
| face_crop: Cropped face image (BGR). | |
| mask_region: Optional custom mask region. If None, uses lower half. | |
| Returns: | |
| Inpainted face crop. | |
| """ | |
| if face_crop is None or face_crop.size == 0: | |
| return face_crop | |
| h, w = face_crop.shape[:2] | |
| if h < 20 or w < 20: | |
| return face_crop | |
| if mask_region is not None: | |
| inpaint_mask = mask_region | |
| else: | |
| # Create mask for lower half of face (below nose area) | |
| inpaint_mask = np.zeros((h, w), dtype=np.uint8) | |
| # Lower 45% of face (chin + mouth area) | |
| mask_start = int(h * 0.45) | |
| inpaint_mask[mask_start:, :] = 255 | |
| # Also mask the sides (cheeks) for more natural blending | |
| side_width = int(w * 0.08) | |
| inpaint_mask[mask_start:, :side_width] = 255 | |
| inpaint_mask[mask_start:, w - side_width:] = 255 | |
| # Apply inpainting | |
| inpainted = cv2.inpaint( | |
| face_crop, | |
| inpaint_mask, | |
| inpaintRadius=3, | |
| flags=cv2.INPAINT_TELEA | |
| ) | |
| return inpainted | |
| if __name__ == "__main__": | |
| # Quick test | |
| mp = MaskProcessor() | |
| test_face = np.random.randint(0, 255, (200, 150, 3), dtype=np.uint8) | |
| is_masked, conf = mp.detect_mask(test_face) | |
| print(f"Mask detection test: is_masked={is_masked}, confidence={conf:.3f}") | |
| inpainted = mp.remove_mask(test_face) | |
| print(f"Inpainting test: output shape={inpainted.shape}") | |
| print("MaskProcessor OK!") |