资讯动态

ComfyUI自定义节点:实现图像压缩加密输出

发布时间:2026/10/21 22:57:02 来源:尧图企业网站定制
最近在 ComfyUI 里整理输出流程的时候我写了一个图生压缩加密输出节点把生成好的图片统一压缩、加密再落盘。这个东西听起来小但解决了我很大的痛点工作流跑完一批图输出目录能堆几个 GB而且中间过程图全是敏感内容裸着放实在不踏实。今天就把这个节点的设计思路、完整代码、踩坑记录一起放出来最后再聊几句我对 AI 这波浪潮的个人感叹。1. 为什么需要压缩加密输出节点1.1 生成图像的存储痛点ComfyUI 默认的 Save Image 节点保存的是 PNG 格式PNG 为了无损会把像素细节原原本本留下来。单张 1024x1024 的图还好说体积通常在两三 MB 左右但一旦开了多批次、高分放大、多角度生成同一个工作流跑一次就能产出几十上百张图输出目录轻松突破几 GB。我更头疼的是工作流里往往会放多个 Save Image 节点一个为了检查中间步骤一个为了保存最终结果中间步骤的图也全堆在硬盘上删又不舍得留又占地方。这种情况下最直接的办法就是在工作流出口加一个压缩节点把最终结果统一转成体积更小的格式。压缩不是新鲜事但很多人会纠结“怕损画质”。实际上对于大多数出图场景WebP 在 quality 80 左右的视觉差异很小体积却可以降到 PNG 的十分之一甚至更低。如果你要的是“能看、能分享、能入库”压缩完全够用如果你要的是“后续还要精修、还要放大”那就保留一份 PNG 原图再压缩一份归档即可。图生图场景下这个问题更明显。输入一张参考图模型会生成多个变体这些变体本身就是中间产物不是最终成品。把它们全部原样保存很快就把磁盘塞满。我在做某跨平台系统的素材生成时批量跑了几轮风格迁移输出目录直接爆掉。后来想明白了中间变体只需要留下可预览、可筛选的轻量版本真正需要精修的少数图再单独走无损流程。1.2 加密保护的必要性如果说压缩解决的是“太大”那加密解决的是“太裸”。很多人觉得本机上的图片无所谓但实际情况是你的 ComfyUI 输出目录可能被同步工具上传到网盘、可能被某次临时分享暴露、可能在调试时被日志插件扫描。生成图的内容有时候很敏感私人照片换风格、未公开的角色设计、商用项目的概念图。文件裸奔在硬盘上等于把自己的一部分未发布资产直接晾在外面。加密输出节点做的事情很朴素在图片压缩成字节流之后用口令派生的密钥把内容加密再写成带自定义扩展名的文件。这样就算文件被别人拷走没有口令也只是一坨无法解析的随机数据。它不是绝对安全系统但足以挡住绝大多数“顺手拿走看一眼”的场景。配合文件名用时间戳或 UUID 随机化外部人员连这个文件到底存了什么都无法从命名上判断。还有一层保护容易被忽略工作流 JSON 文件里保存着所有节点参数包括你在节点面板里填的字符串。如果你打算做“密码”功能千万别直接把口令明文放进工作流。后面我会专门讲怎么用环境变量接入口令避免把密钥从后门漏出去。2. 节点功能设计与技术选型2.1 整体流程怎么设计这个节点的定位是“最终输出”而不是“中间处理”。所以它的输入是 ComfyUI 的 Image 张量输出是加密文件路径。整体流程可以拆成四步把 ComfyUI 的 Tensor 图像转换成 Pillow 的 Image 对象。按参数做压缩编码得到图片字节流。从口令或环境变量中派生 AES 密钥对字节流加密。把盐、IV、认证标签、密文打包写入文件返回文件路径。为什么不在节点里直接做“解码预览”这个功能因为职责越单一越不容易出错。加密输出节点只负责“写文件”解密恢复交给另一个节点或脚本这样工作流里可以只挂加密不强行绑定解密流程。另一个考虑是 ComfyUI 的执行顺序是线性的如果节点又把解密后的图像返回给前端内存里就多了一份大张量反而拖慢整个工作流。2.2 压缩格式与参数怎么选压缩格式我重点对比了 PNG、JPEG、WebP 三种。PNG 无损但大JPEG 兼容性最好但透明的 alpha 通道会丢WebP 在体积、画质、透明度支持上都挺均衡唯一的缺点是一些老旧软件不认。如果输出目标是网页展示、素材归档、图库管理WebP 是当前最推荐的方案如果输出物要发给设计师去 PS 里二次处理就老老实实给 PNG 或者高质量 JPEG。参数上WebP 有两个关键选项quality 控制有损压缩质量method 控制编码器努力程度。method 越高越费 CPU但体积会更小。我自己常用的组合是 quality80、method4在画质和速度之间比较平衡。JPEG 的 quality 建议设到 90 以上否则文字边缘和渐变区域会出现恼人的色块。透明通道方面如果图片带 alpha 且必须保留可以选 WebP losslessFalse 但带 alpha 参数或者直接用 PNGJPEG 会直接填黑底或白底这个坑我踩过。格式体积表现画质表现透明支持适用场景PNG最大无损支持需要二次编辑、文字截图JPEG中等有损但观感好不支持照片、无透明需求的最终图WebP q80最小肉眼难辨支持批量预览、素材归档、网页展示WebP q90较小很好支持对画质要求较高的归档场景还有一点容易被忽略尺寸缩放。如果生成的是 2048x2048但实际只需要 1024 的预览图可以在压缩前先缩一半体积会再降一个数量级。我在节点里加了一个max_size参数默认 0 表示不缩放设成 1024 就会把长边压到 1024短边等比缩放。这个参数很实用但要注意一旦缩放原图信息就真的丢了所以保留原图选项必须单独存在。2.3 加密算法与密钥管理加密我选的是 AES-GCM而不是 AES-CBC。原因很简单GCM 是 AEAD 算法加密后自动生成认证标签解密时如果密文被改动或口令错误会直接校验失败并抛异常。CBC 模式需要额外做 HMAC 才能防篡改代码复杂度高容易漏。现代密码学实践中能用 AEAD 就优先用GCM 也是 TLS 里最常见的对称加密方案。密钥不能直接用口令本身口令长度和熵通常不够要用密钥派生函数。我用 PBKDF2-HMAC-SHA256迭代次数设成 200000。这个次数在短口令场景下已经能拖慢暴力破解但又不至于让单次加密卡顿。每次加密生成随机 16 字节盐和随机 12 字节 IV然后把盐、IV、认证标签、密文按固定顺序写入文件。随机盐保证了相同明文、相同口令加密出来的结果完全不同随机 IV 防止了 GCM 重复使用 IV 的危险。密钥管理是这类节点最容易出问题的地方。如果直接把口令写在节点的字符串参数里ComfyUI 保存工作流时会把字符串写进 JSON下次加载时还能看到明文。更糟糕的是分享工作流的人不会想到自己把密码也分享出去了。所以我设计成当password参数为空时自动读取环境变量ENCRYPT_OUTPUT_PASSWORD。如果两者都为空就抛异常提示用户。这样既可以在本地快速测试也可以保证分享工作流时不会把敏感口令带出去。3. 核心代码实现与接入 ComfyUI3.1 自定义节点骨架ComfyUI 自定义节点的基本要求很低在任意custom_nodes子目录下创建一个 Python 文件定义类然后提供NODE_CLASS_MAPPINGS字典。下面是我这个节点的最简骨架。import os import hashlib from pathlib import Path import numpy as np import torch from PIL import Image from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives import hashes import folder_paths class CompressEncryptOutput: def __init__(self): self.output_dir folder_paths.get_output_directory() classmethod def INPUT_TYPES(cls): return { required: { images: (IMAGE,), password: (STRING, {default: }), format: ([webp, jpeg, png], {default: webp}), quality: (INT, {default: 80, min: 1, max: 100}), max_size: (INT, {default: 0, min: 0, max: 8192}), filename_prefix: (STRING, {default: compressed}), overwrite: (BOOLEAN, {default: False}), } } RETURN_TYPES (STRING,) RETURN_NAMES (file_path,) FUNCTION save_encrypted CATEGORY custom/security def save_encrypted(self, images, password, format, quality, max_size, filename_prefix, overwrite): # 后续实现 pass NODE_CLASS_MAPPINGS { CompressEncryptOutput: CompressEncryptOutput, }这里的folder_paths.get_output_directory()是 ComfyUI 内置的 API能拿到当前输出目录。用Path拼接路径比字符串拼接更稳尤其跨平台时不用纠结斜杠问题。format参数限制为下拉列表避免用户输入非法值。3.2 压缩、加密与写文件下面把save_encrypted完整实现。首先要处理图像张量转换这是最容易写错的地方。ComfyUI 传入的images是形状为[B, H, W, C]的 PyTorch 张量数值范围 0.0 到 1.0。Pillow 需要uint8数组而且通道顺序要从 RGB 保持一致不能多转了一次 BGR。批量处理时每个 batch 里的图像可以命名成prefix_00001.webp.enc这样的编号。def save_encrypted(self, images, password, format, quality, max_size, filename_prefix, overwrite): if not password: password os.environ.get(ENCRYPT_OUTPUT_PASSWORD, ) if not password: raise ValueError(必须提供 password 参数或设置 ENCRYPT_OUTPUT_PASSWORD 环境变量) os.makedirs(self.output_dir, exist_okTrue) saved_paths [] batch_size images.shape[0] for i in range(batch_size): img_tensor images[i].cpu() img_np img_tensor.numpy() img_np np.clip(img_np, 0.0, 1.0) img_uint8 (img_np * 255.0).astype(np.uint8) pil_image Image.fromarray(img_uint8, modeRGB) if pil_image.mode ! RGB: pil_image pil_image.convert(RGB) if max_size 0: pil_image resize_image(pil_image, max_size) image_bytes compress_to_bytes(pil_image, format, quality) encrypted_bytes, salt, iv, tag encrypt_bytes(image_bytes, password) filename f{filename_prefix}_{i:05d}.{format}.enc filepath Path(self.output_dir) / filename if filepath.exists() and not overwrite: saved_paths.append(str(filepath)) continue with open(filepath, wb) as f: f.write(salt) f.write(iv) f.write(tag) f.write(encrypted_bytes) saved_paths.append(str(filepath)) return ( .join(saved_paths),)辅助函数resize_image、compress_to_bytes、encrypt_bytes需要单写。compress_to_bytes用 Pillow 的save方法把图片存储到BytesIOdef compress_to_bytes(pil_image, fmt, quality): buffer io.BytesIO() if fmt webp: pil_image.save(buffer, formatWEBP, qualityquality, method4) elif fmt jpeg: pil_image.save(buffer, formatJPEG, qualityquality, optimizeTrue) else: pil_image.save(buffer, formatPNG, optimizeTrue) return buffer.getvalue()加密部分需要注意 PBKDF2 参数和 GCM 输入格式def encrypt_bytes(data, password): salt os.urandom(16) kdf PBKDF2HMAC( algorithmhashes.SHA256(), length32, saltsalt, iterations200000, ) key kdf.derive(password.encode(utf-8)) aesgcm AESGCM(key) iv os.urandom(12) ciphertext aesgcm.encrypt(iv, data, None) # ciphertext 尾部自动追加 16 字节认证标签 tag ciphertext[-16:] encrypted_payload ciphertext[:-16] return encrypted_payload, salt, iv, tag这里我把tag单独切出来其实AESGCM.encrypt返回的ciphertext已经包含认证标签。切出来只是为了写文件时把结构看得更清楚解密时要把四个部分重新拼回去。3.3 注册节点并接入工作流把文件保存为custom_nodes/ComfyUICompressEncrypt/__init__.py然后在 ComfyUI 根目录启动控制台应当能看到类似“ComfyUICompressEncrypt”的加载日志。如果看不到检查NODE_CLASS_MAPPINGS必须在模块顶层定义且不依赖只在if __name__ __main__里执行的代码。工作流里接入很简单把原来接向 Save Image 的换出的连线接到这个节点的images输入设置一个自己记得住的口令或者直接留空靠环境变量。节点返回的file_path是字符串可以接一个显示文本的节点也可以不接运行结束后到 ComfyUI 的 output 目录找文件。我在实际使用中会把filename_prefix设成当前项目名加日期例如character_red_20250115这样输出目录能一眼看出是哪一批图。文件名末尾的.webp.enc既告诉我是“压缩过的加密文件”也避免被普通看图软件直接打开。3.4 密钥管理的补充建议只靠环境变量还不够因为环境变量对同机所有用户可见。更稳妥的做法是让节点支持从一个只读文件中读取口令字符串文件权限设为 600。不过 ComfyUI 节点大多数时候跑在个人电脑上环境变量已经比明文工作流选项好很多。如果你要部署到多人使用的服务器建议再加一层不把加密口令给所有跑工作流的人而是把加密后的文件统一交给一个“发布账号”处理解密密钥单独保管。KDF 迭代次数不要设太低。默认 200000 次在很多机器上只要 0.1 秒到 0.2 秒一次性加密几十张图也还好。如果你嫌慢降到 10000 次那不如用弱一点的加密方案因为攻击者可以更快地尝试口令。迭代次数本质上是用你的计算成本换取攻击者的破解成本这钱值得花。还有一个容易忽略的文件头问题很多人在解密时直接对文件字节流做AESGCM.decrypt忘了先读前 44 字节的盐和 IV。我的文件布局固定为salt(16) iv(12) tag(16) ciphertext(变长)解密代码必须严格按这个长度切分。如果以后想加入“文件版本号”或“原始文件名”可以在 salt 前面再加一个版本字节但那样解密端要同步更新。我目前没做版本号因为节点自产自销格式可控。4. 实操过程与实测数据4.1 搭建测试工作流为了验证节点稳定性我在 ComfyUI 里搭了一个最小可用的图生图测试流加载一张输入图过一个简单的局部重绘采样器再经过 VAE Decode 得到最终图像最后接入CompressEncryptOutput。测试时我没有跑很长的采样步数每张图只出 512x512然后丢给节点做压缩加密。这样能快速验证节点在批量、单图、不同格式下的表现。由于节点同时涉及 PIL 压缩和 cryptography 加密建议先单独在命令行里跑一遍工具函数确认依赖装好了。ComfyUI 自带 Python 环境版本可能和你手头环境不一样如果报ModuleNotFoundError: cryptography要在 ComfyUI 所在的 Python 里执行pip install cryptography不是随便找个终端 pip 一下。测试时的参数组合我列在下面的表格里。图片本身是一张简单的风景图1024x1024原图 PNG 体积 2.6MB。注意这个数字只代表我这组测试图你的图像内容、噪点程度、颜色复杂度都会影响最终压缩率尤其是大面积平坦色和密集纹理差别很大。格式压缩参数压缩后体积加密耗时文件大小PNGoptimizeTrue2.5MB约 150ms2.5MBJPEGquality90620KB约 140ms622KBJPEGquality80380KB约 135ms382KBWebPquality90780KB约 150ms782KBWebPquality80410KB约 145ms412KBWebPquality80, max_size512120KB约 120ms122KB从这次测试能看出WebP q80 是体积和画质最平衡的档位PNG 在这个场景下几乎没占到便宜。加密耗时主要是 PBKDF2 派生密钥这个时间和图片体积关系不大主要取决于 KDF 迭代次数。实际跑批量图时加密 60 张图大概多花 9 秒完全可以接受。4.2 画质损失怎么判断压缩有损这件事不能只凭“肉眼看着没问题”来糊弄。我在测试中把压缩后的图用另一个脚本解开然后与原图逐像素算 PSNR。WebP q80 在风景图上通常能到 38dB 以上人眼很难察觉q70 可能掉到 33dB 以下树梢和草叶会出现轻微糊感。文字和 UI 截图则不同哪怕 q90边缘也偶尔会出现轻微磨损。所以我的建议是“包含小号文字、需要放大检查的图一律走 PNG”“视觉向、展示向的图放心用 WebP q80”。如果你希望节点能在压缩之前自动判断“这张图是否适合有损压缩”可行但意义不大。因为判断依据是“用途”而不是“图像内容”节点不知道你后续拿它干嘛。人工决策仍然是最可靠的我通常在预览后把需要高清保留的图单独跑一次无损分支。4.3 解密恢复与边角情况解密端我从一开始就做了独立脚本没有做成 ComfyUI 节点。原因很现实解密后的图像如果要再次回到 ComfyUI 做处理需要还原成[B,H,W,C]的 float 张量这并不难但会导致内存里同时存在加密字节流、PIL 图像、Tensor 三份数据。对小图无所谓对 4K 大图就是几十 MB 的额外开销。脚本方式更轻def decrypt_file(filepath, password): with open(filepath, rb) as f: salt f.read(16) iv f.read(12) tag f.read(16) ciphertext f.read() kdf PBKDF2HMAC(algorithmhashes.SHA256(), length32, saltsalt, iterations200000) key kdf.derive(password.encode(utf-8)) aesgcm AESGCM(key) plaintext aesgcm.decrypt(iv, ciphertext tag, None) return plaintext特别提醒加密时我把tag切出来了解密时必须把ciphertext和tag重新拼成 AESGCM 期望的完整密文。如果漏了这一步解密会报InvalidTag。这个报错在很多初学者那里会被误以为是口令错误其实只是切分格式不对。最好在代码注释里写明文件布局方便半年后自己回来看。还有一个不得不接受的现实有损压缩是不可逆的。就算你解密成功拿到也是压缩后的图片不是原始像素。所以节点我默认不删原图也不把原图路径暴露到输出目录之外。真正常用的方式是工作流出口接两个节点——一个保留 PNG 到original子目录一个压缩加密到archive子目录。前者用于后续编辑后者用于分享和长期存储。5. 常见问题与排查技巧实录5.1 加密后的文件打不开最常见的三类原因口令错误、文件头被破坏、目录不存在。先确认自己是不是用了环境变量如果环境变量里有旧口令节点里就算填了新口令也不会生效因为我代码里的优先级是参数优先、环境变量兜底。如果你既填了参数又设置了环境变量一定要清楚到底用的是哪个否则会出现“同一个文件换参数也解不开”的怪现象。文件头被破坏多发生在传输或改名过程中。有人会把.webp.enc改成.jpg再分享接收方没有还原后缀解密脚本读文件头时发现长度不足直接抛异常。解决办法很简单不要随便改扩展名接收方要严格按 salt(16)iv(12)tag(16) 的布局解析。也可以在我的代码里加一层防御读文件后先检查文件大小是否大于 44 字节小于这个长度立刻报“文件不完整”。5.2 ComfyUI 界面里找不到这个节点节点没显示一半原因是没导入成功。先看 ComfyUI 控制台后台有没有红色报错常见的是ModuleNotFoundError: cryptography。注意 ComfyUI 有自己打包的 Python特别是在桌面版里安装依赖要装到对应的解释器里。另一个隐蔽问题是NODE_CLASS_MAPPINGS名字重复如果你之前装过同名节点ComfyUI 会加载最后一个导致你的节点不生效。建议把类名和映射名都改成具有辨识度的名称比如SecureCompressOutput_V2。还有可能是文件放在了非custom_nodes目录。ComfyUI 默认只扫描custom_nodes下的子目录如果你直接放在modules或根目录是不会被加载的。检查一下文件路径是否真的在custom_nodes/ComfyUICompressEncrypt/__init__.py然后重启 ComfyUI看到日志里出现节点类才算加载成功。5.3 中文路径和文件名乱码Windows 下用Path对象而不是直接字符串拼接能减少大部分编码问题。我一开始用os.path.join加字符串文件名里带中文项目名时偶尔会出现UnicodeEncodeError。改用pathlib.Path之后稳了很多。另外一个习惯是给filename_prefix只填 ASCII 字符比如拼音缩略或日期中文信息放进一个单独的meta.json里和加密文件分开存储。加密文件的命名越不含信息越好这样别人看到这个文件也猜不出内容类型。5.4 压缩后画质变糊怎么调都糊先检查是不是max_size把图缩小了。很多人把max_size设成 512然后抱怨“压缩后噪点全糊了”其实不是压缩格式的问题是分辨率降低。只要把max_size设回 0立刻清楚。排除这个之后再看quality。WebP 的 q80 对大多数内容很安全但如果是文字截图、含有细线稿的图建议 q90 以上。如果还糊大概率是原图本身有轻微模糊生成模型的问题不是节点问题。给一个更系统的检查方法把format设成pngquality随便填因为 PNG 不适用多少 quality 参数。跑完看 PNG 是否清晰。如果 PNG 清晰而 WebP 模糊说明是压缩有损如果 PNG 也不清晰问题出在工作流或者节点前面的图像处理逻辑和压缩没关系。5.5 批量跑大量图片的性能问题加密的 200000 次 PBKDF2 在每张图开始时都会跑一遍这意味着 100 张图就要派生 100 次密钥。每张图多花 0.15 秒总计 15 秒。如果你觉得慢可以只派生一次密钥然后把同一个密钥复用到所有图片上但每张图仍然要用不同的 IV 和 salt。这种做法在性能上更好安全性也不差。我在当前版本里偷懒选了逐张派生因为代码简单、不需要维护全局密钥状态但你要在批量场景用建议改成“任务级派生一次”。批量跑的时候还要注意磁盘 IO 并发。ComfyUI 的节点执行默认是单线程连续写多个文件不会爆炸。如果你在外部脚本里并发调用多个加密任务记得控制线程池数量避免一次开几十个文件句柄。SSD 还好机械硬盘一并发写就会让最终速度慢到怀疑人生。6. 对 AI 的一些感叹6.1 工具越好用数字垃圾越多写完这个加密输出节点我反而开始想一个更大的问题AI 生成的门槛已经低到“会打字就能出图”但我身边真正高质量的应用案例并不是那些一次性生成千张图的“跑图狂魔”而是愿意在几百张候选里认真挑出三五张、再亲手修改的人。技术让批量产出变的廉价也让筛选成本变成新的瓶颈。加密和压缩本质上是在帮我们管理 AI 产生的海量数字资产但资产的前提是有价值。如果一张图生成之后永远没人看、没人用、没人在意那它只是占用硬盘的“数字垃圾”。我做了这个节点后开始有意识地在工作流里加一个“人工筛选”环节生成后只保留通过筛选项的图进入加密归档。工具压缩了存储但真正压缩无效内容的还是人的判断。6.2 安全、版权与人的判断这次实现加密输出表面上解决的是“文件裸奔”但更深一层它让我重新思考 AI 内容的安全边界。加密能挡住未经授权的访问但挡不住内容本身的版权争议。一个模型生成出来的图可能在风格上高度接近某位创作者的作品也可能在语义上让人联想到真实人物或品牌。技术手段能做的只是“文件级别”的保护而“内容级别”的合规与尊重原作者的判断永远需要人来把关。我也见过不少团队把 AI 生成图直接当作“无主素材”使用这种心态很危险。加密输出的意义不在于让你隐藏来源而在于让你正视自己持有这份内容的责任。当你需要特意加密一批图的时候你就必须回答一个问题这些图为什么不能随便公开答案往往指向隐私、版权或商业机密。承认这一点比单纯追求“跑图效率”有意义得多。最后分享一个我自己的小习惯每批生成图加密归档后我会在同一个目录写一个README.txt记录生成时间、模型版本、用途和授权情况。这些信息本身不加密但当我在几个月后翻到这批文件时能立刻想起当初为什么生成它们。工具负责保管记忆负责判断。这个压缩加密输出节点的代码只是基础你完全可以根据自己的场景继续扩展加一个自动上传到对象存储的分支、加一个图片指纹去重、加一个收到回调后自动删除本地原图的功能。把它当成一块积木而不是终点。希望这些实操细节能帮你少踩几个坑也让你在享受 AI 效率的同时多保留一点对自己产出内容的掌控感。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑