commit 72fe5549654650ee1037d51fb02eafbb3bb4750b Author: overcloud81 Date: Tue Mar 17 22:48:40 2026 +0800 Initial commit diff --git a/ComfyUI-ColorConsistencyHSL.py b/ComfyUI-ColorConsistencyHSL.py new file mode 100644 index 0000000..01356e8 --- /dev/null +++ b/ComfyUI-ColorConsistencyHSL.py @@ -0,0 +1,442 @@ +# Version: 5.2 (Final) - Bilingual UI & Comments +# Author: overcloud81(@github @modelscope) with deepseek +# Copyright (c) 2025 overcloud81. All rights reserved. +# 功能:支持亮度(L)、色相(H)、饱和度(S)独立匹配,新增“色彩精确”模式(色相/饱和度像素级精确,亮度不变) +# 所有参数已设为合理默认值,开箱即用 +# Features: Independent matching of Luminance (L), Hue (H), Saturation (S). New "Color Exact" mode (pixel-perfect hue/saturation, luminance unchanged). +# All parameters have sensible defaults for out-of-the-box use. + +import torch +import numpy as np +from scipy.ndimage import gaussian_filter + +class ColorConsistencyHSLAdvanced: + """ + HSL-based color consistency node. + Supports independent matching of Luminance, Hue, Saturation. + Four anchor modes: statistical match, pixel-perfect, luminance stat + color exact, color exact. + Bilingual UI (Chinese/English) with automatic adaptation based on ComfyUI language setting. + """ + @classmethod + def INPUT_TYPES(cls): + # 定义多语言选项字典 / Define bilingual option dictionaries + mode_options = { + "亮度": "luminance", + "色相": "hue", + "饱和度": "saturation", + "亮度+色相": "luminance+hue", + "亮度+饱和度": "luminance+saturation", + "色相+饱和度": "hue+saturation", + "亮度+色相+饱和度": "luminance+hue+saturation" + } + anchor_mode_options = { + "统计匹配": "statistical match", + "像素级精确": "pixel-perfect", + "亮度统计+色彩精确": "luminance stat + color exact", + "色彩精确": "color exact" + } + luma_space_options = { + "LAB L通道": "LAB L channel", + "线性RGB亮度": "linear RGB luminance" + } + interpolation_options = { + "bilinear": "bilinear", + "nearest": "nearest" + } + + return { + "required": { + "reference": ("IMAGE",), + "target": ("IMAGE",), + "mode": (mode_options, {"default": "亮度+色相+饱和度"}), + "anchor_mode": (anchor_mode_options, { + "default": "统计匹配", + "tooltip": "色彩精确:色相和饱和度像素级精确,亮度不变 / Color exact: pixel-perfect hue/saturation, luminance unchanged" + }), + "luma_space": (luma_space_options, { + "default": "LAB L通道", + "tooltip": "亮度统计匹配的空间(仅当亮度参与匹配时有效) / Space for luminance statistical matching (only when luminance is matched)" + }), + "align_corners": ("BOOLEAN", {"default": True, "tooltip": "对齐四角,True可减少像素偏移 / Align corners, True reduces pixel shift"}), + "interpolation": (interpolation_options, {"default": "bilinear", "tooltip": "缩放插值方式 / Interpolation method for scaling"}), + "force_match_size": ("BOOLEAN", {"default": False, "tooltip": "强制尺寸一致(不一致时自动缩放) / Force size match (auto-scale if different)"}), + "luma_strength": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "display": "slider", + "tooltip": "亮度统计匹配强度(仅亮度参与统计匹配时有效) / Strength of luminance statistical matching (only when luminance is matched statistically)" + }), + "strength": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "display": "slider", + "tooltip": "整图混合强度 / Global blend strength" + }), + "protect_strength": ("FLOAT", { + "default": 0.0, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "display": "slider", + "tooltip": "编辑区域保留原图的比例 / Proportion of original image retained in edit area" + }), + "feather_radius": ("INT", { + "default": 10, + "min": 0, + "max": 100, + "step": 1, + "display": "slider", + "tooltip": "蒙版羽化半径(像素) / Mask feather radius (pixels)" + }), + "auto_mask": ("BOOLEAN", {"default": False, "tooltip": "启用自动蒙版 / Enable auto mask"}), + "mask_threshold": ("FLOAT", { + "default": 0.1, + "min": 0.0, + "max": 1.0, + "step": 0.01, + "display": "slider", + "tooltip": "自动蒙版阈值 / Auto mask threshold" + }), + }, + "optional": { + "external_mask": ("MASK",), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "blend" + CATEGORY = "image/postprocessing" + + def blend(self, reference, target, mode, anchor_mode, luma_space, align_corners, interpolation, force_match_size, + luma_strength, strength, protect_strength, feather_radius, auto_mask, mask_threshold, external_mask=None): + # 参数有效性验证 / Parameter validation + valid_interpolations = ["bilinear", "nearest"] + if interpolation not in valid_interpolations: + print(f"警告: interpolation '{interpolation}' 无效,使用默认 'bilinear' / Warning: interpolation '{interpolation}' invalid, using default 'bilinear'") + interpolation = "bilinear" + + # 安全裁剪数值参数 / Safely clip numeric parameters + luma_strength = np.clip(luma_strength, 0.0, 1.0) + protect_strength = np.clip(protect_strength, 0.0, 1.0) + strength = np.clip(strength, 0.0, 1.0) + feather_radius = max(0, int(feather_radius)) + mask_threshold = np.clip(mask_threshold, 0.0, 1.0) + + # 将中文模式映射到通道索引列表 / Map Chinese mode to channel indices + mode_map = { + "亮度": [0], + "色相": [1], + "饱和度": [2], + "亮度+色相": [0, 1], + "亮度+饱和度": [0, 2], + "色相+饱和度": [1, 2], + "亮度+色相+饱和度": [0, 1, 2], + } + channels_to_match = mode_map.get(mode, [0, 1, 2]) # 默认全通道 / default all channels + + # 转换为 (B,H,W,C) float32 0-1 / Convert to (B,H,W,C) float32 0-1 + ref_np = self._prepare_image(reference) + tgt_np = self._prepare_image(target) + + # 批次兼容 / Batch compatibility + if ref_np.shape[0] == 1 and tgt_np.shape[0] > 1: + ref_np = np.repeat(ref_np, tgt_np.shape[0], axis=0) + elif tgt_np.shape[0] == 1 and ref_np.shape[0] > 1: + tgt_np = np.repeat(tgt_np, ref_np.shape[0], axis=0) + elif ref_np.shape[0] != tgt_np.shape[0]: + raise ValueError(f"批次不匹配: ref {ref_np.shape[0]}, tgt {tgt_np.shape[0]} / Batch mismatch: ref {ref_np.shape[0]}, tgt {tgt_np.shape[0]}") + + # 尺寸检查与处理 / Size check and handling + if ref_np.shape[1:3] != tgt_np.shape[1:3]: + if force_match_size: + print(f"警告: force_match_size=True 但尺寸不一致,将自动缩放参考图以匹配目标图。 / Warning: force_match_size=True but sizes differ, auto-scaling reference to match target.") + ref_tensor = torch.from_numpy(ref_np).permute(0, 3, 1, 2) + mode_map_interp = {"bilinear": "bilinear", "nearest": "nearest"} + ref_tensor = torch.nn.functional.interpolate(ref_tensor, size=tgt_np.shape[1:3], mode=mode_map_interp[interpolation], align_corners=align_corners if interpolation == "bilinear" else None) + ref_np = ref_tensor.permute(0, 2, 3, 1).numpy() + + # 获取原始蒙版(白色=编辑区域,黑色=稳定区域) / Get raw mask (white=edit area, black=stable area) + raw_mask = self._get_mask(ref_np, tgt_np, auto_mask, mask_threshold, external_mask) + + # 羽化蒙版 / Feather mask + mask = self._feather_mask(raw_mask, feather_radius) if raw_mask is not None else None + + # 色相/饱和度处理始终在 LAB 空间进行,亮度处理根据 luma_space 选择 + # Hue/saturation always processed in LAB space, luminance processing depends on luma_space + ref_lab = self._rgb_to_lab(ref_np) + tgt_lab = self._rgb_to_lab(tgt_np) + + # 将 a,b 转换为极坐标:色相 H (角度) 和 饱和度 S (模长) + # Convert a,b to polar: Hue H (angle) and Saturation S (magnitude) + ref_H, ref_S = self._cartesian_to_polar(ref_lab[..., 1], ref_lab[..., 2]) + tgt_H, tgt_S = self._cartesian_to_polar(tgt_lab[..., 1], tgt_lab[..., 2]) + + # 构建 HSL 堆叠:L, H, S(L 可能来自 LAB 或 线性亮度) + # Build HSL stack: L, H, S (L may come from LAB or linear luminance) + if luma_space == "LAB L通道": + ref_luma = ref_lab[..., 0] + tgt_luma = tgt_lab[..., 0] + else: # 线性RGB亮度 / linear RGB luminance + ref_lin = self._srgb_to_linear(ref_np) + tgt_lin = self._srgb_to_linear(tgt_np) + ref_luma = 0.2126 * ref_lin[..., 0] + 0.7152 * ref_lin[..., 1] + 0.0722 * ref_lin[..., 2] + tgt_luma = 0.2126 * tgt_lin[..., 0] + 0.7152 * tgt_lin[..., 1] + 0.0722 * tgt_lin[..., 2] + + ref_hsl = np.stack([ref_luma, ref_H, ref_S], axis=-1) + tgt_hsl = np.stack([tgt_luma, tgt_H, tgt_S], axis=-1) + + # 根据锚定模式生成匹配结果 / Generate matching result based on anchor mode + matched_hsl = tgt_hsl.copy() + + if anchor_mode == "统计匹配": + matched_hsl = self._match_channels_hsl(ref_hsl, tgt_hsl, mask, channels_to_match, luma_strength) + elif anchor_mode == "像素级精确": + for ch in channels_to_match: + matched_hsl[..., ch] = ref_hsl[..., ch] + elif anchor_mode == "亮度统计+色彩精确": + # 亮度统计匹配,色相/饱和度精确 / luminance statistical match, hue/saturation exact + for ch in channels_to_match: + if ch == 0: # 亮度 / luminance + if mask is None: + ref_mean = ref_hsl[..., ch].mean(axis=(1,2), keepdims=True) + ref_std = ref_hsl[..., ch].std(axis=(1,2), keepdims=True) + tgt_mean = tgt_hsl[..., ch].mean(axis=(1,2), keepdims=True) + tgt_std = tgt_hsl[..., ch].std(axis=(1,2), keepdims=True) + adjusted = (tgt_hsl[..., ch] - tgt_mean.squeeze()) / (tgt_std.squeeze() + 1e-8) * ref_std.squeeze() + ref_mean.squeeze() + matched_hsl[..., ch] = luma_strength * adjusted + (1 - luma_strength) * tgt_hsl[..., ch] + else: + for i in range(ref_hsl.shape[0]): + stable = mask[i] < 0.5 + if np.sum(stable) == 0: + stable = np.ones_like(mask[i], dtype=bool) + ref_vals = ref_hsl[i, ..., ch][stable] + tgt_vals = tgt_hsl[i, ..., ch][stable] + ref_mean = ref_vals.mean() + ref_std = ref_vals.std() + tgt_mean = tgt_vals.mean() + tgt_std = tgt_vals.std() + adjusted = (tgt_hsl[i, ..., ch] - tgt_mean) / (tgt_std + 1e-8) * ref_std + ref_mean + matched_hsl[i, ..., ch] = luma_strength * adjusted + (1 - luma_strength) * tgt_hsl[i, ..., ch] + else: # 色相或饱和度 / hue or saturation + matched_hsl[..., ch] = ref_hsl[..., ch] + else: # "色彩精确" / "color exact" + # 只对色相(1)和饱和度(2)进行像素级精确替换 / pixel-perfect replacement only for hue(1) and saturation(2) + for ch in [1, 2]: + if ch in channels_to_match: + matched_hsl[..., ch] = ref_hsl[..., ch] + # 亮度(0)保持原样 / luminance(0) unchanged + + # 从匹配后的 HSL 分离出 L、H、S / Extract L, H, S from matched HSL + matched_luma = matched_hsl[..., 0] + matched_H = matched_hsl[..., 1] + matched_S = matched_hsl[..., 2] + + # 将极坐标转换回 a,b / Convert polar back to a,b + matched_a, matched_b = self._polar_to_cartesian(matched_H, matched_S) + + # 根据 luma_space 重建最终图像 / Reconstruct final image based on luma_space + if luma_space == "LAB L通道": + matched_lab = np.stack([matched_luma, matched_a, matched_b], axis=-1) + matched_rgb = self._lab_to_rgb(matched_lab) + else: # 线性RGB亮度 / linear RGB luminance + tgt_lin = self._srgb_to_linear(tgt_np) + eps = 1e-8 + scale = matched_luma / (tgt_luma + eps) + scale = scale[..., np.newaxis] + matched_lin = tgt_lin * scale + matched_rgb = self._linear_to_srgb(matched_lin) + + # 合成最终结果:背景区域完全使用 matched_rgb,编辑区域根据 protect_strength 混合 matched_rgb 和原图 + # Composite final result: background uses matched_rgb fully, edit area blends matched_rgb and original based on protect_strength + if mask is None: + final_rgb = matched_rgb + else: + mask_exp = mask[..., np.newaxis] + edit_blend = protect_strength * tgt_np + (1 - protect_strength) * matched_rgb + final_rgb = (1 - mask_exp) * matched_rgb + mask_exp * edit_blend + + # 强度混合(整图混合) / Strength blend (global) + blended_rgb = strength * final_rgb + (1 - strength) * tgt_np + blended_rgb = np.clip(blended_rgb, 0.0, 1.0) + + out_tensor = torch.from_numpy(blended_rgb).to(reference.device) + return (out_tensor,) + + # ---------- 辅助函数 / Helper functions ---------- + def _prepare_image(self, img): + """将输入图像统一为 (B,H,W,C) float32 0-1 格式 / Normalize input image to (B,H,W,C) float32 0-1""" + if isinstance(img, torch.Tensor): + img = img.cpu().numpy() + img = img.astype(np.float32) + + if img.ndim == 4 and img.shape[1] in (1, 3, 4) and img.shape[3] not in (1, 3, 4): + img = np.transpose(img, (0, 2, 3, 1)) + elif img.ndim == 3: + img = img[np.newaxis, ...] + + if img.max() > 1.0: + img = img / 255.0 + return img + + def _get_mask(self, ref, tgt, auto_mask, threshold, external_mask): + """获取蒙版:白色=编辑区域,黑色=稳定区域 / Get mask: white=edit area, black=stable area""" + if external_mask is not None: + mask = self._prepare_mask(external_mask, tgt.shape[1:3]) + return mask + elif auto_mask: + diff = np.mean(np.abs(ref - tgt), axis=-1) + mask = (diff > threshold).astype(np.float32) + return mask + else: + return None + + def _prepare_mask(self, mask, target_size): + """将蒙版转为 (B,H,W) float32 0-1 并调整尺寸 / Convert mask to (B,H,W) float32 0-1 and resize""" + if isinstance(mask, torch.Tensor): + mask = mask.cpu().numpy() + mask = mask.astype(np.float32) + + if mask.ndim == 4: + if mask.shape[1] == 1: + mask = mask[:, 0, :, :] + elif mask.shape[-1] == 1: + mask = mask[..., 0] + elif mask.ndim == 2: + mask = mask[np.newaxis, ...] + + if mask.max() > 1.0: + mask = mask / 255.0 + + if mask.shape[1:3] != target_size: + mask_tensor = torch.from_numpy(mask).unsqueeze(1) + mask_tensor = torch.nn.functional.interpolate(mask_tensor, size=target_size, mode='bilinear', align_corners=False) + mask = mask_tensor.squeeze(1).numpy() + return mask + + def _feather_mask(self, mask, radius): + """对蒙版进行高斯模糊羽化 / Gaussian blur feathering on mask""" + if radius <= 0 or mask is None: + return mask + B, H, W = mask.shape + feathered = np.zeros_like(mask) + for i in range(B): + feathered[i] = gaussian_filter(mask[i], sigma=radius, mode='reflect') + return feathered + + def _cartesian_to_polar(self, a, b): + """笛卡尔坐标 (a,b) 转极坐标 (角度, 模长) / Cartesian to polar (angle, magnitude)""" + H = np.arctan2(b, a) + S = np.sqrt(a**2 + b**2) + return H, S + + def _polar_to_cartesian(self, H, S): + """极坐标 (角度, 模长) 转笛卡尔坐标 (a,b) / Polar to Cartesian (a,b)""" + a = S * np.cos(H) + b = S * np.sin(H) + return a, b + + def _match_channels_hsl(self, ref_hsl, tgt_hsl, mask, channels, luma_strength=1.0): + """ + 对指定通道进行统计匹配(带蒙版) / Statistical matching on specified channels (with mask) + channels: list of channel indices to match (0=L, 1=H, 2=S) + """ + result = tgt_hsl.copy() + B, H, W, C = ref_hsl.shape + + if mask is None: + for ch in channels: + ref_mean = ref_hsl[..., ch].mean(axis=(1,2), keepdims=True) + ref_std = ref_hsl[..., ch].std(axis=(1,2), keepdims=True) + tgt_mean = tgt_hsl[..., ch].mean(axis=(1,2), keepdims=True) + tgt_std = tgt_hsl[..., ch].std(axis=(1,2), keepdims=True) + adjusted = (tgt_hsl[..., ch] - tgt_mean.squeeze()) / (tgt_std.squeeze() + 1e-8) * ref_std.squeeze() + ref_mean.squeeze() + if ch == 0: + result[..., ch] = luma_strength * adjusted + (1 - luma_strength) * tgt_hsl[..., ch] + else: + result[..., ch] = adjusted + else: + for i in range(B): + stable = mask[i] < 0.5 + if np.sum(stable) == 0: + stable = np.ones_like(mask[i], dtype=bool) + for ch in channels: + ref_vals = ref_hsl[i, ..., ch][stable] + tgt_vals = tgt_hsl[i, ..., ch][stable] + ref_mean = ref_vals.mean() + ref_std = ref_vals.std() + tgt_mean = tgt_vals.mean() + tgt_std = tgt_vals.std() + adjusted = (tgt_hsl[i, ..., ch] - tgt_mean) / (tgt_std + 1e-8) * ref_std + ref_mean + if ch == 0: + result[i, ..., ch] = luma_strength * adjusted + (1 - luma_strength) * tgt_hsl[i, ..., ch] + else: + result[i, ..., ch] = adjusted + return result + + # ---------- sRGB 与线性 RGB 转换 / sRGB to/from linear RGB ---------- + def _srgb_to_linear(self, rgb): + """sRGB 非线性转线性 RGB (OETF逆变换) / sRGB to linear RGB (inverse OETF)""" + rgb = np.clip(rgb, 0.0, 1.0) + linear = np.where(rgb <= 0.04045, rgb / 12.92, ((rgb + 0.055) / 1.055) ** 2.4) + return linear.astype(np.float32) + + def _linear_to_srgb(self, linear): + """线性 RGB 转 sRGB 非线性 (EOTF变换) / Linear RGB to sRGB (EOTF)""" + linear = np.clip(linear, 0.0, 1.0) + srgb = np.where(linear <= 0.0031308, linear * 12.92, 1.055 * (linear ** (1/2.4)) - 0.055) + return srgb.astype(np.float32) + + # ---------- LAB 转换 / LAB conversion ---------- + def _rgb_to_lab(self, rgb): + """RGB 转 LAB (近似公式,基于D65) / RGB to LAB (approximate, D65)""" + r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2] + # 转 XYZ / to XYZ + x = 0.412453 * r + 0.357580 * g + 0.180423 * b + y = 0.212671 * r + 0.715160 * g + 0.072169 * b + z = 0.019334 * r + 0.119193 * g + 0.950227 * b + # 归一化 D65 / normalize D65 + x /= 0.950456 + z /= 1.088754 + # 非线性变换 / nonlinear transform + eps = 0.008856 + k = 903.3 + def f(t): + return np.where(t > eps, t ** (1/3), (k * t + 16) / 116) + fx = f(x) + fy = f(y) + fz = f(z) + L = 116 * fy - 16 + a = 500 * (fx - fy) + b_lab = 200 * (fy - fz) + return np.stack([L, a, b_lab], axis=-1) + + def _lab_to_rgb(self, lab): + """LAB 转 RGB / LAB to RGB""" + L, a, b_lab = lab[..., 0], lab[..., 1], lab[..., 2] + fy = (L + 16) / 116 + fx = a / 500 + fy + fz = fy - b_lab / 200 + eps = 0.008856 + k = 903.3 + def finv(t): + return np.where(t > eps, t ** 3, (116 * t - 16) / k) + x = finv(fx) * 0.950456 + y = finv(fy) + z = finv(fz) * 1.088754 + # XYZ 转 RGB / XYZ to RGB + r = 3.240479 * x - 1.537150 * y - 0.498535 * z + g = -0.969256 * x + 1.875992 * y + 0.041556 * z + b = 0.055648 * x - 0.204043 * y + 1.057311 * z + return np.stack([r, g, b], axis=-1) + + +NODE_CLASS_MAPPINGS = { + "Color Consistency HSL Advanced": ColorConsistencyHSLAdvanced, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "Color Consistency HSL Advanced": "色彩亮度一致性混合 (HSL高级版)", +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f066cfc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 overcloud81 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..65c7b0f --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# ComfyUI-ColorConsistencyHSL + +A ComfyUI custom node for color/luminance consistency correction. +Supports independent matching of **Luminance (L)**, **Hue (H)**, and **Saturation (S)** with multiple anchor modes and mask protection. + +**Bilingual UI** (Chinese/English) – adapts automatically to your ComfyUI language setting. + +## Features +- Independent control of L, H, S +- Four anchor modes: + - Statistical match + - Pixel-perfect + - Luminance statistical + color exact + - Color exact (hue/saturation pixel-perfect, luminance unchanged) +- Luminance matching in **LAB L channel** (perceptual) or **linear RGB luminance** (physical) +- Auto mask / external mask with feathering +- Protect edited regions with adjustable strength +- Automatic size alignment with interpolation options +- Fully documented (bilingual comments in code) + +## Installation + +### Option 1: ComfyUI Manager (Recommended) +Search for "Color Consistency HSL" in ComfyUI Manager and install directly. + +### Option 2: Git Clone +```bash +cd ComfyUI/custom_nodes/ +git clone https://github.com/yourusername/ComfyUI-ColorConsistencyHSL.git \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c2783e2 --- /dev/null +++ b/__init__.py @@ -0,0 +1,12 @@ +# __init__.py +from .color_consistency_hsl_advanced import ColorConsistencyHSLAdvanced + +NODE_CLASS_MAPPINGS = { + "Color Consistency HSL Advanced": ColorConsistencyHSLAdvanced, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Color Consistency HSL Advanced": "HSL Color Consistency Advanced", +} + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aca988f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "color-consistency-hsl" +version = "5.2.0" +description = "HSL-based color consistency node for ComfyUI. Supports independent matching of Luminance, Hue, and Saturation with mask protection and bilingual UI." +readme = "README.md" +requires-python = ">=3.8" +license = { text = "MIT" } +authors = [ + { name = "overcloud81", email = "your-email@example.com" } +] +dependencies = [ + "scipy>=1.9.0" +] + +[project.urls] +Repository = "https://github.com/yourusername/ComfyUI-ColorConsistencyHSL" +"Bug Tracker" = "https://github.com/yourusername/ComfyUI-ColorConsistencyHSL/issues" + +[tool.comfy] +PublisherId = "overcloud81" +DisplayName = "色彩亮度一致性混合 (HSL高级版)" \ No newline at end of file