This commit is contained in:
KUN
2024-08-13 08:03:27 +08:00
commit 97698dbef4
6 changed files with 446 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# ComfyUI nodes to use M3net
https://github.com/SingleZombie/DiffusersExample/tree/main/ReplaceAttn
![image](preview.png)
## Install
- 推荐使用管理器 ComfyUI Manager 安装(On the Way)
- 手动安装:
1. `cd custom_nodes`
2. `git clone https://github.com/leeguandong/ComfyUI_VideoEditing.git`
3. 重启 ComfyUI
## Use
- 节点:
- "Load Video to Images": 将视频转成图片
- "Image to Canny": controlnet使用的canny算子
- "ControlNet Model Loader": controlnet模型加载
- "VEDit Model Loader": SD 模型加载
- "VEdit Sampler": 推理
## Stars
[![Star History Chart](https://api.star-history.com/svg?repos=leeguandong/ComfyUI_VideoEditing&type=Date)](https://star-history.com/#leeguandong/ComfyUI_VideoEditing&Date)
+3
View File
@@ -0,0 +1,3 @@
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+292
View File
@@ -0,0 +1,292 @@
import os
import cv2
import numpy as np
import torch
from PIL import Image
from diffusers import ControlNetModel
from diffusers import (
DDIMScheduler,
DPMSolverMultistepScheduler,
EulerDiscreteScheduler,
EulerAncestralDiscreteScheduler,
AutoencoderKL,
LCMScheduler,
DDPMScheduler,
DEISMultistepScheduler,
PNDMScheduler
)
from .video_editing_pipeline import VideoEditingPipeline
from contextlib import nullcontext
import folder_paths
import comfy.model_management as mm
class LoadVideo2Images:
@classmethod
def INPUT_TYPES(s):
video_extensions = ['webm', 'mp4', 'mkv', 'gif']
input_dir = folder_paths.get_input_directory()
files = []
for f in os.listdir(input_dir):
if os.path.isfile(os.path.join(input_dir, f)):
file_parts = f.split('.')
if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
files.append(f)
return {"required": {
"video": (sorted(files), {"video_upload": True}),
"input_interval": ("INT", {"default": 10, "min": -1, "step": 1, "max": 50, "display": "slider"}),
}, }
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "load_video"
CATEGORY = "vdeiting"
# OUTPUT_IS_LIST = (True,)
def load_video(self, video, input_interval):
# import pdb;pdb.set_trace()
video_path = folder_paths.get_annotated_filepath(video)
frames = self.video_to_frame(video_path, input_interval)
# images = {"frames": frames}
return (frames,)
def video_to_frame(self, video_path: str, interval: int):
vidcap = cv2.VideoCapture(video_path)
success = True
count = 0
res = []
while success:
count += 1
success, image = vidcap.read()
if count % interval != 1:
continue
if image is not None:
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = cv2.resize(image[:, 100:800], (512, 512))
res.append(image)
vidcap.release()
return res
class VEdit_image2canny:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"low_threshold": ("INT", {"default": 50, "min": 0, "max": 255, "step": 1, "display": "slider"}),
"high_threshold": ("INT", {"default": 100, "min": 0, "max": 255, "step": 1, "display": "slider"}),
}
}
RETURN_TYPES = ("CONTROL_IMAGE",)
RETURN_NAMES = ("control_image",)
FUNCTION = "image2canny"
CATEGORY = "vdeiting"
def image2canny(self, image, low_threshold, high_threshold):
# import pdb;pdb.set_trace()
# frames = image["frames"]
frames = image
control_images = []
if not isinstance(frames, list):
frames = [frames]
for frame in frames:
np_image = cv2.Canny(frame, low_threshold, high_threshold)
np_image = np_image[:, :, None]
np_image = np.concatenate([np_image, np_image, np_image], axis=2)
canny_image = Image.fromarray(np_image)
control_images.append(canny_image)
return (control_images,)
class VEdit_ControlNet_ModelLoader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ([
"lllyasviel/sd-controlnet-canny"
],
{"default": "lllyasviel/sd-controlnet-canny"}),
}
}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "load_controlnet"
CATEGORY = "vdeiting"
def load_controlnet(self, model):
device = mm.get_torch_device()
controlnet = ControlNetModel.from_pretrained(model).to(device)
return (controlnet,)
class VEdit_ModelLoader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"base_model": (
[
"runwayml/stable-diffusion-v1-5",
"stabilityai/stable-diffusion-2-1",
],
{
"default": "runwayml/stable-diffusion-v1-5",
}),
"controlnet": ("MODEL",)
}
}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "load_model"
CATEGORY = "vdeiting"
def load_model(self, base_model, controlnet):
device = mm.get_torch_device()
pipeline = VideoEditingPipeline.from_pretrained(base_model,
controlnet=controlnet,
torch_dtype=torch.float16).to(device)
pipeline.safety_checker = None
return (pipeline,)
class VEdit_Sampler:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"model": ("MODEL",),
"image": ("IMAGE",),
"control_image": ("IMAGE",),
"prompt": ("STRING", {"multiline": True, "default": "a beautiful woman with red hair", }),
"n_prompt": ("STRING", {"multiline": True,
"default": "longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality", }),
"steps": ("INT", {"default": 25, "min": 1, "max": 200, "step": 1, "display": "slider"}),
"controlnet_conditioning_scale": (
"FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
"strength": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "display": "slider"}),
"scheduler": (
[
'DPMSolverMultistepScheduler',
'DPMSolverMultistepScheduler_SDE_karras',
'DDPMScheduler',
'DDIMScheduler',
'LCMScheduler',
'PNDMScheduler',
'DEISMultistepScheduler',
'EulerDiscreteScheduler',
'EulerAncestralDiscreteScheduler'
], {
"default": 'DPMSolverMultistepScheduler'
}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "sampler"
CATEGORY = "vdeiting"
def sampler(self,
model,
image,
control_image,
prompt,
n_prompt,
steps,
controlnet_conditioning_scale,
strength,
seed,
scheduler):
device = mm.get_torch_device()
mm.unload_all_models()
mm.soft_empty_cache()
dtype = mm.unet_dtype()
offload_device = mm.unet_offload_device()
pipe = model
scheduler_config = {
'num_train_timesteps': 1000,
'beta_start': 0.00085,
'beta_end': 0.012,
'beta_schedule': "linear",
'steps_offset': 1,
}
if scheduler == 'DPMSolverMultistepScheduler':
noise_scheduler = DPMSolverMultistepScheduler(**scheduler_config)
elif scheduler == 'DDIMScheduler':
noise_scheduler = DDIMScheduler(**scheduler_config)
elif scheduler == 'DPMSolverMultistepScheduler_SDE_karras':
scheduler_config.update({"algorithm_type": "sde-dpmsolver++"})
scheduler_config.update({"use_karras_sigmas": True})
noise_scheduler = DPMSolverMultistepScheduler(**scheduler_config)
elif scheduler == 'DDPMScheduler':
noise_scheduler = DDPMScheduler(**scheduler_config)
elif scheduler == 'LCMScheduler':
noise_scheduler = LCMScheduler(**scheduler_config)
elif scheduler == 'PNDMScheduler':
scheduler_config.update({"set_alpha_to_one": False})
scheduler_config.update({"trained_betas": None})
noise_scheduler = PNDMScheduler(**scheduler_config)
elif scheduler == 'DEISMultistepScheduler':
noise_scheduler = DEISMultistepScheduler(**scheduler_config)
elif scheduler == 'EulerDiscreteScheduler':
noise_scheduler = EulerDiscreteScheduler(**scheduler_config)
elif scheduler == 'EulerAncestralDiscreteScheduler':
noise_scheduler = EulerAncestralDiscreteScheduler(**scheduler_config)
else:
raise TypeError(f"not support {scheduler}!!!")
# import pdb;pdb.set_trace()
pipe.scheduler = noise_scheduler
autocast_condition = (dtype != torch.float32) and not mm.is_device_mps(device)
image = [Image.fromarray(frame) for frame in image]
with torch.autocast(mm.get_autocast_device(device), dtype=dtype) if autocast_condition else nullcontext():
generator = torch.Generator(device=device)
generator.manual_seed(seed)
out_frames = pipe(
images=image,
control_images=control_image,
prompt=prompt,
negative_prompt=n_prompt,
num_inference_steps=steps,
strength=strength,
generator=generator,
controlnet_conditioning_scale=controlnet_conditioning_scale,
)
# pipe.to(offload_device)
# pipe.to(offload_device)
# image_out = out_frames.squeeze(0).permute(1, 2, 3, 0).cpu().float()
# return (image_out,)
out_frames = torch.cat(out_frames)
out_frames = torch.from_numpy(out_frames)
return (out_frames,)
NODE_CLASS_MAPPINGS = {
"LoadVideo2Images": LoadVideo2Images,
"VEdit_image2canny": VEdit_image2canny,
"VEdit_ControlNet_ModelLoader": VEdit_ControlNet_ModelLoader,
"VEdit_ModelLoader": VEdit_ModelLoader,
"VEdit_Sampler": VEdit_Sampler
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoadVideo2Images": "Load Video to Images",
"VEdit_image2canny": "Image to Canny",
"VEdit_ControlNet_ModelLoader": "ControlNet Model Loader",
"VEdit_ModelLoader": "VEDit Model Loader",
"VEdit_Sampler": "VEdit Sampler"
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 974 KiB

+2
View File
@@ -0,0 +1,2 @@
opencv-python
+116
View File
@@ -0,0 +1,116 @@
from diffusers import StableDiffusionControlNetImg2ImgPipeline, AutoencoderKL, UNet2DConditionModel
from diffusers.schedulers import KarrasDiffusionSchedulers
from diffusers.models.attention_processor import Attention, AttnProcessor
from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
from transformers import CLIPImageProcessor, CLIPTextModel, CLIPTokenizer, CLIPVisionModelWithProjection
import torch
class AttnState:
STORE = 0
LOAD = 1
def __init__(self):
self.reset()
@property
def state(self):
return self.__state
@property
def timestep(self):
return self.__timestep
def set_timestep(self, t):
self.__timestep = t
def reset(self):
self.__state = AttnState.STORE
self.__timestep = 0
def to_load(self):
self.__state = AttnState.LOAD
class CrossFrameAttnProcessor(AttnProcessor):
"""
Cross frame attention processor. Each frame attends the first frame and previous frame.
Args:
attn_state: Whether the model is processing the first frame or an intermediate frame
"""
def __init__(self, attn_state: AttnState):
super().__init__()
self.attn_state = attn_state
self.cur_timestep = 0
self.first_maps = {}
self.prev_maps = {}
def __call__(self, attn: Attention, hidden_states, encoder_hidden_states=None, **kwargs):
if encoder_hidden_states is None: # kv
# Is self attention
tot_timestep = self.attn_state.timestep
if self.attn_state.state == AttnState.STORE:
self.first_maps[self.cur_timestep] = hidden_states.detach()
self.prev_maps[self.cur_timestep] = hidden_states.detach()
res = super().__call__(attn, hidden_states, encoder_hidden_states, **kwargs)
else:
tmp = hidden_states.detach()
cross_map = torch.cat(
(self.first_maps[self.cur_timestep], self.prev_maps[self.cur_timestep]), dim=1)
res = super().__call__(attn, hidden_states, cross_map, **kwargs)
self.prev_maps[self.cur_timestep] = tmp
self.cur_timestep += 1
if self.cur_timestep == tot_timestep:
self.cur_timestep = 0
else:
# Is cross attention
res = super().__call__(attn, hidden_states, encoder_hidden_states, **kwargs)
return res
class VideoEditingPipeline(StableDiffusionControlNetImg2ImgPipeline):
def __init__(
self,
vae: AutoencoderKL,
text_encoder: CLIPTextModel,
tokenizer: CLIPTokenizer,
unet: UNet2DConditionModel,
controlnet,
scheduler: KarrasDiffusionSchedulers,
safety_checker: StableDiffusionSafetyChecker,
feature_extractor: CLIPImageProcessor,
image_encoder: CLIPVisionModelWithProjection = None,
requires_safety_checker: bool = True,
):
super().__init__(vae, text_encoder, tokenizer, unet, controlnet, scheduler,
safety_checker, feature_extractor, image_encoder, requires_safety_checker)
self.attn_state = AttnState()
attn_processor_dict = {}
for k in unet.attn_processors.keys():
if k.startswith("up"):
attn_processor_dict[k] = CrossFrameAttnProcessor(
self.attn_state)
else:
attn_processor_dict[k] = AttnProcessor()
self.unet.set_attn_processor(attn_processor_dict)
def __call__(self, *args, images=None, control_images=None, **kwargs):
self.attn_state.reset()
self.attn_state.set_timestep(
int(kwargs['num_inference_steps'] * kwargs['strength']))
outputs = [super().__call__(
*args, **kwargs, image=images[0], control_image=control_images[0]).images[0]]
self.attn_state.to_load()
for i in range(1, len(images)):
image = images[i]
control_image = control_images[i]
outputs.append(super().__call__(
*args, **kwargs, image=image, control_image=control_image).images[0])
return outputs