Files
blepping-ComfyUI-bleh/py/latent_utils.py
T
blepping 347e7eee6c August '25 updates (#26)
* Previewer refactors
Support visual previews for ACE-Step
Allow setting dtype for previewer model
Allow scaling q/k/v input and output for Sage

* Start updating docs and example configuration files

* Add slice blend modes

* Rewrite blend mode system.
More blend modes.
Added BlehCFGInitSampler.
Basic Sparge Attention support.
Make samplers a bit more compatible with wrapping wrappers.

* Add probinject and probsubtract_b blend modes
Add BlehLatentAsImage and BlehImageAsLatent nodes

* Add BlehModelPatchFastTerminate node
Update docs/changelog
Add cosinesimilarity blend modes (basically SLERP)

* Remove some dead code
2025-08-09 05:27:34 -06:00

1713 lines
54 KiB
Python

# Credits:
# Blending, slice and filtering functions based on https://github.com/WASasquatch/FreeU_Advanced
from __future__ import annotations
import math
import os
from functools import partial
from typing import ClassVar
import kornia.filters as kf
import numpy as np
import torch
import torch.nn.functional as nnf
from torch import FloatTensor, LongTensor, fft
OVERRIDE_NO_SCALE = "COMFYUI_BLEH_OVERRIDE_NO_SCALE" in os.environ
USE_ORIG_NORMALIZE = "COMFYUI_BLEH_ORIG_NORMALIZE" in os.environ
def normalize_orig(latent, target_min=None, target_max=None, **_unused_kwargs: dict):
min_val = latent.min()
max_val = latent.max()
if target_min is None:
target_min = min_val
if target_max is None:
target_max = max_val
normalized = (latent - min_val) / (max_val - min_val)
return normalized * (target_max - target_min) + target_min
def normalize(latent, *, reference_latent=None, dim=(-3, -2, -1)):
if reference_latent is None:
return latent
min_val, max_val = (
latent.amin(dim=dim, keepdim=True),
latent.amax(dim=dim, keepdim=True),
)
target_min, target_max = (
reference_latent.amin(dim=dim, keepdim=True),
reference_latent.amax(dim=dim, keepdim=True),
)
normalized = (latent - min_val) / (max_val - min_val)
return normalized * (target_max - target_min) + target_min
if USE_ORIG_NORMALIZE:
normalize = normalize_orig
def normalize_to_scale(
latent: torch.Tensor,
target_min: float,
target_max: float,
*,
dim=(-3, -2, -1),
eps: float = 1e-07,
) -> torch.Tensor:
min_val, max_val = (
latent.amin(dim=dim, keepdim=True),
latent.amax(dim=dim, keepdim=True),
)
normalized = latent - min_val
normalized /= (max_val - min_val).add_(eps)
return (
normalized.mul_(target_max - target_min)
.add_(target_min)
.clamp_(target_min, target_max)
)
def hslerp(a, b, t):
if a.shape != b.shape:
raise ValueError("Input tensors a and b must have the same shape.")
num_channels = a.size(1)
interpolation_tensor = torch.zeros(
1,
num_channels,
1,
1,
device=a.device,
dtype=a.dtype,
)
interpolation_tensor[0, 0, 0, 0] = 1.0 if t < 0.5 else -1.0
result = (1 - t) * a + t * b
result += (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor
return result
# This may be far from correct.
def hslerp_alt(a, b, t):
if a.shape != b.shape:
raise ValueError("Input tensors a and b must have the same shape.")
interp = torch.zeros(
1,
a.size(1),
device=a.device,
dtype=a.dtype,
)
interp[0, 0] = 1.0
result = (1 - t) * a + t * b
norm = (torch.norm(b - a, dim=1, keepdim=True) / 6) * interp
norm[t.broadcast_to(norm.shape) < 0.5] *= -1
return result.add_(norm)
# This should be more correct but the results are worse. :(
def hslerp_alt2(a, b, t, *, sign_order=(1.0, -1.0), sign_threshold=0.5):
if a.shape != b.shape:
raise ValueError("Input tensors a and b must have the same shape.")
t_expanded = t.broadcast_to(a.shape[-2:])
while t_expanded.ndim < a.ndim:
t_expanded = t_expanded.unsqueeze(0)
return (
((1 - t) * a)
.add_(t * b)
.add_(
torch.norm(b - a, dim=1, keepdim=True).div_(6)
* torch.where(t_expanded.abs() < sign_threshold, *sign_order),
)
)
# Copied from ComfyUI
def slerp_orig(b1, b2, r):
c = b1.shape[-1]
# norms
b1_norms = torch.norm(b1, dim=-1, keepdim=True)
b2_norms = torch.norm(b2, dim=-1, keepdim=True)
# normalize
b1_normalized = b1 / b1_norms
b2_normalized = b2 / b2_norms
# zero when norms are zero
b1_normalized[b1_norms.expand(-1, c) == 0.0] = 0.0
b2_normalized[b2_norms.expand(-1, c) == 0.0] = 0.0
# slerp
dot = (b1_normalized * b2_normalized).sum(1)
omega = torch.acos(dot)
so = torch.sin(omega)
# technically not mathematically correct, but more pleasing?
res = (torch.sin((1.0 - r.squeeze(1)) * omega) / so).unsqueeze(
1,
) * b1_normalized + (torch.sin(r.squeeze(1) * omega) / so).unsqueeze(
1,
) * b2_normalized
res *= (b1_norms * (1.0 - r) + b2_norms * r).expand(-1, c)
# edge cases for same or polar opposites
res[dot > 1 - 1e-5] = b1[dot > 1 - 1e-5]
res[dot < 1e-5 - 1] = (b1 * (1.0 - r) + b2 * r)[dot < 1e-5 - 1]
return res
# From https://gist.github.com/Birch-san/230ac46f99ec411ed5907b0a3d728efa
def altslerp( # noqa: PLR0914
v0: FloatTensor,
v1: FloatTensor,
t: float | FloatTensor,
*,
dot_threshold=0.9995,
dim=-1,
):
# Normalize the vectors to get the directions and angles
v0_norm: FloatTensor = torch.linalg.norm(v0, dim=dim)
v1_norm: FloatTensor = torch.linalg.norm(v1, dim=dim)
v0_normed: FloatTensor = v0 / v0_norm.unsqueeze(dim)
v1_normed: FloatTensor = v1 / v1_norm.unsqueeze(dim)
# Dot product with the normalized vectors
dot: FloatTensor = (v0_normed * v1_normed).sum(dim)
dot_mag: FloatTensor = dot.abs()
# if dp is NaN, it's because the v0 or v1 row was filled with 0s
# If absolute value of dot product is almost 1, vectors are ~colinear, so use lerp
gotta_lerp: LongTensor = dot_mag.isnan() | (dot_mag > dot_threshold)
can_slerp: LongTensor = ~gotta_lerp
t_batch_dim_count: int = (
max(0, t.dim() - v0.dim()) if isinstance(t, torch.Tensor) else 0
)
t_batch_dims: torch.Size = (
t.shape[:t_batch_dim_count] if isinstance(t, torch.Tensor) else torch.Size([])
)
out: FloatTensor = torch.zeros_like(v0.expand(*t_batch_dims, *(dim,) * v0.dim()))
# if no elements are lerpable, our vectors become 0-dimensional, preventing broadcasting
if gotta_lerp.any():
lerped: FloatTensor = torch.lerp(v0, v1, t)
out: FloatTensor = lerped.where(gotta_lerp.unsqueeze(dim), out)
# if no elements are slerpable, our vectors become 0-dimensional, preventing broadcasting
if can_slerp.any():
# Calculate initial angle between v0 and v1
theta_0: FloatTensor = dot.arccos().unsqueeze(dim)
sin_theta_0: FloatTensor = theta_0.sin()
# Angle at timestep t
theta_t: FloatTensor = theta_0 * t
sin_theta_t: FloatTensor = theta_t.sin()
# Finish the slerp algorithm
s0: FloatTensor = (theta_0 - theta_t).sin() / sin_theta_0
s1: FloatTensor = sin_theta_t / sin_theta_0
slerped: FloatTensor = s0 * v0 + s1 * v1
out: FloatTensor = slerped.where(can_slerp.unsqueeze(dim), out)
return out
def stochasistic_blend(
a,
b,
t,
*,
cpu=False,
fuzz=0.1,
clamp_t: bool | tuple = True,
blend=torch.lerp,
):
if not isinstance(t, torch.Tensor):
t = torch.tensor((t,), dtype=a.dtype, device=a.device)
t_orig = t
t = t.broadcast_to(a.shape)
tadj = torch.rand(
*t.shape,
dtype=a.dtype,
layout=a.layout,
device="cpu" if cpu else a.device,
)
if tadj.device != a.device:
tadj = tadj.to(a.device)
tadj = tadj.mul_(fuzz * 2).sub_(fuzz)
tadj += t
if isinstance(clamp_t, tuple):
tadj = tadj.clamp_(*clamp_t)
elif clamp_t:
tmin, tmax = t_orig.aminmax()
tadj = tadj.clamp_(min(0, tmin), max(1.0, tmax))
return blend(a, b, tadj)
def gaussian_smoothing(
t: torch.Tensor,
kernel_size,
sigma: float | tuple | list,
) -> torch.Tensor:
if not isinstance(kernel_size, (list, tuple)):
kernel_size = (kernel_size,)
if not isinstance(sigma, (list, tuple)):
sigma = (sigma,)
ndim = t.ndim
ts = t.shape
if ndim == 1:
gk = kf.kernels.gaussian(
kernel_size[0],
torch.tensor(sigma, dtype=t.dtype, device=t.device),
device=t.device,
dtype=t.dtype,
)[None, None, ...]
return nnf.conv2d(
t[None, None, None, ...],
gk,
padding=(0, gk.numel() // 2),
).view(t.numel())
if ndim == 2:
t = t[None, None, ...]
elif ndim == 3:
t = t[None, ...]
elif ndim == 5:
t = t.reshape(ts[0], ts[1] * ts[2], *ts[3:])
elif ndim != 4:
raise ValueError("Can't handle tensor shape")
if len(kernel_size) == 1:
kernel_size = kernel_size * 2 # noqa: PLR6104
if len(sigma) == 1:
sigma = sigma * 2 # noqa: PLR6104
result = kf.gaussian_blur2d(t, kernel_size, sigma)
if ndim == 5:
return result.reshape(*ts)
while result.ndim > ndim and result.shape[0] == 1:
result = result.squeeze(0)
return result
class ProbBlend:
@staticmethod
def output(a: torch.Tensor, b: torch.Tensor, b_t: torch.Tensor) -> torch.Tensor:
return torch.where(b_t.to(device=a.device, dtype=torch.bool), b, a)
def __call__(
self,
a,
b,
t,
*,
cpu=False,
collapse_dims=(),
**kwargs: dict,
):
t_device = torch.device("cpu") if cpu else a.device
if not isinstance(t, torch.Tensor):
t = torch.tensor((t,), dtype=a.dtype, device=t_device)
elif t.device != t_device:
t = t.detach().clone().to(t_device)
tmin, tmax = t.aminmax()
tmin, tmax = min(tmin, 0.0), max(tmax, 1.0)
t = t - tmin # noqa: PLR6104
tdiv = tmax - tmin
if tdiv != 0:
t /= tdiv
if collapse_dims:
dims = a.ndim
prob_shape = list(a.shape)
for didx in collapse_dims:
if didx >= dims:
continue
prob_shape[didx] = 1
else:
prob_shape = a.shape
t = torch.bernoulli(t.clamp_(0, 1).broadcast_to(prob_shape)).to(a)
return self.output(a, b, t, **kwargs)
class ProbBlendSmoothed(ProbBlend):
@staticmethod
def output(
a: torch.Tensor,
b: torch.Tensor,
b_t: torch.Tensor,
*,
output_blend=torch.lerp,
kernel_size: int | tuple | list = 3,
sigma: float | tuple | list = 1.0,
) -> torch.Tensor:
t = b_t.to(device=a.device, dtype=a.dtype)
t = gaussian_smoothing(t, kernel_size, sigma)
return output_blend(a, b, t)
prob_blend = ProbBlend()
prob_blend_smoothed = ProbBlendSmoothed()
# Originally referenced from https://github.com/54rt1n/ComfyUI-DareMerge
# Doesn't handle non-scalar t very well.
def gradient_blend_(
a: torch.Tensor,
b: torch.Tensor,
t: float | torch.Tensor,
*,
dim=-1,
scaling_constant=0.9,
blend_function=torch.lerp,
) -> torch.Tensor:
dim = max(0, min(a.ndim - 1, a.ndim + dim if dim < 0 else dim))
if not isinstance(t, torch.Tensor):
t = a.new_full((1,), t)
if t.ndim > 0 and t.numel() > 1:
t = t.broadcast_to(a.shape).mean(dim=dim, keepdim=True)
count = a.shape[dim]
peak_idx = int(count * (1 - t))
ratios = a.new_zeros(count)
torch.arange(peak_idx, out=ratios[:peak_idx]).div_(peak_idx)
torch.arange(count - peak_idx - 1, -1, -1, out=ratios[peak_idx:]).div_(
count - peak_idx,
)
if scaling_constant != 1:
ratios *= scaling_constant
ratios = ratios.view(tuple(1 if i != dim else -1 for i in range(a.ndim)))
return blend_function(a, b, ratios)
def gradient_blend(
a: torch.Tensor,
b: torch.Tensor,
t: float | torch.Tensor,
*,
flatten_start_dim=1,
scaling_constant=0.9,
blend_function=torch.lerp,
) -> torch.Tensor:
shape = a.shape
# print("\nBLEND:", t)
if isinstance(t, torch.Tensor) and t.ndim > 0 and t.numel() > 1:
t = t.mean()
if a.ndim > 2:
a = a.flatten(start_dim=flatten_start_dim)
b = b.flatten(start_dim=flatten_start_dim)
count = a.shape[-1]
peak_idx = int(count * (1 - t))
ratios = a.new_zeros(count)
torch.arange(peak_idx, out=ratios[:peak_idx]).div_(peak_idx)
torch.arange(count - peak_idx - 1, -1, -1, out=ratios[peak_idx:]).div_(
count - peak_idx,
)
if scaling_constant != 1:
ratios *= scaling_constant
result = blend_function(a, b, ratios)
if result.shape != shape:
return result.reshape(*shape).contiguous()
return result
def slice_blend(
a: torch.Tensor,
b: torch.Tensor,
t: float | torch.Tensor,
*,
flatten=True,
dim=1,
flip_a=False,
flip_b=False,
flip_out=False,
) -> torch.Tensor:
if isinstance(t, torch.Tensor):
t = t.mean().clamp(0, 1)
else:
t = a.new_full((1,), t).clamp(0, 1)
if t == 0:
return a
if t == 1:
return b
orig_shape = a.shape
if a.ndim > 2 and flatten:
a = a.flatten(start_dim=dim)
b = b.flatten(start_dim=dim)
elsb = int(a.shape[dim] * t)
elsa = a.shape[dim] - elsb
astart, aend = (None, elsa) if not flip_a else (a.shape[dim] - elsa, None)
bstart, bend = (None, elsb) if flip_b else (a.shape[dim] - elsb, None)
aslice = tuple(
slice(None) if i != dim else slice(astart, aend) for i in range(a.ndim)
)
bslice = tuple(
slice(None) if i != dim else slice(bstart, bend) for i in range(a.ndim)
)
achunk, bchunk = a[aslice], b[bslice]
result = torch.cat((bchunk, achunk) if flip_out else (achunk, bchunk), dim=dim)
return result.reshape(orig_shape)
def slice_blend_smooth( # noqa: PLR0914
a: torch.Tensor,
b: torch.Tensor,
t: float | torch.Tensor,
*,
flatten: bool = True,
dim: int = 1,
fade_percent_l: float = 0.1,
fade_percent_r: float = 0.1,
always_fade: bool = False,
b_start_percent: float = 1.0,
b_blend_max: float = 1.0,
invert: bool = False, # Doesn't work propertly at the moment.
blend_function=torch.lerp,
) -> torch.Tensor:
if isinstance(t, torch.Tensor):
t = t.mean().clamp(0, 1)
else:
t = a.new_full((1,), t).clamp_(0, 1)
if invert:
t = 1 - t
b, a = a, b
if t == 0:
return a
b_start_percent = max(0.0, min(1.0, b_start_percent))
fade_percent_l = (
max(0.0, min(1.0, fade_percent_l))
if b_start_percent > 0 and not always_fade
else 0.0
)
fade_percent_r = (
max(0.0, min(1.0, fade_percent_r))
if b_start_percent < 1 and not always_fade
else 0.0
)
fade_mul = 1.0 / max(1.0, fade_percent_l + fade_percent_r)
orig_shape = a.shape
if flatten and dim < a.ndim - 1:
a = a.flatten(start_dim=dim)
b = b.flatten(start_dim=dim)
dim_els = a.shape[dim]
els_b = int(dim_els * t)
if invert:
els_b += int((dim_els - els_b) * (fade_percent_l + fade_percent_r) * fade_mul)
els_a = dim_els - els_b
b_start = int(els_a * b_start_percent)
b_end = b_start + els_b
elslfade, elsrfade = (
int(els_b * fade_percent_l * fade_mul),
int(els_b * fade_percent_r * fade_mul),
)
blend_mask = a.new_zeros(dim_els)
blend_mask[b_start:b_end] = b_blend_max
if elslfade > 0:
blend_mask[b_start : b_start + elslfade] = torch.linspace(
0.0,
b_blend_max,
steps=elslfade + 2,
device=blend_mask.device,
dtype=blend_mask.dtype,
)[1:-1]
if elsrfade > 0:
rfade_start = b_end - elsrfade
blend_mask[rfade_start : rfade_start + elsrfade] = torch.linspace(
b_blend_max,
0.0,
steps=elsrfade + 2,
device=blend_mask.device,
dtype=blend_mask.dtype,
)[1:-1]
blend_mask = blend_mask.view(
tuple(dim_els if d == dim else 1 for d in range(a.ndim)),
)
return blend_function(a, b, blend_mask).reshape(orig_shape)
def lop_lerp(
a: torch.Tensor,
b: torch.Tensor,
t: torch.tensor | float,
*,
a_ratio=1.0,
b_ratio=1.0,
):
if not isinstance(t, torch.Tensor):
t = a.new_full((1,), t)
return (a_ratio - t.clamp(max=a_ratio)).mul(a).add_(b * (t * b_ratio))
# # Thanks, ChatGPT though you did get the ratio reversed.
def cosine_similarity_blend_chatgpt_orig(
b: torch.Tensor,
a: torch.Tensor,
ratio: float | torch.Tensor,
*,
dim: int = -1,
eps: float = 1e-6,
) -> torch.Tensor:
a_n = a / (a.norm(dim=dim, keepdim=True).clamp_min(eps))
b_n = b / (b.norm(dim=dim, keepdim=True).clamp_min(eps))
c = torch.sum(a_n * b_n, dim=dim, keepdim=True)
s = 2 * ratio - 1
if not torch.is_tensor(s):
s = a.new_tensor(s)
a_ = 1 - c
alpha = a_ * (a_ - 2 * s**2)
beta = 2 * a_ * (c + s**2)
gamma = c**2 - s**2
disc = beta**2 - 4 * alpha * gamma
disc = disc.clamp_min(0.0)
sqrt_disc = torch.sqrt(disc)
lam1 = (-beta + sqrt_disc) / (2 * alpha).clamp_min(eps)
lam2 = (-beta - sqrt_disc) / (2 * alpha).clamp_min(eps)
lam = torch.where((lam1 >= 0) & (lam1 <= 1), lam1, lam2)
lam = torch.where((lam >= 0) & (lam <= 1), lam, ratio)
return torch.lerp(b, a, lam.expand_as(a))
def cosine_similarity_blend_chatgpt( # noqa: PLR0914
a: torch.Tensor,
b: torch.Tensor,
ratio: float,
*,
dim: int = -1,
eps: float = 1e-8,
small_angle: float = 1e-4,
opp_eps: float = 1e-6,
) -> torch.Tensor:
# --- normalize directions ---
mag_a = a.norm(dim=dim, keepdim=True).clamp_min(eps)
mag_b = b.norm(dim=dim, keepdim=True).clamp_min(eps)
a_n = a / mag_a
b_n = b / mag_b
# cosine & angle between a and b
cos_ab = (a_n * b_n).sum(dim=dim, keepdim=True).clamp(-1.0, 1.0)
theta = torch.acos(cos_ab)
# map blend ratio -> fraction along the arc
# we want angle from a -> out = theta * t
t = ratio if torch.is_tensor(ratio) else a.new_tensor(ratio)
# handle exact-opposite case: fallback to lerp then renorm
opp_mask = torch.abs(cos_ab + 1) < opp_eps
if opp_mask.any():
# simple normalized lerp + renormalize
lerp_dir = (1 - t) * a_n + t * b_n
lerp_dir /= lerp_dir.norm(dim=dim, keepdim=True).clamp_min(eps)
# magnitude later will apply
a_n = torch.where(opp_mask, lerp_dir, a_n)
b_n = torch.where(opp_mask, b_n, b_n) # no-op but keeps shapes aligned
theta = torch.where(
opp_mask,
torch.acos((a_n * b_n).sum(dim=dim, keepdim=True)),
theta,
)
# for very small angles, do lerp+renormalize
lerp_mask = theta < small_angle
if lerp_mask.any():
lerp_dir = (1 - t) * a_n + t * b_n
lerp_dir /= lerp_dir.norm(dim=dim, keepdim=True).clamp_min(eps)
# override only where theta is small
a_n = torch.where(lerp_mask, lerp_dir, a_n)
b_n = torch.where(lerp_mask, b_n, b_n)
cos_ab = (a_n * b_n).sum(dim=dim, keepdim=True).clamp(-1, 1)
theta = torch.acos(cos_ab)
# now true SLERP coefficients
sin_theta = torch.sin(theta).clamp_min(eps)
coef_a = torch.sin((1 - t) * theta) / sin_theta
coef_b = torch.sin(t * theta) / sin_theta
dir_out = coef_a * a_n + coef_b * b_n
# --- geometric magnitude interpolation ---
log_a = torch.log(mag_a)
log_b = torch.log(mag_b)
log_out = (1 - t) * log_a + t * log_b
mag_out = torch.exp(log_out)
return dir_out * mag_out
def cosine_similarity_blend_deepseek( # noqa: PLR0914
a: torch.Tensor,
b: torch.Tensor,
ratio: float,
*,
dim: int = -1,
eps=1e-08,
threshold=1e-06,
) -> torch.Tensor:
if not torch.is_tensor(ratio):
ratio = a.new_tensor(ratio)
# Compute magnitudes of a and b along the specified dimension
mag_a = torch.norm(a, p=2, dim=dim, keepdim=True).add_(eps)
mag_b = torch.norm(b, p=2, dim=dim, keepdim=True).add_(eps)
# Avoid division by zero during normalization
a_norm = a / mag_a
b_norm = b / mag_b
# Compute cosine similarity (dot product of normalized vectors)
d = (a_norm * b_norm).sum(dim=dim, keepdim=True).clamp_(-1.0, 1.0)
# Compute angle between a_norm and b_norm
theta = torch.acos(d)
# Calculate desired cosine similarity with b (s_b) from blend ratio
s_b = (2.0 * ratio - 1.0).clamp_(-1.0, 1.0)
# Compute angle from result to b based on s_b
angle_from_b = torch.acos(s_b)
# Calculate interpolation parameter t_val
t_val = (1.0 - angle_from_b / theta).clamp_(0.0, 1.0)
# Precompute sin_theta for slerp
sin_theta = torch.sin(theta)
# Linear interpolation fallback for small sin_theta
linear_part_norm = torch.lerp(a_norm, b_norm, t_val)
# linear_part_norm = (1.0 - t_val) * a_norm + t_val * b_norm
# Slerp computation
sin_t_theta = torch.sin(t_val * theta)
sin_comp_theta = torch.sin((1.0 - t_val) * theta)
slerp_denom = sin_theta + eps # Avoid division by zero
slerp_part_norm = (sin_comp_theta / slerp_denom) * a_norm + (
sin_t_theta / slerp_denom
) * b_norm
# Choose slerp unless sin_theta is too small (use linear then)
v_norm = torch.where(sin_theta < threshold, linear_part_norm, slerp_part_norm)
# Linearly interpolate magnitude
mag = torch.lerp(mag_a, mag_b, ratio)
# Scale normalized vector by interpolated magnitude
return v_norm * mag
DEFAULT_COSINE_SIMILARITY_BLEND_BACKEND = "chatgpt"
COSINE_SIMILARITY_BLEND_BACKENDS = {
"altslerp": altslerp,
"deepseek": cosine_similarity_blend_deepseek,
"chatgpt": cosine_similarity_blend_chatgpt,
"chatgpt_orig": cosine_similarity_blend_chatgpt_orig,
}
def cosine_similarity_blend(
a: torch.Tensor,
b: torch.Tensor,
t: float | torch.Tensor,
*args: list,
backend=DEFAULT_COSINE_SIMILARITY_BLEND_BACKEND,
**kwargs: dict,
) -> torch.Tensor:
fun = COSINE_SIMILARITY_BLEND_BACKENDS.get(backend)
if fun is None:
errstr = f"Bad cosine similarity blend backend {backend}, must be one of {tuple(COSINE_SIMILARITY_BLEND_BACKENDS)}"
raise ValueError(errstr)
return fun(a, b, t, *args, **kwargs)
def cosine_similarity_blend_avg(
a: torch.Tensor,
b: torch.Tensor,
t: float | torch.Tensor,
*,
backend=DEFAULT_COSINE_SIMILARITY_BLEND_BACKEND,
dims=(-1, -2),
) -> torch.Tensor:
blend_fun = partial(cosine_similarity_blend, backend=backend)
multiplier = 1.0 / len(dims)
result = None
for dim in dims:
curr_result = blend_fun(a, b, t, dim=dim).mul_(multiplier)
result = curr_result if result is None else result.add_(curr_result)
return result
def cosine_similarity_blend_flat(
a: torch.Tensor,
b: torch.Tensor,
t: float | torch.Tensor,
*,
start_dim=0,
end_dim=1,
backend=DEFAULT_COSINE_SIMILARITY_BLEND_BACKEND,
) -> torch.Tensor:
if a.shape != b.shape:
raise ValueError("Tensor shape mismatch, a and b must be the same shape")
if start_dim < 0:
start_dim = a.ndim + start_dim
if end_dim < 0:
end_dim = a.ndim + end_dim
if start_dim < 0 or end_dim < 0 or start_dim >= a.ndim or end_dim >= a.ndim:
raise ValueError("Bad start/end_dim parameters")
orig_shape = a.shape
a = a.flatten(start_dim=start_dim, end_dim=end_dim)
b = b.flatten(start_dim=start_dim, end_dim=end_dim)
if isinstance(t, torch.Tensor) and t.ndim == len(orig_shape):
t = t.flatten(start_dim=start_dim, end_dim=end_dim)
return cosine_similarity_blend(a, b, t, dim=start_dim, backend=backend).reshape(
orig_shape,
)
def blend_blend(
a: torch.Tensor,
b: torch.Tensor,
t: float | torch.Tensor,
*,
blend_mode_a: str = "lerp",
blend_mode_b="cosinesimilarity_flat_spatdims",
blend_blend: float | torch.Tensor = 0.5,
blend_mode_blend: str = "lerp",
blend_a_kwargs: dict | None = None,
blend_b_kwargs: dict | None = None,
blend_blend_kwargs: dict | None = None,
) -> torch.Tensor:
fun_a = BLENDING_MODES[blend_mode_a]
fun_b = BLENDING_MODES[blend_mode_b]
fun_blend = BLENDING_MODES[blend_mode_blend]
if not torch.is_tensor(blend_blend):
blend_blend = a.new_tensor(blend_blend)
return fun_blend(
fun_a(a, b, t, **({} if blend_a_kwargs is None else blend_a_kwargs)),
fun_b(a, b, t, **({} if blend_b_kwargs is None else blend_b_kwargs)),
blend_blend,
**({} if blend_blend_kwargs is None else blend_blend_kwargs),
)
class BlendMode:
__slots__ = (
"allow_scale",
"f",
"f_kwargs",
"f_raw",
"force_rescale",
"norm",
"norm_dims",
"rescale_dims",
"rev",
)
class _Empty:
pass
def __init__( # noqa: PLR0917
self,
f,
norm=None,
norm_dims=(-3, -2, -1),
rev=False,
allow_scale=True,
rescale_dims=(-3, -2, -1),
force_rescale=False,
**kwargs: dict,
):
self.f_raw = f
self.f = f if not kwargs else partial(f, **kwargs)
self.f_kwargs = kwargs
if norm is True:
norm = normalize
elif norm is False:
norm = None
self.norm = norm
self.norm_dims = norm_dims
self.rev = rev
self.allow_scale = allow_scale
self.rescale_dims = rescale_dims
self.force_rescale = force_rescale
def edited(
self,
*,
f=_Empty,
norm=_Empty,
norm_dims=_Empty,
rev=_Empty,
allow_scale=_Empty,
rescale_dims=_Empty,
force_rescale=_Empty,
preserve_kwargs=True,
**kwargs: dict,
) -> object:
empty = self._Empty
kwargs = (self.f_kwargs | kwargs) if preserve_kwargs else kwargs
return self.__class__(
f if f is not empty else self.f_raw,
norm=norm if norm is not empty else self.norm,
norm_dims=norm_dims if norm_dims is not empty else self.norm_dims,
rev=rev if rev is not empty else self.rev,
allow_scale=allow_scale if allow_scale is not empty else self.allow_scale,
rescale_dims=rescale_dims
if rescale_dims is not empty
else self.rescale_dims,
force_rescale=force_rescale
if force_rescale is not empty
else self.force_rescale,
**kwargs,
)
def rescale(self, t, *, rescale_dims=_Empty):
if t.ndim > 2:
rescale_dims = (
self.rescale_dims if rescale_dims is self._Empty else rescale_dims
)
else:
# Meh.
rescale_dims = -1
tmin = torch.amin(t, keepdim=True, dim=rescale_dims)
tmax = torch.amax(t, keepdim=True, dim=rescale_dims)
return (t - tmin).div_(tmax - tmin).clamp_(0, 1), tmin, tmax
def __call__(self, a, b, t, *, norm_dims=_Empty) -> torch.Tensor:
if not self.force_rescale:
return self.__call__internal(a, b, t, norm_dims=norm_dims)
a, amin, amax = self.rescale(a)
b, bmin, bmax = self.rescale(b)
result = self.__call__internal(a, b, t, norm_dims=norm_dims)
del a, b
rmin, rmax = torch.lerp(amin, bmin, 0.5), torch.lerp(amax, bmax, 0.5)
del amin, amax, bmin, bmax
return result.mul_(rmax.sub_(rmin)).add_(rmin)
def __call__internal(self, a, b, t, *, norm_dims=_Empty) -> torch.Tensor:
if not isinstance(t, torch.Tensor) and isinstance(a, torch.Tensor):
t = a.new_full((1,), t)
if self.rev:
a, b = b, a
if self.norm is None:
return self.f(a, b, t)
return self.norm(
self.f(a, b, t),
reference_latent=torch.lerp(a, b, t),
dim=self.norm_dims if norm_dims is self._Empty else norm_dims,
)
class BlendingModes:
def __init__(self, builtins=None):
self.builtins = {} if builtins is None else builtins
self.cache = {}
def get(self, k: str, default=None):
result = self.builtins.get(k)
if result is not None:
return result
result = self.cache.get(k)
if result is not None:
return result
return self.try_extended(k, default=default)
_simple_value_map: ClassVar = {
"true": True,
"false": False,
"()": (),
"none": None,
}
def parse_value(self, k: str, v: str):
k = k.strip().lower()
v = v.strip()
if not v:
raise ValueError("Empty value")
if len(v) > 1 and v[0] == "^":
literal_mode = True
v = v[1:]
else:
literal_mode = False
vl = v.lower()
result = self._simple_value_map.get(vl, vl)
if result is not vl:
return result
v0 = v[0]
if v0.isdigit() or v0 in "-+":
if "," in v:
result = tuple(
self.parse_value(k, subv)
for subv in (_subv for _subv in v.split(",") if _subv.strip())
)
if len(result) > 1 and not all(
subv.__class__ is result[0].__class__ for subv in result[1:]
):
errstr = f"Mismatched items in list for key {k}"
raise ValueError(errstr)
return result
return float(v) if "." in v else int(v)
if not literal_mode and k.startswith("blend"):
# It won't be a numeric value here.
result = self.builtins.get(v)
if result is None:
errstr = f"Unknown blend mode {v}"
raise ValueError(errstr)
return result
return v
def parse_arg(self, s: str, idx: int) -> tuple:
kv = s.split("=", 1)
if len(kv) != 2:
errstr = f"Failed to parse argument at position {idx}"
raise ValueError(errstr)
k, v = kv[0].strip(), kv[1].strip()
if not k:
errstr = f"Empty key at argument position {idx}"
raise ValueError(errstr)
try:
v_out = self.parse_value(k, v)
except ValueError as exc:
errstr = f"Parse failed at argument position {idx}: {exc}"
raise ValueError(errstr) from exc
return (k, v_out)
def try_extended(self, k: str, default=None) -> object:
if ":" not in k:
return default
name, *arglist = k.strip().split(":")
name = name.strip()
base_bm = self.builtins.get(name)
if base_bm is None:
errstr = f"Unknown mode {name} for extended blend specification"
raise ValueError(errstr)
bm_kwargs = dict(self.parse_arg(arg, idx) for idx, arg in enumerate(arglist))
bm = base_bm.edited(**bm_kwargs)
self.cache[k] = bm
return bm
def items(self):
return self.builtins.items()
def values(self):
return self.builtins.values()
def __contains__(self, k: str) -> bool:
return self.get(k) is not None
def __iter__(self):
return self.builtins.__iter__()
keys = __iter__
def __setitem__(self, k: str, v) -> str:
self.builtins[k] = v if isinstance(v, BlendMode) else BlendMode(v)
def __getitem__(self, k: str):
result = self.get(k)
if result is None:
raise KeyError(k)
return result
def __ior__(self, other: dict | object):
if isinstance(other, dict):
self.builtins |= {
k: v if isinstance(v, BlendMode) else BlendMode(v)
for k, v in other.items()
}
return self
self.builtins |= other.builtins
self.cache |= other.cache
return self
def __or__(self, other: dict | object) -> object:
clone = self.__class__()
clone.builtins = self.builtins.copy()
clone.cache = self.cache.copy()
if isinstance(other, dict):
clone.builtins |= {
k: v if isinstance(v, BlendMode) else BlendMode(v)
for k, v in other.items()
}
return clone
clone.builtins |= other.builtins
clone.cache |= other.cache
return clone
def copy(self):
return self | {}
BLENDING_MODES = {
# Args:
# - a (tensor): Latent input 1
# - b (tensor): Latent input 2
# - t (float): Blending factor
"a_only": BlendMode(lambda a, _b, t: a * t, allow_scale=False),
"b_only": BlendMode(lambda _a, b, t: b * t, allow_scale=False),
# Interpolates between tensors a and b using normalized linear interpolation.
"bislerp": BlendMode(
lambda a, b, t: ((1 - t) * a).add_(t * b),
normalize,
),
# "nbislerp": BlendMode(lambda a, b, t: (1 - t) * a + t * b, normalize),
"slerp": BlendMode(altslerp),
# Transfer the color from `b` to `a` by t` factor
"colorize": BlendMode(lambda a, b, t: (b - a).mul_(t).add_(a)),
# Interpolates between tensors a and b using cosine interpolation.
"cosinterp": BlendMode(
lambda a, b, t: (
(a + b).sub_((a - b).mul_(torch.cos(t * torch.tensor(math.pi))))
).div_(2),
),
# Interpolates between tensors a and b using cubic interpolation.
"cuberp": BlendMode(lambda a, b, t: (b - a).mul_(3 * t**2 - 2 * t**3).add_(a)),
# Interpolates between tensors a and b using normalized linear interpolation,
# with a twist when t is greater than or equal to 0.5.
"hslerp": BlendMode(hslerp),
"hslerpalt": BlendMode(hslerp_alt2),
"hslerpalt110x": BlendMode(hslerp_alt2, sign_order=(1.1, -1.1)),
"hslerpalt125x": BlendMode(hslerp_alt2, sign_order=(1.25, -1.25)),
"hslerpalt150x": BlendMode(hslerp_alt2, sign_order=(1.5, -1.5)),
"hslerpalt300x": BlendMode(hslerp_alt2, sign_order=(3.0, -3.0)),
"hslerpaltflipsign": BlendMode(hslerp_alt2, sign_order=(-1.0, 1.0)),
"hslerpaltflipsign110x": BlendMode(hslerp_alt2, sign_order=(-1.1, 1.1)),
"hslerpaltflipsign125x": BlendMode(hslerp_alt2, sign_order=(-1.25, 1.25)),
"hslerpaltflipsign150x": BlendMode(hslerp_alt2, sign_order=(-1.5, 1.5)),
"hslerpaltflipsign300x": BlendMode(hslerp_alt2, sign_order=(-3.0, 3.0)),
"problerp0.25": BlendMode(stochasistic_blend, fuzz=0.25),
"problerp0.1": BlendMode(stochasistic_blend, fuzz=0.1),
"problerp0.025": BlendMode(stochasistic_blend, fuzz=0.025),
"probselect": BlendMode(prob_blend),
"probselect_channels": BlendMode(prob_blend, collapse_dims=(1,)),
"probselectsmoothed": BlendMode(prob_blend_smoothed),
"probselectsmoothed_channels": BlendMode(
prob_blend_smoothed,
collapse_dims=(1,),
),
"probselectsmoothed_ks5": BlendMode(prob_blend_smoothed, kernel_size=5),
"probselectsmoothed_ks9": BlendMode(prob_blend_smoothed, kernel_size=9),
"probselectsmoothed_ks9_sigma3": BlendMode(
prob_blend_smoothed,
kernel_size=9,
sigma=3.0,
),
"probinject": BlendMode(
lambda a, b, t, **kwargs: prob_blend(torch.zeros_like(b), b, t, **kwargs).add_(
a,
),
),
"probsubtract_b": BlendMode(
lambda a, b, t, **kwargs: a - prob_blend(torch.zeros_like(b), b, t, **kwargs),
),
"gradient": BlendMode(gradient_blend),
# Adds tensor b to tensor a, scaled by t.
"inject": BlendMode(lambda a, b, t: (b * t).add_(a)),
"injecthalf": BlendMode(lambda a, b, t: (b * (t * 0.5)).add_(a)),
"injectquarter": BlendMode(lambda a, b, t: (b * (t * 0.25)).add_(a)),
"inject_difference": BlendMode(lambda a, b, t: (a - b).mul_(t).add_(a)),
"inject_copysign_a": BlendMode(lambda a, b, t: (b * t).add_(a).copysign_(a)),
"inject_copysign_b": BlendMode(lambda a, b, t: (b * t).add_(a).copysign_(b)),
"inject_avoidsign_a": BlendMode(lambda a, b, t: (b * t).add_(a).copysign_(a.neg())),
"inject_avoidsign_b": BlendMode(lambda a, b, t: (b * t).add_(a).copysign_(b.neg())),
# Interpolates between tensors a and b using linear interpolation.
# "lerp": BlendMode(lambda a, b, t: ((1.0 - t) * a).add_(t * b)),
"lerp": BlendMode(torch.lerp),
"lerp050x": BlendMode(lambda a, b, t: (((1 - t) * a).add_(t * b)).mul_(0.5)),
"lerp075x": BlendMode(lambda a, b, t: (((1 - t) * a).add_(t * b)).mul_(0.75)),
"lerp110x": BlendMode(lambda a, b, t: (((1 - t) * a).add_(t * b)).mul_(1.1)),
"lerp125x": BlendMode(lambda a, b, t: (((1 - t) * a).add_(t * b)).mul_(1.25)),
"lerp150x": BlendMode(lambda a, b, t: (((1 - t) * a).add_(t * b)).mul_(1.5)),
"lerp_copysign_a": BlendMode(
lambda a, b, t: ((1.0 - t) * a).add_(t * b).copysign_(a),
),
"lerp_copysign_b": BlendMode(
lambda a, b, t: ((1.0 - t) * a).add_(t * b).copysign_(b),
),
"lerp_avoidsign_a": BlendMode(
lambda a, b, t: ((1.0 - t) * a).add_(t * b).copysign_(a.neg()),
),
"lerp_avoidsign_b": BlendMode(
lambda a, b, t: ((1.0 - t) * a).add_(t * b).copysign_(b.neg()),
),
# Simulates a brightening effect by adding tensor b to tensor a, scaled by t.
"lineardodge": BlendMode(lambda a, b, t: (b * t).add_(a)),
"copysign": BlendMode(lambda a, b, _t: torch.copysign(a, b)),
"probcopysign": BlendMode(lambda a, b, t: torch.copysign(a, prob_blend(a, b, t))),
"slice_flat_d1": BlendMode(slice_blend, dim=1, flatten=True),
"slice_flat_d2": BlendMode(slice_blend, dim=2, flatten=True),
"slice_d1": BlendMode(slice_blend, dim=1, flatten=False),
"slice_d2": BlendMode(slice_blend, dim=2, flatten=False),
"slice_d3": BlendMode(slice_blend, dim=3, flatten=False),
"slice_d1_flip": BlendMode(
slice_blend,
dim=1,
flatten=False,
flip_a=True,
flip_b=True,
flip_out=True,
),
"slice_d2_flip": BlendMode(
slice_blend,
dim=2,
flatten=False,
flip_a=True,
flip_b=True,
flip_out=True,
),
"slice_d3_flip": BlendMode(
slice_blend,
dim=3,
flatten=False,
flip_a=True,
flip_b=True,
flip_out=True,
),
"slicesmooth_d1": BlendMode(slice_blend_smooth, dim=1, flatten=False),
"slicesmooth_d2": BlendMode(slice_blend_smooth, dim=2, flatten=False),
"slicesmooth_d3": BlendMode(slice_blend_smooth, dim=3, flatten=False),
"loplerp_a098": BlendMode(lop_lerp, a_ratio=0.98),
"loplerp_a101": BlendMode(lop_lerp, a_ratio=1.01),
"loplerp_a102": BlendMode(lop_lerp, a_ratio=1.02),
"loplerp_a105": BlendMode(lop_lerp, a_ratio=1.05),
"cosinesimilarity": BlendMode(
cosine_similarity_blend,
backend=DEFAULT_COSINE_SIMILARITY_BLEND_BACKEND,
),
"cosinesimilarity_flat": BlendMode(
cosine_similarity_blend_flat,
start_dim=1,
end_dim=-1,
backend=DEFAULT_COSINE_SIMILARITY_BLEND_BACKEND,
),
"cosinesimilarity_flat_spatdims": BlendMode(
cosine_similarity_blend_flat,
start_dim=-2,
end_dim=-1,
backend=DEFAULT_COSINE_SIMILARITY_BLEND_BACKEND,
),
"cosinesimilarity_avg_spatdims": BlendMode(
cosine_similarity_blend_avg,
dims=(-1, -2),
backend=DEFAULT_COSINE_SIMILARITY_BLEND_BACKEND,
),
"hybrid_lerp_cosinesimilarity": BlendMode(
blend_blend,
blend_mode_a="lerp",
blend_mode_b="cosinesimilarity_flat_spatdims",
blend_mode_blend="lerp",
blend_blend=0.5,
),
# Simulates a brightening effect by dividing a by (1 - b) with a small epsilon to avoid division by zero.
"colordodge": BlendMode(
lambda a, b, _t: a / (1 - b + 1e-6),
allow_scale=False,
force_rescale=True,
),
"difference": BlendMode(
lambda a, b, t: abs(a - b) * t,
# normalize,
allow_scale=False,
force_rescale=True,
),
"exclusion": BlendMode(
lambda a, b, t: (a + b - 2 * a * b) * t,
# normalize,
allow_scale=False,
force_rescale=True,
),
"glow": BlendMode(
lambda a, b, _t: torch.where(
a <= 1,
a**2 / (1 - b + 1e-6),
b * (a - 1) / (a + 1e-6),
),
allow_scale=False,
force_rescale=True,
),
"hardlight": BlendMode(
lambda a, b, t: (
2 * a * b * (a < 0.5).float()
+ (1 - 2 * (1 - a) * (1 - b)) * (a >= 0.5).float()
)
* t,
allow_scale=False,
force_rescale=True,
),
"linearlight": BlendMode(
lambda a, b, _t: torch.where(b <= 0.5, a + 2 * b - 1, a + 2 * (b - 0.5)),
force_rescale=True,
),
"multiply": BlendMode(
lambda a, b, t: (a * t).mul_(b * t),
normalize,
allow_scale=False,
),
"overlay": BlendMode(
lambda a, b, t: (2 * a * b + a**2 - 2 * a * b * a) * t
if torch.all(b < 0.5)
else (1 - 2 * (1 - a) * (1 - b)) * t,
allow_scale=False,
force_rescale=True,
),
# Combines tensors a and b using the Pin Light formula.
"pinlight": BlendMode(
lambda a, b, _t: torch.where(
b <= 0.5,
torch.min(a, 2 * b),
torch.max(a, 2 * b - 1),
),
force_rescale=True,
),
"reflect": BlendMode(
lambda a, b, _t: torch.where(
b <= 1,
b**2 / (1 - a + 1e-6),
a * (b - 1) / (b + 1e-6),
),
allow_scale=False,
force_rescale=True,
),
"screen": BlendMode(
lambda a, b, t: 1 - (1 - a) * (1 - b) * (1 - t),
allow_scale=False,
force_rescale=True,
),
"subtract": BlendMode(lambda a, b, t: a * t - b * t, allow_scale=False),
"subtract_b": BlendMode(lambda a, b, t: a - b * t, allow_scale=False),
"subtract_b_scaleup_a": BlendMode(
lambda a, b, t: a * (1.0 + t) - b * t,
allow_scale=False,
),
"vividlight": BlendMode(
lambda a, b, _t: torch.where(
b <= 0.5,
a / (1 - 2 * b + 1e-6),
(a + 2 * b - 1) / (2 * (1 - b) + 1e-6),
),
allow_scale=False,
force_rescale=True,
),
}
BLENDING_MODES |= {
f"norm{k}": v.edited(norm=normalize)
for k, v in BLENDING_MODES.items()
if k != "hslerp" and v.norm is None
}
BLENDING_MODES |= {f"rev{k}": v.edited(rev=True) for k, v in BLENDING_MODES.items()}
BLENDING_MODES = BlendingModes(BLENDING_MODES)
BIDERP_MODES = {
k: v.edited(norm_dims=0)
for k, v in BLENDING_MODES.items()
if (v.allow_scale or OVERRIDE_NO_SCALE) and not k.endswith("slerp")
}
BIDERP_MODES |= {
"hslerp": hslerp_alt,
"bislerp": slerp_orig,
"altbislerp": altslerp,
"revaltbislerp": lambda a, b, t: altslerp(b, a, t),
"bibislerp": BLENDING_MODES["bislerp"].edited(norm_dims=0),
"revhslerp": lambda a, b, t: hslerp_alt(b, a, t),
"revbislerp": lambda a, b, t: slerp_orig(b, a, t),
"revbibislerp": BLENDING_MODES["revbislerp"].edited(norm_dims=0),
}
FILTER_PRESETS = {
"none": (),
"bandpass": (
(5, 0.0), # Low-pass filter
(15, 1.0), # Pass-through filter (allows mid-range frequencies)
(25, 0.0), # High-pass filter
),
"lowpass": (
(
10,
1.0,
),
), # Allows low-frequency components, suppresses high-frequency components
"highpass": (
(
10,
0.0,
),
), # Suppresses low-frequency components, allows high-frequency components
"passthrough": ((10, 1.0),), # Passes all frequencies unchanged, no filtering
"gaussianblur": (
(
10,
0.5,
), # Blurs the image by allowing a range of frequencies with a Gaussian shape
),
"edge": (
(
10,
2.0,
),
), # Enhances edges and high-frequency features while suppressing low-frequency details
"sharpen": (
(
10,
1.5,
),
), # Increases the sharpness of the image by emphasizing high-frequency components
"multilowpass": ((5, 1.0), (10, 0.5), (15, 0.2)), # Multi-scale low-pass filter
"multihighpass": ((5, 0.0), (10, 0.5), (15, 0.8)), # Multi-scale high-pass filter
"multipassthrough": (
(5, 1.0),
(10, 1.0),
(15, 1.0),
), # Pass-through at different scales
"multigaussianblur": ((5, 0.5), (10, 0.8), (15, 0.2)), # Multi-scale Gaussian blur
"multiedge": ((5, 1.2), (10, 1.5), (15, 2.0)), # Multi-scale edge enhancement
"multisharpen": ((5, 1.5), (10, 2.0), (15, 2.5)), # Multi-scale sharpening
}
ENHANCE_METHODS = (
"lowpass",
"multilowpass",
"highpass",
"multihighpass",
"bandpass",
"randhilowpass",
"randmultihilowpass",
"randhibandpass",
"randlowbandpass",
"gaussianblur",
"multigaussianblur",
"edge",
"multiedge",
"sharpen",
"multisharpen",
"korniabilateralblur",
"korniagaussianblur",
"korniasharpen",
"korniaedge",
"korniarevedge",
"korniarandblursharp",
"renoise1",
"renoise2",
)
UPSCALE_METHODS = (
"bicubic",
"nearest-exact",
"bilinear",
"area",
"adaptive_avg_pool2d",
"adaptive_max_pool2d",
"fractional_max_pool2d",
"lp_pool2d_1",
"lp_pool2d_2",
"lp_pool2d_4",
*BIDERP_MODES.keys(),
*(
f"{meth}+{enh}"
for meth in ("bicubic", "bislerp", "hslerp", "random")
for enh in ENHANCE_METHODS
),
"random",
"randomaa",
)
RAND_UPSCALE_METHODS = (
"bicubic",
"colorize",
"bislerp",
"revcosinterp",
"bilinear",
)
FILTER_SIZES = (
np.array([1.0]),
np.array([1.0, 1.0]),
np.array([1.0, 2.0, 1.0]),
np.array([1.0, 3.0, 3.0, 1.0]),
np.array([1.0, 4.0, 6.0, 4.0, 1.0]),
np.array([1.0, 5.0, 10.0, 10.0, 5.0, 1.0]),
np.array([1.0, 6.0, 15.0, 20.0, 15.0, 6.0, 1.0]),
)
def make_filter(channels, dtype, size=3):
a = FILTER_SIZES[size - 1]
filt = torch.tensor(a[:, None] * a[None, :], dtype=dtype)
filt /= torch.sum(filt)
return filt[None, None, :, :].repeat((channels, 1, 1, 1))
def antialias_tensor(x, antialias_size):
channels = x.shape[1]
filt = make_filter(channels, x.dtype, antialias_size).to(x.device)
return nnf.conv2d(x, filt, groups=channels, padding="same")
def enhance_tensor( # noqa: PLR0911
x,
name,
scale=1.0,
sigma=None,
*,
skip_multiplier=1,
adjust_scale=True,
):
randitems = None
orig_scale = scale
randskip = 0
if name == "randmultihilowpass":
scale *= 0.1
randskip = 4
randitems = ("multilowpass", "multihighpass")
elif name == "randhilowpass":
scale *= 0.1
randskip = 6
randitems = ("lowpass", "highpass")
elif name == "randlowbandpass":
scale *= 0.25
randskip = 1
randitems = ("lowpass", "multilowpass", "bandpass")
elif name == "randhibandpass":
scale *= 0.25
randskip = 1
randitems = ("highpass", "multihighpass", "bandpass")
elif name == "bandpass":
scale *= 0.2
elif name in {"renoise1", "renoise2"}:
if sigma is None:
return x
noise_scale = (
min(sigma / 6.0, 2.0 / max(sigma, 1e-05))
if name == "renoise1"
else sigma / 8.0
)
if noise_scale < 1e-04:
return x
noise = torch.randn_like(x)
return noise.mul_(noise_scale).add_(x)
if not adjust_scale:
scale = orig_scale
randskip = int(randskip * skip_multiplier)
if randitems:
ridx = torch.randint(len(randitems) + randskip, (1,), device="cpu").item()
if ridx >= len(randitems):
return x
return enhance_tensor(x, randitems[ridx], scale=scale)
fpreset = FILTER_PRESETS.get(name)
if fpreset is not None:
if not adjust_scale:
scale *= 2
return ffilter(x, 1, 1.0, fpreset, 0.5 * scale)
if name == "korniabilateralblur":
return x + (kf.bilateral_blur(x, (3, 3), 0.1, (1.5, 1.5)) - x) * (scale * 2.0)
if name == "korniagaussianblur":
return kf.gaussian_blur2d(x, (3, 3), (1.5, 1.5)) * scale
if name == "korniasharpen":
return x + (kf.unsharp_mask(x, (3, 3), (1.5, 1.5)) - x) * (scale / 2.0)
if name in {"korniaedge", "korniarevedge"}:
blur = kf.bilateral_blur(x, (3, 3), 0.1, (1.5, 1.5)) - x
sharpened = kf.unsharp_mask(x, (3, 3), (1.5, 1.5)) - x
if name == "korniarevedge":
scale *= -1.0
return x + (sharpened + blur) * (scale / 2.0)
if name == "korniarandblursharp":
return enhance_tensor(
x,
"korniagaussianblur"
if torch.rand(1, device="cpu").item() < 0.5
else "korniasharpen",
scale=scale,
)
raise ValueError("Unknown enhancement")
@torch.no_grad()
def scale_samples(
samples,
width,
height,
*,
mode="bicubic",
mode_h=None,
antialias_size=0,
post_effect_strength=1.0,
sigma=None,
):
if mode_h is None:
mode_h = mode
mode, *enhancement = mode.split("+", 1)
mode_h = mode_h.split("+", 1)[0]
modes = (mode, mode_h)
if "randomaa" in modes:
raasize, useraa = torch.rand(2, device="cpu").detach()
antialias_size = (int(raasize * 7) + 1) * int(useraa * 2)
if "random" in modes or "randomaa" in modes:
ridxs = torch.randint(
len(RAND_UPSCALE_METHODS),
(2,),
dtype=torch.uint8,
).tolist()
mode, mode_h = (
m if mode not in {"random", "randomaa"} else RAND_UPSCALE_METHODS[ridx]
for ridx, m in zip(ridxs, (mode, mode_h))
)
mode_h = mode
if mode in {"bicubic", "nearest-exact", "bilinear", "area"}:
result = nnf.interpolate(
samples,
size=(height, width),
mode=mode,
antialias=antialias_size > 7,
)
elif mode == "adaptive_avg_pool2d":
result = nnf.adaptive_avg_pool2d(samples, (height, width))
elif mode == "adaptive_max_pool2d":
result = nnf.adaptive_max_pool2d(samples, (height, width))
elif mode == "fractional_max_pool2d":
h, w = samples.shape[-2:]
result = nnf.fractional_max_pool2d(
samples,
kernel_size=3,
output_ratio=(height / h, width / w),
)
elif mode.startswith("lp_pool2d_"):
h, w = samples.shape[-2:]
result = nnf.lp_pool2d(
samples,
float(mode.rsplit("_", 1)[1]),
kernel_size=(int(h // height), int(w // width)),
ceil_mode=True,
)
else:
result = biderp(samples, width, height, mode, mode_h)
if enhancement:
result = enhance_tensor(
result,
enhancement[-1],
scale=post_effect_strength,
sigma=sigma,
)
if antialias_size < 1 or antialias_size > 7:
return result
return antialias_tensor(result, antialias_size)
# Modified from ComfyUI
def biderp(samples, width, height, mode="bislerp", mode_h=None): # noqa: PLR0914
if mode_h is None:
mode_h = mode
derp_w, derp_h = (
BIDERP_MODES.get(mode, slerp_orig),
BIDERP_MODES.get(mode_h, slerp_orig),
)
def generate_bilinear_data(length_old, length_new, device):
coords_1 = torch.arange(length_old, dtype=torch.float32, device=device).reshape(
(1, 1, 1, -1),
)
coords_1 = nnf.interpolate(
coords_1,
size=(1, length_new),
mode="bilinear",
)
ratios = coords_1 - coords_1.floor()
coords_1 = coords_1.to(torch.int64)
coords_2 = (
torch.arange(length_old, dtype=torch.float32, device=device).reshape(
(1, 1, 1, -1),
)
+ 1
)
coords_2[:, :, :, -1] -= 1
coords_2 = nnf.interpolate(
coords_2,
size=(1, length_new),
mode="bilinear",
)
coords_2 = coords_2.to(torch.int64)
return ratios, coords_1, coords_2
orig_dtype = samples.dtype
samples = samples.float()
n, c, h, w = samples.shape
h_new, w_new = (height, width)
# linear w
ratios, coords_1, coords_2 = generate_bilinear_data(w, w_new, samples.device)
coords_1 = coords_1.expand((n, c, h, -1))
coords_2 = coords_2.expand((n, c, h, -1))
ratios = ratios.expand((n, 1, h, -1))
pass_1 = samples.gather(-1, coords_1).movedim(1, -1).reshape((-1, c))
pass_2 = samples.gather(-1, coords_2).movedim(1, -1).reshape((-1, c))
ratios = ratios.movedim(1, -1).reshape((-1, 1))
result = derp_w(pass_1, pass_2, ratios)
result = result.reshape(n, h, w_new, c).movedim(-1, 1)
# linear h
ratios, coords_1, coords_2 = generate_bilinear_data(h, h_new, samples.device)
coords_1 = coords_1.reshape((1, 1, -1, 1)).expand((n, c, -1, w_new))
coords_2 = coords_2.reshape((1, 1, -1, 1)).expand((n, c, -1, w_new))
ratios = ratios.reshape((1, 1, -1, 1)).expand((n, 1, -1, w_new))
pass_1 = result.gather(-2, coords_1).movedim(1, -1).reshape((-1, c))
pass_2 = result.gather(-2, coords_2).movedim(1, -1).reshape((-1, c))
ratios = ratios.movedim(1, -1).reshape((-1, 1))
result = derp_h(pass_1, pass_2, ratios)
result = result.reshape(n, h_new, w_new, c).movedim(-1, 1)
return result.to(orig_dtype)
def ffilter(x, threshold, scale, scales=None, strength=1.0):
# FFT
if isinstance(x, list):
x = x[0]
if not isinstance(x, torch.Tensor):
raise TypeError("Expected tensor")
# return x
x_freq = fft.fftn(x.float(), dim=(-2, -1))
x_freq = fft.fftshift(x_freq, dim=(-2, -1))
_batch, _channels, height, width = x_freq.shape
mask = torch.ones(x_freq.shape, device=x.device)
crow, ccol = height // 2, width // 2
mask[
...,
crow - threshold : crow + threshold,
ccol - threshold : ccol + threshold,
] = scale
if scales:
for scale_threshold, scale_value in scales:
scaled_scale_value = scale_value * strength
scale_mask = torch.ones(x_freq.shape, device=x.device)
scale_mask[
...,
crow - scale_threshold : crow + scale_threshold,
ccol - scale_threshold : ccol + scale_threshold,
] = scaled_scale_value
mask += (scale_mask - mask) * strength
x_freq *= mask
# IFFT
x_freq = fft.ifftshift(x_freq, dim=(-2, -1))
return fft.ifftn(x_freq, dim=(-2, -1)).real.to(x.dtype)