added color replace node

This commit is contained in:
Fill
2025-09-14 04:39:58 -07:00
parent 1e60d3802b
commit 9ffc6ce9b4
4 changed files with 345 additions and 1 deletions
+3
View File
@@ -10,6 +10,7 @@ from .nodes.FL_Image_Pixelator import FL_ImagePixelator
from .nodes.FL_Image_AddToBatch import FL_ImageAddToBatch
from .nodes.FL_DirectoryCrawl import FL_DirectoryCrawl
from .nodes.FL_Ascii import FL_Ascii
from .nodes.FL_ReplaceColor import FL_ReplaceColor
from .nodes.FL_ImageAddNoise import FL_ImageAddNoise
from .nodes.FL_WordFrequencyGraph import FL_WordFrequencyGraph
from .nodes.FL_Glitch import FL_Glitch
@@ -160,6 +161,7 @@ NODE_CLASS_MAPPINGS = {
"FL_ImageAddToBatch": FL_ImageAddToBatch,
"FL_DirectoryCrawl": FL_DirectoryCrawl,
"FL_Ascii": FL_Ascii,
"FL_ReplaceColor": FL_ReplaceColor,
"FL_ImageAddNoise": FL_ImageAddNoise,
"FL_WordFrequencyGraph": FL_WordFrequencyGraph,
"FL_Glitch": FL_Glitch,
@@ -307,6 +309,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FL_ImageAddToBatch": "FL Image Add To Batch",
"FL_DirectoryCrawl": "FL Directory Crawl",
"FL_Ascii": "FL Ascii",
"FL_ReplaceColor": "FL Replace Color",
"FL_ImageAddNoise": "FL Image Add Noise",
"FL_WordFrequencyGraph": "FL Word Frequency Graph",
"FL_Glitch": "FL Glitch",
+82
View File
@@ -0,0 +1,82 @@
import torch
import numpy as np
from PIL import Image
class FL_ReplaceColor:
DESCRIPTION = """
FL_ReplaceColor allows you to replace a specific color in an image with another color.
You can use the interactive color pickers to select the source color to replace and the target color to replace it with.
The tolerance parameter controls how closely colors need to match the source color to be replaced.
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"source_color": ("STRING", {"default": "#FF0000"}),
"target_color": ("STRING", {"default": "#00FF00"}),
"tolerance": ("FLOAT", {
"default": 0.1,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"description": "Color matching tolerance (0.0 = exact match, 1.0 = very loose)"
})
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "replace_color"
CATEGORY = "🏵️Fill Nodes/VFX"
def replace_color(self, image: torch.Tensor, source_color: str, target_color: str, tolerance: float):
batch_size = image.shape[0]
result = torch.zeros_like(image)
# Convert hex colors to RGB
source_rgb = self.hex_to_rgb(source_color)
target_rgb = self.hex_to_rgb(target_color)
for b in range(batch_size):
img_b = image[b] * 255.0
img_array = img_b.numpy().astype('uint8')
# Convert to PIL Image for processing
pil_image = Image.fromarray(img_array, 'RGB')
result_image = self.replace_color_in_image(pil_image, source_rgb, target_rgb, tolerance)
# Convert back to tensor
result_array = np.array(result_image).astype(np.float32) / 255.0
result[b] = torch.from_numpy(result_array)
return (result,)
def hex_to_rgb(self, hex_color: str):
"""Convert hex color to RGB tuple"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def replace_color_in_image(self, image: Image.Image, source_rgb, target_rgb, tolerance):
"""Replace colors in PIL Image"""
img_array = np.array(image)
# Calculate color distance for each pixel
source_color = np.array(source_rgb)
# Compute Euclidean distance in RGB space
distances = np.sqrt(np.sum((img_array - source_color) ** 2, axis=2))
# Normalize distance to 0-1 range (max distance in RGB is sqrt(3*255^2))
max_distance = np.sqrt(3 * 255 ** 2)
normalized_distances = distances / max_distance
# Create mask for pixels within tolerance
mask = normalized_distances <= tolerance
# Replace colors
result_array = img_array.copy()
result_array[mask] = target_rgb
return Image.fromarray(result_array.astype(np.uint8))
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_fill-nodes"
description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection."
version = "1.7.7"
version = "1.7.8"
license = "LICENSE"
dependencies = ["diffusers", "librosa", "sounddevice", "glitch_this", "PyOpenGL", "glfw", "scipy>=1.13.1", "requests", "aiohttp", "moviepy", "matplotlib", "reportlab", "openai", "PyPDF2", "pdf2image", "PyMuPDF", "reportlab", "PyPDF2", "ollama", "kornia", "opencv-python", "gdown", "open_clip_torch", "google-genai"]
+259
View File
@@ -0,0 +1,259 @@
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
import { api } from "../../../scripts/api.js";
// Helper function to convert RGB to hex
function rgb_to_hex(r, g, b) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}
// Helper function to convert hex to RGB
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// Create an eyedropper color picker widget
function createEyedropperPicker(node, colorWidget, label) {
const PADDING = 10;
const WIDGET_HEIGHT = 40;
const WIDGET_MARGIN = 5;
let selectedColor = colorWidget.value || "#FF0000";
function updateColorOutput() {
if (colorWidget) colorWidget.value = selectedColor;
if (hexInputWidget) hexInputWidget.value = selectedColor;
let rgb = hexToRgb(selectedColor);
if (rSlider) rSlider.value = rgb.r;
if (gSlider) gSlider.value = rgb.g;
if (bSlider) bSlider.value = rgb.b;
node.setDirtyCanvas(true, true);
}
// Check if EyeDropper API is supported
const supportsEyeDropper = typeof window.EyeDropper !== 'undefined';
// Eyedropper button widget
const eyedropperWidget = {
name: `${label}_EYEDROPPER_DISPLAY`,
type: "CANVAS_WIDGET",
draw: function (ctx, node, widgetWidth, widgetY, height) {
const drawY = widgetY + PADDING;
ctx.save();
ctx.translate(PADDING, drawY);
// Draw label
ctx.fillStyle = "white";
ctx.font = "12px Arial";
ctx.textAlign = "left";
ctx.fillText(label, 0, -5);
// Draw eyedropper button
const buttonWidth = widgetWidth - PADDING * 2;
const buttonHeight = WIDGET_HEIGHT;
// Button background
ctx.fillStyle = supportsEyeDropper ? "#4a4a4a" : "#666666";
ctx.fillRect(0, 0, buttonWidth, buttonHeight);
// Button border
ctx.strokeStyle = "#888888";
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, buttonWidth, buttonHeight);
// Eyedropper icon (simplified)
ctx.strokeStyle = "white";
ctx.lineWidth = 2;
const centerX = buttonWidth / 2;
const centerY = buttonHeight / 2;
if (supportsEyeDropper) {
// Draw eyedropper icon
ctx.beginPath();
ctx.moveTo(centerX - 8, centerY + 8);
ctx.lineTo(centerX - 4, centerY + 4);
ctx.lineTo(centerX + 4, centerY - 4);
ctx.lineTo(centerX + 8, centerY - 8);
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX + 6, centerY - 6, 3, 0, Math.PI * 2);
ctx.stroke();
// Button text
ctx.fillStyle = "white";
ctx.font = "11px Arial";
ctx.textAlign = "center";
ctx.fillText("Pick Color", centerX, centerY + 15);
} else {
// Not supported message
ctx.fillStyle = "#999999";
ctx.font = "10px Arial";
ctx.textAlign = "center";
ctx.fillText("Eyedropper not supported", centerX, centerY - 2);
ctx.fillText("Use manual input below", centerX, centerY + 10);
}
// Color preview bar
ctx.fillStyle = selectedColor;
ctx.fillRect(0, buttonHeight + WIDGET_MARGIN, buttonWidth, 20);
// Color text
ctx.fillStyle = "white";
ctx.font = "10px Arial";
ctx.textAlign = "left";
ctx.fillText(selectedColor, 5, buttonHeight + WIDGET_MARGIN + 15);
ctx.restore();
},
mouse: function (event, pos, node) {
if (!supportsEyeDropper) return false;
// Simplified click detection - check if click is within widget bounds
const widgetRect = this.computeArea();
const relativeX = pos[0] - PADDING;
const relativeY = pos[1] - (widgetRect.y + PADDING + 12); // Account for label offset
const buttonWidth = node.size[0] - PADDING * 2;
const buttonHeight = WIDGET_HEIGHT;
if (event.type === "pointerdown") {
// Check if click is within button area
if (relativeX >= 0 && relativeX <= buttonWidth &&
relativeY >= 0 && relativeY <= buttonHeight) {
console.log("Eyedropper button clicked"); // Debug log
// Prevent event bubbling immediately
event.preventDefault();
event.stopPropagation();
// Add small delay to ensure click is processed before launching eyedropper
setTimeout(() => {
try {
const eyeDropper = new EyeDropper();
console.log("Opening eyedropper...");
eyeDropper.open()
.then(result => {
console.log("Color picked successfully:", result.sRGBHex);
selectedColor = result.sRGBHex.toUpperCase();
updateColorOutput();
node.setDirtyCanvas(true, true); // Force redraw
})
.catch(err => {
if (err.name === 'AbortError') {
console.log("Eyedropper selection was canceled by user");
} else {
console.error("Eyedropper failed:", err);
}
});
} catch (error) {
console.error("Error creating EyeDropper:", error);
alert("EyeDropper API is not available in this browser. Please use the hex input or RGB sliders below.");
}
}, 100); // Small delay to prevent conflicts
return true;
}
}
return false;
},
computeSize: function (width) {
const totalHeight = PADDING * 2 + 12 + WIDGET_HEIGHT + WIDGET_MARGIN + 20; // label + button + color bar
return [width, totalHeight];
},
computeArea: function() {
let y = this.y || 0;
let totalHeight = this.computeSize(node.size[0])[1];
return { x: 0, y: y, w: node.size[0], h: totalHeight };
}
};
// Manual hex input widget
const hexInputWidget = node.addWidget("text", `${label} Hex`, selectedColor, (value) => {
if (/^#([0-9A-Fa-f]{6})$/.test(value)) {
selectedColor = value.toUpperCase();
updateColorOutput();
}
});
// RGB Sliders for manual adjustment
let rgb = hexToRgb(selectedColor);
const rSlider = node.addWidget("slider", `${label} R`, rgb.r, (value) => {
rgb.r = Math.round(value);
selectedColor = rgb_to_hex(rgb.r, rgb.g, rgb.b);
updateColorOutput();
}, { min: 0, max: 255, step: 1 });
const gSlider = node.addWidget("slider", `${label} G`, rgb.g, (value) => {
rgb.g = Math.round(value);
selectedColor = rgb_to_hex(rgb.r, rgb.g, rgb.b);
updateColorOutput();
}, { min: 0, max: 255, step: 1 });
const bSlider = node.addWidget("slider", `${label} B`, rgb.b, (value) => {
rgb.b = Math.round(value);
selectedColor = rgb_to_hex(rgb.r, rgb.g, rgb.b);
updateColorOutput();
}, { min: 0, max: 255, step: 1 });
// Hide original widget
colorWidget.type = "HIDDEN";
// Setup synchronization
const originalCallback = colorWidget.callback;
colorWidget.callback = (value) => {
if (value !== selectedColor) {
selectedColor = value;
node.setDirtyCanvas(true, true);
}
};
const originalSetValue = colorWidget.setValue;
colorWidget.setValue = function(value) {
originalSetValue?.call(this, value);
selectedColor = value || "#FF0000";
updateColorOutput();
};
// Initial setup
selectedColor = colorWidget.value || "#FF0000";
setTimeout(() => {
selectedColor = colorWidget.value || "#FF0000";
updateColorOutput();
}, 0);
return eyedropperWidget;
}
app.registerExtension({
name: "Comfy.FL_ReplaceColor",
async nodeCreated(node) {
if (node.comfyClass === "FL_ReplaceColor") {
// Find the color widgets
const sourceColorWidget = node.widgets.find(w => w.name === "source_color");
const targetColorWidget = node.widgets.find(w => w.name === "target_color");
if (!sourceColorWidget || !targetColorWidget) {
console.error("FL_ReplaceColor: Could not find color widgets!");
return;
}
// Create eyedropper pickers
const sourcePickerWidget = createEyedropperPicker(node, sourceColorWidget, "Source Color");
const targetPickerWidget = createEyedropperPicker(node, targetColorWidget, "Target Color");
node.addCustomWidget(sourcePickerWidget);
node.addCustomWidget(targetPickerWidget);
// Recompute node size after adding widgets
node.setSize(node.computeSize());
}
}
});