|
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, |
|
CenterFace, |
|
) |
|
from deepface.commons import logger as log |
|
|
|
logger = log.get_singletonish_logger() |
|
|
|
|
|
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 |
|
|
|
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, |
|
"centerface": CenterFace.CenterFaceClient, |
|
} |
|
|
|
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) |
|
|
|
|
|
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 |
|
|
|
|
|
facial_areas = face_detector.detect_faces(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: |
|
|
|
|
|
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) |
|
|
|
|
|
detected_face = img[int(y) : int(y + h), int(x) : int(x + w)] |
|
|
|
|
|
if align is True: |
|
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. |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
direction = 1 if angle >= 0 else -1 |
|
angle = abs(angle) % 360 |
|
if angle == 0: |
|
return facial_area |
|
|
|
|
|
angle = angle * np.pi / 180 |
|
|
|
height, weight = size |
|
|
|
|
|
x = (facial_area[0] + facial_area[2]) / 2 - weight / 2 |
|
y = (facial_area[1] + facial_area[3]) / 2 - height / 2 |
|
|
|
|
|
x_new = x * np.cos(angle) + y * direction * np.sin(angle) |
|
y_new = -x * direction * np.sin(angle) + y * np.cos(angle) |
|
|
|
|
|
x_new = x_new + weight / 2 |
|
y_new = y_new + height / 2 |
|
|
|
|
|
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 |
|
|
|
|
|
x1 = max(int(x1), 0) |
|
y1 = max(int(y1), 0) |
|
x2 = min(int(x2), weight) |
|
y2 = min(int(y2), height) |
|
|
|
return (x1, y1, x2, y2) |
|
|