commitb608558b9eMerge:ad29b02dd205abAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Wed Jul 24 17:56:47 2024 +0300 Merge branch 'develop' of https://github.com/kijai/ComfyUI-LivePortraitKJ into develop commitad29b02bc1Author: kijai <40791699+kijai@users.noreply.github.com> Date: Wed Jul 24 17:56:46 2024 +0300 update workflows commitdd205ab4a4Author: Jukka Seppänen <40791699+kijai@users.noreply.github.com> Date: Wed Jul 24 17:54:47 2024 +0300 Update readme.md commitba0886a905Author: kijai <40791699+kijai@users.noreply.github.com> Date: Wed Jul 24 16:09:26 2024 +0300 fix running without insightface installed commit068ab2c280Author: kijai <40791699+kijai@users.noreply.github.com> Date: Wed Jul 24 03:57:59 2024 +0300 Add MediaPipe as alternative face detector commit6261f4e474Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 23 22:58:33 2024 +0300 cleanup, memory fixes commit46675b2016Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 23 21:13:15 2024 +0300 update workflows, cleanup commit806263dd25Author: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 22 20:39:43 2024 +0300 cleanup, fixes commitac89dc1e2fAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 22 19:19:46 2024 +0300 fix no face frame skip commit27d745b53eAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 22 17:52:34 2024 +0300 add other examples commit052762578cAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 22 17:45:49 2024 +0300 Update readme.md commite825c51c87Author: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 22 16:21:35 2024 +0300 separate composition to it's own node commit177b324fcdAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 22 01:02:56 2024 +0300 Update live_portrait_pipeline.py commit5c03bd8439Author: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 22 00:57:44 2024 +0300 MPS fallbacks commitef5ff7075fAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Sun Jul 21 20:35:33 2024 +0300 Update requirements.txt commit92fad03ee5Author: kijai <40791699+kijai@users.noreply.github.com> Date: Sun Jul 21 20:20:46 2024 +0300 restructure a bit for more caching commit4cefac79b8Author: kijai <40791699+kijai@users.noreply.github.com> Date: Sun Jul 21 19:42:22 2024 +0300 Add single_frame mode for webcam commit5e3c92d55cAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Sun Jul 21 19:20:52 2024 +0300 restructuring, video smoothing commitcc0501a2dbAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Sun Jul 21 13:21:26 2024 +0300 flag_relative_rotation_only commit3dc822fd2fAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Sat Jul 20 20:24:15 2024 +0300 to use GPU for pasteback commit697b9a78e6Author: kijai <40791699+kijai@users.noreply.github.com> Date: Sat Jul 20 17:39:44 2024 +0300 Restructure nodes, skip frames with no face detect commit2a7bd6116fAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Wed Jul 10 17:14:43 2024 +0300 Update nodes.py commita7d09f5d49Author: kijai <40791699+kijai@users.noreply.github.com> Date: Wed Jul 10 16:33:31 2024 +0300 example workflow commit8e85d5b96dAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Wed Jul 10 16:06:01 2024 +0300 some optimizations commit30989a9d37Author: kijai <40791699+kijai@users.noreply.github.com> Date: Wed Jul 10 01:17:10 2024 +0300 Update nodes.py commiteecf645603Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 23:02:47 2024 +0300 rotate option for cropper commit1b080706dfAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 22:46:51 2024 +0300 Update nodes.py commit336f3f7c23Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 22:19:56 2024 +0300 add cut method commitf27e1cca13Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 22:11:19 2024 +0300 remove nearest option commit86e91a6e9dAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 22:06:56 2024 +0300 better error for retargeting commit92529f7ca8Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 21:59:32 2024 +0300 cleanup commitc0959056aeAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 21:49:54 2024 +0300 eye/lip retargeting fixes commit2e40fe3820Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 21:23:10 2024 +0300 Update live_portrait_pipeline.py commit0a5e187637Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 21:15:17 2024 +0300 keep Cropper in memory commit4e19dbd6d1Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 20:19:53 2024 +0300 Do video cropping on the cropped node too commit9c190804a7Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 19:02:27 2024 +0300 big cleanup commitd9ca40e1d6Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 15:08:17 2024 +0300 logging commitb68cf8788cAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 14:35:51 2024 +0300 fix warning commite702b26895Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 14:31:22 2024 +0300 Update cropper.py commitc21705edb5Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 14:25:39 2024 +0300 Don't draw keypoints for every frame by default commita284bb52b2Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 14:04:56 2024 +0300 Bring back mismatch_method selection commit9884aac18aAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 14:00:43 2024 +0300 Fix eye/lip retargeting commitf8aada81dbAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 13:00:03 2024 +0300 face_index selection commit6735771664Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 11:51:38 2024 +0300 tqdm progress bars commit857ddbc6d7Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 02:54:27 2024 +0300 skip autocast if not needed for mps commita6edcda97dAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 01:37:05 2024 +0300 output masks commitca01d706d0Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 00:57:13 2024 +0300 custom mask support commit0dc9a8a695Merge:ee7d5b4ba6b3f5Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 00:13:07 2024 +0300 Merge branch 'add_video_source' into develop commitba6b3f5f68Author: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 23:09:56 2024 +0200 bring KJ edits commitee7d5b4241Author: kijai <40791699+kijai@users.noreply.github.com> Date: Tue Jul 9 00:08:10 2024 +0300 revert this for compatibility commit03df9f35cdMerge:ec6b5c88509d9aAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 8 23:55:22 2024 +0300 Merge branch 'add_video_source' into develop commitec6b5c8c85Merge:6f9dba7e724da1Author: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 8 23:52:58 2024 +0300 calc_combined_eye_ratio commit8509d9a551Author: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 22:52:48 2024 +0200 remove unused imports commite724da1161Author: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 21:38:27 2024 +0200 fix relative mode use R_d_0 instead of source commit68d0ddf72aAuthor: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 21:22:50 2024 +0200 remove reference frame attempt also use batches for driving when either retargetting is enabled commit6f9dba7777Author: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 8 20:50:44 2024 +0300 fixes commit811ca557fbAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 8 20:31:00 2024 +0300 more commit6d790bdcc3Merge:ef8b426eb5fddfAuthor: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 8 20:30:35 2024 +0300 Merge branch 'add_video_source' into develop commitef8b4263b4Author: kijai <40791699+kijai@users.noreply.github.com> Date: Mon Jul 8 20:21:45 2024 +0300 separating functions to nodes commiteb5fddf4deAuthor: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 19:10:53 2024 +0200 fix issues from merge commit9c7db3c59aMerge:bf3410c1f28e12Author: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 19:09:02 2024 +0200 Merge branch 'main' into add_video_source commitbf3410cd0dAuthor: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 19:04:42 2024 +0200 trying reference frame commit24c65627dbAuthor: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 19:03:28 2024 +0200 local updates before merging main commit72bb6910e9Author: Mel Massadian <mel@melmassadian.com> Date: Mon Jul 8 16:48:15 2024 +0200 initial too much diff due to formatting
481 lines
17 KiB
Python
481 lines
17 KiB
Python
# coding: utf-8
|
|
|
|
"""
|
|
cropping function and the related preprocess functions for cropping
|
|
"""
|
|
|
|
import cv2#; cv2.setNumThreads(0); cv2.ocl.setUseOpenCL(False) # NOTE: enforce single thread
|
|
import numpy as np
|
|
from math import sin, cos, acos, degrees
|
|
DTYPE = np.float32
|
|
CV2_INTERP = cv2.INTER_LINEAR
|
|
import comfy.model_management as mm
|
|
|
|
def _transform_img(img, M, dsize, flags=CV2_INTERP, borderMode=None):
|
|
""" conduct similarity or affine transformation to the image, do not do border operation!
|
|
img:
|
|
M: 2x3 matrix or 3x3 matrix
|
|
dsize: target shape (width, height)
|
|
"""
|
|
if isinstance(dsize, tuple) or isinstance(dsize, list):
|
|
_dsize = tuple(dsize)
|
|
else:
|
|
_dsize = (dsize, dsize)
|
|
|
|
if borderMode is not None:
|
|
return cv2.warpAffine(img, M[:2, :], dsize=_dsize, flags=flags, borderMode=borderMode, borderValue=(0, 0, 0))
|
|
else:
|
|
return cv2.warpAffine(img, M[:2, :], dsize=_dsize, flags=flags)
|
|
|
|
import torch
|
|
import kornia.geometry.transform as KGT
|
|
|
|
def _transform_img_kornia(img, M, dsize, device, flags='bilinear', borderMode='zeros'):
|
|
"""Conduct similarity or affine transformation to the image using Kornia.
|
|
|
|
img: Input image as a PyTorch tensor of shape (C, H, W).
|
|
M: 2x3 transformation matrix as a PyTorch tensor.
|
|
dsize: Target shape (width, height).
|
|
"""
|
|
|
|
# Convert dsize to tensor shape (H, W)
|
|
_dsize = torch.tensor([dsize[1], dsize[0]]) # Kornia expects (H, W)
|
|
|
|
# Convert M from numpy.ndarray to PyTorch tensor
|
|
M = torch.from_numpy(M).float().to(device)
|
|
if M.shape == (3, 3):
|
|
M = M[:2, :].unsqueeze(0) # Adjust M to the expected shape Bx2x3
|
|
elif M.shape == (2, 3):
|
|
M = M.unsqueeze(0) # Add batch dimension if not present
|
|
|
|
# Reshape M for Kornia (1, 2, 3) and upscale to 3D affine matrix if not already
|
|
if M.shape == (2, 3):
|
|
M = M.unsqueeze(0) # Add batch dimension
|
|
|
|
# Convert image to floating point tensor if not already
|
|
if img.dtype != torch.float32:
|
|
img = img.float()
|
|
img = img.to(device)
|
|
|
|
# Reshape img for Kornia (B, C, H, W)
|
|
img = img.permute(0, 3, 1, 2)
|
|
|
|
# Apply the affine transformation
|
|
img_warped = KGT.warp_affine(img, M, _dsize, mode=flags, padding_mode=borderMode)
|
|
|
|
return img_warped
|
|
|
|
def _transform_pts(pts, M):
|
|
""" conduct similarity or affine transformation to the pts
|
|
pts: Nx2 ndarray
|
|
M: 2x3 matrix or 3x3 matrix
|
|
return: Nx2
|
|
"""
|
|
return pts @ M[:2, :2].T + M[:2, 2]
|
|
|
|
|
|
def parse_pt2_from_pt478(pt478, use_lip=True):
|
|
"""
|
|
parsing the 2 points according to the 101 points, which cancels the roll
|
|
"""
|
|
# the former version use the eye center, but it is not robust, now use interpolation
|
|
pt_left_eye = pt478[468] # left eye center
|
|
pt_right_eye = pt478[473] # right eye center
|
|
|
|
if use_lip:
|
|
# use lip
|
|
pt_center_eye = (pt_left_eye + pt_right_eye) / 2
|
|
pt_center_lip = pt478[14]
|
|
pt2 = np.stack([pt_center_eye, pt_center_lip], axis=0)
|
|
else:
|
|
pt2 = np.stack([pt_left_eye, pt_right_eye], axis=0)
|
|
return pt2
|
|
|
|
def parse_pt2_from_pt101(pt101, use_lip=True):
|
|
"""
|
|
parsing the 2 points according to the 101 points, which cancels the roll
|
|
"""
|
|
# the former version use the eye center, but it is not robust, now use interpolation
|
|
pt_left_eye = np.mean(pt101[[39, 42, 45, 48]], axis=0) # left eye center
|
|
pt_right_eye = np.mean(pt101[[51, 54, 57, 60]], axis=0) # right eye center
|
|
|
|
if use_lip:
|
|
# use lip
|
|
pt_center_eye = (pt_left_eye + pt_right_eye) / 2
|
|
pt_center_lip = (pt101[75] + pt101[81]) / 2
|
|
pt2 = np.stack([pt_center_eye, pt_center_lip], axis=0)
|
|
else:
|
|
pt2 = np.stack([pt_left_eye, pt_right_eye], axis=0)
|
|
return pt2
|
|
|
|
|
|
def parse_pt2_from_pt106(pt106, use_lip=True):
|
|
"""
|
|
parsing the 2 points according to the 106 points, which cancels the roll
|
|
"""
|
|
pt_left_eye = np.mean(pt106[[33, 35, 40, 39]], axis=0) # left eye center
|
|
pt_right_eye = np.mean(pt106[[87, 89, 94, 93]], axis=0) # right eye center
|
|
|
|
if use_lip:
|
|
# use lip
|
|
pt_center_eye = (pt_left_eye + pt_right_eye) / 2
|
|
pt_center_lip = (pt106[52] + pt106[61]) / 2
|
|
pt2 = np.stack([pt_center_eye, pt_center_lip], axis=0)
|
|
else:
|
|
pt2 = np.stack([pt_left_eye, pt_right_eye], axis=0)
|
|
return pt2
|
|
|
|
|
|
def parse_pt2_from_pt203(pt203, use_lip=True):
|
|
"""
|
|
parsing the 2 points according to the 203 points, which cancels the roll
|
|
"""
|
|
pt_left_eye = np.mean(pt203[[0, 6, 12, 18]], axis=0) # left eye center
|
|
pt_right_eye = np.mean(pt203[[24, 30, 36, 42]], axis=0) # right eye center
|
|
if use_lip:
|
|
# use lip
|
|
pt_center_eye = (pt_left_eye + pt_right_eye) / 2
|
|
pt_center_lip = (pt203[48] + pt203[66]) / 2
|
|
pt2 = np.stack([pt_center_eye, pt_center_lip], axis=0)
|
|
else:
|
|
pt2 = np.stack([pt_left_eye, pt_right_eye], axis=0)
|
|
return pt2
|
|
|
|
def parse_pt2_from_pt9(pt9, use_lip=True):
|
|
'''
|
|
animal_face = {"keypoints": ['right eye right', 'right eye left', 'left eye right', 'left eye left', 'nose tip', 'lip right', 'lip left', 'upper lip', 'lower lip'], "skeleton": []}
|
|
|
|
|
|
'''
|
|
if use_lip:
|
|
pt9 = np.stack([
|
|
(pt9[2]+pt9[3])/2, # left eye
|
|
(pt9[0]+pt9[1])/2, # right eye
|
|
pt9[4],
|
|
# (pt9[5]+pt9[6]+pt9[7]+pt9[8])/4 # lip
|
|
(pt9[5] + pt9[6] ) / 2 # lip
|
|
], axis=0)
|
|
pt2 = np.stack([
|
|
(pt9[0] + pt9[1]) / 2, # eye
|
|
pt9[3] # lip
|
|
], axis=0)
|
|
else:
|
|
pt2 = np.stack([
|
|
(pt9[2] + pt9[3]) / 2,
|
|
(pt9[0] + pt9[1]) / 2,
|
|
], axis=0)
|
|
|
|
return pt2
|
|
|
|
def parse_pt2_from_pt68(pt68, use_lip=True):
|
|
'''
|
|
face = {"keypoints": ['right cheekbone 1', 'right cheekbone 2', 'right cheek 1', 'right cheek 2', 'right cheek 3', 'right cheek 4', 'right cheek 5', 'right chin', 'chin center',
|
|
'left chin', 'left cheek 5', 'left cheek 4', 'left cheek 3', 'left cheek 2', 'left cheek 1', 'left cheekbone 2', 'left cheekbone 1', 'right eyebrow 1', 'right eyebrow 2', 'right eyebrow 3',
|
|
'right eyebrow 4', 'right eyebrow 5', 'left eyebrow 1', 'left eyebrow 2', 'left eyebrow 3', 'left eyebrow 4', 'left eyebrow 5', 'nasal bridge 1', 'nasal bridge 2', 'nasal bridge 3', 'nasal bridge 4',
|
|
'right nasal wing 1', 'right nasal wing 2', 'nasal wing center', 'left nasal wing 1', 'left nasal wing 2', 'right eye eye corner 1', 'right eye upper eyelid 1', 'right eye upper eyelid 2',
|
|
'right eye eye corner 2', 'right eye lower eyelid 2', 'right eye lower eyelid 1', 'left eye eye corner 1', 'left eye upper eyelid 1', 'left eye upper eyelid 2', 'left eye eye corner 2', 'left eye lower eyelid 2',
|
|
'left eye lower eyelid 1', 'right mouth corner', 'upper lip outer edge 1', 'upper lip outer edge 2', 'upper lip outer edge 3', 'upper lip outer edge 4', 'upper lip outer edge 5', 'left mouth corner',
|
|
'lower lip outer edge 5', 'lower lip outer edge 4', 'lower lip outer edge 3', 'lower lip outer edge 2', 'lower lip outer edge 1', 'upper lip inter edge 1', 'upper lip inter edge 2', 'upper lip inter edge 3',
|
|
'upper lip inter edge 4', 'upper lip inter edge 5', 'lower lip inter edge 3', 'lower lip inter edge 2', 'lower lip inter edge 1'], "skeleton": []}
|
|
|
|
|
|
'''
|
|
if use_lip:
|
|
pt68 = np.stack([
|
|
(pt68[42] + pt68[43] + pt68[44] + pt68[45] + pt68[46]+ pt68[47])/6, # left eye
|
|
(pt68[36] + pt68[37] + pt68[38] + pt68[39] + pt68[40] + pt68[41]) / 6, # right eye
|
|
(pt68[48] + pt68[54])/2
|
|
|
|
], axis=0)
|
|
pt2 = np.stack([
|
|
(pt68[0] + pt68[1]) / 2,
|
|
pt68[2]
|
|
], axis=0)
|
|
else:
|
|
pt2 = np.stack([
|
|
(pt68[42] + pt68[43] + pt68[44] + pt68[45] + pt68[46] + pt68[47]) / 6, # left eye
|
|
(pt68[36] + pt68[37] + pt68[38] + pt68[39] + pt68[40] + pt68[41]) / 6, # right eye
|
|
], axis=0)
|
|
|
|
return pt2
|
|
|
|
|
|
def parse_pt2_from_pt5(pt5, use_lip=True):
|
|
"""
|
|
parsing the 2 points according to the 5 points, which cancels the roll
|
|
"""
|
|
if use_lip:
|
|
pt2 = np.stack([
|
|
(pt5[0] + pt5[1]) / 2,
|
|
(pt5[3] + pt5[4]) / 2
|
|
], axis=0)
|
|
else:
|
|
pt2 = np.stack([
|
|
pt5[0],
|
|
pt5[1]
|
|
], axis=0)
|
|
return pt2
|
|
|
|
|
|
def parse_pt2_from_pt_x(pts, use_lip=True):
|
|
if pts.shape[0] == 101:
|
|
pt2 = parse_pt2_from_pt101(pts, use_lip=use_lip)
|
|
elif pts.shape[0] == 106:
|
|
pt2 = parse_pt2_from_pt106(pts, use_lip=use_lip)
|
|
elif pts.shape[0] == 68:
|
|
pt2 = parse_pt2_from_pt68(pts, use_lip=use_lip)
|
|
elif pts.shape[0] == 5:
|
|
pt2 = parse_pt2_from_pt5(pts, use_lip=use_lip)
|
|
elif pts.shape[0] == 203:
|
|
pt2 = parse_pt2_from_pt203(pts, use_lip=use_lip)
|
|
elif pts.shape[0] == 478:
|
|
pt2 = parse_pt2_from_pt478(pts, use_lip=use_lip)
|
|
elif pts.shape[0] > 101:
|
|
# take the first 101 points
|
|
pt2 = parse_pt2_from_pt101(pts[:101], use_lip=use_lip)
|
|
elif pts.shape[0] == 9:
|
|
pt2 = parse_pt2_from_pt9(pts, use_lip=use_lip)
|
|
else:
|
|
raise Exception(f'Unknow shape: {pts.shape}')
|
|
|
|
if not use_lip:
|
|
# NOTE: to compile with the latter code, need to rotate the pt2 90 degrees clockwise manually
|
|
v = pt2[1] - pt2[0]
|
|
pt2[1, 0] = pt2[0, 0] - v[1]
|
|
pt2[1, 1] = pt2[0, 1] + v[0]
|
|
|
|
return pt2
|
|
|
|
|
|
def parse_rect_from_landmark(
|
|
pts,
|
|
scale=1.5,
|
|
need_square=True,
|
|
vx_ratio=0,
|
|
vy_ratio=0,
|
|
use_deg_flag=False,
|
|
**kwargs
|
|
):
|
|
"""parsing center, size, angle from 101/68/5/x landmarks
|
|
vx_ratio: the offset ratio along the pupil axis x-axis, multiplied by size
|
|
vy_ratio: the offset ratio along the pupil axis y-axis, multiplied by size, which is used to contain more forehead area
|
|
|
|
judge with pts.shape
|
|
"""
|
|
pt2 = parse_pt2_from_pt_x(pts, use_lip=kwargs.get('use_lip', True))
|
|
|
|
uy = pt2[1] - pt2[0]
|
|
l = np.linalg.norm(uy)
|
|
if l <= 1e-3:
|
|
uy = np.array([0, 1], dtype=DTYPE)
|
|
else:
|
|
uy /= l
|
|
ux = np.array((uy[1], -uy[0]), dtype=DTYPE)
|
|
|
|
# the rotation degree of the x-axis, the clockwise is positive, the counterclockwise is negative (image coordinate system)
|
|
# print(uy)
|
|
# print(ux)
|
|
angle = acos(ux[0])
|
|
if ux[1] < 0:
|
|
angle = -angle
|
|
|
|
# rotation matrix
|
|
M = np.array([ux, uy])
|
|
|
|
# calculate the size which contains the angle degree of the bbox, and the center
|
|
center0 = np.mean(pts, axis=0)
|
|
rpts = (pts - center0) @ M.T # (M @ P.T).T = P @ M.T
|
|
lt_pt = np.min(rpts, axis=0)
|
|
rb_pt = np.max(rpts, axis=0)
|
|
center1 = (lt_pt + rb_pt) / 2
|
|
|
|
size = rb_pt - lt_pt
|
|
if need_square:
|
|
m = max(size[0], size[1])
|
|
size[0] = m
|
|
size[1] = m
|
|
|
|
size *= scale # scale size
|
|
center = center0 + ux * center1[0] + uy * center1[1] # counterclockwise rotation, equivalent to M.T @ center1.T
|
|
center = center + ux * (vx_ratio * size) + uy * \
|
|
(vy_ratio * size) # considering the offset in vx and vy direction
|
|
|
|
if use_deg_flag:
|
|
angle = degrees(angle)
|
|
|
|
return center, size, angle
|
|
|
|
|
|
def parse_bbox_from_landmark(pts, **kwargs):
|
|
center, size, angle = parse_rect_from_landmark(pts, **kwargs)
|
|
cx, cy = center
|
|
w, h = size
|
|
|
|
# calculate the vertex positions before rotation
|
|
bbox = np.array([
|
|
[cx-w/2, cy-h/2], # left, top
|
|
[cx+w/2, cy-h/2],
|
|
[cx+w/2, cy+h/2], # right, bottom
|
|
[cx-w/2, cy+h/2]
|
|
], dtype=DTYPE)
|
|
|
|
# construct rotation matrix
|
|
bbox_rot = bbox.copy()
|
|
R = np.array([
|
|
[np.cos(angle), -np.sin(angle)],
|
|
[np.sin(angle), np.cos(angle)]
|
|
], dtype=DTYPE)
|
|
|
|
# calculate the relative position of each vertex from the rotation center, then rotate these positions, and finally add the coordinates of the rotation center
|
|
bbox_rot = (bbox_rot - center) @ R.T + center
|
|
|
|
return {
|
|
'center': center, # 2x1
|
|
'size': size, # scalar
|
|
'angle': angle, # rad, counterclockwise
|
|
'bbox': bbox, # 4x2
|
|
'bbox_rot': bbox_rot, # 4x2
|
|
}
|
|
|
|
|
|
def crop_image_by_bbox(img, bbox, lmk=None, dsize=512, angle=None, flag_rot=False, **kwargs):
|
|
left, top, right, bot = bbox
|
|
if int(right - left) != int(bot - top):
|
|
print(f'right-left {right-left} != bot-top {bot-top}')
|
|
size = right - left
|
|
|
|
src_center = np.array([(left + right) / 2, (top + bot) / 2], dtype=DTYPE)
|
|
tgt_center = np.array([dsize / 2, dsize / 2], dtype=DTYPE)
|
|
|
|
s = dsize / size # scale
|
|
if flag_rot and angle is not None:
|
|
costheta, sintheta = cos(angle), sin(angle)
|
|
cx, cy = src_center[0], src_center[1] # ori center
|
|
tcx, tcy = tgt_center[0], tgt_center[1] # target center
|
|
# need to infer
|
|
M_o2c = np.array(
|
|
[[s * costheta, s * sintheta, tcx - s * (costheta * cx + sintheta * cy)],
|
|
[-s * sintheta, s * costheta, tcy - s * (-sintheta * cx + costheta * cy)]],
|
|
dtype=DTYPE
|
|
)
|
|
else:
|
|
M_o2c = np.array(
|
|
[[s, 0, tgt_center[0] - s * src_center[0]],
|
|
[0, s, tgt_center[1] - s * src_center[1]]],
|
|
dtype=DTYPE
|
|
)
|
|
|
|
if flag_rot and angle is None:
|
|
print('angle is None, but flag_rotate is True', style="bold yellow")
|
|
|
|
img_crop = _transform_img(img, M_o2c, dsize=dsize, borderMode=kwargs.get('borderMode', None))
|
|
|
|
lmk_crop = _transform_pts(lmk, M_o2c) if lmk is not None else None
|
|
|
|
M_o2c = np.vstack([M_o2c, np.array([0, 0, 1], dtype=DTYPE)])
|
|
M_c2o = np.linalg.inv(M_o2c)
|
|
|
|
# cv2.imwrite('crop.jpg', img_crop)
|
|
|
|
return {
|
|
'img_crop': img_crop,
|
|
'lmk_crop': lmk_crop,
|
|
'M_o2c': M_o2c,
|
|
'M_c2o': M_c2o,
|
|
}
|
|
|
|
|
|
def _estimate_similar_transform_from_pts(
|
|
pts,
|
|
dsize,
|
|
scale=1.5,
|
|
vx_ratio=0,
|
|
vy_ratio=-0.1,
|
|
flag_do_rot=True,
|
|
**kwargs
|
|
):
|
|
""" calculate the affine matrix of the cropped image from sparse points, the original image to the cropped image, the inverse is the cropped image to the original image
|
|
pts: landmark, 101 or 68 points or other points, Nx2
|
|
scale: the larger scale factor, the smaller face ratio
|
|
vx_ratio: x shift
|
|
vy_ratio: y shift, the smaller the y shift, the lower the face region
|
|
rot_flag: if it is true, conduct correction
|
|
"""
|
|
center, size, angle = parse_rect_from_landmark(
|
|
pts, scale=scale, vx_ratio=vx_ratio, vy_ratio=vy_ratio,
|
|
use_lip=kwargs.get('use_lip', True)
|
|
)
|
|
|
|
s = dsize / size[0] # scale
|
|
tgt_center = np.array([dsize / 2, dsize / 2], dtype=DTYPE) # center of dsize
|
|
|
|
if flag_do_rot:
|
|
costheta, sintheta = cos(angle), sin(angle)
|
|
cx, cy = center[0], center[1] # ori center
|
|
tcx, tcy = tgt_center[0], tgt_center[1] # target center
|
|
# need to infer
|
|
M_INV = np.array(
|
|
[[s * costheta, s * sintheta, tcx - s * (costheta * cx + sintheta * cy)],
|
|
[-s * sintheta, s * costheta, tcy - s * (-sintheta * cx + costheta * cy)]],
|
|
dtype=DTYPE
|
|
)
|
|
else:
|
|
M_INV = np.array(
|
|
[[s, 0, tgt_center[0] - s * center[0]],
|
|
[0, s, tgt_center[1] - s * center[1]]],
|
|
dtype=DTYPE
|
|
)
|
|
|
|
M_INV_H = np.vstack([M_INV, np.array([0, 0, 1])])
|
|
M = np.linalg.inv(M_INV_H)
|
|
|
|
# M_INV is from the original image to the cropped image, M is from the cropped image to the original image
|
|
return M_INV, M[:2, ...]
|
|
|
|
|
|
def crop_image(img, pts: np.ndarray, **kwargs):
|
|
dsize = kwargs.get('dsize', 224)
|
|
scale = kwargs.get('scale', 1.5) # 1.5 | 1.6
|
|
vy_ratio = kwargs.get('vy_ratio', -0.1) # -0.0625 | -0.1
|
|
vx_ratio = kwargs.get('vx_ratio', 0)
|
|
|
|
M_INV, _ = _estimate_similar_transform_from_pts(
|
|
pts,
|
|
dsize=dsize,
|
|
scale=scale,
|
|
vy_ratio=vy_ratio,
|
|
vx_ratio=vx_ratio,
|
|
flag_do_rot=kwargs.get('rotate', True),
|
|
)
|
|
|
|
if img is None:
|
|
M_INV_H = np.vstack([M_INV, np.array([0, 0, 1], dtype=DTYPE)])
|
|
M = np.linalg.inv(M_INV_H)
|
|
ret_dct = {
|
|
'M': M[:2, ...], # from the original image to the cropped image
|
|
'M_o2c': M[:2, ...], # from the cropped image to the original image
|
|
'img_crop': None,
|
|
'pt_crop': None,
|
|
}
|
|
return ret_dct
|
|
|
|
img_crop = _transform_img(img, M_INV, dsize) # origin to crop
|
|
pt_crop = _transform_pts(pts, M_INV)
|
|
|
|
M_o2c = np.vstack([M_INV, np.array([0, 0, 1], dtype=DTYPE)])
|
|
M_c2o = np.linalg.inv(M_o2c)
|
|
|
|
ret_dct = {
|
|
'M_o2c': M_o2c, # from the original image to the cropped image 3x3
|
|
'M_c2o': M_c2o, # from the cropped image to the original image 3x3
|
|
'pt_crop': pt_crop, # the landmarks of the cropped image
|
|
}
|
|
|
|
return ret_dct, img_crop
|
|
|
|
def average_bbox_lst(bbox_lst):
|
|
if len(bbox_lst) == 0:
|
|
return None
|
|
bbox_arr = np.array(bbox_lst)
|
|
return np.mean(bbox_arr, axis=0).tolist()
|