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 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"
)

View File

@ -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):

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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:
"""

View File

@ -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)

View File

@ -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

View File

@ -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.