Files
2025-03-21 10:47:06 +08:00

486 lines
22 KiB
Python

import numpy as np
import torch
from PIL import Image
import cv2
import math
import os
import logging
import time
from typing import Tuple, Optional, Dict, Any, List
import mediapipe as mp
try:
import dlib
has_dlib = True
except ImportError:
has_dlib = False
print("建议安装dlib库作为备用检测器: pip install dlib")
try:
from face_alignment import FaceAlignment, LandmarksType
has_face_alignment = True
except ImportError:
has_face_alignment = False
print("请安装face_alignment库: pip install face-alignment")
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("FaceAlignSingle")
class PerformanceTimer:
"""简单的性能计时器类"""
def __init__(self, name):
self.name = name
self.start_time = None
def __enter__(self):
self.start_time = time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.time() - self.start_time
logger.info(f"[性能] {self.name}: {elapsed:.4f}秒")
class FaceAlignSingle:
"""Face alignment and pasting node for ComfyUI (Single Face Version)"""
def __init__(self):
with PerformanceTimer("初始化人脸检测模型"):
# 使用MediaPipe Face Mesh作为主要检测器
self.mp_face_mesh = mp.solutions.face_mesh
self.face_mesh = self.mp_face_mesh.FaceMesh(
static_image_mode=True,
max_num_faces=1, # 只检测一个人脸
refine_landmarks=True, # 使用精细特征点以提高准确性
min_detection_confidence=0.5
)
# 初始化dlib检测器作为备用
if has_dlib:
# 获取模型文件路径 - 修改为使用models子目录
current_dir = os.path.dirname(os.path.abspath(__file__))
models_dir = os.path.join(current_dir, "models")
# 如果models目录不存在,创建它
if not os.path.exists(models_dir):
os.makedirs(models_dir)
logger.info(f"创建models目录: {models_dir}")
shape_predictor_path = os.path.join(models_dir, "shape_predictor_68_face_landmarks.dat")
# 检查模型文件是否存在
if not os.path.exists(shape_predictor_path):
logger.warning(f"dlib模型文件不存在: {shape_predictor_path}")
logger.warning("请从http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2下载并解压到models目录")
self.dlib_detector = None
self.dlib_predictor = None
else:
logger.info(f"加载dlib模型: {shape_predictor_path}")
self.dlib_detector = dlib.get_frontal_face_detector()
self.dlib_predictor = dlib.shape_predictor(shape_predictor_path)
logger.info("dlib模型加载成功")
else:
self.dlib_detector = None
self.dlib_predictor = None
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 缓存机制
self.landmarks_cache = {}
self.last_processed_images = {}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"target": ("IMAGE",),
"source": ("IMAGE",),
"mask": ("MASK",),
},
"optional": {
"feather": ("INT", {
"default": 0,
"min": 0,
"max": 100,
"step": 1
}),
"size": ("FLOAT", {
"default": 1.0,
"min": 0.5,
"max": 2.0,
"step": 0.05
}),
"move_x": ("INT", {
"default": 0,
"min": -20,
"max": 20,
"step": 1
}),
"move_y": ("INT", {
"default": 0,
"min": -20,
"max": 20,
"step": 1
}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "MASK")
RETURN_NAMES = ("image", "mask", "raw_mask")
FUNCTION = "align_and_paste_face"
CATEGORY = "image/face"
def get_face_landmarks(self, image: np.ndarray) -> Optional[List[np.ndarray]]:
"""使用MediaPipe和dlib获取人脸特征点"""
# 创建图像哈希作为缓存键
h, w = image.shape[:2]
# 只使用中心区域计算哈希,提高速度
center_crop = image[h//2-50:h//2+50, w//2-50:w//2+50, :] if h > 100 and w > 100 else image
downsampled = center_crop[::8, ::8, :]
image_hash = hash(downsampled.tobytes())
# 检查缓存
if image_hash in self.landmarks_cache:
logger.info("使用缓存的人脸特征点")
return self.landmarks_cache[image_hash]
with PerformanceTimer("人脸特征点检测"):
# 准备RGB图像用于MediaPipe
if image.shape[2] == 3:
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
else:
rgb_image = image
# 1. 首先使用MediaPipe检测
mediapipe_landmarks = None
results = self.face_mesh.process(rgb_image)
if results.multi_face_landmarks:
# 转换MediaPipe特征点格式
mediapipe_landmarks = []
for face_landmarks in results.multi_face_landmarks:
face_points = []
for landmark in face_landmarks.landmark:
# 转换为图像坐标
x = landmark.x * image.shape[1]
y = landmark.y * image.shape[0]
face_points.append([x, y])
mediapipe_landmarks.append(np.array(face_points))
logger.info("MediaPipe成功检测到人脸")
else:
logger.warning("MediaPipe未检测到人脸,尝试使用dlib")
# 2. 如果MediaPipe失败或者我们有dlib,也用dlib检测
dlib_landmarks = None
if self.dlib_detector is not None and (mediapipe_landmarks is None or len(mediapipe_landmarks) == 0):
# 转换为灰度图像用于dlib
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 检测人脸
dlib_faces = self.dlib_detector(gray)
if len(dlib_faces) > 0:
# 获取第一个人脸的特征点
dlib_landmarks = []
for face in dlib_faces:
shape = self.dlib_predictor(gray, face)
points = []
for i in range(68): # dlib有68个特征点
x = shape.part(i).x
y = shape.part(i).y
points.append([x, y])
dlib_landmarks.append(np.array(points))
logger.info(f"dlib成功检测到{len(dlib_landmarks)}个人脸")
# 3. 决定使用哪个检测结果
final_landmarks = None
if mediapipe_landmarks and len(mediapipe_landmarks) > 0:
if dlib_landmarks and len(dlib_landmarks) > 0:
# 两者都检测到了,检查一致性
# 比较MediaPipe和dlib的眼睛中心位置
mp_left_eye = np.mean(mediapipe_landmarks[0][[33, 133, 157, 158, 159, 160, 161, 173, 246]], axis=0)
mp_right_eye = np.mean(mediapipe_landmarks[0][[263, 362, 384, 385, 386, 387, 388, 398, 466]], axis=0)
mp_eye_center = (mp_left_eye + mp_right_eye) / 2
# dlib的眼睛特征点索引 (36-41是左眼,42-47是右眼)
dlib_left_eye = np.mean(dlib_landmarks[0][36:42], axis=0)
dlib_right_eye = np.mean(dlib_landmarks[0][42:48], axis=0)
dlib_eye_center = (dlib_left_eye + dlib_right_eye) / 2
# 计算两个眼睛中心的距离
eye_center_distance = np.linalg.norm(mp_eye_center - dlib_eye_center)
# 如果距离小于阈值,使用MediaPipe结果
if eye_center_distance < 30: # 30像素阈值可调整
logger.info(f"MediaPipe和dlib检测结果一致(距离:{eye_center_distance:.2f}像素),使用MediaPipe结果")
final_landmarks = mediapipe_landmarks
else:
logger.warning(f"MediaPipe和dlib检测结果不一致(距离:{eye_center_distance:.2f}像素),使用dlib结果")
final_landmarks = self._convert_dlib_to_mediapipe_format(dlib_landmarks, image.shape)
else:
# 只有MediaPipe检测到了
logger.info("只有MediaPipe检测到人脸,使用MediaPipe结果")
final_landmarks = mediapipe_landmarks
elif dlib_landmarks and len(dlib_landmarks) > 0:
# 只有dlib检测到了
logger.info("只有dlib检测到人脸,使用dlib结果")
final_landmarks = self._convert_dlib_to_mediapipe_format(dlib_landmarks, image.shape)
else:
# 两者都没检测到
logger.warning("MediaPipe和dlib均未检测到人脸")
return None
# 存入缓存
self.landmarks_cache[image_hash] = final_landmarks
# 限制缓存大小
if len(self.landmarks_cache) > 50:
self.landmarks_cache.pop(next(iter(self.landmarks_cache)))
return final_landmarks
def _convert_dlib_to_mediapipe_format(self, dlib_landmarks: List[np.ndarray], image_shape: tuple) -> List[np.ndarray]:
"""将dlib的68点特征转换为与MediaPipe兼容的格式"""
# 这是一个简化的转换,实际上MediaPipe有468个特征点
# 我们只关心眼睛和鼻子的位置,所以只需要确保这些关键点被正确映射
result = []
for dlib_points in dlib_landmarks:
# 创建一个与MediaPipe特征点数量相同的数组
mp_points = np.zeros((468, 2), dtype=np.float32)
# 映射关键点 (这些映射是近似的)
# 左眼
mp_points[33] = dlib_points[36] # 左眼外角
mp_points[133] = dlib_points[39] # 左眼内角
mp_points[157] = dlib_points[37]
mp_points[158] = dlib_points[38]
mp_points[159] = dlib_points[39]
mp_points[160] = dlib_points[40]
mp_points[161] = dlib_points[41]
mp_points[173] = dlib_points[38]
mp_points[246] = dlib_points[37]
# 右眼
mp_points[263] = dlib_points[45] # 右眼外角
mp_points[362] = dlib_points[42] # 右眼内角
mp_points[384] = dlib_points[43]
mp_points[385] = dlib_points[44]
mp_points[386] = dlib_points[45]
mp_points[387] = dlib_points[46]
mp_points[388] = dlib_points[47]
mp_points[398] = dlib_points[44]
mp_points[466] = dlib_points[43]
# 鼻尖
mp_points[1] = dlib_points[30]
# 下巴
mp_points[152] = dlib_points[8]
result.append(mp_points)
return result
def process_mask(self, mask: torch.Tensor, feather: int = 0) -> np.ndarray:
"""处理mask,添加羽化效果"""
with PerformanceTimer("处理遮罩"):
if torch.is_tensor(mask):
mask = mask.cpu().numpy().squeeze()
mask = (mask * 255).astype(np.uint8)
if feather > 0:
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (feather, feather))
mask = cv2.dilate(mask, kernel)
mask = cv2.GaussianBlur(mask, (feather//2*2+1, feather//2*2+1), 0)
return mask
def get_face_info(self, landmarks: np.ndarray) -> Dict:
"""获取单个人脸的信息"""
with PerformanceTimer("计算面部信息"):
# 使用更多的眼睛周围特征点来提高定位精度
# 左眼特征点(MediaPipe的眼睛轮廓点)
left_eye_points = landmarks[[33, 133, 157, 158, 159, 160, 161, 173, 246]]
# 右眼特征点
right_eye_points = landmarks[[263, 362, 384, 385, 386, 387, 388, 398, 466]]
# 计算眼睛中心(使用多个点的平均值)
left_eye = np.mean(left_eye_points, axis=0)
right_eye = np.mean(right_eye_points, axis=0)
# 鼻尖和下巴
nose_tip = landmarks[1] # 鼻尖
chin = landmarks[152] # 下巴
eye_center = (left_eye + right_eye) / 2
eye_vector = right_eye - left_eye
face_angle = np.degrees(np.arctan2(eye_vector[1], eye_vector[0]))
# 记录关键点位置用于调试
logger.info(f"左眼位置(多点平均): {left_eye}, 右眼位置(多点平均): {right_eye}")
logger.info(f"眼睛中心: {eye_center}, 面部角度: {face_angle}°")
return {
'eye_center': eye_center,
'left_eye': left_eye,
'right_eye': right_eye,
'nose_tip': nose_tip,
'chin': chin,
'face_angle': face_angle,
'eye_distance': np.linalg.norm(eye_vector)
}
def align_and_paste_face(self, target: torch.Tensor,
source: torch.Tensor,
mask: torch.Tensor,
feather: int = 0,
size: float = 1.0,
move_x: int = 0,
move_y: int = 0) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""处理单个人脸的对齐和替换"""
try:
with PerformanceTimer("总处理时间"):
# 优化哈希计算
target_np = target[0].cpu().numpy()
source_np = source[0].cpu().numpy()
mask_np = mask.cpu().numpy()
# 计算输入哈希
th, tw = target_np.shape[:2]
sh, sw = source_np.shape[:2]
# 使用低分辨率计算哈希
t_down = target_np[::16, ::16, :]
s_down = source_np[::16, ::16, :]
m_down = mask_np[0, ::16, ::16]
input_hash = hash((
hash(t_down.tobytes()),
hash(s_down.tobytes()),
hash(m_down.tobytes()),
feather, size, move_x, move_y
))
# 检查缓存
if input_hash in self.last_processed_images:
logger.info("使用缓存的处理结果")
return self.last_processed_images[input_hash]
# 数据准备
with PerformanceTimer("数据准备"):
# 转换为8位整数格式
source_np = (source_np * 255).astype(np.uint8)
target_np = (target_np * 255).astype(np.uint8)
mask_np = self.process_mask(mask, feather)
# 获取特征点
with PerformanceTimer("特征点提取"):
src_landmarks_list = self.get_face_landmarks(source_np)
target_landmarks_list = self.get_face_landmarks(target_np)
if not src_landmarks_list:
logger.error("源图像未检测到人脸,请确保源图像中有清晰的人脸")
empty_mask = torch.zeros((1, target.shape[2], target.shape[3]), dtype=torch.float32)
return (target.cpu(), empty_mask, empty_mask)
if not target_landmarks_list:
logger.error("目标图像未检测到人脸,请确保目标图像中有清晰的人脸")
empty_mask = torch.zeros((1, target.shape[2], target.shape[3]), dtype=torch.float32)
return (target.cpu(), empty_mask, empty_mask)
src_landmarks = src_landmarks_list[0]
target_landmarks = target_landmarks_list[0]
# 计算面部信息
src_info = self.get_face_info(src_landmarks)
tgt_info = self.get_face_info(target_landmarks)
# 计算变换参数
with PerformanceTimer("图像变换"):
# 只使用眼睛中心进行对齐,不进行扭曲变换
# 计算源图像和目标图像的眼睛中心
src_eye_center = src_info['eye_center']
tgt_eye_center = tgt_info['eye_center']
# 计算缩放比例 - 基于眼睛距离
scale = tgt_info['eye_distance'] / src_info['eye_distance'] * size
# 计算旋转角度 - 基于眼睛连线的角度差
angle_diff = tgt_info['face_angle'] - src_info['face_angle']
logger.info(f"变换参数 - 角度差: {angle_diff:.2f}°, 缩放比例: {scale:.2f}")
# 创建仿射变换矩阵 - 先旋转和缩放
matrix = cv2.getRotationMatrix2D(
tuple(src_eye_center), # 以源图像眼睛中心为旋转中心
-angle_diff, # 旋转角度
scale # 缩放比例
)
# 添加平移分量 - 使眼睛中心对齐
matrix[0, 2] += tgt_eye_center[0] - src_eye_center[0] # X轴平移
matrix[1, 2] += tgt_eye_center[1] - src_eye_center[1] # Y轴平移
# 应用额外的移动
if move_x != 0 or move_y != 0:
matrix[0, 2] += move_x * 4 # X轴额外平移
matrix[1, 2] -= move_y * 4 # Y轴额外平移
# 执行变换
img_transformed = cv2.warpAffine(
source_np, matrix, (target_np.shape[1], target_np.shape[0]),
flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_REPLICATE
)
mask_transformed = cv2.warpAffine(
mask_np, matrix, (target_np.shape[1], target_np.shape[0]),
flags=cv2.INTER_LINEAR
)
# 保存原始mask(未羽化)
raw_mask_transformed = mask_transformed.copy()
# 创建混合mask
with PerformanceTimer("图像混合"):
if feather > 0:
mask_blur = cv2.GaussianBlur(mask_transformed, (feather*2+1, feather*2+1), 0)
else:
mask_blur = mask_transformed
# 检查mask是否有效
if np.max(mask_blur) < 10: # 如果mask几乎全黑
logger.warning("变换后的mask几乎为空,可能是人脸定位不准确导致")
# 使用向量化操作
alpha = mask_blur[..., None].astype(np.float32) / 255.0
# 合成图像
result = target_np.astype(np.float32) * (1 - alpha) + img_transformed.astype(np.float32) * alpha
result = result.astype(np.uint8)
# 转换结果
with PerformanceTimer("结果转换"):
result_tensor = torch.from_numpy(result.astype(np.float32) / 255.0).unsqueeze(0)
mask_tensor = torch.from_numpy(mask_blur.astype(np.float32) / 255.0).unsqueeze(0)
raw_mask_tensor = torch.from_numpy(raw_mask_transformed.astype(np.float32) / 255.0).unsqueeze(0)
# 缓存处理结果
output = (result_tensor, mask_tensor, raw_mask_tensor)
self.last_processed_images[input_hash] = output
# 限制缓存大小
if len(self.last_processed_images) > 30:
self.last_processed_images.pop(next(iter(self.last_processed_images)))
return output
except Exception as e:
logger.error(f"人脸处理失败: {str(e)}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
empty_mask = torch.zeros((1, target.shape[2], target.shape[3]), dtype=torch.float32)
return (target.cpu(), empty_mask, empty_mask)
NODE_CLASS_MAPPINGS = {"FaceAlignSingle": FaceAlignSingle}
NODE_DISPLAY_NAME_MAPPINGS = {"FaceAlignSingle": "Face Align Single @ CHAOS"}