added color replace node
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user