先讲个场景。你手里有一张客户给的实拍图,想把衣服换成新款,背景换成浅灰棚拍。第一次上手作图生图,你把 denoise(去噪强度)拉到 0.5,点了生成,出来一张脸还像、衣服已经不像、头发边缘毛毛躁躁的图。你把 denoise 调到 0.6,脸变了;调回 0.4,衣服没换成功,只是加了一层灰。来回折腾两小时,最后放弃,改用手动 PS。
问题不在于你不会调,而在于大部分人把 denoise 理解成了「强度百分比」。它不是强度百分比,它是接管时机。理解这一点,上面那一整套折腾都能省掉。这篇文章把 ComfyUI 图生图的 denoise 拆成一把有刻度的尺子,从 0.2 到 0.95 逐档讲透,再把 ControlNet 补偿逻辑和 Python 批量跑脚本完整交给你。
一张图看懂:denoise 到底在改什么
想真正搞懂 ComfyUI 图生图,先要搞懂扩散模型的「加噪—去噪」循环。SD 系列包括 SDXL、SD3.5、Flux 常见的工作流,采样器默认跑 20-30 步。图生图的做法是:把原图先用 VAE Encode 编码成潜空间特征(latent),然后按 denoise 的比例往里加噪,从某个中间步开始去噪。
关键就在这里:denoise 0.5,不是「把原图改动 50%」,而是「跳过前面的 50% 步数,从第 10 步(按 20 步算)开始接管」。前面那 10 步本来是用来从纯噪声里搭大结构的,现在被跳过了,模型从一张已经含半程噪声的原图上接着画。
所以 0.5 的真实含义是:模型认为原图的大结构已经定好了,我只负责把它细化重画。这也是为什么 0.5 换装总是「贴片感」——模型觉得整体构图不用改,只把看得见的细节重绘了一遍,衣服只是被重画了一层同样的衣服。
denoise 和采样步数的关系
设总步数为 N,denoise 为 d,那么模型实际执行的步数是 N × d,起始步是 N × (1 – d)。20 步 + 0.5,就是第 10 步到第 20 步,10 步。30 步 + 0.5,是第 15 步到第 30 步,15 步。
这带出一个容易被忽略的坑:改采样总步数会改变 denoise 的实际效果。很多人把 denoise 从 0.5 调到 0.55 发现效果变化很小,原因是 20 步下这 0.05 只折算成 1 步;但如果你同时把步数改到 40,同样的 0.55 就变成第 18 步到第 40 步,接管时间往后挪了 4 步,变化会马上显出来。
建议习惯:定一套固定的采样步数,只动 denoise。我个人做图生图默认 24 步,euler_ancestral 或 dpmpp_2m + karras。这样每次调整 denoise 的变化是可比的,脑子里那把尺子才能校准。
为什么 0.85 是那道坎
扩散模型的前 15% 步数(大约前 3 步,20 步制)几乎只负责一件事:把潜空间里的画面定成一个整体形状。超过 0.85 意味着你连这几步都跳过了,模型从一张几乎纯噪声的潜变量开始画,原图里能留下的只有很少一点低频道信息——大致构图、主色调、主体位置。
这就是为什么 0.9 以上的图生图和文生图看起来没有本质区别,只是构图上有原图的影子。想在这个区间「借构图、重画内容」,必须靠 ControlNet 把结构拉回来,这一节后面专门展开。
denoise 四档标尺:从调色到重构
我给这套标尺排了一个不用背的口诀:0.2 是修,0.5 是换,0.8 是改,0.95 是造。修是修光修色,换是换物换景,改是改风格改构图,造是从头造。
0.15-0.3:调色、调光、材质微调
这个区间的可见变化几乎只有:明暗关系、色调倾向、质感纹理。人物脸上的痘印会被磨平,衣服面料的织纹会换一种,天空的色温会有一点点偏,但轮廓、位置、姿势、五官比例全部锁定。
适合的场景:实拍图转成胶片感、产品图换一种材质(绒面改缎面)、室内照片统一灯光风格、把阴天改成黄昏。它有点像摄影后期里的一步轻量 LUT,但比 LUT 聪明,知道哪里该亮哪里该暗。
配套参数:步数 20-24,CFG 3.5-5,denoise 0.2-0.25,正向提示词里重点描述光照和材质,别写新物体。提示词写「soft window light from left, warm color grade, film grain」,不要写「new dress」——它换不了。
0.2 档的两个实操细节
第一,VAE Encode 的节点有一个「upscale latent」选项,很多人忽略。如果你的原图是 1024×1024,直接走 VAE Encode 进潜空间就是 128×128 的 latent,出来的图细节受限于 latent 分辨率。0.2 档主要靠纹理,纹理又完全靠 latent 空间的分辨率,所以这一档建议原图短边不低于 1024,或者前置一个 Latent Upscale。
第二,提示词的权重不要过高。CFG 拉到 7 以上,0.2 档下会出现明显的「颜色烧焦」,尤其在皮肤高光区域。CFG 控制在 4 左右,提示词只写风格和光,是最稳的。
0.4-0.55:换装、换背景、改局部物件
0.5 这个档位是所有人最想挑战、也最容易翻车的区间。它的可见变化是:局部的内容可以被全新填充(衣服、背景、手里的东西),但主体的轮廓和姿势保持住。前提是——你必须告诉模型改哪块。
这里就是本文第一个独到观点:0.5 换装之所以失败,不是参数不对,是没有蒙版意识。整张图 0.5 denoise,模型会把脸部也重画一遍,而脸部细节本来就够,重画只会往「AI 感」跑;衣服被重画成一件「相似但不对」的衣服,因为原衣服提供的信息浓度太高,模型更愿意沿用它。
正确做法是用 VAE Encode (for Inpainting) 或者 Load Image → Mask,把衣服区域抠出来,只在蒙版内做 0.5-0.65 的重绘。蒙版外的区域用原图潜变量直接拼回(latent composite 或者直接走 inpaint 节点),脸部信息就一点都不动。
配套参数:步数 24-30,CFG 5-7,denoise 0.5-0.65(蒙版内可以更高),Inpaint 类模型或全模型 + inpaint 节点。提示词里明确写新物体的材质、颜色、版型,例如「oversized white cotton shirt, visible stitching, soft folds」。
换背景比换装简单
背景信息浓度低,模型的「依恋」也弱。0.45-0.5 的 denoise 加上一个好用的场景提示词,绝大部分背景都能换干净,不需要蒙版。但要注意头发丝——高 denoise 下头发边缘会和新背景粘连,解决方案是先把背景用 Remove Background 类节点干净地分离,或者让模型自己去处理 soft edge:提示词加「clean hair edges」「matted background」。
0.6-0.75:改风格、换构图
这一档的可见变化开始大幅拉开:模型不再忠实于原图的每一根线,而是「用原图当参考,重画一张」。构图会变,比例会变,主体的形状会被重新解释。适合的任务:照片转插画、写实转二次元、竖构图改横构图、把静止人物改成动作姿势(虽然姿势大幅变化要靠 ControlNet)。
这一档里 提示词的权重会陡然上升。0.2 档提示词只是加一点点气味,0.7 档提示词决定了整张图的走向。所以在这一档,模型选择要比提示词校准更花时间。
配套参数:步数 25-30,CFG 6-8,denoise 0.65-0.75,如果保留原构图就用 img2img 模式,如果要真换姿势,就要配 ControlNet OpenPose / Depth。
0.8-1.0:仅借构图,纯重构
0.8 以上,原图提供的只有「画面大致怎么摆」。你给一张办公室照片,0.9 出来可能是一间赛博仓库,构图、大小关系对得上,内容全换。这就是重构。
0.9 以上,如果你的 prompt 没有刻意描述原图主体,模型会完全放飞。所以这一档不要指望「保住什么」,它的价值在于快速从一张构图里批量生成变体——电商场景里,一张产品构图能变成几十种背景、几十种风格,用来选版型的草图。
0.95 的实际用途
0.95 不是「图生图失败」,它是「用图当 prompt」。当你的参考图是一张粗糙的草图、一张老照片、一张低清图,你想用它的构图生成一张高清成片,0.95 是把原图信息全部丢弃、只留构图骨架的操作。往往配合 ControlNet Canny 或 Lineart 一起用,结构才保得住。
denoise 四档对照表
| denoise 区间 | 一句话定位 | 可见变化 | 典型任务 | 推荐 CFG / 步数 | 是否需要蒙版或 ControlNet |
|---|---|---|---|---|---|
| 0.15 – 0.3 | 修 | 色调、明暗、材质纹理改变;轮廓、五官、姿势全锁 | 调色、调光、实拍转胶片、产品材质微调 | 3.5 – 5 / 20 – 24 | 不需要,但原图短边建议 ≥ 1024 |
| 0.4 – 0.55 | 换 | 局部内容被全新填充,大结构和姿势保持 | 换装、换背景、手持物替换 | 5 – 7 / 24 – 30 | 换装强烈建议蒙版,换背景可不加 |
| 0.6 – 0.75 | 改 | 构图被重新解释,风格彻底变化,主体形状重绘 | 照片转插画、写实转二次元、竖改横 | 6 – 8 / 25 – 30 | 保姿势需 ControlNet,其余可纯 img2img |
| 0.8 – 1.0 | 造 | 只保留构图暗示,内容完全重新生成 | 草图生成成片、构图批量变体、老图修复起稿 | 7 – 9 / 25 – 32 | Canny / Depth / Lineart 几乎必备 |
这张表建议直接截图放在桌面。很多人调图生的图,只是「不知道自己在哪一档」,把它对一下,参数方向立刻明确。
三张图的实测对照:同一张源图跑四个档位
拿一张 1024×1365 的室内人像实拍图做实验,固定 24 步、dpmpp_2m + karras、CFG 5.5,只改 denoise,结果大致是这样:
0.25:整张图的轮廓和原图逐像素级贴合。变化集中在皮肤的光泽、墙面光的色温(从偏冷 5600K 变成偏暖 4200K 的视觉)、衣服织纹更清晰。放大看,头发丝的走向一模一样。这张图可以当「修图」用,客户基本看不出是 AI 处理的。
0.5:人物姿势、脸型、发型完全一致,但背景的窗帘多了一道褶、墙上的画换成了另一幅、衣服的袖口长度变了约两指。整体还是那张图,细看每一处都是新的。这一档最危险——它给你「差不多」的错觉,但交付时客户会发现袖口不对。
0.7:构图基本还在,但人物被重新解释了——脸型略微变窄、肩膀比例更协调、背景变成另一种室内风格。原图里「靠窗站着」的信息还在,但「这扇窗长什么样」已经完全丢失。这一档的图更适合当插画或宣传图,不适合当产品图。
0.9:只剩「一个女性站在画面偏左、光线从右侧来」这一点暗示。如果你不写提示词,出来可能是一间完全不同风格的房间。这一档需要配 Canny 或 Depth,否则结构完全依赖模型自由发挥。
顺便说一个观察:从 0.25 到 0.5,变化是「线性累积」的,你能感觉到每一处都在动一点点;从 0.5 到 0.7,变化是「跳跃」的,前一秒还是同一间屋子,后一秒就换了一间;从 0.7 到 0.9,变化又变平了,因为能丢的信息已经丢光了,剩下的只有骨架。这条「缓—陡—缓」的曲线,就是 denoise 这把尺子的真实手感。记住它,比记任何数值表都管用。
这个对照实验建议你自己做一遍,用同一张图和同一组固定 seed(注意是固定,不是随机),把四档并排。做过一次之后,你对 denoise 的手感会比看十篇文章都准。这也是「把 denoise 当尺子」最直接的校准方式——尺子不是背出来的,是用出来的。
高 denoise 不跑形:ControlNet 补偿逻辑
这是本文最有价值的一节。很多教程只是说「0.8 重构用 ControlNet」,但没讲清楚为什么、补什么、补多少。我们把逻辑捋清楚。
为什么高 denoise 会让结构崩
还记得前面说的「接管时机」吗?denoise 0.85,模型跳过前面 85% 步,直接从中后段开始去噪,这时候潜变量里的原图信息浓度已经极低。模型靠文本来重建画面,而文本只告诉你「一个女孩站在窗前」,不告诉你女孩站在画面左侧还是右侧、肩宽多少、头发多长。这些结构信息在 high denoise 下是缺的,缺了自然就崩。
而 ControlNet 提供的附加条件是外挂的、不受 denoise 削减的结构约束。它从原图里提取一张线稿、一张深度图、一张姿态骨骼,然后每一步去噪都在这些特征上强化——注意,是每一步,不只是起始步。
所以 ControlNet 即使在高 denoise 下也能把结构拉回来。这就是「高 denoise + ControlNet」是「重构但不跑形」的核心配方的根本原因。
四类 ControlNet 怎么选
不同控制信号保的是不同的东西,选错就白忙。
| ControlNet 类型 | 保的是什么 | 适用 denoise 区段 | 推荐 strength | 典型场景 |
|---|---|---|---|---|
| Canny / Lineart | 所有边缘轮廓,最硬 | 0.8 – 1.0 | 0.9 – 1.2 | 草图转成片、线稿上色、建筑线框重构 |
| Depth | 前后景空间关系,不保细节 | 0.7 – 0.95 | 0.6 – 0.9 | 景深重构、换场景但保留空间占位 |
| OpenPose | 人体骨骼姿态,不保脸不保衣 | 0.6 – 0.95 | 0.8 – 1.0 | 换装、换人物、姿势锁定 |
| Tile / Blur | 整体色调与大致结构,弱约束 | 0.4 – 0.7 | 0.4 – 0.7 | 放大、加细节、调色但保留整体 |
看表能看出一个规律:Canny 用于最硬的结构锁定(0.9+),Depth 用于空间锁定(0.8 左右),OpenPose 用于人物姿态锁定(换装场景利器),Tile 用于纹理而非结构。
为什么 0.8 + OpenPose + IPAdapter 是换装的黄金组合
这三个插件分工很清晰:denoise 0.8 负责把原图旧衣服「洗掉」,OpenPose 负责锁住人物姿势(防止 0.8 下人物走形),IPAdapter 或 Reference Only 负责把参考图里新衣服的版型、颜色注入模型。
这个组合下,人物站位、手势、脸朝向稳如老狗,衣服全新,头发丝位置也跟原图契合。这是目前电商换装最稳的工程路径。但 ControlNet 的权重不要拉到 1.5,容易画面发死,硬边明显。
ControlNet 权重与 denoise 的联动
这是个很多人不知道的隐性关系:denoise 越高,ControlNet 权重需要越接近满值。原因是 ControlNet 的引导衰减和采样步数挂钩,denoise 0.9 只跑 10 步(按 100 步制的十分之一),ControlNet 能发挥的机会更少,所以要拉高。
0.65 的 denoise 配 0.5 的 ControlNet strength 通常就够;0.9 的 denoise 配同样的 0.5 strength,结构大概率还是崩。这需要你心里有一张对齐表。
ControlNet 权重过高会怎样
拉到 1.5 以上,问题会出现:画面像硬贴上去的线稿,边缘发光,颜色被拉爆。尤其 Canny 这种硬边缘控制,权重超过 1.2 就开始显脏。这个区间我的建议是 0.95-1.1,宁可结构略松也不要糊边。
还有一种情况:ControlNet 权重给够了,但 denoise 也高,结果结构保住了、纹理却「AI 感」很重。原因是 ControlNet 只管轮廓不管材质。这时候叠一个低权重的 Tile 或 Detailer 节点,质感就回来了。
多 ControlNet 叠加:什么时候该加第二层
单层 ControlNet 保住的是一维结构。当你的任务需要同时保住「姿势」和「空间层次」,就要叠两层。ComfyUI 里用 ControlNet Apply (Stack) 或者两个 ControlNet Apply Advanced 串联,后者更灵活,因为可以分别设 strength 和起止步数。
叠层的经验规则:第一层放最想保留的结构,权重略高;第二层放辅助性信息,权重压低到 0.4-0.6。比如人物换装场景,OpenPose 权重 0.95 打底,Depth 权重 0.45 补充空间占位。反过来如果 Depth 给到 0.8,画面会变得死板,衣褶和手的姿态被压平。
叠加超过两层后,收益急剧下降,还会让提示词的表达力被压制。我见过的稳定组合基本是两层封顶,最多加一个 Tile 做纹理补充。三层以上的 ControlNet 在批量跑里非常容易出「僵硬图」——不是崩,而是每张都像被钉在模板上,缺少自然变化。
ControlNet 的起止步数比权重更值得调
ControlNet Apply Advanced 节点有 start_percent 和 end_percent,很多人从不动它,默认 0 到 1。这个参数其实很关键。
把 end_percent 设到 0.8,意思是后 20% 的采样步不再受 ControlNet 约束,模型可以自由打磨细节。这样结构在前期被锁住,后期又能获得自然的边缘过渡和纹理,比全程 100% 约束要自然得多。换装场景推荐 start 0 / end 0.75,重构场景推荐 start 0 / end 0.9。
反过来,如果你发现生成的图「边缘发光」「像贴上去的」,先别急着降权重,试试把 end_percent 从 1 调到 0.8。这个调整比降权重更能保留结构、改善观感。
写 workflow:图生图的节点连法
先看节点结构。ComfyUI 图生图最基础的链条是:
Load Image ──► VAE Encode ──► KSampler.latent_image
▲
Checkpoint Loader ──► CLIP Text Encode (正向/负向)
│
Empty Latent (不连,img2img 时弃用) │
▼
VAE Decode ──► Save Image
几个关键点:
第一,VAE Encode 和 Empty Latent Image 二选一。Empty Latent 是文生图的起点,图生图改用 VAE Encode 输出。很多新手忘了断开 Empty Latent,结果 KSampler 接到的是纯噪声,图生图等于没生。
第二,VAE Encode 节点里有个 upscale_latent 开关,关着时只对原图编码,开着时会先插值放大再编码。图像大分辨率(4096×4096)时开它,可以减少潜空间压缩损失。但注意,它也会让显存压力上去。
第三,KSampler 节点上,denoise 参数就是那个尺子的刻度。其他参数:steps、cfg、sampler_name、scheduler、seed。批量跑时代码要改的就是 denoise、seed 和 Load Image 的 filename,其余参数尽量固定。
workflow JSON 关键片段
ComfyUI 保存的 workflow 默认格式是「UI 格式」,节点以数字 ID 为键,参数在 inputs 子对象里。下面是核心三个节点的真实结构,省略了 pos、size 等界面参数:
{
"3": {
"class_type": "KSampler",
"inputs": {
"seed": 1023947,
"steps": 24,
"cfg": 5.5,
"sampler_name": "dpmpp_2m",
"scheduler": "karras",
"denoise": 0.5,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["10", 0]
}
},
"10": {
"class_type": "VAEEncode",
"inputs": {
"pixels": ["8", 0],
"vae": ["4", 2]
}
},
"8": {
"class_type": "LoadImage",
"inputs": {
"image": "source_0001.png",
"upload": "image"
}
},
"11": {
"class_type": "SaveImage",
"inputs": {
"filename_prefix": "i2i_batch",
"images": ["12", 0]
}
}
}
批量脚本要做的事很清晰:把 8.inputs.image 换成新的图片名,把 3.inputs.seed 换一个新的随机数,把 3.inputs.denoise 按档位向量覆盖。然后 POST 到 /prompt。
注意:ComfyUI 的 API 格式 workflow 有时在保存面板里叫「Export (API)」,和 UI 格式略有差别——UI 格式带 extra、links 等 UI 渲染字段,API 格式只有 class_type 和 inputs,并且没有 pos、size。批量脚本要用 API 格式。UI 格式也能提交,ComfyUI 会容忍并忽略多余字段,但官方推荐 API 格式。
还有一点必须说明:inputs 里的连线是数组形式 ["4", 0],第一位是上游节点的数字 ID,第二位是输出槽位序号。比如 "model": ["4", 0] 表示接第 4 号节点的第 0 个输出(模型),"vae": ["4", 2] 表示同一个节点的第 2 个输出(VAE)。改连线的本质就是改这两个数字,但让脚本自动改连线很容易出错——所以推荐做法是先在界面里把整条链搭对、跑通一张,再导出 JSON,脚本只做参数替换,绝不碰连线。
这也是批量跑能稳定跑起来的前提:workflow 一旦导出,它就是一个参数化的模板。模板里哪些是可变的(image、seed、denoise、filename_prefix),哪些是冻结的(节点结构、提示词、采样器),必须在脑子里分清楚。混在一起改,整批图就跑歪了。
一个高频陷阱:KSampler 的 latent_image 接错
图生图时,KSampler 的 latent_image 必须接 VAE Encode 的输出(上面片段里是 ["10", 0])。如果你沿用文生图模板,这里很可能还指着 Empty Latent Image。结果就是 denoise 设了 0.5 却和文生图一模一样——很多人以为「denoise 不起作用」,其实是 latent 根本没接对。
排查方法很简单:把 denoise 设 0,如果输出仍然是随机画面,那百分之百是 latent 接错了。denoise 0 在图生图里应该原样输出原图(理论上是,实际会有轻微 VAE 重建偏差)。这个 10 秒的小测试,比翻半天节点连线有效。
Save Image 的 filename_prefix
批量跑图时,同一个 filename_prefix 让所有图落到一个前缀下,ComfyUI 会自动追加序号 _00001_ 这样的后缀。这很方便,但也容易导致你分不清哪张属于哪个源图。方案是批处理每次迭代动态改 prefix,例如 i2i_batch/src_0001,这样落盘目录结构清晰对应源文件。
Python 批量跑脚本(完整版)
思路四步:读 workflow JSON → 改 Load Image 的 filename、KSampler 的 seed 和 denoise → POST 到 http://127.0.0.1:8188/prompt → 轮询 /history 拿结果、下载 / 存档。脚本不依赖任何 ComfyUI 的 Python 包,只用标准库和 requests。
环境准备:开启 API 服务
ComfyUI 默认就对外提供 HTTP 服务,端口 8188。中文社区常见的启动命令:
python main.py --listen 127.0.0.1 --port 8188
批量跑的机器上,如果脚本和服务同机,--listen 127.0.0.1 就够。如果脚本在另一台机器,就要 --listen 0.0.0.0,并确保不是公网暴露(ComfyUI 没有内置鉴权,务必加防火墙)。
脚本主体:可运行的代码
import copy
import json
import os
import random
import time
from pathlib import Path
import requests
# ───────── 配置 ─────────
SERVER = "http://127.0.0.1:8188"
WORKFLOW_PATH = "./workflow_api.json"
SRC_DIR = Path("./inputs")
OUT_DIR = Path("./outputs")
OUT_DIR.mkdir(parents=True, exist_ok=True)
# 批次:每张源图 × 每个 denoise 档位 都跑一遍
DENOISE_LEVELS = [0.25, 0.5, 0.65, 0.85]
# 每张源图在每个档位跑几张
SAMPLES_PER_LEVEL = 2
# 提交后等待完成的轮询间隔
POLL_INTERVAL = 1.5
POLL_TIMEOUT = 600 # 秒
def load_workflow(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def find_nodes_by_class(wf, class_type):
"""按 class_type 找出所有节点 id,兼容可能存在的多个同类节点。"""
return [nid for nid, node in wf.items()
if node.get("class_type") == class_type]
def upload_image(session, local_path):
"""把本地图片上传到 ComfyUI 的 input 目录,返回 ComfyUI 内的文件名。"""
with open(local_path, "rb") as f:
files = {"image": (Path(local_path).name, f, "image/png")}
data = {"overwrite": "true", "type": "input"}
r = session.post(f"{SERVER}/upload/image", files=files, data=data, timeout=60)
r.raise_for_status()
info = r.json()
sub = info.get("subfolder", "")
return f"{sub}/{info['name']}" if sub else info["name"]
def build_prompt(base_wf, image_name, denoise, seed, prefix):
wf = copy.deepcopy(base_wf)
for nid in find_nodes_by_class(wf, "LoadImage"):
wf[nid]["inputs"]["image"] = image_name
for nid in find_nodes_by_class(wf, "KSampler"):
wf[nid]["inputs"]["denoise"] = denoise
wf[nid]["inputs"]["seed"] = seed
for nid in find_nodes_by_class(wf, "SaveImage"):
wf[nid]["inputs"]["filename_prefix"] = prefix
return wf
def submit(session, wf):
payload = {"prompt": wf, "client_id": "batch_i2i"}
r = session.post(f"{SERVER}/prompt", json=payload, timeout=60)
if r.status_code != 200:
raise RuntimeError(f"提交失败 {r.status_code}: {r.text[:400]}")
data = r.json()
if "error" in data:
raise RuntimeError(f"工作流校验失败: {data['error']}")
return data["prompt_id"]
def wait_result(session, prompt_id):
"""轮询 /history/{prompt_id},直到出现输出或超时。"""
deadline = time.time() + POLL_TIMEOUT
while time.time() < deadline:
r = session.get(f"{SERVER}/history/{prompt_id}", timeout=30)
if r.status_code == 200:
hist = r.json()
if prompt_id in hist:
entry = hist[prompt_id]
outputs = entry.get("outputs", {})
files = []
for node_out in outputs.values():
for img in node_out.get("images", []):
files.append(img)
if files:
return files
# 有时 status 已 completed 但文件列表稍晚出现,再等一下
time.sleep(POLL_INTERVAL)
raise TimeoutError(f"等待 {prompt_id} 超时")
def download(session, image_meta, dest_dir, prefix):
params = {
"filename": image_meta["filename"],
"subfolder": image_meta.get("subfolder", ""),
"type": image_meta.get("type", "output"),
}
r = session.get(f"{SERVER}/view", params=params, timeout=120)
r.raise_for_status()
dest = dest_dir / f"{prefix}_{image_meta['filename']}"
dest.write_bytes(r.content)
return dest
def main():
base_wf = load_workflow(WORKFLOW_PATH)
sources = sorted(SRC_DIR.glob("*.png")) + sorted(SRC_DIR.glob("*.jpg"))
if not sources:
raise SystemExit(f"{SRC_DIR} 下没有找到图片")
session = requests.Session()
total, done = 0, 0
for src in sources:
tag = src.stem
remote_name = upload_image(session, src)
for d in DENOISE_LEVELS:
for k in range(SAMPLES_PER_LEVEL):
total += 1
seed = random.randint(1, 2**31 - 1)
prefix = f"{tag}_d{int(d*100):03d}_s{seed % 10000}"
wf = build_prompt(base_wf, remote_name, d, seed, prefix)
try:
pid = submit(session, wf)
files = wait_result(session, pid)
for meta in files:
saved = download(session, meta, OUT_DIR, prefix)
print(f"[OK] {saved.name}")
done += 1
except Exception as e:
print(f"[FAIL] {tag} denoise={d} seed={seed}: {e}")
print(f"完成 {done}/{total}")
if __name__ == "__main__":
main()
这个脚本 100 行左右,覆盖了完整流程。几个细节值得特别说明:
为什么用 /upload/image 而不是直接改 filename
ComfyUI 的 LoadImage 只会从 input 目录读图,你直接把本地路径塞进去会报错。用 /upload/image 上传到 input 目录后,返回的 name 就是可以在 workflow 里引用的文件名。这也是迭代多图时的规范做法。
seed 一定要换
ComfyUI 默认 seed 是固定值,直接复用同一个 workflow 批量提交 denoise 相同、seed 相同、图相同,你只会得到完全一样的图。脚本中 random.randint(1, 2**31-1) 每次都换 seed。如果你想要「同 seed 对比不同 denoise」这种排查实验,就要反过来——把 seed 固定,只动 denoise,脚本里把这行改成 seed = 42 即可。
轮询 /history 的坑
/history/{prompt_id} 在 ComfyUI 的队列里是「完成后才出现」,而不是「进行中出现 status 字段」。所以脚本不能依赖「查询到 status 就是完成」,而要判断输出是否出现。上面脚本判断 prompt_id in hist 且 outputs 里有 images。这个逻辑在 ComfyUI 0.3.x 以上版本都是对的。
进阶:并发提交与队列
上面脚本是串行的,一张跑完再下一张。这很稳但速度慢,因为每张图要等 30-90 秒。ComfyUI 本身有队列,允许一次提交多个 prompt 排队。如果你想并发,可以把脚本改成:每张图都先 submit,把 prompt_id 存起来,最后再统一轮询——但这样不适合稳定跑完,因为出错无法中断。
更稳的并发方案是启动两个 ComfyUI 实例,端口 8188 和 8189,各跑一个脚本,靠 GPU 自身调度分配算力。这种方式在批量跑里效果最好,也最好排查。
两个实例的启动很简单,用不同的端口和不同的 base 目录即可,让它们的 input、output 互不干扰:
# 实例 A
python main.py --listen 127.0.0.1 --port 8188 --base-directory ./comfy_a
# 实例 B
python main.py --listen 127.0.0.1 --port 8189 --base-directory ./comfy_b
然后用环境变量把脚本的 SERVER 变成可配置项(os.environ.get("COMFY_SERVER")),开两个终端各跑一份,任务列表用源文件编号取模分给两边。24GB 显存下这种双实例方案的实际吞吐比单实例 + 大 batch 高 30% 左右,因为两个进程可以互相补空档——一个在做 VAE 解码时,另一个正在采样。
队列与取消:别让一条坏任务堵死整批
ComfyUI 的队列是先进先出。如果某张图卡住(比如 ControlNet 模型加载失败反复重试),它会一直占用队列头部,后面的全部等着。生产环境里,脚本最好给每个 prompt 加一个「看门狗」:超时就调用 /interrupt 中断当前执行,并 /queue 清空残留。
另外,提交前先 POST 到 /prompt 拿到的是 prompt_id,如果返回里带 node_errors,说明工作流校验不过(比如模型名写错、节点缺输入),这时候直接跳过这张,别反复重试——重试一百次也是同样的错误。
显存与 batch_size:batch 和 tiled VAE 的配合
批量跑的真正难点不是代码,是显存。很多人写完脚本一跑,跑到第五张爆 OOM,问题就卡在那里。
模型尺寸下的 batch_size 推荐
以下推荐基于 SD1.5 / SDXL 常用模型,64 步以内,不含 ControlNet 和 LoRA。挂上 ControlNet 后大概砍半。
| 显存 | SD1.5 512-768 | SDXL 1024 | Flux 1024 | 备注 |
|---|---|---|---|---|
| 8 GB | 1 - 2 | 1 | 勉强 1(需 fp8) | 必须开 --lowvram 或 --medvram |
| 12 GB | 2 - 4 | 1 - 2 | 1 | SDXL 建议 --medvram |
| 16 GB | 4 - 8 | 2 - 4 | 1 - 2 | 流畅,可挂一个 ControlNet |
| 24 GB | 8 - 16 | 4 - 8 | 2 - 4 | 可同时跑两个实例 |
为什么 batch_size 不等于速度翻倍
batch_size 从 1 加到 2,速度提升往往只有 1.4-1.7 倍;从 4 加到 8,边际收益只剩 10-20%。原因是注意力计算对显存的占用是平方级增长的,batch 越大,注意力窗口占用的显存越多,GPU 的 L2 缓存命中率下降。
所以我的经验是:不要为了 batch 大而大。16GB 显存跑 SD1.5,batch 4 的吞吐量最平衡,batch 8 只比 4 快不到 20%,但显存风险翻倍。批量跑项目里,我更推荐batch 小一点、种子组合多一点,这样单图失败不会影响整批。
VAE 解码:真正的显存大户
很多人以为采样器是显存大户,其实 VAE Decode 才是。4K 图的 VAE Decode 峰值可以吃掉 8-10 GB,而同样分辨率下采样器可能只用 4 GB。原因是 VAE 解码要把整张特征图展开成 RGB,特征通道数远高于采样时的 latent。
解决办法:换用 VAE Decode (Tiled) 节点。这个节点把图切成小块逐块解码,显著降低峰值显存。推荐参数:
VAE Decode (Tiled) 输入:
samples: VAE Encode 的输出
vae: Checkpoint 的 vae
tile_size: 512
overlap: 64
temporal_size: 64
temporal_overlap: 8
tile_size 512 + overlap 64 是我在 8GB 和 12GB 卡上的稳定组合。overlap 太小时(比如 32)会在拼接处看到网格纹路,太大会让显存回涨。512/64 是 8 倍关系,接缝几乎看不见。
如果你发现接缝明显,可以加一个两步策略:先用 VAE Decode 输出小尺寸(latent 先 downsample 再 decode 估个底),再用 Tiled 精细出图,取 Tiled 版本。但一般情况下 512/64 就够用。
高分辨率图再叠加 VAE 编码端的 tiled
VAE Encode 也有 tiled 版本 VAE Encode (Tiled),大图进潜空间时同样能省一半显存。但要注意,Encode 的 tiled 和 Decode 的 tiled 不是对称好——编码端的块边缘会影响中间结果,拼接后可能影响细节。如果原图是 2K 以上,建议直接缩到 1536 短边再做图生图,然后再用 upscale 链把结果拉回目标尺寸,比硬放大更省显存。
启动参数与显存优化
如果显存实在紧张,ComfyUI 的启动参数可以救急。常见的几个:
python main.py --listen 127.0.0.1 --port 8188 \
--lowvram \
--force-fp16 \
--fp8_e4m3fn-unet \
--use-pytorch-cross-attention \
--disable-smart-memory
逐项说明:
--lowvram 把模型拆到 CPU 与 GPU 之间搬运,显存占用最低,但速度下降 30-50%。--medvram 是中间档,SDXL 跑 12GB 卡时这个最合适。
--force-fp16 强制半精度计算,几乎所有现代显卡都支持,少占 40% 显存。--fp8_e4m3fn-unet 把 U-Net 权重压到 fp8 存储,模型占的显存直接砍半,精度影响在 SDXL 上几乎看不出,Flux 上也是非常接近。
--use-pytorch-cross-attention 换成 PyTorch 原生的注意力实现,部分显卡上省 1-2 GB。--disable-smart-memory 让 ComfyUI 不去懒加载模型,虽不会省峰值显存,但能避免「模型在显存和内存之间搬运」导致的抖动,批量跑图时更稳定。
注意 不要同时挂 --lowvram 和 --fp8_e4m3fn-unet。前者已经在权重搬运打满,再压精度只会让速度雪上加霜。根据你的卡选一档即可。
还有两个经常被忽视的显存项。其一是 浏览器别开着 ComfyUI 界面跑批量——界面会把所有结果图加载到 canvas 预览,几百张跑下来浏览器能吃掉几个 GB 显存(如果开了硬件加速)。纯 API 批量时,界面可以直接关掉,服务端照样跑。
其二,LoRA 的数量对显存有隐性成本。同时挂 5 个 LoRA 时,每个都会做一次权重合并上下文,显存占用比单个 LoRA 高出 1-2 GB。批量跑时建议把 LoRA 焊进 checkpoint(提前 merge 一份出来),别在采样时动态挂四五个。
四种典型场景的参数配方
启动参数不是越多越好,而是按"你缺什么补什么"来选。下面把最常见的四种处境拆开讲,每种给一套可直接照抄的组合,并说明它的代价在哪里。
不同启动参数组合的取舍
把参数按「你缺什么补什么」来选:缺显存最多、不太在意速度 → --lowvram --fp8_e4m3fn-unet;缺显存但要速度 → --medvram --fp8_e4m3fn-unet;显存够但偶发 OOM → --disable-smart-memory;显存富裕想追极限 → 什么都不加,再加 --highvram。
注意 --highvram 在 24GB 卡上跑 SD1.5 会适得其反,它让模型全部常驻显存,反而没能留给采样中间结果。SD1.5 用默认就行,别乱开。
吞吐优先还是稳定优先
批量项目里,吞吐优先(低精、高 batch、允许偶发失败)和稳定优先(fp16 原精度、低 batch、跑完每一张)要二选一。如果你要跑 500 张电商图,稳定优先;如果你是调试 denoise 标尺、需要 20 张对比图,吞吐优先。
我的脚本里没有做自动重试,只打印失败。但实际生产环境建议包一个 for attempt in range(3) 重试外壳,且失败后把 --disable-smart-memory 打开重跑这一张——它能让「模型反复搬运」这种偶发 OOM 消失。
真实吞吐参考:一组可对照的数字
给一组实测数字,方便估工期。测试条件:SDXL 全模型 + 一个 OpenPose ControlNet,1024×1024,24 步,dpmpp_2m + karras,VAE Decode (Tiled) 512/64。
| 显卡 | 启动参数 | batch_size | 单张耗时 | 100 张预估 | 显存峰值 |
|---|---|---|---|---|---|
| RTX 3060 12G | --medvram | 1 | 约 22 s | 约 40 min | 10.5 GB |
| RTX 4060 Ti 16G | 默认 | 2 | 约 9 s / 张 | 约 17 min | 13 GB |
| RTX 4080 16G | 默认 | 2 | 约 6 s / 张 | 约 11 min | 14 GB |
| RTX 4090 24G | 默认 | 4 | 约 3.2 s / 张 | 约 6 min | 20 GB |
看这张表能得出一个务实结论:显存够就开 batch,显存不够就用 tiled 保命。3060 12G 加小 batch 跑出来的总时长,是 4090 的六七倍,但它是稳定能跑完的。批量跑的第一目标是「跑完」,第二目标才是「跑快」。
七种常见失败与解法
把踩过的坑汇总一下,按发生频率排序。
失败一:0.5 换装贴片感
这是最典型的。原因前面已经分析了——整张图 0.5,模型把脸和衣服都重画了一遍,但衣服信息浓度太高,重画出来的还是原衣服。解法:加蒙版,只在衣服区域 0.55-0.65,其他区域 0 改动;或者 denoise 拉到 0.75 用 OpenPose 锁姿势,让模型敢把衣服真正换掉。
如果客户给的图衣服很复杂(花纹、褶皱、多层次),0.65 + 蒙版仍会保留原花纹。这时只能走 0.8 + OpenPose + IPAdapter 的组合路线,没有更便宜的办法。
失败二:0.8 换风格脸部崩
0.8 的 denoise 下,脸部信息浓度下降严重,模型靠文本重建五官,出来的人「很像 AI」,五官会微妙地移位,眼神涣散。解法两个:
其一,分区域采样:先整图 0.8 跑一遍,再在脸部区域(自动用 face detection 或手动框)单独跑一遍 0.3 的局部图生图来「修复」脸部。这是一个两步流程,但效果最稳。
其二,加 ControlNet FaceID / InstantID / PuLID 之类的人脸一致性控制,把原图的脸部特征注入到高 denoise 生成过程。这类 ControlNet 在 ComfyUI 里生态已经比较成熟,0.8 以下的图生图配上后脸部会稳很多。
失败三:批量跑一半 OOM
这个在显存小的卡上非常常见。原因通常是前面几张图没释放显存,或者连续跑下来「显存碎片」积压。解法:
第一,在脚本每轮之间不要刷新 ComfyUI 页面(如果是 API 模式本来就不涉及界面)。第二,ComfyUI 内部有一个 /free 端点可以手动释放显存,可以在每 N 张图之后调用一次。
第三,也是最容易忽视的:换源图时分辨率要统一。如果一批输入里混着 512×512 和 2048×2048,前面几张吃满显存后缓存没清,跑到大的那张就直接爆。预处理阶段统一短边,是杜绝这类 OOM 的根本办法。
session.post(f"{SERVER}/free",
json={"unload_models": True, "free_memory": True})
但它会让后续图片重新加载模型,速度下降。一般每 10-20 张调一次够用。或者用我刚才说的 --disable-smart-memory 启动参数,从根源上避免搬运抖动。
失败四:denoise 调了但效果没变
检查三件事:采样步数是否被改了;KSampler 的 denoise 是否接到正确的节点(多 KSampler 工作流容易接错);VAE Encode 是否在生效(Empty Latent 节点有没有被误连)。
还有一种冷门原因:某些自定义采样器节点(比如 SamplerCustomAdvanced 的自定义版)会忽略 denoise,需要检查节点内部对 denoise 的读取逻辑。切回官方 KSampler 采样器就正常了。
失败五:无缝拼接出现网格
使用 VAE Decode (Tiled) 时,如果 tile_size 和 overlap 比例不合适,拼缝会出现肉眼可见的格子。经验参数是 overlap / tile_size ≈ 1/8。512 配 64 是完美的 1/8;1024 配 128 同理。低于 1/10 的接缝会显。
另外,如果原图是纯色背景(白墙、纯灰),这些区域接缝反而更明显,因为对比度低、边缘被压缩。这种场景我建议先 VAE Decode 输出,再用一个很小的 img2img × 0.1 的修复一遍,接缝就没了。
失败六:批量跑的图分辨率不对
有些工作流会带 Resize 节点,若你换原图时新图分辨率不同,Resize 节点可能按原基准缩放导致最终输出尺寸和预期不一致。批量脚本里读取源图 resolution 并覆盖 Resize 节点参数是个基本操作。另外,VAE Encode 出来的 latent 尺寸如果和某些约束不匹配(必须能被 64 整除),也会被自动裁剪。
所以推荐:批量跑前,把所有输入图预处理到统一的短边 768-1536 px,长边按比例裁到能被 64 整除。这样整批跑下来,输出尺寸稳定,接缝风险也低。
失败七(隐性):结果图「都对,但都不对」
这一类没有报错,也不崩溃,但你打开一看,每张图都有一种说不出的「像」——像同一个模板印出来的。根源是 seed 没变,或者 prompt 没随源图变化。
图生图的提示词应该跟着源图走,而不是一份提示词套所有图。实拍室内图配「室内人像、窗光」,实拍户外图还配「室内人像」,模型只能用 0.5 的 denoise 硬把户外改造成室内,结果比例失调。生产环境里,每张源图应该有一份对应的提示词种子,脚本里按文件名匹配去替换 CLIPTextEncode 节点的 text 字段。
完整批量实践:准备、跑、验收
最后讲一个完整的批量实战流程,可以照着抄。
step 1:准备输入
把所有源图放进 ./inputs,统一短边 1024 px(不超过 1536),长边裁到能被 64 整除。文件命名统一 src_0001.png 这样的格式,方便脚本按序号对应。
step 2:导出 API 格式 workflow
打开 ComfyUI 界面,搭好一个能跑的图生图 workflow,先手动跑通一张。然后在菜单里选「Export (API)」,保存为 workflow_api.json。打开这个 JSON 确认:LoadImage 节点有 "image" 字段;KSampler 节点有 denoise 和 seed;SaveImage 有 filename_prefix。
step 3:跑脚本,先跑一张验证
把 DENOISE_LEVELS 临时改成 [0.5],SAMPLES_PER_LEVEL = 1,跑通一张,看结果对不对。确认后再放开全量。这一步能省下后面几十分钟的排查时间。
step 4:批量跑,落盘
跑完后 ./outputs 里的文件命名会像 src_0001_d050_s1234_i2i_batch_00001_.png 这样,源图编号、denoise 档位、seed 前四位全部对得上。可以直接按前缀去筛选和对比。
step 5:验收与 compare
打开一个简易对比器,把同源图的 4 个档位缩略图并排。重点看三个东西:颜色有没有偏、结构有没有走形、风格是否达成。然后在四档里选一张进第二轮精修,精修用 0.25 左右的局部 denoise 修细节。
这个「先批量出档,再单张精修」的流程,比「一张一张反复调 denoise」快得多。这也是 denoise 标尺的工程价值:让参数成为可枚举的空间,而不是靠直觉摸索的黑箱。
最后补一个思考:很多人觉得 denoise 是「调参玄学」,是因为没有把它放回「接管时机」的物理含义里。一旦把它当成刻度尺,0.2、0.5、0.8 就变成了三个不同的工具,而不是同一件工具的三种强度。
常见问题
denoise 0.5 换装为什么总是效果不明显?
主要原因是没加蒙版,整张图被统一重绘,衣服区域原信息浓度太高,模型绕不开原衣服。解决办法是只在衣服区域用 0.55-0.65 的 denoise 配合蒙版重绘,或者用 0.75-0.8 的 denoise 加 OpenPose 锁姿势,让模型敢彻底换掉衣服。ComfyUI 图生图的换装任务里,蒙版意识比参数调优更重要。
denoise 和采样步数怎么搭配最稳?
建议固定采样步数只动 denoise。20-24 步配 0.2 调色档、24-30 步配 0.5 换装档、25-30 步配 0.8 重构档。改步数会联动改变 denoise 的实际接管步数,两者都变会失去可比性。
0.8 以上一定要挂 ControlNet 吗?
不一定,但强烈建议。0.85 以上原图信息只剩构图暗示,人物姿态和五官很容易走形。挂 Canny 保硬边、Depth 保空间、OpenPose 保姿势,再配高 denoise,才能实现「重构不跑形」。不挂 ControlNet 时,0.8 以上的批量跑大概率整批失败。
批量跑脚本为什么要用 /upload/image?
因为 ComfyUI 的 LoadImage 节点只从 input 目录读文件,本地绝对路径不被接受。脚本把源图上传到 /upload/image 接口后会返回文件名,再把这个文件名写进 workflow 的 LoadImage.inputs.image,才能正常加载。这是 ComfyUI 图生图批量跑的标准做法。
8 GB 显存能批量跑 SDXL 图生图吗?
可以,但必须把 batch_size 压到 1,并开启 --medvram 或 --lowvram,外加 --fp8_e4m3fn-unet。VAE 解码端建议改用 VAE Decode (Tiled),tile_size 512 / overlap 64。这样原始速度大概下降一半,但能稳稳跑完,批量跑不至于中途 OOM。
批量跑到一半 OOM,最有效的修复是什么?
优先检查三点:batch_size 是否真在使用时才爆(即前面能跑通、后面才挂);是否在每张之间释放显存(可每 10 张调用一次 /free);启动时是否加了 --disable-smart-memory 防止模型反复搬运。这三项处理好,8 GB 显存也能连续跑完上百张。ComfyUI 图生图的批量跑,稳定性比绝对速度值钱。









暂无评论内容