fix smart colors

This commit is contained in:
Slava Cherkasov
2026-04-10 13:23:40 +02:00
parent e46ecd19b5
commit 1336cc8d66
2 changed files with 159 additions and 12 deletions
+124
View File
@@ -0,0 +1,124 @@
import { app } from "../../../scripts/app.js";
function normalizeHexColor(value, fallback = "#000000") {
if (typeof value !== "string") {
return fallback;
}
const normalized = value.startsWith("#") ? value : `#${value}`;
return /^#[0-9a-fA-F]{6}$/.test(normalized) ? normalized : fallback;
}
function getBrightness(hexColor) {
const hex = normalizeHexColor(hexColor).slice(1);
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return (r * 299 + g * 587 + b * 114) / 1000;
}
function createColorWidget(name, value) {
const widget = {
name,
type: "SMART_COLOR",
value: normalizeHexColor(value),
options: { default: "#000000" },
draw(ctx, node, widgetWidth, widgetY, height) {
const border = 3;
ctx.fillStyle = "#000";
ctx.fillRect(0, widgetY, widgetWidth, height);
ctx.fillStyle = this.value;
ctx.fillRect(
border,
widgetY + border,
widgetWidth - border * 2,
height - border * 2
);
ctx.fillStyle = getBrightness(this.value) > 125 ? "#000" : "#fff";
ctx.font = "14px Arial";
ctx.textAlign = "center";
ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14);
},
mouse(event, pos, node) {
if (event.type !== "pointerdown") {
return false;
}
const widgets = node.widgets.filter((widgetItem) => widgetItem.type === "SMART_COLOR");
for (const colorWidget of widgets) {
const rect = [colorWidget.last_y, colorWidget.last_y + 32];
if (pos[1] <= rect[0] || pos[1] >= rect[1]) {
continue;
}
const picker = document.createElement("input");
picker.type = "color";
picker.value = normalizeHexColor(this.value);
Object.assign(picker.style, {
position: "fixed",
left: `${event.clientX}px`,
top: `${event.clientY}px`,
height: "0px",
width: "0px",
padding: "0px",
opacity: 0,
});
picker.addEventListener("blur", () => {
this.callback?.(this.value);
node.graph._version++;
picker.remove();
});
picker.addEventListener("input", () => {
if (!picker.value) {
return;
}
this.value = normalizeHexColor(picker.value, this.value);
app.canvas.setDirty(true);
});
document.body.appendChild(picker);
requestAnimationFrame(() => {
if (picker.showPicker) {
picker.showPicker();
} else {
picker.click();
}
picker.focus();
});
return true;
}
return false;
},
computeSize(width) {
return [width, 32];
},
};
return widget;
}
app.registerExtension({
name: "Comfy.SmartNodes.SmartColorWidget",
getCustomWidgets() {
return {
SMART_COLOR: (node, inputName, inputData) => ({
widget: node.addCustomWidget(
createColorWidget(inputName, inputData?.[1]?.default || "#000000")
),
minWidth: 150,
minHeight: 32,
}),
};
},
});
+35 -12
View File
@@ -1068,6 +1068,7 @@ class SmartBackgroundRemove:
"tolerance": ("FLOAT", {"default": 0.01, "min": 0.0, "max": 1.0, "step": 0.01}),
"each_point_own_color": ("BOOLEAN", {"default": True}),
"debug_points": ("BOOLEAN", {"default": False}),
"clearEdges": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}),
},
"optional": {
"start_point": ("POINT",),
@@ -1140,7 +1141,7 @@ class SmartBackgroundRemove:
return background_mask # Return the final uint8 background mask
def flood_remove(self, image, fill_size, tolerance, each_point_own_color, debug_points, start_point=None, start_points=None):
def flood_remove(self, image, fill_size, tolerance, each_point_own_color, debug_points, clearEdges=0, start_point=None, start_points=None):
# Convert from tensor to numpy array
input_image = 255. * image.cpu().numpy()
batch_size = input_image.shape[0]
@@ -1228,6 +1229,14 @@ class SmartBackgroundRemove:
result[:, :, :3] = img
result[:, :, 3] = mask * 255
# Clear edge pixels to transparent
if clearEdges > 0:
ce = clearEdges
result[:ce, :, 3] = 0 # Top
result[-ce:, :, 3] = 0 # Bottom
result[:, :ce, 3] = 0 # Left
result[:, -ce:, 3] = 0 # Right
# Draw debug points if requested
if debug_points and points_to_use:
# Ensure result is uint8 for OpenCV drawing functions
@@ -2204,9 +2213,11 @@ class SmartSaveAnimatedPNG:
else:
filename = filename + '.gif'
# Convert relative path to absolute path within ComfyUI's output directory
output_dir = folder_paths.get_output_directory()
full_path = os.path.join(output_dir, filename)
if os.path.isabs(filename):
full_path = filename
else:
full_path = os.path.join(output_dir, filename)
# Create directories if they don't exist
directory = os.path.dirname(full_path)
@@ -2397,9 +2408,18 @@ class SmartSaveAnimatedPNG:
return (full_path,)
def save_apng(self, images, fps, filename_prefix="ComfyUI", lossless=True, save_metadata=True, format="APNG", prompt=None, extra_pnginfo=None):
full_output_folder, filename, counter, subfolder, filename_prefix_ = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
# Extract the base filename part from the potentially path-like filename_prefix_
base_filename = os.path.basename(filename_prefix_)
abs_prefix = os.path.abspath(filename_prefix)
is_absolute = os.path.isabs(filename_prefix) or (len(filename_prefix) >= 2 and filename_prefix[1] == ':')
cross_drive = is_absolute and os.path.splitdrive(abs_prefix)[0].lower() != os.path.splitdrive(self.output_dir)[0].lower()
if cross_drive:
full_output_folder = os.path.dirname(abs_prefix)
base_filename = os.path.basename(abs_prefix)
subfolder = ""
os.makedirs(full_output_folder, exist_ok=True)
else:
full_output_folder, filename, counter, subfolder, filename_prefix_ = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
base_filename = os.path.basename(filename_prefix_)
# Use a single filename, not numbered sequence
if format == "APNG":
ext = "apng"
@@ -2414,7 +2434,10 @@ class SmartSaveAnimatedPNG:
if format == "GIF":
results = list()
try:
relative_filename = os.path.join(subfolder, file) if subfolder else file
if cross_drive:
relative_filename = file_path
else:
relative_filename = os.path.join(subfolder, file) if subfolder else file
self.save_animated_gif(
images=images,
filename=relative_filename,
@@ -2726,11 +2749,11 @@ class SmartImagePaletteCreate:
return {
"required": {
"colors": ("INT", {"default": 5, "min": 1, "max": 5, "step": 1}),
"color_1": ("COLOR", {"default": "#000000"}), # Black
"color_2": ("COLOR", {"default": "#000000"}), # Black
"color_3": ("COLOR", {"default": "#000000"}), # Black
"color_4": ("COLOR", {"default": "#000000"}), # Black
"color_5": ("COLOR", {"default": "#000000"}), # Black
"color_1": ("SMART_COLOR", {"default": "#000000"}),
"color_2": ("SMART_COLOR", {"default": "#000000"}),
"color_3": ("SMART_COLOR", {"default": "#000000"}),
"color_4": ("SMART_COLOR", {"default": "#000000"}),
"color_5": ("SMART_COLOR", {"default": "#000000"}),
}
}