diff --git a/README.md b/README.md index 95b9b5c..5922181 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ This repository contains custom nodes for ComfyUI created and used by SuperBeasts.AI (@SuperBeasts.AI on Instagram) +##Updates +- 30/04/24: +-- Updated readme with documentation +-- Added Deflicker and PixelDeflicker nodes for reducing flickering artifacts in image sequences. +-- Introduced CrossFadeImageBatches node for smooth cross-fade transitions between image batches +-- Enhanced ImageBatchManagement and MaskBatchManagement nodes with resizing, cropping, and reordering capabilities +- 27/03/24: Released and also pushed an update to resolve batched images for videos etc + +# Image Effects + ## HDR Effects (SuperBeasts.AI) The HDR Effects is an image processing application that enhances the dynamic range and visual appeal of input images. It provides a set of adjustable parameters to fine-tune the HDR effect according to user preferences. @@ -51,5 +61,61 @@ The application provides the following adjustable parameters: - Enhances the color saturation of the image - Higher values result in more vibrant colors -##Updates -- 27/03/24: Released and also pushed an update to resolve batched images for videos etc + +#Video tools + +## Deflicker + +The Deflicker node is designed to reduce flickering artifacts in a sequence of images. It adjusts the brightness of each frame based on the average brightness of its surrounding context frames. + +### Features +- Adjusts brightness of each frame based on the context frames +- Applies noise reduction and gradient smoothing to the adjusted images +- Blends the adjusted images with the original images using adaptive blending strength +- Supports batch processing for efficient computation + +## PixelDeflicker + +The PixelDeflicker node reduces flickering artifacts in a sequence of images by applying temporal smoothing at the pixel level. It blends the smoothed frames with the original frames to achieve a more stable output. + +### Features +- Applies temporal smoothing to reduce flickering +- Blends the smoothed frames with the original frames +- Supports batch processing for efficient computation + +# Image & Mask Batch Management + +Keep your image and masks sized, cropped and ordered how ever you like without having to recreate the masks or mess with connections. +Note the resizing capability doesn't provide any settings, we simply crop and resize to maximise the image size with your provided width/height. + +![Batch Management](examples/BatchManagers.jpg) + + +## ImageBatchManagement + +The ImageBatchManagement node provides functionality to resize, crop, and reorder a batch of images. It ensures that all images in the batch have the same specified dimensions. + +### Features +- Resizes and crops images to the specified width and height +- Allows reordering the images in the batch based on a provided order +- Supports unlimited input images + +## MaskBatchManagement + +The MaskBatchManagement node is similar to the ImageBatchManagement node but works with mask inputs. It resizes, crops, and reorders a batch of masks to match the specified dimensions. + +### Features +- Resizes and crops masks to the specified width and height +- Allows reordering the masks in the batch based on a provided order +- Supports unlimited input masks + + +# Deprecated / Removed + +## MakeResizedMaskBatch (Deprecated please use MaskBatchManagement) +The MakeResizedMaskBatch node creates a batch of masks from multiple individual masks or batches. +It resizes and crops the input masks to match the specified width and height. + +## Cross Fade Image Batches (SuperBeasts.AI) +There is another preexisting node that does this and more so I've removed this function. + diff --git a/__init__.py b/__init__.py index 1b0585f..4acdb1f 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,3 @@ +WEB_DIRECTORY = "js" from .superbeastsai_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/examples/BatchManagers.jpg b/examples/BatchManagers.jpg new file mode 100644 index 0000000..f373996 Binary files /dev/null and b/examples/BatchManagers.jpg differ diff --git a/js/superbeasts.js b/js/superbeasts.js new file mode 100644 index 0000000..e923d4c --- /dev/null +++ b/js/superbeasts.js @@ -0,0 +1,77 @@ +import { ComfyApp, app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; + +app.registerExtension({ + name: "Comfy.superbeastsai_nodes", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name == "Image Batch Manager (SuperBeasts.AI)" || + nodeData.name == "Mask Batch Manager (SuperBeasts.AI)") { + + var input_name = "input"; + + switch(nodeData.name) { + + case 'Image Batch Manager (SuperBeasts.AI)': + input_name = "image"; + break; + + case 'Mask Batch Manager (SuperBeasts.AI)': + input_name = "mask"; + break; + } + + const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) { + if (!link_info) return; + + const node = app.graph.getNodeById(link_info.origin_id); + if (!node) { + return; + } + + let slot_type = node.outputs[link_info.origin_slot].type; + + + + let select_slot = this.inputs.find(x => x.name == "select"); + let mode_slot = this.inputs.find(x => x.name == "sel_mode"); + + let converted_count = 0; + converted_count += select_slot?1:0; + converted_count += mode_slot?1:0; + + if (!connected && (this.inputs.length > 1+converted_count)) { + const stackTrace = new Error().stack; + + if( + !stackTrace.includes('LGraphNode.prototype.connect') && // for touch device + !stackTrace.includes('LGraphNode.connect') && // for mouse device + !stackTrace.includes('loadGraphData') && + this.inputs[index].name != 'select') { + this.removeInput(index); + } + } + + let slot_i = 1; + + for (let i = 0; i < this.inputs.length; i++) { + let input_i = this.inputs[i]; + if(input_i.name != 'select'&& input_i.name != 'sel_mode') { + input_i.name = `${input_name}${slot_i}` + slot_i++; + } + } + + let last_slot = this.inputs[this.inputs.length - 1]; + + if ( + (last_slot.name == 'select' && last_slot.name != 'sel_mode' && this.inputs[this.inputs.length - 2].link != undefined) + || (last_slot.name != 'select' && last_slot.name != 'sel_mode' && last_slot.link != undefined)) { + this.addInput(`${input_name}${slot_i}`, this.outputs[0].type); + } + + } + + } + } +}); diff --git a/superbeastsai_nodes.py b/superbeastsai_nodes.py index 01f2c91..102307b 100644 --- a/superbeastsai_nodes.py +++ b/superbeastsai_nodes.py @@ -410,57 +410,6 @@ class PixelDeflicker: return (blended_tensor,) -class CrossFadeImageBatches: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "batch1": ("IMAGE",), - "batch2": ("IMAGE",), - "fade_frames": ("INT", {"default": 10, "min": 1, "max": 100, "step": 1}), - "fade_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), - "ease_type": (["linear", "ease_in", "ease_out", "ease_in_out"], {"default": "linear"}), - } - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "crossfade" - CATEGORY = "SuperBeastsAI/Animation" - - def crossfade(self, batch1, batch2, fade_frames=10, fade_strength=0.5, ease_type="linear"): - num_frames_batch1 = batch1.shape[0] - num_frames_batch2 = batch2.shape[0] - - if num_frames_batch1 < fade_frames or num_frames_batch2 < fade_frames: - raise ValueError("The number of fade frames must be less than or equal to the number of frames in each batch.") - - if batch1.shape[2:] != batch2.shape[2:]: - raise ValueError("The spatial dimensions of batch1 and batch2 must match. Please resize the batches to the same size before using this node.") - - # Extract the frames to be faded from each batch - fade_frames_batch1 = batch1[-fade_frames:] - fade_frames_batch2 = batch2[:fade_frames] - - # Generate the interpolation weights based on the selected easing function - if ease_type == "linear": - weights = torch.linspace(0, 1, fade_frames) - elif ease_type == "ease_in": - weights = torch.pow(torch.linspace(0, 1, fade_frames), 2) - elif ease_type == "ease_out": - weights = 1 - torch.pow(torch.linspace(1, 0, fade_frames), 2) - elif ease_type == "ease_in_out": - weights = 0.5 * (1 - torch.cos(torch.linspace(0, torch.pi, fade_frames))) - - weights = weights.view(-1, 1, 1, 1) # Reshape weights to match the dimensions of the frames - - # Perform the cross-fade between the fade frames of each batch - faded_frames = fade_frames_batch1 * (1 - weights * fade_strength) + fade_frames_batch2 * (weights * fade_strength) - - # Combine the non-faded frames from batch1, the faded frames, and the remaining frames from batch2 - output_batch = torch.cat((batch1[:-fade_frames], faded_frames, batch2[fade_frames:]), dim=0) - - return (output_batch,) - def resize_and_crop(pil_img, target_width, target_height): """Resize and crop an image to fit exactly the specified dimensions.""" original_width, original_height = pil_img.size @@ -503,11 +452,11 @@ class ImageBatchManagement: "required": { "width": ("INT", {"default": 512}), "height": ("INT", {"default": 768}), - "ordering_enabled": (["disabled", "enabled"], {"default": "disabled"}) + "ordering_enabled": (["disabled", "enabled"], {"default": "disabled"}), + "image1": ("IMAGE",) # Ensure at least one image is required }, "optional": { "new_order": ("STRING", {"default": ""}), - **{f"image{i}": ("IMAGE",) for i in range(1, 13)} }, } @@ -516,8 +465,12 @@ class ImageBatchManagement: CATEGORY = "SuperBeastsAI/Image" def reorder(self, width, height, ordering_enabled, new_order, **kwargs): - image_keys = [f'image{i}' for i in range(1, 13)] - images = [kwargs.get(key) for key in image_keys if kwargs.get(key) is not None] + images = [kwargs["image1"]] # Start with the required image1 input + + i = 2 + while f"image{i}" in kwargs: + images.append(kwargs[f"image{i}"]) + i += 1 if ordering_enabled == "enabled" and new_order: order_indices = [int(idx) - 1 for idx in new_order.split(',') if idx.strip()] @@ -578,49 +531,15 @@ class MaskBatchManagement: return (result,) -class ConcatConditionings2: - @classmethod - def INPUT_TYPES(s): - return {"required": { - "conditioning1": ("CONDITIONING", ), - }, - } - RETURN_TYPES = ("CONDITIONING", ) - FUNCTION = "doit" - - CATEGORY = "ImpactPack/Util" - - def doit(self, **kwargs): - conditioning_to = list(kwargs.values())[0] - - for k, conditioning_from in list(kwargs.items())[1:]: - out = [] - if len(conditioning_from) > 1: - print("Warning: ConcatConditionings {k} contains more than 1 cond, only the first one will actually be applied to conditioning1.") - - cond_from = conditioning_from[0][0] - - for i in range(len(conditioning_to)): - t1 = conditioning_to[i][0] - tw = torch.cat((t1, cond_from), 1) - n = [tw, conditioning_to[i][1].copy()] - out.append(n) - - conditioning_to = out - - return (out, ) NODE_CLASS_MAPPINGS = { 'HDR Effects (SuperBeasts.AI)': HDREffects, 'Make Resized Mask Batch (SuperBeasts.AI)': MakeResizedMaskBatch, 'Deflicker (SuperBeasts.AI)': Deflicker, 'Pixel Deflicker (SuperBeasts.AI)': PixelDeflicker, - 'Cross Fade Image Batches (SuperBeasts.AI)': CrossFadeImageBatches, 'Mask Batch Manager (SuperBeasts.AI)': MaskBatchManagement, - 'Image Batch Manager (SuperBeasts.AI)': ImageBatchManagement, - 'Batch Manager Test (SuperBeasts.AI)': ConcatConditionings2 - + 'Image Batch Manager (SuperBeasts.AI)': ImageBatchManagement } NODE_DISPLAY_NAME_MAPPINGS = { @@ -628,9 +547,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { 'MakeResizedMaskBatch': 'Make Resized Mask Batch (SuperBeasts.AI)', 'Deflicker': 'Deflicker (SuperBeasts.AI)', 'PixelDeflicker': 'Pixel Deflicker (SuperBeasts.AI)', - 'CrossFadeImageBatches': 'Cross Fade Image Batches (SuperBeasts.AI)', 'MaskBatchManagement':'Mask Batch Manager (SuperBeasts.AI)', - 'ImageBatchManagement':'Image Batch Manager (SuperBeasts.AI)', - 'ConcatConditionings2':'Batch Manager Test (SuperBeasts.AI)' - + 'ImageBatchManagement':'Image Batch Manager (SuperBeasts.AI)' } \ No newline at end of file