Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batching on .extract_faces to improve performance and utilize GPU in full #1435

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f4d18a7
batched detection
galthran-wq Feb 12, 2025
0ad7c57
deepFace batch detection; typing
galthran-wq Feb 12, 2025
b38e95c
test batch extract faces
galthran-wq Feb 12, 2025
737ee79
chagne detector interface
galthran-wq Feb 12, 2025
b2d6178
opencv pseudo batching
galthran-wq Feb 12, 2025
1bd8335
yolo detect batched
galthran-wq Feb 12, 2025
bbf6a55
enhance batched detector test
galthran-wq Feb 12, 2025
ba2ff90
mtcnn batching
galthran-wq Feb 12, 2025
ad01724
soft test
galthran-wq Feb 13, 2025
799f83c
true batching on detect_faces
galthran-wq Feb 13, 2025
619930c
detection skip
galthran-wq Feb 16, 2025
b544a2d
pseudo batched retinaface
galthran-wq Feb 16, 2025
8bfdcf1
test diff detetors
galthran-wq Feb 16, 2025
7e59cdf
lint
galthran-wq Feb 16, 2025
0f67dda
optional MtCnn batching (does not work in python3.8)
galthran-wq Feb 17, 2025
c4b4b4a
lint
galthran-wq Feb 17, 2025
60bee4e
detect faces return list of lists on batched inputs
galthran-wq Feb 13, 2025
f3d05ef
add a couple to test batch extract faces
galthran-wq Feb 18, 2025
1c825e8
add more models and detector-specific rtol
galthran-wq Feb 18, 2025
1d358aa
pseudo-batching dlib
galthran-wq Feb 18, 2025
f5188c8
pseudo-batching centerface
galthran-wq Feb 18, 2025
26e537d
psedu-batching fastmtcnn
galthran-wq Feb 18, 2025
7f04e6b
mediapipe pseudo bathcing
galthran-wq Feb 18, 2025
991566f
yunet pseudobatching
galthran-wq Feb 18, 2025
70b61a7
change interface in a special case
galthran-wq Feb 18, 2025
27dea80
batch test add other detector models
galthran-wq Feb 18, 2025
526ab1b
test numpy array batched input
galthran-wq Feb 18, 2025
988afa6
fix batched numpy array input
galthran-wq Feb 18, 2025
3e34675
lint
galthran-wq Feb 18, 2025
2eb5cac
batch extract faces on single image special case
galthran-wq Feb 18, 2025
c30f55c
lint
galthran-wq Feb 18, 2025
dc6cb81
batch test assert shape
galthran-wq Feb 21, 2025
6143ed9
clearify test batch extract
galthran-wq Feb 21, 2025
c46d886
more shape checks
galthran-wq Feb 21, 2025
add4c73
disable mtcnn batching by default due to unexpected behaviour
galthran-wq Feb 21, 2025
93b8af1
comments
galthran-wq Feb 23, 2025
8b1b465
change behaviour in special case batched single image
galthran-wq Feb 23, 2025
aae3af0
rm opencv from batch test since it still occasionally fails
galthran-wq Feb 23, 2025
8c7c2cb
refactor detectors to have default detect_faces method that is based …
galthran-wq Feb 23, 2025
c5ba4a7
lint
galthran-wq Feb 23, 2025
6a3d14c
Update test_extract_faces.py
galthran-wq Feb 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions deepface/DeepFace.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import warnings
import logging
from typing import Any, Dict, IO, List, Union, Optional
from typing import Any, Dict, IO, List, Union, Optional, Sequence

# this has to be set before importing tensorflow
os.environ["TF_USE_LEGACY_KERAS"] = "1"
Expand Down Expand Up @@ -510,7 +510,7 @@ def stream(


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",
enforce_detection: bool = True,
align: bool = True,
Expand All @@ -519,14 +519,14 @@ def extract_faces(
color_face: str = "rgb",
normalize_face: bool = True,
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:
img_path (str or np.ndarray or IO[bytes]): Path to the first image. Accepts exact image path
as a string, numpy array (BGR), a file object that supports at least `.read` and is
opened in binary mode, or base64 encoded images.
img_path (Union[str, np.ndarray, IO[bytes], Sequence[Union[str, np.ndarray, IO[bytes]]]]):
Path(s) to the image(s). Accepts a string path, a numpy array (BGR), a file object
that supports at least `.read` and is opened in binary mode, or base64 encoded images.

detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
'mtcnn', 'ssd', 'dlib', 'mediapipe', 'yolov8', 'yolov11n', 'yolov11s', 'yolov11m',
Expand All @@ -551,7 +551,8 @@ def extract_faces(
anti_spoofing (boolean): Flag to enable anti spoofing (default is False).

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.

Expand Down
80 changes: 55 additions & 25 deletions deepface/models/Detector.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
from typing import List, Tuple, Optional
from abc import ABC, abstractmethod
from typing import List, Tuple, Optional, Union
from abc import ABC
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: np.ndarray) -> List["FacialAreaRegion"]:
"""
Interface for detect and align face

Args:
img (np.ndarray): pre-loaded image as numpy array

Returns:
results (List[FacialAreaRegion]): A list 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


@dataclass
class FacialAreaRegion:
"""
Expand Down Expand Up @@ -72,3 +49,56 @@ 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"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not clear that which subclass is giving this error. IMO, we should print the detector name in the exception message.

)
4 changes: 2 additions & 2 deletions deepface/models/face_detection/CenterFace.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ def build_model(self):

return CenterFace(weight_path=weights_path)

def detect_faces(self, img: np.ndarray) -> List["FacialAreaRegion"]:
def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
"""
Detect and align face with CenterFace
Helper function to detect faces in a single image.

Args:
img (np.ndarray): pre-loaded image as numpy array
Expand Down
4 changes: 2 additions & 2 deletions deepface/models/face_detection/Dlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def build_model(self) -> dict:
detector["sp"] = sp
return detector

def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
"""
Detect and align face with dlib
Helper function to detect faces in a single image.

Args:
img (np.ndarray): pre-loaded image as numpy array
Expand Down
4 changes: 2 additions & 2 deletions deepface/models/face_detection/FastMtCnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ class FastMtCnnClient(Detector):
def __init__(self):
self.model = self.build_model()

def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
"""
Detect and align face with mtcnn
Helper function to detect faces in a single image.

Args:
img (np.ndarray): pre-loaded image as numpy array
Expand Down
4 changes: 2 additions & 2 deletions deepface/models/face_detection/MediaPipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ def build_model(self) -> Any:
)
return face_detection

def detect_faces(self, img: np.ndarray) -> List[FacialAreaRegion]:
def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
"""
Detect and align face with mediapipe
Helper function to detect faces in a single image.

Args:
img (np.ndarray): pre-loaded image as numpy array
Expand Down
111 changes: 82 additions & 29 deletions deepface/models/face_detection/MtCnn.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# built-in dependencies
from typing import List
import logging
from typing import List, Union

# 3rd party dependencies
import numpy as np
Expand All @@ -8,6 +9,8 @@
# project dependencies
from deepface.models.Detector import Detector, FacialAreaRegion

logger = logging.getLogger(__name__)

# pylint: disable=too-few-public-methods
class MtCnnClient(Detector):
"""
Expand All @@ -16,45 +19,95 @@ class MtCnnClient(Detector):

def __init__(self):
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:
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:
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 = []

# mtcnn expects RGB but OpenCV read BGR
# img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_rgb = img[:, :, ::-1]
detections = self.model.detect_faces(img_rgb)

if detections is not None and len(detections) > 0:

for current_detection in detections:
x, y, w, h = current_detection["box"]
confidence = current_detection["confidence"]
# mtcnn detector assigns left eye with respect to the observer
# but we are setting it with respect to the person itself
left_eye = current_detection["keypoints"]["right_eye"]
right_eye = current_detection["keypoints"]["left_eye"]

facial_area = FacialAreaRegion(
x=x,
y=y,
w=w,
h=h,
left_eye=left_eye,
right_eye=right_eye,
confidence=confidence,
)

resp.append(facial_area)
img_rgb = [img[:, :, ::-1] for img in img]
if self.supports_batch_detection:
detections = self.model.detect_faces(img_rgb)
else:
detections = [self.model.detect_faces(single_img) for single_img in img_rgb]

for image_detections in detections:
image_resp = []
if image_detections is not None and len(image_detections) > 0:
for current_detection in image_detections:
x, y, w, h = current_detection["box"]
confidence = current_detection["confidence"]
# mtcnn detector assigns left eye with respect to the observer
# but we are setting it with respect to the person itself
left_eye = current_detection["keypoints"]["right_eye"]
right_eye = current_detection["keypoints"]["left_eye"]

facial_area = FacialAreaRegion(
x=x,
y=y,
w=w,
h=h,
left_eye=left_eye,
right_eye=right_eye,
confidence=confidence,
)

image_resp.append(facial_area)

resp.append(image_resp)

if not is_batched_input:
return resp[0]
return resp

def _supports_batch_detection(self) -> bool:
import mtcnn
import os
supports_batch_detection = os.getenv(
"ENABLE_MTCNN_BATCH_DETECTION", "false"
).lower() == "true"
if not supports_batch_detection:
logger.warning(
"Batch detection is disabled for mtcnn by default "
"since the results are not consistent with single image detection. "
"You can force enable it by setting the environment variable "
"ENABLE_MTCNN_BATCH_DETECTION to true."
)
return False
try:
mtcnn_version = mtcnn.__version__
supports_batch_detection = mtcnn_version >= "1.0.0"
except AttributeError:
try:
import mtcnn.metadata
mtcnn_version = mtcnn.metadata.__version__
except AttributeError:
logger.warning("Failed to determine mtcnn version")
logger.warning("Fallback to single image detection")
return False
supports_batch_detection = mtcnn_version >= "1.0.0"
if not supports_batch_detection:
logger.warning("MtCnn version is less than 1.0.0, batch detection is not supported")
logger.warning("Fallback to single image detection")
return supports_batch_detection
Loading