| '''
|
| Create face mask and face boundary mask according to face landmarks,
|
| so as to supervize the activation of Conv layer.
|
| '''
|
|
|
| import os
|
| import numpy as np
|
| import cv2
|
| import dlib
|
| import random
|
| import argparse
|
| from tqdm import tqdm
|
| import time
|
| from skimage import transform as trans
|
|
|
| from .warp import gen_warp_params, warp_by_params, warp_mask
|
|
|
|
|
| def crop_img_bbox(img, bbox, res, scale=1.3):
|
| x, y, w, h = bbox
|
| left, right = x, x+w
|
| top, bottom = y, y+h
|
|
|
| H, W, C = img.shape
|
| cx, cy = (left+right)//2, (top+bottom)//2
|
| w, h = (right-left)//2, (bottom-top)//2
|
|
|
| x1 = max(0, int(cx-w*scale))
|
| x2 = min(W, int(cx+w*scale))
|
| y1 = max(0, int(cy-h*scale))
|
| y2 = min(H, int(cy+h*scale))
|
|
|
| roi = img[y1:y2, x1:x2]
|
| roi = cv2.resize(roi, (res, res))
|
|
|
| return roi
|
|
|
|
|
| def get_mask_center(mask):
|
| l, t, w, h = cv2.boundingRect(mask[:, :, 0:1].astype(np.uint8))
|
| center = int(l+w/2), int(t+h/2)
|
| return center
|
|
|
|
|
| def get_5_keypoint(shape):
|
| def get_point(idx):
|
|
|
| return shape[idx]
|
|
|
| def center(pt1, pt2):
|
| return [(pt1[0]+pt2[0])//2, (pt1[1]+pt2[1])//2]
|
|
|
| leye = np.array(center(get_point(36), get_point(39)),
|
| dtype=int).reshape(-1, 2)
|
| reye = np.array(center(get_point(45), get_point(42)),
|
| dtype=int).reshape(-1, 2)
|
| nose = np.array(get_point(30), dtype=int).reshape(-1, 2)
|
| lmouth = np.array(get_point(48),
|
| dtype=int).reshape(-1, 2)
|
| rmouth = np.array(get_point(54),
|
| dtype=int).reshape(-1, 2)
|
|
|
| pts = np.concatenate([leye, reye, nose, lmouth, rmouth], axis=0)
|
|
|
| return pts
|
|
|
|
|
| def get_boundary(mask):
|
| if len(mask.shape) == 3:
|
| mask = mask[:, :, 0]
|
| mask = cv2.GaussianBlur(mask, (3, 3), 0)
|
| boundary = mask / 255.
|
| boundary = 4*boundary*(1.-boundary)
|
| return boundary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def blur_mask(mask):
|
| blur_k = 2*np.random.randint(1, 10)-1
|
|
|
|
|
|
|
|
|
| mask = cv2.GaussianBlur(mask, (blur_k, blur_k), 0)
|
|
|
|
|
| return mask
|
|
|
|
|
| def random_deform(pt, tgt, scale=0.3):
|
| x1, y1 = pt
|
| x2, y2 = tgt
|
|
|
| x = x1+(x2-x1)*np.random.rand()*scale
|
| y = y1+(y2-y1)*np.random.rand()*scale
|
|
|
| return [int(x), int(y)]
|
|
|
|
|
| def get_specific_mask(img, shape, mtype='mouth', random_side=False):
|
| if mtype == 'eyes':
|
| landmarks = shape[42:45] if random.choice([True, False]) else shape[36:39]
|
|
|
| elif mtype == 'nose':
|
| landmarks = shape[27:35]
|
|
|
| elif mtype == 'mouth':
|
| landmarks = shape[48:60]
|
|
|
| elif mtype == 'eyebrows':
|
| landmarks = shape[22:26] if random.choice([True, False]) else shape[17:21]
|
|
|
| else:
|
| raise ValueError(f"Invalid mtype. Choose from 'eyes', 'nose', 'mouth', or 'eyebrows', but got {mtype}")
|
|
|
|
|
| hull = cv2.convexHull(landmarks)
|
| hull = hull.astype(int)
|
|
|
|
|
| hull_mask = np.zeros_like(img)
|
| cv2.fillPoly(hull_mask, [hull], (255, 255, 255))
|
| mask = hull_mask
|
| return mask
|
|
|
|
|
| def get_hull_mask(img, shape, mtype='hull'):
|
| if mtype == 'normal-hull':
|
| landmarks = np.array(shape)
|
|
|
|
|
| hull = cv2.convexHull(landmarks)
|
| hull = hull.astype(int)
|
|
|
|
|
| hull_mask = np.zeros_like(img)
|
| cv2.fillPoly(hull_mask, [hull], (255, 255, 255))
|
| mask = hull_mask
|
|
|
| elif mtype == 'inner-hull':
|
| landmarks = shape[17:]
|
| landmarks = np.array(landmarks)
|
|
|
|
|
| hull = cv2.convexHull(landmarks)
|
| hull = hull.astype(int)
|
|
|
|
|
| hull_mask = np.zeros_like(img)
|
| cv2.fillPoly(hull_mask, [hull], (255, 255, 255))
|
|
|
| mask = hull_mask
|
|
|
| elif mtype == 'inner-hull-no-eyebrow':
|
| landmarks = shape[27:]
|
| landmarks = np.array(landmarks)
|
|
|
| hull = cv2.convexHull(landmarks)
|
| hull = hull.astype(int)
|
|
|
|
|
| hull_mask = np.zeros_like(img)
|
| cv2.fillPoly(hull_mask, [hull], (255, 255, 255))
|
|
|
| mask = hull_mask
|
|
|
| elif mtype == 'mouth-hull':
|
| landmarks = shape[2:15]
|
|
|
| landmarks = np.concatenate([landmarks, shape[29].reshape(1, -1)], axis=0)
|
|
|
|
|
| hull = cv2.convexHull(landmarks)
|
| hull = hull.astype(int)
|
|
|
|
|
| hull_mask = np.zeros_like(img)
|
| cv2.fillPoly(hull_mask, [hull], (255, 255, 255))
|
|
|
|
|
|
|
| mask = hull_mask
|
|
|
| elif mtype == 'whole-hull':
|
| face_height = shape[9][1] - shape[22][1]
|
| landmarks = []
|
| for i in range(27):
|
| lmk = shape[i]
|
| if i >= 5 and i <= 11:
|
| x, y = lmk[0], lmk[1]
|
| lmk = [x, max(0, y+15)]
|
|
|
| if i >= 18 and i <= 27:
|
| x, y = lmk[0], lmk[1]
|
| lmk = [x, max(0, y-face_height//4)]
|
|
|
| landmarks.append(lmk)
|
|
|
|
|
| landmarks = np.array(landmarks, dtype=np.int32)
|
| hull = cv2.convexHull(landmarks)
|
| hull = np.reshape(hull, (1, -1, 2))
|
|
|
|
|
| hull_mask = np.zeros_like(img)
|
| cv2.fillPoly(hull_mask, [hull], (255, 255, 255))
|
|
|
|
|
|
|
| mask = hull_mask
|
| '''
|
| elif mtype == 'rect':
|
| cnt = []
|
| for idx in [5, 11, 17, 26]:
|
| cnt.append(shape[idx])
|
| x, y, w, h = cv2.boundingRect(np.array(cnt))
|
| rect_mask = np.zeros_like(img)
|
| cv2.rectangle(rect_mask, (x, y), (x+w, y+h),
|
| (255, 255, 255), cv2.FILLED)
|
| mask = rect_mask
|
| '''
|
| return mask
|
|
|
|
|
| def get_mask(shape, img, std=20, deform=True, restrict_mask=None):
|
| mask_type = [
|
| 'normal-hull',
|
| 'inner-hull',
|
| 'inner-hull-no-eyebrow',
|
| 'mouth-hull',
|
| 'whole-hull'
|
| ]
|
| max_mask = get_hull_mask(img, shape, 'whole-hull')
|
| if deform:
|
| mtype = mask_type[np.random.randint(len(mask_type))]
|
| if mtype == 'rect':
|
| mask = get_hull_mask(img, shape, 'inner-hull-no-eyebrow')
|
| x, y, w, h = cv2.boundingRect(mask[:,:,0])
|
| for i in range(y, y+h):
|
| for j in range(x, x+w):
|
| for k in range(mask.shape[2]):
|
| mask[i, j, k] = 255
|
| else:
|
| mask = get_hull_mask(img, shape, mtype)
|
|
|
|
|
| if np.random.rand() < 0.9:
|
| mask = warp_mask(mask, std=std)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| else:
|
| mask = max_mask.copy()
|
|
|
| if restrict_mask is not None:
|
| mask = mask*(restrict_mask//255)
|
|
|
|
|
| mask = mask *(max_mask//255)
|
|
|
|
|
| if deform and np.random.rand() < 0.9:
|
| mask = blur_mask(mask)
|
|
|
| return mask[:,:,0]
|
|
|
| def mask_postprocess(mask):
|
|
|
| prob = np.random.rand()
|
| if prob < 0.3:
|
| erode_k = 2*np.random.randint(1, 10)+1
|
| kernel = np.ones((erode_k, erode_k), np.uint8)
|
| mask = cv2.erode(mask, kernel)
|
| elif prob < 0.6:
|
| erode_k = 2*np.random.randint(1, 10)+1
|
| kernel = np.ones((erode_k, erode_k), np.uint8)
|
| mask = cv2.dilate(mask, kernel)
|
|
|
|
|
| if np.random.rand() < 0.9:
|
| mask = blur_mask(mask)
|
|
|
| return mask
|
|
|
|
|
| def get_affine_param(from_, to_):
|
|
|
| tform = trans.SimilarityTransform()
|
| tform.estimate(from_.astype(np.float32), to_.astype(
|
| np.float32))
|
| M = tform.params[0:2, :]
|
|
|
| return M
|
|
|
|
|
| def random_sharpen_img(img):
|
| cand = ['bsharpen', 'gsharpen']
|
| mode = cand[np.random.randint(len(cand))]
|
|
|
| if mode == "bsharpen":
|
|
|
| kernel = np.ones((3, 3)) * (-1)
|
| kernel[1, 1] = 9
|
|
|
| out = cv2.filter2D(img, -1, kernel)
|
| elif mode == "gsharpen":
|
|
|
| gaussain_blur = cv2.GaussianBlur(img, (0, 0), 3.0)
|
| out = cv2.addWeighted(
|
| img, 1.5, gaussain_blur, -0.5, 0, img)
|
| else:
|
| out = img
|
|
|
| return out
|
|
|
|
|
| def random_blur_img(img):
|
| cand = ['avg', 'gaussion', 'med']
|
| mode = cand[np.random.randint(len(cand))]
|
|
|
| ksize = 2*np.random.randint(1, 5)+1
|
|
|
| if mode == 'avg':
|
|
|
| out = cv2.blur(img, (ksize, ksize))
|
| elif mode == 'gaussion':
|
|
|
| out = cv2.GaussianBlur(img, (ksize, ksize), 0)
|
| elif mode == 'med':
|
|
|
| out = cv2.medianBlur(img, ksize)
|
| else:
|
| out = img
|
|
|
|
|
|
|
|
|
| return out
|
|
|
|
|
| def random_warp_img(img, prob=0.5):
|
| H, W, C = img.shape
|
| param = gen_warp_params(W, flip=False)
|
| choice = [True, False]
|
|
|
| out = warp_by_params(param, img,
|
| can_flip=False,
|
| can_transform=False,
|
| can_warp=(np.random.randint(10) < int(prob*10)),
|
| border_replicate=choice[np.random.randint(2)])
|
| return out
|
|
|
|
|
| def main(args):
|
| np.random.seed(int(time.time()))
|
| detector = dlib.get_frontal_face_detector()
|
| landmark_predictor = dlib.shape_predictor(args.model)
|
|
|
| src_im = cv2.imread(args.src)
|
| tgt_im = cv2.imread(args.tgt)
|
|
|
| H, W, C = tgt_im.shape
|
| src_im = cv2.resize(src_im, (W, H))
|
|
|
| def get_shape(img):
|
| img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| dets = detector(img, 1)
|
| det = dets[0]
|
| shape = landmark_predictor(img, det)
|
|
|
| return shape, det
|
|
|
| src_shape, src_det = get_shape(src_im)
|
| src_5_pts = get_5_keypoint(src_shape)
|
| src_mask = get_mask(src_shape, src_im, whole=True, deform=False)
|
|
|
| tgt_shape, tgt_det = get_shape(tgt_im)
|
| tgt_5_pts = get_5_keypoint(tgt_shape)
|
| tgt_mask = get_mask(tgt_shape, tgt_im, whole=False, deform=True)
|
|
|
|
|
|
|
|
|
| mask = src_mask[:, :, 0:1]/255.
|
| ct_modes = ['lct', 'rct', 'idt', 'idt-m', 'mkl', 'mkl-m',
|
| 'sot', 'sot-m', 'mix-m']
|
| for mode in ct_modes:
|
| colored_src = color_transfer(mode, src_im, tgt_im, mask)
|
| cv2.imwrite('{}_colored.png'.format(mode), colored_src)
|
| src_im = colored_src
|
|
|
| w1, h1 = src_det.right()-src_det.left(), src_det.bottom()-src_det.top()
|
| w2, h2 = tgt_det.right()-tgt_det.left(), tgt_det.bottom()-tgt_det.top()
|
| w_scale, h_scale = w2/w1, h2/h1
|
|
|
| scaled_src = cv2.resize(src_im, (int(W*w_scale), int(H*h_scale)))
|
| scaled_mask = cv2.resize(src_mask, (int(W*w_scale), int(H*h_scale)))
|
|
|
| src_5_pts[:, 0] = src_5_pts[:, 0]*w_scale
|
| src_5_pts[:, 1] = src_5_pts[:, 1]*h_scale
|
| aff_param = get_affine_param(src_5_pts, tgt_5_pts)
|
|
|
| aligned_src = cv2.warpAffine(
|
| scaled_src, aff_param, (W, H), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REFLECT)
|
| aligned_mask = cv2.warpAffine(
|
| scaled_mask, aff_param, (W, H), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REFLECT)
|
|
|
| center = get_mask_center(aligned_mask)
|
| print('mask center:', center)
|
|
|
|
|
| init_blend = cv2.seamlessClone(
|
| aligned_src, tgt_im, aligned_mask, center, cv2.NORMAL_CLONE)
|
| cv2.imwrite('init_blended.png', init_blend)
|
|
|
|
|
|
|
| b_mask = tgt_mask[:, :, 0:1]/255.
|
| out_blend = init_blend*b_mask + tgt_im*(1. - b_mask)
|
| cv2.imwrite('out_blended.png', out_blend)
|
|
|
| res = 256
|
| blend_crop = crop_img_bbox(out_blend, tgt_det, res, scale=1.5)
|
| mask_crop = crop_img_bbox(tgt_mask, tgt_det, res, scale=1.5)
|
| boundary = get_boundary(mask_crop)
|
|
|
| cv2.imwrite('crop_blend.png', blend_crop)
|
| cv2.imwrite('crop_mask.png', mask_crop)
|
| cv2.imwrite('crop_bound.png', boundary*255)
|
|
|
|
|
| if __name__ == "__main__":
|
| p = argparse.ArgumentParser(
|
| formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
| )
|
| p.add_argument('-s', '--src', type=str,
|
| help='src image')
|
| p.add_argument('-t', '--tgt', type=str,
|
| help='tgt image')
|
| p.add_argument('--model', type=str, default='/data1/yuchen/download/face_landmark/shape_predictor_68_face_landmarks.dat',
|
| help="path to downloaded detector")
|
| args = p.parse_args()
|
| print(args)
|
|
|
| main(args)
|
|
|