This commit is contained in:
justumen
2025-05-18 15:10:18 +02:00
parent b3690aaf3e
commit b9dffcfb96
4 changed files with 210 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
# 🔗 Comfyui : Bjornulf_custom_nodes v1.1.1 🔗
# 🔗 Comfyui : Bjornulf_custom_nodes v1.1.2 🔗
A list of 167 custom nodes for Comfyui : Display, manipulate, create and edit text, images, videos, loras, generate characters and more.
A list of 168 custom nodes for Comfyui : Display, manipulate, create and edit text, images, videos, loras, generate characters and more.
You can manage looping operations, generate randomized content, trigger logical conditions, pause and manually control your workflows and even work with external AI tools, like Ollama or Text To Speech.
⚠️ Warning : Very active development. Work in progress. 🏗

View File

@@ -127,7 +127,7 @@ from .style_selector import StyleSelector
from .split_image import SplitImageGrid, ReassembleImageGrid
from .API_openai import APIGenerateGPT4o
from .masks_nodes import LargestMaskOnly
from .masks_nodes import LargestMaskOnly, BoundingRectangleMask
from .openai_nodes import OpenAIVisionNode
from .loop_random_seed import LoopRandomSeed
@@ -141,6 +141,7 @@ from .loop_random_seed import LoopRandomSeed
NODE_CLASS_MAPPINGS = {
"Bjornulf_MatchTextToInput": MatchTextToInput,
"Bjornulf_LargestMaskOnly": LargestMaskOnly,
"Bjornulf_BoundingRectangleMask": BoundingRectangleMask,
"Bjornulf_OpenAIVisionNode": OpenAIVisionNode,
"Bjornulf_LoopRandomSeed": LoopRandomSeed,
# "Bjornulf_PurgeCLIPNode": PurgeCLIPNode,
@@ -325,7 +326,8 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"Bjornulf_MatchTextToInput": "🔛📝 Match 10 Text to Input",
"Bjornulf_LargestMaskOnly": "🖼🔪 Largest Mask Only",
"Bjornulf_LargestMaskOnly": "👺🔪 Largest Mask Only",
"Bjornulf_BoundingRectangleMask": "👺➜▢ Convert mask to rectangle",
"Bjornulf_OpenAIVisionNode": "🔮 OpenAI Vision Node",
"Bjornulf_LoopRandomSeed": "♻🎲 Loop Random Seed",
# "Bjornulf_RemoteTextEncodingWithCLIPs": "[BETA] 🔮 Remote Text Encoding with CLIPs",

View File

@@ -2,51 +2,220 @@ import numpy as np
import scipy.ndimage as ndi
import torch
class LargestMaskOnly:
class BodyPartSelectorMask:
@classmethod
def INPUT_TYPES(s):
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"selection": (["head", "hands", "feet"],),
}
}
RETURN_TYPES = ("MASK",)
FUNCTION = "process"
CATEGORY = "mask"
CATEGORY = "Bjornulf"
def process(self, mask):
def process_single(self, mask_np, selection):
"""
Process a single 2D mask to select head, hands, or feet based on position.
Args:
mask_np: 2D numpy array (H, W)
selection: str, one of "head", "hands", "feet"
Returns:
2D numpy array with selected shapes
"""
# Convert to binary mask
binary_mask = (mask_np > 0.5).astype(np.uint8)
# Label connected components
labeled_array, num_features = ndi.label(binary_mask)
if num_features < 5:
raise ValueError(f"Expected at least 5 components, found {num_features}")
# Compute sizes of all components (excluding background)
sizes = np.bincount(labeled_array.ravel())[1:]
# Select the five largest components
largest_indices = np.argsort(sizes)[-5:][::-1] # Top 5 in descending order
largest_labels = largest_indices + 1 # Map to label numbers (1-based)
# Compute centroids for the five largest components
centroids = []
for label in largest_labels:
positions = np.argwhere(labeled_array == label)
if len(positions) > 0:
centroid_row = positions[:, 0].mean() # Average row
centroid_col = positions[:, 1].mean() # Average column
centroids.append((label, centroid_row, centroid_col))
# Sort by centroid row (ascending, since row 0 is top)
centroids.sort(key=lambda x: x[1])
# Assign components based on vertical position
head_label = centroids[0][0] # Smallest row (top)
hand_labels = [centroids[1][0], centroids[2][0]] # Middle two
feet_labels = [centroids[3][0], centroids[4][0]] # Largest rows (bottom)
# Select labels based on user input
if selection == "head":
selected_labels = [head_label]
elif selection == "hands":
selected_labels = hand_labels
elif selection == "feet":
selected_labels = feet_labels
else:
raise ValueError("Selection must be 'head', 'hands', or 'feet'")
# Create new mask with selected components
new_mask = np.isin(labeled_array, selected_labels).astype(np.float32)
return new_mask
def process(self, mask, selection):
"""
Process the input mask(s) and return a new mask with selected parts.
Args:
mask: torch tensor, either 2D (H, W) or 3D (N, H, W)
selection: str, one of "head", "hands", "feet"
Returns:
Tuple containing the output mask tensor
"""
mask_np = mask.cpu().numpy()
if mask_np.ndim == 2:
# Single mask
result = self.process_single(mask_np, selection)
result = result[None, ...] # Add batch dimension: (1, H, W)
elif mask_np.ndim == 3:
# Batched masks
results = [self.process_single(mask_np[i], selection)
for i in range(mask_np.shape[0])]
result = np.stack(results, axis=0) # Stack to (N, H, W)
else:
raise ValueError("Mask must be 2D (H, W) or 3D (N, H, W)")
return (torch.from_numpy(result),)
class LargestMaskOnly:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"num_masks": ("INT", {"default": 1, "min": 1, "max": 10}),
}
}
RETURN_TYPES = ("MASK",)
FUNCTION = "process"
CATEGORY = "Bjornulf"
def process_single(self, mask_np, num_masks):
"""Process a single mask to keep the top num_masks largest components."""
# Convert to binary mask
binary_mask = (mask_np > 0.5).astype(np.uint8)
# Label connected components
labeled_array, num_features = ndi.label(binary_mask)
if num_features > 0:
# Get sizes of all components, excluding background (label 0)
sizes = np.bincount(labeled_array.ravel())[1:]
# Determine how many components to keep
k = min(num_masks, num_features)
if k > 0:
# Get indices of the top k largest components (descending order)
top_indices = np.argsort(sizes)[::-1][:k]
# Map indices to labels (add 1 since sizes[1:] starts at label 1)
top_labels = top_indices + 1
# Create mask with only the top k components
largest_mask = np.isin(labeled_array, top_labels).astype(np.float32)
else:
largest_mask = np.zeros_like(binary_mask, dtype=np.float32)
else:
# No components found, return an empty mask
largest_mask = np.zeros_like(binary_mask, dtype=np.float32)
return largest_mask
def process(self, mask, num_masks):
"""Process the input mask(s) and return the top num_masks largest components."""
# Convert mask to numpy array
mask_np = mask.cpu().numpy()
# Print debug info about mask
print(f"Mask shape: {mask_np.shape}")
print(f"Mask dtype: {mask_np.dtype}")
print(f"Mask min: {mask_np.min()}, max: {mask_np.max()}")
# Ensure binary mask (0 and 1)
binary_mask = (mask_np > 0.5).astype(np.uint8)
# Use scipy's label function instead of OpenCV
labeled_array, num_features = ndi.label(binary_mask)
print(f"Found {num_features} connected components")
if num_features <= 1: # No components or just one
return (mask,)
# Find sizes of all labeled regions
sizes = np.bincount(labeled_array.ravel())
# Skip background (label 0)
if len(sizes) > 1:
sizes = sizes[1:]
# Find the label of the largest component (add 1 because we skipped background)
largest_label = np.argmax(sizes) + 1
# Create a mask with only the largest component
largest_mask = (labeled_array == largest_label).astype(np.float32)
if mask_np.ndim == 2:
# Single mask: process and add batch dimension
result = self.process_single(mask_np, num_masks)
result = result[None, ...] # Shape becomes (1, H, W)
elif mask_np.ndim == 3:
# Batched masks: process each mask independently
results = [self.process_single(mask_np[i], num_masks) for i in range(mask_np.shape[0])]
result = np.stack(results, axis=0) # Shape remains (N, H, W)
else:
# Fallback if something went wrong with the labeling
largest_mask = binary_mask.astype(np.float32)
raise ValueError("Invalid mask shape: expected 2D (H, W) or 3D (N, H, W)")
# Convert back to tensor and return
result = torch.from_numpy(largest_mask)
return (result,)
# Convert back to torch tensor and return as a tuple
return (torch.from_numpy(result),)
class BoundingRectangleMask:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"up": ("INT", {"default": 0, "min": -10000, "max": 10000}),
"down": ("INT", {"default": 0, "min": -10000, "max": 10000}),
"right": ("INT", {"default": 0, "min": -10000, "max": 10000}),
"left": ("INT", {"default": 0, "min": -10000, "max": 10000}),
}
}
RETURN_TYPES = ("MASK",)
FUNCTION = "process"
CATEGORY = "Bjornulf"
def process_single(self, mask_np, up, down, right, left):
active = mask_np > 0.5
if not np.any(active):
return np.zeros_like(mask_np, dtype=np.float32)
rows_with_active = np.any(active, axis=1)
cols_with_active = np.any(active, axis=0)
min_row = np.where(rows_with_active)[0][0]
max_row = np.where(rows_with_active)[0][-1]
min_col = np.where(cols_with_active)[0][0]
max_col = np.where(cols_with_active)[0][-1]
min_row_adj = min_row - up
max_row_adj = max_row + down
min_col_adj = min_col - left
max_col_adj = max_col + right
H, W = mask_np.shape
min_row_adj = max(0, min_row_adj)
max_row_adj = min(H - 1, max_row_adj)
min_col_adj = max(0, min_col_adj)
max_col_adj = min(W - 1, max_col_adj)
if min_row_adj > max_row_adj or min_col_adj > max_col_adj:
return np.zeros_like(mask_np, dtype=np.float32)
new_mask = np.zeros_like(mask_np, dtype=np.float32)
new_mask[min_row_adj:max_row_adj + 1, min_col_adj:max_col_adj + 1] = 1.0
return new_mask
def process(self, mask, up, down, right, left):
mask_np = mask.cpu().numpy()
if mask_np.ndim == 2:
result = self.process_single(mask_np, up, down, right, left)
result = result[None, ...]
elif mask_np.ndim == 3:
results = [self.process_single(mask_np[i], up, down, right, left)
for i in range(mask_np.shape[0])]
result = np.stack(results, axis=0)
else:
raise ValueError("Mask must be 2D (H, W) or 3D (N, H, W)")
return (torch.from_numpy(result),)

View File

@@ -1,7 +1,7 @@
[project]
name = "bjornulf_custom_nodes"
description = "167 ComfyUI nodes : Display, manipulate, and edit text, images, videos, loras, generate characters and more. Manage looping operations, generate randomized content, use logical conditions and work with external AI tools, like Ollama or Text To Speech, etc..."
version = "1.1.1"
description = "168 ComfyUI nodes : Display, manipulate, and edit text, images, videos, loras, generate characters and more. Manage looping operations, generate randomized content, use logical conditions and work with external AI tools, like Ollama or Text To Speech, etc..."
version = "1.1.2"
license = {file = "LICENSE"}
[project.urls]