Files
2025-04-21 20:11:32 +08:00

462 lines
19 KiB
Python

import os
import sys
import torch
import numpy as np
from diffusers.models.attention_processor import XFormersAttnProcessor
from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection
from diffusers import AutoencoderKLTemporalDecoder, EulerDiscreteScheduler
import folder_paths
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(ROOT_DIR)
sys.path.append(f"{ROOT_DIR}/StableAnimator")
from StableAnimator.animation.modules.attention_processor import AnimationAttnProcessor
from StableAnimator.animation.modules.attention_processor_normalized import AnimationIDAttnNormalizedProcessor
# from .StableAnimator.animation.modules.face_model import FaceModel
from .utils.face_model import FaceModel #这里重写StableAnimator.animation.modules.face_model.FaceModel 类,以适应ComfyUI的模型加载机制
from StableAnimator.animation.modules.id_encoder import FusionFaceId
from StableAnimator.animation.modules.pose_net import PoseNet
from StableAnimator.animation.modules.unet import UNetSpatioTemporalConditionModel
from StableAnimator.animation.pipelines.inference_pipeline_animation import InferenceAnimationPipeline
from .utils.image_utils import tensor_to_pil, tensor_to_np, np_to_tensor, load_images_from_folder, Resize
# from .StableAnimator.DWPose.dwpose_utils.dwpose_detector import dwpose_detector_aligned
from .utils.dwpose.dwpose_detector import DWposeDetectorAligned # 这里重写StableAnimator.DWPose.dwpose_utils.dwpose_detector.dwpose_detector_aligned 函数,以适应ComfyUI的模型加载机制
from .utils.dwpose.skeleton_extraction import draw_pose
GLOBAL_CATEGORY = "HJH_StableAnimatorNode🪅"
class StableAnimatorModels:
@classmethod
def INPUT_TYPES(cls):
"""定义输入参数"""
return {
"required": {
},
}
RETURN_TYPES = ("STABLEANIMATORMODELS",)
RETURN_NAMES = ("stable_animator_models",)
FUNCTION = "load_models"
CATEGORY = GLOBAL_CATEGORY
# OUTPUT_IS_LIST = (True,)
def __init__(self):
self.models_loaded = False
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.dtype = torch.bfloat16 if torch.cuda.get_device_capability()[0] >= 8 else torch.float16
# 模型路径配置
self.root_dir = folder_paths.get_folder_paths("stable_animator")[0]
# os.path.join(, "stable_animator")
self.model_config = {
"pretrained_model": "stable-video-diffusion-img2vid-xt",
"pose_net": "Animation/pose_net.pth",
"face_encoder": "Animation/face_encoder.pth",
"unet": "Animation/unet.pth"
}
def load_models(self):
# 初始化模型组件
self.feature_extractor = CLIPImageProcessor.from_pretrained(
os.path.join(self.root_dir, self.model_config["pretrained_model"]),
subfolder="feature_extractor"
)
self.noise_scheduler = EulerDiscreteScheduler.from_pretrained(
os.path.join(self.root_dir, self.model_config["pretrained_model"]),
subfolder="scheduler"
)
self.image_encoder = CLIPVisionModelWithProjection.from_pretrained(
os.path.join(self.root_dir, self.model_config["pretrained_model"]),
subfolder="image_encoder"
)
self.vae = AutoencoderKLTemporalDecoder.from_pretrained(
os.path.join(self.root_dir, self.model_config["pretrained_model"]),
subfolder="vae"
)
self.unet = UNetSpatioTemporalConditionModel.from_pretrained(
os.path.join(self.root_dir, self.model_config["pretrained_model"]),
subfolder="unet",
low_cpu_mem_usage=True
)
# 加载自定义组件
self.pose_net = PoseNet(noise_latent_channels=self.unet.config.block_out_channels[0])
self.face_encoder = FusionFaceId(
cross_attention_dim=1024,
id_embeddings_dim=512,
clip_embeddings_dim=1024,
num_tokens=4
)
# 加载预训练权重
self._load_weights(
self.pose_net,
os.path.join(self.root_dir, self.model_config["pose_net"])
)
self._load_weights(
self.face_encoder,
os.path.join(self.root_dir, self.model_config["face_encoder"])
)
self._load_weights(
self.unet,
os.path.join(self.root_dir, self.model_config["unet"])
)
# 配置注意力处理器
self._configure_attention_processors()
# 冻结模型参数
for model in [self.vae, self.image_encoder, self.unet, self.pose_net, self.face_encoder]:
model.requires_grad_(False)
model.to(device=self.device, dtype=self.dtype)
self.face_model = FaceModel(self.root_dir)
return self,
def _configure_attention_processors(self):
"""配置注意力处理器(保持原始实现逻辑)"""
lora_rank = 128
attn_procs = {}
unet = self.unet
unet_svd = unet.state_dict()
for name in unet.attn_processors.keys():
if "transformer_blocks" in name and "temporal_transformer_blocks" not in name:
cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim
if name.startswith("mid_block"):
hidden_size = unet.config.block_out_channels[-1]
elif name.startswith("up_blocks"):
block_id = int(name[len("up_blocks.")])
hidden_size = list(reversed(unet.config.block_out_channels))[block_id]
elif name.startswith("down_blocks"):
block_id = int(name[len("down_blocks.")])
hidden_size = unet.config.block_out_channels[block_id]
if cross_attention_dim is None:
# print(f"This is AnimationAttnProcessor: {name}")
attn_procs[name] = AnimationAttnProcessor(hidden_size=hidden_size, cross_attention_dim=cross_attention_dim, rank=lora_rank)
else:
# print(f"This is AnimationIDAttnProcessor: {name}")
layer_name = name.split(".processor")[0]
weights = {
"to_k_ip.weight": unet_svd[layer_name + ".to_k.weight"],
"to_v_ip.weight": unet_svd[layer_name + ".to_v.weight"],
}
attn_procs[name] = AnimationIDAttnNormalizedProcessor(hidden_size=hidden_size, cross_attention_dim=cross_attention_dim, rank=lora_rank)
attn_procs[name].load_state_dict(weights, strict=False)
elif "temporal_transformer_blocks" in name:
cross_attention_dim = None if name.endswith("attn1.processor") else unet.config.cross_attention_dim
if name.startswith("mid_block"):
hidden_size = unet.config.block_out_channels[-1]
elif name.startswith("up_blocks"):
block_id = int(name[len("up_blocks.")])
hidden_size = list(reversed(unet.config.block_out_channels))[block_id]
elif name.startswith("down_blocks"):
block_id = int(name[len("down_blocks.")])
hidden_size = unet.config.block_out_channels[block_id]
if cross_attention_dim is None:
attn_procs[name] = XFormersAttnProcessor()
else:
attn_procs[name] = XFormersAttnProcessor()
unet.set_attn_processor(attn_procs)
def _load_weights(self, model, path):
"""加载权重辅助函数"""
if path.endswith(".pth"):
state_dict = torch.load(path, map_location="cpu",weights_only=True)
model.load_state_dict(state_dict, strict=False)
model.to(device=self.device, dtype=self.dtype)
else:
raise ValueError(f"Invalid model weights: {path}")
class StableAnimatorNode:
"""
StableAnimator视频生成节点
"""
def __init__(self):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.dtype = torch.bfloat16 if torch.cuda.get_device_capability()[0] >= 8 else torch.float16
pass
@classmethod
def INPUT_TYPES(cls):
"""定义输入参数"""
return {
"required": {
"stable_animator_models":("STABLEANIMATORMODELS",),
"reference_image": ("IMAGE",),
# "pose_images_dir": ("STRING",{"default":"",}), # 输入为图像序列文件夹路径
"pose_images": ("IMAGE",),
"format":(["512x512","576x1024"],{}),
"tile_size":("INT",{"default":16,"min":16,"max":64,"step":16}),
"frames_overlap":("INT",{"default":4,"min":1,"max":64,"step":1}),
"decode_chunk_size":("INT",{"default":4,"min":1,"max":64,"step":1}),
"num_inference_steps":("INT",{"default":25,"min":1,"max":64,"step":1}),
"noise_aug_strength": ("FLOAT", {
"default": 0.02,
"min": 0.01,
"max": 1,
"step": 0.01
}),
"guidance_scale": ("FLOAT", {
"default": 3.0,
"min": 1.0,
"max": 10.0,
"step": 0.1
}),
"num_inference_steps": ("INT", {
"default": 25,
"min": 10,
"max": 50,
"step": 1
}),
"seed": ("INT", {
"default": -1,
"min": -1,
"max": 2**32-1
}),
},
"optional": {
"fps": ("INT", {
"default": 8,
"min": 1,
"max": 60
})
}
}
FUNCTION = "generate"
CATEGORY = GLOBAL_CATEGORY
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("video_frames",)
# OUTPUT_IS_LIST = (True,)
def generate(self, **kwargs):
"""执行视频生成"""
# 准备输入参数
width, height = (512,512) if kwargs["format"]=="512x512" else (576,1024)
models = kwargs["stable_animator_models"]
reference_image = tensor_to_pil(kwargs["reference_image"])
reference_image_np = tensor_to_np(kwargs["reference_image"])
# pose_images_dir = kwargs["pose_images_dir"]
# pose_images = load_images_from_folder(pose_images_dir, width=width, height=height)
tensor_pose_images = kwargs["pose_images"]
pose_images = []
for pose_image in tensor_pose_images:
_img = pose_image.unsqueeze(0)
_img = Resize.crop_center(_img, width, height)
pose_images.append(tensor_to_pil(_img))
seed = kwargs["seed"] if kwargs["seed"] != -1 else torch.randint(0, 2**32-1, (1,)).item()
generator = torch.Generator(device=self.device).manual_seed(seed)
num_frames = len(pose_images)
# 初始化pipeline
pipeline = InferenceAnimationPipeline(
vae=models.vae,
image_encoder=models.image_encoder,
unet=models.unet,
scheduler=models.noise_scheduler,
feature_extractor=models.feature_extractor,
pose_net=models.pose_net,
face_encoder=models.face_encoder,
).to(device=models.device, dtype=models.dtype)
face_model = models.face_model
face_model.face_helper.clean_all()
validation_image_face_info = models.face_model.app.get(reference_image_np)
if len(validation_image_face_info) > 0:
validation_image_face_info = sorted(validation_image_face_info, key=lambda x: (x['bbox'][2] - x['bbox'][0]) * (x['bbox'][3] - x['bbox'][1]))[-1]
validation_image_id_ante_embedding = validation_image_face_info['embedding']
else:
validation_image_id_ante_embedding = None
if validation_image_id_ante_embedding is None:
face_model.face_helper.read_image(reference_image_np)
face_model.face_helper.get_face_landmarks_5(only_center_face=True)
face_model.face_helper.align_warp_face()
if len(face_model.face_helper.cropped_faces) == 0:
validation_image_id_ante_embedding = np.zeros((512,))
else:
validation_image_align_face = face_model.face_helper.cropped_faces[0]
print('fail to detect face using insightface, extract embedding on align face')
validation_image_id_ante_embedding = face_model.handler_ante.get_feat(validation_image_align_face)
# 运行推理
video_frames = pipeline(
image=reference_image,
image_pose=pose_images,
height=height,
width=width,
num_frames=num_frames,
tile_size=kwargs["tile_size"],
tile_overlap=kwargs["frames_overlap"],
decode_chunk_size=kwargs["decode_chunk_size"],
motion_bucket_id=127.,
fps=kwargs["fps"],
min_guidance_scale=kwargs["guidance_scale"],
max_guidance_scale=kwargs["guidance_scale"],
noise_aug_strength=kwargs["noise_aug_strength"],
num_inference_steps=kwargs["num_inference_steps"],
generator=generator,
output_type="pil",
validation_image_id_ante_embedding=validation_image_id_ante_embedding,
).frames[0]
# 转换为张量输出
output_tensors = []
for frame in video_frames:
output_tensors.append(np_to_tensor(np.array(frame)))
return torch.cat(output_tensors, dim=0),
class StableAnimatorDWPoseDetectorAlignedModels:
"""
StableAnimator加载DWPose模型,
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
"""定义输入参数"""
return {
"required": {
},
}
FUNCTION = "load"
CATEGORY = GLOBAL_CATEGORY
RETURN_TYPES = ("DWPOSEDETECTORALIGNED",)
RETURN_NAMES = ("dwpose_detector_aligned",)
def load(self, ):
"""执行模型加载"""
return DWposeDetectorAligned(),
class StableAnimatorSkeletonNode:
"""
StableAnimator生成视频POSE骨架
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
"""定义输入参数"""
return {
"required": {
"dwpose_detector_aligned":("DWPOSEDETECTORALIGNED",),
"reference_image":("IMAGE",),
"video_frames":("IMAGE",),
},
}
FUNCTION = "extraction"
CATEGORY = GLOBAL_CATEGORY
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("pose_frames",)
def extraction(self, **kwargs):
'''
以下代码参考 StableAnimator.DWPose.dwpose_utils.dwpose_detector.get_video_pose, 以适应ComfyUI的参数输入
'''
dwpose_detector_aligned = kwargs["dwpose_detector_aligned"]
ref_image = tensor_to_np(kwargs["reference_image"])
height, width, _ = ref_image.shape
ref_pose = dwpose_detector_aligned(ref_image)
ref_keypoint_id = [0, 1, 2, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
ref_keypoint_id = [i for i in ref_keypoint_id \
if len(ref_pose['bodies']['subset']) > 0 and ref_pose['bodies']['subset'][0][i] >= .0]
ref_body = ref_pose['bodies']['candidate'][ref_keypoint_id]
video_frames = kwargs["video_frames"]
detected_poses = []
for video_frame in video_frames:
frame = tensor_to_np(video_frame.unsqueeze(0))
frame = Resize.crop_center(frame, width, height)
pose = dwpose_detector_aligned(frame)
detected_poses.append(pose)
detected_bodies = np.stack([p['bodies']['candidate'] for p in detected_poses if p['bodies']['candidate'].shape[0] == 18])[:,ref_keypoint_id]
ay, by = np.polyfit(detected_bodies[:, :, 1].flatten(), np.tile(ref_body[:, 1], len(detected_bodies)), 1)
fh = height
fw = width
ax = ay / (fh / fw / height * width)
bx = np.mean(np.tile(ref_body[:, 0], len(detected_bodies)) - detected_bodies[:, :, 0].flatten() * ax)
a = np.array([ax, ay])
b = np.array([bx, by])
output_pose = []
for detected_pose in detected_poses:
detected_pose['bodies']['candidate'] = detected_pose['bodies']['candidate'] * a + b
detected_pose['faces'] = detected_pose['faces'] * a + b
detected_pose['hands'] = detected_pose['hands'] * a + b
im = draw_pose(detected_pose, height, width)
output_pose.append(np_to_tensor(np.array(im)))
output = torch.cat(output_pose, dim=0)
return output,
class StableAnimatorLoadFramesFromFolderNode:
"""
StableAnimator从文件夹加载帧
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
"""定义输入参数"""
return {
"required": {
"pose_images_folder": ("STRING",{"default":"",}), # 输入为图像序列文件夹路径
},
}
FUNCTION = "load"
CATEGORY = GLOBAL_CATEGORY
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("frames",)
def load(self, **kwargs):
frames = []
folder = kwargs["pose_images_folder"]
frames = load_images_from_folder(folder,)
return frames,
NODE_CLASS_MAPPINGS = {
"StableAnimatorNode": StableAnimatorNode,
"StableAnimatorModels": StableAnimatorModels,
"StableAnimatorSkeletonNode": StableAnimatorSkeletonNode,
"StableAnimatorDWPoseDetectorAlignedModels": StableAnimatorDWPoseDetectorAlignedModels,
"StableAnimatorLoadFramesFromFolderNode": StableAnimatorLoadFramesFromFolderNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"StableAnimatorNode": "HJH-StableAnimator - Video Generation",
"StableAnimatorModels":"HJH-StableAnimator - Load Models",
"StableAnimatorSkeletonNode": "HJH-StableAnimator - Pose Extraction",
"StableAnimatorDWPoseDetectorAlignedModels": "HJH-StableAnimator - Load DWPose Models",
"StableAnimatorLoadFramesFromFolderNode": "HJH-StableAnimator - Load Pose Frames From Folder",
}