Skip to content

Commit

Permalink
v0.1.2
Browse files Browse the repository at this point in the history
  • Loading branch information
kk8bit committed Oct 22, 2024
1 parent e530ac7 commit 729b3bb
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 63 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# Changelog

## [0.1.2] - 2024-10-23
### Added
- 增加色彩调节节点
- 支持模拟相机曝光调节、对比度、色温、色调、饱和度调节
- 增加网红滤镜
- 支持所有滤镜一键预览


## [0.0.1] - 2024-10-19
### Added
- 实现自定义保存图像节点
- 创建自定义保存图像节点
- 支持保存为 PNG 和 JPG 格式。
- 添加 JPG 质量调整选项。
- 支持嵌入作者和版权信息。
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

KayTool is a custom node utility package developed for ComfyUI. I plan to add more features in the future.

## Custom Save Images Node:
# Custom Save Images Node:
![preview_custom_save_image_node](https://github.com/user-attachments/assets/4934de86-e723-450d-b0bb-817f23b20cff)

## Current Features:
- Choose the image format to save (PNG or JPG)
- Option to save metadata (workflow information)
- Support for two color profiles (sRGB IEC61966-2.1 and Adobe RGB (1998))
- Automatically convert images to Adobe RGB when selected, ensuring color accuracy
- Customize JPG image quality (0-100)
- Customize JPG image vquality (0-100)
- Ability to embed author and copyright information
- Automatically generate unique filenames
- Automatically create a `custom_save_images` folder in the default `output` directory to save all images
Expand All @@ -23,5 +23,5 @@ KayTool is a custom node utility package developed for ComfyUI. I plan to add mo
- When saving with **metadata** (workflow information), the format will automatically switch to **PNG**, and any **JPG** settings will be ignored.
- For smaller file sizes without noticeable quality loss, set the **JPG** quality to 40. Lower values may cause visible compression artifacts (blockiness).

## Installation and Usage:
# Installation and Usage:
Clone this project into your `ComfyUI/custom_nodes` directory, and make sure to place the `sRGB Profile.icc` and `AdobeRGB1998.icc` files in the `resources` directory.
4 changes: 2 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

这是一个为 ComfyUI 开发的自定义节点实用工具包,在未来我会陆续为它增加功能

## 自定义保存图片节点:
# 自定义保存图片节点:
![preview_custom_save_image_node](https://github.com/user-attachments/assets/4934de86-e723-450d-b0bb-817f23b20cff)

## 当前功能:
Expand All @@ -22,6 +22,6 @@
- 当选择保存"metadata“(工作流信息)时,将自动切换为"PNG"格式,并忽略"JPG"格式的设置
- 如果你需要更小容量但不损失肉眼可见的画质,可以讲"JPG"图片质量设置为 40,更低的数值将肉眼可见色块

## 安装与使用
# 安装与使用

- 将本项目克隆到你的 `ComfyUI/custom_nodes`目录下,并确保将 sRGB Profile.icc 和AdobeRGB1998.icc 文件放在 resources 目录中
13 changes: 11 additions & 2 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
from .custom_save_image import register_custom_node as register_save_node
from .nodes.custom_save_image import CustomSaveImage
from .nodes.color_adjustment import ColorAdjustment

NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = register_save_node()
NODE_CLASS_MAPPINGS = {
"Custom_Save_Image": CustomSaveImage,
"Color_Adjustment": ColorAdjustment,
}

NODE_DISPLAY_NAME_MAPPINGS = {
"Custom_Save_Image": "Custom Save Image",
"Color_Adjustment": "Color Adjustment",
}
136 changes: 136 additions & 0 deletions nodes/color_adjustment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import os
import numpy as np
from PIL import Image, ImageEnhance
import torch
import subprocess

# 安装 pilgram 库(如果尚未安装)
try:
import pilgram
except ImportError:
subprocess.check_call(['pip', 'install', 'pilgram'])

# 将 tensor 转换为 PIL 图像
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))

# 将 PIL 图像转换为 tensor
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)

# 动态获取所有 pilgram 滤镜,并为每个滤镜编号,增加一个 "None" 选项
def get_pilgram_filters():
filters = [f for f in dir(pilgram) if not f.startswith('_') and callable(getattr(pilgram, f))]
numbered_filters = [f"{i+1}_{filters[i]}" for i in range(len(filters))]
numbered_filters.insert(0, "None")
return numbered_filters

class ColorAdjustment:
def __init__(self):
pass

@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"exposure": ("INT", {"default": 0, "min": -100, "max": 100}),
"contrast": ("INT", {"default": 0, "min": -100, "max": 100}),
"temperature": ("INT", {"default": 0, "min": -100, "max": 100}), # 色温
"tint": ("INT", {"default": 0, "min": -100, "max": 100}),
"saturation": ("INT", {"default": 0, "min": -100, "max": 100}),
"style": (get_pilgram_filters(),),
},
"optional": {
"All": ("BOOLEAN", {"default": False}),
},
}

RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_filter"
CATEGORY = "KayTool"

def apply_filter(self, image, exposure=0, contrast=0, temperature=0, tint=0, saturation=0, style="None", All=False):
if All:
tensors = []
for img in image:
pil_img = tensor2pil(img)
pil_img = self.adjust_exposure_contrast_color(pil_img, exposure, contrast, temperature, tint, saturation)

for filter_name in get_pilgram_filters():
if filter_name == "None":
continue
filter_name_clean = filter_name.split('_', 1)[1]
filter_func = getattr(pilgram, filter_name_clean)
filtered_image = pil2tensor(filter_func(pil_img))
tensors.append(filtered_image)

tensors = torch.cat(tensors, dim=0)
return (tensors,)
else:
tensors = []
for img in image:
pil_img = tensor2pil(img)
pil_img = self.adjust_exposure_contrast_color(pil_img, exposure, contrast, temperature, tint, saturation)

if style != "None":
filter_name_clean = style.split('_', 1)[1]
filter_func = getattr(pilgram, filter_name_clean)
pil_img = filter_func(pil_img)

tensors.append(pil2tensor(pil_img))

tensors = torch.cat(tensors, dim=0)
return (tensors,)

# 调整曝光、对比度、色温、色调和饱和度的方法
def adjust_exposure_contrast_color(self, img, exposure, contrast, temperature, tint, saturation):
if exposure == 0 and contrast == 0 and temperature == 0 and tint == 0 and saturation == 0:
return img

if exposure != 0:
exposure_factor = 1 + (exposure / 100.0)
enhancer = ImageEnhance.Brightness(img)
img = enhancer.enhance(exposure_factor)

if contrast != 0:
contrast_factor = 1 + (contrast / 100.0)
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(contrast_factor)

if temperature != 0:
img = self.adjust_temperature(img, temperature)

if tint != 0:
img = self.adjust_tint(img, tint)

if saturation != 0:
saturation_factor = 1 + (saturation / 100.0)
enhancer = ImageEnhance.Color(img)
img = enhancer.enhance(saturation_factor)

return img

# 色温调整函数(负值增加蓝色,正值增加黄色)
def adjust_temperature(self, img, temperature):
img_array = np.array(img)
if temperature > 0:
img_array[:, :, 2] = np.clip(img_array[:, :, 2] * (1 - temperature / 100.0), 0, 255)
img_array[:, :, 0] = np.clip(img_array[:, :, 0] * (1 + temperature / 100.0), 0, 255)
else:
img_array[:, :, 2] = np.clip(img_array[:, :, 2] * (1 + abs(temperature) / 100.0), 0, 255)
img_array[:, :, 0] = np.clip(img_array[:, :, 0] * (1 - abs(temperature) / 100.0), 0, 255)
return Image.fromarray(img_array)

# 色调调整函数
def adjust_tint(self, img, tint):
img_array = np.array(img)
if tint > 0:
img_array[:, :, 1] = np.clip(img_array[:, :, 1] * (1 - tint / 100.0), 0, 255)
img_array[:, :, 0] = np.clip(img_array[:, :, 0] * (1 + tint / 100.0), 0, 255)
img_array[:, :, 2] = np.clip(img_array[:, :, 2] * (1 + tint / 100.0), 0, 255)
else:
img_array[:, :, 1] = np.clip(img_array[:, :, 1] * (1 + abs(tint) / 100.0), 0, 255)
img_array[:, :, 0] = np.clip(img_array[:, :, 0] * (1 - abs(tint) / 100.0), 0, 255)
img_array[:, :, 2] = np.clip(img_array[:, :, 2] * (1 - abs(tint) / 100.0), 0, 255)
return Image.fromarray(img_array)
68 changes: 15 additions & 53 deletions custom_save_image.py → nodes/custom_save_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,154 +3,116 @@
import numpy as np
from PIL import Image
from PIL.PngImagePlugin import PngInfo
from PIL import ExifTags, TiffImagePlugin, ImageCms
from PIL import ImageCms

class CustomSaveImage:
@classmethod
def INPUT_TYPES(s):
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "Custom_Save_Image"}),
"save_metadata": ("BOOLEAN", {"default": True}),
"color_profile": (["sRGB IEC61966-2.1", "Adobe RGB (1998)"], {"default": "sRGB IEC61966-2.1"}), # 将 color_profile 移到 save_metadata 下面
"color_profile": (["sRGB IEC61966-2.1", "Adobe RGB (1998)"], {"default": "sRGB IEC61966-2.1"}),
"format": (["PNG", "JPG"], {"default": "PNG"}),
"jpg_quality": ("INT", {"default": 95, "min": 0, "max": 100}),
"author": ("STRING", {"default": ""}),
"copyright_info": ("STRING", {"default": ""})
"copyright_info": ("STRING", {"default": ""}),
"save_metadata": ("BOOLEAN", {"default": False}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}

RETURN_TYPES = ()
FUNCTION = "save_images"

OUTPUT_NODE = True

CATEGORY = "KayTool"

def save_images(self, images, filename_prefix="Custom_Save_Image", save_metadata=True, format="PNG", jpg_quality=95, author="", copyright_info="", color_profile="sRGB IEC61966-2.1", prompt=None, extra_pnginfo=None):
def save_images(self, images, filename_prefix="Custom_Save_Image", save_metadata=True, format="PNG", jpg_quality=95,
author="", copyright_info="", color_profile="sRGB IEC61966-2.1", prompt=None, extra_pnginfo=None):
output_dir = self.get_output_directory()
os.makedirs(output_dir, exist_ok=True) # 确保目录存在
os.makedirs(output_dir, exist_ok=True)

results = [] # 存储图片保存路径以供UI预览使用

# 动态获取 sRGB 和 AdobeRGB 的 ICC profile 路径
srgb_profile_path = os.path.join(os.path.dirname(__file__), 'resources', 'sRGB Profile.icc')
adobergb_profile_path = os.path.join(os.path.dirname(__file__), 'resources', 'AdobeRGB1998.icc')
results = []
# 更新 ICC profile 路径
srgb_profile_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'sRGB Profile.icc')
adobergb_profile_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'AdobeRGB1998.icc')

for image in images:
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))

# 处理颜色配置文件
if color_profile == "Adobe RGB (1998)":
img = self.convert_to_adobe_rgb(img, srgb_profile_path, adobergb_profile_path)
icc_profile_path = adobergb_profile_path
else:
icc_profile_path = srgb_profile_path

# 加载选择的 ICC profile
icc_profile = self.load_icc_profile(icc_profile_path)

# 如果 save_metadata 为 True,默认保存为 PNG
if save_metadata:
metadata = PngInfo()

# 无论 save_metadata,始终嵌入作者和版权信息
if author:
metadata.add_text("Author", author)
if copyright_info:
metadata.add_text("Copyright", copyright_info)

# 嵌入额外元数据
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for k, v in extra_pnginfo.items():
metadata.add_text(k, json.dumps(v))

# 保存为 PNG 格式,嵌入所选颜色配置文件
filename = f"{filename_prefix}_{self.get_unique_filename()}.png"
full_output_path = os.path.join(output_dir, filename)
img.save(full_output_path, pnginfo=metadata, icc_profile=icc_profile)

# 如果 save_metadata 为 False,保存为指定的格式
else:
if format == "PNG":
metadata = PngInfo()

# 嵌入作者和版权信息
if author:
metadata.add_text("Author", author)
if copyright_info:
metadata.add_text("Copyright", copyright_info)

# 保存为 PNG 格式
filename = f"{filename_prefix}_{self.get_unique_filename()}.png"
full_output_path = os.path.join(output_dir, filename)
img.save(full_output_path, pnginfo=metadata, icc_profile=icc_profile)

elif format == "JPG":
exif_data = img.getexif()

# 嵌入作者和版权信息
if author:
exif_data[0x013B] = author # EXIF中的作者字段(Artist)
exif_data[0x013B] = author
if copyright_info:
exif_data[0x8298] = copyright_info # EXIF中的版权字段(Copyright)

exif_data[0x8298] = copyright_info
exif_bytes = exif_data.tobytes()

# 保存为 JPG 格式,附加 EXIF 和 ICC profile
filename = f"{filename_prefix}_{self.get_unique_filename()}.jpg"
full_output_path = os.path.join(output_dir, filename)
img.save(full_output_path, quality=jpg_quality, exif=exif_bytes, icc_profile=icc_profile)

# 添加保存的图片信息用于UI预览
results.append({
"filename": filename,
"subfolder": "Custom_Save_Image",
"type": "output",
})

# 返回给UI的图片预览信息
return {"ui": {"images": results}, "status": "Images saved successfully"}

def convert_to_adobe_rgb(self, img, srgb_profile_path, adobergb_profile_path):
"""
将图像从 sRGB 转换为 Adobe RGB
"""
srgb_profile = ImageCms.getOpenProfile(srgb_profile_path)
adobergb_profile = ImageCms.getOpenProfile(adobergb_profile_path)
img = ImageCms.profileToProfile(img, srgb_profile, adobergb_profile)
return img

def load_icc_profile(self, path):
"""
加载 ICC profile,如果加载失败,抛出异常
"""
try:
with open(path, "rb") as f:
return f.read()
except FileNotFoundError:
raise Exception(f"ICC profile not found at: {path}")

def get_unique_filename(self):
"""
生成基于时间戳的唯一文件名
"""
import time
return f"{int(time.time() * 1000)}"

def get_output_directory(self):
"""
返回输出目录
"""
return os.path.join(os.getcwd(), "output", "Custom_Save_Image")

# 这个函数用于注册节点
def register_custom_node():
NODE_CLASS_MAPPINGS = {"CustomSaveImage": CustomSaveImage}
NODE_DISPLAY_NAME_MAPPINGS = {"CustomSaveImage": "Custom Save Image"}
return NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
return os.path.join(os.getcwd(), "output", "Custom_Save_Image")
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "kaytool"
description = "KayTool is a growing toolkit for ComfyUI. It includes the 'Custom Save Image' node, allowing image saving in PNG or JPG format, with support for ICC profiles like sRGB and Adobe RGB, metadata control, JPG quality adjustment."
version = "0.0.1"
description = KayTool is a growing toolkit for ComfyUI, featuring essential nodes such as ‘Custom Save Image’ and ‘Color Adjustment.’ Key features include image saving in PNG/JPG, ICC profile support (sRGB/Adobe RGB), metadata embedding, JPG quality adjustment, exposure/contrast/color temperature adjustments, tint/saturation controls, and stylish filter previews.”
version = "0.1.2"
license = {file = "LICENSE"}

[project.urls]
Expand Down

0 comments on commit 729b3bb

Please sign in to comment.