Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
983b5f361c | ||
|
|
88b41ce5a6 | ||
|
|
1d65a0df67 | ||
|
|
510799aba7 | ||
|
|
82387cf301 | ||
|
|
e9639a58c3 | ||
|
|
36ca96ff96 | ||
|
|
5e92827e0a |
+47
-13
@@ -1,27 +1,27 @@
|
||||
from typing import List, Type
|
||||
from typing import Type
|
||||
|
||||
from .seq_processing import *
|
||||
from .base import *
|
||||
from .curves import *
|
||||
from .loaders import *
|
||||
from .output import *
|
||||
from .utility import *
|
||||
from .colors import *
|
||||
from .noise import *
|
||||
from .curves import *
|
||||
from .image_processing import *
|
||||
from .loaders import *
|
||||
from .noise import *
|
||||
from .output import *
|
||||
from .seq_processing import *
|
||||
from .utility import *
|
||||
|
||||
_NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBeatCurve, DreamFrameDimensions,
|
||||
DreamImageMotion,
|
||||
DreamImageMotion, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift,
|
||||
DreamDirectoryFileCount, DreamFrameCounterOffset, DreamDirectoryBackedFrameCounter,
|
||||
DreamSimpleFrameCounter, DreamImageSequenceInputWithDefaultFallback,
|
||||
DreamImageSequenceOutput, DreamCSVGenerator,
|
||||
DreamImageSequenceOutput, DreamCSVGenerator, DreamImageAreaSampler,
|
||||
DreamVideoEncoder, DreamSequenceTweening, DreamSequenceBlend, DreamColorAlign,
|
||||
DreamImageSampler, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift]
|
||||
DreamImageSampler, DreamNoiseFromAreaPalettes]
|
||||
_SIGNATURE_SUFFIX = " [Dream]"
|
||||
|
||||
MANIFEST = {
|
||||
"name": "Dream Project Animation",
|
||||
"version": (1, 1, 0),
|
||||
"version": (2, 1, 0),
|
||||
"author": "Dream Project",
|
||||
"project": "https://github.com/alt-key-project/comfyui-dream-project",
|
||||
"description": "Various utility nodes for creating animations with ComfyUI",
|
||||
@@ -31,13 +31,47 @@ NODE_CLASS_MAPPINGS = {}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
config = DreamConfig()
|
||||
|
||||
|
||||
def update_category(cls):
|
||||
top = config.get("ui.top_category", "").strip().strip("/")
|
||||
leaf_icon = ""
|
||||
if top and "CATEGORY" in cls.__dict__:
|
||||
cls.CATEGORY = top + "/" + cls.CATEGORY.lstrip("/")
|
||||
if "CATEGORY" in cls.__dict__:
|
||||
joined = []
|
||||
for partial in cls.CATEGORY.split("/"):
|
||||
icon = config.get("ui.category_icons." + partial, "")
|
||||
if icon:
|
||||
leaf_icon = icon
|
||||
if config.get("ui.prepend_icon_to_category", False):
|
||||
partial = icon.lstrip() + " " + partial
|
||||
if config.get("ui.append_icon_to_category", False):
|
||||
partial = partial + " " + icon.rstrip()
|
||||
joined.append(partial)
|
||||
cls.CATEGORY = "/".join(joined)
|
||||
return leaf_icon
|
||||
|
||||
|
||||
def update_display_name(cls, category_icon, display_name):
|
||||
icon = cls.__dict__.get("ICON", category_icon)
|
||||
if config.get("ui.prepend_icon_to_node", False):
|
||||
display_name = icon.lstrip() + " " + display_name
|
||||
if config.get("ui.append_icon_to_node", False):
|
||||
display_name = display_name + " " + icon.rstrip()
|
||||
return display_name
|
||||
|
||||
|
||||
for cls in _NODE_CLASSES:
|
||||
category_icon = update_category(cls)
|
||||
clsname = cls.__name__
|
||||
if "NODE_NAME" in cls.__dict__:
|
||||
node_name = cls.__dict__["NODE_NAME"] + _SIGNATURE_SUFFIX
|
||||
NODE_CLASS_MAPPINGS[node_name] = cls
|
||||
display_name = cls.__dict__.get("DISPLAY_NAME", cls.__dict__["NODE_NAME"]) + _SIGNATURE_SUFFIX
|
||||
NODE_DISPLAY_NAME_MAPPINGS[node_name] = display_name
|
||||
NODE_DISPLAY_NAME_MAPPINGS[node_name] = update_display_name(cls, category_icon,
|
||||
cls.__dict__.get("DISPLAY_NAME",
|
||||
cls.__dict__["NODE_NAME"]))
|
||||
else:
|
||||
raise Exception("Class {} is missing NODE_NAME!".format(str(cls)))
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import glob
|
||||
|
||||
from .categories import NodeCategories
|
||||
from .shared import *
|
||||
from .types import *
|
||||
import glob
|
||||
|
||||
|
||||
class DreamDirectoryFileCount:
|
||||
NODE_NAME = "File Count"
|
||||
ICON = "📂"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -37,6 +40,8 @@ class DreamDirectoryFileCount:
|
||||
class DreamFrameCounterOffset:
|
||||
NODE_NAME = "Frame Counter Offset"
|
||||
|
||||
ICON = "±"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -60,6 +65,7 @@ class DreamFrameCounterOffset:
|
||||
|
||||
class DreamSimpleFrameCounter:
|
||||
NODE_NAME = "Frame Counter (Simple)"
|
||||
ICON = "⚋"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -87,6 +93,7 @@ class DreamSimpleFrameCounter:
|
||||
|
||||
class DreamDirectoryBackedFrameCounter:
|
||||
NODE_NAME = "Frame Counter (Directory)"
|
||||
ICON = "⚋"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class NodeCategories:
|
||||
ANIMATION = "animation"
|
||||
ANIMATION_POSTPROCESSING = ANIMATION + "/postprocessing"
|
||||
@@ -8,4 +10,4 @@ class NodeCategories:
|
||||
IMAGE_COLORS = "image/color"
|
||||
IMAGE_GENERATE = "image/generate"
|
||||
IMAGE = "image"
|
||||
UTILS = "utils"
|
||||
UTILS = "utils"
|
||||
@@ -1,6 +1,85 @@
|
||||
from .categories import NodeCategories
|
||||
from .shared import *
|
||||
from .types import *
|
||||
from .categories import NodeCategories
|
||||
|
||||
|
||||
class DreamImageAreaSampler:
|
||||
NODE_NAME = "Sample Image Area as Palette"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"samples": ("INT", {"default": 256, "min": 1, "max": 1024 * 4}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"area": (["top-left", "top-center", "top-right",
|
||||
"center-left", "center", "center-right",
|
||||
"bottom-left", "bottom-center", "bottom-right"],)
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = NodeCategories.IMAGE_COLORS
|
||||
RETURN_TYPES = (RGBPalette.ID,)
|
||||
RETURN_NAMES = ("palette",)
|
||||
FUNCTION = "result"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, *values):
|
||||
return ALWAYS_CHANGED_FLAG
|
||||
|
||||
def _get_pixel_area(self, img: DreamImage, area):
|
||||
w = img.width
|
||||
h = img.height
|
||||
wpart = round(w / 3)
|
||||
hpart = round(h / 3)
|
||||
x0 = 0
|
||||
x1 = wpart - 1
|
||||
x2 = wpart
|
||||
x3 = wpart + wpart - 1
|
||||
x4 = wpart + wpart
|
||||
x5 = w - 1
|
||||
y0 = 0
|
||||
y1 = hpart - 1
|
||||
y2 = hpart
|
||||
y3 = hpart + hpart - 1
|
||||
y4 = hpart + hpart
|
||||
y5 = h - 1
|
||||
if area == "center":
|
||||
return (x2, y2, x3, y3)
|
||||
elif area == "top-center":
|
||||
return (x2, y0, x3, y1)
|
||||
elif area == "bottom-center":
|
||||
return (x2, y4, x3, y5)
|
||||
elif area == "center-left":
|
||||
return (x0, y2, x1, y3)
|
||||
elif area == "top-left":
|
||||
return (x0, y0, x1, y1)
|
||||
elif area == "bottom-left":
|
||||
return (x0, y4, x1, y5)
|
||||
elif area == "center-right":
|
||||
return (x4, y2, x5, y3)
|
||||
elif area == "top-right":
|
||||
return (x4, y0, x5, y1)
|
||||
elif area == "bottom-right":
|
||||
return (x4, y4, x5, y5)
|
||||
|
||||
def result(self, image, samples, seed, area):
|
||||
result = list()
|
||||
r = random.Random()
|
||||
r.seed(seed)
|
||||
for data in image:
|
||||
di = DreamImage(tensor_image=data)
|
||||
area = self._get_pixel_area(di, area)
|
||||
|
||||
pixels = list()
|
||||
for i in range(samples):
|
||||
x = r.randint(area[0], area[2])
|
||||
y = r.randint(area[1], area[3])
|
||||
pixels.append(di.get_pixel(x, y))
|
||||
result.append(RGBPalette(colors=pixels))
|
||||
|
||||
return (tuple(result),)
|
||||
|
||||
|
||||
class DreamImageSampler:
|
||||
@@ -11,7 +90,7 @@ class DreamImageSampler:
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"samples": ("INT", {"default": 1024, "min": 1, "max": 1024 * 64}),
|
||||
"samples": ("INT", {"default": 1024, "min": 1, "max": 1024 * 4}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
|
||||
},
|
||||
}
|
||||
@@ -136,6 +215,7 @@ class DreamColorShift:
|
||||
|
||||
class DreamAnalyzePalette:
|
||||
NODE_NAME = "Analyze Palette"
|
||||
NODE = "📊"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import math, csv
|
||||
import csv
|
||||
import math
|
||||
|
||||
from .types import SharedTypes, FrameCounter
|
||||
from .shared import hashed_as_strings
|
||||
from .categories import NodeCategories
|
||||
from .shared import hashed_as_strings
|
||||
from .types import SharedTypes, FrameCounter
|
||||
|
||||
|
||||
class DreamSineWave:
|
||||
@@ -82,7 +83,8 @@ class DreamBeatCurve:
|
||||
return 1.0 - ((frame - accent_start) / frames_per_beat)
|
||||
return 0
|
||||
|
||||
def result(self, bpm, frame_counter: FrameCounter, measure_length, low_value, high_value, power, invert, time_offset, **accents):
|
||||
def result(self, bpm, frame_counter: FrameCounter, measure_length, low_value, high_value, power, invert,
|
||||
time_offset, **accents):
|
||||
frame_offset = int(round(time_offset * frame_counter.frames_per_second))
|
||||
accents_set = set(filter(lambda v: v >= 1 and v <= measure_length,
|
||||
map(lambda i: accents.get("accent_" + str(i), -1), range(30))))
|
||||
@@ -133,6 +135,7 @@ def _is_as_float(s: str):
|
||||
|
||||
class DreamCSVGenerator:
|
||||
NODE_NAME = "CSV Generator"
|
||||
ICON = "⌗"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
|
||||
def run_disable():
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_disable()
|
||||
run_disable()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
def run_enable():
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_enable()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
def _get_node_name(cls):
|
||||
return cls.__dict__.get("NODE_NAME", str(cls))
|
||||
|
||||
|
||||
def on_error(node_cls: type, message: str):
|
||||
msg = "Failure in [" + _get_node_name(node_cls) + "]:" + message
|
||||
print(msg)
|
||||
raise Exception(msg)
|
||||
File diff suppressed because it is too large
Load Diff
+6
-4
@@ -1,13 +1,14 @@
|
||||
import math
|
||||
|
||||
import numpy
|
||||
import torch
|
||||
from PIL.Image import Resampling
|
||||
from PIL import Image, ImageDraw
|
||||
from .categories import *
|
||||
from .types import SharedTypes, FrameCounter
|
||||
from PIL.Image import Resampling
|
||||
|
||||
from .categories import *
|
||||
from .shared import ALWAYS_CHANGED_FLAG, convertTensorImageToPIL, DreamImageProcessor, \
|
||||
DreamImage, DreamMask
|
||||
from .types import SharedTypes, FrameCounter
|
||||
|
||||
|
||||
class DreamImageMotion:
|
||||
@@ -85,7 +86,8 @@ class DreamImageMotion:
|
||||
def _limit_range(f):
|
||||
return max(-1.0, min(1.0, f))
|
||||
|
||||
def _motion(image: DreamImage, batch_counter, zoom, x_translation, y_translation, mask_1_overlap, mask_2_overlap,
|
||||
def _motion(image: DreamImage, batch_counter, zoom, x_translation, y_translation, mask_1_overlap,
|
||||
mask_2_overlap,
|
||||
mask_3_overlap):
|
||||
zoom = _limit_range(zoom / frame_counter.frames_per_second)
|
||||
x_translation = _limit_range(x_translation / frame_counter.frames_per_second)
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Morgan Johansson/Dream Project
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
from PIL import Image
|
||||
from .types import SharedTypes, FrameCounter
|
||||
from .shared import ALWAYS_CHANGED_FLAG, list_images_in_directory, convertFromPILToTensorImage, DreamImage
|
||||
from .categories import NodeCategories
|
||||
import os
|
||||
from .shared import ALWAYS_CHANGED_FLAG, list_images_in_directory, DreamImage
|
||||
from .types import SharedTypes, FrameCounter
|
||||
|
||||
|
||||
class DreamImageSequenceInputWithDefaultFallback:
|
||||
NODE_NAME = "Image Sequence Loader"
|
||||
ICON = "💾"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
"Image Sequence Saver [Dream]": "Saves a frame to a directory",
|
||||
"Image Sequence Tweening [Dream]": "Post processing for animation sequences generating blended in-between frames",
|
||||
"Linear Curve [Dream]": "Linear interpolation between two value over the full animation",
|
||||
"Noise from Area Palettes [Dream]": "Generates noise based on the colors of up to nine different palettes",
|
||||
"Noise from Palette [Dream]": "Generates noise based on the colors in a palette",
|
||||
"Palette Color Align [Dream]": "Shifts the colors of one palette towards another target palette",
|
||||
"Palette Color Shift [Dream]": "Multiplies the color values in a palette",
|
||||
"Sample Image Area as Palette [Dream]": "Samples a palette from an image based on pre-defined areas",
|
||||
"Sample Image as Palette [Dream]": "Randomly samples pixel values to build a palette from an image",
|
||||
"Sine Curve [Dream]": "Simple sine wave curve"
|
||||
}
|
||||
@@ -1,10 +1,30 @@
|
||||
import math
|
||||
|
||||
from .categories import NodeCategories
|
||||
from .shared import *
|
||||
from .types import *
|
||||
from .categories import NodeCategories
|
||||
|
||||
|
||||
def _generate_noise(image: DreamImage, color_function, rng: random.Random, block_size, blur_amount,
|
||||
density) -> DreamImage:
|
||||
w = block_size[0]
|
||||
h = block_size[1]
|
||||
blur_radius = round(max(image.width, image.height) * blur_amount * 0.25)
|
||||
if w <= (image.width // 128) or h <= (image.height // 128):
|
||||
return image
|
||||
max_placements = round(density * (image.width * image.height))
|
||||
num = min(max_placements, round((image.width * image.height * 2) / (w * h)))
|
||||
for i in range(num):
|
||||
x = rng.randint(-w + 1, image.width - 1)
|
||||
y = rng.randint(-h + 1, image.height - 1)
|
||||
image.color_area(x, y, w, h, color_function(x + (w >> 1), y + (h >> 1)))
|
||||
image = image.blur(blur_radius)
|
||||
return _generate_noise(image, color_function, rng, (w >> 1, h >> 1), blur_amount, density)
|
||||
|
||||
|
||||
class DreamNoiseFromPalette:
|
||||
NODE_NAME = "Noise from Palette"
|
||||
ICON = "🌫"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -12,8 +32,8 @@ class DreamNoiseFromPalette:
|
||||
"required": SharedTypes.palette | {
|
||||
"width": ("INT", {"default": 512, "min": 1, "max": 8192}),
|
||||
"height": ("INT", {"default": 512, "min": 1, "max": 8192}),
|
||||
"blur_amount": ("FLOAT", {"default": 0.1, "min": 0, "max": 1.0, "step": 0.05}),
|
||||
"iterations": ("INT", {"default": 4, "min": 1, "max": 64}),
|
||||
"blur_amount": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.05}),
|
||||
"density": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.025}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
|
||||
},
|
||||
}
|
||||
@@ -27,28 +47,116 @@ class DreamNoiseFromPalette:
|
||||
def IS_CHANGED(cls, *values):
|
||||
return ALWAYS_CHANGED_FLAG
|
||||
|
||||
def generate_noise(self, image: DreamImage, color_function, rng: random.Random, i: int, blur_amount) -> DreamImage:
|
||||
w = image.width >> i
|
||||
h = image.height >> i
|
||||
blur_radius = round(max(image.width, image.height) * blur_amount * 0.25)
|
||||
if w <= 1 or h <= 1:
|
||||
return image
|
||||
for i in range(1 << (i*2)):
|
||||
x = rng.randint(-w+1, image.width - 1)
|
||||
y = rng.randint(-h+1, image.height - 1)
|
||||
image.color_area(x, y, w, h, color_function())
|
||||
image = image.blur(blur_radius)
|
||||
return self.generate_noise(image, color_function, rng, i + 1, blur_amount)
|
||||
|
||||
def result(self, palette: Tuple[RGBPalette], width, height, seed, blur_amount, iterations):
|
||||
def result(self, palette: Tuple[RGBPalette], width, height, seed, blur_amount, density):
|
||||
outputs = list()
|
||||
rng = random.Random()
|
||||
for p in palette:
|
||||
seed += 1
|
||||
color_iterator = p.random_iteration(seed)
|
||||
image = DreamImage(pil_image=Image.new("RGB", (width, height), color=next(color_iterator)))
|
||||
for n in range(iterations):
|
||||
image = self.generate_noise(image, lambda: next(color_iterator), rng, 1, blur_amount)
|
||||
image = _generate_noise(image, lambda x, y: next(color_iterator), rng,
|
||||
(image.width >> 1, image.height >> 1), blur_amount, density)
|
||||
outputs.append(image)
|
||||
|
||||
return (DreamImage.join_to_tensor_data(outputs),)
|
||||
|
||||
|
||||
class DreamNoiseFromAreaPalettes:
|
||||
NODE_NAME = "Noise from Area Palettes"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"optional": {
|
||||
"top_left_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"top_center_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"top_right_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"center_left_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"center_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"center_right_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"bottom_left_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"bottom_center_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
"bottom_right_palette": (RGBPalette.ID, {"forceInput": True}),
|
||||
},
|
||||
"required": {
|
||||
"area_sharpness": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"width": ("INT", {"default": 512, "min": 1, "max": 8192}),
|
||||
"height": ("INT", {"default": 512, "min": 1, "max": 8192}),
|
||||
"blur_amount": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.05}),
|
||||
"density": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.025}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = NodeCategories.IMAGE_GENERATE
|
||||
ICON = "🌫"
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "result"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, *values):
|
||||
return ALWAYS_CHANGED_FLAG
|
||||
|
||||
def _area_coordinates(self, width, height):
|
||||
dx = width / 6
|
||||
dy = height / 6
|
||||
return {
|
||||
"top_left_palette": (dx, dy),
|
||||
"top_center_palette": (dx * 3, dy),
|
||||
"top_right_palette": (dx * 5, dy),
|
||||
"center_left_palette": (dx, dy * 3),
|
||||
"center_palette": (dx * 3, dy * 3),
|
||||
"center_right_palette": (dx * 5, dy * 3),
|
||||
"bottom_left_palette": (dx * 1, dy * 5),
|
||||
"bottom_center_palette": (dx * 3, dy * 5),
|
||||
"bottom_right_palette": (dx * 5, dy * 5),
|
||||
}
|
||||
|
||||
def _pick_random_area(self, active_coordinates, x, y, rng, area_sharpness):
|
||||
def _dst(x1, y1, x2, y2):
|
||||
a = x1 - x2
|
||||
b = y1 - y2
|
||||
return math.sqrt(a * a + b * b)
|
||||
|
||||
distances = list(map(lambda item: (item[0], _dst(item[1][0], item[1][1], x, y)), active_coordinates))
|
||||
areas_by_weight = list(
|
||||
map(lambda item: (math.pow((1.0 / max(1, item[1])), 0.5 + 4.5 * area_sharpness), item[0]), distances))
|
||||
return pick_random_by_weight(areas_by_weight, rng)
|
||||
|
||||
def _setup_initial_colors(self, image: DreamImage, color_func):
|
||||
w = image.width
|
||||
h = image.height
|
||||
wpart = round(w / 3)
|
||||
hpart = round(h / 3)
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
image.color_area(wpart * i, hpart * j, w, h,
|
||||
color_func(wpart * i + w // 2, hpart * j + h // 2))
|
||||
|
||||
def result(self, width, height, seed, blur_amount, density, area_sharpness, **palettes):
|
||||
outputs = list()
|
||||
rng = random.Random()
|
||||
coordinates = self._area_coordinates(width, height)
|
||||
active_palettes = list(filter(lambda pair: pair[1] is not None and len(pair[1]) > 0, palettes.items()))
|
||||
active_coordinates = list(map(lambda item: (item[0], coordinates[item[0]]), active_palettes))
|
||||
|
||||
n = max(list(map(len, palettes.values())) + [0])
|
||||
for b in range(n):
|
||||
batch_palettes = dict(map(lambda item: (item[0], item[1][b].random_iteration(seed)), active_palettes))
|
||||
|
||||
def _color_func(x, y):
|
||||
name = self._pick_random_area(active_coordinates, x, y, rng, area_sharpness)
|
||||
rgb = batch_palettes[name]
|
||||
return next(rgb)
|
||||
|
||||
image = DreamImage(pil_image=Image.new("RGB", (width, height)))
|
||||
self._setup_initial_colors(image, _color_func)
|
||||
image = _generate_noise(image, _color_func, rng, (round(image.width / 3), round(image.height / 3)),
|
||||
blur_amount, density)
|
||||
outputs.append(image)
|
||||
|
||||
if not outputs:
|
||||
outputs.append(DreamImage(pil_image=Image.new("RGB", (width, height))))
|
||||
|
||||
return (DreamImage.join_to_tensor_data(outputs),)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import json
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
from .categories import NodeCategories
|
||||
import folder_paths as comfy_paths
|
||||
from .types import SharedTypes, FrameCounter, AnimationSequence
|
||||
from .shared import hashed_as_strings, DreamImageProcessor, DreamImage, \
|
||||
list_images_in_directory, DreamConfig
|
||||
import os
|
||||
|
||||
import folder_paths as comfy_paths
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
|
||||
from .categories import NodeCategories
|
||||
from .shared import hashed_as_strings, DreamImageProcessor, DreamImage, \
|
||||
list_images_in_directory, DreamConfig
|
||||
from .types import SharedTypes, FrameCounter, AnimationSequence
|
||||
|
||||
CONFIG = DreamConfig()
|
||||
|
||||
|
||||
def _save_png(pil_image, filepath, embed_info, prompt, extra_pnginfo):
|
||||
info = PngInfo()
|
||||
if extra_pnginfo is not None:
|
||||
@@ -28,6 +31,7 @@ def _save_jpg(pil_image, filepath, quality):
|
||||
|
||||
class DreamImageSequenceOutput:
|
||||
NODE_NAME = "Image Sequence Saver"
|
||||
ICON = "💾"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
@@ -9,6 +9,12 @@ I have demonstrated the use of these custom nodes in this [youtube video](https:
|
||||
|
||||
## Installation
|
||||
|
||||
### Simple option
|
||||
|
||||
You can install Dream Project Animation Nodes using the ComfyUI Manager.
|
||||
|
||||
### Manual option
|
||||
|
||||
Run within (ComfyUI)/custom_nodes/ folder:
|
||||
|
||||
* git clone https://github.com/alt-key-project/comfyui-dream-project.git
|
||||
@@ -26,6 +32,41 @@ Finally:
|
||||
After startup, a configuration file 'config.json' should have been created in the 'comfyui-dream-project' directory.
|
||||
Specifically check that the path of ffmpeg works in your system (add full path to the command if needed).
|
||||
|
||||
## Configuration
|
||||
|
||||
### ffmpeg.path
|
||||
|
||||
Path to the ffmpeg executable or just the command if ffmpeg is in PATH.
|
||||
|
||||
### ffmpeg.arguments
|
||||
|
||||
The arguments sent to FFMPEG. A few of the values are provided by the node:
|
||||
|
||||
* %FPS% the target framerate
|
||||
* %FRAMES% a frame ionput file
|
||||
* %OUTPUT% output video file path
|
||||
|
||||
### encoding.jpeg__quality
|
||||
|
||||
Sets the encoding quality of jpeg images.
|
||||
|
||||
### ui.top_category
|
||||
|
||||
Sets the name of the top level category on the menu. Set to empty string "" to remove the top level. If the top level
|
||||
is removed you may also want to disable the category icons to get nodes into existing category folders.
|
||||
|
||||
### prepend_icon_to_category / append_icon_to_category
|
||||
|
||||
Flags to add a icon before and/or after the category name at each level.
|
||||
|
||||
### prepend_icon_icon_to_node / append_icon_icon_to_node
|
||||
|
||||
Flags to add an icon before and/or after the node name.
|
||||
|
||||
### ui.category_icons
|
||||
|
||||
Each key defines a unicode symbol as an icon used for the specified category.
|
||||
|
||||
## Concepts used
|
||||
|
||||
These are some concepts used in nodes:
|
||||
@@ -51,83 +92,111 @@ video file using ffmpeg. These nodes should be seen as a convenience and they ar
|
||||
nodes in parallel - they will not work as intended!
|
||||
|
||||
## The nodes
|
||||
### Analyze Palette [Dream]
|
||||
Output brightness, red, green and blue averages of a palette. Useful to control other processing.
|
||||
### Analyze Palette [Dream]
|
||||
Output brightness, red, green and blue averages of a palette. Useful to control other processing.
|
||||
|
||||
### Beat Curve [Dream]
|
||||
Beat pattern curve with impulses at specified beats of a measure.
|
||||
|
||||
### CSV Curve [Dream]
|
||||
CSV input curve where first column is frame or second and second column is value.
|
||||
|
||||
### CSV Generator [Dream]
|
||||
CSV output, mainly for debugging purposes. First column is frame number and second is value.
|
||||
Recreates file at frame 0 (removing and existing content in the file).
|
||||
|
||||
### Common Frame Dimensions [Dream]
|
||||
Utility for calculating good width/height based on common video dimensions.
|
||||
|
||||
### FFMPEG Video Encoder [Dream]
|
||||
Post processing for animation sequences calling FFMPEG to generate video file.
|
||||
|
||||
### File Count [Dream]
|
||||
Finds the number of files in a directory matching specified patterns.
|
||||
|
||||
### Frame Counter (Directory) [Dream]
|
||||
Directory backed frame counter, for output directories.
|
||||
|
||||
### Frame Counter (Simple) [Dream]
|
||||
Integer value used as frame counter. Useful for testing or if an auto-incrementing primitive is used as a frame
|
||||
counter.
|
||||
|
||||
### Frame Counter Offset [Dream]
|
||||
Adds an offset to a frame counter.
|
||||
|
||||
### Image Motion [Dream]
|
||||
Node supporting zooming in/out and translating an image.
|
||||
|
||||
### Image Sequence Blend [Dream]
|
||||
Post processing for animation sequences blending frame for a smoother blurred effect.
|
||||
|
||||
### Image Sequence Loader [Dream]
|
||||
Loads a frame from a directory of images.
|
||||
|
||||
### Image Sequence Saver [Dream]
|
||||
Saves a frame to a directory.
|
||||
|
||||
### Image Sequence Tweening [Dream]
|
||||
Post processing for animation sequences generating blended in-between frames.
|
||||
|
||||
### Linear Curve [Dream]
|
||||
Linear interpolation between two values over the full animation.
|
||||
|
||||
### Noise from Palette [Dream]
|
||||
Generates noise based on the colors in a palette.
|
||||
|
||||
### Palette Color Align [Dream]
|
||||
Shifts the colors of one palette towards another target palette. If the alignment factor
|
||||
is 0.5 the result is nearly an average of the two palettes. At 0 no alignment is done and at 1 we get a close
|
||||
alignment to the target. Above one we will overshoot the alignment.
|
||||
|
||||
### Palette Color Shift [Dream]
|
||||
Multiplies the color values in a palette to shift the color balance or brightness.
|
||||
|
||||
### Sample Image as Palette [Dream]
|
||||
Randomly samples pixels from a source image to build a palette from it.
|
||||
|
||||
### Sine Curve [Dream]
|
||||
Simple sine wave curve.
|
||||
|
||||
### Other custom nodes
|
||||
|
||||
Many of the nodes found in 'WAS Node Suite' are useful the Dream Project Animation nodes - I suggest you install those
|
||||
custom nodes as well!
|
||||
### Beat Curve [Dream]
|
||||
Beat pattern curve with impulses at specified beats of a measure.
|
||||
|
||||
## Examples
|
||||
### CSV Curve [Dream]
|
||||
CSV input curve where first column is frame or second and second column is value.
|
||||
|
||||
### Image Motion with Curves
|
||||
### CSV Generator [Dream]
|
||||
CSV output, mainly for debugging purposes. First column is frame number and second is value.
|
||||
Recreates file at frame 0 (removing and existing content in the file).
|
||||
|
||||
### Common Frame Dimensions [Dream]
|
||||
Utility for calculating good width/height based on common video dimensions.
|
||||
|
||||
### FFMPEG Video Encoder [Dream]
|
||||
Post processing for animation sequences calling FFMPEG to generate video file.
|
||||
|
||||
### File Count [Dream]
|
||||
Finds the number of files in a directory matching specified patterns.
|
||||
|
||||
### Frame Counter (Directory) [Dream]
|
||||
Directory backed frame counter, for output directories.
|
||||
|
||||
### Frame Counter (Simple) [Dream]
|
||||
Integer value used as frame counter. Useful for testing or if an auto-incrementing primitive is used as a frame
|
||||
counter.
|
||||
|
||||
### Frame Counter Offset [Dream]
|
||||
Adds an offset to a frame counter.
|
||||
|
||||
### Image Motion [Dream]
|
||||
Node supporting zooming in/out and translating an image.
|
||||
|
||||
### Image Sequence Blend [Dream]
|
||||
Post processing for animation sequences blending frame for a smoother blurred effect.
|
||||
|
||||
### Image Sequence Loader [Dream]
|
||||
Loads a frame from a directory of images.
|
||||
|
||||
### Image Sequence Saver [Dream]
|
||||
Saves a frame to a directory.
|
||||
|
||||
### Image Sequence Tweening [Dream]
|
||||
Post processing for animation sequences generating blended in-between frames.
|
||||
|
||||
### Linear Curve [Dream]
|
||||
Linear interpolation between two values over the full animation.
|
||||
|
||||
### Noise from Area Palettes [Dream]
|
||||
Generates noise based on the colors of up to nine different palettes, each connected to position/area of the
|
||||
image. Although the palettes are optional, at least one palette should be provided.
|
||||
|
||||
### Noise from Palette [Dream]
|
||||
Generates noise based on the colors in a palette.
|
||||
|
||||
### Palette Color Align [Dream]
|
||||
Shifts the colors of one palette towards another target palette. If the alignment factor
|
||||
is 0.5 the result is nearly an average of the two palettes. At 0 no alignment is done and at 1 we get a close
|
||||
alignment to the target. Above one we will overshoot the alignment.
|
||||
|
||||
### Palette Color Shift [Dream]
|
||||
Multiplies the color values in a palette to shift the color balance or brightness.
|
||||
|
||||
### Sample Image Area as Palette [Dream]
|
||||
Randomly samples a palette from an image based on pre-defined areas. The image is separated into nine rectangular areas
|
||||
of equal size and each node may sample one of these.
|
||||
|
||||
### Sample Image as Palette [Dream]
|
||||
Randomly samples pixels from a source image to build a palette from it.
|
||||
|
||||
### Sine Curve [Dream]
|
||||
Simple sine wave curve.
|
||||
|
||||
### Other custom nodes
|
||||
|
||||
Many of the nodes found in 'WAS Node Suite' are useful the Dream Project Animation nodes - I suggest you install those
|
||||
custom nodes as well!
|
||||
|
||||
## Examples
|
||||
|
||||
### Image Motion with Curves
|
||||
|
||||
This example should be a starting point for anyone wanting to build with the Dream Project Animation nodes.
|
||||
|
||||
[motion-workflow-example](examples/motion-workflow-example.json)
|
||||
|
||||
### Image Motion with Color Coherence.
|
||||
|
||||
Same as above but with added color coherence through palettes.
|
||||
|
||||
[motion-workflow-with-color-coherence](examples/motion-workflow-with-color-coherence.json)
|
||||
|
||||
## Known issues
|
||||
|
||||
### FFMPEG
|
||||
|
||||
The call to FFMPEG currently in the default configuration (in config.json) does not seem to work for everyone. The good
|
||||
news is that you can change the arguments to whatever works for you - the node-supplied parameters (that probably all need to be in the call)
|
||||
are:
|
||||
|
||||
* -i %FRAMES% (the input file listing frames)
|
||||
* -r %FPS% (sets the frame rate)
|
||||
* %OUTPUT% (the path to the video file)
|
||||
|
||||
If possible, I will change the default configuration to one that more versions/builds of ffmpeg will accept. Do let me
|
||||
know what arguments are causing issues for you!
|
||||
|
||||
+13
-9
@@ -1,12 +1,16 @@
|
||||
from typing import Iterable, Tuple
|
||||
|
||||
from .types import *
|
||||
from .categories import NodeCategories
|
||||
from .shared import DreamConfig, DreamImage
|
||||
import os, tempfile, subprocess, shutil, random
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from functools import lru_cache
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .categories import NodeCategories
|
||||
from .err import on_error
|
||||
from .shared import DreamConfig
|
||||
from .types import *
|
||||
|
||||
CONFIG = DreamConfig()
|
||||
|
||||
|
||||
@@ -117,13 +121,14 @@ def _ffmpeg(config, filenames, fps, output):
|
||||
for (key, value) in replacements.items():
|
||||
cmd = list(map(lambda s: s.replace(key, value), cmd))
|
||||
|
||||
subprocess.run(cmd, shell=True)
|
||||
subprocess.check_output(cmd, shell=True)
|
||||
finally:
|
||||
os.unlink(tempfilepath)
|
||||
|
||||
|
||||
class DreamVideoEncoder:
|
||||
NODE_NAME = "FFMPEG Video Encoder"
|
||||
ICON = "🎬"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -174,8 +179,7 @@ class DreamVideoEncoder:
|
||||
if os.path.isfile(imagepath):
|
||||
os.unlink(imagepath)
|
||||
except Exception as e:
|
||||
print("Failed to encode files in dir {}!".format(os.path.dirname(images[0])))
|
||||
print(str(e))
|
||||
on_error(self.__class__, str(e))
|
||||
return ()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import hashlib, os, json, glob
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
|
||||
import folder_paths as comfy_paths
|
||||
import glob
|
||||
import numpy
|
||||
import torch
|
||||
from PIL import Image, ImageFilter
|
||||
@@ -7,8 +14,6 @@ from PIL.ImageDraw import ImageDraw
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
from typing import Dict, Tuple, List
|
||||
|
||||
import folder_paths as comfy_paths
|
||||
|
||||
NODE_FILE = os.path.abspath(__file__)
|
||||
DREAM_NODES_SOURCE_ROOT = os.path.dirname(NODE_FILE)
|
||||
TEMP_PATH = os.path.join(os.path.abspath(comfy_paths.temp_directory), "Dream_Anim")
|
||||
@@ -30,6 +35,9 @@ def _replace_pil_image(data):
|
||||
return data
|
||||
|
||||
|
||||
_config_data = None
|
||||
|
||||
|
||||
class DreamConfig:
|
||||
FILEPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
|
||||
DEFAULT_CONFIG = {
|
||||
@@ -40,15 +48,55 @@ class DreamConfig:
|
||||
},
|
||||
"encoding": {
|
||||
"jpeg_quality": 95
|
||||
}
|
||||
},
|
||||
"ui": {
|
||||
"top_category": "Dream",
|
||||
"prepend_icon_to_category": True,
|
||||
"append_icon_to_category": False,
|
||||
"prepend_icon_to_node": True,
|
||||
"append_icon_to_node": False,
|
||||
"category_icons": {
|
||||
"animation": "🎥",
|
||||
"postprocessing": "⚙",
|
||||
"transforms": "🔀",
|
||||
"curves": "📈",
|
||||
"color": "🎨",
|
||||
"generate": "⚡",
|
||||
"utils": "🛠",
|
||||
"image": "🌄",
|
||||
"Dream": "✨"
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
global _config_data
|
||||
if not os.path.isfile(DreamConfig.FILEPATH):
|
||||
with open(DreamConfig.FILEPATH, "w") as f:
|
||||
json.dump(DreamConfig.DEFAULT_CONFIG, f, indent=2)
|
||||
with open(DreamConfig.FILEPATH) as f:
|
||||
self._data = json.load(f)
|
||||
self._data = DreamConfig.DEFAULT_CONFIG
|
||||
self._save()
|
||||
if _config_data is None:
|
||||
with open(DreamConfig.FILEPATH, encoding="utf-8") as f:
|
||||
self._data = json.load(f)
|
||||
if self._merge_with_defaults(self._data, DreamConfig.DEFAULT_CONFIG):
|
||||
self._save()
|
||||
_config_data = self._data
|
||||
else:
|
||||
self._data = _config_data
|
||||
|
||||
def _save(self):
|
||||
with open(DreamConfig.FILEPATH, "w", encoding="utf-8") as f:
|
||||
json.dump(self._data, f, indent=2)
|
||||
|
||||
def _merge_with_defaults(self, config: dict, default_config: dict) -> bool:
|
||||
changed = False
|
||||
for key in default_config.keys():
|
||||
if key not in config:
|
||||
changed = True
|
||||
config[key] = default_config[key]
|
||||
elif isinstance(default_config[key], dict):
|
||||
changed = changed or self._merge_with_defaults(config[key], default_config[key])
|
||||
return changed
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
key = key.split(".")
|
||||
@@ -89,6 +137,16 @@ class DreamImageProcessor:
|
||||
return tuple(map(lambda l: torch.cat(l, dim=0), output))
|
||||
|
||||
|
||||
def pick_random_by_weight(data: List[Tuple[float, object]], rng: random.Random):
|
||||
total_weight = sum(map(lambda item: item[0], data))
|
||||
r = rng.random()
|
||||
for (weight, obj) in data:
|
||||
r -= weight / total_weight
|
||||
if r <= 0:
|
||||
return obj
|
||||
return data[0][1]
|
||||
|
||||
|
||||
class DreamImage:
|
||||
@classmethod
|
||||
def join_to_tensor_data(cls, images):
|
||||
@@ -113,7 +171,6 @@ class DreamImage:
|
||||
self.pil_image = pil_image
|
||||
self._draw = ImageDraw(self.pil_image)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
class _Pixels:
|
||||
def __init__(self, image: DreamImage):
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
from typing import List, Dict
|
||||
|
||||
from .shared import DreamImage
|
||||
import random, time
|
||||
|
||||
|
||||
class RGBPalette:
|
||||
|
||||
+4
-2
@@ -1,7 +1,8 @@
|
||||
from .shared import hashed_as_strings
|
||||
from .categories import NodeCategories
|
||||
import math
|
||||
|
||||
from .categories import NodeCategories
|
||||
from .shared import hashed_as_strings
|
||||
|
||||
|
||||
def _align_num(n: int, alignment: int, type: str):
|
||||
if alignment <= 1:
|
||||
@@ -16,6 +17,7 @@ def _align_num(n: int, alignment: int, type: str):
|
||||
|
||||
class DreamFrameDimensions:
|
||||
NODE_NAME = "Common Frame Dimensions"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user