8 Commits
20 changed files with 6293 additions and 148 deletions
+47 -13
View File
@@ -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)))
+8 -1
View File
@@ -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
View File
@@ -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"
+82 -2
View File
@@ -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):
+7 -4
View File
@@ -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
View File
@@ -1,6 +1,6 @@
def run_disable():
pass
if __name__ == "__main__":
run_disable()
run_disable()
+1
View File
@@ -1,5 +1,6 @@
def run_enable():
pass
if __name__ == "__main__":
run_enable()
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+2
View File
@@ -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"
}
+127 -19
View File
@@ -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),)
+10 -6
View File
@@ -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):
+141 -72
View File
@@ -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
View File
@@ -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 ()
+66 -9
View File
@@ -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):
+4 -1
View File
@@ -1,6 +1,9 @@
import random
import time
from typing import List, Dict
from .shared import DreamImage
import random, time
class RGBPalette:
+4 -2
View File
@@ -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 {