from typing import Any, List, Tuple import numpy as np from deepface.modules import detection from deepface.models.Detector import Detector, DetectedFace, FacialAreaRegion from deepface.detectors import ( FastMtCnn, MediaPipe, MtCnn, OpenCv, Dlib, RetinaFace, Ssd, Yolo, YuNet, ) from deepface.commons.logger import Logger logger = Logger(module="deepface/detectors/DetectorWrapper.py") def build_model(detector_backend: str) -> Any: """ Build a face detector model Args: detector_backend (str): backend detector name Returns: built detector (Any) """ global face_detector_obj # singleton design pattern backends = { "opencv": OpenCv.OpenCvClient, "mtcnn": MtCnn.MtCnnClient, "ssd": Ssd.SsdClient, "dlib": Dlib.DlibClient, "retinaface": RetinaFace.RetinaFaceClient, "mediapipe": MediaPipe.MediaPipeClient, "yolov8": Yolo.YoloClient, "yunet": YuNet.YuNetClient, "fastmtcnn": FastMtCnn.FastMtCnnClient, } if not "face_detector_obj" in globals(): face_detector_obj = {} built_models = list(face_detector_obj.keys()) if detector_backend not in built_models: face_detector = backends.get(detector_backend) if face_detector: face_detector = face_detector() face_detector_obj[detector_backend] = face_detector else: raise ValueError("invalid detector_backend passed - " + detector_backend) return face_detector_obj[detector_backend] def detect_faces( detector_backend: str, img: np.ndarray, align: bool = True, expand_percentage: int = 0 ) -> List[DetectedFace]: """ Detect face(s) from a given image Args: detector_backend (str): detector name img (np.ndarray): pre-loaded image align (bool): enable or disable alignment after detection expand_percentage (int): expand detected facial area with a percentage (default is 0). Returns: results (List[DetectedFace]): A list of DetectedFace objects where each object contains: - img (np.ndarray): The detected face as a NumPy array. - facial_area (FacialAreaRegion): The facial area region represented as x, y, w, h, left_eye and right eye. left eye and right eye are eyes on the left and right with respect to the person instead of observer. - confidence (float): The confidence score associated with the detected face. """ face_detector: Detector = build_model(detector_backend) # validate expand percentage score if expand_percentage < 0: logger.warn( f"Expand percentage cannot be negative but you set it to {expand_percentage}." "Overwritten it to 0." ) expand_percentage = 0 # find facial areas of given image facial_areas = face_detector.detect_faces(img=img) results = [] for facial_area in facial_areas: x = facial_area.x y = facial_area.y w = facial_area.w h = facial_area.h left_eye = facial_area.left_eye right_eye = facial_area.right_eye confidence = facial_area.confidence if expand_percentage > 0: # Expand the facial region height and width by the provided percentage # ensuring that the expanded region stays within img.shape limits expanded_w = w + int(w * expand_percentage / 100) expanded_h = h + int(h * expand_percentage / 100) x = max(0, x - int((expanded_w - w) / 2)) y = max(0, y - int((expanded_h - h) / 2)) w = min(img.shape[1] - x, expanded_w) h = min(img.shape[0] - y, expanded_h) # extract detected face unaligned detected_face = img[int(y) : int(y + h), int(x) : int(x + w)] # align original image, then find projection of detected face area after alignment if align is True: # and left_eye is not None and right_eye is not None: aligned_img, angle = detection.align_face( img=img, left_eye=left_eye, right_eye=right_eye ) rotated_x1, rotated_y1, rotated_x2, rotated_y2 = rotate_facial_area( facial_area=(x, y, x + w, y + h), angle=angle, size=(img.shape[0], img.shape[1]) ) detected_face = aligned_img[ int(rotated_y1) : int(rotated_y2), int(rotated_x1) : int(rotated_x2) ] result = DetectedFace( img=detected_face, facial_area=FacialAreaRegion( x=x, y=y, h=h, w=w, confidence=confidence, left_eye=left_eye, right_eye=right_eye ), confidence=confidence, ) results.append(result) return results def rotate_facial_area( facial_area: Tuple[int, int, int, int], angle: float, size: Tuple[int, int] ) -> Tuple[int, int, int, int]: """ Rotate the facial area around its center. Inspried from the work of @UmutDeniz26 - github.com/serengil/retinaface/pull/80 Args: facial_area (tuple of int): Representing the (x1, y1, x2, y2) of the facial area. x2 is equal to x1 + w1, and y2 is equal to y1 + h1 angle (float): Angle of rotation in degrees. Its sign determines the direction of rotation. Note that angles > 360 degrees are normalized to the range [0, 360). size (tuple of int): Tuple representing the size of the image (width, height). Returns: rotated_coordinates (tuple of int): Representing the new coordinates (x1, y1, x2, y2) or (x1, y1, x1+w1, y1+h1) of the rotated facial area. """ # Normalize the witdh of the angle so we don't have to # worry about rotations greater than 360 degrees. # We workaround the quirky behavior of the modulo operator # for negative angle values. direction = 1 if angle >= 0 else -1 angle = abs(angle) % 360 if angle == 0: return facial_area # Angle in radians angle = angle * np.pi / 180 # Translate the facial area to the center of the image x = (facial_area[0] + facial_area[2]) / 2 - size[1] / 2 y = (facial_area[1] + facial_area[3]) / 2 - size[0] / 2 # Rotate the facial area x_new = x * np.cos(angle) + y * direction * np.sin(angle) y_new = -x * direction * np.sin(angle) + y * np.cos(angle) # Translate the facial area back to the original position x_new = x_new + size[1] / 2 y_new = y_new + size[0] / 2 # Calculate the new facial area x1 = x_new - (facial_area[2] - facial_area[0]) / 2 y1 = y_new - (facial_area[3] - facial_area[1]) / 2 x2 = x_new + (facial_area[2] - facial_area[0]) / 2 y2 = y_new + (facial_area[3] - facial_area[1]) / 2 return (int(x1), int(y1), int(x2), int(y2))