|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""" Masks functions for faceswap.py """ |
|
|
|
import inspect |
|
import logging |
|
import sys |
|
|
|
import cv2 |
|
import numpy as np |
|
import random |
|
from math import ceil, floor |
|
logger = logging.getLogger(__name__) |
|
|
|
def landmarks_to_bbox(landmarks: np.ndarray) -> np.ndarray: |
|
if not isinstance(landmarks, np.ndarray): |
|
landmarks = np.array(landmarks) |
|
assert landmarks.shape[1] == 2 |
|
x0, y0 = np.min(landmarks, axis=0) |
|
x1, y1 = np.max(landmarks, axis=0) |
|
bbox = np.array([x0, y0, x1, y1]) |
|
return bbox |
|
|
|
def mask_from_points(image: np.ndarray, points: np.ndarray) -> np.ndarray: |
|
"""8 (or omitted) - 8-connected line. |
|
4 - 4-connected line. |
|
LINE_AA - antialiased line.""" |
|
h, w = image.shape[:2] |
|
points = points.astype(int) |
|
assert points.shape[1] == 2, f"points.shape: {points.shape}" |
|
out = np.zeros((h, w), dtype=np.uint8) |
|
hull = cv2.convexHull(points.astype(int)) |
|
cv2.fillConvexPoly(out, hull, 255, lineType=4) |
|
return out |
|
|
|
def get_available_masks(): |
|
""" Return a list of the available masks for cli """ |
|
masks = sorted([name for name, obj in inspect.getmembers(sys.modules[__name__]) |
|
if inspect.isclass(obj) and name != "Mask"]) |
|
masks.append("none") |
|
|
|
return masks |
|
|
|
def landmarks_68_symmetries(): |
|
|
|
|
|
sym_ids = [9, 58, 67, 63, 52, 34, 31, 30, 29, 28] |
|
sym = { |
|
1: 17, |
|
2: 16, |
|
3: 15, |
|
4: 14, |
|
5: 13, |
|
6: 12, |
|
7: 11, |
|
8: 10, |
|
|
|
51: 53, |
|
50: 54, |
|
49: 55, |
|
60: 56, |
|
59: 57, |
|
|
|
62: 64, |
|
61: 65, |
|
68: 66, |
|
|
|
33: 35, |
|
32: 36, |
|
|
|
37: 46, |
|
38: 45, |
|
39: 44, |
|
40: 43, |
|
41: 48, |
|
42: 47, |
|
|
|
18: 27, |
|
19: 26, |
|
20: 25, |
|
21: 24, |
|
22: 23, |
|
|
|
|
|
9: 9, |
|
58: 58, |
|
67: 67, |
|
63: 63, |
|
52: 52, |
|
34: 34, |
|
31: 31, |
|
30: 30, |
|
29: 29, |
|
28: 28, |
|
} |
|
return sym, sym_ids |
|
|
|
|
|
|
|
def get_default_mask(): |
|
""" Set the default mask for cli """ |
|
masks = get_available_masks() |
|
default = "dfl_full" |
|
default = default if default in masks else masks[0] |
|
|
|
return default |
|
|
|
|
|
class Mask(): |
|
""" Parent class for masks |
|
the output mask will be <mask_type>.mask |
|
channels: 1, 3 or 4: |
|
1 - Returns a single channel mask |
|
3 - Returns a 3 channel mask |
|
4 - Returns the original image with the mask in the alpha channel """ |
|
|
|
def __init__(self, landmarks, face, channels=4, idx = 0): |
|
|
|
|
|
self.landmarks = landmarks |
|
self.face = face |
|
self.channels = channels |
|
self.cols = 4 |
|
self.rows = 4 |
|
self.idx = idx |
|
|
|
mask = self.build_mask() |
|
self.mask = self.merge_mask(mask) |
|
|
|
|
|
def build_mask(self): |
|
""" Override to build the mask """ |
|
raise NotImplementedError |
|
|
|
def merge_mask(self, mask): |
|
""" Return the mask in requested shape """ |
|
|
|
assert self.channels in (1, 3, 4), "Channels should be 1, 3 or 4" |
|
assert mask.shape[2] == 1 and mask.ndim == 3, "Input mask be 3 dimensions with 1 channel" |
|
|
|
if self.channels == 3: |
|
retval = np.tile(mask, 3) |
|
elif self.channels == 4: |
|
retval = np.concatenate((self.face, mask), -1) |
|
else: |
|
retval = mask |
|
|
|
|
|
return retval |
|
|
|
|
|
class dfl_full(Mask): |
|
""" DFL facial mask """ |
|
def build_mask(self): |
|
mask = np.zeros(self.face.shape[0:2] + (1, ), dtype=np.float32) |
|
|
|
nose_ridge = (self.landmarks[27:31], self.landmarks[33:34]) |
|
jaw = (self.landmarks[0:17], |
|
self.landmarks[48:68], |
|
self.landmarks[0:1], |
|
self.landmarks[8:9], |
|
self.landmarks[16:17]) |
|
eyes = (self.landmarks[17:27], |
|
self.landmarks[0:1], |
|
self.landmarks[27:28], |
|
self.landmarks[16:17], |
|
self.landmarks[33:34]) |
|
parts = [jaw, nose_ridge, eyes] |
|
|
|
for item in parts: |
|
merged = np.concatenate(item) |
|
cv2.fillConvexPoly(mask, cv2.convexHull(merged), 255.) |
|
return mask |
|
|
|
|
|
class components(Mask): |
|
""" Component model mask """ |
|
def build_mask(self): |
|
mask = np.zeros(self.face.shape[0:2] + (1, ), dtype=np.float32) |
|
|
|
r_jaw = (self.landmarks[0:9], self.landmarks[17:18]) |
|
l_jaw = (self.landmarks[8:17], self.landmarks[26:27]) |
|
r_cheek = (self.landmarks[17:20], self.landmarks[8:9]) |
|
l_cheek = (self.landmarks[24:27], self.landmarks[8:9]) |
|
nose_ridge = (self.landmarks[19:25], self.landmarks[8:9],) |
|
r_eye = (self.landmarks[17:22], |
|
self.landmarks[27:28], |
|
self.landmarks[31:36], |
|
self.landmarks[8:9]) |
|
l_eye = (self.landmarks[22:27], |
|
self.landmarks[27:28], |
|
self.landmarks[31:36], |
|
self.landmarks[8:9]) |
|
nose = (self.landmarks[27:31], self.landmarks[31:36]) |
|
parts = [r_jaw, l_jaw, r_cheek, l_cheek, nose_ridge, r_eye, l_eye, nose] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for item in parts: |
|
merged = np.concatenate(item) |
|
cv2.fillConvexPoly(mask, cv2.convexHull(merged), 255.) |
|
return mask |
|
|
|
|
|
class extended(Mask): |
|
""" Extended mask |
|
Based on components mask. Attempts to extend the eyebrow points up the forehead |
|
""" |
|
def build_mask(self): |
|
mask = np.zeros(self.face.shape[0:2] + (1, ), dtype=np.float32) |
|
|
|
landmarks = self.landmarks.copy() |
|
|
|
ml_pnt = (landmarks[36] + landmarks[0]) // 2 |
|
mr_pnt = (landmarks[16] + landmarks[45]) // 2 |
|
|
|
|
|
ql_pnt = (landmarks[36] + ml_pnt) // 2 |
|
qr_pnt = (landmarks[45] + mr_pnt) // 2 |
|
|
|
|
|
bot_l = np.array((ql_pnt, landmarks[36], landmarks[37], landmarks[38], landmarks[39])) |
|
bot_r = np.array((landmarks[42], landmarks[43], landmarks[44], landmarks[45], qr_pnt)) |
|
|
|
|
|
top_l = landmarks[17:22] |
|
top_r = landmarks[22:27] |
|
|
|
|
|
landmarks[17:22] = top_l + ((top_l - bot_l) // 2) |
|
landmarks[22:27] = top_r + ((top_r - bot_r) // 2) |
|
|
|
r_jaw = (landmarks[0:9], landmarks[17:18]) |
|
l_jaw = (landmarks[8:17], landmarks[26:27]) |
|
r_cheek = (landmarks[17:20], landmarks[8:9]) |
|
l_cheek = (landmarks[24:27], landmarks[8:9]) |
|
nose_ridge = (landmarks[19:25], landmarks[8:9],) |
|
r_eye = (landmarks[17:22], landmarks[27:28], landmarks[31:36], landmarks[8:9]) |
|
l_eye = (landmarks[22:27], landmarks[27:28], landmarks[31:36], landmarks[8:9]) |
|
nose = (landmarks[27:31], landmarks[31:36]) |
|
parts = [r_jaw, l_jaw, r_cheek, l_cheek, nose_ridge, r_eye, l_eye, nose] |
|
|
|
for item in parts: |
|
merged = np.concatenate(item) |
|
cv2.fillConvexPoly(mask, cv2.convexHull(merged), 255.) |
|
return mask |
|
|
|
|
|
class facehull(Mask): |
|
""" Basic face hull mask """ |
|
def build_mask(self): |
|
mask = np.zeros(self.face.shape[0:2] + (1, ), dtype=np.float32) |
|
hull = cv2.convexHull( |
|
np.array(self.landmarks).reshape((-1, 2))) |
|
cv2.fillConvexPoly(mask, hull, 255.0, lineType=cv2.LINE_AA) |
|
return mask |
|
|
|
|
|
|
|
class facehull2(Mask): |
|
""" Basic face hull mask """ |
|
def build_mask(self): |
|
mask = np.zeros(self.face.shape[0:2] + (1, ), dtype=np.uint8) |
|
hull = cv2.convexHull( |
|
np.array(self.landmarks).reshape((-1, 2))) |
|
cv2.fillConvexPoly(mask, hull, 1.0, lineType=cv2.LINE_AA) |
|
return mask |
|
|
|
|
|
|
|
class gridMasking(Mask): |
|
|
|
def build_mask(self): |
|
h, w = self.face.shape[:2] |
|
landmarks = self.landmarks[:68] |
|
|
|
|
|
r, c = divmod(self.idx, self.cols) |
|
|
|
|
|
xmin, ymin, xmax, ymax = landmarks_to_bbox(landmarks) |
|
dx = ceil((xmax - xmin) / self.cols) |
|
dy = ceil((ymax - ymin) / self.rows) |
|
|
|
mask = np.zeros((h, w), dtype=np.uint8) |
|
|
|
|
|
x0, y0 = floor(xmin + dx * c), floor(ymin + dy * r) |
|
x1, y1 = floor(x0 + dx), floor(y0 + dy) |
|
cv2.rectangle(mask, (x0, y0), (x1, y1), 255, -1) |
|
|
|
|
|
ch = mask_from_points(self.face, landmarks) |
|
|
|
|
|
mask = cv2.bitwise_and(mask, mask, mask=ch) |
|
mask = mask.reshape([mask.shape[0],mask.shape[1], 1]) |
|
|
|
|
|
return mask |
|
|
|
class MeshgridMasking(Mask): |
|
areas = [ |
|
[1, 2, 3, 4, 5, 6, 7, 49, 32, 40, 41, 42, 37, 18], |
|
[37, 38, 39, 40, 41, 42], |
|
[18, 19, 20, 21, 22, 28, 40, 39, 38, 37], |
|
[28, 29, 30, 31, 32, 40], |
|
] |
|
areas_asym = [ |
|
[20, 21, 22, 28, 23, 24, 25], |
|
[31, 32, 33, 34, 35, 36], |
|
[32, 33, 34, 35, 36, 55, 54, 53, 52, 51, 50, 49], |
|
[49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60], |
|
[7, 8, 9, 10, 11, 55, 56, 57, 58, 59, 60, 49], |
|
] |
|
|
|
def init(self, **kwargs): |
|
|
|
|
|
sym, _ = landmarks_68_symmetries() |
|
|
|
paths = [] |
|
paths += self.areas_asym |
|
paths += self.areas |
|
paths += [[sym[ld68_id] for ld68_id in area] for area in self.areas] |
|
assert len(paths) == self.total |
|
self.paths = paths |
|
|
|
@property |
|
def total(self) -> int: |
|
total = len(self.areas_asym) + len(self.areas) * 2 |
|
return total |
|
|
|
def transform_landmarks(self, landmarks): |
|
"""Transform landmarks to extend the eyebrow points up the forehead""" |
|
new_landmarks = landmarks.copy() |
|
|
|
ml_pnt = (new_landmarks[36] + new_landmarks[0]) // 2 |
|
mr_pnt = (new_landmarks[16] + new_landmarks[45]) // 2 |
|
|
|
|
|
ql_pnt = (new_landmarks[36] + ml_pnt) // 2 |
|
qr_pnt = (new_landmarks[45] + mr_pnt) // 2 |
|
|
|
|
|
bot_l = np.array( |
|
( |
|
ql_pnt, |
|
new_landmarks[36], |
|
new_landmarks[37], |
|
new_landmarks[38], |
|
new_landmarks[39], |
|
) |
|
) |
|
bot_r = np.array( |
|
( |
|
new_landmarks[42], |
|
new_landmarks[43], |
|
new_landmarks[44], |
|
new_landmarks[45], |
|
qr_pnt, |
|
) |
|
) |
|
|
|
|
|
top_l = new_landmarks[17:22] |
|
top_r = new_landmarks[22:27] |
|
|
|
|
|
new_landmarks[17:22] = top_l + ((top_l - bot_l) // 2) |
|
new_landmarks[22:27] = top_r + ((top_r - bot_r) // 2) |
|
|
|
return new_landmarks |
|
|
|
def build_mask(self) -> np.ndarray: |
|
self.init() |
|
h, w = self.face.shape[:2] |
|
|
|
path = self.paths[self.idx] |
|
new_landmarks = self.transform_landmarks(self.landmarks) |
|
points = [new_landmarks[ld68_id - 1] for ld68_id in path] |
|
points = np.array(points, dtype=np.int32) |
|
|
|
|
|
mask = np.zeros((h, w), dtype=np.uint8) |
|
cv2.fillPoly(mask, [points], 255) |
|
mask = mask.reshape([mask.shape[0],mask.shape[1], 1]) |
|
return mask |