Progress on CameraCtrl support

This commit is contained in:
Jedrzej Kosinski
2024-04-04 14:51:49 -05:00
parent 8edda0421a
commit 7451bb58fa
5 changed files with 230 additions and 3 deletions
+195
View File
@@ -0,0 +1,195 @@
# Modified from https://github.com/hehao13/CameraCtrl/blob/main/cameractrl/models/pose_adaptor.py
# (whose parts were also taken from https://github.com/TencentARC/T2I-Adapter)
import torch
import torch.nn as nn
from torch import Tensor
from collections import OrderedDict
from einops import rearrange
import comfy.ops
from .motion_module_ad import TemporalTransformerBlock
def conv_nd(dims, *args, **kwargs):
"""
Create a 1D, 2D, or 3D convolution module.
"""
if dims == 1:
return nn.Conv1d(*args, **kwargs)
elif dims == 2:
return nn.Conv2d(*args, **kwargs)
elif dims == 3:
return nn.Conv3d(*args, **kwargs)
raise ValueError(f"unsupported dimensions: {dims}")
def avg_pool_nd(dims, *args, **kwargs):
"""
Create a 1D, 2D, or 3D average pooling module.
"""
if dims == 1:
return nn.AvgPool1d(*args, **kwargs)
elif dims == 2:
return nn.AvgPool2d(*args, **kwargs)
elif dims == 3:
return nn.AvgPool3d(*args, **kwargs)
raise ValueError(f"unsupported dimensions: {dims}")
# class PoseAdapter(nn.Module):
# def __init__(self, unet, pose_encoder):
# super().__init__()
# self.unet = unet
# self.pose_encoder = pose_encoder
# def forward(self, noisy_latents: Tensor, timesteps, encoder_hidden_states: Tensor, pose_embedding):
# # original code needed to convert from 4 dims (bf c h w) to 5 dims (b c f h w),
# # but ComfyUI already deals with everything in 4 dims
# pose_embedding_features = self.pose_encoder(pose_embedding)
# noise_pred = self.unet(noisy_latents,
# timesteps,
# encoder_hidden_states,
# pose_embedding_features).sample
# return noise_pred
class CameraPoseEncoder(nn.Module):
def __init__(self,
downscale_factor=8,
channels=[320, 640, 1280, 1280],
nums_rb=3,
cin=64,
ksize=3,
sk=False,
use_conv=True,
compression_factor=1,
temporal_attention_nhead=8,
attention_block_types=("Temporal_Self", ),
temporal_position_encoding=False,
temporal_position_encoding_max_len=16,
rescale_output_factor=1.0,
ops=comfy.ops.disable_weight_init):
super(CameraPoseEncoder, self).__init__()
self.unshuffle = nn.PixelUnshuffle(downscale_factor)
self.channels = channels
self.nums_rb = nums_rb
self.encoder_conv_in = ops.Conv2d(cin, channels[0], 3, 1, 1)
self.encoder_down_conv_blocks = nn.ModuleList()
self.encoder_down_attention_blocks = nn.ModuleList()
for i in range(len(channels)):
conv_layers = nn.ModuleList()
temporal_attention_layers = nn.ModuleList()
for j in range(len(nums_rb)):
if j == 0 and i != 0:
in_dim = channels[i - 1]
out_dim = int(channels[i] / compression_factor)
conv_layer = ResnetBlock(in_dim, out_dim, down=True, ksize=ksize, sk=sk, use_conv=use_conv, ops=ops)
elif j == 0:
in_dim = channels[0]
out_dim = int(channels[i] / compression_factor)
conv_layer = ResnetBlock(in_dim, out_dim, down=False, ksize=ksize, sk=sk, use_conv=use_conv, ops=ops)
elif j == nums_rb - 1:
in_dim = channels[i] / compression_factor
out_dim = channels[i]
conv_layer = ResnetBlock(in_dim, out_dim, down=False, ksize=ksize, sk=sk, use_conv=use_conv, ops=ops)
else:
in_dim = int(channels[i] / compression_factor)
out_dim = int(channels[i] / compression_factor)
conv_layer = ResnetBlock(in_dim, out_dim, down=False, ksize=ksize, sk=sk, use_conv=use_conv, ops=ops)
temporal_attention_layer = TemporalTransformerBlock(dim=out_dim,
num_attention_heads=temporal_attention_nhead,
attention_head_dim=int(out_dim / temporal_attention_nhead),
attention_block_types=attention_block_types,
dropout=0.0,
cross_attention_dim=None,
temporal_position_encoding=temporal_position_encoding,
temporal_position_encoding_max_len=temporal_position_encoding_max_len,
rescale_output_factor=rescale_output_factor,
ops=ops)
conv_layers.append(conv_layer)
temporal_attention_layers.append(temporal_attention_layer)
self.encoder_down_conv_blocks.append(conv_layers)
self.encoder_down_attention_blocks.append(temporal_attention_layers)
def forward(self, x: Tensor):
# unshuffle
x = self.unshuffle
# extract features
features = []
x = self.encoder_conv_in(x)
for res_block, attention_block in zip(self.encoder_down_conv_blocks, self.encoder_down_attention_blocks):
for res_layer, attention_layer in zip(res_block, attention_block):
x = res_layer(x)
h, w = x.shape[-2:]
x = rearrange(x, 'b c h w -> (h w) b c')
x = attention_layer(x)
x = rearrange(x, '(h w) b c -> b c h w', h=h, w=w)
features.append(x)
return features
class ResnetBlock(nn.Module):
def __init__(self, in_c, out_c, down: bool, ksize=3, sk=False, use_conv=True,
ops=comfy.ops.disable_weight_init):
super().__init__()
ps = ksize // 2 # padding size
if in_c != out_c or sk == False:
self.in_conv = ops.Conv2d(in_c, out_c, ksize, 1, ps)
else:
self.in_conv = None
self.block1 = ops.Conv2d(out_c, out_c, 3, 1, 1)
self.act = nn.ReLU()
self.block2 = ops.Conv2d(out_c, out_c, ksize, 1, ps)
if sk == False:
self.skep = ops.Conv2d(in_c, out_c, ksize, 1, ps)
else:
self.skep = None
self.down = down
if self.down == True:
self.down_opt = Downsample(in_c, use_conv=use_conv)
def forward(self, x: Tensor):
if self.down == True:
x = self.down_opt(x)
if self.in_conv is not None: # edit
x = self.in_conv(x)
h = self.block1(x)
h = self.act(h)
h = self.block2(h)
if self.skep is not None:
return h + self.skep(x)
else:
return h + x
class Downsample(nn.Module):
"""
A downsampling layer with an optional convolution.
:param channels: channels in the inputs and outputs.
:param use_conv: a bool determining if a convolution is applied.
:param dims: determines if the signal is 1D, 2D, or 3D. If 3D, then
downsampling occurs in the inner-two dimensions.
"""
def __init__(self, channels, use_conv: bool, dims=2, out_channels=None, padding=1,
ops=comfy.ops.disable_weight_init):
super().__init__()
self.channels = channels
self.out_channels = out_channels or channels
self.use_conv = use_conv
self.dims = dims
stride = 2 if dims != 3 else (1, 2, 2)
if use_conv:
self.operation = ops.conv_nd(dims, in_channels=self.channels, out_channels=self.out_channels,
kernel_size=3, stride=stride, padding=padding)
else:
assert self.channels == self.out_channels
self.operation = avg_pool_nd(dims, kernel_size=stride, stride=stride) # both are stride value on purpose
def forward(self, x: Tensor):
assert x.shape[1] == self.channels
return self.operation(x)
+1 -1
View File
@@ -16,7 +16,7 @@ import comfy.ops
import comfy.model_management
from .context import ContextFuseMethod, ContextOptions, get_context_weights, get_context_windows
from .animatelcm_i2v_adapter import AdapterEmbed
from .adapter_animatelcm_i2v import AdapterEmbed
from .utils_motion import CrossAttentionMM, MotionCompatibilityError, DummyNNModule, extend_to_batch_size, prepare_mask_batch
from .utils_model import BetaSchedules, ModelTypeSD
from .logger import logger
+6 -1
View File
@@ -4,7 +4,8 @@ from .sampling import motion_sample_factory
from .nodes_gen1 import (AnimateDiffLoaderGen1, LegacyAnimateDiffLoaderWithContext)
from .nodes_gen2 import (UseEvolvedSamplingNode, ApplyAnimateDiffModelNode, ApplyAnimateDiffModelBasicNode, ApplyAnimateLCMI2VModel, ADKeyframeNode,
LoadAnimateDiffModelNode, LoadAnimateLCMI2VModelNode, LoadAnimateDiffAndInjectI2VNode, UpscaleAndVaeEncode)
LoadAnimateDiffModelNode, LoadAnimateLCMI2VModelNode, LoadAnimateDiffAndInjectI2VNode, UpscaleAndVaeEncode,
LoadCameraCtrlAdapter)
from .nodes_multival import MultivalDynamicNode, MultivalScaledMaskNode
from .nodes_sample import (FreeInitOptionsNode, NoiseLayerAddWeightedNode, SampleSettingsNode, NoiseLayerAddNode, NoiseLayerReplaceNode, IterationOptionsNode,
CustomCFGNode, CustomCFGKeyframeNode)
@@ -92,6 +93,8 @@ NODE_CLASS_MAPPINGS = {
"ADE_LoadAnimateLCMI2VModel": LoadAnimateLCMI2VModelNode,
"ADE_UpscaleAndVAEEncode": UpscaleAndVaeEncode,
"ADE_InjectI2VIntoAnimateDiffModel": LoadAnimateDiffAndInjectI2VNode,
# CameraCtrl Nodes
"ADE_LoadCameraCtrlAdapter": LoadCameraCtrlAdapter,
# MaskedLoraLoader
#"ADE_MaskedLoadLora": MaskedLoraLoader,
# Deprecated Nodes
@@ -165,6 +168,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"ADE_LoadAnimateLCMI2VModel": "Load AnimateLCM-I2V Model 🎭🅐🅓②",
"ADE_UpscaleAndVAEEncode": "Scale Ref Image and VAE Encode 🎭🅐🅓②",
"ADE_InjectI2VIntoAnimateDiffModel": "🧪Inject I2V into AnimateDiff Model 🎭🅐🅓②",
# CameraCtrl Nodes
"ADE_LoadCameraCtrlAdapter": "Load CameraCtrl Adapter 🎭🅐🅓②",
# MaskedLoraLoader
#"ADE_MaskedLoadLora": "Load LoRA (Masked) 🎭🅐🅓",
# Deprecated Nodes
+28 -1
View File
@@ -10,7 +10,7 @@ from comfy.sd import VAE
from .ad_settings import AnimateDiffSettings
from .context import ContextOptionsGroup
from .logger import logger
from .utils_model import BIGMAX, BetaSchedules, ScaleMethods, CropMethods, get_available_motion_models
from .utils_model import BIGMAX, BetaSchedules, ScaleMethods, CropMethods, get_available_motion_models, get_motion_model_path
from .utils_motion import ADKeyframeGroup, ADKeyframe
from .motion_lora import MotionLoraList
from .model_injection import (InjectionParams, ModelPatcherAndInjector, MotionModelGroup, MotionModelPatcher, create_fresh_motion_module, create_fresh_encoder_only_model,
@@ -286,6 +286,33 @@ class LoadAnimateDiffAndInjectI2VNode:
return (loaded_motion_model,)
class LoadCameraCtrlAdapter:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model_name": (get_available_motion_models(),),
},
}
RETURN_TYPES = ("MOTION_MODEL_ADE",)#("CAMERA_CTRL",)
CATEGORY = "Animate Diff 🎭🅐🅓/② Gen2 nodes ②/CameraCtrl"
FUNCTION = "load_camera_ctrl"
def load_camera_ctrl(self, model_name: str):
model_path = get_motion_model_path(model_name)
logger.info(f"Loading CameraCtrl Adapter {model_name}")
mm_state_dict: dict[str, torch.Tensor] = comfy.utils.load_torch_file(model_path, safe_load=True)
from pathlib import Path
with open(Path(__file__).parent.parent.parent / "cameractrl_keys.txt", "w") as cfile:
for key in mm_state_dict:
if type(mm_state_dict[key]) == torch.Tensor:
cfile.write(f"{key}: {list(mm_state_dict[key].shape)}\n")
else:
cfile.write(f"{key}: {mm_state_dict[key]}\n")
return (None,)
class ADKeyframeNode:
@classmethod
def INPUT_TYPES(s):