2 Commits
Author SHA1 Message Date
Morgan Johansson 240176df63 Color sampling/noise for areas. 2023-09-08 07:03:26 +02:00
Morgan Johansson 5e92827e0a Readme update - known issue. 2023-09-07 18:35:32 +02:00
16 changed files with 374 additions and 136 deletions
+11 -11
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, 0, 0),
"author": "Dream Project",
"project": "https://github.com/alt-key-project/comfyui-dream-project",
"description": "Various utility nodes for creating animations with ComfyUI",
+2 -1
View File
@@ -1,7 +1,8 @@
import glob
from .categories import NodeCategories
from .shared import *
from .types import *
import glob
class DreamDirectoryFileCount:
+81 -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})
},
}
+6 -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))))
+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()
+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)
+2 -3
View File
@@ -1,8 +1,7 @@
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:
+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]": "",
"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"
}
+125 -19
View File
@@ -1,6 +1,25 @@
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:
@@ -12,8 +31,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 +46,115 @@ 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
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),)
+9 -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:
+94 -72
View File
@@ -51,83 +51,105 @@ 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)
## 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 fixed parameters (that 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!
+9 -5
View File
@@ -1,11 +1,15 @@
from typing import Iterable, Tuple
import os
import random
import shutil
import subprocess
import tempfile
from functools import lru_cache
from PIL import Image
from .types import *
from .categories import NodeCategories
from .shared import DreamConfig, DreamImage
import os, tempfile, subprocess, shutil, random
from functools import lru_cache
from PIL import Image
from .types import *
CONFIG = DreamConfig()
+16 -4
View File
@@ -1,14 +1,17 @@
import hashlib, os, json, glob
import glob
import hashlib
import json
import os
import folder_paths as comfy_paths
import numpy
import random
import torch
from PIL import Image, ImageFilter
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")
@@ -89,6 +92,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 +126,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 {