Add Thin-Plate Spline Motion Model for Image Animation (#6)

This commit is contained in:
Fuou Marinas
2024-08-04 15:53:09 +07:00
committed by GitHub
parent cd43aad974
commit f1f1e37952
21 changed files with 2263 additions and 14 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Stuff
articulate_module/models/*.pth
spline_module/models/*.tar
checkpoints/*
!checkpoints/place_checkpoints_here.txt
+24 -11
View File
@@ -3,20 +3,15 @@ A ComfyUI-native node to run First Order Motion Model for Image Animation
https://github.com/AliaksandrSiarohin/first-order-model
Now supports Face Swapping using Motion Supervised co-part Segmentation
Now supports:
1. Face Swapping using [Motion Supervised co-part Segmentation](https://github.com/AliaksandrSiarohin/motion-cosegmentation):
2. [Motion Representations for Articulated Animation](https://github.com/snap-research/articulated-animation)
3. [Thin-Plate Spline Motion Model for Image Animation](https://github.com/yoyo-nb/thin-plate-spline-motion-model)
https://github.com/AliaksandrSiarohin/motion-cosegmentation
Now also supports Motion Representations for Articulated Animation
https://github.com/snap-research/articulated-animation
https://github.com/user-attachments/assets/b2948efb-3b44-440b-bff2-dde7b95a9946
## Workflow:
https://github.com/FuouM/ComfyUI-FirstOrderMM/assets/57849916/e8adca97-b53c-48ce-a211-f336dbf7d7bc
### FOMM
[FOMM.json](workflows/FOMM.json)
@@ -35,6 +30,14 @@ https://github.com/FuouM/ComfyUI-FirstOrderMM/assets/57849916/e8adca97-b53c-48ce
![Workflow Articulate](workflows/workflow_articulate.png)
### Spline
[SPLINE.json](workflows/SPLINE.json)
![Workflow Spline](workflows/workflow_spline.png)
## Arguments
### FOMM
@@ -56,6 +59,14 @@ https://github.com/FuouM/ComfyUI-FirstOrderMM/assets/57849916/e8adca97-b53c-48ce
Doesn't need any
### Spline
* `predict_mode`: Can be
* `relative`: Similar to FOMM's `relative_movement` and `adapt_movement_scale` set to True
* `standard`: Similar to FOMM's `adapt_movement_scale` set to False
* `avd`: similar to `relative`, may yield better but more "jittery/jumpy" result
* `find_best_frame`: Same as FOMM
## Installation
1. Clone the repo to `ComfyUI/custom_nodes/`
@@ -113,4 +124,6 @@ resnet18-5c106cde.pth
79999_iter.pth
```
For **Articulate**, download the model from [Pre-trained checkpoints](https://github.com/snap-research/articulated-animation?tab=readme-ov-file#pre-trained-checkpoints) section and place it here: `articulate_module\models\vox256.pth`
For **Articulate**, download the model from [Pre-trained checkpoints](https://github.com/snap-research/articulated-animation?tab=readme-ov-file#pre-trained-checkpoints) section and place it here: `articulate_module/models/vox256.pth`
For **Spline**, download the model from [Pre-trained models](https://github.com/yoyo-nb/thin-plate-spline-motion-model?tab=readme-ov-file#pre-trained-models) section and place it here: `spline_module/models/vox.pth.tar`. To use `find_best_frame`, install `face-alignment`.
+3
View File
@@ -5,6 +5,7 @@ from .run import (
FOMM_Seg10Chooser,
FOMM_Seg15Chooser,
Articulate_Runner,
Spline_Runner
)
NODE_CLASS_MAPPINGS = {
@@ -14,6 +15,7 @@ NODE_CLASS_MAPPINGS = {
"FOMM_Seg10Chooser": FOMM_Seg10Chooser,
"FOMM_Seg15Chooser": FOMM_Seg15Chooser,
"Articulate_Runner": Articulate_Runner,
"Spline_Runner": Spline_Runner,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -23,6 +25,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FOMM_Seg10Chooser": "FOMM Seg10 Chooser",
"FOMM_Seg15Chooser": "FOMM Seg15 Chooser",
"Articulate_Runner": "Articulate Runner",
"Spline_Runner": "Spline Runner",
}
+7 -1
View File
@@ -100,4 +100,10 @@ seg_model_dict = {
}
ARTICULATE_MODEL_PATH = "articulate_module/models/vox256.pth"
ARTICULATE_CFG_PATH = "articulate_module/config/vox256.yaml"
ARTICULATE_CFG_PATH = "articulate_module/config/vox256.yaml"
SPLINE_MODES = ['relative', 'standard', 'avd']
SPLINE_DEFAULT = 'relative'
SPLINE_MODEL_PATH = "spline_module/models/vox.pth.tar"
SPLINE_CFG_PATH = "spline_module/config/vox-256.yaml"
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-firstordermm"
description = "A ComfyUI-native node to run First Order Motion Model for Image Animation. [a/https://github.com/AliaksandrSiarohin/first-order-model](https://github.com/AliaksandrSiarohin/first-order-model)"
version = "1.0.2"
version = "1.0.3"
license = { file = "LICENSE" }
dependencies = ["numpy", "torch", "scipy", "yaml", "matplotlib"]
+81
View File
@@ -13,6 +13,10 @@ from .articulate_inference import articulate_inference
from .constants import (
ARTICULATE_CFG_PATH,
ARTICULATE_MODEL_PATH,
SPLINE_CFG_PATH,
SPLINE_DEFAULT,
SPLINE_MODEL_PATH,
SPLINE_MODES,
config_folder,
default_model_name,
default_partswap_model_name,
@@ -29,6 +33,7 @@ from .fomm_loader import load_checkpoint
from .partswap_inference import partswap_inference
from .partswap_loader import load_partswap_checkpoint
from .seg_viz import visualize_frame
from .spline_inference import spline_inference
from .utils import (
build_seg_arguments,
get_config_path,
@@ -407,6 +412,82 @@ class Articulate_Runner:
)
class Spline_Runner:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"source_image": ("IMAGE",),
"driving_video_input": ("IMAGE",),
"frame_rate": ("FLOAT", {"default": 30.0}),
"predict_mode": (SPLINE_MODES, {"default": SPLINE_DEFAULT}),
"find_best_frame": (
"BOOLEAN",
{"default": False},
), # Generate from the frame that is the most alligned with source
},
"optional": {"audio": ("AUDIO",)},
}
RETURN_TYPES = (
"IMAGE",
"AUDIO",
"FLOAT",
)
RETURN_NAMES = (
"images",
"audio",
"frame_rate",
)
FUNCTION = "todo"
CATEGORY = "FirstOrderMM"
def todo(
self,
source_image,
driving_video_input,
frame_rate: float,
predict_mode: str,
find_best_frame: bool,
audio=None,
):
print(f"{type(source_image)=}")
print(f"{type(driving_video_input)=}")
print(f"{source_image.shape=}")
print(f"{driving_video_input.shape=}")
print(f"{type(audio)=}")
print(base_dir)
config_path = f"{base_dir}/{SPLINE_CFG_PATH}"
checkpoint_path = f"{base_dir}/{SPLINE_MODEL_PATH}"
source_image = reshape_image(source_image, (256, 256))
driving_video = reshape_image(driving_video_input, (256, 256)).unsqueeze(0)
driving_video = driving_video.permute(0, 2, 1, 3, 4)
print("After reshaping")
print(f"{source_image.shape=}")
print(f"{driving_video.shape=}")
params = {
"source_image": source_image,
"driving_video": driving_video,
"config_path": config_path,
"checkpoint_path": checkpoint_path,
"predict_mode": predict_mode,
"find_best_frame": find_best_frame,
}
predictions = spline_inference(**params)
output_images = out_video(predictions)
return (
output_images,
audio,
frame_rate,
)
def serialize_integers(int_list):
return "_".join(map(str, int_list))
+203
View File
@@ -0,0 +1,203 @@
import torch
import numpy as np
from scipy.spatial import ConvexHull
import yaml
import tqdm
from comfy.utils import ProgressBar
from .fomm_inference import find_best_frame
from .spline_module.avd_network import AVDNetwork
from .spline_module.dense_motion import DenseMotionNetwork
from .spline_module.keypoint_detector import KPDetector
from .spline_module.inpainting_network import InpaintingNetwork
def spline_inference(
source_image,
driving_video: list,
config_path: str,
checkpoint_path: str,
predict_mode="relative",
find_best_frame=False,
cpu=False,
):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
inpainting, kp_detector, dense_motion_network, avd_network = load_checkpoints(
config_path, checkpoint_path, device
)
source_image = source_image.to(device)
driving_video = driving_video.to(device)
if find_best_frame:
predictions = spline_inference_best_frame(
source_image,
driving_video,
inpainting,
kp_detector,
dense_motion_network,
avd_network,
predict_mode,
cpu,
)
else:
predictions = inference(
source_image,
driving_video,
inpainting,
kp_detector,
dense_motion_network,
avd_network,
predict_mode,
)
return predictions
def spline_inference_best_frame(
source_image,
driving_video: list,
inpainting,
kp_detector,
dense_motion_network,
avd_network,
predict_mode,
cpu=False,
):
best_frame_idx = find_best_frame(source_image, driving_video, cpu)
first_half = driving_video[
:, :, : best_frame_idx + 1
] # Include the best frame in the first half
first_half = torch.flip(first_half, dims=[2]) # Reverse the first half
second_half = driving_video[:, :, best_frame_idx + 1 :]
predictions_first = inference(
source_image,
first_half,
inpainting,
kp_detector,
dense_motion_network,
avd_network,
predict_mode,
)
predictions_second = inference(
source_image,
second_half,
inpainting,
kp_detector,
dense_motion_network,
avd_network,
predict_mode,
)
predictions = []
predictions_first = predictions_first[::-1] # Reverse the first half back
predictions.extend(predictions_first)
predictions.extend(predictions_second)
return predictions
def inference(
source_image,
driving_video: list,
inpainting,
kp_detector,
dense_motion_network,
avd_network,
predict_mode,
):
with torch.no_grad():
source = source_image
driving = driving_video
predictions = []
kp_source = kp_detector(source)
kp_driving_initial = kp_detector(driving[:, :, 0])
num_frames = driving.shape[2]
pbar = ProgressBar(num_frames)
for frame_idx in tqdm.tqdm(range(num_frames)):
driving_frame = driving[:, :, frame_idx]
kp_driving = kp_detector(driving_frame)
if predict_mode == "standard":
kp_norm = kp_driving
elif predict_mode == "relative":
kp_norm = relative_kp(
kp_source=kp_source,
kp_driving=kp_driving,
kp_driving_initial=kp_driving_initial,
)
elif predict_mode == "avd":
kp_norm = avd_network(kp_source, kp_driving)
dense_motion = dense_motion_network(
source_image=source,
kp_driving=kp_norm,
kp_source=kp_source,
bg_param=None,
dropout_flag=False,
)
out = inpainting(source, dense_motion)
out_np = np.transpose(out["prediction"].data.cpu().numpy(), [0, 2, 3, 1])[0]
predictions.append(out_np)
pbar.update_absolute(frame_idx, num_frames)
return predictions
def relative_kp(kp_source, kp_driving, kp_driving_initial):
source_area = ConvexHull(kp_source["fg_kp"][0].data.cpu().numpy()).volume
driving_area = ConvexHull(kp_driving_initial["fg_kp"][0].data.cpu().numpy()).volume
adapt_movement_scale = np.sqrt(source_area) / np.sqrt(driving_area)
kp_new = {k: v for k, v in kp_driving.items()}
kp_value_diff = kp_driving["fg_kp"] - kp_driving_initial["fg_kp"]
kp_value_diff *= adapt_movement_scale
kp_new["fg_kp"] = kp_value_diff + kp_source["fg_kp"]
return kp_new
def load_checkpoints(config_path, checkpoint_path, device):
with open(config_path) as f:
config = yaml.full_load(f)
inpainting = InpaintingNetwork(
**config["model_params"]["generator_params"],
**config["model_params"]["common_params"],
)
kp_detector = KPDetector(**config["model_params"]["common_params"])
dense_motion_network = DenseMotionNetwork(
**config["model_params"]["common_params"],
**config["model_params"]["dense_motion_params"],
)
avd_network = AVDNetwork(
num_tps=config["model_params"]["common_params"]["num_tps"],
**config["model_params"]["avd_network_params"],
)
kp_detector.to(device)
dense_motion_network.to(device)
inpainting.to(device)
avd_network.to(device)
checkpoint = torch.load(checkpoint_path, map_location=device)
inpainting.load_state_dict(checkpoint["inpainting_network"])
kp_detector.load_state_dict(checkpoint["kp_detector"])
dense_motion_network.load_state_dict(checkpoint["dense_motion_network"])
if "avd_network" in checkpoint:
avd_network.load_state_dict(checkpoint["avd_network"])
inpainting.eval()
kp_detector.eval()
dense_motion_network.eval()
avd_network.eval()
return inpainting, kp_detector, dense_motion_network, avd_network
+63
View File
@@ -0,0 +1,63 @@
import torch
from torch import nn
class AVDNetwork(nn.Module):
"""
Animation via Disentanglement network
"""
def __init__(self, num_tps, id_bottle_size=64, pose_bottle_size=64):
super(AVDNetwork, self).__init__()
input_size = 5 * 2 * num_tps
self.num_tps = num_tps
self.id_encoder = nn.Sequential(
nn.Linear(input_size, 256),
nn.BatchNorm1d(256),
nn.ReLU(inplace=True),
nn.Linear(256, 512),
nn.BatchNorm1d(512),
nn.ReLU(inplace=True),
nn.Linear(512, 1024),
nn.BatchNorm1d(1024),
nn.ReLU(inplace=True),
nn.Linear(1024, id_bottle_size),
)
self.pose_encoder = nn.Sequential(
nn.Linear(input_size, 256),
nn.BatchNorm1d(256),
nn.ReLU(inplace=True),
nn.Linear(256, 512),
nn.BatchNorm1d(512),
nn.ReLU(inplace=True),
nn.Linear(512, 1024),
nn.BatchNorm1d(1024),
nn.ReLU(inplace=True),
nn.Linear(1024, pose_bottle_size),
)
self.decoder = nn.Sequential(
nn.Linear(pose_bottle_size + id_bottle_size, 1024),
nn.BatchNorm1d(1024),
nn.ReLU(),
nn.Linear(1024, 512),
nn.BatchNorm1d(512),
nn.ReLU(),
nn.Linear(512, 256),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Linear(256, input_size),
)
def forward(self, kp_source, kp_random):
bs = kp_source["fg_kp"].shape[0]
pose_emb = self.pose_encoder(kp_random["fg_kp"].view(bs, -1))
id_emb = self.id_encoder(kp_source["fg_kp"].view(bs, -1))
rec = self.decoder(torch.cat([pose_emb, id_emb], dim=1))
rec = {"fg_kp": rec.view(bs, self.num_tps * 5, -1)}
return rec
+29
View File
@@ -0,0 +1,29 @@
import torch
from torch import nn
from torchvision import models
class BGMotionPredictor(nn.Module):
"""
Module for background estimation, return single transformation, parametrized as 3x3 matrix. The third row is [0 0 1]
"""
def __init__(self):
super(BGMotionPredictor, self).__init__()
self.bg_encoder = models.resnet18(pretrained=False)
self.bg_encoder.conv1 = nn.Conv2d(
6, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False
)
num_features = self.bg_encoder.fc.in_features
self.bg_encoder.fc = nn.Linear(num_features, 6)
self.bg_encoder.fc.weight.data.zero_()
self.bg_encoder.fc.bias.data.copy_(
torch.tensor([1, 0, 0, 0, 1, 0], dtype=torch.float)
)
def forward(self, source_image, driving_image):
bs = source_image.shape[0]
out = torch.eye(3).unsqueeze(0).repeat(bs, 1, 1).type(source_image.type())
prediction = self.bg_encoder(torch.cat([source_image, driving_image], dim=1))
out[:, :2, :] = prediction.view(bs, 2, 3)
return out
+75
View File
@@ -0,0 +1,75 @@
dataset_params:
root_dir: ../moving-gif
frame_shape: null
id_sampling: False
augmentation_params:
flip_param:
horizontal_flip: True
time_flip: True
crop_param:
size: [256, 256]
resize_param:
ratio: [0.9, 1.1]
jitter_param:
hue: 0.5
model_params:
common_params:
num_tps: 10
num_channels: 3
bg: False
multi_mask: True
generator_params:
block_expansion: 64
max_features: 512
num_down_blocks: 3
dense_motion_params:
block_expansion: 64
max_features: 1024
num_blocks: 5
scale_factor: 0.25
avd_network_params:
id_bottle_size: 128
pose_bottle_size: 128
train_params:
num_epochs: 100
num_repeats: 50
epoch_milestones: [70, 90]
lr_generator: 2.0e-4
batch_size: 28
scales: [1, 0.5, 0.25, 0.125]
dataloader_workers: 12
checkpoint_freq: 50
dropout_epoch: 35
dropout_maxp: 0.5
dropout_startp: 0.2
dropout_inc_epoch: 10
bg_start: 0
transform_params:
sigma_affine: 0.05
sigma_tps: 0.005
points_tps: 5
loss_weights:
perceptual: [10, 10, 10, 10, 10]
equivariance_value: 10
warp_loss: 10
bg: 10
train_avd_params:
num_epochs: 100
num_repeats: 50
batch_size: 256
dataloader_workers: 24
checkpoint_freq: 10
epoch_milestones: [70, 90]
lr: 1.0e-3
lambda_shift: 1
lambda_affine: 1
random_scale: 0.25
visualizer_params:
kp_size: 5
draw_border: True
colormap: 'gist_rainbow'
+134
View File
@@ -0,0 +1,134 @@
# Dataset parameters
# Each dataset should contain 2 folders train and test
# Each video can be represented as:
# - an image of concatenated frames
# - '.mp4' or '.gif'
# - folder with all frames from a specific video
# In case of Taichi. Same (youtube) video can be splitted in many parts (chunks). Each part has a following
# format (id)#other#info.mp4. For example '12335#adsbf.mp4' has an id 12335. In case of TaiChi id stands for youtube
# video id.
dataset_params:
# Path to data, data can be stored in several formats: .mp4 or .gif videos, stacked .png images or folders with frames.
root_dir: ../taichi
# Image shape, needed for staked .png format.
frame_shape: null
# In case of TaiChi single video can be splitted in many chunks, or the maybe several videos for single person.
# In this case epoch can be a pass over different videos (if id_sampling=True) or over different chunks (if id_sampling=False)
# If the name of the video '12335#adsbf.mp4' the id is assumed to be 12335
id_sampling: True
# Augmentation parameters see augmentation.py for all posible augmentations
augmentation_params:
flip_param:
horizontal_flip: True
time_flip: True
jitter_param:
brightness: 0.1
contrast: 0.1
saturation: 0.1
hue: 0.1
# Defines model architecture
model_params:
common_params:
# Number of TPS transformation
num_tps: 10
# Number of channels per image
num_channels: 3
# Whether to estimate affine background transformation
bg: True
# Whether to estimate the multi-resolution occlusion masks
multi_mask: True
generator_params:
# Number of features mutliplier
block_expansion: 64
# Maximum allowed number of features
max_features: 512
# Number of downsampling blocks and Upsampling blocks.
num_down_blocks: 3
dense_motion_params:
# Number of features mutliplier
block_expansion: 64
# Maximum allowed number of features
max_features: 1024
# Number of block in Unet.
num_blocks: 5
# Optical flow is predicted on smaller images for better performance,
# scale_factor=0.25 means that 256x256 image will be resized to 64x64
scale_factor: 0.25
avd_network_params:
# Bottleneck for identity branch
id_bottle_size: 128
# Bottleneck for pose branch
pose_bottle_size: 128
# Parameters of training
train_params:
# Number of training epochs
num_epochs: 100
# For better i/o performance when number of videos is small number of epochs can be multiplied by this number.
# Thus effectivlly with num_repeats=100 each epoch is 100 times larger.
num_repeats: 150
# Drop learning rate by 10 times after this epochs
epoch_milestones: [70, 90]
# Initial learing rate for all modules
lr_generator: 2.0e-4
batch_size: 28
# Scales for perceptual pyramide loss. If scales = [1, 0.5, 0.25, 0.125] and image resolution is 256x256,
# than the loss will be computer on resolutions 256x256, 128x128, 64x64, 32x32.
scales: [1, 0.5, 0.25, 0.125]
# Dataset preprocessing cpu workers
dataloader_workers: 12
# Save checkpoint this frequently. If checkpoint_freq=50, checkpoint will be saved every 50 epochs.
checkpoint_freq: 50
# Parameters of dropout
# The first dropout_epoch training uses dropout operation
dropout_epoch: 35
# The probability P will linearly increase from dropout_startp to dropout_maxp in dropout_inc_epoch epochs
dropout_maxp: 0.7
dropout_startp: 0.0
dropout_inc_epoch: 10
# Estimate affine background transformation from the bg_start epoch.
bg_start: 0
# Parameters of random TPS transformation for equivariance loss
transform_params:
# Sigma for affine part
sigma_affine: 0.05
# Sigma for deformation part
sigma_tps: 0.005
# Number of point in the deformation grid
points_tps: 5
loss_weights:
# Weights for perceptual loss.
perceptual: [10, 10, 10, 10, 10]
# Weights for value equivariance.
equivariance_value: 10
# Weights for warp loss.
warp_loss: 10
# Weights for bg loss.
bg: 10
# Parameters of training (animation-via-disentanglement)
train_avd_params:
# Number of training epochs, visualization is produced after each epoch.
num_epochs: 100
# For better i/o performance when number of videos is small number of epochs can be multiplied by this number.
# Thus effectively with num_repeats=100 each epoch is 100 times larger.
num_repeats: 150
# Batch size.
batch_size: 256
# Save checkpoint this frequently. If checkpoint_freq=50, checkpoint will be saved every 50 epochs.
checkpoint_freq: 10
# Dataset preprocessing cpu workers
dataloader_workers: 24
# Drop learning rate 10 times after this epochs
epoch_milestones: [70, 90]
# Initial learning rate
lr: 1.0e-3
# Weights for equivariance loss.
lambda_shift: 1
random_scale: 0.25
visualizer_params:
kp_size: 5
draw_border: True
colormap: 'gist_rainbow'
+73
View File
@@ -0,0 +1,73 @@
dataset_params:
root_dir: ../TED384-v2
frame_shape: null
id_sampling: True
augmentation_params:
flip_param:
horizontal_flip: True
time_flip: True
jitter_param:
brightness: 0.1
contrast: 0.1
saturation: 0.1
hue: 0.1
model_params:
common_params:
num_tps: 10
num_channels: 3
bg: True
multi_mask: True
generator_params:
block_expansion: 64
max_features: 512
num_down_blocks: 3
dense_motion_params:
block_expansion: 64
max_features: 1024
num_blocks: 5
scale_factor: 0.25
avd_network_params:
id_bottle_size: 128
pose_bottle_size: 128
train_params:
num_epochs: 100
num_repeats: 150
epoch_milestones: [70, 90]
lr_generator: 2.0e-4
batch_size: 12
scales: [1, 0.5, 0.25, 0.125]
dataloader_workers: 6
checkpoint_freq: 50
dropout_epoch: 35
dropout_maxp: 0.5
dropout_startp: 0.0
dropout_inc_epoch: 10
bg_start: 0
transform_params:
sigma_affine: 0.05
sigma_tps: 0.005
points_tps: 5
loss_weights:
perceptual: [10, 10, 10, 10, 10]
equivariance_value: 10
warp_loss: 10
bg: 10
train_avd_params:
num_epochs: 30
num_repeats: 500
batch_size: 256
dataloader_workers: 24
checkpoint_freq: 10
epoch_milestones: [20, 25]
lr: 1.0e-3
lambda_shift: 1
random_scale: 0.25
visualizer_params:
kp_size: 5
draw_border: True
colormap: 'gist_rainbow'
+74
View File
@@ -0,0 +1,74 @@
dataset_params:
root_dir: ../vox
frame_shape: null
id_sampling: True
augmentation_params:
flip_param:
horizontal_flip: True
time_flip: True
jitter_param:
brightness: 0.1
contrast: 0.1
saturation: 0.1
hue: 0.1
model_params:
common_params:
num_tps: 10
num_channels: 3
bg: True
multi_mask: True
generator_params:
block_expansion: 64
max_features: 512
num_down_blocks: 3
dense_motion_params:
block_expansion: 64
max_features: 1024
num_blocks: 5
scale_factor: 0.25
avd_network_params:
id_bottle_size: 128
pose_bottle_size: 128
train_params:
num_epochs: 100
num_repeats: 75
epoch_milestones: [70, 90]
lr_generator: 2.0e-4
batch_size: 28
scales: [1, 0.5, 0.25, 0.125]
dataloader_workers: 12
checkpoint_freq: 50
dropout_epoch: 35
dropout_maxp: 0.3
dropout_startp: 0.1
dropout_inc_epoch: 10
bg_start: 10
transform_params:
sigma_affine: 0.05
sigma_tps: 0.005
points_tps: 5
loss_weights:
perceptual: [10, 10, 10, 10, 10]
equivariance_value: 10
warp_loss: 10
bg: 10
train_avd_params:
num_epochs: 200
num_repeats: 300
batch_size: 256
dataloader_workers: 24
checkpoint_freq: 50
epoch_milestones: [140, 180]
lr: 1.0e-3
lambda_shift: 1
random_scale: 0.25
visualizer_params:
kp_size: 5
draw_border: True
colormap: 'gist_rainbow'
+241
View File
@@ -0,0 +1,241 @@
import math
import torch
import torch.nn.functional as F
from torch import nn
from .util import (
TPS,
AntiAliasInterpolation2d,
Hourglass,
UpBlock2d,
from_homogeneous,
kp2gaussian,
make_coordinate_grid,
to_homogeneous,
)
class DenseMotionNetwork(nn.Module):
"""
Module that estimating an optical flow and multi-resolution occlusion masks
from K TPS transformations and an affine transformation.
"""
def __init__(
self,
block_expansion,
num_blocks,
max_features,
num_tps,
num_channels,
scale_factor=0.25,
bg=False,
multi_mask=True,
kp_variance=0.01,
):
super(DenseMotionNetwork, self).__init__()
if scale_factor != 1:
self.down = AntiAliasInterpolation2d(num_channels, scale_factor)
self.scale_factor = scale_factor
self.multi_mask = multi_mask
self.hourglass = Hourglass(
block_expansion=block_expansion,
in_features=(num_channels * (num_tps + 1) + num_tps * 5 + 1),
max_features=max_features,
num_blocks=num_blocks,
)
hourglass_output_size = self.hourglass.out_channels
self.maps = nn.Conv2d(
hourglass_output_size[-1], num_tps + 1, kernel_size=(7, 7), padding=(3, 3)
)
if multi_mask:
up = []
self.up_nums = int(math.log(1 / scale_factor, 2))
self.occlusion_num = 4
channel = [hourglass_output_size[-1] // (2**i) for i in range(self.up_nums)]
for i in range(self.up_nums):
up.append(
UpBlock2d(channel[i], channel[i] // 2, kernel_size=3, padding=1)
)
self.up = nn.ModuleList(up)
channel = [
hourglass_output_size[-i - 1]
for i in range(self.occlusion_num - self.up_nums)[::-1]
]
for i in range(self.up_nums):
channel.append(hourglass_output_size[-1] // (2 ** (i + 1)))
occlusion = []
for i in range(self.occlusion_num):
occlusion.append(
nn.Conv2d(channel[i], 1, kernel_size=(7, 7), padding=(3, 3))
)
self.occlusion = nn.ModuleList(occlusion)
else:
occlusion = [
nn.Conv2d(
hourglass_output_size[-1], 1, kernel_size=(7, 7), padding=(3, 3)
)
]
self.occlusion = nn.ModuleList(occlusion)
self.num_tps = num_tps
self.bg = bg
self.kp_variance = kp_variance
def create_heatmap_representations(self, source_image, kp_driving, kp_source):
spatial_size = source_image.shape[2:]
gaussian_driving = kp2gaussian(
kp_driving["fg_kp"], spatial_size=spatial_size, kp_variance=self.kp_variance
)
gaussian_source = kp2gaussian(
kp_source["fg_kp"], spatial_size=spatial_size, kp_variance=self.kp_variance
)
heatmap = gaussian_driving - gaussian_source
zeros = (
torch.zeros(heatmap.shape[0], 1, spatial_size[0], spatial_size[1])
.type(heatmap.type())
.to(heatmap.device)
)
heatmap = torch.cat([zeros, heatmap], dim=1)
return heatmap
def create_transformations(self, source_image, kp_driving, kp_source, bg_param):
# K TPS transformaions
bs, _, h, w = source_image.shape
kp_1 = kp_driving["fg_kp"]
kp_2 = kp_source["fg_kp"]
kp_1 = kp_1.view(bs, -1, 5, 2)
kp_2 = kp_2.view(bs, -1, 5, 2)
trans = TPS(mode="kp", bs=bs, kp_1=kp_1, kp_2=kp_2)
driving_to_source = trans.transform_frame(source_image)
identity_grid = make_coordinate_grid((h, w), type=kp_1.type()).to(kp_1.device)
identity_grid = identity_grid.view(1, 1, h, w, 2)
identity_grid = identity_grid.repeat(bs, 1, 1, 1, 1)
# affine background transformation
if bg_param is not None:
identity_grid = to_homogeneous(identity_grid)
identity_grid = torch.matmul(
bg_param.view(bs, 1, 1, 1, 3, 3), identity_grid.unsqueeze(-1)
).squeeze(-1)
identity_grid = from_homogeneous(identity_grid)
transformations = torch.cat([identity_grid, driving_to_source], dim=1)
return transformations
def create_deformed_source_image(self, source_image, transformations):
bs, _, h, w = source_image.shape
source_repeat = (
source_image.unsqueeze(1)
.unsqueeze(1)
.repeat(1, self.num_tps + 1, 1, 1, 1, 1)
)
source_repeat = source_repeat.view(bs * (self.num_tps + 1), -1, h, w)
transformations = transformations.view((bs * (self.num_tps + 1), h, w, -1))
deformed = F.grid_sample(source_repeat, transformations, align_corners=True)
deformed = deformed.view((bs, self.num_tps + 1, -1, h, w))
return deformed
def dropout_softmax(self, X, P):
"""
Dropout for TPS transformations. Eq(7) and Eq(8) in the paper.
"""
drop = (
(torch.rand(X.shape[0], X.shape[1]) < (1 - P)).type(X.type()).to(X.device)
)
drop[..., 0] = 1
drop = drop.repeat(X.shape[2], X.shape[3], 1, 1).permute(2, 3, 0, 1)
maxx = X.max(1).values.unsqueeze_(1)
X = X - maxx
X_exp = X.exp()
X[:, 1:, ...] /= 1 - P
mask_bool = drop == 0
X_exp = X_exp.masked_fill(mask_bool, 0)
partition = X_exp.sum(dim=1, keepdim=True) + 1e-6
return X_exp / partition
def forward(
self,
source_image,
kp_driving,
kp_source,
bg_param=None,
dropout_flag=False,
dropout_p=0,
):
if self.scale_factor != 1:
source_image = self.down(source_image)
bs, _, h, w = source_image.shape
out_dict = dict()
heatmap_representation = self.create_heatmap_representations(
source_image, kp_driving, kp_source
)
transformations = self.create_transformations(
source_image, kp_driving, kp_source, bg_param
)
deformed_source = self.create_deformed_source_image(
source_image, transformations
)
out_dict["deformed_source"] = deformed_source
# out_dict['transformations'] = transformations
deformed_source = deformed_source.view(bs, -1, h, w)
input = torch.cat([heatmap_representation, deformed_source], dim=1)
input = input.view(bs, -1, h, w)
prediction = self.hourglass(input, mode=1)
contribution_maps = self.maps(prediction[-1])
if dropout_flag:
contribution_maps = self.dropout_softmax(contribution_maps, dropout_p)
else:
contribution_maps = F.softmax(contribution_maps, dim=1)
out_dict["contribution_maps"] = contribution_maps
# Combine the K+1 transformations
# Eq(6) in the paper
contribution_maps = contribution_maps.unsqueeze(2)
transformations = transformations.permute(0, 1, 4, 2, 3)
deformation = (transformations * contribution_maps).sum(dim=1)
deformation = deformation.permute(0, 2, 3, 1)
out_dict["deformation"] = deformation # Optical Flow
occlusion_map = []
if self.multi_mask:
for i in range(self.occlusion_num - self.up_nums):
occlusion_map.append(
torch.sigmoid(
self.occlusion[i](
prediction[self.up_nums - self.occlusion_num + i]
)
)
)
prediction = prediction[-1]
for i in range(self.up_nums):
prediction = self.up[i](prediction)
occlusion_map.append(
torch.sigmoid(
self.occlusion[i + self.occlusion_num - self.up_nums](
prediction
)
)
)
else:
occlusion_map.append(torch.sigmoid(self.occlusion[0](prediction[-1])))
out_dict["occlusion_map"] = occlusion_map # Multi-resolution Occlusion Masks
return out_dict
+164
View File
@@ -0,0 +1,164 @@
import torch
import torch.nn.functional as F
from torch import nn
from .util import DownBlock2d, ResBlock2d, SameBlock2d, UpBlock2d
class InpaintingNetwork(nn.Module):
"""
Inpaint the missing regions and reconstruct the Driving image.
"""
def __init__(
self,
num_channels,
block_expansion,
max_features,
num_down_blocks,
multi_mask=True,
**kwargs,
):
super(InpaintingNetwork, self).__init__()
self.num_down_blocks = num_down_blocks
self.multi_mask = multi_mask
self.first = SameBlock2d(
num_channels, block_expansion, kernel_size=(7, 7), padding=(3, 3)
)
down_blocks = []
up_blocks = []
resblock = []
for i in range(num_down_blocks):
in_features = min(max_features, block_expansion * (2**i))
out_features = min(max_features, block_expansion * (2 ** (i + 1)))
down_blocks.append(
DownBlock2d(
in_features, out_features, kernel_size=(3, 3), padding=(1, 1)
)
)
decoder_in_feature = out_features * 2
if i == num_down_blocks - 1:
decoder_in_feature = out_features
up_blocks.append(
UpBlock2d(
decoder_in_feature, in_features, kernel_size=(3, 3), padding=(1, 1)
)
)
resblock.append(
ResBlock2d(decoder_in_feature, kernel_size=(3, 3), padding=(1, 1))
)
resblock.append(
ResBlock2d(decoder_in_feature, kernel_size=(3, 3), padding=(1, 1))
)
self.down_blocks = nn.ModuleList(down_blocks)
self.up_blocks = nn.ModuleList(up_blocks[::-1])
self.resblock = nn.ModuleList(resblock[::-1])
self.final = nn.Conv2d(
block_expansion, num_channels, kernel_size=(7, 7), padding=(3, 3)
)
self.num_channels = num_channels
def deform_input(self, inp, deformation):
_, h_old, w_old, _ = deformation.shape
_, _, h, w = inp.shape
if h_old != h or w_old != w:
deformation = deformation.permute(0, 3, 1, 2)
deformation = F.interpolate(
deformation, size=(h, w), mode="bilinear", align_corners=True
)
deformation = deformation.permute(0, 2, 3, 1)
return F.grid_sample(inp, deformation, align_corners=True)
def occlude_input(self, inp, occlusion_map):
if not self.multi_mask:
if (
inp.shape[2] != occlusion_map.shape[2]
or inp.shape[3] != occlusion_map.shape[3]
):
occlusion_map = F.interpolate(
occlusion_map,
size=inp.shape[2:],
mode="bilinear",
align_corners=True,
)
out = inp * occlusion_map
return out
def forward(self, source_image, dense_motion):
out = self.first(source_image)
encoder_map = [out]
for i in range(len(self.down_blocks)):
out = self.down_blocks[i](out)
encoder_map.append(out)
output_dict = {}
output_dict["contribution_maps"] = dense_motion["contribution_maps"]
output_dict["deformed_source"] = dense_motion["deformed_source"]
occlusion_map = dense_motion["occlusion_map"]
output_dict["occlusion_map"] = occlusion_map
deformation = dense_motion["deformation"]
out_ij = self.deform_input(out.detach(), deformation)
out = self.deform_input(out, deformation)
out_ij = self.occlude_input(out_ij, occlusion_map[0].detach())
out = self.occlude_input(out, occlusion_map[0])
warped_encoder_maps = []
warped_encoder_maps.append(out_ij)
for i in range(self.num_down_blocks):
out = self.resblock[2 * i](out)
out = self.resblock[2 * i + 1](out)
out = self.up_blocks[i](out)
encode_i = encoder_map[-(i + 2)]
encode_ij = self.deform_input(encode_i.detach(), deformation)
encode_i = self.deform_input(encode_i, deformation)
occlusion_ind = 0
if self.multi_mask:
occlusion_ind = i + 1
encode_ij = self.occlude_input(
encode_ij, occlusion_map[occlusion_ind].detach()
)
encode_i = self.occlude_input(encode_i, occlusion_map[occlusion_ind])
warped_encoder_maps.append(encode_ij)
if i == self.num_down_blocks - 1:
break
out = torch.cat([out, encode_i], 1)
deformed_source = self.deform_input(source_image, deformation)
output_dict["deformed"] = deformed_source
output_dict["warped_encoder_maps"] = warped_encoder_maps
occlusion_last = occlusion_map[-1]
if not self.multi_mask:
occlusion_last = F.interpolate(
occlusion_last, size=out.shape[2:], mode="bilinear", align_corners=True
)
out = out * (1 - occlusion_last) + encode_i
out = self.final(out)
out = torch.sigmoid(out)
out = out * (1 - occlusion_last) + deformed_source * occlusion_last
output_dict["prediction"] = out
return output_dict
def get_encode(self, driver_image, occlusion_map):
out = self.first(driver_image)
encoder_map = []
encoder_map.append(self.occlude_input(out.detach(), occlusion_map[-1].detach()))
for i in range(len(self.down_blocks)):
out = self.down_blocks[i](out.detach())
out_mask = self.occlude_input(out.detach(), occlusion_map[2 - i].detach())
encoder_map.append(out_mask.detach())
return encoder_map
+29
View File
@@ -0,0 +1,29 @@
import torch
from torch import nn
from torchvision import models
class KPDetector(nn.Module):
"""
Predict K*5 keypoints.
"""
def __init__(self, num_tps, **kwargs):
super(KPDetector, self).__init__()
self.num_tps = num_tps
self.fg_encoder = models.resnet18(pretrained=False)
num_features = self.fg_encoder.fc.in_features
self.fg_encoder.fc = nn.Linear(num_features, num_tps * 5 * 2)
def forward(self, image):
fg_kp = self.fg_encoder(image)
(
bs,
_,
) = fg_kp.shape
fg_kp = torch.sigmoid(fg_kp)
fg_kp = fg_kp * 2 - 1
out = {"fg_kp": fg_kp.view(bs, self.num_tps * 5, -1)}
return out
+222
View File
@@ -0,0 +1,222 @@
import numpy as np
import torch
import torch.nn.functional as F
from torch import nn
from torchvision import models
from .util import TPS, AntiAliasInterpolation2d
class Vgg19(torch.nn.Module):
"""
Vgg19 network for perceptual loss. See Sec 3.3.
"""
def __init__(self, requires_grad=False):
super(Vgg19, self).__init__()
vgg_pretrained_features = models.vgg19(pretrained=True).features
self.slice1 = torch.nn.Sequential()
self.slice2 = torch.nn.Sequential()
self.slice3 = torch.nn.Sequential()
self.slice4 = torch.nn.Sequential()
self.slice5 = torch.nn.Sequential()
for x in range(2):
self.slice1.add_module(str(x), vgg_pretrained_features[x])
for x in range(2, 7):
self.slice2.add_module(str(x), vgg_pretrained_features[x])
for x in range(7, 12):
self.slice3.add_module(str(x), vgg_pretrained_features[x])
for x in range(12, 21):
self.slice4.add_module(str(x), vgg_pretrained_features[x])
for x in range(21, 30):
self.slice5.add_module(str(x), vgg_pretrained_features[x])
self.mean = torch.nn.Parameter(
data=torch.Tensor(np.array([0.485, 0.456, 0.406]).reshape((1, 3, 1, 1))),
requires_grad=False,
)
self.std = torch.nn.Parameter(
data=torch.Tensor(np.array([0.229, 0.224, 0.225]).reshape((1, 3, 1, 1))),
requires_grad=False,
)
if not requires_grad:
for param in self.parameters():
param.requires_grad = False
def forward(self, X):
X = (X - self.mean) / self.std
h_relu1 = self.slice1(X)
h_relu2 = self.slice2(h_relu1)
h_relu3 = self.slice3(h_relu2)
h_relu4 = self.slice4(h_relu3)
h_relu5 = self.slice5(h_relu4)
out = [h_relu1, h_relu2, h_relu3, h_relu4, h_relu5]
return out
class ImagePyramide(torch.nn.Module):
"""
Create image pyramide for computing pyramide perceptual loss. See Sec 3.3
"""
def __init__(self, scales, num_channels):
super(ImagePyramide, self).__init__()
downs = {}
for scale in scales:
downs[str(scale).replace(".", "-")] = AntiAliasInterpolation2d(
num_channels, scale
)
self.downs = nn.ModuleDict(downs)
def forward(self, x):
out_dict = {}
for scale, down_module in self.downs.items():
out_dict["prediction_" + str(scale).replace("-", ".")] = down_module(x)
return out_dict
def detach_kp(kp):
return {key: value.detach() for key, value in kp.items()}
class GeneratorFullModel(torch.nn.Module):
"""
Merge all generator related updates into single model for better multi-gpu usage
"""
def __init__(
self,
kp_extractor,
bg_predictor,
dense_motion_network,
inpainting_network,
train_params,
*kwargs,
):
super(GeneratorFullModel, self).__init__()
self.kp_extractor = kp_extractor
self.inpainting_network = inpainting_network
self.dense_motion_network = dense_motion_network
self.bg_predictor = None
if bg_predictor:
self.bg_predictor = bg_predictor
self.bg_start = train_params["bg_start"]
self.train_params = train_params
self.scales = train_params["scales"]
self.pyramid = ImagePyramide(self.scales, inpainting_network.num_channels)
if torch.cuda.is_available():
self.pyramid = self.pyramid.cuda()
self.loss_weights = train_params["loss_weights"]
self.dropout_epoch = train_params["dropout_epoch"]
self.dropout_maxp = train_params["dropout_maxp"]
self.dropout_inc_epoch = train_params["dropout_inc_epoch"]
self.dropout_startp = train_params["dropout_startp"]
if sum(self.loss_weights["perceptual"]) != 0:
self.vgg = Vgg19()
if torch.cuda.is_available():
self.vgg = self.vgg.cuda()
def forward(self, x, epoch):
kp_source = self.kp_extractor(x["source"])
kp_driving = self.kp_extractor(x["driving"])
bg_param = None
if self.bg_predictor:
if epoch >= self.bg_start:
bg_param = self.bg_predictor(x["source"], x["driving"])
if epoch >= self.dropout_epoch:
dropout_flag = False
dropout_p = 0
else:
# dropout_p will linearly increase from dropout_startp to dropout_maxp
dropout_flag = True
dropout_p = min(
epoch / self.dropout_inc_epoch * self.dropout_maxp
+ self.dropout_startp,
self.dropout_maxp,
)
dense_motion = self.dense_motion_network(
source_image=x["source"],
kp_driving=kp_driving,
kp_source=kp_source,
bg_param=bg_param,
dropout_flag=dropout_flag,
dropout_p=dropout_p,
)
generated = self.inpainting_network(x["source"], dense_motion)
generated.update({"kp_source": kp_source, "kp_driving": kp_driving})
loss_values = {}
pyramide_real = self.pyramid(x["driving"])
pyramide_generated = self.pyramid(generated["prediction"])
# reconstruction loss
if sum(self.loss_weights["perceptual"]) != 0:
value_total = 0
for scale in self.scales:
x_vgg = self.vgg(pyramide_generated["prediction_" + str(scale)])
y_vgg = self.vgg(pyramide_real["prediction_" + str(scale)])
for i, weight in enumerate(self.loss_weights["perceptual"]):
value = torch.abs(x_vgg[i] - y_vgg[i].detach()).mean()
value_total += self.loss_weights["perceptual"][i] * value
loss_values["perceptual"] = value_total
# equivariance loss
if self.loss_weights["equivariance_value"] != 0:
transform_random = TPS(
mode="random",
bs=x["driving"].shape[0],
**self.train_params["transform_params"],
)
transform_grid = transform_random.transform_frame(x["driving"])
transformed_frame = F.grid_sample(
x["driving"],
transform_grid,
padding_mode="reflection",
align_corners=True,
)
transformed_kp = self.kp_extractor(transformed_frame)
generated["transformed_frame"] = transformed_frame
generated["transformed_kp"] = transformed_kp
warped = transform_random.warp_coordinates(transformed_kp["fg_kp"])
kp_d = kp_driving["fg_kp"]
value = torch.abs(kp_d - warped).mean()
loss_values["equivariance_value"] = (
self.loss_weights["equivariance_value"] * value
)
# warp loss
if self.loss_weights["warp_loss"] != 0:
occlusion_map = generated["occlusion_map"]
encode_map = self.inpainting_network.get_encode(x["driving"], occlusion_map)
decode_map = generated["warped_encoder_maps"]
value = 0
for i in range(len(encode_map)):
value += torch.abs(encode_map[i] - decode_map[-i - 1]).mean()
loss_values["warp_loss"] = self.loss_weights["warp_loss"] * value
# bg loss
if (
self.bg_predictor
and epoch >= self.bg_start
and self.loss_weights["bg"] != 0
):
bg_param_reverse = self.bg_predictor(x["driving"], x["source"])
value = torch.matmul(bg_param, bg_param_reverse)
eye = torch.eye(3).view(1, 1, 3, 3).type(value.type())
value = torch.abs(eye - value).mean()
loss_values["bg"] = self.loss_weights["bg"] * value
return loss_values, generated
View File
+412
View File
@@ -0,0 +1,412 @@
import torch
import torch.nn.functional as F
from torch import nn
class TPS:
"""
TPS transformation, mode 'kp' for Eq(2) in the paper, mode 'random' for equivariance loss.
"""
def __init__(self, mode, bs, **kwargs):
self.bs = bs
self.mode = mode
if mode == "random":
noise = torch.normal(
mean=0, std=kwargs["sigma_affine"] * torch.ones([bs, 2, 3])
)
self.theta = noise + torch.eye(2, 3).view(1, 2, 3)
self.control_points = make_coordinate_grid(
(kwargs["points_tps"], kwargs["points_tps"]), type=noise.type()
)
self.control_points = self.control_points.unsqueeze(0)
self.control_params = torch.normal(
mean=0,
std=kwargs["sigma_tps"]
* torch.ones([bs, 1, kwargs["points_tps"] ** 2]),
)
elif mode == "kp":
kp_1 = kwargs["kp_1"]
kp_2 = kwargs["kp_2"]
device = kp_1.device
kp_type = kp_1.type()
self.gs = kp_1.shape[1]
n = kp_1.shape[2]
K = torch.norm(kp_1[:, :, :, None] - kp_1[:, :, None, :], dim=4, p=2)
K = K**2
K = K * torch.log(K + 1e-9)
one1 = (
torch.ones(self.bs, kp_1.shape[1], kp_1.shape[2], 1)
.to(device)
.type(kp_type)
)
kp_1p = torch.cat([kp_1, one1], 3)
zero = torch.zeros(self.bs, kp_1.shape[1], 3, 3).to(device).type(kp_type)
P = torch.cat([kp_1p, zero], 2)
L = torch.cat([K, kp_1p.permute(0, 1, 3, 2)], 2)
L = torch.cat([L, P], 3)
zero = torch.zeros(self.bs, kp_1.shape[1], 3, 2).to(device).type(kp_type)
Y = torch.cat([kp_2, zero], 2)
one = torch.eye(L.shape[2]).expand(L.shape).to(device).type(kp_type) * 0.01
L = L + one
param = torch.matmul(torch.inverse(L), Y)
self.theta = param[:, :, n:, :].permute(0, 1, 3, 2)
self.control_points = kp_1
self.control_params = param[:, :, :n, :]
else:
raise Exception("Error TPS mode")
def transform_frame(self, frame):
grid = (
make_coordinate_grid(frame.shape[2:], type=frame.type())
.unsqueeze(0)
.to(frame.device)
)
grid = grid.view(1, frame.shape[2] * frame.shape[3], 2)
shape = [self.bs, frame.shape[2], frame.shape[3], 2]
if self.mode == "kp":
shape.insert(1, self.gs)
grid = self.warp_coordinates(grid).view(*shape)
return grid
def warp_coordinates(self, coordinates):
theta = self.theta.type(coordinates.type()).to(coordinates.device)
control_points = self.control_points.type(coordinates.type()).to(
coordinates.device
)
control_params = self.control_params.type(coordinates.type()).to(
coordinates.device
)
if self.mode == "kp":
transformed = (
torch.matmul(theta[:, :, :, :2], coordinates.permute(0, 2, 1))
+ theta[:, :, :, 2:]
)
distances = coordinates.view(
coordinates.shape[0], 1, 1, -1, 2
) - control_points.view(self.bs, control_points.shape[1], -1, 1, 2)
distances = distances**2
result = distances.sum(-1)
result = result * torch.log(result + 1e-9)
result = torch.matmul(result.permute(0, 1, 3, 2), control_params)
transformed = transformed.permute(0, 1, 3, 2) + result
elif self.mode == "random":
theta = theta.unsqueeze(1)
transformed = (
torch.matmul(theta[:, :, :, :2], coordinates.unsqueeze(-1))
+ theta[:, :, :, 2:]
)
transformed = transformed.squeeze(-1)
ances = coordinates.view(
coordinates.shape[0], -1, 1, 2
) - control_points.view(1, 1, -1, 2)
distances = ances**2
result = distances.sum(-1)
result = result * torch.log(result + 1e-9)
result = result * control_params
result = result.sum(dim=2).view(self.bs, coordinates.shape[1], 1)
transformed = transformed + result
else:
raise Exception("Error TPS mode")
return transformed
def kp2gaussian(kp, spatial_size, kp_variance):
"""
Transform a keypoint into gaussian like representation
"""
coordinate_grid = make_coordinate_grid(spatial_size, kp.type()).to(kp.device)
number_of_leading_dimensions = len(kp.shape) - 1
shape = (1,) * number_of_leading_dimensions + coordinate_grid.shape
coordinate_grid = coordinate_grid.view(*shape)
repeats = kp.shape[:number_of_leading_dimensions] + (1, 1, 1)
coordinate_grid = coordinate_grid.repeat(*repeats)
# Preprocess kp shape
shape = kp.shape[:number_of_leading_dimensions] + (1, 1, 2)
kp = kp.view(*shape)
mean_sub = coordinate_grid - kp
out = torch.exp(-0.5 * (mean_sub**2).sum(-1) / kp_variance)
return out
def make_coordinate_grid(spatial_size, type):
"""
Create a meshgrid [-1,1] x [-1,1] of given spatial_size.
"""
h, w = spatial_size
x = torch.arange(w).type(type)
y = torch.arange(h).type(type)
x = 2 * (x / (w - 1)) - 1
y = 2 * (y / (h - 1)) - 1
yy = y.view(-1, 1).repeat(1, w)
xx = x.view(1, -1).repeat(h, 1)
meshed = torch.cat([xx.unsqueeze_(2), yy.unsqueeze_(2)], 2)
return meshed
class ResBlock2d(nn.Module):
"""
Res block, preserve spatial resolution.
"""
def __init__(self, in_features, kernel_size, padding):
super(ResBlock2d, self).__init__()
self.conv1 = nn.Conv2d(
in_channels=in_features,
out_channels=in_features,
kernel_size=kernel_size,
padding=padding,
)
self.conv2 = nn.Conv2d(
in_channels=in_features,
out_channels=in_features,
kernel_size=kernel_size,
padding=padding,
)
self.norm1 = nn.InstanceNorm2d(in_features, affine=True)
self.norm2 = nn.InstanceNorm2d(in_features, affine=True)
def forward(self, x):
out = self.norm1(x)
out = F.relu(out)
out = self.conv1(out)
out = self.norm2(out)
out = F.relu(out)
out = self.conv2(out)
out += x
return out
class UpBlock2d(nn.Module):
"""
Upsampling block for use in decoder.
"""
def __init__(self, in_features, out_features, kernel_size=3, padding=1, groups=1):
super(UpBlock2d, self).__init__()
self.conv = nn.Conv2d(
in_channels=in_features,
out_channels=out_features,
kernel_size=kernel_size,
padding=padding,
groups=groups,
)
self.norm = nn.InstanceNorm2d(out_features, affine=True)
def forward(self, x):
out = F.interpolate(x, scale_factor=2)
out = self.conv(out)
out = self.norm(out)
out = F.relu(out)
return out
class DownBlock2d(nn.Module):
"""
Downsampling block for use in encoder.
"""
def __init__(self, in_features, out_features, kernel_size=3, padding=1, groups=1):
super(DownBlock2d, self).__init__()
self.conv = nn.Conv2d(
in_channels=in_features,
out_channels=out_features,
kernel_size=kernel_size,
padding=padding,
groups=groups,
)
self.norm = nn.InstanceNorm2d(out_features, affine=True)
self.pool = nn.AvgPool2d(kernel_size=(2, 2))
def forward(self, x):
out = self.conv(x)
out = self.norm(out)
out = F.relu(out)
out = self.pool(out)
return out
class SameBlock2d(nn.Module):
"""
Simple block, preserve spatial resolution.
"""
def __init__(self, in_features, out_features, groups=1, kernel_size=3, padding=1):
super(SameBlock2d, self).__init__()
self.conv = nn.Conv2d(
in_channels=in_features,
out_channels=out_features,
kernel_size=kernel_size,
padding=padding,
groups=groups,
)
self.norm = nn.InstanceNorm2d(out_features, affine=True)
def forward(self, x):
out = self.conv(x)
out = self.norm(out)
out = F.relu(out)
return out
class Encoder(nn.Module):
"""
Hourglass Encoder
"""
def __init__(self, block_expansion, in_features, num_blocks=3, max_features=256):
super(Encoder, self).__init__()
down_blocks = []
for i in range(num_blocks):
down_blocks.append(
DownBlock2d(
in_features
if i == 0
else min(max_features, block_expansion * (2**i)),
min(max_features, block_expansion * (2 ** (i + 1))),
kernel_size=3,
padding=1,
)
)
self.down_blocks = nn.ModuleList(down_blocks)
def forward(self, x):
outs = [x]
# print('encoder:' ,outs[-1].shape)
for down_block in self.down_blocks:
outs.append(down_block(outs[-1]))
# print('encoder:' ,outs[-1].shape)
return outs
class Decoder(nn.Module):
"""
Hourglass Decoder
"""
def __init__(self, block_expansion, in_features, num_blocks=3, max_features=256):
super(Decoder, self).__init__()
up_blocks = []
self.out_channels = []
for i in range(num_blocks)[::-1]:
in_filters = (1 if i == num_blocks - 1 else 2) * min(
max_features, block_expansion * (2 ** (i + 1))
)
self.out_channels.append(in_filters)
out_filters = min(max_features, block_expansion * (2**i))
up_blocks.append(
UpBlock2d(in_filters, out_filters, kernel_size=3, padding=1)
)
self.up_blocks = nn.ModuleList(up_blocks)
self.out_channels.append(block_expansion + in_features)
# self.out_filters = block_expansion + in_features
def forward(self, x, mode=0):
out = x.pop()
outs = []
for up_block in self.up_blocks:
out = up_block(out)
skip = x.pop()
out = torch.cat([out, skip], dim=1)
outs.append(out)
if mode == 0:
return out
else:
return outs
class Hourglass(nn.Module):
"""
Hourglass architecture.
"""
def __init__(self, block_expansion, in_features, num_blocks=3, max_features=256):
super(Hourglass, self).__init__()
self.encoder = Encoder(block_expansion, in_features, num_blocks, max_features)
self.decoder = Decoder(block_expansion, in_features, num_blocks, max_features)
self.out_channels = self.decoder.out_channels
# self.out_filters = self.decoder.out_filters
def forward(self, x, mode=0):
return self.decoder(self.encoder(x), mode)
class AntiAliasInterpolation2d(nn.Module):
"""
Band-limited downsampling, for better preservation of the input signal.
"""
def __init__(self, channels, scale):
super(AntiAliasInterpolation2d, self).__init__()
sigma = (1 / scale - 1) / 2
kernel_size = 2 * round(sigma * 4) + 1
self.ka = kernel_size // 2
self.kb = self.ka - 1 if kernel_size % 2 == 0 else self.ka
kernel_size = [kernel_size, kernel_size]
sigma = [sigma, sigma]
# The gaussian kernel is the product of the
# gaussian function of each dimension.
kernel = 1
meshgrids = torch.meshgrid(
[torch.arange(size, dtype=torch.float32) for size in kernel_size]
)
for size, std, mgrid in zip(kernel_size, sigma, meshgrids):
mean = (size - 1) / 2
kernel *= torch.exp(-((mgrid - mean) ** 2) / (2 * std**2))
# Make sure sum of values in gaussian kernel equals 1.
kernel = kernel / torch.sum(kernel)
# Reshape to depthwise convolutional weight
kernel = kernel.view(1, 1, *kernel.size())
kernel = kernel.repeat(channels, *[1] * (kernel.dim() - 1))
self.register_buffer("weight", kernel)
self.groups = channels
self.scale = scale
def forward(self, input):
if self.scale == 1.0:
return input
out = F.pad(input, (self.ka, self.kb, self.ka, self.kb))
out = F.conv2d(out, weight=self.weight, groups=self.groups)
out = F.interpolate(out, scale_factor=(self.scale, self.scale))
return out
def to_homogeneous(coordinates):
ones_shape = list(coordinates.shape)
ones_shape[-1] = 1
ones = torch.ones(ones_shape).type(coordinates.type())
return torch.cat([coordinates, ones], dim=-1)
def from_homogeneous(coordinates):
return coordinates[..., :2] / coordinates[..., 2:3]
+427
View File
@@ -0,0 +1,427 @@
{
"last_node_id": 6,
"last_link_id": 11,
"nodes": [
{
"id": 6,
"type": "VHS_VideoCombine",
"pos": [
1236,
163
],
"size": [
315,
615
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 11
},
{
"name": "audio",
"type": "AUDIO",
"link": 10
},
{
"name": "meta_batch",
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": null
},
{
"name": "frame_rate",
"type": "FLOAT",
"link": 9,
"widget": {
"name": "frame_rate"
}
}
],
"outputs": [
{
"name": "Filenames",
"type": "VHS_FILENAMES",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "VHS_VideoCombine"
},
"widgets_values": {
"frame_rate": 8,
"loop_count": 0,
"filename_prefix": "Spline",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 0,
"save_metadata": true,
"pingpong": false,
"save_output": true,
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "Spline_00005-audio.mp4",
"subfolder": "",
"type": "output",
"format": "video/h264-mp4",
"frame_rate": 29.970029970029966
}
}
}
},
{
"id": 5,
"type": "VHS_VideoInfoLoaded",
"pos": [
783,
373
],
"size": {
"0": 304.79998779296875,
"1": 106
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "video_info",
"type": "VHS_VIDEOINFO",
"link": 7
}
],
"outputs": [
{
"name": "fps🟦",
"type": "FLOAT",
"links": [
8
],
"shape": 3,
"slot_index": 0
},
{
"name": "frame_count🟦",
"type": "INT",
"links": null,
"shape": 3
},
{
"name": "duration🟦",
"type": "FLOAT",
"links": null,
"shape": 3
},
{
"name": "width🟦",
"type": "INT",
"links": null,
"shape": 3
},
{
"name": "height🟦",
"type": "INT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "VHS_VideoInfoLoaded"
},
"widgets_values": {}
},
{
"id": 2,
"type": "LoadImage",
"pos": [
145,
158
],
"size": [
315,
314
],
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4
],
"shape": 3
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"Rick1.png",
"image"
]
},
{
"id": 3,
"type": "VHS_LoadVideo",
"pos": [
485,
232
],
"size": [
235.1999969482422,
491.1999969482422
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "meta_batch",
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
5
],
"shape": 3,
"slot_index": 0
},
{
"name": "frame_count",
"type": "INT",
"links": null,
"shape": 3
},
{
"name": "audio",
"type": "AUDIO",
"links": [
6
],
"shape": 3,
"slot_index": 2
},
{
"name": "video_info",
"type": "VHS_VIDEOINFO",
"links": [
7
],
"shape": 3,
"slot_index": 3
}
],
"properties": {
"Node name for S&R": "VHS_LoadVideo"
},
"widgets_values": {
"video": "damedane.mp4",
"force_rate": 0,
"force_size": "Disabled",
"custom_width": 512,
"custom_height": 512,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1,
"choose video to upload": "image",
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"frame_load_cap": 0,
"skip_first_frames": 0,
"force_rate": 0,
"filename": "damedane.mp4",
"type": "input",
"format": "video/mp4",
"select_every_nth": 1
}
}
}
},
{
"id": 4,
"type": "Spline_Runner",
"pos": [
777,
161
],
"size": [
380.4000244140625,
146
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "source_image",
"type": "IMAGE",
"link": 4
},
{
"name": "driving_video_input",
"type": "IMAGE",
"link": 5
},
{
"name": "audio",
"type": "AUDIO",
"link": 6
},
{
"name": "frame_rate",
"type": "FLOAT",
"link": 8,
"widget": {
"name": "frame_rate"
}
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
11
],
"shape": 3,
"slot_index": 0
},
{
"name": "audio",
"type": "AUDIO",
"links": [
10
],
"shape": 3,
"slot_index": 1
},
{
"name": "frame_rate",
"type": "FLOAT",
"links": [
9
],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "Spline_Runner"
},
"widgets_values": [
30,
"relative",
false
]
}
],
"links": [
[
4,
2,
0,
4,
0,
"IMAGE"
],
[
5,
3,
0,
4,
1,
"IMAGE"
],
[
6,
3,
2,
4,
2,
"AUDIO"
],
[
7,
3,
3,
5,
0,
"VHS_VIDEOINFO"
],
[
8,
5,
0,
4,
3,
"FLOAT"
],
[
9,
4,
2,
6,
4,
"FLOAT"
],
[
10,
4,
1,
6,
1,
"AUDIO"
],
[
11,
4,
0,
6,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
-23,
68
]
}
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB