1 Commits
Author SHA1 Message Date
Morgan Johansson 240176df63 Color sampling/noise for areas. 2023-09-08 07:03:26 +02:00
16 changed files with 288 additions and 64 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:
+8
View File
@@ -101,6 +101,10 @@ 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.
@@ -112,6 +116,10 @@ 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.
+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 {