Files

404 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ZImage Noise Injection 节点
通过 CFG 机制将参考图像的特征注入到生成中。
可以让生成结果"学习"参考图像的某些特质(如水珠、纹理、质感等)。
"""
import torch
import torch.nn.functional as F
import logging
import comfy.model_management
import comfy.latent_formats
class LGNoiseInjection:
"""
LG Noise Injection - 特征注入
工作原理:
参考图像的 latent 特征会被注入到 CFG 过程中,
模型会"学习"参考图像的某些特质并应用到生成结果上。
适用场景:
- 添加水珠、汗珠等表面细节
- 添加纹理、材质感
- 添加光泽、反射效果
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL", {
"tooltip": "模型"
}),
"vae": ("VAE", {
"tooltip": "VAE 编码器"
}),
"reference_image": ("IMAGE", {
"tooltip": "参考图像(含有你想要注入的特征)"
}),
"strength": ("FLOAT", {
"default": 0.15,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "注入强度。0.1-0.2 轻微,0.2-0.4 明显"
}),
"start_percent": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "开始注入的采样进度"
}),
"end_percent": ("FLOAT", {
"default": 0.6,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "结束注入的采样进度"
}),
},
"optional": {
"mask": ("MASK", {
"tooltip": "遮罩,白色区域会被注入特征"
}),
},
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "apply"
CATEGORY = "advanced/model"
DESCRIPTION = "将参考图像的特征(如水珠、纹理等)注入到生成结果中。"
def apply(self, model, vae, reference_image, strength, start_percent, end_percent, mask=None):
if strength <= 0:
return (model,)
m = model.clone()
# 编码参考图像(使用模型的 latent format)
ref_latent = self._encode_reference(vae, reference_image, model)
# 预处理 mask
mask_latent = None
if mask is not None:
mask_latent = self._prepare_mask(mask)
step_counter = [0]
def cfg_function(args):
"""在 CFG 合并后注入参考特征"""
cond = args["cond"]
uncond = args["uncond"]
cond_scale = args["cond_scale"]
timestep = args["timestep"]
step_counter[0] += 1
# 标准 CFG
cfg_result = uncond + cond_scale * (cond - uncond)
# 计算进度
sigma = float(timestep[0]) if timestep.dim() > 0 else float(timestep)
progress = 1.0 - min(sigma, 1.0)
# 检查是否在作用范围内
if progress < start_percent or progress > end_percent:
return cfg_result
# 准备 reference
ref = ref_latent.to(device=cfg_result.device, dtype=cfg_result.dtype)
if ref.shape[2:] != cfg_result.shape[2:]:
# 根据张量维度选择插值模式
if len(ref.shape) == 5: # 5D张量 [B, C, T, H, W]
ref = F.interpolate(ref, size=cfg_result.shape[2:], mode='trilinear', align_corners=False)
else: # 4D张量 [B, C, H, W]
ref = F.interpolate(ref, size=cfg_result.shape[2:], mode='bilinear', align_corners=False)
if ref.shape[0] != cfg_result.shape[0]:
if ref.shape[0] == 1:
# 根据张量维度调整expand
if len(ref.shape) == 5:
ref = ref.expand(cfg_result.shape[0], -1, -1, -1, -1)
else:
ref = ref.expand(cfg_result.shape[0], -1, -1, -1)
else:
ref = ref[:cfg_result.shape[0]]
# 准备 mask
current_mask = None
if mask_latent is not None:
current_mask = mask_latent.to(device=cfg_result.device, dtype=cfg_result.dtype)
# 调整 mask 尺寸到 latent 空间
if current_mask.shape[2:] != cfg_result.shape[2:]:
# 根据张量维度选择插值模式
if len(current_mask.shape) == 5: # 5D张量 [B, C, T, H, W]
current_mask = F.interpolate(current_mask, size=cfg_result.shape[2:], mode='trilinear', align_corners=False)
else: # 4D张量 [B, C, H, W]
current_mask = F.interpolate(current_mask, size=cfg_result.shape[2:], mode='bilinear', align_corners=False)
# 调整 batch size
if current_mask.shape[0] != cfg_result.shape[0]:
if current_mask.shape[0] == 1:
# 根据张量维度调整expand
if len(current_mask.shape) == 5:
current_mask = current_mask.expand(cfg_result.shape[0], -1, -1, -1, -1)
else:
current_mask = current_mask.expand(cfg_result.shape[0], -1, -1, -1)
else:
current_mask = current_mask[:cfg_result.shape[0]]
# 计算有效强度(线性衰减)
if end_percent > start_percent:
range_progress = (progress - start_percent) / (end_percent - start_percent)
decay = 1.0 - range_progress
else:
decay = 1.0
effective_strength = strength * decay
# 计算特征注入方向
feature_direction = ref - cfg_result
# 控制注入幅度,避免过度偏移
cfg_std = cfg_result.std()
feature_std = feature_direction.std()
if feature_std > cfg_std * 3:
feature_direction = feature_direction * (cfg_std * 3 / feature_std)
# 应用遮罩
if current_mask is not None:
# mask: 1 = 注入区域, 0 = 保持原样
feature_direction = feature_direction * current_mask
# 应用特征注入
injected = cfg_result + feature_direction * effective_strength
if step_counter[0] <= 3:
mask_info = "with mask" if current_mask is not None else "no mask"
logging.warning(f"[FeatureInj] step={step_counter[0]} | progress={progress:.2f} | eff_str={effective_strength:.3f} | {mask_info}")
return injected
m.set_model_sampler_cfg_function(cfg_function)
return (m,)
def _encode_reference(self, vae, reference_image, model):
"""编码参考图像"""
loaded_models = comfy.model_management.loaded_models(only_currently_used=True)
latent = vae.encode(reference_image)
# 使用模型自己的 latent format,而不是硬编码 Flux
latent_format = model.get_model_object("latent_format")
latent = latent_format.process_in(latent)
comfy.model_management.load_models_gpu(loaded_models)
logging.warning(f"[FeatureInj] Reference encoded: shape={latent.shape}")
return latent
def _prepare_mask(self, mask):
"""准备遮罩,转换为 latent 空间格式"""
# mask 输入格式: [B, H, W] 或 [H, W]
if mask.dim() == 2:
mask = mask.unsqueeze(0) # [H, W] -> [1, H, W]
# 添加 channel 维度: [B, H, W] -> [B, 1, H, W]
mask = mask.unsqueeze(1)
logging.warning(f"[FeatureInj] Mask prepared: shape={mask.shape}")
return mask
class LGNoiseInjectionLatent:
"""
LG Noise Injection (Latent) - 直接使用 Latent 的特征注入
工作原理:
直接输入参考 latent,其特征会被注入到 CFG 过程中。
如果 latent 包含 noise_mask,则自动使用该遮罩。
适用场景:
- 添加水珠、汗珠等表面细节
- 添加纹理、材质感
- 添加光泽、反射效果
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL", {
"tooltip": "模型"
}),
"reference_latent": ("LATENT", {
"tooltip": "参考 latent(含有你想要注入的特征)"
}),
"strength": ("FLOAT", {
"default": 0.15,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "注入强度。0.1-0.2 轻微,0.2-0.4 明显"
}),
"start_percent": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "开始注入的采样进度"
}),
"end_percent": ("FLOAT", {
"default": 0.6,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"tooltip": "结束注入的采样进度"
}),
},
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "apply"
CATEGORY = "advanced/model"
DESCRIPTION = "直接输入 latent 进行特征注入,自动使用 latent 的 noise_mask 作为遮罩。"
def apply(self, model, reference_latent, strength, start_percent, end_percent):
if strength <= 0:
return (model,)
m = model.clone()
# 获取 latent samples
ref_latent = reference_latent["samples"]
# 获取 noise_mask(如果存在)
mask_latent = None
if "noise_mask" in reference_latent:
mask_latent = self._prepare_mask(reference_latent["noise_mask"])
logging.warning(f"[FeatureInjLatent] Using noise_mask from latent")
logging.warning(f"[FeatureInjLatent] Reference latent: shape={ref_latent.shape}")
step_counter = [0]
def cfg_function(args):
"""在 CFG 合并后注入参考特征"""
cond = args["cond"]
uncond = args["uncond"]
cond_scale = args["cond_scale"]
timestep = args["timestep"]
step_counter[0] += 1
# 标准 CFG
cfg_result = uncond + cond_scale * (cond - uncond)
# 计算进度
sigma = float(timestep[0]) if timestep.dim() > 0 else float(timestep)
progress = 1.0 - min(sigma, 1.0)
# 检查是否在作用范围内
if progress < start_percent or progress > end_percent:
return cfg_result
# 准备 reference
ref = ref_latent.to(device=cfg_result.device, dtype=cfg_result.dtype)
if ref.shape[2:] != cfg_result.shape[2:]:
# 根据张量维度选择插值模式
if len(ref.shape) == 5: # 5D张量 [B, C, T, H, W]
ref = F.interpolate(ref, size=cfg_result.shape[2:], mode='trilinear', align_corners=False)
else: # 4D张量 [B, C, H, W]
ref = F.interpolate(ref, size=cfg_result.shape[2:], mode='bilinear', align_corners=False)
if ref.shape[0] != cfg_result.shape[0]:
if ref.shape[0] == 1:
# 根据张量维度调整expand
if len(ref.shape) == 5:
ref = ref.expand(cfg_result.shape[0], -1, -1, -1, -1)
else:
ref = ref.expand(cfg_result.shape[0], -1, -1, -1)
else:
ref = ref[:cfg_result.shape[0]]
# 准备 mask
current_mask = None
if mask_latent is not None:
current_mask = mask_latent.to(device=cfg_result.device, dtype=cfg_result.dtype)
# 调整 mask 尺寸到 latent 空间
if current_mask.shape[2:] != cfg_result.shape[2:]:
# 根据张量维度选择插值模式
if len(current_mask.shape) == 5: # 5D张量 [B, C, T, H, W]
current_mask = F.interpolate(current_mask, size=cfg_result.shape[2:], mode='trilinear', align_corners=False)
else: # 4D张量 [B, C, H, W]
current_mask = F.interpolate(current_mask, size=cfg_result.shape[2:], mode='bilinear', align_corners=False)
# 调整 batch size
if current_mask.shape[0] != cfg_result.shape[0]:
if current_mask.shape[0] == 1:
# 根据张量维度调整expand
if len(current_mask.shape) == 5:
current_mask = current_mask.expand(cfg_result.shape[0], -1, -1, -1, -1)
else:
current_mask = current_mask.expand(cfg_result.shape[0], -1, -1, -1)
else:
current_mask = current_mask[:cfg_result.shape[0]]
# 计算有效强度(线性衰减)
if end_percent > start_percent:
range_progress = (progress - start_percent) / (end_percent - start_percent)
decay = 1.0 - range_progress
else:
decay = 1.0
effective_strength = strength * decay
# 计算特征注入方向
feature_direction = ref - cfg_result
# 控制注入幅度,避免过度偏移
cfg_std = cfg_result.std()
feature_std = feature_direction.std()
if feature_std > cfg_std * 3:
feature_direction = feature_direction * (cfg_std * 3 / feature_std)
# 应用遮罩
if current_mask is not None:
# mask: 1 = 注入区域, 0 = 保持原样
feature_direction = feature_direction * current_mask
# 应用特征注入
injected = cfg_result + feature_direction * effective_strength
if step_counter[0] <= 3:
mask_info = "with noise_mask" if current_mask is not None else "no mask"
logging.warning(f"[FeatureInjLatent] step={step_counter[0]} | progress={progress:.2f} | eff_str={effective_strength:.3f} | {mask_info}")
return injected
m.set_model_sampler_cfg_function(cfg_function)
return (m,)
def _prepare_mask(self, mask):
"""准备遮罩,转换为 latent 空间格式"""
# mask 输入格式: [B, H, W] 或 [H, W]
if mask.dim() == 2:
mask = mask.unsqueeze(0) # [H, W] -> [1, H, W]
# 添加 channel 维度: [B, H, W] -> [B, 1, H, W]
if mask.dim() == 3:
mask = mask.unsqueeze(1)
return mask
NODE_CLASS_MAPPINGS = {
"LGNoiseInjection": LGNoiseInjection,
"LGNoiseInjectionLatent": LGNoiseInjectionLatent,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LGNoiseInjection": "🎈LG Noise Injection",
"LGNoiseInjectionLatent": "🎈LG Noise Injection (Latent)",
}