5 Commits
15 changed files with 981 additions and 68 deletions
+11 -3
View File
@@ -5,12 +5,15 @@ from .base import *
from .colors import *
from .curves import *
from .image_processing import *
from .inputfields import *
from .loaders import *
from .noise import *
from .output import *
from .prompting import *
from .seq_processing import *
from .switches import *
from .utility import *
from .inputfields import *
from .calculate import *
_NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBeatCurve, DreamFrameDimensions,
DreamImageMotion, DreamNoiseFromPalette, DreamAnalyzePalette, DreamColorShift,
@@ -19,12 +22,17 @@ _NODE_CLASSES: List[Type] = [DreamSineWave, DreamLinear, DreamCSVCurve, DreamBea
DreamImageSequenceOutput, DreamCSVGenerator, DreamImageAreaSampler,
DreamVideoEncoder, DreamSequenceTweening, DreamSequenceBlend, DreamColorAlign,
DreamImageSampler, DreamNoiseFromAreaPalettes, DreamVideoEncoderMpegCoder,
DreamInputString, DreamInputFloat, DreamInputInt, DreamInputText]
DreamInputString, DreamInputFloat, DreamInputInt, DreamInputText, DreamBigLatentSwitch,
DreamFrameCountCalculator, DreamBigImageSwitch, DreamBigTextSwitch, DreamBigFloatSwitch,
DreamBigIntSwitch, DreamBigPaletteSwitch, DreamWeightedPromptBuilder, DreamPromptFinalizer,
DreamFrameCounterInfo, DreamBoolToFloat, DreamBoolToInt, DreamSawWave, DreamTriangleWave,
DreamTriangleEvent, DreamSmoothEvent, DreamCalculation, DreamImageColorShift,
DreamComparePalette, DreamImageContrast, DreamImageBrightness]
_SIGNATURE_SUFFIX = " [Dream]"
MANIFEST = {
"name": "Dream Project Animation",
"version": (3, 0, 1),
"version": (3, 2, 0),
"author": "Dream Project",
"project": "https://github.com/alt-key-project/comfyui-dream-project",
"description": "Various utility nodes for creating animations with ComfyUI",
+63 -2
View File
@@ -6,6 +6,37 @@ from .shared import *
from .types import *
class DreamFrameCounterInfo:
NODE_NAME = "Frame Counter Info"
ICON = "⚋"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter
}
CATEGORY = NodeCategories.ANIMATION
RETURN_TYPES = ("INT", "INT", "BOOLEAN", "BOOLEAN", "FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("frames_completed", "total_frames", "first_frame", "last_frame",
"elapsed_seconds", "remaining_seconds", "total_seconds", "completion")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *v):
return ALWAYS_CHANGED_FLAG
def result(self, frame_counter: FrameCounter):
return (frame_counter.current_frame,
frame_counter.total_frames,
frame_counter.is_first_frame,
frame_counter.is_final_frame,
frame_counter.current_time_in_seconds,
frame_counter.remaining_time_in_seconds,
frame_counter.total_time_in_seconds,
frame_counter.current_time_in_seconds / max(0.01, frame_counter.total_time_in_seconds))
class DreamDirectoryFileCount:
NODE_NAME = "File Count"
ICON = "📂"
@@ -35,7 +66,7 @@ class DreamDirectoryFileCount:
for pattern in patterns.split("|"):
files = list(glob.glob(pattern, root_dir=directory_path))
total += len(files)
print("total "+str(total))
print("total " + str(total))
return (total,)
@@ -105,7 +136,7 @@ class DreamDirectoryBackedFrameCounter:
"pattern": ("STRING", {"default": '*', "multiline": False}),
"indexing": (["numeric", "alphabetic order"],),
"total_frames": ("INT", {"default": 100, "min": 2, "max": 24 * 3600 * 60}),
"frames_per_second": ("INT", {"min": 1, "default": 25}),
"frames_per_second": ("INT", {"min": 1, "default": 30}),
},
}
@@ -124,3 +155,33 @@ class DreamDirectoryBackedFrameCounter:
return (FrameCounter(0, total_frames, frames_per_second),)
n = max(results.keys()) + 1
return (FrameCounter(n, total_frames, frames_per_second),)
class DreamFrameCountCalculator:
NODE_NAME = "Frame Count Calculator"
ICON = "⌛"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"hours": ("INT", {"min": 0, "default": 0, "max": 23}),
"minutes": ("INT", {"min": 0, "default": 0, "max": 59}),
"seconds": ("INT", {"min": 0, "default": 10, "max": 59}),
"milliseconds": ("INT", {"min": 0, "default": 0, "max": 59}),
"frames_per_second": ("INT", {"min": 1, "default": 30})
},
}
CATEGORY = NodeCategories.ANIMATION
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("TOTAL",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *v):
return ALWAYS_CHANGED_FLAG
def result(self, hours, minutes, seconds, milliseconds, frames_per_second):
total_s = seconds + 0.001 * milliseconds + minutes * 60 + hours * 3600
return (round(total_s * frames_per_second),)
+99
View File
@@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
import math
from evalidate import Expr, EvalException, base_eval_model
from .categories import *
from .err import on_error
from .shared import hashed_as_strings
class DreamCalculation:
NODE_NAME = "Calculation"
ICON = "🖩"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"expression": ("STRING", {"default": "a + b + c - (r * s * t)", "multiline": True})
},
"optional": {
"a_int": ("INT", {"default": 0, "multiline": False}),
"b_int": ("INT", {"default": 0, "multiline": False}),
"c_int": ("INT", {"default": 0, "multiline": False}),
"r_float": ("FLOAT", {"default": 0.0, "multiline": False}),
"s_float": ("FLOAT", {"default": 0.0, "multiline": False}),
"t_float": ("FLOAT", {"default": 0.0, "multiline": False})
}
}
CATEGORY = NodeCategories.UTILS
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def _make_model(self):
funcs = self._make_functions()
m = base_eval_model.clone()
m.nodes.append('Mult')
m.nodes.append('Call')
for funname in funcs.keys():
m.allowed_functions.append(funname)
return (m, funcs)
def _make_functions(self):
return {
"round": round,
"float": float,
"int": int,
"abs": abs,
"min": min,
"max": max,
"tan": math.tan,
"tanh": math.tanh,
"sin": math.sin,
"sinh": math.sinh,
"cos": math.cos,
"cosh": math.cosh,
"pow": math.pow,
"sqrt": math.sqrt,
"ceil": math.ceil,
"floor": math.floor,
"pi": math.pi,
"log": math.log,
"log2": math.log2,
"acos": math.acos,
"asin": math.asin,
"acosh": math.acosh,
"asinh": math.asinh,
"atan": math.atan,
"atanh": math.atanh,
"exp": math.exp,
"fmod": math.fmod,
"factorial": math.factorial,
"dist": math.dist,
"atan2": math.atan2,
"log10": math.log10
}
def result(self, expression, **values):
model, funcs = self._make_model()
vars = funcs
for key in ("a_int", "b_int", "c_int", "r_float", "s_float", "t_float"):
nm = key.split("_")[0]
v = values.get(key, None)
if v is not None:
vars[nm] = v
try:
data = Expr(expression, model=model).eval(vars)
if isinstance(data, (int, float)):
return float(data), int(round(data))
else:
return 0.0, 0
except EvalException as e:
on_error(DreamCalculation, str(e))
+3 -1
View File
@@ -5,9 +5,11 @@ class NodeCategories:
ANIMATION_POSTPROCESSING = ANIMATION + "/postprocessing"
ANIMATION_TRANSFORMS = ANIMATION + "/transforms"
ANIMATION_CURVES = "animation/curves"
CONDITIONING = "conditioning"
IMAGE_POSTPROCESSING = "image/postprocessing"
IMAGE_ANIMATION = "image/animation"
IMAGE_COLORS = "image/color"
IMAGE_GENERATE = "image/generate"
IMAGE = "image"
UTILS = "utils"
UTILS = "utils"
UTILS_SWITCHES = "utils/switches"
+152 -9
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from .categories import NodeCategories
from .shared import *
from .types import *
@@ -128,7 +129,7 @@ class DreamColorAlign:
def INPUT_TYPES(cls):
return {
"required": SharedTypes.palette | {
"target_align": (RGBPalette.ID, ),
"target_align": (RGBPalette.ID,),
"alignment_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.1}),
}
}
@@ -151,8 +152,8 @@ class DreamColorAlign:
for i in range(len(palette)):
p = palette[i]
t = target_align[i]
(_, r1, g1, b1) = p.analyze()
(_, r2, g2, b2) = t.analyze()
(_, _, r1, g1, b1) = p.analyze()
(_, _, r2, g2, b2) = t.analyze()
dr = (r2 - r1) * alignment_factor
dg = (g2 - g1) * alignment_factor
@@ -214,9 +215,150 @@ class DreamColorShift:
return (tuple(results),)
class DreamImageColorShift:
NODE_NAME = "Image Color Shift"
ICON = "🖼"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"image": ("IMAGE",),
"red_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}),
"green_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}),
"blue_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0}),
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, image, red_multiplier, green_multiplier, blue_multiplier):
proc = DreamImageProcessor(inputs=image)
def recolor(im: DreamImage, *a, **args):
return (im.adjust_colors(red_multiplier, green_multiplier, blue_multiplier),)
return proc.process(recolor)
class DreamImageBrightness:
NODE_NAME = "Image Brightness Adjustment"
ICON = "☼"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"image": ("IMAGE",),
"factor": ("FLOAT", {"default": 1.0, "min": 0.0}),
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, image, factor):
proc = DreamImageProcessor(inputs=image)
def change(im: DreamImage, *a, **args):
return (im.change_brightness(factor),)
return proc.process(change)
class DreamImageContrast:
NODE_NAME = "Image Contrast Adjustment"
ICON = "◐"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"image": ("IMAGE",),
"factor": ("FLOAT", {"default": 1.0, "min": 0.0}),
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, image, factor):
proc = DreamImageProcessor(inputs=image)
def change(im: DreamImage, *a, **args):
return (im.change_contrast(factor),)
return proc.process(change)
class DreamComparePalette:
NODE_NAME = "Compare Palettes"
ICON = "📊"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": (RGBPalette.ID,),
"b": (RGBPalette.ID,),
},
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = (
"brightness_multiplier", "contrast_multiplier", "red_multiplier", "green_multiplier", "blue_multiplier")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def result(self, a, b):
MIN_VALUE = 1 / 255.0
brightness = list()
contrasts = list()
reds = list()
greens = list()
blues = list()
for i in range(min(len(a), len(b))):
(bright, ctr, red, green, blue) = a[i].analyze()
(bright2, ctr2, red2, green2, blue2) = b[i].analyze()
brightness.append(bright2 / max(MIN_VALUE, bright))
contrasts.append(ctr2 / max(MIN_VALUE, ctr))
reds.append(red2 / max(MIN_VALUE, red))
greens.append(green2 / max(MIN_VALUE, green))
blues.append(blue2 / max(MIN_VALUE, blue))
n = len(brightness)
return (sum(brightness) / n, sum(contrasts) / n, sum(reds) / n,
sum(greens) / n, sum(blues) / n)
class DreamAnalyzePalette:
NODE_NAME = "Analyze Palette"
NODE = "📊"
ICON = "📊"
@classmethod
def INPUT_TYPES(cls):
@@ -226,8 +368,8 @@ class DreamAnalyzePalette:
}
CATEGORY = NodeCategories.IMAGE_COLORS
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("brightness", "redness", "greenness", "blueness")
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "FLOAT", "FLOAT")
RETURN_NAMES = ("brightness", "contrast", "redness", "greenness", "blueness")
FUNCTION = "result"
@classmethod
@@ -236,12 +378,13 @@ class DreamAnalyzePalette:
def result(self, palette):
f = 1.0 / len(palette)
(w, r, g, b) = (0, 0, 0, 0)
(w, c, r, g, b) = (0, 0, 0, 0, 0)
for p in palette:
(brightness, red, green, blue) = p.analyze()
(brightness, contrast, red, green, blue) = p.analyze()
w += brightness
c += contrast
r += red
g += green
b += blue
return w * f, r * f, g * f, b * f
return w * f, c * f, r * f, g * f, b * f
+158 -2
View File
@@ -7,6 +7,20 @@ from .shared import hashed_as_strings
from .types import SharedTypes, FrameCounter
def _linear_value_calc(x, x_start, x_end, y_start, y_end):
if x <= x_start:
return y_start
if x >= x_end:
return y_end
dx = max(x_end - x_start, 0.0001)
n = (x - x_start) / dx
return (y_end - y_start) * n + y_start
def _curve_result(f: float):
return (f, int(round(f)))
class DreamSineWave:
NODE_NAME = "Sine Curve"
@@ -37,7 +51,149 @@ class DreamSineWave:
b = 2 * math.pi / periodicity_seconds
d = (max_value + min_value) / 2
y = a * math.sin(b * (x + c)) + d
return (y, int(round(y)))
return _curve_result(y)
class DreamSawWave:
NODE_NAME = "Saw Curve"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"max_value": ("FLOAT", {"default": 1.0, "multiline": False}),
"min_value": ("FLOAT", {"default": 0.0, "multiline": False}),
"periodicity_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.01}),
"phase": ("FLOAT", {"default": 0.0, "multiline": False, "min": -1, "max": 1}),
},
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, frame_counter: FrameCounter, max_value, min_value, periodicity_seconds, phase):
x = frame_counter.current_time_in_seconds
x = ((x + periodicity_seconds * phase) % periodicity_seconds) / periodicity_seconds
y = x * (max_value - min_value) + min_value
return _curve_result(y)
class DreamTriangleWave:
NODE_NAME = "Triangle Curve"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"max_value": ("FLOAT", {"default": 1.0, "multiline": False}),
"min_value": ("FLOAT", {"default": 0.0, "multiline": False}),
"periodicity_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.01}),
"phase": ("FLOAT", {"default": 0.0, "multiline": False, "min": -1, "max": 1}),
},
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, frame_counter: FrameCounter, max_value, min_value, periodicity_seconds, phase):
x = frame_counter.current_time_in_seconds
x = ((x + periodicity_seconds * phase) % periodicity_seconds) / periodicity_seconds
if x <= 0.5:
x *= 2
y = x * (max_value - min_value) + min_value
else:
x = (x - 0.5) * 2
y = max_value - x * (max_value - min_value)
return _curve_result(y)
class DreamTriangleEvent:
NODE_NAME = "Triangle Event Curve"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"max_value": ("FLOAT", {"default": 1.0, "multiline": False}),
"min_value": ("FLOAT", {"default": 0.0, "multiline": False}),
"width_seconds": ("FLOAT", {"default": 1.0, "multiline": False, "min": 0.1}),
"center_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.0}),
},
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, frame_counter: FrameCounter, max_value, min_value, width_seconds, center_seconds):
x = frame_counter.current_time_in_seconds
start = center_seconds - width_seconds * 0.5
end = center_seconds + width_seconds * 0.5
if start <= x <= center_seconds:
y = _linear_value_calc(x, start, center_seconds, min_value, max_value)
elif center_seconds < x <= end:
y = _linear_value_calc(x, center_seconds, end, max_value, min_value)
else:
y = min_value
return _curve_result(y)
class DreamSmoothEvent:
NODE_NAME = "Smooth Event Curve"
@classmethod
def INPUT_TYPES(cls):
return {
"required": SharedTypes.frame_counter | {
"max_value": ("FLOAT", {"default": 1.0, "multiline": False}),
"min_value": ("FLOAT", {"default": 0.0, "multiline": False}),
"width_seconds": ("FLOAT", {"default": 1.0, "multiline": False, "min": 0.1}),
"center_seconds": ("FLOAT", {"default": 10.0, "multiline": False, "min": 0.0}),
},
}
CATEGORY = NodeCategories.ANIMATION_CURVES
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("FLOAT", "INT")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, frame_counter: FrameCounter, max_value, min_value, width_seconds, center_seconds):
x = frame_counter.current_time_in_seconds
start = center_seconds - width_seconds * 0.5
end = center_seconds + width_seconds * 0.5
if start <= x <= center_seconds:
y = _linear_value_calc(x, start, center_seconds, 0.0, 1.0)
elif center_seconds < x <= end:
y = _linear_value_calc(x, center_seconds, end, 1.0, 0.0)
else:
y = 0.0
if y < 0.5:
y = ((y + y) * (y + y)) * 0.5
else:
a = (y - 0.5) * 2
y = math.pow(a, 0.25) * 0.5 + 0.5
return _curve_result(y * (max_value - min_value) + min_value)
class DreamBeatCurve:
@@ -96,7 +252,7 @@ class DreamBeatCurve:
v = 1.0 - v
r = low_value + v * (high_value - low_value)
return (r, int(round(r)))
return _curve_result(r)
class DreamLinear:
+41
View File
@@ -0,0 +1,41 @@
EMBEDDED_CONFIGURATION = {
"ffmpeg": {
"file_extension": "mp4",
"path": "ffmpeg",
"arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-vsync",
"cfr", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
"yuv420p", "%OUTPUT%"]
},
"mpeg_coder": {
"encoding_threads": 4,
"bitrate_factor": 1.0,
"max_b_frame": 2,
"file_extension": "mp4",
"codec_name": "libx265"
},
"encoding": {
"jpeg_quality": 95
},
"debug": False,
"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": "🌄",
"switches": "⭆",
"conditioning": "☯",
"Dream": "✨"
}
},
}
+22 -1
View File
@@ -1,15 +1,32 @@
{
"Analyze Palette [Dream]": "Output brightness, red, green and blue averages of a palette",
"Analyze Palette [Dream]": "Output brightness, contrast, red, green and blue averages of a palette",
"Beat Curve [Dream]": "Beat pattern curve with impulses at specified beats of a measure",
"Big Float Switch [Dream]": "Switch for up to 10 inputs",
"Big Image Switch [Dream]": "Switch for up to 10 inputs",
"Big Int Switch [Dream]": "Switch for up to 10 inputs",
"Big Latent Switch [Dream]": "Switch for up to 10 inputs",
"Big Palette Switch [Dream]": "Switch for up to 10 inputs",
"Big Text Switch [Dream]": "Switch for up to 10 inputs",
"Boolean To Float [Dream]": "Converts a boolean value to two different float values",
"Boolean To Int [Dream]": "Converts a boolean value to two different int values",
"Build Prompt [Dream]": "Weighted text prompt builder utility",
"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",
"Calculation [Dream]": "Mathematical calculation node",
"Common Frame Dimensions [Dream]": "Utility for calculating good width/height based on common video dimensions",
"Compare Palettes [Dream]": "Analyses two palettes producing the factor for each color channel",
"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",
"Finalize Prompt [Dream]": "Used in conjunction with 'Build Prompt'",
"Float Input [Dream]": "Float input (until primitive routing issues are solved)",
"Frame Count Calculator [Dream]": "Simple utility to calculate number of frames based on duration and framerate",
"Frame Counter (Directory) [Dream]": "Directory backed frame counter, for output directories",
"Frame Counter (Simple) [Dream]": "Integer value used as frame counter",
"Frame Counter Info [Dream]": "Extracts information from the frame counter",
"Frame Counter Offset [Dream]": "Adds an offset to a frame counter",
"Image Brightness Adjustment [Dream]": "Adjusts the brightness of an image by a factor",
"Image Color Shift [Dream]": "Adjust the colors (or brightness) of an image",
"Image Contrast Adjustment [Dream]": "Adjusts the contrast of an image by a factor",
"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",
@@ -23,8 +40,12 @@
"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",
"Saw Curve [Dream]": "Saw wave curve",
"Sine Curve [Dream]": "Simple sine wave curve",
"Smooth Event Curve [Dream]": "Single event/peak curve with a slight bell-shape",
"String Input [Dream]": "String input (until primitive routing issues are solved)",
"Text Input [Dream]": "Multiline string input (until primitive routing issues are solved)",
"Triangle Curve [Dream]": "Triangle wave curve",
"Triangle Event Curve [Dream]": "Single event/peak curve with triangular shape",
"Video Encoder (mpegCoder) [Dream]": "Post processing for animation sequences using mpegCoder module to generate video file"
}
+69
View File
@@ -0,0 +1,69 @@
from .categories import NodeCategories
from .shared import hashed_as_strings
from .types import PartialPrompt
class DreamWeightedPromptBuilder:
NODE_NAME = "Build Prompt"
ICON = "⚖"
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"partial_prompt": (PartialPrompt.ID,)
},
"required": {
"added_prompt": ("STRING", {"default": "", "multiline": True}),
"weight": ("FLOAT", {"default": 1.0}),
},
}
CATEGORY = NodeCategories.CONDITIONING
RETURN_TYPES = (PartialPrompt.ID,)
RETURN_NAMES = ("partial_prompt",)
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, added_prompt, weight, **args):
input = args.get("partial_prompt", PartialPrompt())
p = input.add(added_prompt, weight)
return (p,)
class DreamPromptFinalizer:
NODE_NAME = "Finalize Prompt"
ICON = "🗫"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"partial_prompt": (PartialPrompt.ID,),
"adjustment": (["raw", "by_abs_max", "by_abs_sum"],),
"clamp": ("FLOAT", {"default": 2.0, "min": 0.1, "step": 0.1}),
"adjustment_reference": ("FLOAT", {"default": 1.0, "min": 0.1}),
},
}
CATEGORY = NodeCategories.CONDITIONING
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("positive", "negative")
FUNCTION = "result"
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(*values)
def result(self, partial_prompt: PartialPrompt, adjustment, adjustment_reference, clamp):
if adjustment == "raw" or partial_prompt.is_empty():
return partial_prompt.finalize(clamp)
elif adjustment == "by_abs_sum":
f = adjustment_reference / partial_prompt.abs_sum()
return partial_prompt.scaled_by(f).finalize(clamp)
else:
f = adjustment_reference / partial_prompt.abs_max()
return partial_prompt.scaled_by(f).finalize(clamp)
+44
View File
@@ -133,6 +133,23 @@ Output brightness, red, green and blue averages of a palette. Useful to control
### Beat Curve [Dream]
Beat pattern curve with impulses at specified beats of a measure.
### Big *** Switch [Dream]
Switch nodes for different type for up to ten inputs.
### Boolean To Float/Int [Dream]
Converts a boolean value to two different numeric values.
### Build Prompt [Dream] (and Finalize Prompt [Dream])
Weighted text prompt builder utility. Chain any number of these nodes and terminate with 'Finalize Prompt'.
### Calculation [Dream]
Mathematical calculation node. Exposes most of the mathematical functions in the python
[math module](https://docs.python.org/3/library/math.html), mathematical operators as well as round, abs, int,
float, max and min.
### Compare Palettes [Dream]
Analyses two palettes and produces the quotient for each individual channel (b/a) and brightness.
### CSV Curve [Dream]
CSV input curve where first column is frame or second and second column is value.
@@ -152,6 +169,9 @@ Post processing for animation sequences using the python module mpegCoder with f
### File Count [Dream]
Finds the number of files in a directory matching specified patterns.
### Frame Count Calculator [Dream]
Simple utility to calculate number of frames based on time and framerate.
### Frame Counter (Directory) [Dream]
Directory backed frame counter, for output directories.
@@ -159,9 +179,21 @@ Directory backed frame counter, for output directories.
Integer value used as frame counter. Useful for testing or if an auto-incrementing primitive is used as a frame
counter.
### Frame Counter Info [Dream]
Extracts information from the frame counter.
### Frame Counter Offset [Dream]
Adds an offset to a frame counter.
### Image Brightness Adjustment [Dream]
Adjusts the brightness of an image by a factor.
### Image Color Shift [Dream]
Allows changing the colors of an image with a multiplier for each channel (RGB).
### Image Contrast Adjustment [Dream]
Adjusts the contrast of an image by a factor.
### Image Motion [Dream]
Node supporting zooming in/out and translating an image.
@@ -202,9 +234,21 @@ 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.
### Saw Curve [Dream]
Saw wave curve.
### Sine Curve [Dream]
Simple sine wave curve.
### Smooth Event Curve [Dream]
Single event/peak curve with a slight bell-shape.
### Triangle Curve [Dream]
Triangle wave curve.
### Triangle Event Curve [Dream]
Single event/peak curve with triangular shape.
### Other custom nodes
Many of the nodes found in 'WAS Node Suite' are useful the Dream Project Animation nodes - I suggest you install those
+1
View File
@@ -4,3 +4,4 @@ scipy
numpy<1.24>=1.18
torchvision
mpegCoder
evalidate
+5 -7
View File
@@ -4,9 +4,7 @@ import shutil
import subprocess
import tempfile
from functools import lru_cache
from PIL import Image
from PIL import Image as PilImage
from .categories import NodeCategories
from .err import on_error
from .shared import DreamConfig, MpegEncoderUtility
@@ -16,8 +14,8 @@ CONFIG = DreamConfig()
@lru_cache(5)
def _load_image_cached(filename) -> Image:
return Image.open(filename)
def _load_image_cached(filename):
return PilImage.open(filename)
class TempFileSet:
@@ -166,7 +164,7 @@ class DreamVideoEncoderMpegCoder:
if not sequence.is_defined:
return ()
config = DreamConfig()
filename = _make_video_filename(name, config.get("mpeg_coder.file_exension"))
filename = _make_video_filename(name, config.get("mpeg_coder.file_extension", "mp4"))
for batch_num in sequence.batches:
try:
images = list(sequence.get_image_files_of_batch(batch_num))
@@ -236,7 +234,7 @@ class DreamVideoEncoder:
return ()
config = DreamConfig()
filename = _make_video_filename(name, config.get("ffmpeg.file_exension"))
filename = _make_video_filename(name, config.get("ffmpeg.file_extension", "mp4"))
for batch_num in sequence.batches:
try:
images = list(sequence.get_image_files_of_batch(batch_num))
+23 -42
View File
@@ -10,12 +10,13 @@ import folder_paths as comfy_paths
import glob
import numpy
import torch
from PIL import Image, ImageFilter
from PIL import Image, ImageFilter, ImageEnhance
from PIL.ImageDraw import ImageDraw
from PIL.PngImagePlugin import PngInfo
from typing import Dict, Tuple, List
from .dreamlogger import DreamLog
from .embedded_config import EMBEDDED_CONFIGURATION
NODE_FILE = os.path.abspath(__file__)
DREAM_NODES_SOURCE_ROOT = os.path.dirname(NODE_FILE)
@@ -43,45 +44,7 @@ _config_data = None
class DreamConfig:
FILEPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json")
DEFAULT_CONFIG = {
"ffmpeg": {
"file_exension": "mp4",
"path": "ffmpeg",
"arguments": ["-r", "%FPS%", "-f", "concat", "-safe", "0", "-vsync",
"cfr", "-i", "%FRAMES%", "-c:v", "libx265", "-pix_fmt",
"yuv420p", "%OUTPUT%"]
},
"mpeg_coder": {
"encoding_threads": 4,
"bitrate_factor": 1.0,
"max_b_frame": 2,
"file_exension": "mp4",
"codec_name": "libx265"
},
"encoding": {
"jpeg_quality": 95
},
"debug": False,
"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": "✨"
}
},
}
DEFAULT_CONFIG = EMBEDDED_CONFIGURATION
def __init__(self):
global _config_data
@@ -188,6 +151,14 @@ class DreamImage:
self.size = self.pil_image.size
self._draw = ImageDraw(self.pil_image)
def change_brightness(self, factor):
enhancer = ImageEnhance.Brightness(self.pil_image)
return DreamImage(pil_image=enhancer.enhance(factor))
def change_contrast(self, factor):
enhancer = ImageEnhance.Contrast(self.pil_image)
return DreamImage(pil_image=enhancer.enhance(factor))
def numpy_array(self):
return numpy.array(self.pil_image)
@@ -232,6 +203,15 @@ class DreamImage:
def blur(self, amount):
return DreamImage(pil_image=self.pil_image.filter(ImageFilter.GaussianBlur(amount)))
def adjust_colors(self, red_factor=1.0, green_factor=1.0, blue_factor=1.0):
# newRed = 1.1*oldRed + 0*oldGreen + 0*oldBlue + constant
# newGreen = 0*oldRed + 0.9*OldGreen + 0*OldBlue + constant
# newBlue = 0*oldRed + 0*OldGreen + 1*OldBlue + constant
matrix = (red_factor, 0, 0, 0,
0, green_factor, 0, 0,
0, 0, blue_factor, 0)
return DreamImage(pil_image=self.pil_image.convert("RGB", matrix))
def get_pixel(self, x, y):
p = self.pil_image.getpixel((x, y))
if len(p) == 4:
@@ -397,7 +377,7 @@ class MpegEncoderUtility:
self._logger = get_logger()
self._enc = mpegCoder.MpegEncoder()
bit_rate = self._calculate_bit_rate(width, height, fps, bit_rate_factor)
self._logger.info("Bitrate "+str(bit_rate))
self._logger.info("Bitrate " + str(bit_rate))
self._enc.setParameter(
videoPath=video_path, codecName=codec_name,
nthread=encoding_threads, bitRate=bit_rate, width=width, height=height, widthSrc=width,
@@ -405,7 +385,7 @@ class MpegEncoderUtility:
GOPSize=len(files), maxBframe=max_b_frame, frameRate=self._fps_to_tuple(fps))
def _calculate_bit_rate(self, width: int, height: int, fps: float, bit_rate_factor: float):
bits_per_pixel_base = 0.075
bits_per_pixel_base = 0.125
return round(max(10, float(width * height * fps * bits_per_pixel_base * bit_rate_factor * 0.001)))
def encode(self):
@@ -427,6 +407,7 @@ class MpegEncoderUtility:
def _fps_to_tuple(self, fps: float):
def _is_almost_int(f: float):
return abs(f - int(f)) < 0.001
a = fps
b = 1
while not _is_almost_int(a) and b < 100:
+212
View File
@@ -0,0 +1,212 @@
from .categories import NodeCategories
from .err import *
from .shared import ALWAYS_CHANGED_FLAG, hashed_as_strings
from .types import RGBPalette
def _generate_switch_input(type: str):
d = dict()
for i in range(10):
d["input_" + str(i)] = (type,)
return {
"required": {
"select": ("INT", {"defualt": 0, "min": 0, "max": 9}),
"on_missing": (["previous", "next"],)
},
"optional": d
}
def _do_pick(cls, select, on_missing, **args):
direction = 1
if on_missing == "previous":
direction = -1
if len(args) == 0:
on_error(cls, "No inputs provided!")
while args.get("input_" + str(select), None) is None:
select = (select + direction) % 10
return args["input_" + str(select)],
class DreamBigImageSwitch:
_switch_type = "IMAGE"
NODE_NAME = "Big Image Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigLatentSwitch:
_switch_type = "LATENT"
NODE_NAME = "Big Latent Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigTextSwitch:
_switch_type = "STRING"
NODE_NAME = "Big Text Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigPaletteSwitch:
_switch_type = RGBPalette.ID
NODE_NAME = "Big Palette Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return ALWAYS_CHANGED_FLAG
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigFloatSwitch:
_switch_type = "FLOAT"
NODE_NAME = "Big Float Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBigIntSwitch:
_switch_type = "INT"
NODE_NAME = "Big Int Switch"
ICON = "⭆"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = (_switch_type,)
RETURN_NAMES = ("selected",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return _generate_switch_input(cls._switch_type)
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, select, on_missing, **args):
return _do_pick(self.__class__, select, on_missing, **args)
class DreamBoolToFloat:
NODE_NAME = "Boolean To Float"
ICON = "⬖"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("result",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"boolean": ("BOOLEAN", {"default": False}),
"on_true": ("FLOAT", {"default": 1.0}),
"on_false": ("FLOAT", {"default": 0.0})
}
}
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, boolean, on_true, on_false):
if boolean:
return (on_true,)
else:
return (on_false,)
class DreamBoolToInt:
NODE_NAME = "Boolean To Int"
ICON = "⬖"
CATEGORY = NodeCategories.UTILS_SWITCHES
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("result",)
FUNCTION = "pick"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"boolean": ("BOOLEAN", {"default": False}),
"on_true": ("INT", {"default": 1}),
"on_false": ("INT", {"default": 0})
}
}
@classmethod
def IS_CHANGED(cls, *values):
return hashed_as_strings(values)
def pick(self, boolean, on_true, on_false):
if boolean:
return (on_true,)
else:
return (on_false,)
+78 -1
View File
@@ -26,6 +26,24 @@ class RGBPalette:
for c in colors:
self._colors.append(_fix_tuple(c))
def _calculate_channel_contrast(self, c):
hist = list(map(lambda _: 0, range(16)))
for pixel in self._colors:
hist[pixel[c] // 16] += 1
s = 0
max_possible = (15 - 0) * (len(self) // 2) * (len(self) // 2)
for i in range(16):
for j in range(i):
if i != j:
s += abs(i - j) * hist[i] * hist[j]
return s / max_possible
def _calculate_combined_contrast(self):
s = 0
for c in range(3):
s += self._calculate_channel_contrast(c)
return s / 3
def analyze(self):
total_red = 0
total_blue = 0
@@ -38,7 +56,7 @@ class RGBPalette:
r = float(total_red) / (255 * n)
g = float(total_green) / (255 * n)
b = float(total_blue) / (255 * n)
return ((r + g + b) / 3.0, r, g, b)
return ((r + g + b) / 3.0, self._calculate_combined_contrast(), r, g, b)
def __len__(self):
return len(self._colors)
@@ -64,6 +82,57 @@ class RGBPalette:
return _ColorIterator()
class PartialPrompt:
ID = "PARTIAL_PROMPT"
def __init__(self):
self._data = {}
def add(self, text: str, weight: float):
output = PartialPrompt()
output._data = dict(self._data)
for parts in text.split(","):
parts = parts.strip()
if " " in parts:
output._data["(" + parts + ")"] = weight
else:
output._data[parts] = weight
return output
def is_empty(self):
return not self._data
def abs_sum(self):
if not self._data:
return 0.0
return sum(map(abs, self._data.values()))
def abs_max(self):
if not self._data:
return 0.0
return max(map(abs, self._data.values()))
def scaled_by(self, f: float):
new_data = PartialPrompt()
new_data._data = dict(self._data)
for text, weight in new_data._data.items():
new_data._data[text] = weight * f
return new_data
def finalize(self, clamp: float):
items = self._data.items()
items = sorted(items, key=lambda pair: (pair[1], pair[0]))
pos = list()
neg = list()
for text, w in sorted(items, key=lambda pair: (-pair[1], pair[0])):
if w >= 0.0001:
pos.append("({}:{:.3f})".format(text, min(clamp, w)))
for text, w in sorted(items, key=lambda pair: (pair[1], pair[0])):
if w <= -0.0001:
neg.append("({}:{:.3f})".format(text, min(clamp, -w)))
return ", ".join(pos), ", ".join(neg)
class FrameCounter:
ID = "FRAME_COUNTER"
@@ -91,6 +160,14 @@ class FrameCounter:
def current_time_in_seconds(self):
return float(self.current_frame) / self.frames_per_second
@property
def total_time_in_seconds(self):
return float(self.total_frames) / self.frames_per_second
@property
def remaining_time_in_seconds(self):
return self.total_time_in_seconds - self.current_time_in_seconds
@property
def progress(self):
return float(self.current_frame) / (max(2, self.total_frames) - 1)