ComfyUI 自定义节点开发:从零扩展你的图像生成工作流

写在前面

用 ComfyUI 的人应该都有过这种体验------工作流搭到一半,发现"这个操作为什么没有现成的节点?"

比如生成完图片想自动加水印,或者统一 resize 到某个尺寸再保存。标准节点库里确实没有,等别人做又不一定等到。最直接的办法是自己写一个。

这篇文章会带你从零写一个 ComfyUI 自定义节点,功能是「给生成的图片自动加上半透明文字水印」。功能不复杂,但覆盖了自定义节点开发的全部基础------输入输出定义、图像数据转换、节点注册。读完就能上手改。

自定义节点的原理

ComfyUI 启动时会扫描 custom_nodes/ 目录,自动加载其中的 Python 模块。你只要按约定定义好类,它就会出现在节点菜单里。

核心就三步:

  1. 定义输入参数和输出类型
  2. 实现处理逻辑
  3. 注册节点名称和分类

UI 部分完全不用管,ComfyUI 会根据你的定义自动生成参数面板和连接点。

环境说明

  • ComfyUI(最新 portable 版)
  • Python 3.10+
  • Pillow(通常已经装了)

如果你用 ComfyUI Manager 装过一些社区节点,环境就满足要求。

目录结构

markdown 复制代码
ComfyUI/
└── custom_nodes/
    └── comfyui-watermark-node/
        ├── __init__.py
        └── watermark_node.py

两个文件就够了。__init__.py 负责注册,watermark_node.py 写逻辑。

动手实现

1. 定义节点类

先创建 watermark_node.py

python 复制代码
import torch
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import os

class WatermarkNode:
    """
    给生成图片添加半透明文字水印的节点
    """

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image": ("IMAGE",),
                "text": ("STRING", {
                    "default": "Generated by AI",
                    "multiline": False,
                }),
                "opacity": ("FLOAT", {
                    "default": 0.3,
                    "min": 0.0,
                    "max": 1.0,
                    "step": 0.05,
                }),
                "font_size": ("INT", {
                    "default": 36,
                    "min": 8,
                    "max": 200,
                    "step": 1,
                }),
                "position": (["bottom-right", "bottom-left", "top-right", "top-left", "center"],),
            },
        }

    RETURN_TYPES = ("IMAGE",)
    FUNCTION = "add_watermark"
    CATEGORY = "image/watermark"

INPUT_TYPES 里的 "required" 字典定义了节点面板上会出现哪些参数。"IMAGE" 是 ComfyUI 内置的数据类型,用于节点间传递图像张量。字符串、浮点数、整数、下拉选择框------这些 UI 控件都是自动生成的,不需要写任何前端代码。

RETURN_TYPES 是输出类型,这里只输出一张图。FUNCTION 指定被执行的方法名。CATEGORY 控制节点在右键菜单里出现在哪个分类下。

2. 图像格式转换

这里容易踩坑。ComfyUI 里的图像张量 shape 是 (batch, height, width, channels),值是 0.0~1.0 的 float32。而 Pillow 期望的是 0~255 的 uint8。两边需要互转:

python 复制代码
    def tensor_to_pil(self, img_tensor):
        """ComfyUI 张量 -> Pillow Image(取 batch 中第一张)"""
        img = img_tensor[0].cpu().numpy()
        img = (img * 255).astype(np.uint8)
        return Image.fromarray(img)

    def pil_to_tensor(self, pil_img):
        """Pillow Image -> ComfyUI 张量"""
        img = np.array(pil_img).astype(np.float32) / 255.0
        img = torch.from_numpy(img).unsqueeze(0)
        return img

注意 unsqueeze(0) 这一步------不加 batch 维度的话,下游节点的 shape 校验会报错,而且错误信息通常不怎么友好。

3. 水印绘制逻辑

用 Pillow 的 ImageDraw 画文字:

python 复制代码
    def add_watermark(self, image, text, opacity, font_size, position):
        # 张量 -> PIL
        pil_img = self.tensor_to_pil(image)
        img_w, img_h = pil_img.size

        # 转 RGBA 以支持透明合成
        if pil_img.mode != "RGBA":
            pil_img = pil_img.convert("RGBA")

        # 创建透明覆盖层
        overlay = Image.new("RGBA", pil_img.size, (0, 0, 0, 0))
        draw = ImageDraw.Draw(overlay)

        font = self._get_font(font_size)

        # 计算文字尺寸
        bbox = draw.textbbox((0, 0), text, font=font)
        text_w = bbox[2] - bbox[0]
        text_h = bbox[3] - bbox[1]

        margin = 20

        # 定位
        if position == "bottom-right":
            x = img_w - text_w - margin
            y = img_h - text_h - margin
        elif position == "bottom-left":
            x = margin
            y = img_h - text_h - margin
        elif position == "top-right":
            x = img_w - text_w - margin
            y = margin
        elif position == "top-left":
            x = margin
            y = margin
        else:  # center
            x = (img_w - text_w) // 2
            y = (img_h - text_h) // 2

        # 半透明白色文字
        alpha = int(255 * opacity)
        draw.text((x, y), text, font=font, fill=(255, 255, 255, alpha))

        # 合成并转回 RGB
        pil_img = Image.alpha_composite(pil_img, overlay)
        pil_img = pil_img.convert("RGB")

        return (self.pil_to_tensor(pil_img),)

    def _get_font(self, size):
        """获取字体,找不到就用 Pillow 默认字体"""
        font_paths = [
            "/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc",  # macOS
            "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",   # Linux
            "C:\\Windows\\Fonts\\msgothic.ttc",                   # Windows
        ]
        for path in font_paths:
            if os.path.exists(path):
                return ImageFont.truetype(path, size)
        return ImageFont.load_default()

字体路径按主流 OS 做了 fallback。如果要求稳,把字体文件直接放在节点目录里打包分发是最靠谱的做法。

4. 注册节点

__init__.py 负责把类暴露给 ComfyUI:

python 复制代码
# custom_nodes/comfyui-watermark-node/__init__.py
from .watermark_node import WatermarkNode

NODE_CLASS_MAPPINGS = {
    "WatermarkNode": WatermarkNode,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "WatermarkNode": "Add Watermark",
}

NODE_CLASS_MAPPINGS 是必须的,没有这个字典 ComfyUI 不会识别节点。NODE_DISPLAY_NAME_MAPPINGS 是可选的,控制菜单里的显示名,不写就用类名。

测试

  1. 把两个文件放到 custom_nodes/comfyui-watermark-node/
  2. 重启 ComfyUI
  3. 右键节点列表 → image/watermarkAdd Watermark
  4. 连在 Save Image 节点前面试试

参数说明:

  • text:水印文字(中文也可以,前提是系统有对应字体)
  • opacity:透明度,0.2~0.3 比较自然
  • font_size:按图片分辨率调整
  • position:默认右下角

踩坑备忘

节点列表里找不到__init__.py 里的 NODE_CLASS_MAPPINGS 键名有没有写错。这个 typo 概率最高。

输出图片全黑 → 张量的值域不对。确认是 0.0~1.0 的 float32。

中文显示成方块_get_font() 用的字体不支持中文。改字体路径,或者把字体文件打包进节点目录。

进阶技巧

常用输入类型速查

类型 说明 写法
IMAGE 图像张量 "image": ("IMAGE",)
LATENT 潜空间张量 "latent": ("LATENT",)
STRING 文本输入 "prompt": ("STRING", {"multiline": True})
FLOAT 浮点数滑块 "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0})
INT 整数 "steps": ("INT", {"default": 20, "min": 1, "max": 100})

可选参数

把参数放在 "optional" 而不是 "required" 里,输入端口就变成可选的了:

python 复制代码
"optional": {
    "mask": ("MASK",),
},

多输出

返回元组就行:

python 复制代码
RETURN_TYPES = ("IMAGE", "INT",)

def add_watermark(self, image, text, opacity, font_size, position):
    # ...
    return (output_tensor, len(text))

调试

print() 的输出会出现在启动 ComfyUI 的终端窗口里。大多数情况下够用了。复杂场景可以上 logging 模块。

收尾

ComfyUI 自定义节点的门槛其实不高。核心记住三点:

  • INPUT_TYPES 管参数面板,FUNCTION 管执行逻辑
  • 张量 ↔ PIL 转换时注意值域和 shape
  • 别忘了在 NODE_CLASS_MAPPINGS 里注册

这次做的水印节点是个起点。同样的模式可以扩展出很多实用功能------自动 resize、嵌入元数据、多图拼接成网格,思路都一样。

工欲善其事,不如自己造轮子。

相关推荐
互联网中的一颗神经元2 小时前
小白python入门 - 25. SQL 与表设计入门
数据库·python·sql
RSTJ_16253 小时前
PYTHON+AI LLM DAY ONE HUNDRED AND EIGHTEEN
python
开源量化GO3 小时前
近期量化学习路径,交易理解和技术实现要接上
人工智能·python
tkevinjd4 小时前
MiniCode 项目详解7:Memory 记忆系统
大数据·python·搜索引擎·llm·agent
郭老二5 小时前
【Python】Web框架 FastAPI 详解
python·fastapi
DFT计算杂谈5 小时前
交错磁研究进展材料物性与交叉应用
数据库·人工智能·python·opencv·算法
敖行客 Allthinker5 小时前
docker容器安装Python反推镜像步骤(适用于临时调试用)
python·docker·容器
rrrjqy5 小时前
夏普比率与 Beta——Quant-for-Beginners 量化入门Task7
python·金融
min(a,b)7 小时前
学习第 6 天:类型注解、装饰器与高级特性
python·学习