diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b931f2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.pyc +*.pyd +*.pth +*.pkl +*_fps15.mp4 +.idea/ +data/results +pretrained_models/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e66f6b0 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +

HelloMeme: Integrating Spatial Knitting Attentions to Embed High-Level and Fidelity-Rich Conditions in Diffusion Models

+ +
+ Shengkai Zhang, Nianhong Jiao, Tian Li, Chaojie Yang, Chenhui Xue*, Boya Niu*, Jun Gao +
+ +
+ HelloVision | HelloGroup Inc. +
+ +
+ * Intern +
+ +
+
+ + + +
+ +## Introduction + +This repository is the official implementation of the [`HelloMeme`](https://arxiv.org/pdf/2410.22901) ComfyUI interface, featuring both image and video generation functionalities. Example workflow files can be found in the `ComfyUI_HelloMeme/workflows` directory. Test images and videos are saved in the `ComfyUI_HelloMeme/examples` directory. Below are screenshots of the interfaces for image and video generation. + +### Image Generation Interface + +

+ image_generation_interface +

+ +### Video Generation Interface + +

+ video_generation_interface +

+ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e315d4e --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .meme import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/examples/jgz.mp4 b/examples/jgz.mp4 new file mode 100644 index 0000000..3b15ab8 Binary files /dev/null and b/examples/jgz.mp4 differ diff --git a/examples/majicmix2.jpg b/examples/majicmix2.jpg new file mode 100644 index 0000000..cd46460 Binary files /dev/null and b/examples/majicmix2.jpg differ diff --git a/examples/yao.jpg b/examples/yao.jpg new file mode 100644 index 0000000..491c852 Binary files /dev/null and b/examples/yao.jpg differ diff --git a/hellomeme/__init__.py b/hellomeme/__init__.py new file mode 100644 index 0000000..632a0f7 --- /dev/null +++ b/hellomeme/__init__.py @@ -0,0 +1,12 @@ +# coding: utf-8 + +""" +@File : __init__.py.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/28/2024 +@Desc : +""" + +from .piplines import HMVideoPipeline, HMImagePipeline +from .models import HMDenoising3D, HMDenoisingMotion, HMControlNet, HMReferenceAdapter \ No newline at end of file diff --git a/hellomeme/models/__init__.py b/hellomeme/models/__init__.py new file mode 100644 index 0000000..45e2de5 --- /dev/null +++ b/hellomeme/models/__init__.py @@ -0,0 +1,14 @@ +# coding: utf-8 + +""" +@File : __init__.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/14/2024 +@Desc : +""" + +from .hm_denoising_motion import HMDenoisingMotion +from .hm_control import HMControlNet +from .hm_blocks import HMReferenceAdapter +from .hm_denoising_3d import HMDenoising3D diff --git a/hellomeme/models/hm_blocks.py b/hellomeme/models/hm_blocks.py new file mode 100644 index 0000000..31e2175 --- /dev/null +++ b/hellomeme/models/hm_blocks.py @@ -0,0 +1,330 @@ +# coding: utf-8 + +""" +@File : models6/hm_blocks.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/14/2024 +@Desc : +""" + +import copy +import torch +import torch.nn as nn +import torch.utils.checkpoint +import torch.nn.functional as F +from typing import Optional, Tuple, Union + +from einops import rearrange + +from diffusers.models.modeling_utils import ModelMixin +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.models.attention import Attention, FeedForward +from torchvision.models.resnet import BasicBlock +from diffusers.models.embeddings import SinusoidalPositionalEmbedding +from diffusers.utils import is_transformers_available, logging +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel + +from diffusers.configuration_utils import FrozenDict +from diffusers.models.attention_processor import ( + AttnProcessor, + AttnProcessor2_0, +) + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class HMReferenceAdapter(ModelMixin, ConfigMixin): + @register_to_config + def __init__(self, + block_out_channels: Tuple[int] = (320, 640, 1280, 1280), + down_block_types: Tuple[str] = ( + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D", + ), + up_block_types: Tuple[str] = ( + "UpBlock2D", + "CrossAttnUpBlock2D", + "CrossAttnUpBlock2D", + "CrossAttnUpBlock2D"), + num_attention_heads: Optional[Union[int, Tuple[int]]] = 8, + ): + super().__init__() + + self.reference_modules_down = nn.ModuleList([]) + + if isinstance(num_attention_heads, int): + num_attention_heads = (num_attention_heads,) * len(down_block_types) + + for i, down_block_type in enumerate(down_block_types): + output_channel = block_out_channels[i] + + self.reference_modules_down.append( + SKReferenceAttention( + in_channels=output_channel, + num_attention_heads=num_attention_heads[i], + ) + ) + + self.reference_modules_mid = SKReferenceAttention( + in_channels=block_out_channels[-1], + num_attention_heads=num_attention_heads[-1], + ) + + self.reference_modules_up = nn.ModuleList([]) + + reversed_block_out_channels = list(reversed(block_out_channels)) + reversed_num_attention_heads = list(reversed(num_attention_heads)) + + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + + if i > 0: + self.reference_modules_up.append( + SKReferenceAttention( + in_channels=prev_output_channel, + num_attention_heads=reversed_num_attention_heads[i], + num_positional_embeddings=64 * 2 + ) + ) + +class SKReferenceAttention(nn.Module): + def __init__( + self, + in_channels: int, + num_attention_heads: int = 1, + norm_elementwise_affine: bool = True, + norm_eps: float = 1e-5, + num_positional_embeddings: int = 64*2, + ): + super().__init__() + self.pos_embed = SinusoidalPositionalEmbedding(in_channels, max_seq_length=num_positional_embeddings) + self.attn1 = Attention( + query_dim=in_channels, + heads=num_attention_heads, + dim_head=in_channels // num_attention_heads, + dropout=0.0, + bias=False, + cross_attention_dim=None, + upcast_attention=False, + out_bias=True, + ) + self.attn2 = Attention( + query_dim=in_channels, + heads=num_attention_heads, + dim_head=in_channels // num_attention_heads, + dropout=0.0, + bias=False, + cross_attention_dim=None, + upcast_attention=False, + out_bias=True, + ) + # self.ff = FeedForward( + # in_channels, + # dropout=0.0, + # activation_fn="geglu", + # final_dropout=False, + # inner_dim=in_channels*2, + # bias=True, + # ) + self.norm = nn.LayerNorm(in_channels, elementwise_affine=norm_elementwise_affine, eps=norm_eps) + self.proj = zero_module(nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1)) + + def forward(self, hidden_states, ref_stats, num_frames): + h, w = hidden_states.shape[-2:] + ref_stats = ref_stats.repeat_interleave(num_frames, dim=0) + cat_stats = torch.cat([hidden_states, ref_stats], dim=-1) + + cat_stats = rearrange(cat_stats.contiguous(), "b c h w -> (b h) w c") + res1 = self.attn1(self.norm(self.pos_embed(cat_stats))) + res1 = rearrange(res1[:, :w, :], "(b h) w c -> b c h w", h=h) + + cat_stats2 = torch.cat([res1, ref_stats], dim=-2) + cat_stats2 = rearrange(cat_stats2.contiguous(), "b c h w -> (b w) h c") + res2 = self.attn2(self.norm(self.pos_embed(cat_stats2))) + + res2 = rearrange(res2[:, :h, :], "(b w) h c -> b c h w", w=w) + # res2 = self.ff(res2) + + return hidden_states + self.proj(res2) + +class SKCrossAttention(nn.Module): + def __init__( + self, + channel_in, + channel_out, + heads: int=8, + cross_attention_dim: int = 320, + norm_elementwise_affine: bool = True, + norm_eps: float = 1e-5, + num_positional_embeddings: int = 64, + num_positional_embeddings_hidden: int = 64, + ): + super().__init__() + self.conv = BasicBlock( + inplanes=channel_in, + planes=channel_out, + stride=2, + downsample=nn.Sequential( + nn.Conv2d(channel_in, channel_out, kernel_size=1, stride=2, bias=False), + nn.InstanceNorm2d(channel_out), + nn.SiLU(), + ), + norm_layer=nn.InstanceNorm2d, + ) + + self.pos_embed = SinusoidalPositionalEmbedding(channel_out, max_seq_length=num_positional_embeddings) + self.pos_embed_hidden = SinusoidalPositionalEmbedding(cross_attention_dim, max_seq_length=num_positional_embeddings_hidden) + + self.norm1 = nn.LayerNorm(channel_out, elementwise_affine=norm_elementwise_affine, eps=norm_eps) + self.attn1 = Attention( + query_dim=channel_out, + heads=heads, + dim_head=channel_out // heads, + dropout=0.0, + bias=False, + cross_attention_dim=cross_attention_dim, + upcast_attention=False, + out_bias=True, + ) + + self.norm2 = nn.LayerNorm(channel_out, elementwise_affine=norm_elementwise_affine, eps=norm_eps) + self.attn2 = Attention( + query_dim=channel_out, + heads=heads, + dim_head=channel_out // heads, + dropout=0.0, + bias=False, + cross_attention_dim=cross_attention_dim, + upcast_attention=False, + out_bias=True, + ) + + self.ff = FeedForward( + channel_out, + dropout=0.0, + activation_fn="geglu", + final_dropout=False, + inner_dim=channel_out*2, + bias=True, + ) + + self.proj = zero_module(nn.Conv2d(channel_out, channel_out, kernel_size=3, padding=1)) + + def forward(self, input, hidden_stats): + x = self.conv(input) + h, w = x.shape[-2:] + x = rearrange(x, "b c h w -> (b h) w c") + x = self.attn1(self.norm1(self.pos_embed(x)), self.pos_embed_hidden(hidden_stats.repeat_interleave(h, dim=0).contiguous())) + x = rearrange(x, "(b h) w c -> (b w) h c", h=h) + x = self.ff(self.attn2(self.norm2(self.pos_embed(x)), self.pos_embed_hidden(hidden_stats.repeat_interleave(w, dim=0).contiguous()))) + x = rearrange(x, "(b w) h c -> b c h w", w=w) + x = self.proj(x) + return x + +# https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/controlnet.py +def zero_module(module): + for p in module.parameters(): + nn.init.zeros_(p) + return module + +# https://github.com/huggingface/diffusers/blob/82058a5413ca09561cc5cc236c4abc5eeda7b209/src/diffusers/loaders/ip_adapter.py + +if is_transformers_available(): + from diffusers.models.attention_processor import ( + IPAdapterAttnProcessor, + IPAdapterAttnProcessor2_0, + ) + +class CopyWeights(object): + @classmethod + def from_unet2d(cls, unet: UNet2DConditionModel): + # adapted from :https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/unets/unet_motion_model.py + + config = dict(unet.config) + + # Need this for backwards compatibility with UNet2DConditionModel checkpoints + if not config.get("num_attention_heads"): + config["num_attention_heads"] = config["attention_head_dim"] + + config = FrozenDict(config) + model = cls.from_config(config) + + model.conv_in.load_state_dict(unet.conv_in.state_dict()) + + model.time_proj.load_state_dict(unet.time_proj.state_dict()) + model.time_embedding.load_state_dict(unet.time_embedding.state_dict()) + + if any( + isinstance(proc, (IPAdapterAttnProcessor, IPAdapterAttnProcessor2_0)) + for proc in unet.attn_processors.values() + ): + attn_procs = {} + for name, processor in unet.attn_processors.items(): + if name.endswith("attn1.processor"): + attn_processor_class = ( + AttnProcessor2_0 if hasattr(F, "scaled_dot_product_attention") else AttnProcessor + ) + attn_procs[name] = attn_processor_class() + else: + attn_processor_class = ( + IPAdapterAttnProcessor2_0 + if hasattr(F, "scaled_dot_product_attention") + else IPAdapterAttnProcessor + ) + attn_procs[name] = attn_processor_class( + hidden_size=processor.hidden_size, + cross_attention_dim=processor.cross_attention_dim, + scale=processor.scale, + num_tokens=processor.num_tokens, + ) + for name, processor in model.attn_processors.items(): + if name not in attn_procs: + attn_procs[name] = processor.__class__() + model.set_attn_processor(attn_procs) + model.config.encoder_hid_dim_type = "ip_image_proj" + model.encoder_hid_proj = unet.encoder_hid_proj + + for i, down_block in enumerate(unet.down_blocks): + model.down_blocks[i].resnets.load_state_dict(down_block.resnets.state_dict()) + if hasattr(model.down_blocks[i], "attentions"): + model.down_blocks[i].attentions.load_state_dict(down_block.attentions.state_dict()) + if model.down_blocks[i].downsamplers: + model.down_blocks[i].downsamplers.load_state_dict(down_block.downsamplers.state_dict()) + + for i, up_block in enumerate(unet.up_blocks): + model.up_blocks[i].resnets.load_state_dict(up_block.resnets.state_dict()) + if hasattr(model.up_blocks[i], "attentions"): + model.up_blocks[i].attentions.load_state_dict(up_block.attentions.state_dict()) + if model.up_blocks[i].upsamplers: + model.up_blocks[i].upsamplers.load_state_dict(up_block.upsamplers.state_dict()) + + model.mid_block.resnets.load_state_dict(unet.mid_block.resnets.state_dict()) + model.mid_block.attentions.load_state_dict(unet.mid_block.attentions.state_dict()) + + if unet.conv_norm_out is not None: + model.conv_norm_out.load_state_dict(unet.conv_norm_out.state_dict()) + if unet.conv_act is not None: + model.conv_act.load_state_dict(unet.conv_act.state_dict()) + model.conv_out.load_state_dict(unet.conv_out.state_dict()) + + # ensure that the Motion UNet is the same dtype as the UNet2DConditionModel + model.to(unet.dtype) + + return model + +class InsertReferenceAdapter(object): + def __init__(self): + self.reference_modules_down = None + self.reference_modules_mid = None + self.reference_modules_up = None + + def insert_reference_adapter(self, adapter: HMReferenceAdapter): + self.reference_modules_down = copy.deepcopy(adapter.reference_modules_down) + self.reference_modules_mid = copy.deepcopy(adapter.reference_modules_mid) + self.reference_modules_up = copy.deepcopy(adapter.reference_modules_up) + diff --git a/hellomeme/models/hm_control.py b/hellomeme/models/hm_control.py new file mode 100644 index 0000000..d077214 --- /dev/null +++ b/hellomeme/models/hm_control.py @@ -0,0 +1,88 @@ +# coding: utf-8 + +""" +@File : models6/hm_control.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/14/2024 +@Desc : +""" + +from typing import Tuple + +import torch +import torch.nn as nn +import torch.nn.functional as F +from diffusers.models.modeling_utils import ModelMixin +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.models.embeddings import TimestepEmbedding, Timesteps +from einops import rearrange +from .hm_blocks import SKCrossAttention + + +class HMControlNet(ModelMixin, ConfigMixin): + @register_to_config + def __init__( + self, + embedding_channels: int, + input_channels: int = 3, + scale_factor: int = 8, + cross_attention_dim: int = 320, + block_out_channels: Tuple[int] = (128, 320, 640, 1280), + ): + super().__init__() + self.scale_factor = scale_factor + self.embedding_channels = embedding_channels + self.cross_attention_dim = cross_attention_dim + self.conv_in = nn.Conv2d(input_channels, block_out_channels[0], kernel_size=3, padding=1, bias=False) + + self.exp_embedding = Timesteps(cross_attention_dim, True, 0) + self.exp_proj = TimestepEmbedding(cross_attention_dim, cross_attention_dim) + self.face_proj = TimestepEmbedding(1024, cross_attention_dim) + + self.blocks_down = nn.ModuleList([]) + for i in range(1, len(block_out_channels)): + channel_in = block_out_channels[i-1] + channel_out = block_out_channels[i] + self.blocks_down.append( + SKCrossAttention( + channel_in=channel_in, + channel_out=channel_out, + cross_attention_dim=cross_attention_dim, + num_positional_embeddings=64, + num_positional_embeddings_hidden=64 + ) + ) + + def forward(self, condition, drive_coeff, face_parts): + bs, _, video_length, h, w = condition.shape + + condition = rearrange(condition, "b c f h w -> (b f) c h w") + conditioning = F.interpolate(condition, + size=(h // self.scale_factor, w // self.scale_factor), + mode='bilinear', + align_corners=False) + embedding = self.conv_in(conditioning) + + drive_coeff = rearrange(drive_coeff * 500., "b f c -> (b f c)") + drive_embedding = self.exp_embedding(drive_coeff).to(dtype=embedding.dtype) + drive_embedding = rearrange(drive_embedding, "(b f c) d -> b f c d", + b=bs, f=video_length, d=self.cross_attention_dim) + + face_parts = rearrange(face_parts, "b f c d -> (b f) c d") + face_embedding = self.face_proj(face_parts) + + face_embedding = rearrange(face_embedding, "(b f) c d -> b f c d", f=video_length) + + drive_embedding = torch.cat([face_embedding, drive_embedding], dim=2) + drive_embedding = rearrange(drive_embedding, "b f c d -> (b f) c d") + + drive_embedding = self.exp_proj(drive_embedding) + + ret_dict = {} + for idx, block in enumerate(self.blocks_down): + embedding = block(embedding, drive_embedding) + ret_dict[f'down_{idx}'] = rearrange(embedding, + "(b f) c h w -> b c f h w", + f=video_length) + return ret_dict \ No newline at end of file diff --git a/hellomeme/models/hm_denoising_3d.py b/hellomeme/models/hm_denoising_3d.py new file mode 100644 index 0000000..e989d81 --- /dev/null +++ b/hellomeme/models/hm_denoising_3d.py @@ -0,0 +1,289 @@ +# coding: utf-8 + +""" +@File : models6/hm_denoising_3d.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/14/2024 +@Desc : 删除实验代码,精简结构 +adapted from: https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/unets/unet_2d_condition.py +""" + +import torch +import torch.utils.checkpoint +from typing import Any, Dict, Optional, Tuple, Union + +from einops import rearrange + +from diffusers.utils import USE_PEFT_BACKEND, deprecate, logging, scale_lora_layers, unscale_lora_layers +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel, UNet2DConditionOutput +from .hm_blocks import CopyWeights, InsertReferenceAdapter + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class HMDenoising3D(UNet2DConditionModel, CopyWeights, InsertReferenceAdapter): + def forward( + self, + sample: torch.Tensor, + timestep: Union[torch.Tensor, float, int], + encoder_hidden_states: torch.Tensor, + reference_hidden_states: Optional[dict] = None, + control_hidden_states: Optional[torch.Tensor] = None, + class_labels: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None, + down_block_additional_residuals: Optional[Tuple[torch.Tensor]] = None, + mid_block_additional_residual: Optional[torch.Tensor] = None, + down_intrablock_additional_residuals: Optional[Tuple[torch.Tensor]] = None, + encoder_attention_mask: Optional[torch.Tensor] = None, + return_dict: bool = True, + ) -> Union[UNet2DConditionOutput, Tuple]: + # By default samples have to be AT least a multiple of the overall upsampling factor. + # The overall upsampling factor is equal to 2 ** (# num of upsampling layers). + # However, the upsampling interpolation output size can be forced to fit any upsampling size + # on the fly if necessary. + default_overall_up_factor = 2**self.num_upsamplers + + # upsample size should be forwarded when sample is not a multiple of `default_overall_up_factor` + forward_upsample_size = False + upsample_size = None + + for dim in sample.shape[-2:]: + if dim % default_overall_up_factor != 0: + # Forward upsample size to force interpolation output size. + forward_upsample_size = True + break + + # ensure attention_mask is a bias, and give it a singleton query_tokens dimension + # expects mask of shape: + # [batch, key_tokens] + # adds singleton query_tokens dimension: + # [batch, 1, key_tokens] + # this helps to broadcast it as a bias over attention scores, which will be in one of the following shapes: + # [batch, heads, query_tokens, key_tokens] (e.g. torch sdp attn) + # [batch * heads, query_tokens, key_tokens] (e.g. xformers or classic attn) + if attention_mask is not None: + # assume that mask is expressed as: + # (1 = keep, 0 = discard) + # convert mask into a bias that can be added to attention scores: + # (keep = +0, discard = -10000.0) + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # convert encoder_attention_mask to a bias the same way we do for attention_mask + if encoder_attention_mask is not None: + encoder_attention_mask = (1 - encoder_attention_mask.to(sample.dtype)) * -10000.0 + encoder_attention_mask = encoder_attention_mask.unsqueeze(1) + + # 0. center input if necessary + if self.config.center_input_sample: + sample = 2 * sample - 1.0 + + # 1. time + t_emb = self.get_time_embed(sample=sample, timestep=timestep) + emb = self.time_embedding(t_emb, timestep_cond) + aug_emb = None + + class_emb = self.get_class_embed(sample=sample, class_labels=class_labels) + if class_emb is not None: + if self.config.class_embeddings_concat: + emb = torch.cat([emb, class_emb], dim=-1) + else: + emb = emb + class_emb + + aug_emb = self.get_aug_embed( + emb=emb, encoder_hidden_states=encoder_hidden_states, added_cond_kwargs=added_cond_kwargs + ) + if self.config.addition_embed_type == "image_hint": + aug_emb, hint = aug_emb + sample = torch.cat([sample, hint], dim=1) + + emb = emb + aug_emb if aug_emb is not None else emb + + if self.time_embed_act is not None: + emb = self.time_embed_act(emb) + + num_frames = sample.shape[2] + emb = emb.repeat_interleave(repeats=num_frames, dim=0) + + if len(encoder_hidden_states.shape) == 3: + encoder_hidden_states = encoder_hidden_states.repeat_interleave(repeats=num_frames, dim=0) + elif len(encoder_hidden_states.shape) == 4: + encoder_hidden_states = rearrange(encoder_hidden_states, "b f l d -> (b f) l d") + + if not added_cond_kwargs is None and 'image_embeds' in added_cond_kwargs: + if isinstance(added_cond_kwargs['image_embeds'], torch.Tensor): + added_cond_kwargs['image_embeds'] = added_cond_kwargs['image_embeds'].repeat_interleave(repeats=num_frames, dim=0) + else: + added_cond_kwargs['image_embeds'] = [x.repeat_interleave(repeats=num_frames, dim=0) for x in added_cond_kwargs['image_embeds']] + + encoder_hidden_states = self.process_encoder_hidden_states( + encoder_hidden_states=encoder_hidden_states, added_cond_kwargs=added_cond_kwargs + ) + + # 2. pre-process + sample = rearrange(sample, "b c f h w -> (b f) c h w") + sample = self.conv_in(sample) + + # 2.5 GLIGEN position net + if cross_attention_kwargs is not None and cross_attention_kwargs.get("gligen", None) is not None: + cross_attention_kwargs = cross_attention_kwargs.copy() + gligen_args = cross_attention_kwargs.pop("gligen") + cross_attention_kwargs["gligen"] = {"objs": self.position_net(**gligen_args)} + + # 3. down + # we're popping the `scale` instead of getting it because otherwise `scale` will be propagated + # to the internal blocks and will raise deprecation warnings. this will be confusing for our users. + if cross_attention_kwargs is not None: + cross_attention_kwargs = cross_attention_kwargs.copy() + lora_scale = cross_attention_kwargs.pop("scale", 1.0) + else: + lora_scale = 1.0 + + if USE_PEFT_BACKEND: + # weight the lora layers by setting `lora_scale` for each PEFT layer + scale_lora_layers(self, lora_scale) + + is_controlnet = mid_block_additional_residual is not None and down_block_additional_residuals is not None + # using new arg down_intrablock_additional_residuals for T2I-Adapters, to distinguish from controlnets + is_adapter = down_intrablock_additional_residuals is not None + # maintain backward compatibility for legacy usage, where + # T2I-Adapter and ControlNet both use down_block_additional_residuals arg + # but can only use one or the other + if not is_adapter and mid_block_additional_residual is None and down_block_additional_residuals is not None: + deprecate( + "T2I should not use down_block_additional_residuals", + "1.3.0", + "Passing intrablock residual connections with `down_block_additional_residuals` is deprecated \ + and will be removed in diffusers 1.3.0. `down_block_additional_residuals` should only be used \ + for ControlNet. Please make sure use `down_intrablock_additional_residuals` instead. ", + standard_warn=False, + ) + down_intrablock_additional_residuals = down_block_additional_residuals + is_adapter = True + + res_cache = dict() + down_block_res_samples = (sample,) + for idx, downsample_block in enumerate(self.down_blocks): + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + # For t2i-adapter CrossAttnDownBlock2D + additional_residuals = {} + if is_adapter and len(down_intrablock_additional_residuals) > 0: + additional_residuals["additional_residuals"] = down_intrablock_additional_residuals.pop(0) + + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + **additional_residuals, + ) + res_cache[f"down_{idx}"] = sample.clone() + else: + sample, res_samples = downsample_block(hidden_states=sample, temb=emb) + if is_adapter and len(down_intrablock_additional_residuals) > 0: + sample += down_intrablock_additional_residuals.pop(0) + + if not control_hidden_states is None and f'down_{idx}' in control_hidden_states: + sample += rearrange(control_hidden_states[f'down_{idx}'], "b c f h w -> (b f) c h w") + if hasattr(self, 'reference_modules_down') and not reference_hidden_states is None and f'down_{idx}' in reference_hidden_states: + sample = self.reference_modules_down[idx](sample, reference_hidden_states[f'down_{idx}'], num_frames) + + down_block_res_samples += res_samples + + if is_controlnet: + new_down_block_res_samples = () + + for down_block_res_sample, down_block_additional_residual in zip( + down_block_res_samples, down_block_additional_residuals + ): + down_block_res_sample = down_block_res_sample + down_block_additional_residual + new_down_block_res_samples = new_down_block_res_samples + (down_block_res_sample,) + + down_block_res_samples = new_down_block_res_samples + + # 4. mid + if self.mid_block is not None: + if hasattr(self.mid_block, "has_cross_attention") and self.mid_block.has_cross_attention: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + else: + sample = self.mid_block(sample, emb) + if hasattr(self, 'reference_modules_mid') and not reference_hidden_states is None and f'mid' in reference_hidden_states: + sample = self.reference_modules_mid(sample, reference_hidden_states[f'mid'], num_frames) + + # To support T2I-Adapter-XL + if ( + is_adapter + and len(down_intrablock_additional_residuals) > 0 + and sample.shape == down_intrablock_additional_residuals[0].shape + ): + sample += down_intrablock_additional_residuals.pop(0) + res_cache[f"mid"] = sample.clone() + + if is_controlnet: + sample = sample + mid_block_additional_residual + + # 5. up + for i, upsample_block in enumerate(self.up_blocks): + is_final_block = i == len(self.up_blocks) - 1 + + res_samples = down_block_res_samples[-len(upsample_block.resnets) :] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + # if we have not reached the final block and need to forward the + # upsample size, we do it here + if not is_final_block and forward_upsample_size: + upsample_size = down_block_res_samples[-1].shape[2:] + + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + res_cache[f"up_{i}"] = sample.clone() + if hasattr(self, "reference_modules_up") and not reference_hidden_states is None and f'up_{i}' in reference_hidden_states: + sample = self.reference_modules_up[i-1](sample, reference_hidden_states[f'up_{i}'], num_frames) + + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + upsample_size=upsample_size, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + ) + else: + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + upsample_size=upsample_size, + ) + + # 6. post-process + if self.conv_norm_out: + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + + if USE_PEFT_BACKEND: + # remove `lora_scale` from each PEFT layer + unscale_lora_layers(self, lora_scale) + + # reshape to (batch, channel, framerate, width, height) + sample = rearrange(sample, "(b f) c h w -> b c f h w", f=num_frames) + + if not return_dict: + return (sample, res_cache) + + return (UNet2DConditionOutput(sample=sample), res_cache) diff --git a/hellomeme/models/hm_denoising_motion.py b/hellomeme/models/hm_denoising_motion.py new file mode 100644 index 0000000..49451af --- /dev/null +++ b/hellomeme/models/hm_denoising_motion.py @@ -0,0 +1,241 @@ +# coding: utf-8 + +""" +@File : models6/hm_denoising_motion.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 9/9/2024 +@Desc : +adapted from: https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/unets/unet_motion_model.py +""" + +import torch +import torch.utils.checkpoint +from typing import Any, Dict, Optional, Tuple, Union + +from einops import rearrange + +from diffusers.utils import logging +from diffusers.models.unets.unet_motion_model import UNetMotionModel, UNetMotionOutput +from .hm_blocks import InsertReferenceAdapter + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +class HMDenoisingMotion(UNetMotionModel, InsertReferenceAdapter): + def forward( + self, + sample: torch.Tensor, + timestep: Union[torch.Tensor, float, int], + encoder_hidden_states: torch.Tensor, + reference_hidden_states: Optional[dict] = None, + control_hidden_states: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None, + down_block_additional_residuals: Optional[Tuple[torch.Tensor]] = None, + mid_block_additional_residual: Optional[torch.Tensor] = None, + return_dict: bool = True, + ) -> Union[UNetMotionOutput, Tuple[torch.Tensor]]: + + # By default samples have to be AT least a multiple of the overall upsampling factor. + # The overall upsampling factor is equal to 2 ** (# num of upsampling layears). + # However, the upsampling interpolation output size can be forced to fit any upsampling size + # on the fly if necessary. + default_overall_up_factor = 2 ** self.num_upsamplers + + # upsample size should be forwarded when sample is not a multiple of `default_overall_up_factor` + forward_upsample_size = False + upsample_size = None + + if any(s % default_overall_up_factor != 0 for s in sample.shape[-2:]): + logger.info("Forward upsample size to force interpolation output size.") + forward_upsample_size = True + + # prepare attention_mask + if attention_mask is not None: + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + num_frames = sample.shape[2] + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=self.dtype) + + emb = self.time_embedding(t_emb, timestep_cond) + aug_emb = None + + if self.config.addition_embed_type == "text_time": + if "text_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `text_embeds` to be passed in `added_cond_kwargs`" + ) + + text_embeds = added_cond_kwargs.get("text_embeds") + if "time_ids" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `time_ids` to be passed in `added_cond_kwargs`" + ) + time_ids = added_cond_kwargs.get("time_ids") + time_embeds = self.add_time_proj(time_ids.flatten()) + time_embeds = time_embeds.reshape((text_embeds.shape[0], -1)) + + add_embeds = torch.concat([text_embeds, time_embeds], dim=-1) + add_embeds = add_embeds.to(emb.dtype) + aug_emb = self.add_embedding(add_embeds) + + emb = emb if aug_emb is None else emb + aug_emb + emb = emb.repeat_interleave(repeats=num_frames, dim=0) + if len(encoder_hidden_states.shape) == 3: + encoder_hidden_states = encoder_hidden_states.repeat_interleave(repeats=num_frames, dim=0) + elif len(encoder_hidden_states.shape) == 4: + encoder_hidden_states = rearrange(encoder_hidden_states, "b f l d -> (b f) l d") + + if self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "ip_image_proj": + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `encoder_hid_dim_type` set to 'ip_image_proj' which requires the keyword argument `image_embeds` to be passed in `added_conditions`" + ) + image_embeds = added_cond_kwargs.get("image_embeds") + image_embeds = self.encoder_hid_proj(image_embeds) + image_embeds = [image_embed.repeat_interleave(repeats=num_frames, dim=0) for image_embed in image_embeds] + encoder_hidden_states = (encoder_hidden_states, image_embeds) + + # 2. pre-process + sample = sample.permute(0, 2, 1, 3, 4).reshape((sample.shape[0] * num_frames, -1) + sample.shape[3:]) + sample = self.conv_in(sample) + + # 3. down + down_block_res_samples = (sample,) + for idx, downsample_block in enumerate(self.down_blocks): + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + num_frames=num_frames, + cross_attention_kwargs=cross_attention_kwargs, + ) + else: + sample, res_samples = downsample_block(hidden_states=sample, temb=emb, num_frames=num_frames) + + if not control_hidden_states is None and f'down_{idx}' in control_hidden_states: + sample += rearrange(control_hidden_states[f'down_{idx}'], "b c f h w -> (b f) c h w") + + + if hasattr(self, 'reference_modules_down') and not reference_hidden_states is None and f'down_{idx}' in reference_hidden_states: + sample = self.reference_modules_down[idx](sample, reference_hidden_states[f'down_{idx}'], num_frames) + + down_block_res_samples += res_samples + + if down_block_additional_residuals is not None: + new_down_block_res_samples = () + + for down_block_res_sample, down_block_additional_residual in zip( + down_block_res_samples, down_block_additional_residuals + ): + down_block_res_sample = down_block_res_sample + down_block_additional_residual + new_down_block_res_samples += (down_block_res_sample,) + + down_block_res_samples = new_down_block_res_samples + + # 4. mid + if self.mid_block is not None: + # To support older versions of motion modules that don't have a mid_block + if hasattr(self.mid_block, "motion_modules"): + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + num_frames=num_frames, + cross_attention_kwargs=cross_attention_kwargs, + ) + else: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + if hasattr(self, 'reference_modules_mid') and not reference_hidden_states is None and f'mid' in reference_hidden_states: + sample = self.reference_modules_mid(sample, reference_hidden_states[f'mid'], num_frames) + + if mid_block_additional_residual is not None: + sample = sample + mid_block_additional_residual + + # 5. up + for i, upsample_block in enumerate(self.up_blocks): + is_final_block = i == len(self.up_blocks) - 1 + + res_samples = down_block_res_samples[-len(upsample_block.resnets):] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + # if we have not reached the final block and need to forward the + # upsample size, we do it here + if not is_final_block and forward_upsample_size: + upsample_size = down_block_res_samples[-1].shape[2:] + + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + if hasattr(self, + "reference_modules_up") and not reference_hidden_states is None and f'up_{i}' in reference_hidden_states: + sample = self.reference_modules_up[i - 1](sample, reference_hidden_states[f'up_{i}'], + num_frames) + + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + encoder_hidden_states=encoder_hidden_states, + upsample_size=upsample_size, + attention_mask=attention_mask, + num_frames=num_frames, + cross_attention_kwargs=cross_attention_kwargs, + ) + else: + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + upsample_size=upsample_size, + num_frames=num_frames, + ) + + # 6. post-process + if self.conv_norm_out: + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + + sample = self.conv_out(sample) + + # reshape to (batch, channel, framerate, width, height) + sample = sample[None, :].reshape((-1, num_frames) + sample.shape[1:]).permute(0, 2, 1, 3, 4) + + if not return_dict: + return (sample,) + + return UNetMotionOutput(sample=sample) \ No newline at end of file diff --git a/hellomeme/piplines/__init__.py b/hellomeme/piplines/__init__.py new file mode 100644 index 0000000..a5d36f8 --- /dev/null +++ b/hellomeme/piplines/__init__.py @@ -0,0 +1,12 @@ +# coding: utf-8 + +""" +@File : __init__.py.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/29/2024 +@Desc : +""" + +from .pipline_hm_video import HMVideoPipeline +from .pipline_hm_image import HMImagePipeline diff --git a/hellomeme/piplines/pipline_hm_image.py b/hellomeme/piplines/pipline_hm_image.py new file mode 100644 index 0000000..062a524 --- /dev/null +++ b/hellomeme/piplines/pipline_hm_image.py @@ -0,0 +1,289 @@ +# coding: utf-8 + +""" +@File : hm_pipline_image.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/29/2024 +@Desc : +adapted from: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py +""" + +from typing import Any, Callable, Dict, List, Optional, Union + +import torch + +from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback +from diffusers.image_processor import PipelineImageInput +from diffusers.utils import ( + deprecate, +) +from diffusers.pipelines.stable_diffusion.pipeline_output import StableDiffusionPipelineOutput +from diffusers import StableDiffusionImg2ImgPipeline +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img import retrieve_timesteps, retrieve_latents + +from ..models import HMDenoising3D, HMControlNet +from ..models import HMReferenceAdapter + +class HMImagePipeline(StableDiffusionImg2ImgPipeline): + def caryomitosis(self,): + if not hasattr(self, "unet_ref"): + self.unet_ref = HMDenoising3D.from_unet2d(self.unet) + if not isinstance(self.unet, HMDenoising3D): + unet = HMDenoising3D.from_unet2d(unet=self.unet) + # todo: 不够优雅 + del self.unet + self.unet = unet + + def insert_hm_modules(self, dtype, device): + if isinstance(self.unet, HMDenoising3D): + hm_adapter = HMReferenceAdapter.from_pretrained('songkey/hm_reference') + self.unet.insert_reference_adapter(hm_adapter) + self.unet_ref = self.unet_ref.to(device=device, dtype=dtype).eval() + self.unet = self.unet.to(device=device, dtype=dtype).eval() + + if not hasattr(self, "mp_control"): + self.mp_control = HMControlNet.from_pretrained('songkey/hm_control') + self.mp_control = self.mp_control.to(device=device, dtype=dtype).eval() + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: PipelineImageInput = None, + drive_params: Dict[str, Any] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + timesteps: List[int] = None, + sigmas: List[float] = None, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + eta: Optional[float] = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prompt_embeds: Optional[torch.Tensor] = None, + negative_prompt_embeds: Optional[torch.Tensor] = None, + ip_adapter_image: Optional[PipelineImageInput] = None, + ip_adapter_image_embeds: Optional[List[torch.Tensor]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + clip_skip: int = None, + callback_on_step_end: Optional[ + Union[Callable[[int, int, Dict], None], PipelineCallback, MultiPipelineCallbacks] + ] = None, + callback_on_step_end_tensor_inputs: List[str] = ["latents"], + **kwargs, + ): + callback = kwargs.pop("callback", None) + callback_steps = kwargs.pop("callback_steps", None) + num_images_per_prompt = 1 + + if callback is not None: + deprecate( + "callback", + "1.0.0", + "Passing `callback` as an input argument to `__call__` is deprecated, consider use `callback_on_step_end`", + ) + if callback_steps is not None: + deprecate( + "callback_steps", + "1.0.0", + "Passing `callback_steps` as an input argument to `__call__` is deprecated, consider use `callback_on_step_end`", + ) + + if isinstance(callback_on_step_end, (PipelineCallback, MultiPipelineCallbacks)): + callback_on_step_end_tensor_inputs = callback_on_step_end.tensor_inputs + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, + strength, + callback_steps, + negative_prompt, + prompt_embeds, + negative_prompt_embeds, + ip_adapter_image, + ip_adapter_image_embeds, + callback_on_step_end_tensor_inputs, + ) + + self._guidance_scale = guidance_scale + self._clip_skip = clip_skip + self._cross_attention_kwargs = cross_attention_kwargs + self._interrupt = False + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + device = self.device + + # 3. Encode input prompt + text_encoder_lora_scale = ( + self.cross_attention_kwargs.get("scale", None) if self.cross_attention_kwargs is not None else None + ) + print('device:', device) + prompt_embeds, negative_prompt_embeds = self.encode_prompt( + prompt, + device, + num_images_per_prompt, + self.do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + lora_scale=text_encoder_lora_scale, + clip_skip=self.clip_skip, + ) + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + if self.do_classifier_free_guidance: + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + if ip_adapter_image is not None or ip_adapter_image_embeds is not None: + image_embeds = self.prepare_ip_adapter_image_embeds( + ip_adapter_image, + ip_adapter_image_embeds, + device, + batch_size * num_images_per_prompt, + self.do_classifier_free_guidance, + ) + + # 4. Preprocess + image = self.image_processor.preprocess(image).to(device=device, dtype=prompt_embeds.dtype) + + # 5. set timesteps + timesteps, num_inference_steps = retrieve_timesteps( + self.scheduler, num_inference_steps, device, timesteps, sigmas + ) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + ref_latents = [ + retrieve_latents(self.vae.encode(image[i: i + 1].to(device=device)), generator=generator) + for i in range(batch_size) + ] + ref_latents = torch.cat(ref_latents, dim=0) + ref_latents = self.vae.config.scaling_factor * ref_latents + + # 6. Prepare latent variables + # latents = self.prepare_latents( + # ref_latents, + # latent_timestep, + # batch_size, + # num_images_per_prompt, + # prompt_embeds.dtype, + # device, + # generator, + # ) + + # condition, drive_coeff, face_parts + if self.do_classifier_free_guidance: + drive_params['condition'] = torch.cat([torch.ones_like(drive_params['condition']) * -1, drive_params['condition']], dim=0) + drive_params['drive_coeff'] = torch.cat([torch.zeros_like(drive_params['drive_coeff']), drive_params['drive_coeff']], dim=0) + drive_params['face_parts'] = torch.cat([torch.zeros_like(drive_params['face_parts']), drive_params['face_parts']], dim=0) + + if hasattr(self, "mp_control"): + control_latents = self.mp_control(**drive_params) + else: + control_latents = None + + # 7. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7.1 Add image embeds for IP-Adapter + added_cond_kwargs = ( + {"image_embeds": image_embeds} + if ip_adapter_image is not None or ip_adapter_image_embeds is not None + else None + ) + + latent_model_input = torch.cat([torch.zeros_like(ref_latents), ref_latents]) if self.do_classifier_free_guidance else ref_latents + cached_res = self.unet_ref( + latent_model_input.unsqueeze(2), + 0, + encoder_hidden_states=prompt_embeds, + # added_cond_kwargs=added_cond_kwargs, + return_dict=False, + )[1] + + # 7.2 Optionally get Guidance Scale Embedding + timestep_cond = None + if self.unet.config.time_cond_proj_dim is not None: + guidance_scale_tensor = torch.tensor(self.guidance_scale - 1).repeat(batch_size * num_images_per_prompt) + timestep_cond = self.get_guidance_scale_embedding( + guidance_scale_tensor, embedding_dim=self.unet.config.time_cond_proj_dim + ).to(device=device, dtype=prompt_embeds.dtype) + + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + base_noise = torch.randn_like(ref_latents, device=device, dtype=prompt_embeds.dtype) + latents = self.scheduler.add_noise(ref_latents, base_noise, latent_timestep) + # 8. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + self._num_timesteps = len(timesteps) + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + if self.interrupt: + continue + + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents + latent_model_input = self.scheduler.scale_model_input(latent_model_input, t) + + # predict the noise residual + noise_pred = self.unet( + latent_model_input.unsqueeze(2), + t, + encoder_hidden_states=prompt_embeds, + reference_hidden_states=cached_res, + control_hidden_states=control_latents, + timestep_cond=timestep_cond, + cross_attention_kwargs=self.cross_attention_kwargs, + added_cond_kwargs=added_cond_kwargs, + return_dict=False, + )[0][:,:,0,:,:] + + # perform guidance + if self.do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs, return_dict=False)[0] + + if callback_on_step_end is not None: + callback_kwargs = {} + for k in callback_on_step_end_tensor_inputs: + callback_kwargs[k] = locals()[k] + callback_outputs = callback_on_step_end(self, i, t, callback_kwargs) + + latents = callback_outputs.pop("latents", latents) + prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds) + negative_prompt_embeds = callback_outputs.pop("negative_prompt_embeds", negative_prompt_embeds) + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + step_idx = i // getattr(self.scheduler, "order", 1) + callback(step_idx, t, latents) + + if not output_type == "latent": + image = self.vae.decode(latents / self.vae.config.scaling_factor, return_dict=False, generator=generator)[ + 0 + ] + else: + image = latents + + do_denormalize = [True] * image.shape[0] + + image = self.image_processor.postprocess(image, output_type=output_type, do_denormalize=do_denormalize) + + # Offload all models + self.maybe_free_model_hooks() + + return StableDiffusionPipelineOutput(images=image, nsfw_content_detected=None) \ No newline at end of file diff --git a/hellomeme/piplines/pipline_hm_video.py b/hellomeme/piplines/pipline_hm_video.py new file mode 100644 index 0000000..344932c --- /dev/null +++ b/hellomeme/piplines/pipline_hm_video.py @@ -0,0 +1,376 @@ +# coding: utf-8 + +""" +@File : hm_pipline_image.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/29/2024 +@Desc : +adapted from: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_img2img.py +""" + +from typing import Any, Callable, Dict, List, Optional, Union +import torch + +from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback +from diffusers.image_processor import PipelineImageInput +from diffusers.utils import deprecate +from diffusers.utils.torch_utils import randn_tensor + +from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img import retrieve_timesteps, retrieve_latents +from diffusers import StableDiffusionImg2ImgPipeline +from diffusers import EulerDiscreteScheduler + +from diffusers import MotionAdapter +from ..models import HMDenoising3D, HMDenoisingMotion, HMControlNet +from ..models import HMReferenceAdapter + + +class HMVideoPipeline(StableDiffusionImg2ImgPipeline): + def caryomitosis(self,): + if not hasattr(self, "unet_ref"): + self.unet_ref = HMDenoising3D.from_unet2d(self.unet) + if not isinstance(self.unet, HMDenoisingMotion): + adapter = MotionAdapter.from_pretrained("songkey/hm_animatediff", torch_dtype=torch.float16) + unet = HMDenoisingMotion.from_unet2d(unet=self.unet, motion_adapter=adapter, load_weights=True) + # todo: 不够优雅 + del self.unet + self.unet = unet + + def insert_hm_modules(self, dtype, device): + if isinstance(self.unet, HMDenoisingMotion): + hm_adapter = HMReferenceAdapter.from_pretrained('songkey/hm_reference') + self.unet.insert_reference_adapter(hm_adapter) + self.unet_ref.insert_reference_adapter(hm_adapter) + self.unet_ref = self.unet_ref.to(device=device, dtype=dtype).eval() + self.unet = self.unet.to(device=device, dtype=dtype).eval() + + if not hasattr(self, "mp_control"): + self.mp_control = HMControlNet.from_pretrained('songkey/hm_control') + self.mp_control = self.mp_control.to(device=device, dtype=dtype).eval() + + @torch.no_grad() + def __call__( + self, + prompt: Union[str, List[str]] = None, + image: PipelineImageInput = None, + num_frames: int = 16, + chunk_overlap: int = 4, + drive_params: Dict[str, Any] = None, + strength: float = 0.8, + num_inference_steps: Optional[int] = 50, + timesteps: List[int] = None, + sigmas: List[float] = None, + guidance_scale: Optional[float] = 7.5, + negative_prompt: Optional[Union[str, List[str]]] = None, + eta: Optional[float] = 0.0, + generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, + prompt_embeds: Optional[torch.Tensor] = None, + negative_prompt_embeds: Optional[torch.Tensor] = None, + ip_adapter_image: Optional[PipelineImageInput] = None, + ip_adapter_image_embeds: Optional[List[torch.Tensor]] = None, + output_type: Optional[str] = "pil", + return_dict: bool = True, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + clip_skip: int = None, + callback_on_step_end: Optional[ + Union[Callable[[int, int, Dict], None], PipelineCallback, MultiPipelineCallbacks] + ] = None, + callback_on_step_end_tensor_inputs: List[str] = ["latents"], + **kwargs, + ): + callback = kwargs.pop("callback", None) + callback_steps = kwargs.pop("callback_steps", None) + num_images_per_prompt = 1 + + if callback is not None: + deprecate( + "callback", + "1.0.0", + "Passing `callback` as an input argument to `__call__` is deprecated, consider use `callback_on_step_end`", + ) + if callback_steps is not None: + deprecate( + "callback_steps", + "1.0.0", + "Passing `callback_steps` as an input argument to `__call__` is deprecated, consider use `callback_on_step_end`", + ) + + if isinstance(callback_on_step_end, (PipelineCallback, MultiPipelineCallbacks)): + callback_on_step_end_tensor_inputs = callback_on_step_end.tensor_inputs + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, + strength, + callback_steps, + negative_prompt, + prompt_embeds, + negative_prompt_embeds, + ip_adapter_image, + ip_adapter_image_embeds, + callback_on_step_end_tensor_inputs, + ) + + self._guidance_scale = guidance_scale + self._clip_skip = clip_skip + self._cross_attention_kwargs = cross_attention_kwargs + self._interrupt = False + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + else: + batch_size = prompt_embeds.shape[0] + + device = self._execution_device + + # 3. Encode input prompt + text_encoder_lora_scale = ( + self.cross_attention_kwargs.get("scale", None) if self.cross_attention_kwargs is not None else None + ) + prompt_embeds, negative_prompt_embeds = self.encode_prompt( + prompt, + device, + num_images_per_prompt, + self.do_classifier_free_guidance, + negative_prompt, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + lora_scale=text_encoder_lora_scale, + clip_skip=self.clip_skip, + ) + # For classifier free guidance, we need to do two forward passes. + # Here we concatenate the unconditional and text embeddings into a single batch + # to avoid doing two forward passes + if self.do_classifier_free_guidance: + prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds]) + + if ip_adapter_image is not None or ip_adapter_image_embeds is not None: + image_embeds = self.prepare_ip_adapter_image_embeds( + ip_adapter_image, + ip_adapter_image_embeds, + device, + batch_size * num_images_per_prompt, + self.do_classifier_free_guidance, + ) + + # 4. Preprocess image + image = self.image_processor.preprocess(image).to(device=device, dtype=prompt_embeds.dtype) + + # 5. set timesteps + timesteps, num_inference_steps = retrieve_timesteps( + self.scheduler, num_inference_steps, device, timesteps, sigmas + ) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + raw_video_len = drive_params['drive_coeff'].size(1) + chunk_size = num_frames + chunk_overlap = min(max(0, chunk_overlap), chunk_size // 2) + chunk_stride = chunk_size - chunk_overlap + + drive_idx_list = list(range(raw_video_len)) + + steped_video_len = len(drive_idx_list) + post_pad_num = chunk_stride - (steped_video_len - chunk_overlap) % chunk_stride + + if post_pad_num > 0: + drive_idx_list = drive_idx_list + [drive_idx_list[-1]] * post_pad_num + paded_video_len = len(drive_idx_list) + + drive_idx_chunks = [drive_idx_list[i:i + chunk_size] for i in + range(0, paded_video_len - chunk_stride + 1, chunk_stride)] + + # 6. Prepare latent variables + ref_latents = [ + retrieve_latents(self.vae.encode(image[i: i + 1].to(device=device)), generator=generator) + for i in range(batch_size) + ] + ref_latents = torch.cat(ref_latents, dim=0) + ref_latents = self.vae.config.scaling_factor * ref_latents + c, h, w = ref_latents.shape[1:] + + # 7. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline + extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta) + + # 7.1 Add image embeds for IP-Adapter + added_cond_kwargs = ( + {"image_embeds": image_embeds} + if ip_adapter_image is not None or ip_adapter_image_embeds is not None + else None + ) + + # if hasattr(self, "unet_ref"): + ref_latents = ref_latents.unsqueeze(2) + cached_res = self.unet_ref( + torch.cat([torch.zeros_like(ref_latents), ref_latents], dim=0) if + self.do_classifier_free_guidance else ref_latents, + 0, + encoder_hidden_states=prompt_embeds, + # added_cond_kwargs=added_cond_kwargs, + return_dict=False, + )[1] + # else: + # cached_res = None + + # 7.2 Optionally get Guidance Scale Embedding + timestep_cond = None + if self.unet.config.time_cond_proj_dim is not None: + guidance_scale_tensor = torch.tensor(self.guidance_scale - 1).repeat(batch_size * num_images_per_prompt) + timestep_cond = self.get_guidance_scale_embedding( + guidance_scale_tensor, embedding_dim=self.unet.config.time_cond_proj_dim + ).to(device=device, dtype=prompt_embeds.dtype) + + if self.do_classifier_free_guidance: + drive_params['condition'] = torch.cat( + [torch.ones_like(drive_params['condition']) * -1, drive_params['condition']], dim=0) + drive_params['drive_coeff'] = torch.cat( + [torch.zeros_like(drive_params['drive_coeff']), drive_params['drive_coeff']], dim=0) + drive_params['face_parts'] = torch.cat( + [torch.zeros_like(drive_params['face_parts']), drive_params['face_parts']], dim=0) + + control_latents_chunks = [] + for cidx, chunk in enumerate(drive_idx_chunks): + condition_chunk = drive_params['condition'][:, :, chunk, :, :] + drive_coeff_chunk = drive_params['drive_coeff'][:, chunk, :] + face_parts_chunk = drive_params['face_parts'][:, chunk, :, :] + + + control_latents_chunk = self.mp_control(condition=condition_chunk.to(device=device), + drive_coeff=drive_coeff_chunk.to(device=device), + face_parts=face_parts_chunk.to(device=device)) + control_latents_chunks.append(control_latents_chunk) + drive_idx_chunks[-1] = list(range(drive_idx_chunks[-1][0], paded_video_len)) + + with self.progress_bar(total=len(drive_idx_chunks)) as progress_bar: + chunk_latents = torch.zeros([batch_size, c, paded_video_len, h, w], dtype=prompt_embeds.dtype).cpu() + chunk_latents_counter = torch.zeros([1, 1, paded_video_len, 1, 1], dtype=prompt_embeds.dtype).cpu() + chunk_noise = randn_tensor([batch_size, c, 1, h, w], dtype=prompt_embeds.dtype, generator=generator).to( + device=device) + chunk_noise = chunk_noise.repeat_interleave(chunk_size, dim=2) + for cidx, chunk in enumerate(drive_idx_chunks): + control_latents_chunk = control_latents_chunks[cidx] + + scheduler = EulerDiscreteScheduler( + num_train_timesteps=1000, + beta_start=0.00085, + beta_end=0.012, + beta_schedule="scaled_linear", + ) + + tmp_timesteps, _ = retrieve_timesteps( + scheduler, 8, device, None, sigmas + ) + # chunk_latent = chunk_noise * self.scheduler.init_noise_sigma + chunk_latent = scheduler.add_noise(ref_latents, chunk_noise, tmp_timesteps[:1]) + for i, t in enumerate(tmp_timesteps): + latent_model_input = torch.cat([chunk_latent, chunk_latent], dim=0) if \ + self.do_classifier_free_guidance else chunk_latent + latent_model_input = scheduler.scale_model_input(latent_model_input, t) + noise_pred_chunk = self.unet_ref( + latent_model_input, + t, + encoder_hidden_states=prompt_embeds, + reference_hidden_states=cached_res, + control_hidden_states=control_latents_chunk, + timestep_cond=timestep_cond, + cross_attention_kwargs=self.cross_attention_kwargs, + added_cond_kwargs=added_cond_kwargs, + return_dict=False, + )[0] + if self.do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred_chunk.chunk(2) + noise_pred_chunk = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond) + chunk_latent = scheduler.step(noise_pred_chunk, t, chunk_latent, + **extra_step_kwargs, return_dict=False)[0] + + chunk_latents[:, :, chunk] = chunk_latents[:, :, chunk] + chunk_latent.cpu() + chunk_latents_counter[:, :, chunk] += 1 + progress_bar.update() + chunk_latents = chunk_latents / chunk_latents_counter + + # 5. set timesteps + timesteps, num_inference_steps = retrieve_timesteps( + self.scheduler, num_inference_steps, device, None, sigmas + ) + timesteps, num_inference_steps = self.get_timesteps(num_inference_steps, strength, device) + latent_timestep = timesteps[:1].repeat(batch_size * num_images_per_prompt) + + # 8. Denoising loop + num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order + self._num_timesteps = len(timesteps) + + base_noise = randn_tensor([batch_size, c, paded_video_len, h, w], dtype=prompt_embeds.dtype, generator=generator) + # ref_latents = ref_latents.repeat_interleave(paded_video_len, dim=2) + + latents = self.scheduler.add_noise(chunk_latents.to(device=device), base_noise.to(device=device), latent_timestep).cpu() + + with self.progress_bar(total=num_inference_steps) as progress_bar: + for i, t in enumerate(timesteps): + noise_pred = torch.zeros([batch_size * 2 if self.do_classifier_free_guidance + else batch_size, c, paded_video_len, h, w], dtype=prompt_embeds.dtype).cpu() + noise_pred_counter = torch.zeros([1, 1, paded_video_len, 1, 1], dtype=prompt_embeds.dtype).cpu() + + latent_model_input_all = torch.cat([latents.clone(), latents.clone()], dim=0) if \ + self.do_classifier_free_guidance else latents.clone() + + latent_model_input_all = self.scheduler.scale_model_input(latent_model_input_all, t) + for cidx, chunk in enumerate(drive_idx_chunks): + if self.interrupt: + continue + + control_latents = control_latents_chunks[cidx] + latent_model_input = latent_model_input_all[:, :, chunk].to(device=device) + + # predict the noise residual + noise_pred_chunk = self.unet( + latent_model_input, + t, + encoder_hidden_states=prompt_embeds, + reference_hidden_states=cached_res, + control_hidden_states=control_latents, + timestep_cond=timestep_cond, + cross_attention_kwargs=self.cross_attention_kwargs, + added_cond_kwargs=added_cond_kwargs, + return_dict=False, + )[0].cpu() + + noise_pred[:, :, chunk] = noise_pred[:, :, chunk] + noise_pred_chunk + noise_pred_counter[:, :, chunk] = noise_pred_counter[:, :, chunk] + 1 + + noise_pred = noise_pred / noise_pred_counter + # perform guidance + if self.do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler.step(noise_pred.to(device=device), t, latents.to(device=device), + **extra_step_kwargs, return_dict=False)[0].cpu() + + if callback_on_step_end is not None: + callback_kwargs = {} + for k in callback_on_step_end_tensor_inputs: + callback_kwargs[k] = locals()[k] + callback_outputs = callback_on_step_end(self, i, t, callback_kwargs) + + latents = callback_outputs.pop("latents", latents) + prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds) + negative_prompt_embeds = callback_outputs.pop("negative_prompt_embeds", negative_prompt_embeds) + + # call the callback, if provided + if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0): + progress_bar.update() + if callback is not None and i % callback_steps == 0: + step_idx = i // getattr(self.scheduler, "order", 1) + callback(step_idx, t, latents) + + latents = latents / self.vae.config.scaling_factor + + res_frames = [self.vae.decode(latents[:,:,i,:,:].to(device=device), return_dict=False, generator=generator)[0] + for i in range(raw_video_len)] + res_frames = [self.image_processor.postprocess(frame, output_type=output_type) for frame in res_frames] + return res_frames \ No newline at end of file diff --git a/hellomeme/tools/__init__.py b/hellomeme/tools/__init__.py new file mode 100644 index 0000000..d0ba181 --- /dev/null +++ b/hellomeme/tools/__init__.py @@ -0,0 +1,13 @@ +# coding: utf-8 + +# @File : __init__.py +# @Author : Songkey +# @Email : songkey@pku.edu.cn +# @Date : 8/28/2024 +# @Desc : + +from .hello_arkit import HelloARKitBSPred +from .hello_face_det import HelloFaceDet +from .hello_camera_demo import HelloCameraDemo +from .hello_3dmm import Hello3DMMPred +from .hello_face_alignment import HelloFaceAlignment diff --git a/hellomeme/tools/hello_3dmm.py b/hellomeme/tools/hello_3dmm.py new file mode 100644 index 0000000..4be6d8a --- /dev/null +++ b/hellomeme/tools/hello_3dmm.py @@ -0,0 +1,140 @@ +# coding: utf-8 + +""" +@File : test.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 11/1/2024 +@Desc : Created by Shengjie Wu (wu.shengjie@immomo.com) +这可能是一个很强大的模型 +""" + +import numpy as np +import cv2 + +from huggingface_hub import hf_hub_download +from .utils import get_warp_mat_bbox_by_gt_pts_float, create_onnx_session + +def crop_transl_to_full_transl(crop_trans, crop_center, scale, full_center, focal_length): + """ + :param crop_trans: (3), float + :param crop_center: (2), float + :param scale: (1), float + :param full_center: (2), float + :param focal_length: (1), float + :return: + """ + crop_c_x, crop_c_y = crop_center + full_c_x, full_c_y = full_center + bs = 2 * focal_length / scale / crop_trans[2] + full_x = crop_trans[0] - 2 * (crop_c_x - full_c_x) / bs + full_y = crop_trans[1] + 2 * (crop_c_y - full_c_y) / bs + full_z = crop_trans[2] * scale + + full_trans = np.array([full_x, full_y, full_z], dtype=np.float32) + + return full_trans + +class Hello3DMMPred(object): + def __init__(self, gpu_id=None): + self.deep3d_pred_net = create_onnx_session(hf_hub_download('songkey/hello_group_facemodel', filename='hello_3dmm.onnx'), gpu_id=gpu_id) + self.deep3d_pred_net_input_name = self.deep3d_pred_net.get_inputs()[0].name + self.deep3d_pred_net_output_name = [output.name for output in self.deep3d_pred_net.get_outputs()] + + self.image_size = 224 + self.camera_init_z = -0.4 + self.camera_init_focal_len = 386.2879122887948 + self.used_focal_len = -5.0 / self.camera_init_z * self.camera_init_focal_len + self.id_dims = 526 + self.exp_dims = 203 + self.tex_dims = 439 + + def forward_params(self, src_image, src_pt): + align_mat_info = get_warp_mat_bbox_by_gt_pts_float(src_pt, base_angle=0, dst_size=self.image_size, expand_ratio=0.35, return_info=True) + align_mat = align_mat_info["M"] + + align_image_rgb_uint8 = cv2.cvtColor(cv2.warpAffine(src_image, align_mat, (self.image_size, self.image_size)), cv2.COLOR_BGR2RGB) + + # cv2.imshow('align_image_rgb_uint8', align_image_rgb_uint8) + + align_image_rgb_fp32 = align_image_rgb_uint8.astype(np.float32) / 255.0 + align_image_rgb_fp32_onnx_input = align_image_rgb_fp32.copy().transpose((2, 0, 1))[np.newaxis, ...] + pred_coeffs = self.deep3d_pred_net.run(self.deep3d_pred_net_output_name, + {self.deep3d_pred_net_input_name: align_image_rgb_fp32_onnx_input})[0] + + angles = pred_coeffs[:, self.id_dims + self.exp_dims + self.tex_dims:self.id_dims + self.exp_dims + self.tex_dims + 3] + translations = pred_coeffs[:, self.id_dims + self.exp_dims + self.tex_dims + 3 + 27:] + + crop_global_transl = crop_transl_to_full_transl(translations[0], + crop_center=[align_mat_info["center_x"], + align_mat_info["center_y"]], + scale=align_mat_info["scale"], + full_center=[src_image.shape[1] * 0.5, src_image.shape[0] * 0.5], + focal_length=self.used_focal_len) + return angles, crop_global_transl[np.newaxis, :] + + def compute_rotation_matrix(self, angles): + n_b = angles.shape[0] + sinx = np.sin(angles[:, 0]) + siny = np.sin(angles[:, 1]) + sinz = np.sin(angles[:, 2]) + cosx = np.cos(angles[:, 0]) + cosy = np.cos(angles[:, 1]) + cosz = np.cos(angles[:, 2]) + rotXYZ = np.eye(3).reshape(1, 3, 3).repeat(n_b*3, 0).reshape(3, n_b, 3, 3) + rotXYZ[0, :, 1, 1] = cosx + rotXYZ[0, :, 1, 2] = -sinx + rotXYZ[0, :, 2, 1] = sinx + rotXYZ[0, :, 2, 2] = cosx + rotXYZ[1, :, 0, 0] = cosy + rotXYZ[1, :, 0, 2] = siny + rotXYZ[1, :, 2, 0] = -siny + rotXYZ[1, :, 2, 2] = cosy + rotXYZ[2, :, 0, 0] = cosz + rotXYZ[2, :, 0, 1] = -sinz + rotXYZ[2, :, 1, 0] = sinz + rotXYZ[2, :, 1, 1] = cosz + rotation = np.matmul(np.matmul(rotXYZ[2], rotXYZ[1]), rotXYZ[0]) + return rotation.transpose(0, 2, 1) + + def rigid_transform(self, vs, rot, trans): + vs_r = np.matmul(vs, rot) + vs_t = vs_r + trans.reshape(-1, 1, 3) + return vs_t + + def perspective_projection_points(self, points, image_w, image_h, used_focal_len): + batch_size = points.shape[0] + K = np.zeros([batch_size, 3, 3]) + K[:, 0, 0] = used_focal_len + K[:, 1, 1] = used_focal_len + K[:, 2, 2] = 1. + K[:, 0, 2] = image_w * 0.5 + K[:, 1, 2] = image_h * 0.5 + + reverse_z = np.array([[1, 0, 0], [0, 1, 0], [0, 0, -1]])[np.newaxis, :, :].repeat(batch_size, 0) + + # Transform points + aug_projection = np.matmul(points, reverse_z) + aug_projection = np.matmul(aug_projection, K.transpose((0, 2, 1))) + + # Apply perspective distortion + projected_points = aug_projection[:, :, :2] / aug_projection[:, :, 2:] + return projected_points + + def get_project_points_rect(self, angle, trans, image_w, image_h): + vs = np.array( + [[-1, -1, 0], [-1, 1, 0], [1, 1, 0], [1, -1, 0]], + ) * 0.05 + vs = vs[np.newaxis, :, :] + + rotation = self.compute_rotation_matrix(angle) + translation = trans.copy() + translation[0, 2] *= 0.05 + + vs_t = self.rigid_transform(vs, rotation, translation) + + project_points = self.perspective_projection_points(vs_t, image_w, image_h, self.used_focal_len*0.05) + project_points = np.stack([project_points[:, :, 0], image_h - project_points[:, :, 1]], axis=2) + + return project_points[0] + diff --git a/hellomeme/tools/hello_arkit.py b/hellomeme/tools/hello_arkit.py new file mode 100644 index 0000000..2c8b2f8 --- /dev/null +++ b/hellomeme/tools/hello_arkit.py @@ -0,0 +1,35 @@ +""" +@File : test.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 11/1/2024 +@Desc : Created by Shengjie Wu (wu.shengjie@immomo.com) +""" + +import numpy as np +import cv2 +from huggingface_hub import hf_hub_download +from .utils import create_onnx_session, get_warp_mat_bbox_by_gt_pts_float + +class HelloARKitBSPred(object): + def __init__(self, gpu_id=0): + self.face_rig_net = create_onnx_session(hf_hub_download('songkey/hello_group_facemodel', filename='hello_arkit_blendshape.onnx'), gpu_id=gpu_id) + self.onnx_input_name = self.face_rig_net.get_inputs()[0].name + self.onnx_output_name = [output.name for output in self.face_rig_net.get_outputs()] + self.image_size = 224 + self.expand_ratio = 0.15 + + def forward(self, src_image, src_pt): + left_eye_corner = src_pt[74] + right_eye_corner = src_pt[96] + radian = np.arctan2(right_eye_corner[1] - left_eye_corner[1], right_eye_corner[0] - left_eye_corner[0] + 0.00000001) + rotate_angle = np.rad2deg(radian) + align_warp_mat = get_warp_mat_bbox_by_gt_pts_float(src_pt, base_angle=rotate_angle, dst_size=self.image_size, + expand_ratio=self.expand_ratio) + face_rig_input = cv2.warpAffine(src_image, align_warp_mat, (self.image_size, self.image_size)) + + face_rig_onnx_input = face_rig_input.transpose((2, 0, 1)).astype(np.float32)[np.newaxis, :, :, :] / 255.0 + face_rig_params = self.face_rig_net.run(self.onnx_output_name, + {self.onnx_input_name: face_rig_onnx_input}) + face_rig_params = face_rig_params[0][0] + return face_rig_params diff --git a/hellomeme/tools/hello_camera_demo.py b/hellomeme/tools/hello_camera_demo.py new file mode 100644 index 0000000..105d3ae --- /dev/null +++ b/hellomeme/tools/hello_camera_demo.py @@ -0,0 +1,377 @@ +""" +@File : test.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 11/1/2024 +@Desc : Created by Shengjie Wu (wu.shengjie@immomo.com) +""" + +import numpy as np +from .utils import OneEuroFilter + +def cult_dis(old_kpts, new_kpts): + dis = np.sqrt(np.square(new_kpts[:, 0] - old_kpts[:, 0]) + np.square(new_kpts[:, 1] - old_kpts[:, 1])) + return dis + +class Smoother222(object): + def __init__(self): + # face config + self.face_idx = list(range(0, 33)) + self.face_down_idx = list(range(9, 24)) + self.filter_face = OneEuroFilter() + # nose config + self.nose_idx = list(range(33, 48)) + self.filter_nose = OneEuroFilter() + # eyebrow config + self.eyebrow_idx = list(range(48, 74)) + self.filter_eyebrow = OneEuroFilter() + # eye config + self.left_eye_idx = list(range(74, 96)) + self.filter_left_eye = OneEuroFilter() + self.right_eye_idx = list(range(96, 118)) + self.filter_right_eye = OneEuroFilter() + # mouth config + self.mouth_idx = list(range(118, 182)) + self.filter_mouth = OneEuroFilter() + # pupil config + self.left_pupil_idx = list(range(182, 202)) + self.filter_left_pupil = OneEuroFilter() + self.right_pupil_idx = list(range(202, 222)) + self.filter_right_pupil = OneEuroFilter() + self.prev_points = None + + def smooth(self, new_points, face_dis): + if self.prev_points is None: + self.prev_points = new_points.copy() + return new_points + dis = cult_dis(self.prev_points, new_points) / face_dis + smooth_points = new_points.copy() + + # smooth face + if np.mean(dis[self.face_down_idx]) < 0.005: + # stable + ratio_tmp = np.mean(dis[self.face_down_idx]) / 0.005 + fcmin_tmp = 0.05 * ratio_tmp + beta_tmp = 0.05 * ratio_tmp + smooth_points[self.face_idx] = self.filter_face(new_points[self.face_idx], self.prev_points[self.face_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + elif np.mean(dis[self.face_down_idx]) < 0.02: + # filter + ratio_tmp = (np.mean(dis[self.face_down_idx]) - 0.005) / (0.02 - 0.005) + fcmin_tmp = 0.05 + (0.3 - 0.05) * ratio_tmp + beta_tmp = 0.05 + (0.3 - 0.05) * ratio_tmp + smooth_points[self.face_idx] = self.filter_face(new_points[self.face_idx], self.prev_points[self.face_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + else: + # filter + smooth_points[self.face_idx] = self.filter_face(new_points[self.face_idx], self.prev_points[self.face_idx], + fcmin=0.3, beta=0.3) + # smooth nose + if np.mean(dis[self.nose_idx]) < 0.003: + # stable + ratio_tmp = np.mean(dis[self.nose_idx]) / 0.003 + fcmin_tmp = 0.03 * ratio_tmp + beta_tmp = 0.03 * ratio_tmp + smooth_points[self.nose_idx] = self.filter_nose(new_points[self.nose_idx], self.prev_points[self.nose_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + elif np.mean(dis[self.nose_idx]) < 0.02: + ratio_tmp = (np.mean(dis[self.nose_idx]) - 0.003) / (0.02 - 0.003) + fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + smooth_points[self.nose_idx] = self.filter_nose(new_points[self.nose_idx], self.prev_points[self.nose_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + else: + # filter + smooth_points[self.nose_idx] = self.filter_nose(new_points[self.nose_idx], self.prev_points[self.nose_idx], + fcmin=0.7, beta=0.7) + # smooth eyebrow + # print(np.mean(dis[self.eyebrow_idx])) + if np.mean(dis[self.eyebrow_idx]) < 0.003: + # stable + ratio_tmp = np.mean(dis[self.eyebrow_idx]) / 0.003 + fcmin_tmp = 0.02 * ratio_tmp + beta_tmp = 0.02 * ratio_tmp + smooth_points[self.eyebrow_idx] = self.filter_eyebrow(new_points[self.eyebrow_idx], + self.prev_points[self.eyebrow_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + elif np.mean(dis[self.eyebrow_idx]) < 0.02: + # filter + ratio_tmp = (np.mean(dis[self.eyebrow_idx]) - 0.003) / (0.02 - 0.003) + fcmin_tmp = 0.02 + (0.5 - 0.02) * ratio_tmp + beta_tmp = 0.02 + (0.5 - 0.02) * ratio_tmp + smooth_points[self.eyebrow_idx] = self.filter_eyebrow(new_points[self.eyebrow_idx], + self.prev_points[self.eyebrow_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + else: + # filter + smooth_points[self.eyebrow_idx] = self.filter_eyebrow(new_points[self.eyebrow_idx], + self.prev_points[self.eyebrow_idx], + fcmin=0.5, beta=0.5) + # smooth eye + if np.mean(dis[self.left_eye_idx]) < 0.003: + # stable + ratio_tmp = np.mean(dis[self.left_eye_idx]) / 0.003 + fcmin_tmp = 0.03 * ratio_tmp + beta_tmp = 0.03 * ratio_tmp + smooth_points[self.left_eye_idx] = self.filter_left_eye(new_points[self.left_eye_idx], + self.prev_points[self.left_eye_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + elif np.mean(dis[self.left_eye_idx]) < 0.02: + # filter + ratio_tmp = (np.mean(dis[self.left_eye_idx]) - 0.003) / (0.02 - 0.003) + fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + smooth_points[self.left_eye_idx] = self.filter_left_eye(new_points[self.left_eye_idx], + self.prev_points[self.left_eye_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + else: + # fast + smooth_points[self.left_eye_idx] = self.filter_left_eye(new_points[self.left_eye_idx], + self.prev_points[self.left_eye_idx], + fcmin=0.7, beta=0.7) + if np.mean(dis[self.right_eye_idx]) < 0.003: + # stable + ratio_tmp = np.mean(dis[self.right_eye_idx]) / 0.003 + fcmin_tmp = 0.03 * ratio_tmp + beta_tmp = 0.03 * ratio_tmp + smooth_points[self.right_eye_idx] = self.filter_right_eye(new_points[self.right_eye_idx], + self.prev_points[self.right_eye_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + elif np.mean(dis[self.right_eye_idx]) < 0.02: + # filter + ratio_tmp = (np.mean(dis[self.right_eye_idx]) - 0.003) / (0.02 - 0.003) + fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + smooth_points[self.right_eye_idx] = self.filter_right_eye(new_points[self.right_eye_idx], + self.prev_points[self.right_eye_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + else: + # fast + smooth_points[self.right_eye_idx] = self.filter_right_eye(new_points[self.right_eye_idx], + self.prev_points[self.right_eye_idx], + fcmin=0.7, beta=0.7) + + # smooth mouth + if np.mean(dis[self.mouth_idx]) < 0.003: + # stable + ratio_tmp = np.mean(dis[self.mouth_idx]) / 0.003 + fcmin_tmp = 0.05 * ratio_tmp + beta_tmp = 0.05 * ratio_tmp + smooth_points[self.mouth_idx] = self.filter_mouth(new_points[self.mouth_idx], + self.prev_points[self.mouth_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + elif np.mean(dis[self.mouth_idx]) < 0.02: + # filter + ratio_tmp = (np.mean(dis[self.mouth_idx]) - 0.003) / (0.02 - 0.003) + fcmin_tmp = 0.05 + (0.7 - 0.05) * ratio_tmp + beta_tmp = 0.05 + (0.7 - 0.05) * ratio_tmp + smooth_points[self.mouth_idx] = self.filter_mouth(new_points[self.mouth_idx], + self.prev_points[self.mouth_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + else: + # fast + smooth_points[self.mouth_idx] = self.filter_mouth(new_points[self.mouth_idx], + self.prev_points[self.mouth_idx], + fcmin=0.7, beta=0.7) + + # smooth pupil + if np.mean(dis[self.left_pupil_idx]) < 0.003: + # stable + ratio_tmp = np.mean(dis[self.left_pupil_idx]) / 0.003 + fcmin_tmp = 0.03 * ratio_tmp + beta_tmp = 0.03 * ratio_tmp + smooth_points[self.left_pupil_idx] = self.filter_left_pupil(new_points[self.left_pupil_idx], + self.prev_points[self.left_pupil_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + elif np.mean(dis[self.left_pupil_idx]) < 0.02: + # filter + ratio_tmp = (np.mean(dis[self.left_pupil_idx]) - 0.003) / (0.02 - 0.003) + fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + smooth_points[self.left_pupil_idx] = self.filter_left_pupil(new_points[self.left_pupil_idx], + self.prev_points[self.left_pupil_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + else: + # fast + smooth_points[self.left_pupil_idx] = self.filter_left_pupil(new_points[self.left_pupil_idx], + self.prev_points[self.left_pupil_idx], + fcmin=0.7, beta=0.7) + if np.mean(dis[self.right_pupil_idx]) < 0.003: + # stable + ratio_tmp = np.mean(dis[self.right_pupil_idx]) / 0.003 + fcmin_tmp = 0.03 * ratio_tmp + beta_tmp = 0.03 * ratio_tmp + smooth_points[self.right_pupil_idx] = self.filter_right_pupil(new_points[self.right_pupil_idx], + self.prev_points[self.right_pupil_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + elif np.mean(dis[self.right_pupil_idx]) < 0.02: + # filter + ratio_tmp = (np.mean(dis[self.right_pupil_idx]) - 0.003) / (0.02 - 0.003) + fcmin_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + beta_tmp = 0.03 + (0.7 - 0.03) * ratio_tmp + smooth_points[self.right_pupil_idx] = self.filter_right_pupil(new_points[self.right_pupil_idx], + self.prev_points[self.right_pupil_idx], + fcmin=fcmin_tmp, beta=beta_tmp) + else: + # fast + smooth_points[self.right_pupil_idx] = self.filter_right_pupil(new_points[self.right_pupil_idx], + self.prev_points[self.right_pupil_idx], + fcmin=0.7, beta=0.7) + + # update pre points + self.prev_points = smooth_points + return smooth_points + +class HelloCameraDemo(object): + def __init__(self, face_alignment_module, reset=False): + self.face_alignment_module = face_alignment_module + self.face_prob_th = 0.01 + self.min_face = 96 + self.face_image_size = self.face_alignment_module.face_image_size + self.trackingFaces = [] + self.reset = reset + + def reset_track(self): + self.trackingFaces = [] + + def forward(self, src_image, reset=False, pre_rect=None): + if self.reset or reset: + self.trackingFaces = [] + + if len(self.trackingFaces) == 0: + if pre_rect is not None: + detected_faces = [pre_rect] + else: + detected_faces, _, _ = self.face_alignment_module.face_detector.detect(src_image) + for face_rect in detected_faces: + new_tracking_object = { + 'face_rect': face_rect, + 'rotate_angle': 0.0, + 'pre_kpt_222': None, + 'face_dis': np.sqrt( + np.square((face_rect[2] - face_rect[0])) + np.square((face_rect[3] - face_rect[1]))), + 'smoother_222': Smoother222(), + 'prob': 0 + } + self.trackingFaces.append(new_tracking_object) + else: + detected_faces, _, _ = self.face_alignment_module.face_detector.detect(src_image) + for face_rect in detected_faces: + new_tracking_object = { + 'face_rect': face_rect, + 'rotate_angle': 0.0, + 'pre_kpt_222': None, + 'face_dis': np.sqrt( + np.square((face_rect[2] - face_rect[0])) + np.square((face_rect[3] - face_rect[1]))), + 'smoother_222': Smoother222(), + 'prob': 0 + } + self.trackingFaces.append(new_tracking_object) + + delete_idx_list = [] + for face_idx, tracking_face in enumerate(self.trackingFaces): + if tracking_face['pre_kpt_222'] is not None: + result_dict = self.face_alignment_module.forward(src_image, pre_pts=tracking_face['pre_kpt_222'], + iterations=3) + else: + result_dict = self.face_alignment_module.forward(src_image, face_box=tracking_face['face_rect'], + iterations=3) + + if result_dict['prob'] < self.face_prob_th: + if not face_idx in delete_idx_list: + delete_idx_list.append(face_idx) + continue + + landmarks_final = tracking_face['smoother_222'].smooth(result_dict['pt222'], tracking_face['face_dis']) + tracking_face['pre_kpt_222'] = landmarks_final + + left_eye_corner = landmarks_final[74] + right_eye_corner = landmarks_final[96] + + radian = np.arctan2(right_eye_corner[1] - left_eye_corner[1], + right_eye_corner[0] - left_eye_corner[0] + 0.00000001) + rotate_angle = np.rad2deg(radian) + face_x_min, face_x_max = np.min(landmarks_final[:, 0]), np.max(landmarks_final[:, 0]) + face_y_min, face_y_max = np.min(landmarks_final[:, 1]), np.max(landmarks_final[:, 1]) + face_bbox = [face_x_min, face_y_min, face_x_max, face_y_max] + face_dis = np.linalg.norm(landmarks_final[0] - landmarks_final[32]) + + if face_x_max - face_x_min < self.min_face or face_y_max - face_y_min < self.min_face: + if not face_idx in delete_idx_list: + delete_idx_list.append(face_idx) + + euler_pred = result_dict["euler_rad"] + pitch = np.rad2deg(euler_pred[0]) + yaw = np.rad2deg(euler_pred[1]) + roll = np.rad2deg(euler_pred[2]) + # print("pitch, yaw, roll", pitch, yaw, roll) + + # one filter model + max_euler = abs(pitch) + (abs(yaw)*0.6) + face_dis *= (1.0 + max_euler / 18.0) + + # two filter model + tracking_face['face_rect'] = face_bbox + tracking_face['rotate_angle'] = rotate_angle + tracking_face['face_dis'] = face_dis + tracking_face['prob'] = result_dict['prob'] + tracking_face['pitch'] = pitch + tracking_face['yaw'] = yaw + tracking_face['roll'] = roll + tracking_face['euler_rad'] = result_dict["euler_rad"] + + if len(self.trackingFaces) > 1: + for face_idx, tracking_face_target in enumerate(self.trackingFaces): + if face_idx in delete_idx_list: + continue + for idx, tracking_face in enumerate(self.trackingFaces): + if idx in delete_idx_list: + continue + if face_idx == idx: continue + iou_temp = self.count_iou(tracking_face_target['face_rect'], tracking_face['face_rect']) + # prog 2 + if iou_temp > 0.12: + if self.area(tracking_face_target['face_rect']) - self.area(tracking_face['face_rect']) < 0: + if not face_idx in delete_idx_list: + delete_idx_list.append(face_idx) + else: + if not idx in delete_idx_list: + delete_idx_list.append(idx) + + + idx_offset = 0 + for delete_idx in sorted(delete_idx_list): + self.trackingFaces.pop(delete_idx - idx_offset) + idx_offset += 1 + + return self.trackingFaces + + def count_iou(self, boxA, boxB): + # determine the (x, y)-coordinates of the intersection rectangle + xA = max(boxA[0], boxB[0]) + yA = max(boxA[1], boxB[1]) + xB = min(boxA[2], boxB[2]) + yB = min(boxA[3], boxB[3]) + + # compute the area of intersection rectangle + interArea = abs(max((xB - xA, 0)) * max((yB - yA), 0)) + if interArea == 0: + return 0 + # compute the area of both the prediction and ground-truth + # rectangles + boxAArea = abs((boxA[2] - boxA[0]) * (boxA[3] - boxA[1])) + boxBArea = abs((boxB[2] - boxB[0]) * (boxB[3] - boxB[1])) + + # compute the intersection over union by taking the intersection + # area and dividing it by the sum of prediction + ground-truth + # areas - the interesection area + iou = interArea / float(boxAArea + boxBArea - interArea) + + # return the intersection over union value + return iou + + def area(self, bbox): + w = bbox[3]-bbox[1] + h = bbox[2]-bbox[0] + return w * h + diff --git a/hellomeme/tools/hello_face_alignment.py b/hellomeme/tools/hello_face_alignment.py new file mode 100644 index 0000000..bd9b527 --- /dev/null +++ b/hellomeme/tools/hello_face_alignment.py @@ -0,0 +1,93 @@ +""" +@File : test.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 11/1/2024 +@Desc : Created by Shengjie Wu (wu.shengjie@immomo.com) +""" + +import cv2 +import numpy as np +from .hello_face_det import HelloFaceDet +from .utils import get_warp_mat_bbox, get_warp_mat_bbox_by_gt_pts_float, transform_points +from .utils import create_onnx_session +from huggingface_hub import hf_hub_download + +class HelloFaceAlignment(object): + def __init__(self, gpu_id=None): + expand_ratio = 0.15 + self.face_alignment_net_222 = ( + create_onnx_session(hf_hub_download('songkey/hello_group_facemodel', filename='hello_face_landmark.onnx'), gpu_id=gpu_id)) + self.onnx_input_name_222 = self.face_alignment_net_222.get_inputs()[0].name + self.onnx_output_name_222 = [output.name for output in self.face_alignment_net_222.get_outputs()] + self.face_image_size = 128 + self.face_detector = HelloFaceDet(hf_hub_download('songkey/hello_group_facemodel', filename='hello_face_det.onnx'), gpu_id=gpu_id) + self.expand_ratio = expand_ratio + + def onnx_infer(self, input_uint8): + assert input_uint8.shape[0] == input_uint8.shape[1] == self.face_image_size + onnx_input = input_uint8.transpose((2, 0, 1)).astype(np.float32)[np.newaxis, :, :, :] / 255.0 + landmark, euler, prob = self.face_alignment_net_222.run(self.onnx_output_name_222, + {self.onnx_input_name_222: onnx_input}) + + landmark = np.reshape(landmark[0], (2, -1)).transpose((1, 0)) * self.face_image_size + left_eye_corner = landmark[74] + right_eye_corner = landmark[96] + radian = np.arctan2(right_eye_corner[1] - left_eye_corner[1], + right_eye_corner[0] - left_eye_corner[0] + 0.00000001) + euler_rad = np.array([euler[0, 0], euler[0, 1], radian], dtype=np.float32) + prob = prob[0] + + return landmark, euler_rad, prob + + def forward(self, src_image, face_box=None, pre_pts=None, iterations=3): + if pre_pts is None: + if face_box is None: + # Detect max size face + bounding_boxes, _, score = self.face_detector.detect(src_image) + print("facedet score", score) + if len(bounding_boxes) == 0: + return None + bbox = np.zeros(4, dtype=np.float32) + if len(bounding_boxes) >= 1: + max_area = 0.0 + for each_bbox in bounding_boxes: + area = (each_bbox[2] - each_bbox[0]) * (each_bbox[3] - each_bbox[1]) + if area > max_area: + bbox[:4] = each_bbox[:4] + max_area = area + else: + bbox = bounding_boxes[0, :4] + else: + bbox = face_box.copy() + M_Face = get_warp_mat_bbox(bbox, 0, self.face_image_size, expand_ratio=self.expand_ratio) + else: + left_eye_corner = pre_pts[74] + right_eye_corner = pre_pts[96] + + radian = np.arctan2(right_eye_corner[1] - left_eye_corner[1], + right_eye_corner[0] - left_eye_corner[0] + 0.00000001) + M_Face = get_warp_mat_bbox_by_gt_pts_float(pre_pts, np.rad2deg(radian), self.face_image_size, + expand_ratio=self.expand_ratio) + + face_input = cv2.warpAffine(src_image, M_Face, (self.face_image_size, self.face_image_size)) + landmarks, euler, prob = self.onnx_infer(face_input) + landmarks = transform_points(landmarks, M_Face, invert=True) + + # Repeat + for i in range(iterations - 1): + M_Face = get_warp_mat_bbox_by_gt_pts_float(landmarks, np.rad2deg(euler[2]), self.face_image_size, + expand_ratio=self.expand_ratio) + face_input = cv2.warpAffine(src_image, M_Face, (self.face_image_size, self.face_image_size)) + landmarks, euler, prob = self.onnx_infer(face_input) + landmarks = transform_points(landmarks, M_Face, invert=True) + + return_dict = { + "pt222": landmarks, + "euler_rad": euler, + "prob": prob, + "M_Face": M_Face, + "face_input": face_input + } + + return return_dict diff --git a/hellomeme/tools/hello_face_det.py b/hellomeme/tools/hello_face_det.py new file mode 100644 index 0000000..f11f708 --- /dev/null +++ b/hellomeme/tools/hello_face_det.py @@ -0,0 +1,326 @@ +""" +@File : test.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 11/1/2024 +@Desc : Created by Zemin An (an.zemin@hellogroup.com) +""" + +from abc import ABCMeta, abstractmethod +import cv2 +import numpy as np +from scipy.special import softmax +import os.path as osp +from .utils import create_onnx_session + +songkey_weights_dir = 'pretrained_models' + +_COLORS = ( + np.array( + [ + 0.000, + 0.447, + 0.741, + ] + ) + .astype(np.float32) + .reshape(-1, 3) +) + +def get_resize_matrix(raw_shape, dst_shape, keep_ratio): + """ + Get resize matrix for resizing raw img to input size + :param raw_shape: (width, height) of raw image + :param dst_shape: (width, height) of input image + :param keep_ratio: whether keep original ratio + :return: 3x3 Matrix + """ + r_w, r_h = raw_shape + d_w, d_h = dst_shape + Rs = np.eye(3) + if keep_ratio: + C = np.eye(3) + C[0, 2] = -r_w / 2 + C[1, 2] = -r_h / 2 + + if r_w / r_h < d_w / d_h: + ratio = d_h / r_h + else: + ratio = d_w / r_w + Rs[0, 0] *= ratio + Rs[1, 1] *= ratio + + T = np.eye(3) + T[0, 2] = 0.5 * d_w + T[1, 2] = 0.5 * d_h + return T @ Rs @ C + else: + Rs[0, 0] *= d_w / r_w + Rs[1, 1] *= d_h / r_h + return Rs + +def warp_boxes(boxes, M, width, height): + """Apply transform to boxes + Copy from nanodet/data/transform/warp.py + """ + n = len(boxes) + if n: + # warp points + xy = np.ones((n * 4, 3)) + xy[:, :2] = boxes[:, [0, 1, 2, 3, 0, 3, 2, 1]].reshape( + n * 4, 2 + ) # x1y1, x2y2, x1y2, x2y1 + xy = xy @ M.T # transform + xy = (xy[:, :2] / xy[:, 2:3]).reshape(n, 8) # rescale + # create new boxes + x = xy[:, [0, 2, 4, 6]] + y = xy[:, [1, 3, 5, 7]] + xy = np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T + # clip boxes + xy[:, [0, 2]] = xy[:, [0, 2]].clip(0, width) + xy[:, [1, 3]] = xy[:, [1, 3]].clip(0, height) + return xy.astype(np.float32) + else: + return boxes + +def overlay_bbox_cv(img, all_box, class_names): + """Draw result boxes + Copy from nanodet/util/visualization.py + """ + # all_box array of [label, x0, y0, x1, y1, score] + all_box.sort(key=lambda v: v[5]) + for box in all_box: + label, x0, y0, x1, y1, score = box + # color = self.cmap(i)[:3] + color = (_COLORS[label] * 255).astype(np.uint8).tolist() + text = "{}:{:.1f}%".format(class_names[label], score * 100) + txt_color = (0, 0, 0) if np.mean(_COLORS[label]) > 0.5 else (255, 255, 255) + font = cv2.FONT_HERSHEY_SIMPLEX + txt_size = cv2.getTextSize(text, font, 0.5, 2)[0] + cv2.rectangle(img, (x0, y0), (x1, y1), color, 2) + + cv2.rectangle( + img, + (x0, y0 - txt_size[1] - 1), + (x0 + txt_size[0] + txt_size[1], y0 - 1), + color, + -1, + ) + cv2.putText(img, text, (x0, y0 - 1), font, 0.5, txt_color, thickness=1) + return img + +def hard_nms(box_scores, iou_threshold, top_k=-1, candidate_size=200): + """ + + Args: + box_scores (N, 5): boxes in corner-form and probabilities. + iou_threshold: intersection over union threshold. + top_k: keep top_k results. If k <= 0, keep all the results. + candidate_size: only consider the candidates with the highest scores. + Returns: + picked: a list of indexes of the kept boxes + """ + scores = box_scores[:, -1] + boxes = box_scores[:, :-1] + picked = [] + # _, indexes = scores.sort(descending=True) + indexes = np.argsort(scores) + # indexes = indexes[:candidate_size] + indexes = indexes[-candidate_size:] + while len(indexes) > 0: + # current = indexes[0] + current = indexes[-1] + picked.append(current) + if 0 < top_k == len(picked) or len(indexes) == 1: + break + current_box = boxes[current, :] + # indexes = indexes[1:] + indexes = indexes[:-1] + rest_boxes = boxes[indexes, :] + iou = iou_of( + rest_boxes, + np.expand_dims(current_box, axis=0), + ) + indexes = indexes[iou <= iou_threshold] + + return box_scores[picked, :] + + +def iou_of(boxes0, boxes1, eps=1e-5): + """Return intersection-over-union (Jaccard index) of boxes. + + Args: + boxes0 (N, 4): ground truth boxes. + boxes1 (N or 1, 4): predicted boxes. + eps: a small number to avoid 0 as denominator. + Returns: + iou (N): IoU values. + """ + overlap_left_top = np.maximum(boxes0[..., :2], boxes1[..., :2]) + overlap_right_bottom = np.minimum(boxes0[..., 2:], boxes1[..., 2:]) + + overlap_area = area_of(overlap_left_top, overlap_right_bottom) + area0 = area_of(boxes0[..., :2], boxes0[..., 2:]) + area1 = area_of(boxes1[..., :2], boxes1[..., 2:]) + return overlap_area / (area0 + area1 - overlap_area + eps) + + +def area_of(left_top, right_bottom): + """Compute the areas of rectangles given two corners. + + Args: + left_top (N, 2): left top corner. + right_bottom (N, 2): right bottom corner. + + Returns: + area (N): return the area. + """ + hw = np.clip(right_bottom - left_top, 0.0, None) + return hw[..., 0] * hw[..., 1] + + +class NanoDetABC(metaclass=ABCMeta): + def __init__( + self, + input_shape=[272, 160], + reg_max=7, + strides=[8, 16, 32], + prob_threshold=0.4, + iou_threshold=0.3, + num_candidate=1000, + top_k=-1, + class_names=["face"] + ): + self.strides = strides + self.input_shape = input_shape + self.reg_max = reg_max + self.prob_threshold = prob_threshold + self.iou_threshold = iou_threshold + self.num_candidate = num_candidate + self.top_k = top_k + self.img_mean = [103.53, 116.28, 123.675] + self.img_std = [57.375, 57.12, 58.395] + self.input_size = (self.input_shape[1], self.input_shape[0]) + self.class_names = class_names + self.num_classes = len(self.class_names) + + def preprocess(self, img): + # resize image + ResizeM = get_resize_matrix((img.shape[1], img.shape[0]), self.input_size, True) + img_resize = cv2.warpPerspective(img, ResizeM, dsize=self.input_size) + + # normalize image + img_input = img_resize.astype(np.float32) / 255 + img_mean = np.array(self.img_mean, dtype=np.float32).reshape(1, 1, 3) / 255 + img_std = np.array(self.img_std, dtype=np.float32).reshape(1, 1, 3) / 255 + img_input = (img_input - img_mean) / img_std + + # expand dims + img_input = np.transpose(img_input, [2, 0, 1]) + img_input = np.expand_dims(img_input, axis=0) + return img_input, ResizeM + + def postprocess(self, scores, raw_boxes, ResizeM, raw_shape): + # generate centers + decode_boxes = [] + select_scores = [] + for stride, box_distribute, score in zip(self.strides, raw_boxes, scores): + # centers + fm_h = self.input_shape[0] / stride + fm_w = self.input_shape[1] / stride + + h_range = np.arange(fm_h) + w_range = np.arange(fm_w) + ww, hh = np.meshgrid(w_range, h_range) + + ct_row = hh.flatten() * stride + ct_col = ww.flatten() * stride + + center = np.stack((ct_col, ct_row, ct_col, ct_row), axis=1) + + # box distribution to distance + reg_range = np.arange(self.reg_max + 1) + box_distance = box_distribute.reshape((-1, self.reg_max + 1)) + box_distance = softmax(box_distance, axis=1) + box_distance = box_distance * np.expand_dims(reg_range, axis=0) + box_distance = np.sum(box_distance, axis=1).reshape((-1, 4)) + box_distance = box_distance * stride + + # top K candidate + topk_idx = np.argsort(score.max(axis=1))[::-1] + topk_idx = topk_idx[: self.num_candidate] + center = center[topk_idx] + score = score[topk_idx] + box_distance = box_distance[topk_idx] + + # decode box + decode_box = center + [-1, -1, 1, 1] * box_distance + + select_scores.append(score) + decode_boxes.append(decode_box) + + # nms + bboxes = np.concatenate(decode_boxes, axis=0) + confidences = np.concatenate(select_scores, axis=0) + picked_box_probs = [] + picked_labels = [] + for class_index in range(0, confidences.shape[1]): + probs = confidences[:, class_index] + mask = probs > self.prob_threshold + probs = probs[mask] + if probs.shape[0] == 0: + continue + subset_boxes = bboxes[mask, :] + box_probs = np.concatenate([subset_boxes, probs.reshape(-1, 1)], axis=1) + box_probs = hard_nms( + box_probs, + iou_threshold=self.iou_threshold, + top_k=self.top_k, + ) + picked_box_probs.append(box_probs) + picked_labels.extend([class_index] * box_probs.shape[0]) + if not picked_box_probs: + return np.array([]), np.array([]), np.array([]) + picked_box_probs = np.concatenate(picked_box_probs) + + # resize output boxes + picked_box_probs[:, :4] = warp_boxes( + picked_box_probs[:, :4], np.linalg.inv(ResizeM), raw_shape[1], raw_shape[0] + ) + return ( + picked_box_probs[:, :4].astype(np.int32), + np.array(picked_labels), + picked_box_probs[:, 4], + ) + + @abstractmethod + def infer_image(self, img_input): + pass + + def detect(self, img): + raw_shape = img.shape + img_input, ResizeM = self.preprocess(img) + scores, raw_boxes = self.infer_image(img_input) + if scores[0].ndim == 1: # handling num_classes=1 case + scores = [x[:, None] for x in scores] + bbox, label, score = self.postprocess(scores, raw_boxes, ResizeM, raw_shape) + + return bbox, label, score + +class HelloFaceDet(NanoDetABC): + def __init__(self, model_path=osp.join(songkey_weights_dir, 'face/nanodet_humandet_320-192_220302_model_20220315_test3.onnx'), gpu_id=None, *args, **kwargs): + super(HelloFaceDet, self).__init__(*args, **kwargs) + # print("Using ONNX as inference backend") + # print(f"Using weight: {model_path}") + + # load model + self.model_path = model_path + self.ort_session = create_onnx_session(model_path, gpu_id=gpu_id) + self.input_name = self.ort_session.get_inputs()[0].name + + def infer_image(self, img_input): + inference_results = self.ort_session.run(None, {self.input_name: img_input}) + + scores = [np.squeeze(x) for x in inference_results[:3]] + raw_boxes = [np.squeeze(x) for x in inference_results[3:]] + return scores, raw_boxes diff --git a/hellomeme/tools/utils.py b/hellomeme/tools/utils.py new file mode 100644 index 0000000..4281d9d --- /dev/null +++ b/hellomeme/tools/utils.py @@ -0,0 +1,118 @@ +# coding: utf-8 + +# @File : utils.py +# @Author : Songkey +# @Email : songkey@pku.edu.cn +# @Date : 8/18/2024 +# @Desc : + +import onnx, onnxruntime +import time +import cv2 +import numpy as np +import math + +def create_onnx_session(onnx_path, gpu_id=None)->onnxruntime.InferenceSession: + start = time.perf_counter() + onnx_model = onnx.load(onnx_path) + onnx.checker.check_model(onnx_model) + providers = [ + ('CUDAExecutionProvider', { + 'device_id': int(gpu_id), + 'arena_extend_strategy': 'kNextPowerOfTwo', + #'cuda_mem_limit': 5 * 1024 * 1024 * 1024, + 'cudnn_conv_algo_search': 'EXHAUSTIVE', + 'do_copy_in_default_stream': True, + }), + 'CPUExecutionProvider', + ] if (gpu_id is not None and gpu_id >= 0) else ['CPUExecutionProvider'] + + sess = onnxruntime.InferenceSession(onnx_path, providers=providers) + print('create onnx session cost: {:.3f}s. {}'.format(time.perf_counter() - start, onnx_path)) + return sess + +def smoothing_factor(t_e, cutoff): + r = 2 * math.pi * cutoff * t_e + return r / (r + 1) + +def exponential_smoothing(a, x, x_prev): + return a * x + (1 - a) * x_prev + +class OneEuroFilter: + def __init__(self, dx0=0.0, d_cutoff=1.0): + """Initialize the one euro filter.""" + # self.min_cutoff = float(min_cutoff) + # self.beta = float(beta) + self.d_cutoff = float(d_cutoff) + self.dx_prev = float(dx0) + # self.t_e = fcmin + + def __call__(self, x, x_prev, fcmin=1.0, min_cutoff=1.0, beta=0.0): + if x_prev is None: + return x + # t_e = 1 + a_d = smoothing_factor(fcmin, self.d_cutoff) + dx = (x - x_prev) / fcmin + dx_hat = exponential_smoothing(a_d, dx, self.dx_prev) + cutoff = min_cutoff + beta * abs(dx_hat) + a = smoothing_factor(fcmin, cutoff) + x_hat = exponential_smoothing(a, x, x_prev) + self.dx_prev = dx_hat + return x_hat + +def get_warp_mat_bbox(face_bbox, base_angle, dst_size=128, expand_ratio=0.15, aug_angle=0.0, aug_scale=1.0): + face_x_min, face_y_min, face_x_max, face_y_max = face_bbox + face_x_center = (face_x_min + face_x_max) / 2 + face_y_center = (face_y_min + face_y_max) / 2 + face_width = face_x_max - face_x_min + face_height = face_y_max - face_y_min + scale = dst_size / max(face_width, face_height) * (1 - expand_ratio) * aug_scale + M = cv2.getRotationMatrix2D((face_x_center, face_y_center), angle=base_angle + aug_angle, scale=scale) + offset = [dst_size / 2 - face_x_center, dst_size / 2 - face_y_center] + M[:, 2] += offset + return M + +def transform_points(points, mat, invert=False): + if invert: + mat = cv2.invertAffineTransform(mat) + points = np.expand_dims(points, axis=1) + points = cv2.transform(points, mat, points.shape) + points = np.squeeze(points) + return points + +def get_warp_mat_bbox_by_gt_pts_float(gt_pts, base_angle=0.0, dst_size=128, expand_ratio=0.15, return_info=False): + # step 1 + face_x_min, face_x_max = np.min(gt_pts[:, 0]), np.max(gt_pts[:, 0]) + face_y_min, face_y_max = np.min(gt_pts[:, 1]), np.max(gt_pts[:, 1]) + face_x_center = (face_x_min + face_x_max) / 2 + face_y_center = (face_y_min + face_y_max) / 2 + M_step_1 = cv2.getRotationMatrix2D((face_x_center, face_y_center), angle=base_angle, scale=1.0) + pts_step_1 = transform_points(gt_pts, M_step_1) + face_x_min_step_1, face_x_max_step_1 = np.min(pts_step_1[:, 0]), np.max(pts_step_1[:, 0]) + face_y_min_step_1, face_y_max_step_1 = np.min(pts_step_1[:, 1]), np.max(pts_step_1[:, 1]) + # step 2 + face_width = face_x_max_step_1 - face_x_min_step_1 + face_height = face_y_max_step_1 - face_y_min_step_1 + scale = dst_size / max(face_width, face_height) * (1 - expand_ratio) + M_step_2 = cv2.getRotationMatrix2D((face_x_center, face_y_center), angle=base_angle, scale=scale) + pts_step_2 = transform_points(gt_pts, M_step_2) + face_x_min_step_2, face_x_max_step_2 = np.min(pts_step_2[:, 0]), np.max(pts_step_2[:, 0]) + face_y_min_step_2, face_y_max_step_2 = np.min(pts_step_2[:, 1]), np.max(pts_step_2[:, 1]) + face_x_center_step_2 = (face_x_min_step_2 + face_x_max_step_2) / 2 + face_y_center_step_2 = (face_y_min_step_2 + face_y_max_step_2) / 2 + + M = cv2.getRotationMatrix2D((face_x_center, face_y_center), angle=base_angle, scale=scale) + offset = [dst_size / 2 - face_x_center_step_2, dst_size / 2 - face_y_center_step_2] + M[:, 2] += offset + + if not return_info: + return M + else: + transform_info = { + "M": M, + "center_x": face_x_center, + "center_y": face_y_center, + "rotate_angle": base_angle, + "scale": scale + } + return transform_info diff --git a/hellomeme/utils.py b/hellomeme/utils.py new file mode 100644 index 0000000..20f3ca8 --- /dev/null +++ b/hellomeme/utils.py @@ -0,0 +1,379 @@ +# coding: utf-8 + +""" +@File : utils.py +@Author : Songkey +@Email : songkey@pku.edu.cn +@Date : 8/28/2024 +@Desc : +""" + +import os +import cv2 +import numpy as np +import torch +from PIL import Image +import subprocess +from einops import rearrange +from .tools.utils import transform_points +from diffusers.pipelines.stable_diffusion.convert_from_ckpt import convert_ldm_unet_checkpoint + +from safetensors import safe_open + +def load_safetensors(model_path): + tensors = {} + with safe_open(model_path, framework="pt", device=0) as f: + for k in f.keys(): + tensors[k] = f.get_tensor(k) # loads the full tensor given a key + return tensors + +def load_unet_from_safetensors(safetensors_path, unet_config): + original_stats = load_safetensors(safetensors_path) + return convert_ldm_unet_checkpoint(original_stats, unet_config) + +def image_preprocess(np_bgr, size, dtype=torch.float32): + img_np = cv2.resize(np_bgr, size) + return np_bgr_to_tensor(img_np, dtype) + +def np_bgr_to_tensor(img_np, dtype): + img_rgb = cv2.cvtColor(img_np, cv2.COLOR_BGR2RGB) / 255. * 2 - 1 + return torch.tensor(img_rgb).permute(2, 0, 1).to(dtype=dtype) + +def tensor_to_np_rgb(img_np): + img_rgb = np.clip((img_np.permute(1, 2, 0).detach().cpu().numpy() + 1) / 2 * 255, 0, 255) + return img_rgb.astype(np.uint8) + +def clip_preprocess_from_bgr(image_bgr, dtype=torch.float32): + temp_image = cv2.resize(image_bgr, (224, 224), interpolation=cv2.INTER_CUBIC) + temp_image = cv2.cvtColor(temp_image, cv2.COLOR_BGR2RGB) + temp_image_np = temp_image * 0.00392156862745098 + temp_image_np = (temp_image_np - [0.48145466, 0.4578275, 0.40821073]) / [0.26862954, 0.26130258, 0.27577711] + temp_image_np = np.transpose(temp_image_np, (2, 0, 1)) + return torch.tensor(temp_image_np, dtype=dtype) + +def clip_preprocess_to_bgr(image_tensor): + image_tensor_np = image_tensor.detach().cpu().numpy().astype(float) + image_tensor_np = np.transpose(image_tensor_np, (1, 2, 0)) + image_tensor_np = (image_tensor_np * [0.26862954, 0.26130258, 0.27577711]) + [0.48145466, 0.4578275, 0.40821073] + image_tensor_np = np.clip(image_tensor_np / 0.00392156862745098, 0, 255).astype(np.uint8) + return cv2.cvtColor(image_tensor_np, cv2.COLOR_BGR2RGB) + +def clip_preprocess_from_pil(image_pil, dtype): + temp_image = image_pil.resize((224, 224), resample=Image.BICUBIC) + temp_image_np = np.asarray(temp_image) * 0.00392156862745098 + temp_image_np = (temp_image_np - [0.48145466, 0.4578275, 0.40821073]) / [0.26862954, 0.26130258, 0.27577711] + temp_image_np = np.transpose(temp_image_np, (2, 0, 1)) + return torch.tensor(temp_image_np, dtype=dtype) + +def draw_skl_by_rect(save_size, rect): + rect = rect.astype(np.int32) + ret_img = np.zeros((save_size, save_size, 3), dtype=np.uint8) + # cv2.rectangle(ret_img, rect[0], rect[2], (255, 255, 255), -1) + cv2.line(ret_img, rect[0], rect[1], (0, 255, 0), 15) + cv2.line(ret_img, rect[1], rect[2], (255, 0, 0), 15) + cv2.line(ret_img, rect[2], rect[3], (0, 0, 255), 15) + cv2.line(ret_img, rect[3], rect[0], (255, 0, 255), 15) + return ret_img + +def get_face_part_1frame(image, parts, save_size=256): + ret_list = [] + for part_landmarks in parts: + part_tls, part_brs = np.min(part_landmarks, axis=0), np.max(part_landmarks, axis=0) + center = (part_tls + part_brs) * 0.5 + wh = max(part_brs - part_tls) * 1.2 + + scale = save_size / wh + + M = cv2.getRotationMatrix2D(center, 0, scale) + M[:, 2] += save_size * 0.5 - center + + part = cv2.warpAffine(image, M, (save_size, save_size)) + ret_list.append(part) + return np.stack(ret_list, axis=0) + +face_parts_regions = [ + [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94], + [61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116], + [118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148], +] +face_region = [33, 74, 96, 133, 149] +face_temp = np.array([[0.50951539, 0.40128625], [0.29259476, 0.40203411], [0.72196212, 0.40857479], [0.63420714, 0.71678643], [0.38437035, 0.71723224]]) + +def get_face_parts(images, landmarks, save_size=256): + H, W = images[0].shape[:2] + warped_imgs = [] + warped_landmarks = [] + + for landmark, frame in zip(landmarks, images): + src_landmarks = landmark[face_region, :] + dst_landmarks = face_temp * (W, H) + + M = umeyama(src_landmarks, dst_landmarks, True)[0:2] + warped_img = cv2.warpAffine(frame, M, (W, H)) + + warped_landmark = transform_points(landmark.astype(np.float32), M)[:,:2] + + # for x, y in warped_landmark: + # warped_img = cv2.circle(warped_img, (int(x), int(y)), 1, (255, 255, 0), -1) + # for x, y in dst_landmarks: + # warped_img = cv2.circle(warped_img, (int(x), int(y)), 1, (0, 255, 0), -1) + # cv2.imshow('warped_img', warped_img) + # cv2.waitKey() + + warped_imgs.append(warped_img) + warped_landmarks.append(warped_landmark) + warped_imgs = np.stack(warped_imgs, axis=0) + warped_landmarks = np.stack(warped_landmarks, axis=0) + + ret_list = [] + for region in face_parts_regions: + part_landmarks = warped_landmarks[:, region, :] + part_tls, part_brs = np.min(np.min(part_landmarks, axis=1), axis=0), np.max(np.max(part_landmarks, axis=1), axis=0) + center = (part_tls + part_brs) * 0.5 + wh = max(part_brs - part_tls) * 1.2 + + scale = save_size / wh + + M = cv2.getRotationMatrix2D(center, 0, scale) + M[:, 2] += save_size * 0.5 - center + tmp_part_list = [] + for warped_img in warped_imgs: + part = cv2.warpAffine(warped_img, M, (save_size, save_size)) + # cv2.imshow('warped_img', warped_img) + # cv2.imshow('part', part) + # cv2.waitKey() + tmp_part_list.append(part) + ret_list.append(np.stack(tmp_part_list, axis=0)) + return np.stack(ret_list, axis=0).transpose(1, 0, 2, 3, 4) + +def get_face_params(h3dmm, harkit_bs, frames, landmarks, save_size=512, align=True): + face_parts = [] + for tmp_face_parts_list in get_face_parts(frames, landmarks, save_size=256): + tmp_face_parts_list = [cv2.GaussianBlur(x, (9, 9), 0) for x in tmp_face_parts_list] + face_parts.append(tmp_face_parts_list) + + if align: + new_frames, new_landmarks = crop_and_resize(np.stack(frames, axis=0), np.stack(landmarks, axis=0), + save_size=save_size, crop=False) + else: + new_frames, new_landmarks = frames, landmarks + + rot_list = [] + trans_list = [] + arkit_bs_list = [] + for landmark, frame in zip(new_landmarks, new_frames): + drive_rot, drive_trans = h3dmm.forward_params(frame, landmark) + rot_list.append(drive_rot) + trans_list.append(drive_trans) + arkit_bs_list.append(harkit_bs.forward(frame, landmark)) + drive_coeff = torch.from_numpy(np.stack(arkit_bs_list, axis=0)) + return face_parts, drive_coeff, rot_list, trans_list + +def face_params_to_tensor(clip_encoder, h3dmm, face_parts, drive_rot, drive_trans, ref_trans, save_size=512, trans_ratio=0.0): + face_parts_list = [] + for tmp_face_parts_list in face_parts: + tmp_face_parts_list = [clip_preprocess_from_bgr(x).unsqueeze(1) for x in tmp_face_parts_list] + face_parts_list.append(torch.cat(tmp_face_parts_list, dim=1)) + face_parts = torch.stack(face_parts_list, dim=2) + + face_parts_tensor = rearrange(face_parts, "c f p h w -> (f p) c h w") + face_parts_embedding = clip_encoder(face_parts_tensor.to(device=clip_encoder.device, dtype=clip_encoder.dtype)).image_embeds + face_parts_embedding = rearrange(face_parts_embedding, + "(f p) c -> f p c", + f=face_parts.size(2)).cpu() + + control_list = [] + control_show_list = [] + for rot, trans in zip(drive_rot, drive_trans): + rect = h3dmm.get_project_points_rect(rot, ref_trans + (trans - drive_trans[0]) * trans_ratio, save_size, save_size) + control_heatmap = draw_skl_by_rect(save_size, rect) + + control_show_list.append(control_heatmap.copy()) + control_heatmap = image_preprocess(control_heatmap, (save_size, save_size)) + control_list.append(control_heatmap) + control_heatmaps = torch.stack(control_list, dim=1) + + return face_parts_embedding, control_heatmaps + +def det_landmarks(face_aligner, frame_list, save_size=(512, 512), reset=False): + rect_list = [] + new_frame_list = [] + for frame in frame_list: + frame = cv2.resize(frame, save_size) + faces = face_aligner.forward(frame, reset=reset) + if len(faces) > 0: + face = sorted(faces, key=lambda x: (x['face_rect'][2] - x['face_rect'][0]) * ( + x['face_rect'][3] - x['face_rect'][1]))[-1] + rect_list.append(face['face_rect']) + new_frame_list.append(frame) + + face_aligner.reset_track() + save_frame_list = [] + save_landmark_list = [] + for frame, rect in zip(new_frame_list, rect_list): + faces = face_aligner.forward(frame, pre_rect=rect, reset=reset) + if len(faces) > 0: + face = sorted(faces, key=lambda x: (x['face_rect'][2] - x['face_rect'][0]) * ( + x['face_rect'][3] - x['face_rect'][1]))[-1] + landmarks = face['pre_kpt_222'] + save_frame_list.append(frame) + save_landmark_list.append(landmarks) + save_landmark_list = np.stack(save_landmark_list, axis=0).astype(np.float16) + + face_aligner.reset_track() + return save_frame_list, save_landmark_list + +def get_drive_params(face_aligner, h3dmm, harkit_bs, frame_list, save_size, align): + frame_num = len(frame_list) + frame_list, landmark_list = det_landmarks(face_aligner, frame_list, save_size=(512, 512), reset=False) + assert len(frame_list) == frame_num + return get_face_params(h3dmm, harkit_bs, frame_list, landmark_list, save_size=save_size, align=align) + +def load_data_list(data_dir, post_fix='.pickle;.txt'): + post_fixs = post_fix.split(';') + ret_list = [] + for root, dirnames, filenames in os.walk(data_dir): + for name in filenames: + if os.path.splitext(name)[1].lower() in post_fixs and not name.startswith('.'): + data_path = os.path.join(root, name) + ret_list.append(data_path) + return ret_list + +def crop_and_resize(frames, landmarks, save_size=512, crop=True): + H, W = frames[0].shape[:2] + if crop: + all_tl, all_br = np.min(landmarks, axis=1), np.max(landmarks, axis=1) + mean_wh = np.mean(all_br - all_tl, axis=0) + tl, br = np.min(all_tl, axis=0), np.max(all_br, axis=0) + new_size = min(max(mean_wh) * 2.2, min(H, W) - 1) + fcenter = (tl + br) * 0.5 + ftl = fcenter - new_size * 0.5 + ftl[1] -= mean_wh[1] * 0.2 + fbr = fcenter + new_size * 0.5 + fbr[1] -= mean_wh[1] * 0.2 + + if ftl[0] < 0: + fbr[0] -= ftl[0] + ftl[0] = 0 + if ftl[1] < 0: + fbr[1] -= ftl[1] + ftl[1] = 0 + + if fbr[0] >= W: + ftl[0] -= fbr[0] - W + 1 + fbr[0] = W - 1 + if fbr[1] >= H: + ftl[1] -= fbr[1] - H + 1 + fbr[1] = H - 1 + ftl = ftl.astype(int) + fbr = (fbr - ftl)[0].astype(int) + ftl + + frames = frames[:, ftl[1]:fbr[1], ftl[0]:fbr[0], :].copy() + landmarks = landmarks - ftl + ratio = save_size / (fbr - ftl) + else: + ratio = (save_size / W, save_size / H) + landmarks = landmarks * ratio + frames = np.stack([cv2.resize(frame, (save_size, save_size), interpolation=cv2.INTER_CUBIC) for frame in frames], axis=0) + return frames, landmarks + +def umeyama(src, dst, estimate_scale): + """Estimate N-D similarity transformation with or without scaling. + Parameters + ---------- + src : (M, N) array + Source coordinates. + dst : (M, N) array + Destination coordinates. + estimate_scale : bool + Whether to estimate scaling factor. + Returns + ------- + T : (N + 1, N + 1) + The homogeneous similarity transformation matrix. The matrix contains + NaN values only if the problem is not well-conditioned. + References + ---------- + .. [1] "Least-squares estimation of transformation parameters between two + point patterns", Shinji Umeyama, PAMI 1991, DOI: 10.1109/34.88573 + """ + + num = src.shape[0] + dim = src.shape[1] + + # Compute mean of src and dst. + src_mean = src.mean(axis=0) + dst_mean = dst.mean(axis=0) + + # Subtract mean from src and dst. + src_demean = src - src_mean + dst_demean = dst - dst_mean + + # Eq. (38). + A = np.dot(dst_demean.T, src_demean) / num + + # Eq. (39). + d = np.ones((dim,), dtype=np.double) + if np.linalg.det(A) < 0: + d[dim - 1] = -1 + + T = np.eye(dim + 1, dtype=np.double) + + U, S, V = np.linalg.svd(A) + + # Eq. (40) and (43). + rank = np.linalg.matrix_rank(A) + if rank == 0: + return np.nan * T + elif rank == dim - 1: + if np.linalg.det(U) * np.linalg.det(V) > 0: + T[:dim, :dim] = np.dot(U, V) + else: + s = d[dim - 1] + d[dim - 1] = -1 + T[:dim, :dim] = np.dot(U, np.dot(np.diag(d), V)) + d[dim - 1] = s + else: + T[:dim, :dim] = np.dot(U, np.dot(np.diag(d), V.T)) + + if estimate_scale: + # Eq. (41) and (42). + scale = 1.0 / src_demean.var(axis=0).sum() * np.dot(S, d) + else: + scale = 1.0 + + T[:dim, dim] = dst_mean - scale * np.dot(T[:dim, :dim], src_mean.T) + T[:dim, :dim] *= scale + + return T + +def ff_cat_video_and_audio(video_path, audio_path, output_path): + cmd = [ + "ffmpeg", + "-y", + "-v", "quiet", "-stats", + "-i", video_path, + "-i", audio_path, + "-c:v", "copy", + "-map", "0:v:0", + "-map", "1:a:0", + "-shortest", + output_path + ] + subprocess.run(cmd) + +def ff_change_fps(input_video, output_video, fps=15): + cmd = [ + "ffmpeg", + "-y", + "-v", "quiet", "-stats", + # "-hwaccel", "cuda", + "-i", input_video, + "-c:v", "libx264", + "-r", f"{fps}", + "-c:a", "copy", + "-crf", "18", + output_video + ] + + subprocess.run(cmd) \ No newline at end of file diff --git a/meme.py b/meme.py new file mode 100644 index 0000000..ac276cc --- /dev/null +++ b/meme.py @@ -0,0 +1,368 @@ +import os +import os.path as osp +import folder_paths +import torch +import numpy as np +import cv2 +import sys +import copy + + +from PIL import Image +import subprocess + +import importlib.metadata + +cur_dir = osp.dirname(osp.abspath(__file__)) + +installed_packages = [package.name for package in importlib.metadata.distributions()] + +REQUIRED = { + 'diffusers', 'transformers', 'einops', 'opencv-python', 'tqdm', 'pillow', 'onnxruntime', 'onnx', 'safetensors', 'accelerate', 'peft' +} + +missing = [name for name in REQUIRED if name not in installed_packages] +print("missing pkgs", missing) + +if missing: + python = sys.executable + subprocess.check_call([python, '-m', 'pip', 'install', *missing], stdout=subprocess.DEVNULL) + +from .hellomeme.utils import (face_params_to_tensor, + get_drive_params, + get_face_params, + load_data_list, + load_unet_from_safetensors) + +from .hellomeme.tools import Hello3DMMPred, HelloARKitBSPred, HelloFaceAlignment, HelloCameraDemo +from .hellomeme import HMImagePipeline, HMVideoPipeline +from transformers import CLIPVisionModelWithProjection + +class HMImagePipelineLoader: + @classmethod + def INPUT_TYPES(s): + checkpoint_files = sorted(load_data_list(osp.join(cur_dir, '../../models/checkpoints'), '.pt;.pth;.ckpt;.safetensors')) + lora_files = sorted(load_data_list(osp.join(cur_dir, '../../models/loras'), '.safetensors')) + + return { + "optional": { + "checkpoint_path": (['None'] + checkpoint_files, ), + "lora_path": (['None'] + lora_files, ), + "gpu_id": ("INT", {"default": 0}), + } + } + RETURN_TYPES = ("HMIMAGEPIPELINE", ) + FUNCTION = "load_pipeline" + CATEGORY = "hellomeme" + def load_pipeline(self, checkpoint_path=None, lora_path=None, gpu_id=0): + dtype = torch.float16 + if gpu_id >= 0: + device = torch.device("cuda:{}".format(gpu_id)) + else: + device = torch.device("cpu") + pipeline = HMImagePipeline.from_pretrained("stable-diffusion-v1-5/stable-diffusion-v1-5").to(dtype=dtype, device=device) + pipeline.caryomitosis() + + ### load customized checkpoint or lora here: + ## checkpoints + + if checkpoint_path and osp.isfile(checkpoint_path): + if checkpoint_path.endswith('.safetensors'): + state_dict = load_unet_from_safetensors(checkpoint_path, pipeline.unet_ref.config) + pipeline.unet.load_state_dict(state_dict, strict=False) + elif osp.splitext(checkpoint_path)[-1] in ['.pt', '.pth', '.ckpt']: + state_dict = torch.load(checkpoint_path) + pipeline.unet.load_state_dict(state_dict, strict=False) + else: + print("Invalid checkpoint path", checkpoint_path) + + ### lora + if lora_path and osp.isfile(lora_path): + pipeline.load_lora_weights(osp.dirname(lora_path), weight_name=osp.basename(lora_path), adapter_name="lora") + + pipeline.insert_hm_modules(dtype=dtype, device=device) + + return (pipeline, ) + +class HMVideoPipelineLoader: + @classmethod + def INPUT_TYPES(s): + checkpoint_files = sorted(load_data_list(osp.join(cur_dir, '../../models/checkpoints'), '.pt;.pth;.ckpt;.safetensors')) + lora_files = sorted(load_data_list(osp.join(cur_dir, '../../models/loras'), '.safetensors')) + + return { + "optional": { + "checkpoint_path": (['None'] + checkpoint_files, ), + "lora_path": (['None'] + lora_files, ), + "gpu_id": ("INT", {"default": 0}), + } + } + + RETURN_TYPES = ("HMVIDEOPIPELINE",) + FUNCTION = "load_pipeline" + CATEGORY = "hellomeme" + + def load_pipeline(self, checkpoint_path=None, lora_path=None, gpu_id=0): + dtype = torch.float16 + if gpu_id >= 0: + device = torch.device("cuda:{}".format(gpu_id)) + else: + device = torch.device("cpu") + pipeline = HMVideoPipeline.from_pretrained("stable-diffusion-v1-5/stable-diffusion-v1-5").to(dtype=dtype, + device=device) + pipeline.caryomitosis() + + ### load customized checkpoint or lora here: + ## checkpoints + + if checkpoint_path and osp.isfile(checkpoint_path): + if checkpoint_path.endswith('.safetensors'): + state_dict = load_unet_from_safetensors(checkpoint_path, pipeline.unet_ref.config) + pipeline.unet.load_state_dict(state_dict, strict=False) + elif osp.splitext(checkpoint_path)[-1] in ['.pt', '.pth', '.ckpt']: + state_dict = torch.load(checkpoint_path) + pipeline.unet.load_state_dict(state_dict, strict=False) + else: + print("Invalid checkpoint path", checkpoint_path) + + ### lora + if lora_path and osp.isfile(lora_path): + pipeline.load_lora_weights(osp.dirname(lora_path), weight_name=osp.basename(lora_path), adapter_name="lora") + + pipeline.insert_hm_modules(dtype=dtype, device=device) + + return (pipeline,) + +class HMFaceToolkitsLoader: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "gpu_id": ("INT", {"default": 0}), + } + } + + RETURN_TYPES = ("FACE_TOOLKITS",) + FUNCTION = "load_face_toolkits" + CATEGORY = "hellomeme" + def load_face_toolkits(self, gpu_id): + dtype = torch.float16 + device = torch.device(f'cuda:{gpu_id}') + + face_aligner = HelloCameraDemo(face_alignment_module=HelloFaceAlignment(gpu_id=gpu_id), reset=True) + harkit_bs = HelloARKitBSPred(gpu_id=gpu_id) + h3dmm = Hello3DMMPred(gpu_id=gpu_id) + clip_image_encoder = CLIPVisionModelWithProjection.from_pretrained( + 'h94/IP-Adapter',subfolder='models/image_encoder').to(dtype=dtype, device=device) + return (dict(face_aligner=face_aligner, harkit_bs=harkit_bs, h3dmm=h3dmm, image_encoder=clip_image_encoder), ) + +class GetReferenceImageRT: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "face_toolkits": ("FACE_TOOLKITS",), + "image": ("IMAGE",), + } + } + + RETURN_TYPES = ("REFRT",) + FUNCTION = "get_reference_image_rt" + CATEGORY = "hellomeme" + + def get_reference_image_rt(self, face_toolkits, image): + image_np = cv2.cvtColor((image[0] * 255).cpu().numpy().astype(np.uint8), cv2.COLOR_BGR2RGB) + image_np = cv2.resize(image_np, (512, 512)) + print(image_np.shape) + face_toolkits['face_aligner'].reset_track() + faces = face_toolkits['face_aligner'].forward(image_np) + assert len(faces) > 0 + face = sorted(faces, key=lambda x: (x['face_rect'][2] - x['face_rect'][0]) * ( + x['face_rect'][3] - x['face_rect'][1]))[-1] + ref_landmark = face['pre_kpt_222'] + + ref_rot, ref_trans = face_toolkits['h3dmm'].forward_params(image_np, ref_landmark) + return (dict(rot=ref_rot, trans=ref_trans), ) + +class GetImageDriveParams: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "face_toolkits": ("FACE_TOOLKITS",), + "image": ("IMAGE",), + "ref_rt": ("REFRT",), + "gpu_id": ("INT", {"default": 0}) + } + } + + RETURN_TYPES = ("DRIVE_IMAGE_PARAMS",) + RETURN_NAMES = ("drive_image_params",) + FUNCTION = "get_face_params" + CATEGORY = "hellomeme" + def get_face_params(self, face_toolkits, image, ref_rt, gpu_id): + dtype = torch.float16 + device = torch.device(f'cuda:{gpu_id}') + + image_np = cv2.cvtColor((image[0] * 255).cpu().numpy().astype(np.uint8), cv2.COLOR_BGR2RGB) + + face_toolkits['face_aligner'].reset_track() + faces = face_toolkits['face_aligner'].forward(image_np) + assert len(faces) > 0 + + face = sorted(faces, key=lambda x: (x['face_rect'][2] - x['face_rect'][0]) * ( + x['face_rect'][3] - x['face_rect'][1]))[-1] + drive_landmark = face['pre_kpt_222'] + + drive_face_parts, drive_coeff, drive_rot, drive_trans = get_face_params(face_toolkits['h3dmm'], + face_toolkits['harkit_bs'], + [image_np], + [drive_landmark], + save_size=(512, 512), + align=False) + + face_parts_embedding, control_heatmaps = face_params_to_tensor(face_toolkits['image_encoder'], face_toolkits['h3dmm'], + drive_face_parts, + drive_rot, drive_trans, ref_rt['trans'], + save_size=512, trans_ratio=0.0) + + drive_params = dict( + face_parts=face_parts_embedding.unsqueeze(0).to(dtype=dtype, device=device), + drive_coeff=drive_coeff.unsqueeze(0).to(dtype=dtype, device=device), + condition=control_heatmaps.unsqueeze(0).to(dtype=dtype, device=device), + ) + return (drive_params, ) + +class HMPipelineImage: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipeline": ("HMIMAGEPIPELINE",), + "image": ("IMAGE",), + "drive_image_params": ("DRIVE_IMAGE_PARAMS",), + "prompt": ("STRING", {"default": '(best quality), highly detailed, ultra-detailed, headshot, person, well-placed five sense organs, looking at the viewer, centered composition, sharp focus, realistic skin texture'}), + "negative_prompt": ("STRING", {"default": ''}), + "steps": ("INT", {"default": 25, "min": 1, "max": 10000}), + "guidance_scale": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 100.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "sample" + CATEGORY = "hellomeme" + + def sample(self, pipeline, image, drive_image_params, prompt, negative_prompt, steps=25, guidance_scale=2.0): + image_np = (image[0] * 255).cpu().numpy().astype(np.uint8) + image_np = cv2.resize(image_np, (512, 512)) + image_pil = Image.fromarray(image_np) + result_img = pipeline( + prompt=[prompt], + strength=1.0, + image=image_pil, + drive_params=copy.deepcopy(drive_image_params), + num_inference_steps=steps, + negative_prompt=[negative_prompt], + guidance_scale=guidance_scale, + output_type='np' + ) + return (torch.from_numpy(np.clip(result_img[0], 0, 1)), ) + +class GetVideoDriveParams: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "face_toolkits": ("FACE_TOOLKITS",), + "images": ("IMAGE",), + "ref_rt": ("REFRT",), + "trans_ratio": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), + } + } + RETURN_TYPES = ("DRIVE_VIDEO_PARAMS",) + RETURN_NAMES = ("drive_video_params",) + FUNCTION = "get_face_params" + CATEGORY = "hellomeme" + def get_face_params(self, face_toolkits, images, ref_rt, trans_ratio): + dtype = torch.float16 + + frame_list = [cv2.cvtColor((frame * 255).cpu().numpy().astype(np.uint8), cv2.COLOR_BGR2RGB) for frame in images] + + face_toolkits['face_aligner'].reset_track() + (drive_face_parts, drive_coeff, drive_rot, drive_trans) = get_drive_params(face_toolkits['face_aligner'], + face_toolkits['h3dmm'], + face_toolkits['harkit_bs'], + frame_list=frame_list, + save_size=512, + align=True) + face_toolkits['face_aligner'].reset_track() + face_parts_embedding, control_heatmaps = face_params_to_tensor( + face_toolkits['image_encoder'], face_toolkits['h3dmm'], + drive_face_parts, + drive_rot, drive_trans, ref_rt['trans'], + save_size=512, trans_ratio=trans_ratio) + + drive_params = dict( + face_parts=face_parts_embedding.unsqueeze(0).to(dtype=dtype), + drive_coeff=drive_coeff.unsqueeze(0).to(dtype=dtype), + condition=control_heatmaps.unsqueeze(0).to(dtype=dtype), + ) + return (drive_params, ) + +class HMPipelineVideo: + @classmethod + def INPUT_TYPES(s): + return {"required": + { + "pipeline": ("HMVIDEOPIPELINE",), + "image": ("IMAGE",), + "drive_video_params": ("DRIVE_VIDEO_PARAMS",), + "prompt": ("STRING", {"default": '(best quality), highly detailed, ultra-detailed, headshot, person, well-placed five sense organs, looking at the viewer, centered composition, sharp focus, realistic skin texture'}), + "negative_prompt": ("STRING", {"default": ''}), + "steps": ("INT", {"default": 25, "min": 1, "max": 10000}), + "guidance_scale": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 100.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "sample" + CATEGORY = "hellomeme" + + def sample(self, pipeline, image, drive_video_params, prompt, negative_prompt, steps=25, guidance_scale=2.0): + image_np = (image[0] * 255).cpu().numpy().astype(np.uint8) + image_np = cv2.resize(image_np, (512, 512)) + image_pil = Image.fromarray(image_np) + res_frames = pipeline( + prompt=[prompt], + strength=1.0, + image=image_pil, + drive_params=copy.deepcopy(drive_video_params), + num_inference_steps=steps, + negative_prompt=[negative_prompt], + guidance_scale=guidance_scale, + output_type='np' + ) + res_frames = [np.clip(x[0], 0, 1) for x in res_frames] + return (torch.from_numpy(np.array(res_frames)), ) + +NODE_CLASS_MAPPINGS = { + "HMImagePipelineLoader": HMImagePipelineLoader, + "HMVideoPipelineLoader": HMVideoPipelineLoader, + "HMFaceToolkitsLoader": HMFaceToolkitsLoader, + "GetReferenceImageRT": GetReferenceImageRT, + "GetImageDriveParams": GetImageDriveParams, + "GetVideoDriveParams": GetVideoDriveParams, + "HMPipelineImage": HMPipelineImage, + "HMPipelineVideo": HMPipelineVideo, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "HMImagePipelineLoader": "Load HelloMemeImage Pipeline", + "HMVideoPipelineLoader": "Load HelloMemeVideo Pipeline", + "HMFaceToolkitsLoader": "Load Face Toolkits", + "GetReferenceImageT": "Get Reference Image Translation", + "GetImageDriveParams": "Get Drive Image Parameters", + "GetVideoDriveParams": "Get Drive Video Parameters", + "HMPipelineImage": "HelloMeme Image Pipeline", + "HMPipelineVideo": "HelloMeme Video Pipeline", +} \ No newline at end of file diff --git a/workflows/hellomeme_image_example.jpg b/workflows/hellomeme_image_example.jpg new file mode 100644 index 0000000..75ef420 Binary files /dev/null and b/workflows/hellomeme_image_example.jpg differ diff --git a/workflows/hellomeme_image_workflow.json b/workflows/hellomeme_image_workflow.json new file mode 100644 index 0000000..eb7af40 --- /dev/null +++ b/workflows/hellomeme_image_workflow.json @@ -0,0 +1,403 @@ +{ + "last_node_id": 14, + "last_link_id": 18, + "nodes": [ + { + "id": 11, + "type": "GetReferenceImageRT", + "pos": { + "0": 621, + "1": 262 + }, + "size": { + "0": 241.79998779296875, + "1": 46 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "face_toolkits", + "type": "FACE_TOOLKITS", + "link": 13 + }, + { + "name": "image", + "type": "IMAGE", + "link": 12 + } + ], + "outputs": [ + { + "name": "REFRT", + "type": "REFRT", + "links": [ + 17 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "GetReferenceImageRT" + }, + "widgets_values": [] + }, + { + "id": 14, + "type": "GetImageDriveParams", + "pos": { + "0": 987, + "1": 226 + }, + "size": { + "0": 330, + "1": 98 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "face_toolkits", + "type": "FACE_TOOLKITS", + "link": 16 + }, + { + "name": "image", + "type": "IMAGE", + "link": 18 + }, + { + "name": "ref_rt", + "type": "REFRT", + "link": 17 + } + ], + "outputs": [ + { + "name": "drive_image_params", + "type": "DRIVE_IMAGE_PARAMS", + "links": [ + 15 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "GetImageDriveParams" + }, + "widgets_values": [ + 0 + ] + }, + { + "id": 6, + "type": "HMPipelineImage", + "pos": { + "0": 1325, + "1": 654 + }, + "size": { + "0": 315, + "1": 170 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "pipeline", + "type": "HMIMAGEPIPELINE", + "link": 7 + }, + { + "name": "image", + "type": "IMAGE", + "link": 9 + }, + { + "name": "drive_image_params", + "type": "DRIVE_IMAGE_PARAMS", + "link": 15 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 11 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "HMPipelineImage" + }, + "widgets_values": [ + "(best quality), highly detailed, ultra-detailed, headshot, person, well-placed five sense organs, looking at the viewer, centered composition, sharp focus, realistic skin texture", + "", + 25, + 2 + ] + }, + { + "id": 10, + "type": "PreviewImage", + "pos": { + "0": 1695, + "1": 652 + }, + "size": { + "0": 210, + "1": 246 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 11 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + }, + { + "id": 2, + "type": "HMFaceToolkitsLoader", + "pos": { + "0": 211, + "1": 180 + }, + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "FACE_TOOLKITS", + "type": "FACE_TOOLKITS", + "links": [ + 13, + 16 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "HMFaceToolkitsLoader" + }, + "widgets_values": [ + 0 + ] + }, + { + "id": 1, + "type": "HMImagePipelineLoader", + "pos": { + "0": 226, + "1": 356 + }, + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "HMIMAGEPIPELINE", + "type": "HMIMAGEPIPELINE", + "links": [ + 7 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "HMImagePipelineLoader" + }, + "widgets_values": [ + "None", + "None", + 0 + ] + }, + { + "id": 8, + "type": "LoadImage", + "pos": { + "0": 233, + "1": 536 + }, + "size": { + "0": 315, + "1": 314 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 9, + 12 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "majicmix2.jpg", + "image" + ] + }, + { + "id": 9, + "type": "LoadImage", + "pos": { + "0": 227, + "1": 915 + }, + "size": { + "0": 315, + "1": 314 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 18 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "yao.jpg", + "image" + ] + } + ], + "links": [ + [ + 7, + 1, + 0, + 6, + 0, + "HMIMAGEPIPELINE" + ], + [ + 9, + 8, + 0, + 6, + 1, + "IMAGE" + ], + [ + 11, + 6, + 0, + 10, + 0, + "IMAGE" + ], + [ + 12, + 8, + 0, + 11, + 1, + "IMAGE" + ], + [ + 13, + 2, + 0, + 11, + 0, + "FACE_TOOLKITS" + ], + [ + 15, + 14, + 0, + 6, + 2, + "DRIVE_PARAMS" + ], + [ + 16, + 2, + 0, + 14, + 0, + "FACE_TOOLKITS" + ], + [ + 17, + 11, + 0, + 14, + 2, + "REFRT" + ], + [ + 18, + 9, + 0, + 14, + 1, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1, + "offset": [ + 150, + -42.66668701171875 + ] + } + }, + "version": 0.4 +} \ No newline at end of file diff --git a/workflows/hellomeme_video_example.jpg b/workflows/hellomeme_video_example.jpg new file mode 100644 index 0000000..932a8fc Binary files /dev/null and b/workflows/hellomeme_video_example.jpg differ diff --git a/workflows/hellomeme_video_workflow.json b/workflows/hellomeme_video_workflow.json new file mode 100644 index 0000000..748362b --- /dev/null +++ b/workflows/hellomeme_video_workflow.json @@ -0,0 +1,502 @@ +{ + "last_node_id": 24, + "last_link_id": 30, + "nodes": [ + { + "id": 15, + "type": "HMPipelineVideo", + "pos": { + "0": 1408, + "1": 390 + }, + "size": { + "0": 315, + "1": 170 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "pipeline", + "type": "HMVIDEOPIPELINE", + "link": 16 + }, + { + "name": "image", + "type": "IMAGE", + "link": 17 + }, + { + "name": "drive_video_params", + "type": "DRIVE_VIDEO_PARAMS", + "link": 29 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 24 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "HMPipelineVideo" + }, + "widgets_values": [ + "(best quality), highly detailed, ultra-detailed, headshot, person, well-placed five sense organs, looking at the viewer, centered composition, sharp focus, realistic skin texture", + "", + 25, + 2 + ] + }, + { + "id": 2, + "type": "HMFaceToolkitsLoader", + "pos": { + "0": 124, + "1": 276 + }, + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "FACE_TOOLKITS", + "type": "FACE_TOOLKITS", + "links": [ + 13, + 27 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "HMFaceToolkitsLoader" + }, + "widgets_values": [ + 0 + ] + }, + { + "id": 11, + "type": "GetReferenceImageRT", + "pos": { + "0": 536, + "1": 516 + }, + "size": { + "0": 241.79998779296875, + "1": 46 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "face_toolkits", + "type": "FACE_TOOLKITS", + "link": 13 + }, + { + "name": "image", + "type": "IMAGE", + "link": 12 + } + ], + "outputs": [ + { + "name": "REFRT", + "type": "REFRT", + "links": [ + 26 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "GetReferenceImageRT" + }, + "widgets_values": [] + }, + { + "id": 21, + "type": "VHS_VideoCombine", + "pos": { + "0": 1927, + "1": 604 + }, + "size": [ + 214.7587890625, + 518.7587890625 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 24 + }, + { + "name": "audio", + "type": "AUDIO", + "link": 25, + "shape": 7 + }, + { + "name": "meta_batch", + "type": "VHS_BatchManager", + "link": null, + "shape": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": null, + "shape": 7 + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": [] + } + ], + "properties": { + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 15, + "loop_count": 0, + "filename_prefix": "HelloMeme", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "HelloMeme_00001-audio.mp4", + "subfolder": "", + "type": "output", + "format": "video/h264-mp4", + "frame_rate": 15 + }, + "muted": false + } + } + }, + { + "id": 22, + "type": "GetVideoDriveParams", + "pos": { + "0": 932, + "1": 503 + }, + "size": { + "0": 330, + "1": 98 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "face_toolkits", + "type": "FACE_TOOLKITS", + "link": 27 + }, + { + "name": "images", + "type": "IMAGE", + "link": 28 + }, + { + "name": "ref_rt", + "type": "REFRT", + "link": 26 + } + ], + "outputs": [ + { + "name": "drive_video_params", + "type": "DRIVE_VIDEO_PARAMS", + "links": [ + 29 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "GetVideoDriveParams" + }, + "widgets_values": [ + 0 + ] + }, + { + "id": 14, + "type": "HMVideoPipelineLoader", + "pos": { + "0": 116, + "1": 105 + }, + "size": { + "0": 352.79998779296875, + "1": 106 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "HMVIDEOPIPELINE", + "type": "HMVIDEOPIPELINE", + "links": [ + 16 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "HMVideoPipelineLoader" + }, + "widgets_values": [ + "None", + "None", + 0 + ] + }, + { + "id": 8, + "type": "LoadImage", + "pos": { + "0": 120, + "1": 414 + }, + "size": { + "0": 315, + "1": 314 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 12, + 17 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "majicmix2.jpg", + "image" + ] + }, + { + "id": 20, + "type": "VHS_LoadVideoFFmpeg", + "pos": { + "0": 139, + "1": 793 + }, + "size": [ + 303.46875, + 535.46875 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "meta_batch", + "type": "VHS_BatchManager", + "link": null, + "shape": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": null, + "shape": 7 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 28 + ], + "slot_index": 0 + }, + { + "name": "mask", + "type": "MASK", + "links": null + }, + { + "name": "audio", + "type": "AUDIO", + "links": [ + 25 + ], + "slot_index": 2 + }, + { + "name": "video_info", + "type": "VHS_VIDEOINFO", + "links": null + } + ], + "properties": { + "Node name for S&R": "VHS_LoadVideoFFmpeg" + }, + "widgets_values": { + "video": "jgz.mp4", + "force_rate": 15, + "force_size": "Disabled", + "custom_width": 512, + "custom_height": 512, + "frame_load_cap": 0, + "start_time": 0, + "choose video to upload": "image", + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "force_rate": 15, + "frame_load_cap": 0, + "start_time": 0, + "filename": "jgz.mp4", + "type": "input", + "format": "video/mp4" + }, + "muted": false + } + } + } + ], + "links": [ + [ + 12, + 8, + 0, + 11, + 1, + "IMAGE" + ], + [ + 13, + 2, + 0, + 11, + 0, + "FACE_TOOLKITS" + ], + [ + 16, + 14, + 0, + 15, + 0, + "HMVIDEOPIPELINE" + ], + [ + 17, + 8, + 0, + 15, + 1, + "IMAGE" + ], + [ + 24, + 15, + 0, + 21, + 0, + "IMAGE" + ], + [ + 25, + 20, + 2, + 21, + 1, + "AUDIO" + ], + [ + 26, + 11, + 0, + 22, + 2, + "REFRT" + ], + [ + 27, + 2, + 0, + 22, + 0, + "FACE_TOOLKITS" + ], + [ + 28, + 20, + 0, + 22, + 1, + "IMAGE" + ], + [ + 29, + 22, + 0, + 15, + 2, + "DRIVE_PARAMS" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1, + "offset": [ + 0, + 0 + ] + } + }, + "version": 0.4 +} \ No newline at end of file