diff --git a/deepface/models/Detector.py b/deepface/models/Detector.py index 2fe93fc..730c432 100644 --- a/deepface/models/Detector.py +++ b/deepface/models/Detector.py @@ -1,8 +1,37 @@ from typing import List, Tuple, Optional, Union -from abc import ABC +from abc import ABC, abstractmethod from dataclasses import dataclass import numpy as np +# Notice that all facial detector models must be inherited from this class + + +# pylint: disable=unnecessary-pass, too-few-public-methods, too-many-instance-attributes +class Detector(ABC): + @abstractmethod + def detect_faces( + self, + img: Union[np.ndarray, List[np.ndarray]] + ) -> Union[List["FacialAreaRegion"], List[List["FacialAreaRegion"]]]: + """ + Interface for detect and align faces in a batch of images + + Args: + img (Union[np.ndarray, List[np.ndarray]]): + Pre-loaded image as numpy array or a list of those + + Returns: + results (Union[List[List[FacialAreaRegion]], List[FacialAreaRegion]]): + A list or a list of lists of FacialAreaRegion objects + where each object contains: + + - 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 respectively with respect to the person + instead of observer. + """ + pass + # pylint: disable=unnecessary-pass, too-few-public-methods, too-many-instance-attributes @dataclass @@ -49,56 +78,3 @@ class DetectedFace: img: np.ndarray facial_area: FacialAreaRegion confidence: float - - -# Notice that all facial detector models must be inherited from this class - -class Detector(ABC): - - def detect_faces( - self, - img: Union[np.ndarray, List[np.ndarray]], - ) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]: - """ - Detect and align faces in an image or a list of images - - Args: - img (Union[np.ndarray, List[np.ndarray]]): - pre-loaded image as numpy array or a list of those - - Returns: - results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): - A list or a list of lists of FacialAreaRegion objects - """ - is_batched_input = isinstance(img, list) - if not is_batched_input: - img = [img] - results = [self._process_single_image(single_img) for single_img in img] - if not is_batched_input: - return results[0] - return results - - def _process_single_image( - self, - img: np.ndarray - ) -> List[FacialAreaRegion]: - """ - Interface for detect and align faces in a single image - - Args: - img (Union[np.ndarray, List[np.ndarray]]): - Pre-loaded image as numpy array or a list of those - - Returns: - results (Union[List[List[FacialAreaRegion]], List[FacialAreaRegion]]): - A list or a list of lists of FacialAreaRegion objects - where each object contains: - - - 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 respectively with respect to the person - instead of observer. - """ - raise NotImplementedError( - "Subclasses that do not implement batch detection must implement this method" - ) diff --git a/deepface/models/face_detection/CenterFace.py b/deepface/models/face_detection/CenterFace.py index e5ef2be..523f2bf 100644 --- a/deepface/models/face_detection/CenterFace.py +++ b/deepface/models/face_detection/CenterFace.py @@ -1,6 +1,6 @@ # built-in dependencies import os -from typing import List +from typing import List, Union # 3rd party dependencies import numpy as np @@ -34,12 +34,35 @@ class CenterFaceClient(Detector): return CenterFace(weight_path=weights_path) - def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]: + def detect_faces( + self, + img: Union[np.ndarray, List[np.ndarray]], + ) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]: + """ + Detect and align face with CenterFace + + Args: + img (Union[np.ndarray, List[np.ndarray]]): + pre-loaded image as numpy array or a list of those + + Returns: + results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): + A list or a list of lists of FacialAreaRegion objects + """ + is_batched_input = isinstance(img, list) + if not is_batched_input: + img = [img] + results = [self._process_single_image(single_img) for single_img in img] + if not is_batched_input: + return results[0] + return results + + def _process_single_image(self, single_img: np.ndarray) -> List[FacialAreaRegion]: """ Helper function to detect faces in a single image. Args: - img (np.ndarray): pre-loaded image as numpy array + single_img (np.ndarray): pre-loaded image as numpy array Returns: results (List[FacialAreaRegion]): A list of FacialAreaRegion objects @@ -53,7 +76,7 @@ class CenterFaceClient(Detector): # img, img.shape[0], img.shape[1], threshold=threshold # ) detections, landmarks = self.build_model().forward( - img, img.shape[0], img.shape[1], threshold=threshold + single_img, single_img.shape[0], single_img.shape[1], threshold=threshold ) for i, detection in enumerate(detections): diff --git a/deepface/models/face_detection/Dlib.py b/deepface/models/face_detection/Dlib.py index e8afce2..254a32b 100644 --- a/deepface/models/face_detection/Dlib.py +++ b/deepface/models/face_detection/Dlib.py @@ -1,5 +1,5 @@ # built-in dependencies -from typing import List +from typing import List, Union # 3rd party dependencies import numpy as np @@ -47,6 +47,29 @@ class DlibClient(Detector): detector["sp"] = sp return detector + def detect_faces( + self, + img: Union[np.ndarray, List[np.ndarray]], + ) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]: + """ + Detect and align face with dlib + + Args: + img (Union[np.ndarray, List[np.ndarray]]): + pre-loaded image as numpy array or a list of those + + Returns: + results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): + A list or a list of lists of FacialAreaRegion objects + """ + is_batched_input = isinstance(img, list) + if not is_batched_input: + img = [img] + results = [self._process_single_image(single_img) for single_img in img] + if not is_batched_input: + return results[0] + return results + def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]: """ Helper function to detect faces in a single image. diff --git a/deepface/models/face_detection/FastMtCnn.py b/deepface/models/face_detection/FastMtCnn.py index 2499dc7..7d5ccf5 100644 --- a/deepface/models/face_detection/FastMtCnn.py +++ b/deepface/models/face_detection/FastMtCnn.py @@ -17,6 +17,29 @@ class FastMtCnnClient(Detector): def __init__(self): self.model = self.build_model() + def detect_faces( + self, + img: Union[np.ndarray, List[np.ndarray]], + ) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]: + """ + Detect and align face with mtcnn + + Args: + img (Union[np.ndarray, List[np.ndarray]]): + pre-loaded image as numpy array or a list of those + + Returns: + results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): + A list or a list of lists of FacialAreaRegion objects + """ + is_batched_input = isinstance(img, list) + if not is_batched_input: + img = [img] + results = [self._process_single_image(single_img) for single_img in img] + if not is_batched_input: + return results[0] + return results + def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]: """ Helper function to detect faces in a single image. diff --git a/deepface/models/face_detection/MediaPipe.py b/deepface/models/face_detection/MediaPipe.py index 61bb0b5..9fcdbbc 100644 --- a/deepface/models/face_detection/MediaPipe.py +++ b/deepface/models/face_detection/MediaPipe.py @@ -1,6 +1,6 @@ # built-in dependencies import os -from typing import Any, List +from typing import Any, List, Union # 3rd party dependencies import numpy as np @@ -43,6 +43,29 @@ class MediaPipeClient(Detector): ) return face_detection + def detect_faces( + self, + img: Union[np.ndarray, List[np.ndarray]], + ) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]: + """ + Detect and align face with mediapipe + + Args: + img (Union[np.ndarray, List[np.ndarray]]): + pre-loaded image as numpy array or a list of those + + Returns: + results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): + A list or a list of lists of FacialAreaRegion objects + """ + is_batched_input = isinstance(img, list) + if not is_batched_input: + img = [img] + results = [self._process_single_image(single_img) for single_img in img] + if not is_batched_input: + return results[0] + return results + def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]: """ Helper function to detect faces in a single image. diff --git a/deepface/models/face_detection/OpenCv.py b/deepface/models/face_detection/OpenCv.py index 801ff11..c86f2da 100644 --- a/deepface/models/face_detection/OpenCv.py +++ b/deepface/models/face_detection/OpenCv.py @@ -1,6 +1,6 @@ # built-in dependencies import os -from typing import Any, List +from typing import Any, List, Union import logging # 3rd party dependencies @@ -45,48 +45,65 @@ class OpenCvClient(Detector): ) return supports_batch_detection - def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]: + def detect_faces( + self, + img: Union[np.ndarray, List[np.ndarray]] + ) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]: """ - Helper function to detect faces in a single image. + Detect and align face with opencv Args: - img (np.ndarray): pre-loaded image as numpy array + img (Union[np.ndarray, List[np.ndarray]]): + Pre-loaded image as numpy array or a list of those Returns: - results (List[FacialAreaRegion]): A list of FacialAreaRegion objects + results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): + A list or a list of lists of FacialAreaRegion objects """ - resp = [] - detected_face = None - faces = [] - try: - faces, _, scores = self.model["face_detector"].detectMultiScale3( - img, 1.1, 10, outputRejectLevels=True - ) - except: - pass + if isinstance(img, np.ndarray): + imgs = [img] + elif self.supports_batch_detection: + imgs = img + else: + return [self.detect_faces(single_img) for single_img in img] - if len(faces) > 0: - for (x, y, w, h), confidence in zip(faces, scores): - detected_face = img[int(y):int(y + h), int(x):int(x + w)] - left_eye, right_eye = self.find_eyes(img=detected_face) + batch_results = [] - if left_eye is not None: - left_eye = (int(x + left_eye[0]), int(y + left_eye[1])) - if right_eye is not None: - right_eye = (int(x + right_eye[0]), int(y + right_eye[1])) - - facial_area = FacialAreaRegion( - x=x, - y=y, - w=w, - h=h, - left_eye=left_eye, - right_eye=right_eye, - confidence=(100 - confidence) / 100, + for single_img in imgs: + resp = [] + detected_face = None + faces = [] + try: + faces, _, scores = self.model["face_detector"].detectMultiScale3( + single_img, 1.1, 10, outputRejectLevels=True ) - resp.append(facial_area) + except: + pass - return resp + if len(faces) > 0: + for (x, y, w, h), confidence in zip(faces, scores): + detected_face = single_img[int(y):int(y + h), int(x):int(x + w)] + left_eye, right_eye = self.find_eyes(img=detected_face) + + if left_eye is not None: + left_eye = (int(x + left_eye[0]), int(y + left_eye[1])) + if right_eye is not None: + right_eye = (int(x + right_eye[0]), int(y + right_eye[1])) + + facial_area = FacialAreaRegion( + x=x, + y=y, + w=w, + h=h, + left_eye=left_eye, + right_eye=right_eye, + confidence=(100 - confidence) / 100, + ) + resp.append(facial_area) + + batch_results.append(resp) + + return batch_results if len(batch_results) > 1 else batch_results[0] def find_eyes(self, img: np.ndarray) -> tuple: """ diff --git a/deepface/models/face_detection/Ssd.py b/deepface/models/face_detection/Ssd.py index 381d664..a620f96 100644 --- a/deepface/models/face_detection/Ssd.py +++ b/deepface/models/face_detection/Ssd.py @@ -1,5 +1,5 @@ # built-in dependencies -from typing import List +from typing import List, Union from enum import IntEnum # 3rd party dependencies @@ -54,25 +54,48 @@ class SsdClient(Detector): return {"face_detector": face_detector, "opencv_module": OpenCv.OpenCvClient()} - def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]: + def detect_faces( + self, + img: Union[np.ndarray, List[np.ndarray]] + ) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]: + """ + Detect and align faces with ssd in a batch of images + + Args: + img (Union[np.ndarray, List[np.ndarray]]): + Pre-loaded image as numpy array or a list of those + + Returns: + results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): + A list or a list of lists of FacialAreaRegion objects + """ + is_batched_input = isinstance(img, list) + if not is_batched_input: + img = [img] + results = [self._process_single_image(single_img) for single_img in img] + if not is_batched_input: + return results[0] + return results + + def _process_single_image(self, single_img: np.ndarray) -> List[FacialAreaRegion]: """ Helper function to detect faces in a single image. Args: - img (np.ndarray): Pre-loaded image as numpy array + single_img (np.ndarray): Pre-loaded image as numpy array Returns: results (List[FacialAreaRegion]): A list of FacialAreaRegion objects """ # Because cv2.dnn.blobFromImage expects CV_8U (8-bit unsigned integer) values - if img.dtype != np.uint8: - img = img.astype(np.uint8) + if single_img.dtype != np.uint8: + single_img = single_img.astype(np.uint8) opencv_module: OpenCv.OpenCvClient = self.model["opencv_module"] target_size = (300, 300) - original_size = img.shape - current_img = cv2.resize(img, target_size) + original_size = single_img.shape + current_img = cv2.resize(single_img, target_size) aspect_ratio_x = original_size[1] / target_size[1] aspect_ratio_y = original_size[0] / target_size[0] @@ -109,7 +132,7 @@ class SsdClient(Detector): for face in faces: confidence = float(face[ssd_labels.confidence]) x, y, w, h = map(int, face[margins]) - detected_face = img[y : y + h, x : x + w] + detected_face = single_img[y : y + h, x : x + w] left_eye, right_eye = opencv_module.find_eyes(detected_face) diff --git a/deepface/models/face_detection/Yolo.py b/deepface/models/face_detection/Yolo.py index 300bc45..cbb8879 100644 --- a/deepface/models/face_detection/Yolo.py +++ b/deepface/models/face_detection/Yolo.py @@ -78,8 +78,7 @@ class YoloDetectorClient(Detector): A list of lists of FacialAreaRegion objects for each image or a list of FacialAreaRegion objects """ - is_batched_input = isinstance(img, list) - if not is_batched_input: + if not isinstance(img, list): img = [img] all_results = [] @@ -143,7 +142,7 @@ class YoloDetectorClient(Detector): all_results.append(resp) - if not is_batched_input: + if len(all_results) == 1: return all_results[0] return all_results diff --git a/deepface/models/face_detection/YuNet.py b/deepface/models/face_detection/YuNet.py index 4b4b939..93e65a8 100644 --- a/deepface/models/face_detection/YuNet.py +++ b/deepface/models/face_detection/YuNet.py @@ -1,6 +1,6 @@ # built-in dependencies import os -from typing import Any, List +from typing import Any, List, Union # 3rd party dependencies import cv2 @@ -57,6 +57,24 @@ class YuNetClient(Detector): ) from err return face_detector + def detect_faces(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]: + """ + Detect and align face with yunet + + Args: + img (Union[np.ndarray, List[np.ndarray]]): pre-loaded image as numpy array or a list of those + + Returns: + results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): A list or a list of lists of FacialAreaRegion objects + """ + is_batched_input = isinstance(img, list) + if not is_batched_input: + img = [img] + results = [self._process_single_image(single_img) for single_img in img] + if not is_batched_input: + return results[0] + return results + def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]: """ Helper function to detect faces in a single image.