import os from abc import ABC, abstractmethod from typing import List import cv2 import numpy as np from retinaface import RetinaFace from retinaface.model import retinaface_model from .box_utils import convert_to_square class FaceDetector(ABC): def __init__(self, target_size): self.target_size = target_size @abstractmethod def detect_crops(self, img, *args, **kwargs) -> List[np.ndarray]: """ Img is a numpy ndarray in range [0..255], uint8 dtype, RGB type Returns ndarray with [x1, y1, x2, y2] in row """ pass @abstractmethod def postprocess_crops(self, crops, *args, **kwargs) -> List[np.ndarray]: return crops def sort_faces(self, crops): sorted_faces = sorted(crops, key=lambda x: -(x[2] - x[0]) * (x[3] - x[1])) sorted_faces = np.stack(sorted_faces, axis=0) return sorted_faces def fix_range_crops(self, img, crops): H, W, _ = img.shape final_crops = [] for crop in crops: x1, y1, x2, y2 = crop x1 = max(min(round(x1), W), 0) y1 = max(min(round(y1), H), 0) x2 = max(min(round(x2), W), 0) y2 = max(min(round(y2), H), 0) new_crop = [x1, y1, x2, y2] final_crops.append(new_crop) final_crops = np.array(final_crops, dtype=np.int) return final_crops def crop_faces(self, img, crops) -> List[np.ndarray]: cropped_faces = [] for crop in crops: x1, y1, x2, y2 = crop face_crop = img[y1:y2, x1:x2, :] cropped_faces.append(face_crop) return cropped_faces def unify_and_merge(self, cropped_images): return cropped_images def __call__(self, img): return self.detect_faces(img) def detect_faces(self, img): crops = self.detect_crops(img) if crops is None or len(crops) == 0: return [], [] crops = self.sort_faces(crops) updated_crops = self.postprocess_crops(crops) updated_crops = self.fix_range_crops(img, updated_crops) cropped_faces = self.crop_faces(img, updated_crops) unified_faces = self.unify_and_merge(cropped_faces) return unified_faces, updated_crops class StatRetinaFaceDetector(FaceDetector): def __init__(self, target_size=None): super().__init__(target_size) self.model = retinaface_model.build_model() #self.relative_offsets = [0.3258, 0.5225, 0.3258, 0.1290] self.relative_offsets = [0.3619, 0.5830, 0.3619, 0.1909] def postprocess_crops(self, crops, *args, **kwargs) -> np.ndarray: final_crops = [] x1_offset, y1_offset, x2_offset, y2_offset = self.relative_offsets for crop in crops: x1, y1, x2, y2 = crop w, h = x2 - x1, y2 - y1 x1 -= w * x1_offset y1 -= h * y1_offset x2 += w * x2_offset y2 += h * y2_offset crop = np.array([x1, y1, x2, y2], dtype=crop.dtype) crop = convert_to_square(crop) final_crops.append(crop) final_crops = np.stack(final_crops, axis=0) return final_crops def detect_crops(self, img, *args, **kwargs): faces = RetinaFace.detect_faces(img, model=self.model) crops = [] if isinstance(faces, tuple): faces = {} for name, face in faces.items(): x1, y1, x2, y2 = face['facial_area'] crop = np.array([x1, y1, x2, y2]) crops.append(crop) if len(crops) > 0: crops = np.stack(crops, axis=0) return crops def unify_and_merge(self, cropped_images): if self.target_size is None: return cropped_images else: resized_images = [] for cropped_image in cropped_images: resized_image = cv2.resize(cropped_image, (self.target_size, self.target_size), interpolation=cv2.INTER_LINEAR) resized_images.append(resized_image) resized_images = np.stack(resized_images, axis=0) return resized_images