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

This commit is contained in:
galthran-wq 2025-02-23 14:07:37 +00:00
parent aae3af0d05
commit 8c7c2cb9b7
9 changed files with 106 additions and 230 deletions

View File

@ -1,37 +1,8 @@
from typing import List, Tuple, Optional, Union from typing import List, Tuple, Optional, Union
from abc import ABC, abstractmethod from abc import ABC
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
@dataclass @dataclass
class FacialAreaRegion: class FacialAreaRegion:
@ -77,3 +48,57 @@ 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
# pylint: disable=unnecessary-pass, too-few-public-methods, too-many-instance-attributes
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, Union from typing import List
# 3rd party dependencies # 3rd party dependencies
import numpy as np import numpy as np
@ -34,35 +34,12 @@ class CenterFaceClient(Detector):
return CenterFace(weight_path=weights_path) return CenterFace(weight_path=weights_path)
def detect_faces( def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
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:
single_img (np.ndarray): pre-loaded image as numpy array 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
@ -76,7 +53,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(
single_img, single_img.shape[0], single_img.shape[1], threshold=threshold img, img.shape[0], 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, Union from typing import List
# 3rd party dependencies # 3rd party dependencies
import numpy as np import numpy as np
@ -47,29 +47,6 @@ 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,29 +17,6 @@ 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, Union from typing import Any, List
# 3rd party dependencies # 3rd party dependencies
import numpy as np import numpy as np
@ -43,29 +43,6 @@ 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, Union from typing import Any, List
import logging import logging
# 3rd party dependencies # 3rd party dependencies
@ -45,65 +45,48 @@ class OpenCvClient(Detector):
) )
return supports_batch_detection return supports_batch_detection
def detect_faces( def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
self,
img: Union[np.ndarray, List[np.ndarray]]
) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]:
""" """
Detect and align face with opencv Helper function to detect faces in a single image.
Args: Args:
img (Union[np.ndarray, List[np.ndarray]]): img (np.ndarray): pre-loaded image as numpy array
Pre-loaded image as numpy array or a list of those
Returns: Returns:
results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]): results (List[FacialAreaRegion]): A list of FacialAreaRegion objects
A list or a list of lists of FacialAreaRegion objects
""" """
if isinstance(img, np.ndarray): resp = []
imgs = [img] detected_face = None
elif self.supports_batch_detection: faces = []
imgs = img try:
else: faces, _, scores = self.model["face_detector"].detectMultiScale3(
return [self.detect_faces(single_img) for single_img in img] img, 1.1, 10, outputRejectLevels=True
)
except:
pass
batch_results = [] 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)
for single_img in imgs: if left_eye is not None:
resp = [] left_eye = (int(x + left_eye[0]), int(y + left_eye[1]))
detected_face = None if right_eye is not None:
faces = [] right_eye = (int(x + right_eye[0]), int(y + right_eye[1]))
try:
faces, _, scores = self.model["face_detector"].detectMultiScale3( facial_area = FacialAreaRegion(
single_img, 1.1, 10, outputRejectLevels=True x=x,
y=y,
w=w,
h=h,
left_eye=left_eye,
right_eye=right_eye,
confidence=(100 - confidence) / 100,
) )
except: resp.append(facial_area)
pass
if len(faces) > 0: return resp
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, Union from typing import List
from enum import IntEnum from enum import IntEnum
# 3rd party dependencies # 3rd party dependencies
@ -54,48 +54,25 @@ class SsdClient(Detector):
return {"face_detector": face_detector, "opencv_module": OpenCv.OpenCvClient()} return {"face_detector": face_detector, "opencv_module": OpenCv.OpenCvClient()}
def detect_faces( def _process_single_image(self, img: np.ndarray) -> List[FacialAreaRegion]:
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:
single_img (np.ndarray): Pre-loaded image as numpy array 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 single_img.dtype != np.uint8: if img.dtype != np.uint8:
single_img = single_img.astype(np.uint8) img = 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]
@ -132,7 +109,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 = single_img[y : y + h, x : x + w] detected_face = 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,7 +78,8 @@ 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
""" """
if not isinstance(img, list): is_batched_input = isinstance(img, list)
if not is_batched_input:
img = [img] img = [img]
all_results = [] all_results = []
@ -142,7 +143,7 @@ class YoloDetectorClient(Detector):
all_results.append(resp) all_results.append(resp)
if len(all_results) == 1: if not is_batched_input:
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, Union from typing import Any, List
# 3rd party dependencies # 3rd party dependencies
import cv2 import cv2
@ -57,24 +57,6 @@ 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.