Add face_alignment as alternative detector

This commit is contained in:
kijai
2024-07-25 11:54:42 +03:00
parent 4d33a47ff2
commit 6cdb0ebde5
23 changed files with 2184 additions and 1 deletions
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2017, Adrian Bulat
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+7
View File
@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
__author__ = """Adrian Bulat"""
__email__ = 'adrian@adrianbulat.com'
__version__ = '1.4.1'
from .api import FaceAlignment, LandmarksType, NetworkSize
+305
View File
@@ -0,0 +1,305 @@
import torch
import warnings
from enum import IntEnum
from skimage import io
import numpy as np
from packaging import version
from tqdm import tqdm
import importlib
from .utils import *
from .folder_data import FolderData
class LandmarksType(IntEnum):
"""Enum class defining the type of landmarks to detect.
``TWO_D`` - the detected points ``(x,y)`` are detected in a 2D space and follow the visible contour of the face
``TWO_HALF_D`` - this points represent the projection of the 3D points into 3D
``THREE_D`` - detect the points ``(x,y,z)``` in a 3D space
"""
TWO_D = 1
TWO_HALF_D = 2
THREE_D = 3
class NetworkSize(IntEnum):
# TINY = 1
# SMALL = 2
# MEDIUM = 3
LARGE = 4
default_model_urls = {
'2DFAN-4': 'https://www.adrianbulat.com/downloads/python-fan/2DFAN4-cd938726ad.zip',
'3DFAN-4': 'https://www.adrianbulat.com/downloads/python-fan/3DFAN4-4a694010b9.zip',
'depth': 'https://www.adrianbulat.com/downloads/python-fan/depth-6c4283c0e0.zip',
}
models_urls = {
'1.6': {
'2DFAN-4': 'https://www.adrianbulat.com/downloads/python-fan/2DFAN4_1.6-c827573f02.zip',
'3DFAN-4': 'https://www.adrianbulat.com/downloads/python-fan/3DFAN4_1.6-ec5cf40a1d.zip',
'depth': 'https://www.adrianbulat.com/downloads/python-fan/depth_1.6-2aa3f18772.zip',
},
'1.5': {
'2DFAN-4': 'https://www.adrianbulat.com/downloads/python-fan/2DFAN4_1.5-a60332318a.zip',
'3DFAN-4': 'https://www.adrianbulat.com/downloads/python-fan/3DFAN4_1.5-176570af4d.zip',
'depth': 'https://www.adrianbulat.com/downloads/python-fan/depth_1.5-bc10f98e39.zip',
},
}
class FaceAlignment:
def __init__(self, landmarks_type, network_size=NetworkSize.LARGE,
device='cuda', dtype=torch.float32, flip_input=False, face_detector='sfd', face_detector_kwargs=None, verbose=False):
self.device = device
self.flip_input = flip_input
self.landmarks_type = landmarks_type
self.verbose = verbose
self.dtype = dtype
if version.parse(torch.__version__) < version.parse('1.5.0'):
raise ImportError(f'Unsupported pytorch version detected. Minimum supported version of pytorch: 1.5.0\
Either upgrade (recommended) your pytorch setup, or downgrade to face-alignment 1.2.0')
network_size = int(network_size)
pytorch_version = torch.__version__
if 'dev' in pytorch_version:
pytorch_version = pytorch_version.rsplit('.', 2)[0]
else:
pytorch_version = pytorch_version.rsplit('.', 1)[0]
#if 'cuda' in device:
# torch.backends.cudnn.benchmark = True
# Get the face detector
package_directory_name = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
face_detector_module = importlib.import_module('.face_alignment.detection.' + face_detector, package=package_directory_name)
face_detector_kwargs = face_detector_kwargs or {}
self.face_detector = face_detector_module.FaceDetector(device=device, verbose=verbose, **face_detector_kwargs)
# Initialise the face alignemnt networks
if landmarks_type == LandmarksType.TWO_D:
network_name = '2DFAN-' + str(network_size)
else:
network_name = '3DFAN-' + str(network_size)
self.face_alignment_net = torch.jit.load(
load_file_from_url(models_urls.get(pytorch_version, default_model_urls)[network_name]))
self.face_alignment_net.to(device, dtype=dtype)
self.face_alignment_net.eval()
# Initialiase the depth prediciton network
if landmarks_type == LandmarksType.THREE_D:
self.depth_prediciton_net = torch.jit.load(
load_file_from_url(models_urls.get(pytorch_version, default_model_urls)['depth']))
self.depth_prediciton_net.to(device, dtype=dtype)
self.depth_prediciton_net.eval()
def get_landmarks(self, image_or_path, detected_faces=None, return_bboxes=False, return_landmark_score=False):
"""Deprecated, please use get_landmarks_from_image
Arguments:
image_or_path {string or numpy.array or torch.tensor} -- The input image or path to it
Keyword Arguments:
detected_faces {list of numpy.array} -- list of bounding boxes, one for each face found
in the image (default: {None})
return_bboxes {boolean} -- If True, return the face bounding boxes in addition to the keypoints.
return_landmark_score {boolean} -- If True, return the keypoint scores along with the keypoints.
"""
return self.get_landmarks_from_image(image_or_path, detected_faces, return_bboxes, return_landmark_score)
@torch.no_grad()
def get_landmarks_from_image(self, image_or_path, detected_faces=None, return_bboxes=False,
return_landmark_score=False):
"""Predict the landmarks for each face present in the image.
This function predicts a set of 68 2D or 3D images, one for each image present.
If detect_faces is None the method will also run a face detector.
Arguments:
image_or_path {string or numpy.array or torch.tensor} -- The input image or path to it.
Keyword Arguments:
detected_faces {list of numpy.array} -- list of bounding boxes, one for each face found
in the image (default: {None})
return_bboxes {boolean} -- If True, return the face bounding boxes in addition to the keypoints.
return_landmark_score {boolean} -- If True, return the keypoint scores along with the keypoints.
Return:
result:
1. if both return_bboxes and return_landmark_score are False, result will be:
landmark
2. Otherwise, result will be one of the following, depending on the actual value of return_* arguments.
(landmark, landmark_score, detected_face)
(landmark, None, detected_face)
(landmark, landmark_score, None )
"""
image = get_image(image_or_path)
if detected_faces is None:
detected_faces = self.face_detector.detect_from_image(image.copy())
if len(detected_faces) == 0:
warnings.warn("No faces were detected.")
if return_bboxes or return_landmark_score:
return None, None, None
else:
return None
landmarks = []
landmarks_scores = []
for i, d in enumerate(detected_faces):
center = torch.tensor(
[d[2] - (d[2] - d[0]) / 2.0, d[3] - (d[3] - d[1]) / 2.0])
center[1] = center[1] - (d[3] - d[1]) * 0.12
scale = (d[2] - d[0] + d[3] - d[1]) / self.face_detector.reference_scale
inp = crop(image, center, scale)
inp = torch.from_numpy(inp.transpose(
(2, 0, 1))).float()
inp = inp.to(self.device, dtype=self.dtype)
inp.div_(255.0).unsqueeze_(0)
out = self.face_alignment_net(inp).detach()
if self.flip_input:
out += flip(self.face_alignment_net(flip(inp)).detach(), is_label=True)
out = out.to(device='cpu', dtype=torch.float32).numpy()
pts, pts_img, scores = get_preds_fromhm(out, center.numpy(), scale)
pts, pts_img = torch.from_numpy(pts), torch.from_numpy(pts_img)
pts, pts_img = pts.view(68, 2) * 4, pts_img.view(68, 2)
scores = scores.squeeze(0)
if self.landmarks_type == LandmarksType.THREE_D:
heatmaps = np.zeros((68, 256, 256), dtype=np.float32)
for i in range(68):
if pts[i, 0] > 0 and pts[i, 1] > 0:
heatmaps[i] = draw_gaussian(
heatmaps[i], pts[i], 2)
heatmaps = torch.from_numpy(
heatmaps).unsqueeze_(0)
heatmaps = heatmaps.to(self.device, dtype=self.dtype)
depth_pred = self.depth_prediciton_net(
torch.cat((inp, heatmaps), 1)).data.cpu().view(68, 1).to(dtype=torch.float32)
pts_img = torch.cat(
(pts_img, depth_pred * (1.0 / (256.0 / (200.0 * scale)))), 1)
landmarks.append(pts_img.numpy())
landmarks_scores.append(scores)
if not return_bboxes:
detected_faces = None
if not return_landmark_score:
landmarks_scores = None
if return_bboxes or return_landmark_score:
return landmarks, landmarks_scores, detected_faces
else:
return landmarks
@torch.no_grad()
def get_landmarks_from_batch(self, image_batch, detected_faces=None, return_bboxes=False,
return_landmark_score=False):
"""Predict the landmarks for each face present in the image.
This function predicts a set of 68 2D or 3D images, one for each image in a batch in parallel.
If detect_faces is None the method will also run a face detector.
Arguments:
image_batch {torch.tensor} -- The input images batch
Keyword Arguments:
detected_faces {list of numpy.array} -- list of bounding boxes, one for each face found
in the image (default: {None})
return_bboxes {boolean} -- If True, return the face bounding boxes in addition to the keypoints.
return_landmark_score {boolean} -- If True, return the keypoint scores along with the keypoints.
Return:
result:
1. if both return_bboxes and return_landmark_score are False, result will be:
landmarks
2. Otherwise, result will be one of the following, depending on the actual value of return_* arguments.
(landmark, landmark_score, detected_face)
(landmark, None, detected_face)
(landmark, landmark_score, None )
"""
if detected_faces is None:
detected_faces = self.face_detector.detect_from_batch(image_batch)
if len(detected_faces) == 0:
warnings.warn("No faces were detected.")
if return_bboxes or return_landmark_score:
return None, None, None
else:
return None
landmarks = []
landmarks_scores_list = []
# A batch for each frame
for i, faces in enumerate(detected_faces):
res = self.get_landmarks_from_image(
image_batch[i].cpu().numpy().transpose(1, 2, 0),
detected_faces=faces,
return_landmark_score=return_landmark_score,
)
if return_landmark_score:
landmark_set, landmarks_scores, _ = res
landmarks_scores_list.append(landmarks_scores)
else:
landmark_set = res
# Bacward compatibility
if landmark_set is not None:
landmark_set = np.concatenate(landmark_set, axis=0)
else:
landmark_set = []
landmarks.append(landmark_set)
if not return_bboxes:
detected_faces = None
if not return_landmark_score:
landmarks_scores_list = None
if return_bboxes or return_landmark_score:
return landmarks, landmarks_scores_list, detected_faces
else:
return landmarks
def get_landmarks_from_directory(self, path, extensions=['.jpg', '.png'], recursive=True, show_progress_bar=True,
return_bboxes=False, return_landmark_score=False):
"""Scan a directory for images with a given extension type(s) and predict the landmarks for each
face present in the images found.
Arguments:
path {str} -- path to the target directory containing the images
Keyword Arguments:
extensions {list of str} -- list containing the image extensions considered (default: ['.jpg', '.png'])
recursive {boolean} -- If True, scans for images recursively (default: True)
show_progress_bar {boolean} -- If True displays a progress bar (default: True)
return_bboxes {boolean} -- If True, return the face bounding boxes in addition to the keypoints.
return_landmark_score {boolean} -- If True, return the keypoint scores along with the keypoints.
"""
dataset = FolderData(path, self.face_detector.tensor_or_path_to_ndarray, extensions, recursive, self.verbose)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, num_workers=2, prefetch_factor=4)
predictions = {}
for (image_path, image) in tqdm(dataloader, disable=not show_progress_bar):
image_path, image = image_path[0], image[0]
bounding_boxes = self.face_detector.detect_from_image(image)
if return_bboxes or return_landmark_score:
preds, bbox, score = self.get_landmarks_from_image(
image, bounding_boxes, return_bboxes=return_bboxes, return_landmark_score=return_landmark_score)
predictions[image_path] = (preds, bbox, score)
else:
preds = self.get_landmarks_from_image(image, bounding_boxes)
predictions[image_path] = preds
return predictions
+1
View File
@@ -0,0 +1 @@
from .core import FaceDetector
@@ -0,0 +1 @@
from .blazeface_detector import BlazeFaceDetector as FaceDetector
@@ -0,0 +1,69 @@
from torch.utils.model_zoo import load_url
from ..core import FaceDetector
from ...utils import load_file_from_url
from .net_blazeface import BlazeFace
from .detect import *
models_urls = {
'blazeface_weights': 'https://github.com/hollance/BlazeFace-PyTorch/blob/master/blazeface.pth?raw=true',
'blazeface_anchors': 'https://github.com/hollance/BlazeFace-PyTorch/blob/master/anchors.npy?raw=true',
'blazeface_back_weights': 'https://github.com/hollance/BlazeFace-PyTorch/blob/master/blazefaceback.pth?raw=true',
'blazeface_back_anchors': 'https://github.com/hollance/BlazeFace-PyTorch/blob/master/anchorsback.npy?raw=true'
}
class BlazeFaceDetector(FaceDetector):
def __init__(self, device, back_model=False, path_to_detector=None, path_to_anchor=None, verbose=False,
min_score_thresh=0.5, min_suppression_threshold=0.3):
super(BlazeFaceDetector, self).__init__(device, verbose)
# Initialise the face detector
self.back_model = back_model
if path_to_detector is None:
if back_model:
model_weights = load_url(models_urls['blazeface_back_weights'])
model_anchors = np.load(load_file_from_url(models_urls['blazeface_back_anchors']))
else:
model_weights = load_url(models_urls['blazeface_weights'])
model_anchors = np.load(load_file_from_url(models_urls['blazeface_anchors']))
else:
model_weights = torch.load(path_to_detector)
model_anchors = np.load(path_to_anchor)
self.face_detector = BlazeFace(back_model=back_model)
self.face_detector.load_state_dict(model_weights)
self.face_detector.load_anchors_from_npy(model_anchors, device)
# Optionally change the thresholds:
self.face_detector.min_score_thresh = min_score_thresh
self.face_detector.min_suppression_threshold = min_suppression_threshold
self.face_detector.to(device)
self.face_detector.eval()
def detect_from_image(self, tensor_or_path):
image = self.tensor_or_path_to_ndarray(tensor_or_path)
image_size = 256 if self.back_model else 128
bboxlist = detect(self.face_detector, image, target_size=image_size, device=self.device)[0]
return bboxlist
def detect_from_batch(self, tensor):
image_size = 256 if self.back_model else 128
bboxlists = batch_detect(self.face_detector, tensor, target_size=image_size, device=self.device)
return bboxlists
@property
def reference_scale(self):
return 195
@property
def reference_x_shift(self):
return 0
@property
def reference_y_shift(self):
return 0
@@ -0,0 +1,74 @@
import torch
import torch.nn.functional as F
import cv2
import numpy as np
from .utils import *
def detect(net, img, target_size=128, device='cuda'):
H, W, C = img.shape
orig_size = min(H, W)
img, (xshift, yshift) = resize_and_crop_image(img, target_size)
preds = net.predict_on_image(img)
if 0 == len(preds):
return [[]]
shift = np.array([xshift, yshift] * 2)
scores = preds[:, -1:]
# TODO: ugly
# reverses, x and y to adapt with face-alignment code
locs = np.concatenate((preds[:, 1:2], preds[:, 0:1], preds[:, 3:4], preds[:, 2:3]), axis=1)
return [np.concatenate((locs * orig_size + shift, scores), axis=1)]
def batch_detect(net, img_batch, target_size=128, device='cuda'):
"""
Inputs:
- net: BlazeFace model
- img_batch: a numpy array or tensor of shape (Batch size, Channels, Height, Width)
- target_size: target size of the input image
Outputs:
- list of 2-dim numpy arrays with shape (faces_on_this_image, 5): x1, y1, x2, y2, confidence
(x1, y1) - top left corner, (x2, y2) - bottom right corner
"""
B, C, H, W = img_batch.shape
orig_size = min(H, W)
if isinstance(img_batch, torch.Tensor):
img_batch = img_batch.cpu().numpy()
img_batch = img_batch.transpose((0, 2, 3, 1))
imgs, (xshift, yshift) = resize_and_crop_batch(img_batch, target_size)
preds = net.predict_on_batch(imgs)
bboxlists = []
for pred in preds:
shift = np.array([xshift, yshift] * 2)
scores = pred[:, -1:]
locs = np.concatenate((pred[:, 1:2], pred[:, 0:1], pred[:, 3:4], pred[:, 2:3]), axis=1)
bboxlists.append(np.concatenate((locs * orig_size + shift, scores), axis=1))
return bboxlists
def flip_detect(net, img, device):
img = cv2.flip(img, 1)
b = detect(net, img, device)
bboxlist = np.zeros(b.shape)
bboxlist[:, 0] = img.shape[1] - b[:, 2]
bboxlist[:, 1] = b[:, 1]
bboxlist[:, 2] = img.shape[1] - b[:, 0]
bboxlist[:, 3] = b[:, 3]
bboxlist[:, 4] = b[:, 4]
return bboxlist
def pts_to_bb(pts):
min_x, min_y = np.min(pts, axis=0)
max_x, max_y = np.max(pts, axis=0)
return np.array([min_x, min_y, max_x, max_y])
@@ -0,0 +1,482 @@
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
class BlazeBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size=3, stride=1):
super(BlazeBlock, self).__init__()
self.stride = stride
self.channel_pad = out_channels - in_channels
# TFLite uses slightly different padding than PyTorch
# on the depthwise conv layer when the stride is 2.
if stride == 2:
self.max_pool = nn.MaxPool2d(kernel_size=stride, stride=stride)
padding = 0
else:
padding = (kernel_size - 1) // 2
self.convs = nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=in_channels,
kernel_size=kernel_size, stride=stride, padding=padding,
groups=in_channels, bias=True),
nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
kernel_size=1, stride=1, padding=0, bias=True),
)
self.act = nn.ReLU(inplace=True)
def forward(self, x):
if self.stride == 2:
h = F.pad(x, (0, 2, 0, 2), "constant", 0)
x = self.max_pool(x)
else:
h = x
if self.channel_pad > 0:
x = F.pad(x, (0, 0, 0, 0, 0, self.channel_pad), "constant", 0)
return self.act(self.convs(h) + x)
class FinalBlazeBlock(nn.Module):
def __init__(self, channels, kernel_size=3):
super(FinalBlazeBlock, self).__init__()
# TFLite uses slightly different padding than PyTorch
# on the depthwise conv layer when the stride is 2.
self.convs = nn.Sequential(
nn.Conv2d(in_channels=channels, out_channels=channels,
kernel_size=kernel_size, stride=2, padding=0,
groups=channels, bias=True),
nn.Conv2d(in_channels=channels, out_channels=channels,
kernel_size=1, stride=1, padding=0, bias=True),
)
self.act = nn.ReLU(inplace=True)
def forward(self, x):
h = F.pad(x, (0, 2, 0, 2), "constant", 0)
return self.act(self.convs(h))
class BlazeFace(nn.Module):
"""The BlazeFace face detection model from MediaPipe.
The version from MediaPipe is simpler than the one in the paper;
it does not use the "double" BlazeBlocks.
Because we won't be training this model, it doesn't need to have
batchnorm layers. These have already been "folded" into the conv
weights by TFLite.
The conversion to PyTorch is fairly straightforward, but there are
some small differences between TFLite and PyTorch in how they handle
padding on conv layers with stride 2.
This version works on batches, while the MediaPipe version can only
handle a single image at a time.
Based on code from https://github.com/tkat0/PyTorch_BlazeFace/ and
https://github.com/google/mediapipe/
"""
def __init__(self, back_model=False):
super(BlazeFace, self).__init__()
# These are the settings from the MediaPipe example graph
# mediapipe/graphs/face_detection/face_detection_mobile_gpu.pbtxt
# and mediapipe/graphs/face_detection/face_detection_back_mobile_gpu.pbtxt
self.num_classes = 1
self.num_anchors = 896
self.num_coords = 16
self.score_clipping_thresh = 100.0
self.back_model = back_model
if back_model:
self.x_scale = 256.0
self.y_scale = 256.0
self.h_scale = 256.0
self.w_scale = 256.0
self.min_score_thresh = 0.65
else:
self.x_scale = 128.0
self.y_scale = 128.0
self.h_scale = 128.0
self.w_scale = 128.0
self.min_score_thresh = 0.75
self.min_suppression_threshold = 0.3
self._define_layers()
def _define_back_model_layers(self):
self.backbone = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=24, kernel_size=5, stride=2, padding=0, bias=True),
nn.ReLU(inplace=True),
*[BlazeBlock(24, 24) for _ in range(7)],
BlazeBlock(24, 24, stride=2),
*[BlazeBlock(24, 24) for _ in range(7)],
BlazeBlock(24, 48, stride=2),
*[BlazeBlock(48, 48) for _ in range(7)],
BlazeBlock(48, 96, stride=2),
*[BlazeBlock(96, 96) for _ in range(7)],
)
self.final = FinalBlazeBlock(96)
self.classifier_8 = nn.Conv2d(96, 2, 1, bias=True)
self.classifier_16 = nn.Conv2d(96, 6, 1, bias=True)
self.regressor_8 = nn.Conv2d(96, 32, 1, bias=True)
self.regressor_16 = nn.Conv2d(96, 96, 1, bias=True)
def _define_front_model_layers(self):
self.backbone1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=24, kernel_size=5,
stride=2, padding=0, bias=True),
nn.ReLU(inplace=True),
BlazeBlock(24, 24),
BlazeBlock(24, 28),
BlazeBlock(28, 32, stride=2),
BlazeBlock(32, 36),
BlazeBlock(36, 42),
BlazeBlock(42, 48, stride=2),
BlazeBlock(48, 56),
BlazeBlock(56, 64),
BlazeBlock(64, 72),
BlazeBlock(72, 80),
BlazeBlock(80, 88),
)
self.backbone2 = nn.Sequential(
BlazeBlock(88, 96, stride=2),
BlazeBlock(96, 96),
BlazeBlock(96, 96),
BlazeBlock(96, 96),
BlazeBlock(96, 96),
)
self.classifier_8 = nn.Conv2d(88, 2, 1, bias=True)
self.classifier_16 = nn.Conv2d(96, 6, 1, bias=True)
self.regressor_8 = nn.Conv2d(88, 32, 1, bias=True)
self.regressor_16 = nn.Conv2d(96, 96, 1, bias=True)
def _define_layers(self):
if self.back_model:
self._define_back_model_layers()
else:
self._define_front_model_layers()
def forward(self, x):
# TFLite uses slightly different padding on the first conv layer
# than PyTorch, so do it manually.
x = F.pad(x, (1, 2, 1, 2), "constant", 0)
b = x.shape[0] # batch size, needed for reshaping later
if self.back_model:
x = self.backbone(x) # (b, 16, 16, 96)
h = self.final(x) # (b, 8, 8, 96)
else:
x = self.backbone1(x) # (b, 88, 16, 16)
h = self.backbone2(x) # (b, 96, 8, 8)
# Note: Because PyTorch is NCHW but TFLite is NHWC, we need to
# permute the output from the conv layers before reshaping it.
c1 = self.classifier_8(x) # (b, 2, 16, 16)
c1 = c1.permute(0, 2, 3, 1) # (b, 16, 16, 2)
c1 = c1.reshape(b, -1, 1) # (b, 512, 1)
c2 = self.classifier_16(h) # (b, 6, 8, 8)
c2 = c2.permute(0, 2, 3, 1) # (b, 8, 8, 6)
c2 = c2.reshape(b, -1, 1) # (b, 384, 1)
c = torch.cat((c1, c2), dim=1) # (b, 896, 1)
r1 = self.regressor_8(x) # (b, 32, 16, 16)
r1 = r1.permute(0, 2, 3, 1) # (b, 16, 16, 32)
r1 = r1.reshape(b, -1, 16) # (b, 512, 16)
r2 = self.regressor_16(h) # (b, 96, 8, 8)
r2 = r2.permute(0, 2, 3, 1) # (b, 8, 8, 96)
r2 = r2.reshape(b, -1, 16) # (b, 384, 16)
r = torch.cat((r1, r2), dim=1) # (b, 896, 16)
return [r, c]
def _device(self):
"""Which device (CPU or GPU) is being used by this model?"""
return self.classifier_8.weight.device
def load_weights(self, path):
self.load_state_dict(torch.load(path))
self.eval()
def load_anchors(self, path, device=None):
device = device or self._device()
self.anchors = torch.tensor(
np.load(path), dtype=torch.float32, device=device)
assert(self.anchors.ndimension() == 2)
assert(self.anchors.shape[0] == self.num_anchors)
assert(self.anchors.shape[1] == 4)
def load_anchors_from_npy(self, arr, device=None):
device = device or self._device()
self.anchors = torch.tensor(
arr, dtype=torch.float32, device=device)
assert(self.anchors.ndimension() == 2)
assert(self.anchors.shape[0] == self.num_anchors)
assert(self.anchors.shape[1] == 4)
def _preprocess(self, x):
"""Converts the image pixels to the range [-1, 1]."""
return x.float() / 127.5 - 1.0
def predict_on_image(self, img):
"""Makes a prediction on a single image.
Arguments:
img: a NumPy array of shape (H, W, 3) or a PyTorch tensor of
shape (3, H, W). The image's height and width should be
128 pixels.
Returns:
A tensor with face detections.
"""
if isinstance(img, np.ndarray):
img = torch.from_numpy(img).permute((2, 0, 1))
return self.predict_on_batch(img.unsqueeze(0))[0]
def predict_on_batch(self, x):
"""Makes a prediction on a batch of images.
Arguments:
x: a NumPy array of shape (b, H, W, 3) or a PyTorch tensor of
shape (b, 3, H, W). The height and width should be 128 pixels.
Returns:
A list containing a tensor of face detections for each image in
the batch. If no faces are found for an image, returns a tensor
of shape (0, 17).
Each face detection is a PyTorch tensor consisting of 17 numbers:
- ymin, xmin, ymax, xmax
- x,y-coordinates for the 6 keypoints
- confidence score
"""
if isinstance(x, np.ndarray):
x = torch.from_numpy(x).permute((0, 3, 1, 2))
assert x.shape[1] == 3
if self.back_model:
assert x.shape[2] == 256
assert x.shape[3] == 256
else:
assert x.shape[2] == 128
assert x.shape[3] == 128
# 1. Preprocess the images into tensors:
x = x.to(self._device())
x = self._preprocess(x)
# 2. Run the neural network:
with torch.inference_mode():
out = self.__call__(x)
# 3. Postprocess the raw predictions:
detections = self._tensors_to_detections(out[0], out[1], self.anchors)
# 4. Non-maximum suppression to remove overlapping detections:
filtered_detections = []
for i in range(len(detections)):
faces = self._weighted_non_max_suppression(detections[i])
faces = torch.stack(faces) if len(
faces) > 0 else torch.zeros((0, 17))
filtered_detections.append(faces)
return filtered_detections
def _tensors_to_detections(self, raw_box_tensor, raw_score_tensor, anchors):
"""The output of the neural network is a tensor of shape (b, 896, 16)
containing the bounding box regressor predictions, as well as a tensor
of shape (b, 896, 1) with the classification confidences.
This function converts these two "raw" tensors into proper detections.
Returns a list of (num_detections, 17) tensors, one for each image in
the batch.
This is based on the source code from:
mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.cc
mediapipe/calculators/tflite/tflite_tensors_to_detections_calculator.proto
"""
assert raw_box_tensor.ndimension() == 3
assert raw_box_tensor.shape[1] == self.num_anchors
assert raw_box_tensor.shape[2] == self.num_coords
assert raw_score_tensor.ndimension() == 3
assert raw_score_tensor.shape[1] == self.num_anchors
assert raw_score_tensor.shape[2] == self.num_classes
assert raw_box_tensor.shape[0] == raw_score_tensor.shape[0]
detection_boxes = self._decode_boxes(raw_box_tensor, anchors)
thresh = self.score_clipping_thresh
raw_score_tensor = raw_score_tensor.clamp(-thresh, thresh)
detection_scores = raw_score_tensor.sigmoid().squeeze(dim=-1)
# Note: we stripped off the last dimension from the scores tensor
# because there is only has one class. Now we can simply use a mask
# to filter out the boxes with too low confidence.
mask = detection_scores >= self.min_score_thresh
# Because each image from the batch can have a different number of
# detections, process them one at a time using a loop.
output_detections = []
for i in range(raw_box_tensor.shape[0]):
boxes = detection_boxes[i, mask[i]]
scores = detection_scores[i, mask[i]].unsqueeze(dim=-1)
output_detections.append(torch.cat((boxes, scores), dim=-1).to('cpu'))
return output_detections
def _decode_boxes(self, raw_boxes, anchors):
"""Converts the predictions into actual coordinates using
the anchor boxes. Processes the entire batch at once.
"""
boxes = torch.zeros_like(raw_boxes)
x_center = raw_boxes[..., 0] / self.x_scale * \
anchors[:, 2] + anchors[:, 0]
y_center = raw_boxes[..., 1] / self.y_scale * \
anchors[:, 3] + anchors[:, 1]
w = raw_boxes[..., 2] / self.w_scale * anchors[:, 2]
h = raw_boxes[..., 3] / self.h_scale * anchors[:, 3]
boxes[..., 0] = y_center - h / 2. # ymin
boxes[..., 1] = x_center - w / 2. # xmin
boxes[..., 2] = y_center + h / 2. # ymax
boxes[..., 3] = x_center + w / 2. # xmax
for k in range(6):
offset = 4 + k * 2
keypoint_x = raw_boxes[..., offset] / \
self.x_scale * anchors[:, 2] + anchors[:, 0]
keypoint_y = raw_boxes[..., offset + 1] / \
self.y_scale * anchors[:, 3] + anchors[:, 1]
boxes[..., offset] = keypoint_x
boxes[..., offset + 1] = keypoint_y
return boxes
def _weighted_non_max_suppression(self, detections):
"""The alternative NMS method as mentioned in the BlazeFace paper:
"We replace the suppression algorithm with a blending strategy that
estimates the regression parameters of a bounding box as a weighted
mean between the overlapping predictions."
The original MediaPipe code assigns the score of the most confident
detection to the weighted detection, but we take the average score
of the overlapping detections.
The input detections should be a Tensor of shape (count, 17).
Returns a list of PyTorch tensors, one for each detected face.
This is based on the source code from:
mediapipe/calculators/util/non_max_suppression_calculator.cc
mediapipe/calculators/util/non_max_suppression_calculator.proto
"""
if len(detections) == 0:
return []
output_detections = []
# Sort the detections from highest to lowest score.
remaining = torch.argsort(detections[:, 16], descending=True)
while len(remaining) > 0:
detection = detections[remaining[0]]
# Compute the overlap between the first box and the other
# remaining boxes. (Note that the other_boxes also include
# the first_box.)
first_box = detection[:4]
other_boxes = detections[remaining, :4]
ious = overlap_similarity(first_box, other_boxes)
# If two detections don't overlap enough, they are considered
# to be from different faces.
mask = ious > self.min_suppression_threshold
overlapping = remaining[mask]
remaining = remaining[~mask]
# Take an average of the coordinates from the overlapping
# detections, weighted by their confidence scores.
weighted_detection = detection.clone()
if len(overlapping) > 1:
coordinates = detections[overlapping, :16]
scores = detections[overlapping, 16:17]
total_score = scores.sum()
weighted = (coordinates * scores).sum(dim=0) / total_score
weighted_detection[:16] = weighted
weighted_detection[16] = total_score / len(overlapping)
output_detections.append(weighted_detection)
return output_detections
# IOU code from https://github.com/amdegroot/ssd.pytorch/blob/master/layers/box_utils.py
def intersect(box_a, box_b):
""" We resize both tensors to [A,B,2] without new malloc:
[A,2] -> [A,1,2] -> [A,B,2]
[B,2] -> [1,B,2] -> [A,B,2]
Then we compute the area of intersect between box_a and box_b.
Args:
box_a: (tensor) bounding boxes, Shape: [A,4].
box_b: (tensor) bounding boxes, Shape: [B,4].
Return:
(tensor) intersection area, Shape: [A,B].
"""
A = box_a.size(0)
B = box_b.size(0)
max_xy = torch.min(box_a[:, 2:].unsqueeze(1).expand(A, B, 2),
box_b[:, 2:].unsqueeze(0).expand(A, B, 2))
min_xy = torch.max(box_a[:, :2].unsqueeze(1).expand(A, B, 2),
box_b[:, :2].unsqueeze(0).expand(A, B, 2))
inter = torch.clamp((max_xy - min_xy), min=0)
return inter[:, :, 0] * inter[:, :, 1]
def jaccard(box_a, box_b):
"""Compute the jaccard overlap of two sets of boxes. The jaccard overlap
is simply the intersection over union of two boxes. Here we operate on
ground truth boxes and default boxes.
E.g.:
A ∩ B / A ∪ B = A ∩ B / (area(A) + area(B) - A ∩ B)
Args:
box_a: (tensor) Ground truth bounding boxes, Shape: [num_objects,4]
box_b: (tensor) Prior boxes from priorbox layers, Shape: [num_priors,4]
Return:
jaccard overlap: (tensor) Shape: [box_a.size(0), box_b.size(0)]
"""
inter = intersect(box_a, box_b)
area_a = ((box_a[:, 2] - box_a[:, 0])
* (box_a[:, 3] - box_a[:, 1])).unsqueeze(1).expand_as(inter) # [A,B]
area_b = ((box_b[:, 2] - box_b[:, 0])
* (box_b[:, 3] - box_b[:, 1])).unsqueeze(0).expand_as(inter) # [A,B]
union = area_a + area_b - inter
return inter / union # [A,B]
def overlap_similarity(box, other_boxes):
"""Computes the IOU between a bounding box and set of other boxes."""
return jaccard(box.unsqueeze(0), other_boxes).squeeze(0)
@@ -0,0 +1,65 @@
import cv2
import numpy as np
def image_resize(image, width=None, height=None, inter=cv2.INTER_AREA):
# initialize the dimensions of the image to be resized and
# grab the image size
dim = None
(h, w) = image.shape[:2]
# if both the width and height are None, then return the
# original image
if width is None and height is None:
return image
# check to see if the width is None
if width is None:
# calculate the ratio of the height and construct the
# dimensions
r = height / float(h)
dim = (int(w * r), height)
# otherwise, the height is None
else:
# calculate the ratio of the width and construct the
# dimensions
r = width / float(w)
dim = (width, int(h * r))
# resize the image
resized = cv2.resize(image, dim, interpolation=inter)
# return the resized image
return resized
def resize_and_crop_image(image, dim):
if image.shape[0] > image.shape[1]:
img = image_resize(image, width=dim)
yshift, xshift = (image.shape[0] - image.shape[1]) // 2, 0
y_start = (img.shape[0] - img.shape[1]) // 2
y_end = y_start + dim
return img[y_start:y_end, :, :], (xshift, yshift)
else:
img = image_resize(image, height=dim)
yshift, xshift = 0, (image.shape[1] - image.shape[0]) // 2
x_start = (img.shape[1] - img.shape[0]) // 2
x_end = x_start + dim
return img[:, x_start:x_end, :], (xshift, yshift)
def resize_and_crop_batch(frames, dim):
"""
Center crop + resize to (dim x dim)
inputs:
- frames: list of images (numpy arrays)
- dim: output dimension size
"""
smframes = []
xshift, yshift = 0, 0
for i in range(len(frames)):
smframe, (xshift, yshift) = resize_and_crop_image(frames[i], dim)
smframes.append(smframe)
smframes = np.stack(smframes)
return smframes, (xshift, yshift)
+150
View File
@@ -0,0 +1,150 @@
import logging
import glob
from tqdm import tqdm
import numpy as np
import torch
from skimage import io
class FaceDetector(object):
"""An abstract class representing a face detector.
Any other face detection implementation must subclass it. All subclasses
must implement ``detect_from_image``, that return a list of detected
bounding boxes. Optionally, for speed considerations detect from path is
recommended.
"""
def __init__(self, device, verbose):
self.device = device
self.verbose = verbose
if verbose:
if 'cpu' in device:
logger = logging.getLogger(__name__)
logger.warning("Detection running on CPU, this may be potentially slow.")
if 'cpu' not in device and 'cuda' not in device and 'mps' not in device:
if verbose:
logger.error("Expected values for device are: {cpu, cuda, mps} but got: %s", device)
raise ValueError
def detect_from_image(self, tensor_or_path):
"""Detects faces in a given image.
This function detects the faces present in a provided BGR(usually)
image. The input can be either the image itself or the path to it.
Arguments:
tensor_or_path {numpy.ndarray, torch.tensor or string} -- the path
to an image or the image itself.
Example::
>>> path_to_image = 'data/image_01.jpg'
... detected_faces = detect_from_image(path_to_image)
[A list of bounding boxes (x1, y1, x2, y2)]
>>> image = cv2.imread(path_to_image)
... detected_faces = detect_from_image(image)
[A list of bounding boxes (x1, y1, x2, y2)]
"""
raise NotImplementedError
def detect_from_batch(self, tensor):
"""Detects faces in a given image.
This function detects the faces present in a provided BGR(usually)
image. The input can be either the image itself or the path to it.
Arguments:
tensor {torch.tensor} -- image batch tensor.
Example::
>>> path_to_image = 'data/image_01.jpg'
... detected_faces = detect_from_image(path_to_image)
[A list of bounding boxes (x1, y1, x2, y2)]
>>> image = cv2.imread(path_to_image)
... detected_faces = detect_from_image(image)
[A list of bounding boxes (x1, y1, x2, y2)]
"""
raise NotImplementedError
def detect_from_directory(self, path, extensions=['.jpg', '.png'], recursive=False, show_progress_bar=True):
"""Detects faces from all the images present in a given directory.
Arguments:
path {string} -- a string containing a path that points to the folder containing the images
Keyword Arguments:
extensions {list} -- list of string containing the extensions to be
consider in the following format: ``.extension_name`` (default:
{['.jpg', '.png']}) recursive {bool} -- option wherever to scan the
folder recursively (default: {False}) show_progress_bar {bool} --
display a progressbar (default: {True})
Example:
>>> directory = 'data'
... detected_faces = detect_from_directory(directory)
{A dictionary of [lists containing bounding boxes(x1, y1, x2, y2)]}
"""
if self.verbose:
logger = logging.getLogger(__name__)
if len(extensions) == 0:
if self.verbose:
logger.error("Expected at list one extension, but none was received.")
raise ValueError
if self.verbose:
logger.info("Constructing the list of images.")
additional_pattern = '/**/*' if recursive else '/*'
files = []
for extension in extensions:
files.extend(glob.glob(path + additional_pattern + extension, recursive=recursive))
if self.verbose:
logger.info("Finished searching for images. %s images found", len(files))
logger.info("Preparing to run the detection.")
predictions = {}
for image_path in tqdm(files, disable=not show_progress_bar):
if self.verbose:
logger.info("Running the face detector on image: %s", image_path)
predictions[image_path] = self.detect_from_image(image_path)
if self.verbose:
logger.info("The detector was successfully run on all %s images", len(files))
return predictions
@property
def reference_scale(self):
raise NotImplementedError
@property
def reference_x_shift(self):
raise NotImplementedError
@property
def reference_y_shift(self):
raise NotImplementedError
@staticmethod
def tensor_or_path_to_ndarray(tensor_or_path):
"""Convert path (represented as a string) or torch.tensor to a numpy.ndarray
Arguments:
tensor_or_path {numpy.ndarray, torch.tensor or string} -- path to the image, or the image itself
"""
if isinstance(tensor_or_path, str):
return io.imread(tensor_or_path)
elif torch.is_tensor(tensor_or_path):
return tensor_or_path.cpu().numpy()
elif isinstance(tensor_or_path, np.ndarray):
return tensor_or_path
else:
raise TypeError
@@ -0,0 +1 @@
from .dlib_detector import DlibDetector as FaceDetector
@@ -0,0 +1,48 @@
import warnings
import cv2
import dlib
from ..core import FaceDetector
from ...utils import load_file_from_url
class DlibDetector(FaceDetector):
def __init__(self, device, path_to_detector=None, verbose=False):
super().__init__(device, verbose)
warnings.warn('Warning: this detector is deprecated. Please use a different one, i.e.: S3FD.')
# Initialise the face detector
if 'cuda' in device:
if path_to_detector is None:
path_to_detector = load_file_from_url(
"https://www.adrianbulat.com/downloads/dlib/mmod_human_face_detector.dat")
self.face_detector = dlib.cnn_face_detection_model_v1(path_to_detector)
else:
self.face_detector = dlib.get_frontal_face_detector()
def detect_from_image(self, tensor_or_path):
image = self.tensor_or_path_to_ndarray(tensor_or_path)
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
detected_faces = self.face_detector(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY))
if 'cuda' not in self.device:
detected_faces = [[d.left(), d.top(), d.right(), d.bottom()] for d in detected_faces]
else:
detected_faces = [[d.rect.left(), d.rect.top(), d.rect.right(), d.rect.bottom()] for d in detected_faces]
return detected_faces
@property
def reference_scale(self):
return 195
@property
def reference_x_shift(self):
return 0
@property
def reference_y_shift(self):
return 0
@@ -0,0 +1 @@
from .folder_detector import FolderDetector as FaceDetector
@@ -0,0 +1,53 @@
import os
import numpy as np
import torch
from ..core import FaceDetector
class FolderDetector(FaceDetector):
'''This is a simple helper module that assumes the faces were detected already
(either previously or are provided as ground truth).
The class expects to find the bounding boxes in the same format used by
the rest of face detectors, mainly ``list[(x1,y1,x2,y2),...]``.
For each image the detector will search for a file with the same name and with one of the
following extensions: .npy, .t7 or .pth
'''
def __init__(self, device, path_to_detector=None, verbose=False):
super(FolderDetector, self).__init__(device, verbose)
def detect_from_image(self, tensor_or_path):
# Only strings supported
if not isinstance(tensor_or_path, str):
raise ValueError
base_name = os.path.splitext(tensor_or_path)[0]
if os.path.isfile(base_name + '.npy'):
detected_faces = np.load(base_name + '.npy')
elif os.path.isfile(base_name + '.t7'):
detected_faces = torch.load(base_name + '.t7')
elif os.path.isfile(base_name + '.pth'):
detected_faces = torch.load(base_name + '.pth')
else:
raise FileNotFoundError
if not isinstance(detected_faces, list):
raise TypeError
return detected_faces
@property
def reference_scale(self):
return 195
@property
def reference_x_shift(self):
return 0
@property
def reference_y_shift(self):
return 0
+1
View File
@@ -0,0 +1 @@
from .sfd_detector import SFDDetector as FaceDetector
+71
View File
@@ -0,0 +1,71 @@
import math
import numpy as np
def nms(dets, thresh):
if 0 == len(dets):
return []
x1, y1, x2, y2, scores = dets[:, 0], dets[:, 1], dets[:, 2], dets[:, 3], dets[:, 4]
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1, yy1 = np.maximum(x1[i], x1[order[1:]]), np.maximum(y1[i], y1[order[1:]])
xx2, yy2 = np.minimum(x2[i], x2[order[1:]]), np.minimum(y2[i], y2[order[1:]])
w, h = np.maximum(0.0, xx2 - xx1 + 1), np.maximum(0.0, yy2 - yy1 + 1)
ovr = w * h / (areas[i] + areas[order[1:]] - w * h)
inds = np.where(ovr <= thresh)[0]
order = order[inds + 1]
return keep
def encode(matched, priors, variances):
"""Encode the variances from the priorbox layers into the ground truth boxes
we have matched (based on jaccard overlap) with the prior boxes.
Args:
matched: (tensor) Coords of ground truth for each prior in point-form
Shape: [num_priors, 4].
priors: (tensor) Prior boxes in center-offset form
Shape: [num_priors,4].
variances: (list[float]) Variances of priorboxes
Return:
encoded boxes (tensor), Shape: [num_priors, 4]
"""
# dist b/t match center and prior's center
g_cxcy = (matched[:, :2] + matched[:, 2:]) / 2 - priors[:, :2]
# encode variance
g_cxcy /= (variances[0] * priors[:, 2:])
# match wh / prior wh
g_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]
g_wh = np.log(g_wh) / variances[1]
# return target for smooth_l1_loss
return np.concatenate([g_cxcy, g_wh], 1) # [num_priors,4]
def decode(loc, priors, variances):
"""Decode locations from predictions using priors to undo
the encoding we did for offset regression at train time.
Args:
loc (tensor): location predictions for loc layers,
Shape: [num_priors,4]
priors (tensor): Prior boxes in center-offset form.
Shape: [num_priors,4].
variances: (list[float]) Variances of priorboxes
Return:
decoded bounding box predictions
"""
boxes = np.concatenate((
priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
priors[:, 2:] * np.exp(loc[:, 2:] * variances[1])), 1)
boxes[:, :2] -= boxes[:, 2:] / 2
boxes[:, 2:] += boxes[:, :2]
return boxes
+85
View File
@@ -0,0 +1,85 @@
import torch
import torch.nn.functional as F
import cv2
import numpy as np
from .bbox import *
def detect(net, img, device):
img = img.transpose(2, 0, 1)
# Creates a batch of 1
img = np.expand_dims(img, 0)
img = torch.from_numpy(img.copy()).to(device, dtype=torch.float32)
return batch_detect(net, img, device)
def batch_detect(net, img_batch, device):
"""
Inputs:
- img_batch: a torch.Tensor of shape (Batch size, Channels, Height, Width)
"""
if 'cuda' in device:
torch.backends.cudnn.benchmark = True
batch_size = img_batch.size(0)
img_batch = img_batch.to(device, dtype=torch.float32)
img_batch = img_batch.flip(-3) # RGB to BGR
img_batch = img_batch - torch.tensor([104.0, 117.0, 123.0], device=device).view(1, 3, 1, 1)
with torch.no_grad():
olist = net(img_batch) # patched uint8_t overflow error
for i in range(len(olist) // 2):
olist[i * 2] = F.softmax(olist[i * 2], dim=1)
olist = [oelem.data.cpu().numpy() for oelem in olist]
bboxlists = get_predictions(olist, batch_size)
return bboxlists
def get_predictions(olist, batch_size):
bboxlists = []
variances = [0.1, 0.2]
for i in range(len(olist) // 2):
ocls, oreg = olist[i * 2], olist[i * 2 + 1]
stride = 2**(i + 2) # 4,8,16,32,64,128
poss = zip(*np.where(ocls[:, 1, :, :] > 0.05))
for Iindex, hindex, windex in poss:
axc, ayc = stride / 2 + windex * stride, stride / 2 + hindex * stride
priors = np.array([[axc / 1.0, ayc / 1.0, stride * 4 / 1.0, stride * 4 / 1.0]])
score = ocls[:, 1, hindex, windex][:,None]
loc = oreg[:, :, hindex, windex].copy()
boxes = decode(loc, priors, variances)
bboxlists.append(np.concatenate((boxes, score), axis=1))
if len(bboxlists) == 0: # No candidates within given threshold
bboxlists = np.array([[] for _ in range(batch_size)])
else:
bboxlists = np.stack(bboxlists, axis=1)
return bboxlists
def flip_detect(net, img, device):
img = cv2.flip(img, 1)
b = detect(net, img, device)
bboxlist = np.zeros(b.shape)
bboxlist[:, 0] = img.shape[1] - b[:, 2]
bboxlist[:, 1] = b[:, 1]
bboxlist[:, 2] = img.shape[1] - b[:, 0]
bboxlist[:, 3] = b[:, 3]
bboxlist[:, 4] = b[:, 4]
return bboxlist
def pts_to_bb(pts):
min_x, min_y = np.min(pts, axis=0)
max_x, max_y = np.max(pts, axis=0)
return np.array([min_x, min_y, max_x, max_y])
+127
View File
@@ -0,0 +1,127 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
class L2Norm(nn.Module):
def __init__(self, n_channels, scale=1.0):
super(L2Norm, self).__init__()
self.n_channels = n_channels
self.scale = scale
self.eps = 1e-10
self.weight = nn.Parameter(torch.empty(self.n_channels).fill_(self.scale))
def forward(self, x):
norm = x.pow(2).sum(dim=1, keepdim=True).sqrt() + self.eps
x = x / norm * self.weight.view(1, -1, 1, 1)
return x
class s3fd(nn.Module):
def __init__(self):
super(s3fd, self).__init__()
self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1)
self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1)
self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1)
self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1)
self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1)
self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
self.fc6 = nn.Conv2d(512, 1024, kernel_size=3, stride=1, padding=3)
self.fc7 = nn.Conv2d(1024, 1024, kernel_size=1, stride=1, padding=0)
self.conv6_1 = nn.Conv2d(1024, 256, kernel_size=1, stride=1, padding=0)
self.conv6_2 = nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1)
self.conv7_1 = nn.Conv2d(512, 128, kernel_size=1, stride=1, padding=0)
self.conv7_2 = nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)
self.conv3_3_norm = L2Norm(256, scale=10)
self.conv4_3_norm = L2Norm(512, scale=8)
self.conv5_3_norm = L2Norm(512, scale=5)
self.conv3_3_norm_mbox_conf = nn.Conv2d(256, 4, kernel_size=3, stride=1, padding=1)
self.conv3_3_norm_mbox_loc = nn.Conv2d(256, 4, kernel_size=3, stride=1, padding=1)
self.conv4_3_norm_mbox_conf = nn.Conv2d(512, 2, kernel_size=3, stride=1, padding=1)
self.conv4_3_norm_mbox_loc = nn.Conv2d(512, 4, kernel_size=3, stride=1, padding=1)
self.conv5_3_norm_mbox_conf = nn.Conv2d(512, 2, kernel_size=3, stride=1, padding=1)
self.conv5_3_norm_mbox_loc = nn.Conv2d(512, 4, kernel_size=3, stride=1, padding=1)
self.fc7_mbox_conf = nn.Conv2d(1024, 2, kernel_size=3, stride=1, padding=1)
self.fc7_mbox_loc = nn.Conv2d(1024, 4, kernel_size=3, stride=1, padding=1)
self.conv6_2_mbox_conf = nn.Conv2d(512, 2, kernel_size=3, stride=1, padding=1)
self.conv6_2_mbox_loc = nn.Conv2d(512, 4, kernel_size=3, stride=1, padding=1)
self.conv7_2_mbox_conf = nn.Conv2d(256, 2, kernel_size=3, stride=1, padding=1)
self.conv7_2_mbox_loc = nn.Conv2d(256, 4, kernel_size=3, stride=1, padding=1)
def forward(self, x):
h = F.relu(self.conv1_1(x), inplace=True)
h = F.relu(self.conv1_2(h), inplace=True)
h = F.max_pool2d(h, 2, 2)
h = F.relu(self.conv2_1(h), inplace=True)
h = F.relu(self.conv2_2(h), inplace=True)
h = F.max_pool2d(h, 2, 2)
h = F.relu(self.conv3_1(h), inplace=True)
h = F.relu(self.conv3_2(h), inplace=True)
h = F.relu(self.conv3_3(h), inplace=True)
f3_3 = h
h = F.max_pool2d(h, 2, 2)
h = F.relu(self.conv4_1(h), inplace=True)
h = F.relu(self.conv4_2(h), inplace=True)
h = F.relu(self.conv4_3(h), inplace=True)
f4_3 = h
h = F.max_pool2d(h, 2, 2)
h = F.relu(self.conv5_1(h), inplace=True)
h = F.relu(self.conv5_2(h), inplace=True)
h = F.relu(self.conv5_3(h), inplace=True)
f5_3 = h
h = F.max_pool2d(h, 2, 2)
h = F.relu(self.fc6(h), inplace=True)
h = F.relu(self.fc7(h), inplace=True)
ffc7 = h
h = F.relu(self.conv6_1(h), inplace=True)
h = F.relu(self.conv6_2(h), inplace=True)
f6_2 = h
h = F.relu(self.conv7_1(h), inplace=True)
h = F.relu(self.conv7_2(h), inplace=True)
f7_2 = h
f3_3 = self.conv3_3_norm(f3_3)
f4_3 = self.conv4_3_norm(f4_3)
f5_3 = self.conv5_3_norm(f5_3)
cls1 = self.conv3_3_norm_mbox_conf(f3_3)
reg1 = self.conv3_3_norm_mbox_loc(f3_3)
cls2 = self.conv4_3_norm_mbox_conf(f4_3)
reg2 = self.conv4_3_norm_mbox_loc(f4_3)
cls3 = self.conv5_3_norm_mbox_conf(f5_3)
reg3 = self.conv5_3_norm_mbox_loc(f5_3)
cls4 = self.fc7_mbox_conf(ffc7)
reg4 = self.fc7_mbox_loc(ffc7)
cls5 = self.conv6_2_mbox_conf(f6_2)
reg5 = self.conv6_2_mbox_loc(f6_2)
cls6 = self.conv7_2_mbox_conf(f7_2)
reg6 = self.conv7_2_mbox_loc(f7_2)
# max-out background label
chunk = torch.chunk(cls1, 4, 1)
bmax = torch.max(torch.max(chunk[0], chunk[1]), chunk[2])
cls1 = torch.cat([bmax, chunk[3]], dim=1)
return [cls1, reg1, cls2, reg2, cls3, reg3, cls4, reg4, cls5, reg5, cls6, reg6]
@@ -0,0 +1,71 @@
import torch
from torch.utils.model_zoo import load_url
from ..core import FaceDetector
from .net_s3fd import s3fd
from .bbox import nms
from .detect import detect, batch_detect
models_urls = {
's3fd': 'https://www.adrianbulat.com/downloads/python-fan/s3fd-619a316812.pth',
}
class SFDDetector(FaceDetector):
'''SF3D Detector.
'''
def __init__(self, device, path_to_detector=None, verbose=False, filter_threshold=0.5):
super(SFDDetector, self).__init__(device, verbose)
# Initialise the face detector
if path_to_detector is None:
model_weights = load_url(models_urls['s3fd'])
else:
model_weights = torch.load(path_to_detector)
self.fiter_threshold = filter_threshold
self.face_detector = s3fd()
self.face_detector.load_state_dict(model_weights)
self.face_detector.to(device)
self.face_detector.eval()
def _filter_bboxes(self, bboxlist):
if len(bboxlist) > 0:
keep = nms(bboxlist, 0.3)
bboxlist = bboxlist[keep, :]
bboxlist = [x for x in bboxlist if x[-1] > self.fiter_threshold]
return bboxlist
def detect_from_image(self, tensor_or_path):
image = self.tensor_or_path_to_ndarray(tensor_or_path)
bboxlist = detect(self.face_detector, image, device=self.device)[0]
bboxlist = self._filter_bboxes(bboxlist)
return bboxlist
def detect_from_batch(self, tensor):
bboxlists = batch_detect(self.face_detector, tensor, device=self.device)
new_bboxlists = []
for i in range(bboxlists.shape[0]):
bboxlist = bboxlists[i]
bboxlist = self._filter_bboxes(bboxlist)
new_bboxlists.append(bboxlist)
return new_bboxlists
@property
def reference_scale(self):
return 195
@property
def reference_x_shift(self):
return 0
@property
def reference_y_shift(self):
return 0
+38
View File
@@ -0,0 +1,38 @@
import logging
import glob
import torch
class FolderData(torch.utils.data.Dataset):
def __init__(self, path, transforms, extensions=['.jpg', '.png'], recursive=False, verbose=False):
self.verbose = verbose
if self.verbose:
logger = logging.getLogger(__name__)
if len(extensions) == 0:
if self.verbose:
logger.error("Expected at list one extension, but none was received.")
raise ValueError
if self.verbose:
logger.info("Constructing the list of images.")
additional_pattern = '/**/*' if recursive else '/*'
files = []
for extension in extensions:
files.extend(glob.glob(path + additional_pattern + extension, recursive=recursive))
if self.verbose:
logger.info("Finished searching for images. %s images found", len(files))
logger.info("Preparing to run the detection.")
self.files = files
self.transforms = transforms
def __getitem__(self, idx):
image_path = self.files[idx]
image = self.transforms(image_path)
return image_path, image
def __len__(self):
return len(self.files)
+379
View File
@@ -0,0 +1,379 @@
import os
import sys
import errno
import torch
import math
import numpy as np
import cv2
from skimage import io
from skimage import color
from numba import jit
from urllib.parse import urlparse
from torch.hub import download_url_to_file, HASH_REGEX
try:
from torch.hub import get_dir
except BaseException:
from torch.hub import _get_torch_home as get_dir
gauss_kernel = None
def _gaussian(
size=3, sigma=0.25, amplitude=1, normalize=False, width=None,
height=None, sigma_horz=None, sigma_vert=None, mean_horz=0.5,
mean_vert=0.5):
# handle some defaults
if width is None:
width = size
if height is None:
height = size
if sigma_horz is None:
sigma_horz = sigma
if sigma_vert is None:
sigma_vert = sigma
center_x = mean_horz * width + 0.5
center_y = mean_vert * height + 0.5
gauss = np.empty((height, width), dtype=np.float32)
# generate kernel
for i in range(height):
for j in range(width):
gauss[i][j] = amplitude * math.exp(-(math.pow((j + 1 - center_x) / (
sigma_horz * width), 2) / 2.0 + math.pow((i + 1 - center_y) / (sigma_vert * height), 2) / 2.0))
if normalize:
gauss = gauss / np.sum(gauss)
return gauss
def draw_gaussian(image, point, sigma):
global gauss_kernel
# Check if the gaussian is inside
ul = [math.floor(point[0] - 3 * sigma), math.floor(point[1] - 3 * sigma)]
br = [math.floor(point[0] + 3 * sigma), math.floor(point[1] + 3 * sigma)]
if (ul[0] > image.shape[1] or ul[1] > image.shape[0] or br[0] < 1 or br[1] < 1):
return image
size = 6 * sigma + 1
if gauss_kernel is None:
g = _gaussian(size)
gauss_kernel = g
else:
g = gauss_kernel
g_x = [int(max(1, -ul[0])), int(min(br[0], image.shape[1])) - int(max(1, ul[0])) + int(max(1, -ul[0]))]
g_y = [int(max(1, -ul[1])), int(min(br[1], image.shape[0])) - int(max(1, ul[1])) + int(max(1, -ul[1]))]
img_x = [int(max(1, ul[0])), int(min(br[0], image.shape[1]))]
img_y = [int(max(1, ul[1])), int(min(br[1], image.shape[0]))]
assert (g_x[0] > 0 and g_y[1] > 0)
image[img_y[0] - 1:img_y[1], img_x[0] - 1:img_x[1]
] = image[img_y[0] - 1:img_y[1], img_x[0] - 1:img_x[1]] + g[g_y[0] - 1:g_y[1], g_x[0] - 1:g_x[1]]
image[image > 1] = 1
return image
def transform(point, center, scale, resolution, invert=False):
"""Generate and affine transformation matrix.
Given a set of points, a center, a scale and a targer resolution, the
function generates and affine transformation matrix. If invert is ``True``
it will produce the inverse transformation.
Arguments:
point {torch.tensor} -- the input 2D point
center {torch.tensor or numpy.array} -- the center around which to perform the transformations
scale {float} -- the scale of the face/object
resolution {float} -- the output resolution
Keyword Arguments:
invert {bool} -- define wherever the function should produce the direct or the
inverse transformation matrix (default: {False})
"""
_pt = torch.ones(3)
_pt[0] = point[0]
_pt[1] = point[1]
h = 200.0 * scale
t = torch.eye(3)
t[0, 0] = resolution / h
t[1, 1] = resolution / h
t[0, 2] = resolution * (-center[0] / h + 0.5)
t[1, 2] = resolution * (-center[1] / h + 0.5)
if invert:
t = torch.inverse(t)
new_point = (torch.matmul(t, _pt))[0:2]
return new_point.int()
def crop(image, center, scale, resolution=256.0):
"""Center crops an image or set of heatmaps
Arguments:
image {numpy.array} -- an rgb image
center {numpy.array} -- the center of the object, usually the same as of the bounding box
scale {float} -- scale of the face
Keyword Arguments:
resolution {float} -- the size of the output cropped image (default: {256.0})
Returns:
[type] -- [description]
""" # Crop around the center point
""" Crops the image around the center. Input is expected to be an np.ndarray """
ul = transform([1, 1], center, scale, resolution, True)
br = transform([resolution, resolution], center, scale, resolution, True)
# pad = math.ceil(torch.norm((ul - br).float()) / 2.0 - (br[0] - ul[0]) / 2.0)
if image.ndim > 2:
newDim = np.array([br[1] - ul[1], br[0] - ul[0],
image.shape[2]], dtype=np.int32)
newImg = np.zeros(newDim, dtype=np.uint8)
else:
newDim = np.array([br[1] - ul[1], br[0] - ul[0]], dtype=np.int)
newImg = np.zeros(newDim, dtype=np.uint8)
ht = image.shape[0]
wd = image.shape[1]
newX = np.array(
[max(1, -ul[0] + 1), min(br[0], wd) - ul[0]], dtype=np.int32)
newY = np.array(
[max(1, -ul[1] + 1), min(br[1], ht) - ul[1]], dtype=np.int32)
oldX = np.array([max(1, ul[0] + 1), min(br[0], wd)], dtype=np.int32)
oldY = np.array([max(1, ul[1] + 1), min(br[1], ht)], dtype=np.int32)
newImg[newY[0] - 1:newY[1], newX[0] - 1:newX[1]
] = image[oldY[0] - 1:oldY[1], oldX[0] - 1:oldX[1], :]
newImg = cv2.resize(newImg, dsize=(int(resolution), int(resolution)),
interpolation=cv2.INTER_LINEAR)
return newImg
@jit(nopython=True)
def transform_np(point, center, scale, resolution, invert=False):
"""Generate and affine transformation matrix.
Given a set of points, a center, a scale and a targer resolution, the
function generates and affine transformation matrix. If invert is ``True``
it will produce the inverse transformation.
Arguments:
point {numpy.array} -- the input 2D point
center {numpy.array} -- the center around which to perform the transformations
scale {float} -- the scale of the face/object
resolution {float} -- the output resolution
Keyword Arguments:
invert {bool} -- define wherever the function should produce the direct or the
inverse transformation matrix (default: {False})
"""
_pt = np.ones(3)
_pt[0] = point[0]
_pt[1] = point[1]
h = 200.0 * scale
t = np.eye(3)
t[0, 0] = resolution / h
t[1, 1] = resolution / h
t[0, 2] = resolution * (-center[0] / h + 0.5)
t[1, 2] = resolution * (-center[1] / h + 0.5)
if invert:
t = np.ascontiguousarray(np.linalg.pinv(t))
new_point = np.dot(t, _pt)[0:2]
return new_point.astype(np.int32)
def get_preds_fromhm(hm, center=None, scale=None):
"""Obtain (x,y) coordinates given a set of N heatmaps. If the center
and the scale is provided the function will return the points also in
the original coordinate frame.
Arguments:
hm {torch.tensor} -- the predicted heatmaps, of shape [B, N, W, H]
Keyword Arguments:
center {torch.tensor} -- the center of the bounding box (default: {None})
scale {float} -- face scale (default: {None})
"""
B, C, H, W = hm.shape
hm_reshape = hm.reshape(B, C, H * W)
idx = np.argmax(hm_reshape, axis=-1)
scores = np.take_along_axis(hm_reshape, np.expand_dims(idx, axis=-1), axis=-1).squeeze(-1)
preds, preds_orig = _get_preds_fromhm(hm, idx, center, scale)
return preds, preds_orig, scores
@jit(nopython=True)
def _get_preds_fromhm(hm, idx, center=None, scale=None):
"""Obtain (x,y) coordinates given a set of N heatmaps and the
coresponding locations of the maximums. If the center
and the scale is provided the function will return the points also in
the original coordinate frame.
Arguments:
hm {torch.tensor} -- the predicted heatmaps, of shape [B, N, W, H]
Keyword Arguments:
center {torch.tensor} -- the center of the bounding box (default: {None})
scale {float} -- face scale (default: {None})
"""
B, C, H, W = hm.shape
idx += 1
preds = idx.repeat(2).reshape(B, C, 2).astype(np.float32)
preds[:, :, 0] = (preds[:, :, 0] - 1) % W + 1
preds[:, :, 1] = np.floor((preds[:, :, 1] - 1) / H) + 1
for i in range(B):
for j in range(C):
hm_ = hm[i, j, :]
pX, pY = int(preds[i, j, 0]) - 1, int(preds[i, j, 1]) - 1
if pX > 0 and pX < 63 and pY > 0 and pY < 63:
diff = np.array(
[hm_[pY, pX + 1] - hm_[pY, pX - 1],
hm_[pY + 1, pX] - hm_[pY - 1, pX]])
preds[i, j] += np.sign(diff) * 0.25
preds -= 0.5
preds_orig = np.zeros_like(preds)
if center is not None and scale is not None:
for i in range(B):
for j in range(C):
preds_orig[i, j] = transform_np(
preds[i, j], center, scale, H, True)
return preds, preds_orig
def create_target_heatmap(target_landmarks, centers, scales):
heatmaps = np.zeros((target_landmarks.shape[0], 68, 64, 64), dtype=np.float32)
for i in range(heatmaps.shape[0]):
for p in range(68):
landmark_cropped_coor = transform(target_landmarks[i, p] + 1, centers[i], scales[i], 64, invert=False)
heatmaps[i, p] = draw_gaussian(heatmaps[i, p], landmark_cropped_coor + 1, 2)
return torch.tensor(heatmaps)
def create_bounding_box(target_landmarks, expansion_factor=0.0):
"""
gets a batch of landmarks and calculates a bounding box that includes all the landmarks per set of landmarks in
the batch
:param target_landmarks: batch of landmarks of dim (n x 68 x 2). Where n is the batch size
:param expansion_factor: expands the bounding box by this factor. For example, a `expansion_factor` of 0.2 leads
to 20% increase in width and height of the boxes
:return: a batch of bounding boxes of dim (n x 4) where the second dim is (x1,y1,x2,y2)
"""
# Calc bounding box
x_y_min, _ = target_landmarks.reshape(-1, 68, 2).min(dim=1)
x_y_max, _ = target_landmarks.reshape(-1, 68, 2).max(dim=1)
# expanding the bounding box
expansion_factor /= 2
bb_expansion_x = (x_y_max[:, 0] - x_y_min[:, 0]) * expansion_factor
bb_expansion_y = (x_y_max[:, 1] - x_y_min[:, 1]) * expansion_factor
x_y_min[:, 0] -= bb_expansion_x
x_y_max[:, 0] += bb_expansion_x
x_y_min[:, 1] -= bb_expansion_y
x_y_max[:, 1] += bb_expansion_y
return torch.cat([x_y_min, x_y_max], dim=1)
def shuffle_lr(parts, pairs=None):
"""Shuffle the points left-right according to the axis of symmetry
of the object.
Arguments:
parts {torch.tensor} -- a 3D or 4D object containing the
heatmaps.
Keyword Arguments:
pairs {list of integers} -- [order of the flipped points] (default: {None})
"""
if pairs is None:
pairs = [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 27, 28, 29, 30, 35,
34, 33, 32, 31, 45, 44, 43, 42, 47, 46, 39, 38, 37, 36, 41,
40, 54, 53, 52, 51, 50, 49, 48, 59, 58, 57, 56, 55, 64, 63,
62, 61, 60, 67, 66, 65]
if parts.ndimension() == 3:
parts = parts[pairs, ...]
else:
parts = parts[:, pairs, ...]
return parts
def flip(tensor, is_label=False):
"""Flip an image or a set of heatmaps left-right
Arguments:
tensor {numpy.array or torch.tensor} -- [the input image or heatmaps]
Keyword Arguments:
is_label {bool} -- [denote wherever the input is an image or a set of heatmaps ] (default: {False})
"""
if not torch.is_tensor(tensor):
tensor = torch.from_numpy(tensor)
if is_label:
tensor = shuffle_lr(tensor).flip(tensor.ndimension() - 1)
else:
tensor = tensor.flip(tensor.ndimension() - 1)
return tensor
def get_image(image_or_path):
"""Reads an image from file or array/tensor and converts it to RGB (H,W,3).
Arguments:
tensor {Sstring, numpy.array or torch.tensor} -- [the input image or path to it]
"""
if isinstance(image_or_path, str):
try:
image = io.imread(image_or_path)
except IOError:
print("error opening file :: ", image_or_path)
return None
elif isinstance(image_or_path, torch.Tensor):
image = image_or_path.detach().cpu().numpy()
else:
image = image_or_path
if image.ndim == 2:
image = color.gray2rgb(image)
elif image.ndim == 4:
image = image[..., :3]
return image
# Pytorch load supports only pytorch models
def load_file_from_url(url, model_dir=None, progress=True, check_hash=False, file_name=None):
if model_dir is None:
hub_dir = get_dir()
model_dir = os.path.join(hub_dir, 'checkpoints')
try:
os.makedirs(model_dir)
except OSError as e:
if e.errno == errno.EEXIST:
# Directory already exists, ignore.
pass
else:
# Unexpected OSError, re-raise.
raise
parts = urlparse(url)
filename = os.path.basename(parts.path)
if file_name is not None:
filename = file_name
cached_file = os.path.join(model_dir, filename)
if not os.path.exists(cached_file):
sys.stderr.write('Downloading: "{}" to {}\n'.format(url, cached_file))
hash_prefix = None
if check_hash:
r = HASH_REGEX.search(filename) # r is Optional[Match[str]]
hash_prefix = r.group(1) if r else None
download_url_to_file(url, cached_file, hash_prefix, progress=progress)
return cached_file
+68 -1
View File
@@ -1,6 +1,7 @@
# coding: utf-8
import numpy as np
import torch
from typing import List, Union, Tuple
from dataclasses import dataclass, field
import cv2#; cv2.setNumThreads(0); cv2.ocl.setUseOpenCL(False)
@@ -142,4 +143,70 @@ class CropperMediaPipe(object):
lmk = recon_ret['pts']
ret_dct['lmk_crop'] = lmk
return ret_dct, cropped_image_256
return ret_dct, cropped_image_256
class CropperFaceAlignment(object):
def __init__(self, **kwargs) -> None:
device_id = kwargs.get('device_id', 0)
provider = kwargs.get('onnx_device', 'CPU')
face_detector_device = kwargs.get('face_detector_device', 'cuda')
face_detector = kwargs.get('face_detector', 'blazeface')
face_detector_dtype = kwargs.get('face_detector_dtype', 'fp16')
if provider != "torch_gpu":
self.landmark_runner = LandmarkRunner(
ckpt_path=os.path.join(folder_paths.models_dir, 'liveportrait', 'landmark.onnx'),
onnx_provider=provider,
device_id=device_id
)
self.landmark_runner.warmup()
else:
self.landmark_runner = LandmarkRunnerTorch(
ckpt_path=os.path.join(folder_paths.models_dir, 'liveportrait', 'landmark_model.pth'),
onnx_provider=provider,
device_id=device_id
)
from ...face_alignment import FaceAlignment, LandmarksType
if 'blazeface' in face_detector:
face_detector_kwargs = {'back_model': face_detector == 'blazeface_back_camera'}
self.fa = FaceAlignment(LandmarksType.TWO_D, flip_input=False, device=face_detector_device, dtype=face_detector_dtype, face_detector='blazeface', face_detector_kwargs=face_detector_kwargs)
else:
self.fa = FaceAlignment(LandmarksType.TWO_D, flip_input=False, device=face_detector_device, dtype=face_detector_dtype, face_detector=face_detector)
def crop_single_image(self, img_rgb, dsize, scale, vy_ratio, vx_ratio, face_index, face_index_order, rotate):
face_result = self.fa.get_landmarks_from_image(img_rgb)
if face_result is None:
ret_dct = {}
cropped_image_256 = None
return ret_dct, cropped_image_256
face_landmarks = face_result[face_index]
pts = np.array(face_landmarks)
# crop the face
ret_dct, image_crop = crop_image(
img_rgb, # ndarray
pts, # 106x2 or Nx2
dsize=dsize,
scale=scale,
vy_ratio=vy_ratio,
vx_ratio=vx_ratio,
rotate=rotate
)
# update a 256x256 version for network input or else
cropped_image_256 = cv2.resize(image_crop, (256, 256), interpolation=cv2.INTER_AREA)
ret_dct['pt_crop_256x256'] = ret_dct['pt_crop'] * 256 / dsize
input_image_size = img_rgb.shape[:2]
ret_dct['input_image_size'] = input_image_size
recon_ret = self.landmark_runner.run(img_rgb, pts)
lmk = recon_ret['pts']
ret_dct['lmk_crop'] = lmk
return ret_dct, cropped_image_256
+58
View File
@@ -24,6 +24,10 @@ try:
from .liveportrait.utils.cropper import CropperInsightFace
except:
log.warning("Can't load MediaPipe, MediaPipeCropper not available")
try:
from .liveportrait.utils.cropper import CropperFaceAlignment
except:
log.warning("Can't load FaceAlignment, CropperFaceAlignment not available")
from .liveportrait.modules.spade_generator import SPADEDecoder
from .liveportrait.modules.warping_network import WarpingNetwork
@@ -529,6 +533,58 @@ class LivePortraitLoadMediaPipeCropper:
return (self.cropper,)
class LivePortraitLoadFaceAlignmentCropper:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"face_detector": (
['blazeface', 'blazeface_back_camera', 'sfd'], {
"default": 'blazeface_back_camera'
}),
"landmarkrunner_device": (
['CPU', 'CUDA', 'ROCM', 'CoreML', 'torch_gpu'], {
"default": 'torch_gpu'
}),
"face_detector_device": (
['cuda', 'cpu', 'mps'], {
"default": 'cuda'
}),
"face_detector_dtype": (
[
"fp16",
"bf16",
"fp32",
],
{"default": "fp16"},
),
"keep_model_loaded": ("BOOLEAN", {"default": True})
},
}
RETURN_TYPES = ("LPCROPPER",)
RETURN_NAMES = ("cropper",)
FUNCTION = "crop"
CATEGORY = "LivePortrait"
def crop(self, landmarkrunner_device, keep_model_loaded, face_detector, face_detector_device, face_detector_dtype):
dtype = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[face_detector_dtype]
cropper_init_config = {
'keep_model_loaded': keep_model_loaded,
'onnx_device': landmarkrunner_device,
'face_detector_device': face_detector_device,
'face_detector': face_detector,
'face_detector_dtype': dtype
}
if not hasattr(self, 'cropper') or self.cropper is None or self.current_config != cropper_init_config:
self.current_config = cropper_init_config
self.cropper = CropperFaceAlignment(**cropper_init_config)
return (self.cropper,)
class LivePortraitCropper:
@classmethod
def INPUT_TYPES(s):
@@ -759,6 +815,7 @@ NODE_CLASS_MAPPINGS = {
"KeypointsToImage": KeypointsToImage,
"LivePortraitLoadCropper": LivePortraitLoadCropper,
"LivePortraitLoadMediaPipeCropper": LivePortraitLoadMediaPipeCropper,
"LivePortraitLoadFaceAlignmentCropper": LivePortraitLoadFaceAlignmentCropper,
"LivePortraitComposite": LivePortraitComposite,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -770,5 +827,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"KeypointsToImage": "LivePortrait KeypointsToImage",
"LivePortraitLoadCropper": "LivePortrait Load InsightFaceCropper",
"LivePortraitLoadMediaPipeCropper": "LivePortrait Load MediaPipeCropper",
"LivePortraitLoadFaceAlignmentCropper": "LivePortrait Load FaceAlignmentCropper",
"LivePortraitComposite": "LivePortrait Composite",
}