Files
MachinePainting 3a574494eb Add short DESCRIPTION text to all 41 nodes
None of the nodes showed description text in ComfyUI because ComfyUI
reads obj_class.DESCRIPTION when it builds node_info - it does not read
the class docstring, which is what these nodes had.

Every registered node now carries a one-line DESCRIPTION, kept to
29-67 characters so it fits the limited space in ComfyUI Manager and
the node search results. Image Transform Pro's four-sentence
description is shortened to match.

Descriptions are derived from each node's existing docstring, or from
the README for the five classes that had no docstring
(ImageBlendPro, ColorMatchBlend and the three Boolean switch nodes).

README also updated for the recent changes: the New Nodes table now
lists the real "Rotate & Scale" category, the XY placement direction is
documented on both nodes (positive X right, positive Y up), and the
v2.2.0 changelog records the placement convention, the node outputs and
the MP_ImageTransform internal name.
2026-09-03 03:32:00 -07:00

197 lines
7.6 KiB
Python

import torch
import numpy as np
import cv2
class SelectiveColorPro:
"""
Photoshop-style selective color adjustment.
Adjust cyan, magenta, yellow, black for specific color ranges.
Uses smooth falloff for natural-looking color adjustments.
"""
DESCRIPTION = "CMYK adjustments within specific colour ranges."
COLOR_RANGES = ["reds", "yellows", "greens", "cyans", "blues", "magentas", "whites", "neutrals", "blacks"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"target_color": (cls.COLOR_RANGES, {"default": "reds"}),
"cyan": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 1.0, "display": "slider"}),
"magenta": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 1.0, "display": "slider"}),
"yellow": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 1.0, "display": "slider"}),
"black": ("FLOAT", {"default": 0.0, "min": -100.0, "max": 100.0, "step": 1.0, "display": "slider"}),
},
"optional": {
"mask": ("MASK",),
"invert_mask": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_selective_color"
CATEGORY = "MachinePaintingNodes/Color"
def get_color_mask(self, img_rgb, target_color):
"""
Create a smooth mask for the target color range.
Uses RGB-based color detection with smooth falloff like Photoshop.
"""
r, g, b = img_rgb[:,:,0], img_rgb[:,:,1], img_rgb[:,:,2]
# Calculate color components
max_rgb = np.maximum(np.maximum(r, g), b)
min_rgb = np.minimum(np.minimum(r, g), b)
chroma = max_rgb - min_rgb + 1e-10
# Saturation (0-1)
saturation = np.where(max_rgb > 0, chroma / (max_rgb + 1e-10), 0)
# Lightness (0-1)
lightness = (max_rgb + min_rgb) / 2
if target_color == "reds":
# Red dominance with smooth falloff
red_strength = (r - np.maximum(g, b)) / (chroma + 1e-10)
red_strength = np.clip(red_strength, 0, 1)
# Also catch magentas and oranges that lean red
mask = red_strength * saturation
elif target_color == "yellows":
# Yellow = high red AND green, low blue
yellow_strength = (np.minimum(r, g) - b) / (chroma + 1e-10)
yellow_strength = np.clip(yellow_strength, 0, 1)
mask = yellow_strength * saturation
elif target_color == "greens":
# Green dominance
green_strength = (g - np.maximum(r, b)) / (chroma + 1e-10)
green_strength = np.clip(green_strength, 0, 1)
mask = green_strength * saturation
elif target_color == "cyans":
# Cyan = high green AND blue, low red
cyan_strength = (np.minimum(g, b) - r) / (chroma + 1e-10)
cyan_strength = np.clip(cyan_strength, 0, 1)
mask = cyan_strength * saturation
elif target_color == "blues":
# Blue dominance
blue_strength = (b - np.maximum(r, g)) / (chroma + 1e-10)
blue_strength = np.clip(blue_strength, 0, 1)
mask = blue_strength * saturation
elif target_color == "magentas":
# Magenta = high red AND blue, low green
magenta_strength = (np.minimum(r, b) - g) / (chroma + 1e-10)
magenta_strength = np.clip(magenta_strength, 0, 1)
mask = magenta_strength * saturation
elif target_color == "whites":
# High lightness, low saturation
white_strength = np.clip((lightness - 0.7) / 0.3, 0, 1)
sat_falloff = np.clip(1 - saturation * 3, 0, 1)
mask = white_strength * sat_falloff
elif target_color == "blacks":
# Low lightness with smooth falloff
black_strength = np.clip((0.3 - lightness) / 0.3, 0, 1)
mask = black_strength
else: # neutrals
# Low saturation, mid lightness
neutral_sat = np.clip(1 - saturation * 4, 0, 1)
neutral_light = 1 - np.abs(lightness - 0.5) * 2
neutral_light = np.clip(neutral_light, 0, 1)
mask = neutral_sat * neutral_light
# Smooth the mask slightly to avoid any harsh edges
mask = cv2.GaussianBlur(mask.astype(np.float32), (3, 3), 0)
return mask
def apply_cmyk_adjustment(self, img, color_mask, cyan, magenta, yellow, black):
"""
Apply CMYK adjustments to masked areas.
Photoshop-style: adjustments are relative to the current color values.
"""
result = img.copy()
# Convert adjustments to factors (-1 to 1)
c_adj = cyan / 100.0
m_adj = magenta / 100.0
y_adj = yellow / 100.0
k_adj = black / 100.0
# Expand mask to 3 channels
mask = color_mask[:, :, np.newaxis]
# CMYK to RGB relationship:
# Cyan reduces Red
# Magenta reduces Green
# Yellow reduces Blue
# Black reduces all (lightness)
# Apply adjustments proportionally to mask and current pixel values
# Positive cyan = reduce red, Negative cyan = add red
if c_adj != 0:
adjustment = c_adj * mask * result[:,:,0:1]
result[:,:,0:1] = result[:,:,0:1] - adjustment
if m_adj != 0:
adjustment = m_adj * mask * result[:,:,1:2]
result[:,:,1:2] = result[:,:,1:2] - adjustment
if y_adj != 0:
adjustment = y_adj * mask * result[:,:,2:3]
result[:,:,2:3] = result[:,:,2:3] - adjustment
# Black adjustment affects luminosity
if k_adj != 0:
# Positive black = darken, negative = lighten
if k_adj > 0:
# Darken: multiply by (1 - k_adj * mask)
darkness = k_adj * mask
result = result * (1 - darkness)
else:
# Lighten: move toward white
lightness = -k_adj * mask
result = result + (1 - result) * lightness
return np.clip(result, 0, 1)
def apply_selective_color(self, image, target_color, cyan, magenta, yellow, black,
mask=None, invert_mask=False):
img = image[0].cpu().numpy().astype(np.float32)
original = img.copy()
# Get smooth color mask
color_mask = self.get_color_mask(img, target_color)
# Apply CMYK adjustments
result = self.apply_cmyk_adjustment(img, color_mask, cyan, magenta, yellow, black)
# Apply external mask if provided
if mask is not None:
if len(mask.shape) == 3:
mask_np = mask[0].cpu().numpy()
else:
mask_np = mask.cpu().numpy()
if mask_np.shape[:2] != img.shape[:2]:
mask_np = cv2.resize(mask_np, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_LINEAR)
if invert_mask:
mask_np = 1.0 - mask_np
mask_3ch = mask_np[:, :, np.newaxis]
result = original * (1 - mask_3ch) + result * mask_3ch
result = np.clip(result, 0, 1)
result_tensor = torch.from_numpy(result).unsqueeze(0)
return (result_tensor,)