mirror of
https://github.com/serengil/deepface.git
synced 2025-06-07 12:05:22 +00:00
Merge 3111a2895a0e2561403ea193dd2f75f069160d57 into df1b6ab6fe431f126afe7ff3baa7fe394a967f0a
This commit is contained in:
commit
ab84683bbe
@ -521,7 +521,7 @@ def stream(
|
|||||||
|
|
||||||
|
|
||||||
def extract_faces(
|
def extract_faces(
|
||||||
img_path: Union[str, np.ndarray, IO[bytes]],
|
img_path: Union[str, np.ndarray, IO[bytes], Sequence[Union[str, np.ndarray, IO[bytes]]]],
|
||||||
detector_backend: str = "opencv",
|
detector_backend: str = "opencv",
|
||||||
enforce_detection: bool = True,
|
enforce_detection: bool = True,
|
||||||
align: bool = True,
|
align: bool = True,
|
||||||
@ -530,14 +530,14 @@ def extract_faces(
|
|||||||
color_face: str = "rgb",
|
color_face: str = "rgb",
|
||||||
normalize_face: bool = True,
|
normalize_face: bool = True,
|
||||||
anti_spoofing: bool = False,
|
anti_spoofing: bool = False,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]]:
|
||||||
"""
|
"""
|
||||||
Extract faces from a given image
|
Extract faces from a given image or sequence of images.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img_path (str or np.ndarray or IO[bytes]): Path to the first image. Accepts exact image path
|
img_path (Union[str, np.ndarray, IO[bytes], Sequence[Union[str, np.ndarray, IO[bytes]]]]):
|
||||||
as a string, numpy array (BGR), a file object that supports at least `.read` and is
|
Path(s) to the image(s). Accepts a string path, a numpy array (BGR), a file object
|
||||||
opened in binary mode, or base64 encoded images.
|
that supports at least `.read` and is opened in binary mode, or base64 encoded images.
|
||||||
|
|
||||||
detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
|
detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
|
||||||
'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m',
|
'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m',
|
||||||
@ -562,7 +562,8 @@ def extract_faces(
|
|||||||
anti_spoofing (boolean): Flag to enable anti spoofing (default is False).
|
anti_spoofing (boolean): Flag to enable anti spoofing (default is False).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary contains:
|
results (Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]):
|
||||||
|
A list or a list of lists of dictionaries, where each dictionary contains:
|
||||||
|
|
||||||
- "face" (np.ndarray): The detected face as a NumPy array.
|
- "face" (np.ndarray): The detected face as a NumPy array.
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import List, Tuple, Optional
|
from typing import List, Tuple, Optional, Union
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -9,15 +9,20 @@ import numpy as np
|
|||||||
# pylint: disable=unnecessary-pass, too-few-public-methods, too-many-instance-attributes
|
# pylint: disable=unnecessary-pass, too-few-public-methods, too-many-instance-attributes
|
||||||
class Detector(ABC):
|
class Detector(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def detect_faces(self, img: np.ndarray) -> List["FacialAreaRegion"]:
|
def detect_faces(
|
||||||
|
self,
|
||||||
|
img: Union[np.ndarray, List[np.ndarray]]
|
||||||
|
) -> Union[List["FacialAreaRegion"], List[List["FacialAreaRegion"]]]:
|
||||||
"""
|
"""
|
||||||
Interface for detect and align face
|
Interface for detect and align faces in a batch of images
|
||||||
|
|
||||||
Args:
|
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:
|
Returns:
|
||||||
results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
|
results (Union[List[List[FacialAreaRegion]], List[FacialAreaRegion]]):
|
||||||
|
A list or a list of lists of FacialAreaRegion objects
|
||||||
where each object contains:
|
where each object contains:
|
||||||
|
|
||||||
- facial_area (FacialAreaRegion): The facial area region represented
|
- facial_area (FacialAreaRegion): The facial area region represented
|
||||||
@ -28,6 +33,7 @@ class Detector(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unnecessary-pass, too-few-public-methods, too-many-instance-attributes
|
||||||
@dataclass
|
@dataclass
|
||||||
class FacialAreaRegion:
|
class FacialAreaRegion:
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
import os
|
import os
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -34,12 +34,35 @@ class CenterFaceClient(Detector):
|
|||||||
|
|
||||||
return CenterFace(weight_path=weights_path)
|
return CenterFace(weight_path=weights_path)
|
||||||
|
|
||||||
def detect_faces(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
|
Detect and align face with CenterFace
|
||||||
|
|
||||||
Args:
|
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 (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:
|
||||||
|
single_img (np.ndarray): pre-loaded image as numpy array
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
|
results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
|
||||||
@ -53,7 +76,7 @@ class CenterFaceClient(Detector):
|
|||||||
# img, img.shape[0], img.shape[1], threshold=threshold
|
# img, img.shape[0], img.shape[1], threshold=threshold
|
||||||
# )
|
# )
|
||||||
detections, landmarks = self.build_model().forward(
|
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):
|
for i, detection in enumerate(detections):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -47,10 +47,33 @@ class DlibClient(Detector):
|
|||||||
detector["sp"] = sp
|
detector["sp"] = sp
|
||||||
return detector
|
return detector
|
||||||
|
|
||||||
def detect_faces(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 dlib
|
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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img (np.ndarray): pre-loaded image as numpy array
|
img (np.ndarray): pre-loaded image as numpy array
|
||||||
|
|
||||||
|
@ -17,10 +17,33 @@ class FastMtCnnClient(Detector):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.model = self.build_model()
|
self.model = self.build_model()
|
||||||
|
|
||||||
def detect_faces(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 mtcnn
|
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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img (np.ndarray): pre-loaded image as numpy array
|
img (np.ndarray): pre-loaded image as numpy array
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
import os
|
import os
|
||||||
from typing import Any, List
|
from typing import Any, List, Union
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -43,10 +43,33 @@ class MediaPipeClient(Detector):
|
|||||||
)
|
)
|
||||||
return face_detection
|
return face_detection
|
||||||
|
|
||||||
def detect_faces(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 mediapipe
|
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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img (np.ndarray): pre-loaded image as numpy array
|
img (np.ndarray): pre-loaded image as numpy array
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
from typing import List
|
import logging
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -8,6 +9,8 @@ from mtcnn import MTCNN
|
|||||||
# project dependencies
|
# project dependencies
|
||||||
from deepface.models.Detector import Detector, FacialAreaRegion
|
from deepface.models.Detector import Detector, FacialAreaRegion
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class MtCnnClient(Detector):
|
class MtCnnClient(Detector):
|
||||||
"""
|
"""
|
||||||
@ -16,28 +19,43 @@ class MtCnnClient(Detector):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.model = MTCNN()
|
self.model = MTCNN()
|
||||||
|
self.supports_batch_detection = self._supports_batch_detection()
|
||||||
|
|
||||||
def detect_faces(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 mtcnn
|
Detect and align faces with mtcnn for a list of images
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img (np.ndarray): pre-loaded image as numpy array
|
imgs (Union[np.ndarray, List[np.ndarray]]):
|
||||||
|
pre-loaded image as numpy array or a list of those
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
|
results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]):
|
||||||
|
A list of FacialAreaRegion objects for a single image
|
||||||
|
or a list of lists of FacialAreaRegion objects for each image
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
is_batched_input = isinstance(img, list)
|
||||||
|
if not is_batched_input:
|
||||||
|
img = [img]
|
||||||
|
|
||||||
resp = []
|
resp = []
|
||||||
|
|
||||||
# mtcnn expects RGB but OpenCV read BGR
|
# mtcnn expects RGB but OpenCV read BGR
|
||||||
# img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
# img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||||
img_rgb = img[:, :, ::-1]
|
img_rgb = [img[:, :, ::-1] for img in img]
|
||||||
|
if self.supports_batch_detection:
|
||||||
detections = self.model.detect_faces(img_rgb)
|
detections = self.model.detect_faces(img_rgb)
|
||||||
|
else:
|
||||||
|
detections = [self.model.detect_faces(single_img) for single_img in img_rgb]
|
||||||
|
|
||||||
if detections is not None and len(detections) > 0:
|
for image_detections in detections:
|
||||||
|
image_resp = []
|
||||||
for current_detection in detections:
|
if image_detections is not None and len(image_detections) > 0:
|
||||||
|
for current_detection in image_detections:
|
||||||
x, y, w, h = current_detection["box"]
|
x, y, w, h = current_detection["box"]
|
||||||
confidence = current_detection["confidence"]
|
confidence = current_detection["confidence"]
|
||||||
# mtcnn detector assigns left eye with respect to the observer
|
# mtcnn detector assigns left eye with respect to the observer
|
||||||
@ -55,6 +73,17 @@ class MtCnnClient(Detector):
|
|||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
)
|
)
|
||||||
|
|
||||||
resp.append(facial_area)
|
image_resp.append(facial_area)
|
||||||
|
|
||||||
|
resp.append(image_resp)
|
||||||
|
|
||||||
|
if not is_batched_input:
|
||||||
|
return resp[0]
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def _supports_batch_detection(self) -> bool:
|
||||||
|
logger.warning(
|
||||||
|
"Batch detection is disabled for mtcnn by default "
|
||||||
|
"since the results are not consistent with single image detection. "
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
import os
|
import os
|
||||||
from typing import Any, List
|
from typing import Any, List, Union
|
||||||
|
import logging
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
import cv2
|
import cv2
|
||||||
@ -9,6 +10,7 @@ import numpy as np
|
|||||||
#project dependencies
|
#project dependencies
|
||||||
from deepface.models.Detector import Detector, FacialAreaRegion
|
from deepface.models.Detector import Detector, FacialAreaRegion
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class OpenCvClient(Detector):
|
class OpenCvClient(Detector):
|
||||||
"""
|
"""
|
||||||
@ -17,6 +19,7 @@ class OpenCvClient(Detector):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.model = self.build_model()
|
self.model = self.build_model()
|
||||||
|
self.supports_batch_detection = self._supports_batch_detection()
|
||||||
|
|
||||||
def build_model(self):
|
def build_model(self):
|
||||||
"""
|
"""
|
||||||
@ -29,38 +32,53 @@ class OpenCvClient(Detector):
|
|||||||
detector["eye_detector"] = self.__build_cascade("haarcascade_eye")
|
detector["eye_detector"] = self.__build_cascade("haarcascade_eye")
|
||||||
return detector
|
return detector
|
||||||
|
|
||||||
def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
|
def _supports_batch_detection(self) -> bool:
|
||||||
|
logger.warning(
|
||||||
|
"Batch detection is disabled for opencv by default "
|
||||||
|
"since the results are not consistent with single image detection. "
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def detect_faces(
|
||||||
|
self,
|
||||||
|
img: Union[np.ndarray, List[np.ndarray]]
|
||||||
|
) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]:
|
||||||
"""
|
"""
|
||||||
Detect and align face with opencv
|
Detect and align face with opencv
|
||||||
|
|
||||||
Args:
|
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:
|
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
|
||||||
"""
|
"""
|
||||||
|
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]
|
||||||
|
|
||||||
|
batch_results = []
|
||||||
|
|
||||||
|
for single_img in imgs:
|
||||||
resp = []
|
resp = []
|
||||||
|
|
||||||
detected_face = None
|
detected_face = None
|
||||||
|
|
||||||
faces = []
|
faces = []
|
||||||
try:
|
try:
|
||||||
# faces = detector["face_detector"].detectMultiScale(img, 1.3, 5)
|
|
||||||
|
|
||||||
# note that, by design, opencv's haarcascade scores are >0 but not capped at 1
|
|
||||||
faces, _, scores = self.model["face_detector"].detectMultiScale3(
|
faces, _, scores = self.model["face_detector"].detectMultiScale3(
|
||||||
img, 1.1, 10, outputRejectLevels=True
|
single_img, 1.1, 10, outputRejectLevels=True
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if len(faces) > 0:
|
if len(faces) > 0:
|
||||||
for (x, y, w, h), confidence in zip(faces, scores):
|
for (x, y, w, h), confidence in zip(faces, scores):
|
||||||
detected_face = img[int(y) : int(y + h), int(x) : int(x + w)]
|
detected_face = single_img[int(y):int(y + h), int(x):int(x + w)]
|
||||||
left_eye, right_eye = self.find_eyes(img=detected_face)
|
left_eye, right_eye = self.find_eyes(img=detected_face)
|
||||||
|
|
||||||
# eyes found in the detected face instead image itself
|
|
||||||
# detected face's coordinates should be added
|
|
||||||
if left_eye is not None:
|
if left_eye is not None:
|
||||||
left_eye = (int(x + left_eye[0]), int(y + left_eye[1]))
|
left_eye = (int(x + left_eye[0]), int(y + left_eye[1]))
|
||||||
if right_eye is not None:
|
if right_eye is not None:
|
||||||
@ -77,7 +95,9 @@ class OpenCvClient(Detector):
|
|||||||
)
|
)
|
||||||
resp.append(facial_area)
|
resp.append(facial_area)
|
||||||
|
|
||||||
return resp
|
batch_results.append(resp)
|
||||||
|
|
||||||
|
return batch_results if len(batch_results) > 1 else batch_results[0]
|
||||||
|
|
||||||
def find_eyes(self, img: np.ndarray) -> tuple:
|
def find_eyes(self, img: np.ndarray) -> tuple:
|
||||||
"""
|
"""
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -13,23 +13,34 @@ class RetinaFaceClient(Detector):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.model = rf.build_model()
|
self.model = rf.build_model()
|
||||||
|
|
||||||
def detect_faces(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 retinaface
|
Detect and align faces with retinaface in a batch of images
|
||||||
|
|
||||||
Args:
|
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:
|
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
|
||||||
"""
|
"""
|
||||||
|
is_batched_input = isinstance(img, list)
|
||||||
|
if not is_batched_input:
|
||||||
|
imgs = [img]
|
||||||
|
else:
|
||||||
|
imgs = img
|
||||||
|
|
||||||
|
batch_results = []
|
||||||
|
|
||||||
|
for single_img in imgs:
|
||||||
resp = []
|
resp = []
|
||||||
|
obj = rf.detect_faces(single_img, model=self.model, threshold=0.9)
|
||||||
|
|
||||||
obj = rf.detect_faces(img, model=self.model, threshold=0.9)
|
if isinstance(obj, dict):
|
||||||
|
|
||||||
if not isinstance(obj, dict):
|
|
||||||
return resp
|
|
||||||
|
|
||||||
for face_idx in obj.keys():
|
for face_idx in obj.keys():
|
||||||
identity = obj[face_idx]
|
identity = obj[face_idx]
|
||||||
detection = identity["facial_area"]
|
detection = identity["facial_area"]
|
||||||
@ -39,16 +50,12 @@ class RetinaFaceClient(Detector):
|
|||||||
x = detection[0]
|
x = detection[0]
|
||||||
w = detection[2] - x
|
w = detection[2] - x
|
||||||
|
|
||||||
# retinaface sets left and right eyes with respect to the person
|
left_eye = tuple(int(i) for i in identity["landmarks"]["left_eye"])
|
||||||
left_eye = identity["landmarks"]["left_eye"]
|
right_eye = tuple(int(i) for i in identity["landmarks"]["right_eye"])
|
||||||
right_eye = identity["landmarks"]["right_eye"]
|
|
||||||
nose = identity["landmarks"].get("nose")
|
nose = identity["landmarks"].get("nose")
|
||||||
mouth_right = identity["landmarks"].get("mouth_right")
|
mouth_right = identity["landmarks"].get("mouth_right")
|
||||||
mouth_left = identity["landmarks"].get("mouth_left")
|
mouth_left = identity["landmarks"].get("mouth_left")
|
||||||
|
|
||||||
# eyes are list of float, need to cast them tuple of int
|
|
||||||
left_eye = tuple(int(i) for i in left_eye)
|
|
||||||
right_eye = tuple(int(i) for i in right_eye)
|
|
||||||
if nose is not None:
|
if nose is not None:
|
||||||
nose = tuple(int(i) for i in nose)
|
nose = tuple(int(i) for i in nose)
|
||||||
if mouth_right is not None:
|
if mouth_right is not None:
|
||||||
@ -73,4 +80,8 @@ class RetinaFaceClient(Detector):
|
|||||||
|
|
||||||
resp.append(facial_area)
|
resp.append(facial_area)
|
||||||
|
|
||||||
return resp
|
batch_results.append(resp)
|
||||||
|
|
||||||
|
if not is_batched_input:
|
||||||
|
return batch_results[0]
|
||||||
|
return batch_results
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
@ -54,28 +54,48 @@ class SsdClient(Detector):
|
|||||||
|
|
||||||
return {"face_detector": face_detector, "opencv_module": OpenCv.OpenCvClient()}
|
return {"face_detector": face_detector, "opencv_module": OpenCv.OpenCvClient()}
|
||||||
|
|
||||||
def detect_faces(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 ssd
|
Detect and align faces with ssd in a batch of images
|
||||||
|
|
||||||
Args:
|
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 (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:
|
||||||
|
single_img (np.ndarray): Pre-loaded image as numpy array
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
|
results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Because cv2.dnn.blobFromImage expects CV_8U (8-bit unsigned integer) values
|
# Because cv2.dnn.blobFromImage expects CV_8U (8-bit unsigned integer) values
|
||||||
if img.dtype != np.uint8:
|
if single_img.dtype != np.uint8:
|
||||||
img = img.astype(np.uint8)
|
single_img = single_img.astype(np.uint8)
|
||||||
|
|
||||||
opencv_module: OpenCv.OpenCvClient = self.model["opencv_module"]
|
opencv_module: OpenCv.OpenCvClient = self.model["opencv_module"]
|
||||||
|
|
||||||
target_size = (300, 300)
|
target_size = (300, 300)
|
||||||
|
original_size = single_img.shape
|
||||||
original_size = img.shape
|
current_img = cv2.resize(single_img, target_size)
|
||||||
|
|
||||||
current_img = cv2.resize(img, target_size)
|
|
||||||
|
|
||||||
aspect_ratio_x = original_size[1] / target_size[1]
|
aspect_ratio_x = original_size[1] / target_size[1]
|
||||||
aspect_ratio_y = original_size[0] / target_size[0]
|
aspect_ratio_y = original_size[0] / target_size[0]
|
||||||
@ -112,7 +132,7 @@ class SsdClient(Detector):
|
|||||||
for face in faces:
|
for face in faces:
|
||||||
confidence = float(face[ssd_labels.confidence])
|
confidence = float(face[ssd_labels.confidence])
|
||||||
x, y, w, h = map(int, face[margins])
|
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)
|
left_eye, right_eye = opencv_module.find_eyes(detected_face)
|
||||||
|
|
||||||
@ -133,4 +153,5 @@ class SsdClient(Detector):
|
|||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
)
|
)
|
||||||
resp.append(facial_area)
|
resp.append(facial_area)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
import os
|
import os
|
||||||
from typing import List, Any
|
from typing import List, Any, Union
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
@ -62,25 +62,38 @@ class YoloDetectorClient(Detector):
|
|||||||
# Return face_detector
|
# Return face_detector
|
||||||
return YOLO(weight_file)
|
return YOLO(weight_file)
|
||||||
|
|
||||||
def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
|
def detect_faces(
|
||||||
|
self,
|
||||||
|
img: Union[np.ndarray, List[np.ndarray]]
|
||||||
|
) -> Union[List[List[FacialAreaRegion]], List[FacialAreaRegion]]:
|
||||||
"""
|
"""
|
||||||
Detect and align face with yolo
|
Detect and align faces in a batch of images with yolo
|
||||||
|
|
||||||
Args:
|
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:
|
Returns:
|
||||||
results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
|
results (Union[List[List[FacialAreaRegion]], List[FacialAreaRegion]]):
|
||||||
|
A list of lists of FacialAreaRegion objects
|
||||||
|
for each image or a list of FacialAreaRegion objects
|
||||||
"""
|
"""
|
||||||
resp = []
|
if not isinstance(img, list):
|
||||||
|
img = [img]
|
||||||
|
|
||||||
# Detect faces
|
all_results = []
|
||||||
results = self.model.predict(
|
|
||||||
|
# Detect faces for all images
|
||||||
|
results_list = self.model.predict(
|
||||||
img,
|
img,
|
||||||
verbose=False,
|
verbose=False,
|
||||||
show=False,
|
show=False,
|
||||||
conf=float(os.getenv("YOLO_MIN_DETECTION_CONFIDENCE", "0.25")),
|
conf=float(os.getenv("YOLO_MIN_DETECTION_CONFIDENCE", "0.25")),
|
||||||
)[0]
|
)
|
||||||
|
|
||||||
|
# Iterate over each image's results
|
||||||
|
for results in results_list:
|
||||||
|
resp = []
|
||||||
|
|
||||||
# For each face, extract the bounding box, the landmarks and confidence
|
# For each face, extract the bounding box, the landmarks and confidence
|
||||||
for result in results:
|
for result in results:
|
||||||
@ -104,9 +117,17 @@ class YoloDetectorClient(Detector):
|
|||||||
left_eye = result.keypoints.xy[0][1].tolist()
|
left_eye = result.keypoints.xy[0][1].tolist()
|
||||||
|
|
||||||
# eyes are list of float, need to cast them tuple of int
|
# eyes are list of float, need to cast them tuple of int
|
||||||
left_eye = tuple(int(i) for i in left_eye)
|
# Ensure eyes are tuples of exactly two integers or None
|
||||||
right_eye = tuple(int(i) for i in right_eye)
|
left_eye = (
|
||||||
|
tuple(map(int, left_eye[:2]))
|
||||||
|
if left_eye and len(left_eye) == 2
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
right_eye = (
|
||||||
|
tuple(map(int, right_eye[:2]))
|
||||||
|
if right_eye and len(right_eye) == 2
|
||||||
|
else None
|
||||||
|
)
|
||||||
x, y, w, h = int(x - w / 2), int(y - h / 2), int(w), int(h)
|
x, y, w, h = int(x - w / 2), int(y - h / 2), int(w), int(h)
|
||||||
facial_area = FacialAreaRegion(
|
facial_area = FacialAreaRegion(
|
||||||
x=x,
|
x=x,
|
||||||
@ -119,7 +140,11 @@ class YoloDetectorClient(Detector):
|
|||||||
)
|
)
|
||||||
resp.append(facial_area)
|
resp.append(facial_area)
|
||||||
|
|
||||||
return resp
|
all_results.append(resp)
|
||||||
|
|
||||||
|
if len(all_results) == 1:
|
||||||
|
return all_results[0]
|
||||||
|
return all_results
|
||||||
|
|
||||||
|
|
||||||
class YoloDetectorClientV8n(YoloDetectorClient):
|
class YoloDetectorClientV8n(YoloDetectorClient):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
import os
|
import os
|
||||||
from typing import Any, List
|
from typing import Any, List, Union
|
||||||
|
|
||||||
# 3rd party dependencies
|
# 3rd party dependencies
|
||||||
import cv2
|
import cv2
|
||||||
@ -57,10 +57,28 @@ class YuNetClient(Detector):
|
|||||||
) from err
|
) from err
|
||||||
return face_detector
|
return face_detector
|
||||||
|
|
||||||
def detect_faces(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 yunet
|
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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img (np.ndarray): pre-loaded image as numpy array
|
img (np.ndarray): pre-loaded image as numpy array
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# built-in dependencies
|
# built-in dependencies
|
||||||
from typing import Any, Dict, IO, List, Tuple, Union, Optional
|
from typing import Any, Dict, IO, List, Tuple, Union, Optional, Sequence
|
||||||
|
|
||||||
# 3rd part dependencies
|
# 3rd part dependencies
|
||||||
from heapq import nlargest
|
from heapq import nlargest
|
||||||
@ -19,7 +19,7 @@ logger = Logger()
|
|||||||
|
|
||||||
|
|
||||||
def extract_faces(
|
def extract_faces(
|
||||||
img_path: Union[str, np.ndarray, IO[bytes]],
|
img_path: Union[Sequence[Union[str, np.ndarray, IO[bytes]]], str, np.ndarray, IO[bytes]],
|
||||||
detector_backend: str = "opencv",
|
detector_backend: str = "opencv",
|
||||||
enforce_detection: bool = True,
|
enforce_detection: bool = True,
|
||||||
align: bool = True,
|
align: bool = True,
|
||||||
@ -29,13 +29,14 @@ def extract_faces(
|
|||||||
normalize_face: bool = True,
|
normalize_face: bool = True,
|
||||||
anti_spoofing: bool = False,
|
anti_spoofing: bool = False,
|
||||||
max_faces: Optional[int] = None,
|
max_faces: Optional[int] = None,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]]:
|
||||||
"""
|
"""
|
||||||
Extract faces from a given image
|
Extract faces from a given image or list of images
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img_path (str or np.ndarray or IO[bytes]): Path to the first image. Accepts exact image path
|
img_paths (List[str or np.ndarray or IO[bytes]] or str or np.ndarray or IO[bytes]):
|
||||||
as a string, numpy array (BGR), a file object that supports at least `.read` and is
|
Path(s) to the image(s) as a string,
|
||||||
|
numpy array (BGR), a file object that supports at least `.read` and is
|
||||||
opened in binary mode, or base64 encoded images.
|
opened in binary mode, or base64 encoded images.
|
||||||
|
|
||||||
detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
|
detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
|
||||||
@ -61,7 +62,8 @@ def extract_faces(
|
|||||||
anti_spoofing (boolean): Flag to enable anti spoofing (default is False).
|
anti_spoofing (boolean): Flag to enable anti spoofing (default is False).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
results (List[Dict[str, Any]]): A list of dictionaries, where each dictionary contains:
|
results (Union[List[Dict[str, Any]], List[List[Dict[str, Any]]]):
|
||||||
|
A list or list of lists of dictionaries, where each dictionary contains:
|
||||||
|
|
||||||
- "face" (np.ndarray): The detected face as a NumPy array in RGB format.
|
- "face" (np.ndarray): The detected face as a NumPy array in RGB format.
|
||||||
|
|
||||||
@ -80,30 +82,46 @@ def extract_faces(
|
|||||||
just available in the result only if anti_spoofing is set to True in input arguments.
|
just available in the result only if anti_spoofing is set to True in input arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
resp_objs = []
|
batched_input = (
|
||||||
|
(
|
||||||
|
isinstance(img_path, np.ndarray) and
|
||||||
|
img_path.ndim == 4
|
||||||
|
) or isinstance(img_path, list)
|
||||||
|
)
|
||||||
|
if not batched_input:
|
||||||
|
imgs_path = [img_path]
|
||||||
|
elif isinstance(img_path, np.ndarray):
|
||||||
|
imgs_path = [img_path[i] for i in range(img_path.shape[0])]
|
||||||
|
else:
|
||||||
|
imgs_path = img_path
|
||||||
|
|
||||||
|
all_images = []
|
||||||
|
img_names = []
|
||||||
|
|
||||||
|
for single_img_path in imgs_path:
|
||||||
# img might be path, base64 or numpy array. Convert it to numpy whatever it is.
|
# img might be path, base64 or numpy array. Convert it to numpy whatever it is.
|
||||||
img, img_name = image_utils.load_image(img_path)
|
img, img_name = image_utils.load_image(single_img_path)
|
||||||
|
|
||||||
if img is None:
|
if img is None:
|
||||||
raise ValueError(f"Exception while loading {img_name}")
|
raise ValueError(f"Exception while loading {img_name}")
|
||||||
|
|
||||||
height, width, _ = img.shape
|
all_images.append(img)
|
||||||
|
img_names.append(img_name)
|
||||||
|
|
||||||
base_region = FacialAreaRegion(x=0, y=0, w=width, h=height, confidence=0)
|
# Run detect_faces for all images at once
|
||||||
|
all_face_objs = detect_faces(
|
||||||
if detector_backend == "skip":
|
|
||||||
face_objs = [DetectedFace(img=img, facial_area=base_region, confidence=0)]
|
|
||||||
else:
|
|
||||||
face_objs = detect_faces(
|
|
||||||
detector_backend=detector_backend,
|
detector_backend=detector_backend,
|
||||||
img=img,
|
img=all_images,
|
||||||
align=align,
|
align=align,
|
||||||
expand_percentage=expand_percentage,
|
expand_percentage=expand_percentage,
|
||||||
max_faces=max_faces,
|
max_faces=max_faces,
|
||||||
)
|
)
|
||||||
|
|
||||||
# in case of no face found
|
all_resp_objs = []
|
||||||
|
|
||||||
|
for img, img_name, face_objs in zip(all_images, img_names, all_face_objs):
|
||||||
|
height, width, _ = img.shape
|
||||||
|
|
||||||
if len(face_objs) == 0 and enforce_detection is True:
|
if len(face_objs) == 0 and enforce_detection is True:
|
||||||
if img_name is not None:
|
if img_name is not None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -118,8 +136,10 @@ def extract_faces(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if len(face_objs) == 0 and enforce_detection is False:
|
if len(face_objs) == 0 and enforce_detection is False:
|
||||||
|
base_region = FacialAreaRegion(x=0, y=0, w=width, h=height, confidence=0)
|
||||||
face_objs = [DetectedFace(img=img, facial_area=base_region, confidence=0)]
|
face_objs = [DetectedFace(img=img, facial_area=base_region, confidence=0)]
|
||||||
|
|
||||||
|
img_resp_objs = []
|
||||||
for face_obj in face_objs:
|
for face_obj in face_objs:
|
||||||
current_img = face_obj.img
|
current_img = face_obj.img
|
||||||
current_region = face_obj.facial_area
|
current_region = face_obj.facial_area
|
||||||
@ -138,7 +158,10 @@ def extract_faces(
|
|||||||
elif color_face == "gray":
|
elif color_face == "gray":
|
||||||
current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2GRAY)
|
current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2GRAY)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"The color_face can be rgb, bgr or gray, but it is {color_face}.")
|
raise ValueError(
|
||||||
|
f"The color_face can be rgb, bgr or gray, "
|
||||||
|
f"but it is {color_face}."
|
||||||
|
)
|
||||||
|
|
||||||
if normalize_face:
|
if normalize_face:
|
||||||
current_img = current_img / 255 # normalize input in [0, 1]
|
current_img = current_img / 255 # normalize input in [0, 1]
|
||||||
@ -174,41 +197,43 @@ def extract_faces(
|
|||||||
|
|
||||||
if anti_spoofing is True:
|
if anti_spoofing is True:
|
||||||
antispoof_model = modeling.build_model(task="spoofing", model_name="Fasnet")
|
antispoof_model = modeling.build_model(task="spoofing", model_name="Fasnet")
|
||||||
is_real, antispoof_score = antispoof_model.analyze(img=img, facial_area=(x, y, w, h))
|
is_real, antispoof_score = antispoof_model.analyze(
|
||||||
|
img=img,
|
||||||
|
facial_area=(x, y, w, h)
|
||||||
|
)
|
||||||
resp_obj["is_real"] = is_real
|
resp_obj["is_real"] = is_real
|
||||||
resp_obj["antispoof_score"] = antispoof_score
|
resp_obj["antispoof_score"] = antispoof_score
|
||||||
|
|
||||||
resp_objs.append(resp_obj)
|
img_resp_objs.append(resp_obj)
|
||||||
|
|
||||||
if len(resp_objs) == 0 and enforce_detection == True:
|
all_resp_objs.append(img_resp_objs)
|
||||||
raise ValueError(
|
|
||||||
f"Exception while extracting faces from {img_name}."
|
|
||||||
"Consider to set enforce_detection arg to False."
|
|
||||||
)
|
|
||||||
|
|
||||||
return resp_objs
|
if not batched_input:
|
||||||
|
return all_resp_objs[0]
|
||||||
|
return all_resp_objs
|
||||||
|
|
||||||
|
|
||||||
def detect_faces(
|
def detect_faces(
|
||||||
detector_backend: str,
|
detector_backend: str,
|
||||||
img: np.ndarray,
|
img: Union[np.ndarray, List[np.ndarray]],
|
||||||
align: bool = True,
|
align: bool = True,
|
||||||
expand_percentage: int = 0,
|
expand_percentage: int = 0,
|
||||||
max_faces: Optional[int] = None,
|
max_faces: Optional[int] = None,
|
||||||
) -> List[DetectedFace]:
|
) -> Union[List[List[DetectedFace]], List[DetectedFace]]:
|
||||||
"""
|
"""
|
||||||
Detect face(s) from a given image
|
Detect face(s) from a given image or list of images
|
||||||
Args:
|
Args:
|
||||||
detector_backend (str): detector name
|
detector_backend (str): detector name
|
||||||
|
|
||||||
img (np.ndarray): pre-loaded image
|
img (np.ndarray or List[np.ndarray]): pre-loaded image or list of images
|
||||||
|
|
||||||
align (bool): enable or disable alignment after detection
|
align (bool): enable or disable alignment after detection
|
||||||
|
|
||||||
expand_percentage (int): expand detected facial area with a percentage (default is 0).
|
expand_percentage (int): expand detected facial area with a percentage (default is 0).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
results (List[DetectedFace]): A list of DetectedFace objects
|
results (Union[List[List[DetectedFace]], List[DetectedFace]]):
|
||||||
|
A list of lists of DetectedFace objects or a list of DetectedFace objects
|
||||||
where each object contains:
|
where each object contains:
|
||||||
|
|
||||||
- img (np.ndarray): The detected face as a NumPy array.
|
- img (np.ndarray): The detected face as a NumPy array.
|
||||||
@ -219,11 +244,46 @@ def detect_faces(
|
|||||||
|
|
||||||
- confidence (float): The confidence score associated with the detected face.
|
- confidence (float): The confidence score associated with the detected face.
|
||||||
"""
|
"""
|
||||||
height, width, _ = img.shape
|
batched_input = (
|
||||||
|
(
|
||||||
|
isinstance(img, np.ndarray) and
|
||||||
|
img.ndim == 4
|
||||||
|
) or isinstance(img, list)
|
||||||
|
)
|
||||||
|
if not batched_input:
|
||||||
|
imgs = [img]
|
||||||
|
elif isinstance(img, np.ndarray):
|
||||||
|
imgs = [img[i] for i in range(img.shape[0])]
|
||||||
|
else:
|
||||||
|
imgs = img
|
||||||
|
|
||||||
|
if detector_backend == "skip":
|
||||||
|
all_face_objs = [
|
||||||
|
[
|
||||||
|
DetectedFace(
|
||||||
|
img=single_img,
|
||||||
|
facial_area=FacialAreaRegion(
|
||||||
|
x=0, y=0, w=single_img.shape[1], h=single_img.shape[0]
|
||||||
|
),
|
||||||
|
confidence=0,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for single_img in imgs
|
||||||
|
]
|
||||||
|
if not batched_input:
|
||||||
|
all_face_objs = all_face_objs[0]
|
||||||
|
return all_face_objs
|
||||||
|
|
||||||
face_detector: Detector = modeling.build_model(
|
face_detector: Detector = modeling.build_model(
|
||||||
task="face_detector", model_name=detector_backend
|
task="face_detector", model_name=detector_backend
|
||||||
)
|
)
|
||||||
|
|
||||||
|
preprocessed_images = []
|
||||||
|
width_borders = []
|
||||||
|
height_borders = []
|
||||||
|
for single_img in imgs:
|
||||||
|
height, width, _ = single_img.shape
|
||||||
|
|
||||||
# validate expand percentage score
|
# validate expand percentage score
|
||||||
if expand_percentage < 0:
|
if expand_percentage < 0:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@ -237,8 +297,8 @@ def detect_faces(
|
|||||||
height_border = int(0.5 * height)
|
height_border = int(0.5 * height)
|
||||||
width_border = int(0.5 * width)
|
width_border = int(0.5 * width)
|
||||||
if align is True:
|
if align is True:
|
||||||
img = cv2.copyMakeBorder(
|
single_img = cv2.copyMakeBorder(
|
||||||
img,
|
single_img,
|
||||||
height_border,
|
height_border,
|
||||||
height_border,
|
height_border,
|
||||||
width_border,
|
width_border,
|
||||||
@ -247,26 +307,51 @@ def detect_faces(
|
|||||||
value=[0, 0, 0], # Color of the border (black)
|
value=[0, 0, 0], # Color of the border (black)
|
||||||
)
|
)
|
||||||
|
|
||||||
# find facial areas of given image
|
preprocessed_images.append(single_img)
|
||||||
facial_areas = face_detector.detect_faces(img)
|
width_borders.append(width_border)
|
||||||
|
height_borders.append(height_border)
|
||||||
|
|
||||||
|
# Detect faces in all preprocessed images
|
||||||
|
all_facial_areas = face_detector.detect_faces(preprocessed_images)
|
||||||
|
|
||||||
|
all_detected_faces = []
|
||||||
|
for (
|
||||||
|
single_img,
|
||||||
|
facial_areas,
|
||||||
|
width_border,
|
||||||
|
height_border
|
||||||
|
) in zip(
|
||||||
|
preprocessed_images,
|
||||||
|
all_facial_areas,
|
||||||
|
width_borders,
|
||||||
|
height_borders
|
||||||
|
):
|
||||||
|
if not isinstance(facial_areas, list):
|
||||||
|
facial_areas = [facial_areas]
|
||||||
|
|
||||||
if max_faces is not None and max_faces < len(facial_areas):
|
if max_faces is not None and max_faces < len(facial_areas):
|
||||||
facial_areas = nlargest(
|
facial_areas = nlargest(
|
||||||
max_faces, facial_areas, key=lambda facial_area: facial_area.w * facial_area.h
|
max_faces, facial_areas, key=lambda facial_area: facial_area.w * facial_area.h
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
detected_faces = [
|
||||||
extract_face(
|
extract_face(
|
||||||
facial_area=facial_area,
|
facial_area=facial_area,
|
||||||
img=img,
|
img=single_img,
|
||||||
align=align,
|
align=align,
|
||||||
expand_percentage=expand_percentage,
|
expand_percentage=expand_percentage,
|
||||||
width_border=width_border,
|
width_border=width_border,
|
||||||
height_border=height_border,
|
height_border=height_border,
|
||||||
)
|
)
|
||||||
for facial_area in facial_areas
|
for facial_area in facial_areas if isinstance(facial_area, FacialAreaRegion)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
all_detected_faces.append(detected_faces)
|
||||||
|
|
||||||
|
if not batched_input:
|
||||||
|
return all_detected_faces[0]
|
||||||
|
return all_detected_faces
|
||||||
|
|
||||||
|
|
||||||
def extract_face(
|
def extract_face(
|
||||||
facial_area: FacialAreaRegion,
|
facial_area: FacialAreaRegion,
|
||||||
|
@ -23,6 +23,10 @@ def test_different_detectors():
|
|||||||
|
|
||||||
for detector in detectors:
|
for detector in detectors:
|
||||||
img_objs = DeepFace.extract_faces(img_path=img_path, detector_backend=detector)
|
img_objs = DeepFace.extract_faces(img_path=img_path, detector_backend=detector)
|
||||||
|
|
||||||
|
# Check return type for non-batch input
|
||||||
|
assert isinstance(img_objs, list) and all(isinstance(obj, dict) for obj in img_objs)
|
||||||
|
|
||||||
for img_obj in img_objs:
|
for img_obj in img_objs:
|
||||||
assert "face" in img_obj.keys()
|
assert "face" in img_obj.keys()
|
||||||
assert "facial_area" in img_obj.keys()
|
assert "facial_area" in img_obj.keys()
|
||||||
@ -79,6 +83,169 @@ def test_different_detectors():
|
|||||||
logger.info(f"✅ extract_faces for {detector} backend test is done")
|
logger.info(f"✅ extract_faces for {detector} backend test is done")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("detector_backend", [
|
||||||
|
# "opencv",
|
||||||
|
"ssd",
|
||||||
|
"mtcnn",
|
||||||
|
"retinaface",
|
||||||
|
"yunet",
|
||||||
|
"centerface",
|
||||||
|
# optional
|
||||||
|
# "yolov11s",
|
||||||
|
# "mediapipe",
|
||||||
|
# "dlib",
|
||||||
|
])
|
||||||
|
def test_batch_extract_faces(detector_backend):
|
||||||
|
# Relative tolerance for comparing floating-point values
|
||||||
|
rtol = 0.03
|
||||||
|
img_paths = [
|
||||||
|
"dataset/img2.jpg",
|
||||||
|
"dataset/img3.jpg",
|
||||||
|
"dataset/img11.jpg",
|
||||||
|
"dataset/couple.jpg"
|
||||||
|
]
|
||||||
|
expected_num_faces = [1, 1, 1, 2]
|
||||||
|
|
||||||
|
# Extract faces one by one
|
||||||
|
imgs_objs_individual = [
|
||||||
|
DeepFace.extract_faces(
|
||||||
|
img_path=img_path,
|
||||||
|
detector_backend=detector_backend,
|
||||||
|
align=True,
|
||||||
|
) for img_path in img_paths
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check that individual extraction returns a list of faces
|
||||||
|
for img_objs_individual in imgs_objs_individual:
|
||||||
|
assert isinstance(img_objs_individual, list)
|
||||||
|
assert all(isinstance(face, dict) for face in img_objs_individual)
|
||||||
|
|
||||||
|
# Check that the individual extraction results match the expected number of faces
|
||||||
|
for img_objs_individual, expected_faces in zip(imgs_objs_individual, expected_num_faces):
|
||||||
|
assert len(img_objs_individual) == expected_faces
|
||||||
|
|
||||||
|
# Extract faces in batch
|
||||||
|
imgs_objs_batch = DeepFace.extract_faces(
|
||||||
|
img_path=img_paths,
|
||||||
|
detector_backend=detector_backend,
|
||||||
|
align=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the batch extraction returned the expected number of face lists
|
||||||
|
assert len(imgs_objs_batch) == len(img_paths)
|
||||||
|
|
||||||
|
# Check that each face list has the expected number of faces
|
||||||
|
for i, expected_faces in enumerate(expected_num_faces):
|
||||||
|
assert len(imgs_objs_batch[i]) == expected_faces
|
||||||
|
|
||||||
|
# Check that the individual extraction results match the batch extraction results
|
||||||
|
for img_objs_individual, img_objs_batch in zip(imgs_objs_individual, imgs_objs_batch):
|
||||||
|
assert len(img_objs_batch) == len(img_objs_individual), (
|
||||||
|
"Batch and individual extraction results should have the same number of detected faces"
|
||||||
|
)
|
||||||
|
for img_obj_individual, img_obj_batch in zip(img_objs_individual, img_objs_batch):
|
||||||
|
for key in img_obj_individual["facial_area"]:
|
||||||
|
if isinstance(img_obj_individual["facial_area"][key], tuple):
|
||||||
|
for ind_val, batch_val in zip(
|
||||||
|
img_obj_individual["facial_area"][key],
|
||||||
|
img_obj_batch["facial_area"][key]
|
||||||
|
):
|
||||||
|
# Ensure the difference between individual and batch values
|
||||||
|
# is within rtol% of the individual value
|
||||||
|
assert abs(ind_val - batch_val) <= rtol * ind_val
|
||||||
|
elif (
|
||||||
|
isinstance(img_obj_individual["facial_area"][key], int) or
|
||||||
|
isinstance(img_obj_individual["facial_area"][key], float)
|
||||||
|
):
|
||||||
|
# Ensure the difference between individual and batch values
|
||||||
|
# is within rtol% of the individual value
|
||||||
|
assert abs(
|
||||||
|
img_obj_individual["facial_area"][key] -
|
||||||
|
img_obj_batch["facial_area"][key]
|
||||||
|
) <= rtol * img_obj_individual["facial_area"][key]
|
||||||
|
# Ensure the confidence difference is within rtol% of the individual confidence
|
||||||
|
assert abs(
|
||||||
|
img_obj_individual["confidence"] -
|
||||||
|
img_obj_batch["confidence"]
|
||||||
|
) <= rtol * img_obj_individual["confidence"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("detector_backend", [
|
||||||
|
"opencv",
|
||||||
|
"ssd",
|
||||||
|
"mtcnn",
|
||||||
|
"retinaface",
|
||||||
|
"yunet",
|
||||||
|
# "centerface",
|
||||||
|
# optional
|
||||||
|
# "yolov11s",
|
||||||
|
# "mediapipe",
|
||||||
|
# "dlib",
|
||||||
|
])
|
||||||
|
def test_batch_extract_faces_with_nparray(detector_backend):
|
||||||
|
img_paths = [
|
||||||
|
"dataset/img2.jpg",
|
||||||
|
"dataset/img3.jpg",
|
||||||
|
"dataset/img11.jpg",
|
||||||
|
"dataset/couple.jpg"
|
||||||
|
]
|
||||||
|
imgs = [
|
||||||
|
cv2.resize(image_utils.load_image(img_path)[0], (1920, 1080))
|
||||||
|
for img_path in img_paths
|
||||||
|
]
|
||||||
|
expected_num_faces = [1, 1, 1, 2]
|
||||||
|
|
||||||
|
# load images as numpy arrays
|
||||||
|
imgs_batch = np.stack(imgs, axis=0)
|
||||||
|
|
||||||
|
# extract faces in batch of numpy arrays
|
||||||
|
imgs_objs_batch = DeepFace.extract_faces(
|
||||||
|
img_path=imgs_batch,
|
||||||
|
detector_backend=detector_backend,
|
||||||
|
align=True,
|
||||||
|
enforce_detection=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check return type for batch input
|
||||||
|
assert (
|
||||||
|
isinstance(imgs_objs_batch, list) and
|
||||||
|
all(
|
||||||
|
isinstance(obj, list) and
|
||||||
|
all(isinstance(face, dict) for face in obj)
|
||||||
|
for obj in imgs_objs_batch
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the batch extraction returned the expected number of face lists
|
||||||
|
assert len(imgs_objs_batch) == len(img_paths)
|
||||||
|
for img_objs_batch, img_expected_num_faces in zip(imgs_objs_batch, expected_num_faces):
|
||||||
|
assert len(img_objs_batch) == img_expected_num_faces
|
||||||
|
|
||||||
|
# extract faces in batch of paths
|
||||||
|
imgs_objs_batch_paths = DeepFace.extract_faces(
|
||||||
|
img_path=imgs,
|
||||||
|
detector_backend=detector_backend,
|
||||||
|
align=True,
|
||||||
|
enforce_detection=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# compare results
|
||||||
|
for img_objs_batch, img_objs_batch_paths in zip(imgs_objs_batch, imgs_objs_batch_paths):
|
||||||
|
assert len(img_objs_batch) == len(img_objs_batch_paths), (
|
||||||
|
"Batch and individual extraction results should have the same number of detected faces"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_extract_faces_single_image():
|
||||||
|
img_path = "dataset/couple.jpg"
|
||||||
|
imgs_objs_batch = DeepFace.extract_faces(
|
||||||
|
img_path=[img_path],
|
||||||
|
align=True,
|
||||||
|
)
|
||||||
|
assert len(imgs_objs_batch) == 1 and isinstance(imgs_objs_batch[0], list)
|
||||||
|
assert [isinstance(obj, dict) for obj in imgs_objs_batch[0]]
|
||||||
|
|
||||||
|
|
||||||
def test_backends_for_enforced_detection_with_non_facial_inputs():
|
def test_backends_for_enforced_detection_with_non_facial_inputs():
|
||||||
black_img = np.zeros([224, 224, 3])
|
black_img = np.zeros([224, 224, 3])
|
||||||
for detector in detectors:
|
for detector in detectors:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user