Revert "refactor detectors to have default detect_faces method that is based on process_single_image method"

This reverts commit 8c7c2cb9b7505bd17beed69e3bbf16a8b4c8940d.
This commit is contained in:
galthran-wq 2025-03-08 15:15:36 +00:00
parent 6a3d14cde7
commit da8f644c79
9 changed files with 230 additions and 105 deletions

View File

@ -1,8 +1,37 @@
from typing import List, Tuple, Optional, Union from typing import List, Tuple, Optional, Union
from abc import ABC from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
import numpy as np 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 # pylint: disable=unnecessary-pass, too-few-public-methods, too-many-instance-attributes
@dataclass @dataclass
@ -49,56 +78,3 @@ class DetectedFace:
img: np.ndarray img: np.ndarray
facial_area: FacialAreaRegion facial_area: FacialAreaRegion
confidence: float 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"
)

View File

@ -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 _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. Helper function to detect faces in a single image.
Args: Args:
img (np.ndarray): pre-loaded image as numpy array 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):

View File

@ -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,6 +47,29 @@ class DlibClient(Detector):
detector["sp"] = sp detector["sp"] = sp
return detector 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]: def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
""" """
Helper function to detect faces in a single image. Helper function to detect faces in a single image.

View File

@ -17,6 +17,29 @@ class FastMtCnnClient(Detector):
def __init__(self): def __init__(self):
self.model = self.build_model() 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]: def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
""" """
Helper function to detect faces in a single image. Helper function to detect faces in a single image.

View File

@ -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,6 +43,29 @@ class MediaPipeClient(Detector):
) )
return face_detection 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]: def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
""" """
Helper function to detect faces in a single image. Helper function to detect faces in a single image.

View File

@ -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
import logging import logging
# 3rd party dependencies # 3rd party dependencies
@ -45,48 +45,65 @@ class OpenCvClient(Detector):
) )
return supports_batch_detection 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: 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
""" """
resp = [] if isinstance(img, np.ndarray):
detected_face = None imgs = [img]
faces = [] elif self.supports_batch_detection:
try: imgs = img
faces, _, scores = self.model["face_detector"].detectMultiScale3( else:
img, 1.1, 10, outputRejectLevels=True return [self.detect_faces(single_img) for single_img in img]
)
except:
pass
if len(faces) > 0: batch_results = []
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)
if left_eye is not None: for single_img in imgs:
left_eye = (int(x + left_eye[0]), int(y + left_eye[1])) resp = []
if right_eye is not None: detected_face = None
right_eye = (int(x + right_eye[0]), int(y + right_eye[1])) faces = []
try:
facial_area = FacialAreaRegion( faces, _, scores = self.model["face_detector"].detectMultiScale3(
x=x, single_img, 1.1, 10, outputRejectLevels=True
y=y,
w=w,
h=h,
left_eye=left_eye,
right_eye=right_eye,
confidence=(100 - confidence) / 100,
) )
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: def find_eyes(self, img: np.ndarray) -> tuple:
""" """

View File

@ -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,25 +54,48 @@ class SsdClient(Detector):
return {"face_detector": face_detector, "opencv_module": OpenCv.OpenCvClient()} 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. Helper function to detect faces in a single image.
Args: Args:
img (np.ndarray): Pre-loaded image as numpy array 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 = img.shape original_size = single_img.shape
current_img = cv2.resize(img, target_size) current_img = cv2.resize(single_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]
@ -109,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)

View File

@ -78,8 +78,7 @@ class YoloDetectorClient(Detector):
A list of lists of FacialAreaRegion objects A list of lists of FacialAreaRegion objects
for each image or a list of FacialAreaRegion objects for each image or a list of FacialAreaRegion objects
""" """
is_batched_input = isinstance(img, list) if not isinstance(img, list):
if not is_batched_input:
img = [img] img = [img]
all_results = [] all_results = []
@ -143,7 +142,7 @@ class YoloDetectorClient(Detector):
all_results.append(resp) all_results.append(resp)
if not is_batched_input: if len(all_results) == 1:
return all_results[0] return all_results[0]
return all_results return all_results

View File

@ -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,6 +57,24 @@ class YuNetClient(Detector):
) from err ) from err
return face_detector 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]: def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
""" """
Helper function to detect faces in a single image. Helper function to detect faces in a single image.