This commit is contained in:
galthran-wq 2025-02-16 14:09:05 +00:00
parent 8bfdcf139a
commit 7e59cdf05d
7 changed files with 167 additions and 104 deletions

View File

@ -10,14 +10,15 @@ import numpy as np
class Detector(ABC):
@abstractmethod
def detect_faces(
self,
imgs: Union[np.ndarray, List[np.ndarray]]
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:
imgs (Union[np.ndarray, List[np.ndarray]]): pre-loaded image as numpy array or a list of those
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]]):

View File

@ -18,9 +18,8 @@ class MtCnnClient(Detector):
self.model = MTCNN()
def detect_faces(
self,
img: Union[np.ndarray,
List[np.ndarray]]
self,
img: Union[np.ndarray, List[np.ndarray]]
) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]:
"""
Detect and align faces with mtcnn for a list of images
@ -31,7 +30,8 @@ class MtCnnClient(Detector):
Returns:
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
A list of FacialAreaRegion objects for a single image
or a list of lists of FacialAreaRegion objects for each image
"""
if not isinstance(img, list):

View File

@ -29,35 +29,42 @@ class OpenCvClient(Detector):
detector["eye_detector"] = self.__build_cascade("haarcascade_eye")
return detector
def detect_faces(self, imgs: Union[np.ndarray, List[np.ndarray]]) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]:
def detect_faces(
self,
img: Union[np.ndarray, List[np.ndarray]]
) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]:
"""
Detect and align face with opencv
Args:
imgs (Union[np.ndarray, List[np.ndarray]]): pre-loaded image as numpy array or a list of those
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
results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]):
A list or a list of lists of FacialAreaRegion objects
"""
if isinstance(imgs, np.ndarray):
imgs = [imgs]
if isinstance(img, np.ndarray):
imgs = [img]
else:
imgs = img
batch_results = []
for img in imgs:
for single_img in imgs:
resp = []
detected_face = None
faces = []
try:
faces, _, scores = self.model["face_detector"].detectMultiScale3(
img, 1.1, 10, outputRejectLevels=True
single_img, 1.1, 10, outputRejectLevels=True
)
except:
pass
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)]
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:

View File

@ -13,15 +13,20 @@ class RetinaFaceClient(Detector):
def __init__(self):
self.model = rf.build_model()
def detect_faces(self, img: Union[np.ndarray, List[np.ndarray]]) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]:
def detect_faces(
self,
img: Union[np.ndarray, List[np.ndarray]]
) -> Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]:
"""
Detect and align faces with retinaface in an image or a list of images
Detect and align faces with retinaface in a batch of images
Args:
img (Union[np.ndarray, List[np.ndarray]]): pre-loaded image as numpy array or a list of those
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
results (Union[List[FacialAreaRegion], List[List[FacialAreaRegion]]]):
A list or a list of lists of FacialAreaRegion objects
"""
if isinstance(img, np.ndarray):
imgs = [img]
@ -30,9 +35,9 @@ class RetinaFaceClient(Detector):
batch_results = []
for img in imgs:
for single_img in imgs:
resp = []
obj = rf.detect_faces(img, model=self.model, threshold=0.9)
obj = rf.detect_faces(single_img, model=self.model, threshold=0.9)
if isinstance(obj, dict):
for face_idx in obj.keys():

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,83 +54,96 @@ class SsdClient(Detector):
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:
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
"""
if isinstance(img, np.ndarray):
imgs = [img]
else:
imgs = img
# Because cv2.dnn.blobFromImage expects CV_8U (8-bit unsigned integer) values
if img.dtype != np.uint8:
img = img.astype(np.uint8)
batch_results = []
opencv_module: OpenCv.OpenCvClient = self.model["opencv_module"]
for single_img in imgs:
# Because cv2.dnn.blobFromImage expects CV_8U (8-bit unsigned integer) values
if single_img.dtype != np.uint8:
single_img = single_img.astype(np.uint8)
target_size = (300, 300)
opencv_module: OpenCv.OpenCvClient = self.model["opencv_module"]
original_size = img.shape
target_size = (300, 300)
original_size = single_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_y = original_size[0] / target_size[0]
aspect_ratio_x = original_size[1] / target_size[1]
aspect_ratio_y = original_size[0] / target_size[0]
imageBlob = cv2.dnn.blobFromImage(image=current_img)
imageBlob = cv2.dnn.blobFromImage(image=current_img)
face_detector = self.model["face_detector"]
face_detector.setInput(imageBlob)
detections = face_detector.forward()
face_detector = self.model["face_detector"]
face_detector.setInput(imageBlob)
detections = face_detector.forward()
class ssd_labels(IntEnum):
img_id = 0
is_face = 1
confidence = 2
left = 3
top = 4
right = 5
bottom = 6
class ssd_labels(IntEnum):
img_id = 0
is_face = 1
confidence = 2
left = 3
top = 4
right = 5
bottom = 6
faces = detections[0][0]
faces = faces[
(faces[:, ssd_labels.is_face] == 1) & (faces[:, ssd_labels.confidence] >= 0.90)
]
margins = [ssd_labels.left, ssd_labels.top, ssd_labels.right, ssd_labels.bottom]
faces[:, margins] = np.int32(faces[:, margins] * 300)
faces[:, margins] = np.int32(
faces[:, margins] * [aspect_ratio_x, aspect_ratio_y, aspect_ratio_x, aspect_ratio_y]
)
faces[:, [ssd_labels.right, ssd_labels.bottom]] -= faces[
:, [ssd_labels.left, ssd_labels.top]
]
resp = []
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]
left_eye, right_eye = opencv_module.find_eyes(detected_face)
# eyes found in the detected face instead image itself
# detected face's coordinates should be added
if left_eye is not None:
left_eye = x + int(left_eye[0]), y + int(left_eye[1])
if right_eye is not None:
right_eye = x + int(right_eye[0]), y + int(right_eye[1])
facial_area = FacialAreaRegion(
x=x,
y=y,
w=w,
h=h,
left_eye=left_eye,
right_eye=right_eye,
confidence=confidence,
faces = detections[0][0]
faces = faces[
(faces[:, ssd_labels.is_face] == 1) & (faces[:, ssd_labels.confidence] >= 0.90)
]
margins = [ssd_labels.left, ssd_labels.top, ssd_labels.right, ssd_labels.bottom]
faces[:, margins] = np.int32(faces[:, margins] * 300)
faces[:, margins] = np.int32(
faces[:, margins] * [aspect_ratio_x, aspect_ratio_y, aspect_ratio_x, aspect_ratio_y]
)
resp.append(facial_area)
return resp
faces[:, [ssd_labels.right, ssd_labels.bottom]] -= faces[
:, [ssd_labels.left, ssd_labels.top]
]
resp = []
for face in faces:
confidence = float(face[ssd_labels.confidence])
x, y, w, h = map(int, face[margins])
detected_face = single_img[y : y + h, x : x + w]
left_eye, right_eye = opencv_module.find_eyes(detected_face)
# eyes found in the detected face instead image itself
# detected face's coordinates should be added
if left_eye is not None:
left_eye = x + int(left_eye[0]), y + int(left_eye[1])
if right_eye is not None:
right_eye = x + int(right_eye[0]), y + int(right_eye[1])
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)
batch_results.append(resp)
return batch_results if len(batch_results) > 1 else batch_results[0]

View File

@ -1,6 +1,6 @@
# built-in dependencies
import os
from typing import List, Any, Union, Tuple
from typing import List, Any, Union
from enum import Enum
# 3rd party dependencies
@ -62,25 +62,30 @@ class YoloDetectorClient(Detector):
# Return face_detector
return YOLO(weight_file)
def detect_faces(self, imgs: Union[np.ndarray, List[np.ndarray]]) -> Union[List[List[FacialAreaRegion]], List[FacialAreaRegion]]:
def detect_faces(
self,
img: Union[np.ndarray, List[np.ndarray]]
) -> Union[List[List[FacialAreaRegion]], List[FacialAreaRegion]]:
"""
Detect and align faces in an image or a list of images with yolo
Detect and align faces in a batch of images with yolo
Args:
imgs (Union[np.ndarray, List[np.ndarray]]): pre-loaded image as numpy array or a list of those
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 of lists of FacialAreaRegion objects for each image or a list of FacialAreaRegion objects
A list of lists of FacialAreaRegion objects
for each image or a list of FacialAreaRegion objects
"""
if not isinstance(imgs, list):
imgs = [imgs]
if not isinstance(img, list):
img = [img]
all_results = []
# Detect faces for all images
results_list = self.model.predict(
imgs,
img,
verbose=False,
show=False,
conf=float(os.getenv("YOLO_MIN_DETECTION_CONFIDENCE", "0.25")),
@ -113,9 +118,16 @@ class YoloDetectorClient(Detector):
# eyes are list of float, need to cast them tuple of int
# Ensure eyes are tuples of exactly two integers or None
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
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)
facial_area = FacialAreaRegion(
x=x,

View File

@ -34,8 +34,9 @@ def extract_faces(
Extract faces from a given image or list of images
Args:
img_paths (List[str or np.ndarray or IO[bytes]] or str or np.ndarray or IO[bytes]): Path(s) to the image(s). Accepts exact image path
as a string, numpy array (BGR), a file object that supports at least `.read` and is
img_paths (List[str or np.ndarray or IO[bytes]] or str or np.ndarray or IO[bytes]):
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.
detector_backend (string): face detector backend. Options: 'opencv', 'retinaface',
@ -148,7 +149,10 @@ def extract_faces(
elif color_face == "gray":
current_img = cv2.cvtColor(current_img, cv2.COLOR_BGR2GRAY)
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:
current_img = current_img / 255 # normalize input in [0, 1]
@ -184,7 +188,10 @@ def extract_faces(
if anti_spoofing is True:
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["antispoof_score"] = antispoof_score
@ -226,10 +233,18 @@ def detect_faces(
"""
if not isinstance(img, list):
img = [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)]
[
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 img
]
if len(img) == 1:
@ -280,7 +295,17 @@ def detect_faces(
all_facial_areas = [all_facial_areas]
all_detected_faces = []
for single_img, facial_areas, width_border, height_border in zip(preprocessed_images, all_facial_areas, width_borders, height_borders):
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]