230 lines
6.4 KiB
Python
230 lines
6.4 KiB
Python
import comfy.model_management as model_management
|
|
import numpy as np
|
|
import torch
|
|
import torchvision.transforms.functional as F
|
|
from torchvision.models.optical_flow import Raft_Large_Weights, raft_large
|
|
from torchvision.utils import flow_to_image
|
|
|
|
NODE_CLASS_MAPPINGS = {}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
|
|
|
|
|
def register_node(identifier: str, display_name: str):
|
|
def decorator(cls):
|
|
NODE_CLASS_MAPPINGS[identifier] = cls
|
|
NODE_DISPLAY_NAME_MAPPINGS[identifier] = display_name
|
|
|
|
return cls
|
|
|
|
return decorator
|
|
|
|
|
|
def comfyui_to_native_torch(imgs: torch.Tensor):
|
|
"""
|
|
Convert images in NHWC format to NCHW format.
|
|
|
|
Use this to convert ComfyUI images to torch-native images.
|
|
"""
|
|
return imgs.permute(0, 3, 1, 2)
|
|
|
|
|
|
def native_torch_to_comfyui(imgs: torch.Tensor):
|
|
"""
|
|
Convert images in NCHW format to NHWC format.
|
|
|
|
Use this to convert torch-native images to ComfyUI images.
|
|
"""
|
|
return imgs.permute(0, 2, 3, 1)
|
|
|
|
|
|
_model = None
|
|
|
|
|
|
def load_model():
|
|
global _model
|
|
|
|
if _model is not None:
|
|
return _model
|
|
|
|
try:
|
|
offload_device = model_management.unet_offload_device()
|
|
|
|
_model = raft_large(weights=Raft_Large_Weights.DEFAULT, progress=False).eval()
|
|
_model = _model.to(offload_device)
|
|
|
|
return _model
|
|
except Exception as e:
|
|
_model = None
|
|
raise e
|
|
|
|
|
|
def preprocess_image(img: torch.Tensor):
|
|
# Image size must be divisible by 8
|
|
_, _, h, w = img.shape
|
|
assert h % 8 == 0, "Image height must be divisible by 8"
|
|
assert w % 8 == 0, "Image width must be divisible by 8"
|
|
|
|
img = F.convert_image_dtype(img, torch.float)
|
|
|
|
# map [0, 1] into [-1, 1]
|
|
img = F.normalize(img, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
|
|
|
|
img = img.contiguous()
|
|
|
|
return img
|
|
|
|
|
|
@register_node("RAFTEstimate", "RAFT Estimate")
|
|
class _:
|
|
"""
|
|
https://pytorch.org/vision/main/auto_examples/plot_optical_flow.html
|
|
"""
|
|
|
|
CATEGORY = "jamesWalker55"
|
|
INPUT_TYPES = lambda: {
|
|
"required": {
|
|
"image_a": ("IMAGE",),
|
|
"image_b": ("IMAGE",),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("RAFT_FLOW",)
|
|
FUNCTION = "execute"
|
|
|
|
def execute(self, image_a: torch.Tensor, image_b: torch.Tensor):
|
|
"""
|
|
Code derived from:
|
|
https://pytorch.org/vision/main/auto_examples/plot_optical_flow.html
|
|
"""
|
|
|
|
assert isinstance(image_a, torch.Tensor)
|
|
assert isinstance(image_b, torch.Tensor)
|
|
|
|
torch_device = model_management.get_torch_device()
|
|
offload_device = model_management.unet_offload_device()
|
|
|
|
image_a = comfyui_to_native_torch(image_a).to(torch_device)
|
|
image_b = comfyui_to_native_torch(image_b).to(torch_device)
|
|
model = load_model().to(torch_device)
|
|
|
|
image_a = preprocess_image(image_a)
|
|
image_b = preprocess_image(image_b)
|
|
|
|
all_flows = model(image_a, image_b)
|
|
best_flow = all_flows[-1]
|
|
# best_flow.shape => torch.Size([1, 2, 512, 512])
|
|
|
|
model.to(offload_device)
|
|
image_a = image_a.to("cpu")
|
|
image_b = image_b.to("cpu")
|
|
best_flow = best_flow.to("cpu")
|
|
|
|
return (best_flow,)
|
|
|
|
|
|
@register_node("RAFTFlowToImage", "RAFT Flow to Image")
|
|
class _:
|
|
"""
|
|
https://pytorch.org/vision/main/auto_examples/plot_optical_flow.html
|
|
"""
|
|
|
|
CATEGORY = "jamesWalker55"
|
|
INPUT_TYPES = lambda: {
|
|
"required": {
|
|
"raft_flow": ("RAFT_FLOW",),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "execute"
|
|
|
|
def execute(self, raft_flow: torch.Tensor):
|
|
assert isinstance(raft_flow, torch.Tensor)
|
|
assert raft_flow.shape[1] == 2
|
|
|
|
images = flow_to_image(raft_flow)
|
|
# pixel range is [0, 255], dtype=torch.uint8
|
|
|
|
images = images / 255
|
|
|
|
images = native_torch_to_comfyui(images)
|
|
|
|
return (images,)
|
|
|
|
|
|
def depth_exr_to_numpy(exr_path, typemap={"HALF": np.float16, "FLOAT": np.float32}):
|
|
# Code stolen from:
|
|
# https://gist.github.com/andres-fr/4ddbb300d418ed65951ce88766236f9c
|
|
|
|
import OpenEXR
|
|
|
|
# load EXR and extract shape
|
|
exr = OpenEXR.InputFile(exr_path)
|
|
print(exr.header())
|
|
dw = exr.header()["dataWindow"]
|
|
shape = (dw.max.y - dw.min.y + 1, dw.max.x - dw.min.x + 1)
|
|
#
|
|
arr_maps = {}
|
|
for ch_name, ch in exr.header()["channels"].items():
|
|
print("reading channel", ch_name)
|
|
# This, and __str__ seem to be the only ways to get typename
|
|
exr_typename = ch.type.names[ch.type.v]
|
|
np_type = typemap[exr_typename]
|
|
# convert channel to np array
|
|
bytestring = exr.channel(ch_name, ch.type)
|
|
arr = np.frombuffer(bytestring, dtype=np_type).reshape(shape)
|
|
arr_maps[ch_name] = arr
|
|
|
|
return arr_maps
|
|
|
|
|
|
@register_node("RAFTLoadFlowFromEXRChannels", "RAFT Load Flow from EXR Channels")
|
|
class _:
|
|
"""
|
|
This is a utility function for loading motion flows from an EXR image file.
|
|
This is intended for use with Blender's vector pass in the Cycles renderer.
|
|
|
|
In Blender, enable the vector pass. In the compositor, use "Separate Color" to
|
|
extract the "Blue" and "Alpha" channels of the vector pass. Then, combine them
|
|
using "Combine Color" to two of the RGB channels. Finally, render to the "OpenEXR"
|
|
format.
|
|
|
|
https://gist.github.com/andres-fr/4ddbb300d418ed65951ce88766236f9c
|
|
"""
|
|
|
|
CATEGORY = "jamesWalker55"
|
|
INPUT_TYPES = lambda: {
|
|
"required": {
|
|
"path": ("STRING", {"default": ""}),
|
|
"x_channel": (("R", "G", "B", "A"), {"default": "R"}),
|
|
"y_channel": (("R", "G", "B", "A"), {"default": "G"}),
|
|
"invert_x": (("false", "true"), {"default": "true"}),
|
|
"invert_y": (("false", "true"), {"default": "false"}),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("RAFT_FLOW",)
|
|
FUNCTION = "execute"
|
|
|
|
def execute(
|
|
self, path: str, x_channel: str, y_channel: str, invert_x: str, invert_y: str
|
|
):
|
|
assert isinstance(path, str)
|
|
assert x_channel in ("R", "G", "B", "A")
|
|
assert y_channel in ("R", "G", "B", "A")
|
|
assert invert_x in ("true", "false")
|
|
assert invert_y in ("true", "false")
|
|
|
|
invert_x: bool = invert_x == "true"
|
|
invert_y: bool = invert_y == "true"
|
|
|
|
maps = depth_exr_to_numpy(path)
|
|
|
|
x = torch.from_numpy(maps[x_channel])
|
|
y = torch.from_numpy(maps[y_channel])
|
|
|
|
if invert_x:
|
|
x = x * -1
|
|
|
|
if invert_y:
|
|
y = y * -1
|
|
|
|
return (torch.stack((x, y)).unsqueeze(0),)
|