IS_DLIB_INSTALLED = False try: import dlib IS_DLIB_INSTALLED = True except ImportError: pass import torch #import torch.nn.functional as F import torchvision.transforms.v2 as T import numpy as np import comfy.utils import comfy.model_management as mm import gc from tqdm import tqdm import cv2 import numpy as np from scipy.interpolate import RBFInterpolator import folder_paths import os import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') log = logging.getLogger(__name__) script_directory = os.path.dirname(os.path.abspath(__file__)) try: from .liveportrait.utils.cropper import CropperFaceAlignment except: log.warning("Can't load FaceAlignment, CropperFaceAlignment not available") try: from .liveportrait.utils.cropper import CropperMediaPipe except: log.warning("Can't load MediaPipe, MediaPipeCropper not available") try: from .liveportrait.utils.cropper import CropperInsightFace except: log.warning("Can't load MediaPipe, MediaPipeCropper not available") from .liveportrait.utils.crop import _transform_img_kornia DLIB_DIR = os.path.join(folder_paths.models_dir, "dlib") class DLib: def __init__(self, predictor=68): self.face_detector = dlib.get_frontal_face_detector() # check if the models are available if not os.path.exists(os.path.join(DLIB_DIR, "shape_predictor_5_face_landmarks.dat")): raise Exception("The 5 point landmark model is not available. Please download it from https://huggingface.co/matt3ounstable/dlib_predictor_recognition/blob/main/shape_predictor_5_face_landmarks.dat") if not os.path.exists(os.path.join(DLIB_DIR, "dlib_face_recognition_resnet_model_v1.dat")): raise Exception("The face recognition model is not available. Please download it from https://huggingface.co/matt3ounstable/dlib_predictor_recognition/blob/main/dlib_face_recognition_resnet_model_v1.dat") self.predictor=predictor if predictor == 81: self.shape_predictor = dlib.shape_predictor(os.path.join(DLIB_DIR, "shape_predictor_81_face_landmarks.dat")) elif predictor == 5: self.shape_predictor = dlib.shape_predictor(os.path.join(DLIB_DIR, "shape_predictor_5_face_landmarks.dat")) else: self.shape_predictor = dlib.shape_predictor(os.path.join(DLIB_DIR, "shape_predictor_68_face_landmarks.dat")) self.face_recognition = dlib.face_recognition_model_v1(os.path.join(DLIB_DIR, "dlib_face_recognition_resnet_model_v1.dat")) #self.thresholds = THRESHOLDS["Dlib"] def get_face(self, image): faces = self.face_detector(np.array(image), 1) #faces, scores, _ = self.face_detector.run(np.array(image), 1, -1) if len(faces) > 0: return sorted(faces, key=lambda x: x.area(), reverse=True) #return [face for _, face in sorted(zip(scores, faces), key=lambda x: x[0], reverse=True)] # sort by score return None # 检测面部并提取关键点 def get_landmarks(self, image): gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) faces = self.face_detector(gray) if len(faces) == 0: return None shape = self.shape_predictor(gray, faces[0]) landmarks = np.array([[p.x, p.y] for p in shape.parts()]) if self.predictor == 81: landmarks = np.concatenate((landmarks[:17], landmarks[68:81])) return landmarks elif self.predictor == 5: return landmarks else: return landmarks[:17] def get_all_landmarks(self, image): gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) faces = self.face_detector(gray) if len(faces) == 0: return None shape = self.shape_predictor(gray, faces[0]) output = np.array([[p.x, p.y] for p in shape.parts()]) if self.predictor == 81: leftEye=np.mean( output[36:42],axis=0) rightEye=np.mean( output[42:48],axis=0) mouth=np.mean( output[48:68],axis=0) elif self.predictor == 5: leftEye=np.mean( output[0:3],axis=0) rightEye=np.mean( output[2:4],axis=0) mouth=output[4] else: leftEye=np.mean( output[36:42],axis=0) rightEye=np.mean( output[42:48],axis=0) mouth=np.mean( output[48:68],axis=0) return output,leftEye,rightEye,mouth def draw_landmarks(self, image, landmarks, color=(255, 0, 0), radius=3): # cv2.circle打坐标点的坐标系,如下。左上角是原点,先写x再写y # (0,0)-------------(w,0) # | | # | | # (0,h)-------------(w,h)| #font = cv2.FONT_HERSHEY_SIMPLEX image_cpy = image.copy() for n in range(landmarks.shape[0]): try: cv2.circle(image_cpy, (int(landmarks[n][0]), int(landmarks[n][1])), radius, color, -1) except: pass #cv2.putText(image_cpy, str(n), (landmarks[n][1], landmarks[n][0]), font, 0.5, color, 1, cv2.LINE_AA) return image_cpy def interpolate(self, image1, image2,landmarkType,AlignType,GenLandMarkImg): height,width = image1.shape[:2] w=width h=height if landmarkType == "ALL" or AlignType == "Landmarks": landmarks1,leftEye1,rightEye1,mouth1 = self.get_all_landmarks(image1) landmarks2,leftEye2,rightEye2,mouth2 = self.get_all_landmarks(image2) else: landmarks1 = self.get_landmarks(image1) landmarks2 = self.get_landmarks(image2) #画面划分成16*16个区域,然后去掉边界框以外的区域。 src_points = np.array([ [x, y] for x in np.linspace(0, w, 16) for y in np.linspace(0, h, 16) ]) #上面这些区域同时被加入src和dst,使这些区域不被拉伸(效果是图片边缘不被拉伸) src_points = src_points[(src_points[:, 0] <= w/8) | (src_points[:, 0] >= 7*w/8) | (src_points[:, 1] >= 7*h/8)| (src_points[:, 1] <= h/8)] #mark_img = self.draw_landmarks(mark_img, src_points, color=(255, 0, 255)) dst_points = src_points.copy() #不知道原作者为何把这个数组叫dst,其实这是变形前的坐标,即原图的坐标 dst_points = np.append(dst_points,landmarks1,axis=0) #变形目标人物的landmarks,先计算边界框 landmarks2=np.array(landmarks2) min_x = np.min(landmarks2[:, 0]) max_x = np.max(landmarks2[:, 0]) min_y = np.min(landmarks2[:, 1]) max_y = np.max(landmarks2[:, 1]) #得到目标人物的边界框的长宽比 ratio2 = (max_x - min_x) / (max_y - min_y) #变形原始人物的landmarks,边界框 landmarks1=np.array(landmarks1) min_x = np.min(landmarks1[:, 0]) max_x = np.max(landmarks1[:, 0]) min_y = np.min(landmarks1[:, 1]) max_y = np.max(landmarks1[:, 1]) #得到原始人物的边界框的长宽比以及中心点 ratio1 = (max_x - min_x) / (max_y - min_y) middlePoint = [ (max_x + min_x) / 2, (max_y + min_y) / 2] landmarks1_cpy = landmarks1.copy() if AlignType=="Width": #保持人物脸部边界框中心点不变,垂直方向上缩放,使边界框的比例变得跟目标人物的边界框比例一致 landmarks1_cpy[:, 1] = (landmarks1_cpy[:, 1] - middlePoint[1]) * ratio1 / ratio2 + middlePoint[1] elif AlignType=="Height": #保持人物脸部边界框中心点不变,水平方向上缩放,使边界框的比例变得跟目标人物的边界框比例一致 landmarks1_cpy[:, 0] = (landmarks1_cpy[:, 0] - middlePoint[0]) * ratio2 / ratio1 + middlePoint[0] elif AlignType=="Landmarks": MiddleOfEyes1 = (leftEye1+rightEye1)/2 MiddleOfEyes2 = (leftEye2+rightEye2)/2 # angle = float(np.degrees(np.arctan2(leftEye2[1] - rightEye2[1], leftEye2[0] - rightEye2[0]))) # angle -= float(np.degrees(np.arctan2(leftEye1[1] - rightEye1[1], leftEye1[0] - rightEye1[0]))) # rotation_matrix = np.array([ # [np.cos(angle), -np.sin(angle)], # [np.sin(angle), np.cos(angle)] # ]) distance1 = ((leftEye1[0] - rightEye1[0]) ** 2 + (leftEye1[1] - rightEye1[1]) ** 2) ** 0.5 distance2 = ((leftEye2[0] - rightEye2[0]) ** 2 + (leftEye2[1] - rightEye2[1]) ** 2) ** 0.5 factor = distance1 / distance2 # print("distance1:",distance1) # print("distance2:",distance2) # print("factor:",factor) # print("MiddleOfEyes1:",MiddleOfEyes1) # print("MiddleOfEyes2:",MiddleOfEyes2) # print("angle:",angle) MiddleOfEyes2 = np.array(MiddleOfEyes2) landmarks1_cpy = (landmarks2 - MiddleOfEyes2) * factor + MiddleOfEyes1 #landmarks1_cpy = landmarks1_cpy + MiddleOfEyes1 # landmarks1_cpy = (landmarks2 - MiddleOfEyes2) * factor # landmarks1_cpy = landmarks1_cpy.T # # 旋转坐标 # rotated_landmarks = np.dot(rotation_matrix, landmarks1_cpy) # # 将旋转后的坐标转换回行向量 # rotated_landmarks = rotated_landmarks.T # # 将 MiddleOfEyes1 转换为二维数组 # MiddleOfEyes1 = np.array(MiddleOfEyes1) # # 将 landmarks1_cpy 和 MiddleOfEyes1_expanded 相加 # landmarks1_cpy = landmarks1_cpy + MiddleOfEyes1 #不知道原作者为何把这个数组叫src,其实这是变形后的坐标 src_points = np.append(src_points,landmarks1_cpy,axis=0) #print(landmarks1_cpy) mark_img = self.draw_landmarks(image1, dst_points, color=(255, 255, 0),radius=4) mark_img = self.draw_landmarks(mark_img, src_points, color=(255, 0, 0),radius=3) # Create the RBF interpolator instance #Tried many times, finally find out these array should be exchange w,h before go into RBFInterpolator src_points[:, [0, 1]] = src_points[:, [1, 0]] dst_points[:, [0, 1]] = dst_points[:, [1, 0]] rbfy = RBFInterpolator(src_points,dst_points[:,1],kernel="thin_plate_spline") rbfx = RBFInterpolator(src_points,dst_points[:,0],kernel="thin_plate_spline") # Create a meshgrid to interpolate over the entire image img_grid = np.mgrid[0:height, 0:width] # flatten grid so it could be feed into interpolation flatten=img_grid.reshape(2, -1).T # Interpolate the displacement using the RBF interpolators map_y = rbfy(flatten).reshape(height,width).astype(np.float32) map_x = rbfx(flatten).reshape(height,width).astype(np.float32) # Apply the remapping to the image using OpenCV warped_image = cv2.remap(image1, map_y, map_x, cv2.INTER_LINEAR) if GenLandMarkImg: return warped_image, mark_img else: return warped_image, warped_image class FaceShaperModels: @classmethod def INPUT_TYPES(s): libraries = [] if IS_DLIB_INSTALLED: libraries.append("dlib") return {"required": { "DetectType": ([81,68,5], ), }} RETURN_TYPES = ("FaceShaper_MODELS", ) FUNCTION = "load_models" CATEGORY = "FaceShaper" def load_models(self, DetectType): out = {} # if library == "insightface": # out = InsightFace(provider) # else: # out = DLib() out = DLib(DetectType) return (out, ) def draw_pointsOnImg(image, landmarks, color=(255, 0, 0), radius=3): # cv2.circle打坐标点的坐标系,如下。左上角是原点,先写x再写y # (0,0)-------------(w,0) # | | # | | # (0,h)-------------(w,h)| image_cpy = image.copy() for n in range(landmarks.shape[0]): try: cv2.circle(image_cpy, (int(landmarks[n][0]), int(landmarks[n][1])), radius, color, -1) except: pass return image_cpy def write_pointsOnImg(image, landmarks, color=(255, 0, 0), fontsize=0.25): font = cv2.FONT_HERSHEY_SIMPLEX image_cpy = image.copy() for n in range(landmarks.shape[0]): try: cv2.putText(image_cpy, str(n), (int(landmarks[n][0]), int(landmarks[n][1])), font, fontsize, color, 1, cv2.LINE_AA) cv2.circle(image_cpy, (int(landmarks[n][0]), int(landmarks[n][1])), 1, color, -1) except: pass return image_cpy def drawLineBetweenPoints(image, pointsA, pointsB, color=(255, 0, 0), thickness=1): image_cpy = image.copy() for n in range(pointsA.shape[0]): try: cv2.line(image_cpy, (int(pointsA[n][0]), int(pointsA[n][1])), (int(pointsB[n][0]), int(pointsB[n][1])), color, thickness) except: pass return image_cpy def tensor_to_image(image): return np.array(T.ToPILImage()(image.permute(2, 0, 1)).convert('RGB')) def image_to_tensor(image): return T.ToTensor()(image).permute(1, 2, 0) #return T.ToTensor()(Image.fromarray(image)).permute(1, 2, 0) def mask_from_landmarks(height, width, landmarks): #import cv2 mask = np.zeros((height, width), dtype=np.float64) points = cv2.convexHull(landmarks) points = np.array(points, dtype=np.int32) cv2.fillConvexPoly(mask, points, color=1) return mask def expand_mask(mask, expand, tapered_corners): import scipy c = 0 if tapered_corners else 1 kernel = np.array([[c, 1, c], [1, 1, 1], [c, 1, c]]) mask = mask.reshape((-1, mask.shape[-2], mask.shape[-1])) out = [] for m in mask: output = m.numpy() for _ in range(abs(expand)): if expand < 0: output = scipy.ndimage.grey_erosion(output, footprint=kernel) else: output = scipy.ndimage.grey_dilation(output, footprint=kernel) output = torch.from_numpy(output) out.append(output) return torch.stack(out, dim=0) class FaceShaper: def __init__(self): pass @classmethod def INPUT_TYPES(s): return { "required": { "analysis_models": ("FaceShaper_MODELS", ), "imageFrom": ("IMAGE",), "imageTo": ("IMAGE",), "landmarkType": (["ALL","OUTLINE"], ), "AlignType":(["Width","Height","Landmarks"], ), #"TargetFlip":([True,False],), }, } RETURN_TYPES = ("IMAGE","IMAGE") RETURN_NAMES = ("Image1","LandmarkImg") FUNCTION = "run" CATEGORY = "FaceShaper" def run(self,analysis_models,imageFrom, imageTo,landmarkType,AlignType): tensor1 = imageFrom*255 tensor1 = np.array(tensor1, dtype=np.uint8) tensor2 = imageTo*255 tensor2 = np.array(tensor2, dtype=np.uint8) output=[] image1 = tensor1[0] image2 = tensor2[0] img1,img2 = analysis_models.interpolate(image1,image2,landmarkType,AlignType,True) img1 = torch.from_numpy(img1.astype(np.float32) / 255.0).unsqueeze(0) img2 = torch.from_numpy(img2.astype(np.float32) / 255.0).unsqueeze(0) output.append(img1) output.append(img2) return (output) class FaceShaperShowLandMarks: @classmethod def INPUT_TYPES(s): return {"required": { "crop_info": ("CROPINFO", {"default": []}), "landmark_lines": ("BOOLEAN", {"default": False}), "writeIndexOnKeyPoints": ("BOOLEAN", {"default": False}), "fontSize": ("FLOAT", {"default": 0.25, "min": 0.1, "max": 5, "step": 0.05}), "rescaleCroppedImg": ("INT", {"default": 512, "min": 128, "max": 2048, "step": 64}), }, "optional": { "croppedImg": ("IMAGE", {"default": None}), "sourceImg": ("IMAGE", {"default": None}), } } RETURN_TYPES = ("IMAGE","IMAGE","IMAGE") RETURN_NAMES = ("landmark on source","landmark on cropped","keyPoints on cropped") FUNCTION = "run" CATEGORY = "FaceShaper" def run(self, crop_info, landmark_lines, writeIndexOnKeyPoints,fontSize,rescaleCroppedImg,croppedImg=None, sourceImg=None): if sourceImg is not None: sourceImg = sourceImg*255 sourceImg = np.array(sourceImg, dtype=np.uint8) if croppedImg is not None: croppedImg = croppedImg*255 croppedImg = np.array(croppedImg, dtype=np.uint8) #output=[] lmk = crop_info["crop_info_list"][0]['lmk_source'] height, width = crop_info["crop_info_list"][0]['input_image_size'] img1=self.draw203keypoints(lmk, landmark_lines, sourceImg,height, width,writeIndexOnKeyPoints) #output.append(img1) lmk_crop = crop_info["crop_info_list"][0]['lmk_crop'] dsize = crop_info["crop_info_list"][0]['dsize'] img2=self.draw203keypoints(lmk_crop, landmark_lines, croppedImg,dsize,dsize,writeIndexOnKeyPoints) #output.append(img2) if writeIndexOnKeyPoints: img3 = (self.writePt_crop(crop_info,croppedImg, fontSize,rescaleCroppedImg)) #output.append(img3) else: img3=(self.drawPt_crop(crop_info,croppedImg,rescaleCroppedImg)) #print("img3",img3.shape) #it is torch.Size([1, 1024, 1024, 3]) output=[] output.append(img1) output.append(img2) output.append(img3) return (output) def draw203keypoints(self, lmk, draw_lines, sourceImg,height,width,writeIndex): # left upper eye | left lower eye | right upper eye | right lower eye | upper lip top | lower lip bottom | upper lip bottom | lower lip top | jawline | left eyebrow | right eyebrow | nose | left pupil | right pupil | nose center indices = [ 12, 24, 37, 48, 66, 85, 96, 108, 145, 165, 185, 197, 198, 199, 203] colorlut = [(0, 0, 255), (0, 255, 0), (0, 0, 255), (0, 255, 0), (255, 0, 0), (255, 0, 255), (255, 255, 0), (0, 255, 255), (128, 128, 128), (128, 128, 0), (128, 128, 0), (0,128,128), (255, 255,255), (255, 255,255), (255,255,255)] colors = [] c = 0 for i in range(203): if i == indices[c]: c+=1 colors.append(colorlut[c]) if sourceImg is not None: target_image = sourceImg[0].copy() else: target_image = np.zeros((height, width, 3), dtype=np.uint8) * 255 keypoints_img_list = [] keypoints = lmk.copy() if draw_lines: start_idx = 0 for end_idx in indices: color = colors[start_idx] for i in range(start_idx, end_idx - 1): pt1 = tuple(map(int, keypoints[i])) pt2 = tuple(map(int, keypoints[i+1])) if all(0 <= c < d for c, d in zip(pt1 + pt2, (width, height) * 2)): cv2.line(target_image, pt1, pt2, color, thickness=1) if end_idx == start_idx +1: x,y = keypoints[start_idx] cv2.circle(target_image, (int(x), int(y)), radius=1, thickness=-1, color=colors[start_idx]) start_idx = end_idx else: for index, (x, y) in enumerate(keypoints): cv2.circle(target_image, (int(x), int(y)), radius=1, thickness=-1, color=colors[index]) if(writeIndex): cv2.putText(target_image, str(index), (int(x), int(y)), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 1, cv2.LINE_AA) #keypoints_image = cv2.cvtColor(blank_image, cv2.COLOR_BGR2RGB) return torch.from_numpy(target_image.astype(np.float32) / 255.0).unsqueeze(0) keypoints_img_list.append(keypoints_image) keypoints_img_tensor = ( torch.stack([torch.from_numpy(np_array) for np_array in keypoints_img_list]) / 255).float() return (keypoints_img_tensor,) def drawPt_crop(self, crop_info, croppedImg,ImageSize): pbar = comfy.utils.ProgressBar(len(crop_info)) c=0 output=[] for crop in crop_info["crop_info_list"]: if crop: if ImageSize != 512: inputImg = cv2.resize(croppedImg[c], (ImageSize, ImageSize), interpolation=cv2.INTER_AREA) keys = crop['pt_crop'].copy() keys = keys * (ImageSize / 512) mark_img = draw_pointsOnImg(inputImg, keys, color=(255, 0, 0),radius=3) else: mark_img = draw_pointsOnImg(croppedImg[c], crop['pt_crop'], color=(255, 0, 0),radius=3) else: mark_img = croppedImg[c].copy() c += 1 mark_img= torch.from_numpy(mark_img.astype(np.float32) / 255.0).unsqueeze(0) return mark_img output.append(mark_img) pbar.update(1) return torch.stack(output,) #cropped_image_256 = cv2.resize(image_crop, (256, 256), interpolation=cv2.INTER_AREA) def writePt_crop(self, crop_info, croppedImg, fontSize,ImageSize): pbar = comfy.utils.ProgressBar(len(crop_info)) c=0 output=[] for crop in crop_info["crop_info_list"]: if crop: if ImageSize != 512: inputImg = cv2.resize(croppedImg[c], (ImageSize, ImageSize), interpolation=cv2.INTER_AREA) keys = crop['lmk_crop'].copy() keys = keys * (ImageSize / 512) mark_img = write_pointsOnImg(inputImg, keys, color=(255, 0, 0),fontsize=fontSize) else: mark_img = write_pointsOnImg(croppedImg[c].copy(), crop['lmk_crop'], color=(255, 0, 0),fontsize=fontSize) else: mark_img = croppedImg[c].copy() c += 1 mark_img= torch.from_numpy(mark_img.astype(np.float32) / 255.0).unsqueeze(0) return mark_img output.append(mark_img) pbar.update(1) return torch.stack(output,) class FaceShaperLoadInsightFaceCropper: @classmethod def INPUT_TYPES(s): return {"required": { "onnx_device": ( ['CPU', 'CUDA', 'ROCM', 'CoreML'], { "default": 'CPU' }), "keep_model_loaded": ("BOOLEAN", {"default": True}) }, "optional": { "detection_threshold": ("FLOAT", {"default": 0.5, "min": 0.05, "max": 1.0, "step": 0.01}), }, } RETURN_TYPES = ("FSMCROPPER",) RETURN_NAMES = ("cropper",) FUNCTION = "crop" CATEGORY = "FaceShaper" def crop(self, onnx_device, keep_model_loaded, detection_threshold=0.5): cropper_init_config = { 'keep_model_loaded': keep_model_loaded, 'onnx_device': onnx_device, 'detection_threshold': detection_threshold } 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 = CropperInsightFace(**cropper_init_config) return (self.cropper,) class FaceShaperLoadMediaPipeCropper: @classmethod def INPUT_TYPES(s): return {"required": { "landmarkrunner_onnx_device": ( ['CPU', 'CUDA', 'ROCM', 'CoreML', 'torch_gpu'], { "default": 'CPU' }), "keep_model_loaded": ("BOOLEAN", {"default": True}) }, } RETURN_TYPES = ("FSMCROPPER",) RETURN_NAMES = ("cropper",) FUNCTION = "crop" CATEGORY = "FaceShaper" def crop(self, landmarkrunner_onnx_device, keep_model_loaded): cropper_init_config = { 'keep_model_loaded': keep_model_loaded, 'onnx_device': landmarkrunner_onnx_device } 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 = CropperMediaPipe(**cropper_init_config) return (self.cropper,) class FaceAlignmentCropper: @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 = ("FSMCROPPER",) RETURN_NAMES = ("cropper",) FUNCTION = "crop" CATEGORY = "FaceShaper" 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 FaceShaperCropper: @classmethod def INPUT_TYPES(s): return {"required": { "cropper": ("FSMCROPPER",), "source_image": ("IMAGE",), "dsize": ("INT", {"default": 512, "min": 64, "max": 2048}), "scale": ("FLOAT", {"default": 2.3, "min": 1.0, "max": 4.0, "step": 0.01}), "vx_ratio": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.001}), "vy_ratio": ("FLOAT", {"default": -0.125, "min": -1.0, "max": 1.0, "step": 0.001}), "face_index": ("INT", {"default": 0, "min": 0, "max": 100}), "face_index_order": ( [ 'large-small', 'left-right', 'right-left', 'top-bottom', 'bottom-top', 'small-large', 'distance-from-retarget-face' ], ), "rotate": ("BOOLEAN", {"default": True}), }, } RETURN_TYPES = ("IMAGE", "CROPINFO",) RETURN_NAMES = ("cropped_image", "crop_info",) FUNCTION = "process" CATEGORY = "FaceShaper" def process(self, cropper, source_image, dsize,scale, vx_ratio, vy_ratio, face_index, face_index_order, rotate): source_image_np = (source_image.contiguous() * 255).byte().numpy() # Initialize lists crop_info_list = [] cropped_images_list = [] # source_info = [] # source_rot_list = [] # f_s_list = [] # x_s_list = [] #KJ原版代码里,这个值是输入参数,取值范围64-2048,默认512 #会影响后面的composite操作。本人来不及修改后面的composite操作,所以这里直接写死512 #dsize = 512 # Initialize a progress bar for the combined operation pbar = comfy.utils.ProgressBar(len(source_image_np)) for i in tqdm(range(len(source_image_np)), desc='Detecting, cropping, and processing..', total=len(source_image_np)): # Cropping operation crop_info, cropped_image = cropper.crop_single_image(source_image_np[i], dsize, scale, vy_ratio, vx_ratio, face_index, face_index_order, rotate) # Processing source images if crop_info: crop_info['dsize'] = dsize crop_info_list.append(crop_info) cropped_images_list.append(cropped_image) # f_s_list.append(None) # x_s_list.append(None) # source_info.append(None) # source_rot_list.append(None) else: log.warning(f"Warning: No face detected on frame {str(i)}, skipping") cropped_images_list.append(np.zeros((256, 256, 3), dtype=np.uint8)) crop_info_list.append(None) # f_s_list.append(None) # x_s_list.append(None) # source_info.append(None) # source_rot_list.append(None) # Update progress bar pbar.update(1) #### "crop_info" contains the information of the crop #### ['M_o2c'] Matrix of the transformation from source image to crop #### ['M_c2o'] Matrix of the transformation from crop to source image #### ['pt_crop'] keypoints of the cropped image. # If using insightFace, there are 103 keypoints. If using faceAlignment, there are 68. # If using MediaPipe, there are 478. Yes, that's correct, it's really 478. # 如果使用insightFace,关键点有103个。如果使用faceAlignment,则有68个。如果使用MediaPipe,则有478个。是的,就是这么多,真是478个。 #### ['lmk_source'] 标准化的203个关键点,在sourceImage上的位置 #### ['lmk_crop'] 标准化的203个关键点,在cropped Image上的位置 #### ['input_image_size'] source image size #### ['dsize'] cropped image size-- cropped_tensors_out = ( torch.stack([torch.from_numpy(np_array) for np_array in cropped_images_list]) / 255 ) crop_info_dict = { 'crop_info_list': crop_info_list, # 'source_rot_list': source_rot_list, # 'f_s_list': f_s_list, # 'x_s_list': x_s_list, # 'source_info': source_info } #print(crop_info_dict) return (cropped_tensors_out, crop_info_dict) class FaceShaperComposite: @classmethod def INPUT_TYPES(s): return {"required": { "source_image": ("IMAGE",), "cropped_image": ("IMAGE",), "crop_info": ("CROPINFO", ), }, # "mismatch_method": ( # [ # "constant", # "cycle", # "mirror", # "cut" # ], # {"default": "constant"}, # ), "optional": { "mask": ("MASK", {"default": None}), } } RETURN_TYPES = ( "IMAGE", "MASK", ) RETURN_NAMES = ( "full_images", "mask", ) FUNCTION = "process" CATEGORY = "FaceShaper" def process(self, source_image, cropped_image, crop_info, mask=None): mm.soft_empty_cache() gc.collect() device = mm.get_torch_device() if mm.is_device_mps(device): device = torch.device('cpu') #this function returns NaNs on MPS, defaulting to CPU B, H, W, C = source_image.shape source_image = source_image.permute(0, 3, 1, 2) # B,H,W,C -> B,C,H,W #cropped_image = cropped_image.permute(0, 3, 1, 2) if mask is not None: if len(mask.size())==2: crop_mask = mask.unsqueeze(0).unsqueeze(-1).expand(-1, -1, -1, 3) else: crop_mask = mask.unsqueeze(-1).expand(-1, -1, -1, 3) else: log.info("Using default mask template") crop_mask = cv2.imread(os.path.join(script_directory, "liveportrait", "utils", "resources", "mask_template.png"), cv2.IMREAD_COLOR) crop_mask = torch.from_numpy(crop_mask) crop_mask = crop_mask.unsqueeze(0).float() / 255.0 #crop_info = liveportrait_out["crop_info"] composited_image_list = [] out_mask_list = [] total_frames = len(crop_info["crop_info_list"]) log.info(f"Total frames: {total_frames}") pbar = comfy.utils.ProgressBar(total_frames) for i in tqdm(range(total_frames), desc='Compositing..', total=total_frames): safe_index = min(i, len(crop_info["crop_info_list"]) - 1) #if mismatch_method == "cut": source_frame = source_image[safe_index].unsqueeze(0).to(device) #print("source_frame.shape", source_frame.shape) #else: # source_frame = _get_source_frame(source_image, i, mismatch_method).unsqueeze(0).to(device) croppedImage = cropped_image[safe_index].unsqueeze(0).to(device) #print("cropped_image.shape", cropped_image.shape) #if not : # composited_image_list.append(source_frame.cpu()) # out_mask_list.append(torch.zeros((1, 3, H, W), device="cpu")) #else: #cropped_image = torch.clamp(liveportrait_out["out_list"][i]["out"], 0, 1).permute(0, 2, 3, 1) # Transform and blend cropped_image_to_original = _transform_img_kornia( croppedImage, crop_info["crop_info_list"][safe_index]["M_c2o"], dsize=(W, H), device=device ) #print("cropped_image_to_original.shape", cropped_image_to_original.shape) mask_ori = _transform_img_kornia( crop_mask[0].unsqueeze(0), crop_info["crop_info_list"][safe_index]["M_c2o"], dsize=(W, H), device=device ) #print("mask_ori.shape", mask_ori.shape) cropped_image_to_original_blend = torch.clip( mask_ori * cropped_image_to_original + (1 - mask_ori) * source_frame, 0, 1 ) composited_image_list.append(cropped_image_to_original_blend.cpu()) out_mask_list.append(mask_ori.cpu()) pbar.update(1) full_tensors_out = torch.cat(composited_image_list, dim=0) full_tensors_out = full_tensors_out.permute(0, 2, 3, 1) mask_tensors_out = torch.cat(out_mask_list, dim=0) mask_tensors_out = mask_tensors_out[:, 0, :, :] return ( full_tensors_out.float(), mask_tensors_out.float() ) class FaceShaperMatchV2: def __init__(self): pass @classmethod def INPUT_TYPES(s): return { "required": { "source_image": ("IMAGE",), "source_crop_info": ("CROPINFO", ), "target_crop_info": ("CROPINFO", ), "landmarkType": (["ALL","OUTLINE"], ), "AlignType":(["Width","Height","Landmarks","JawLine"], ), }, } RETURN_TYPES = ("IMAGE","IMAGE") RETURN_NAMES = ("Image1","LandmarkImg") FUNCTION = "run" CATEGORY = "FaceShaper" def LandMark203_to_68(self,source): #jawLine out = [source[108]] out.append( (source[108+2]*3 + source[108+3] )/4) out.append((source[108+4]+source[108+5])/2) out.append( (source[108+6] + source[108+7]*3 )/4) out.append(source[108+9]) out.append( (source[108+11]*3 + source[108+12] )/4) out.append((source[108+13]+source[108+14])/2) out.append( (source[108+15] + source[108+16]*3 )/4) #for i in range(0,7): # out.append((source[110+i*2]+source[111+i*2])/2) out.append(source[126]) # for i in range(0,7): # out.append((source[128+i*2]+source[129+i*2])/2) out.append( (source[126+2]*3 + source[126+3] )/4) out.append((source[126+4]+source[126+5])/2) out.append( (source[126+6] + source[126+7]*3 )/4) out.append(source[126+9]) out.append( (source[126+11]*3 + source[126+12] )/4) out.append((source[126+13]+source[126+14])/2) out.append( (source[126+15] + source[126+16]*3 )/4) out.append(source[144]) #leftEyeBow out.append(source[145]) out.append((source[148]+source[162])/2) out.append((source[150]+source[160])/2) out.append((source[152]+source[158])/2) out.append(source[155]) #rightEyeBow out.append(source[165]) out.append((source[168]+source[182])/2) out.append((source[170]+source[180])/2) out.append((source[172]+source[177])/2) out.append(source[175]) #nose out.append(source[199]) out.append((source[199]+source[200])/2) out.append(source[200]) out.append(source[201]) out.append(source[189]) out.append(source[190]) out.append(source[202]) out.append(source[191]) out.append(source[192]) #leftEye out.append(source[0]) out.append(source[3]) out.append(source[8]) out.append(source[12]) out.append(source[16]) out.append(source[21]) #rightEye out.append(source[24]) out.append(source[28]) out.append(source[33]) out.append(source[36]) out.append(source[39]) out.append(source[45]) #UpperLipUp out.append(source[48]) out.append(source[51]) out.append(source[54]) out.append(source[57]) out.append(source[60]) out.append(source[63]) out.append(source[66]) #LowerLipDown out.append(source[69]) out.append(source[72]) out.append(source[75]) out.append(source[78]) out.append(source[81]) out.append(source[84]) out.append(source[87]) out.append(source[90]) out.append(source[93]) out.append(source[96]) out.append(source[99]) out.append(source[102]) out.append(source[105]) return out def run(self,source_image,source_crop_info, target_crop_info,landmarkType,AlignType): tensor1 = source_image*255 tensor1 = np.array(tensor1, dtype=np.uint8) output=[] image1 = tensor1[0] #image2 = tensor2[0] height,width = image1.shape[:2] w=width h=height landmarks1 = source_crop_info["crop_info_list"][0]['lmk_crop'] landmarks2 = target_crop_info["crop_info_list"][0]['lmk_crop'] #print(len(landmarks1)) #len(lendmarks1) will always be 203 #V2版本改了检测工具,因此只用203个点 #203个点太多,影响液化算法的运行效率,再次转换成68个点 use_68_points=True if(use_68_points): landmarks1 = self.LandMark203_to_68(landmarks1) landmarks2 = self.LandMark203_to_68(landmarks2) landmarks1 = landmarks1[0:65] landmarks2 = landmarks2[0:65] #else: if(use_68_points): leftEye1=np.mean( landmarks1[36:42],axis=0) rightEye1=np.mean( landmarks1[42:48],axis=0) leftEye2=np.mean( landmarks2[36:42],axis=0) rightEye2=np.mean( landmarks2[42:48],axis=0) jaw1=landmarks1[0:17] jaw2=landmarks2[0:17] centerOfJaw1=np.mean( jaw1,axis=0) centerOfJaw2=np.mean( jaw2,axis=0) else: #保留这个是为了以后要是想改回来 leftEye1=np.mean( landmarks1[0:24],axis=0) rightEye1=np.mean( landmarks1[24:48],axis=0) leftEye2=np.mean( landmarks2[0:24],axis=0) rightEye2=np.mean( landmarks2[24:48],axis=0) jaw1=landmarks1[108:145] jaw2=landmarks2[108:145] centerOfJaw1=np.mean( jaw1,axis=0) centerOfJaw2=np.mean( jaw2,axis=0) # if landmarkType == "ALL": # or AlignType == "Landmarks" or AlignType == "JawLine": # if(len(landmarks1)==203): # #保留这个是为了以后要是想改回来 # leftEye1=np.mean( landmarks1[0:24],axis=0) # rightEye1=np.mean( landmarks1[24:48],axis=0) # leftEye2=np.mean( landmarks2[0:24],axis=0) # rightEye2=np.mean( landmarks2[24:48],axis=0) # jaw1=landmarks1[108:145] # jaw2=landmarks2[108:145] # centerOfJaw1=np.mean( jaw1,axis=0) # centerOfJaw2=np.mean( jaw2,axis=0) # else: # leftEye1=np.mean( landmarks1[36:42],axis=0) # rightEye1=np.mean( landmarks1[42:48],axis=0) # leftEye2=np.mean( landmarks2[36:42],axis=0) # rightEye2=np.mean( landmarks2[42:48],axis=0) # jaw1=landmarks1[0:17] # jaw2=landmarks2[0:17] # centerOfJaw1=np.mean( jaw1,axis=0) # centerOfJaw2=np.mean( jaw2,axis=0) # else: # if(len(landmarks1)==203): # #保留这个是为了以后要是想改回来 # landmarks1 = landmarks1[108:145] # landmarks2 = landmarks2[108:145] # else: # landmarks1 = landmarks1[0:17] # landmarks2 = landmarks2[0:17] #画面划分成16*16个区域,然后去掉边界框以外的区域。 src_points = np.array([ [x, y] for x in np.linspace(0, w, 16) for y in np.linspace(0, h, 16) ]) #上面这些区域同时被加入src和dst,使这些区域不被拉伸(效果是图片边缘不被拉伸) src_points = src_points[(src_points[:, 0] <= w/8) | (src_points[:, 0] >= 7*w/8) | (src_points[:, 1] >= 7*h/8)| (src_points[:, 1] <= h/8)] dst_points = src_points.copy() #变形目标人物的landmarks,先计算边界框 landmarks2=np.array(landmarks2) min_x = np.min(landmarks2[:, 0]) max_x = np.max(landmarks2[:, 0]) min_y = np.min(landmarks2[:, 1]) max_y = np.max(landmarks2[:, 1]) #得到目标人物的边界框的长宽比 ratio2 = (max_x - min_x) / (max_y - min_y) middlePoint2 = [ (max_x + min_x) / 2, (max_y + min_y) / 2] #print("ratio2",ratio2) #变形原始人物的landmarks,边界框 landmarks1=np.array(landmarks1) min_x = np.min(landmarks1[:, 0]) max_x = np.max(landmarks1[:, 0]) min_y = np.min(landmarks1[:, 1]) max_y = np.max(landmarks1[:, 1]) #得到原始人物的边界框的长宽比以及中心点 ratio1 = (max_x - min_x) / (max_y - min_y) middlePoint = [ (max_x + min_x) / 2, (max_y + min_y) / 2] #print("ratio1",ratio1) if AlignType=="Width": #保持人物脸部边界框中心点不变,垂直方向上缩放,使边界框的比例变得跟目标人物的边界框比例一致 if(landmarkType=="ALL"): dst_points = np.append(dst_points,landmarks1,axis=0) target_points = landmarks1.copy() else: dst_points = np.append(dst_points,jaw1,axis=0) jaw1=np.array(jaw1) target_points = jaw1.copy() target_points[:, 1] = (target_points[:, 1] - middlePoint[1]) * ratio1 / ratio2 + middlePoint[1] src_points = np.append(src_points,target_points,axis=0)#不知道原作者为何把这个数组叫src,其实这是变形后的坐标 elif AlignType=="Height": #保持人物脸部边界框中心点不变,水平方向上缩放,使边界框的比例变得跟目标人物的边界框比例一致 if(landmarkType=="ALL"): dst_points = np.append(dst_points,landmarks1,axis=0) #不知道原作者为何把这个数组叫dst,其实这是变形前的坐标,即原图的坐标 target_points = landmarks1.copy() else: dst_points = np.append(dst_points,jaw1,axis=0)#不知道原作者为何把这个数组叫dst,其实这是变形前的坐标,即原图的坐标 jaw1=np.array(jaw1) target_points = jaw1.copy() target_points[:, 0] = (target_points[:, 0] - middlePoint[0]) * ratio2 / ratio1 + middlePoint[0] src_points = np.append(src_points,target_points,axis=0)#不知道原作者为何把这个数组叫src,其实这是变形后的坐标 elif AlignType=="Landmarks": if(landmarkType=="ALL"): #以双眼中心为基准点,按双眼距离计算缩放系数。效果是变形前后眼睛位置不变 MiddleOfEyes1 = (leftEye1+rightEye1)/2 MiddleOfEyes2 = (leftEye2+rightEye2)/2 distance1 = ((leftEye1[0] - rightEye1[0]) ** 2 + (leftEye1[1] - rightEye1[1]) ** 2) ** 0.5 distance2 = ((leftEye2[0] - rightEye2[0]) ** 2 + (leftEye2[1] - rightEye2[1]) ** 2) ** 0.5 factor = distance1 / distance2 MiddleOfEyes2 = np.array(MiddleOfEyes2) target_points = (landmarks2 - MiddleOfEyes2) * factor + MiddleOfEyes1 #面部轮廓线则以轮廓线中心点为基准点,缩放系数还是从双眼距离计算 centerOfJaw2 = np.array(centerOfJaw2) jawLineTarget = (landmarks2[108:144] - centerOfJaw2) * factor + centerOfJaw1 target_points[108:144] = jawLineTarget dst_points = np.append(dst_points,landmarks1,axis=0)#不知道原作者为何把这个数组叫dst,其实这是变形前的坐标,即原图的坐标 else: #此时只有轮廓线landMark。对齐两个landMark的中心点,然后用2替换掉1 dst_points = np.append(dst_points,jaw1,axis=0)#不知道原作者为何把这个数组叫dst,其实这是变形前的坐标,即原图的坐标 target_points=(jaw2-centerOfJaw2)+centerOfJaw1 src_points = np.append(src_points,target_points,axis=0)#不知道原作者为何把这个数组叫src,其实这是变形后的坐标 elif AlignType=="JawLine": lenOfJaw=len(jaw1) distance1= ((jaw1[0][0] - jaw1[lenOfJaw-1][0]) ** 2 + (jaw1[0][1] - jaw1[lenOfJaw-1][1]) ** 2) ** 0.5 distance2= ((jaw2[0][0] - jaw2[lenOfJaw-1][0]) ** 2 + (jaw2[0][1] - jaw2[lenOfJaw-1][1]) ** 2) ** 0.5 factor = distance1 / distance2 if landmarkType == "ALL": dst_points = np.append(dst_points,landmarks1,axis=0) target_points=(landmarks2-jaw2[0])*factor+jaw1[0] src_points = np.append(src_points,target_points,axis=0) else: dst_points = np.append(dst_points,jaw1,axis=0) target_points=(jaw2-jaw2[0])*factor+jaw1[0] src_points = np.append(src_points,target_points,axis=0) mark_img = draw_pointsOnImg(image1, dst_points, color=(255, 255, 0),radius=4) mark_img = draw_pointsOnImg(mark_img, src_points, color=(255, 0, 0),radius=3) mark_img = drawLineBetweenPoints(mark_img, dst_points,src_points) #### 开始对图片进行液化变形 #Tried many times, finally find out these array should be exchange w,h before go into RBFInterpolator src_points[:, [0, 1]] = src_points[:, [1, 0]] dst_points[:, [0, 1]] = dst_points[:, [1, 0]] rbfy = RBFInterpolator(src_points,dst_points[:,1],kernel="thin_plate_spline") rbfx = RBFInterpolator(src_points,dst_points[:,0],kernel="thin_plate_spline") # Create a meshgrid to interpolate over the entire image img_grid = np.mgrid[0:height, 0:width] # flatten grid so it could be feed into interpolation flatten=img_grid.reshape(2, -1).T # Interpolate the displacement using the RBF interpolators map_y = rbfy(flatten).reshape(height,width).astype(np.float32) map_x = rbfx(flatten).reshape(height,width).astype(np.float32) # Apply the remapping to the image using OpenCV warped_image = cv2.remap(image1, map_y, map_x, cv2.INTER_LINEAR) ######### 液化变形结束 warped_image = torch.from_numpy(warped_image.astype(np.float32) / 255.0).unsqueeze(0) mark_img = torch.from_numpy(mark_img.astype(np.float32) / 255.0).unsqueeze(0) output.append(warped_image) output.append(mark_img) return (output) class FaceShaperFaceMask: @classmethod def INPUT_TYPES(s): return { "required": { "crop_info": ("CROPINFO", {"default": []}), #"OnSource": ("BOOLEAN", {"default": False}), "MaskSize": (["Source","Crop"], ), #"image": ("IMAGE", ), #"area": (["face", "main_features", "eyes", "left_eye", "right_eye", "nose", "mouth", "face+forehead (if available)"], ), "grow": ("INT", { "default": 0, "min": -4096, "max": 4096, "step": 1 }), "grow_tapered": ("BOOLEAN", { "default": False }), "blur": ("INT", { "default": 13, "min": 1, "max": 4096, "step": 2 }), } } RETURN_TYPES = ("MASK",) RETURN_NAMES = ("mask",) FUNCTION = "segment" CATEGORY = "FaceShaper" def segment(self, crop_info, MaskSize,grow, grow_tapered, blur): # if (OnSource): if MaskSize=="Source": lmk = crop_info["crop_info_list"][0]['lmk_source'] height, width = crop_info["crop_info_list"][0]['input_image_size'] else: lmk = crop_info["crop_info_list"][0]['lmk_crop'] height = width = crop_info["crop_info_list"][0]['dsize'] #### ['lmk_source'] 标准化的203个关键点,在sourceImage上的位置 #### ['lmk_crop'] 标准化的203个关键点,在cropped Image上的位置 #### ['input_image_size'] source image size(二维) #### ['dsize'] cropped image size (就一个数,crop后的图是正方形的) #out_mask = [] mask = mask_from_landmarks(height,width, lmk) mask = image_to_tensor(mask).unsqueeze(0).squeeze(-1).clamp(0, 1) _, y, x = torch.where(mask) x1, x2 = x.min().item(), x.max().item() y1, y2 = y.min().item(), y.max().item() smooth = int(min(max((x2 - x1), (y2 - y1)) * 0.2, 99)) if smooth > 1: if smooth % 2 == 0: smooth+= 1 mask = T.functional.gaussian_blur(mask.bool().unsqueeze(1), smooth).squeeze(1).float() if grow != 0: mask = expand_mask(mask, grow, grow_tapered) if blur > 1: if blur % 2 == 0: blur+= 1 mask = T.functional.gaussian_blur(mask.unsqueeze(1), blur).squeeze(1).float() #print("A -- mask.shape",mask.shape) # A -- mask.shape torch.Size([1, 512, 512]) return (mask,) ##下面的留着以后改造成同时处理多个crop_info mask = mask.squeeze(0).unsqueeze(-1) #print("B -- mask.shape",mask.shape) # B -- mask.shape torch.Size([512, 512, 1]) out_mask.append(mask) out_mask = torch.stack(out_mask).squeeze(-1) print("out_mask.shape", out_mask.shape) #out_mask.shape torch.Size([1, 512, 512]) print("A -- mask.shape",mask.shape) return (out_mask,) # A dictionary that contains all nodes you want to export with their names # NOTE: names should be globally unique NODE_CLASS_MAPPINGS = { "FaceShaper": FaceShaper, "FaceShaperModels": FaceShaperModels, "FaceAlignmentCropper": FaceAlignmentCropper, "FaceShaperCropper": FaceShaperCropper, "FaceShaperShowLandMarks": FaceShaperShowLandMarks, "FaceShaperComposite":FaceShaperComposite, "FaceShaperLoadInsightFaceCropper":FaceShaperLoadInsightFaceCropper, "FaceShaperLoadMediaPipeCropper":FaceShaperLoadMediaPipeCropper, "FaceShaperMatchV2":FaceShaperMatchV2, "FaceShaperFaceMask":FaceShaperFaceMask, } # A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS = { "FaceShaper": "FaceShape Match(legacy)", "FaceShaperModels":" faceShaper LoadModel DLib(legacy)", "FaceAlignmentCropper": "FaceShaper Load FaceAlignment", "FaceShaperCropper": "FaceShaper Cropper", "FaceShaperShowLandMarks": "FaceShaper Showlandmarks", "FaceShaperComposite": "FaceShaper Composite", "FaceShaperLoadInsightFaceCropper": "FaceShaper Load InsightFace", "FaceShaperLoadMediaPipeCropper": "FaceShaper Load MediaPipe", "FaceShaperV2": "FaceShape Match V2", "FaceShaperFaceMask":"FaceShaper Face Mask" }