Refactor node loading, add WASLUT
This commit is contained in:
+162
-5
@@ -371,6 +371,24 @@ class WaveformScope:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _big_font():
|
||||
# Try to load a larger truetype font for better legibility; fallback to default
|
||||
# Common fonts to try across platforms
|
||||
candidates = [
|
||||
"DejaVuSansMono.ttf",
|
||||
"DejaVuSans.ttf",
|
||||
"Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
]
|
||||
for path in candidates:
|
||||
try:
|
||||
return ImageFont.truetype(path, size=14)
|
||||
except Exception:
|
||||
continue
|
||||
return WaveformScope._font()
|
||||
|
||||
@staticmethod
|
||||
def make_waveform_gray(ch_gray: np.ndarray, out_h: int) -> np.ndarray:
|
||||
h, w = ch_gray.shape
|
||||
@@ -426,7 +444,7 @@ class WaveformScope:
|
||||
r_stats: tuple[float, float, float, float, float],
|
||||
g_stats: tuple[float, float, float, float, float],
|
||||
b_stats: tuple[float, float, float, float, float],
|
||||
gap: int = 8, pad: int = 60, left_pad: int = 56) -> np.ndarray:
|
||||
gap: int = 8, pad: int = 72, left_pad: int = 56) -> np.ndarray:
|
||||
h, w = wfr.shape
|
||||
pr = (np.stack([wfr, np.zeros_like(wfr), np.zeros_like(wfr)], -1) * 255.0 + 0.5).astype(np.uint8)
|
||||
pg = (np.stack([np.zeros_like(wfg), wfg, np.zeros_like(wfg)], -1) * 255.0 + 0.5).astype(np.uint8)
|
||||
@@ -452,10 +470,21 @@ class WaveformScope:
|
||||
g_txt = f"G min {g_stats[0]:.4f} max {g_stats[1]:.4f} mean {g_stats[2]:.4f} std {g_stats[3]:.4f} median {g_stats[4]:.4f}"
|
||||
b_txt = f"B min {b_stats[0]:.4f} max {b_stats[1]:.4f} mean {b_stats[2]:.4f} std {b_stats[3]:.4f} median {b_stats[4]:.4f}"
|
||||
d = ImageDraw.Draw(canvas)
|
||||
big_font = WaveformScope._big_font()
|
||||
# Estimate line height for spacing
|
||||
try:
|
||||
bbox = big_font.getbbox("Ag")
|
||||
line_h = (bbox[3] - bbox[1]) + 4
|
||||
except Exception:
|
||||
line_h = 18
|
||||
y0 = H + 6
|
||||
d.text((6, y0), r_txt, fill=(255, 64, 64), font=font, stroke_width=1, stroke_fill=(0, 0, 0))
|
||||
d.text((6, y0 + 16), g_txt, fill=(64, 255, 64), font=font, stroke_width=1, stroke_fill=(0, 0, 0))
|
||||
d.text((6, y0 + 32), b_txt, fill=(64, 128, 255), font=font, stroke_width=1, stroke_fill=(0, 0, 0))
|
||||
# X positions aligned under each channel
|
||||
x_r = left_pad + 0 * (w + gap) + 6
|
||||
x_g = left_pad + 1 * (w + gap) + 6
|
||||
x_b = left_pad + 2 * (w + gap) + 6
|
||||
d.text((x_r, y0), r_txt, fill=(255, 64, 64), font=big_font, stroke_width=1, stroke_fill=(0, 0, 0))
|
||||
d.text((x_g, y0), g_txt, fill=(64, 255, 64), font=big_font, stroke_width=1, stroke_fill=(0, 0, 0))
|
||||
d.text((x_b, y0), b_txt, fill=(64, 128, 255), font=big_font, stroke_width=1, stroke_fill=(0, 0, 0))
|
||||
return np.array(canvas, dtype=np.uint8)
|
||||
|
||||
# Load LUT
|
||||
@@ -498,6 +527,8 @@ class WASLoadLUT:
|
||||
return None
|
||||
|
||||
RETURN_TYPES = ("LUT",)
|
||||
RETURN_NAMES = ("lut",)
|
||||
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "WAS/Color/LUT"
|
||||
|
||||
@@ -660,7 +691,6 @@ class LUTBlender:
|
||||
|
||||
@staticmethod
|
||||
def blend_hsv(a: np.ndarray, b: np.ndarray, t: float) -> np.ndarray:
|
||||
# Convert to HSV, circularly lerp hue, linearly lerp s and v
|
||||
ha, sa, va = LUTBlender._rgb_to_hsv(a)
|
||||
hb, sb, vb = LUTBlender._rgb_to_hsv(b)
|
||||
tt = float(t)
|
||||
@@ -671,6 +701,123 @@ class LUTBlender:
|
||||
out = LUTBlender._hsv_to_rgb(h, s, v)
|
||||
return np.clip(out, 0.0, 1.0).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _srgb_to_linear(x: np.ndarray) -> np.ndarray:
|
||||
x = x.astype(np.float32)
|
||||
return np.where(x <= 0.04045, x / 12.92, ((x + 0.055) / 1.055) ** 2.4).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _linear_to_srgb(x: np.ndarray) -> np.ndarray:
|
||||
x = x.astype(np.float32)
|
||||
return np.where(x <= 0.0031308, x * 12.92, 1.055 * (np.clip(x, 0.0, None) ** (1/2.4)) - 0.055).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _rgb_linear_to_xyz(rgb: np.ndarray) -> np.ndarray:
|
||||
M = np.array([
|
||||
[0.4124564, 0.3575761, 0.1804375],
|
||||
[0.2126729, 0.7151522, 0.0721750],
|
||||
[0.0193339, 0.1191920, 0.9503041],
|
||||
], dtype=np.float32)
|
||||
return np.tensordot(rgb, M.T, axes=1).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _xyz_to_rgb_linear(xyz: np.ndarray) -> np.ndarray:
|
||||
M = np.array([
|
||||
[ 3.2404542, -1.5371385, -0.4985314],
|
||||
[-0.9692660, 1.8760108, 0.0415560],
|
||||
[ 0.0556434, -0.2040259, 1.0572252],
|
||||
], dtype=np.float32)
|
||||
return np.tensordot(xyz, M.T, axes=1).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _rgb_to_lab(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
lin = LUTBlender._srgb_to_linear(rgb)
|
||||
xyz = LUTBlender._rgb_linear_to_xyz(lin)
|
||||
Xn, Yn, Zn = 0.95047, 1.0, 1.08883
|
||||
x = xyz[..., 0] / Xn
|
||||
y = xyz[..., 1] / Yn
|
||||
z = xyz[..., 2] / Zn
|
||||
e = (6/29) ** 3
|
||||
k = (29/6) ** 2 / 3
|
||||
f = lambda t: np.where(t > e, np.cbrt(t), k * t + 4/29)
|
||||
fx, fy, fz = f(x), f(y), f(z)
|
||||
L = 116 * fy - 16
|
||||
a = 500 * (fx - fy)
|
||||
b = 200 * (fy - fz)
|
||||
return L.astype(np.float32), a.astype(np.float32), b.astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _lab_to_rgb(L: np.ndarray, a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
||||
fy = (L + 16.0) / 116.0
|
||||
fx = fy + (a / 500.0)
|
||||
fz = fy - (b / 200.0)
|
||||
e = (6/29)
|
||||
e3 = e ** 3
|
||||
k = 3 * (e ** 2)
|
||||
invf = lambda t: np.where(t > e, t ** 3, (t - 4/29) / k)
|
||||
Xn, Yn, Zn = 0.95047, 1.0, 1.08883
|
||||
x = invf(fx) * Xn
|
||||
y = invf(fy) * Yn
|
||||
z = invf(fz) * Zn
|
||||
xyz = np.stack([x, y, z], axis=-1).astype(np.float32)
|
||||
lin = LUTBlender._xyz_to_rgb_linear(xyz)
|
||||
rgb = LUTBlender._linear_to_srgb(lin)
|
||||
return np.clip(rgb, 0.0, 1.0).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _rgb_to_oklab(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
lin = LUTBlender._srgb_to_linear(rgb)
|
||||
M1 = np.array([
|
||||
[0.4122214708, 0.5363325363, 0.0514459929],
|
||||
[0.2119034982, 0.6806995451, 0.1073969566],
|
||||
[0.0883024619, 0.2817188376, 0.6299787005],
|
||||
], dtype=np.float32)
|
||||
lms = np.tensordot(lin, M1.T, axes=1).astype(np.float32)
|
||||
l_, m_, s_ = np.cbrt(lms[..., 0]), np.cbrt(lms[..., 1]), np.cbrt(lms[..., 2])
|
||||
L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
|
||||
a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
|
||||
b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
|
||||
return L.astype(np.float32), a.astype(np.float32), b.astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _oklab_to_rgb(L: np.ndarray, a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
||||
l_ = L + 0.3963377774 * a + 0.2158037573 * b
|
||||
m_ = L - 0.1055613458 * a - 0.0638541728 * b
|
||||
s_ = L - 0.0894841775 * a - 1.2914855480 * b
|
||||
l = l_ ** 3
|
||||
m = m_ ** 3
|
||||
s = s_ ** 3
|
||||
M2 = np.array([
|
||||
[ 4.0767416621, -3.3077115913, 0.2309699292],
|
||||
[-1.2684380046, 2.6097574011, -0.3413193965],
|
||||
[-0.0041960863, -0.7034186147, 1.7076147010],
|
||||
], dtype=np.float32)
|
||||
lin = np.tensordot(np.stack([l, m, s], axis=-1), M2.T, axes=1).astype(np.float32)
|
||||
rgb = LUTBlender._linear_to_srgb(lin)
|
||||
return np.clip(rgb, 0.0, 1.0).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def blend_lab(a: np.ndarray, b: np.ndarray, t: float) -> np.ndarray:
|
||||
La, aa, ba = LUTBlender._rgb_to_lab(a)
|
||||
Lb, ab, bb = LUTBlender._rgb_to_lab(b)
|
||||
tt = float(t)
|
||||
L = La * (1.0 - tt) + Lb * tt
|
||||
A = aa * (1.0 - tt) + ab * tt
|
||||
B = ba * (1.0 - tt) + bb * tt
|
||||
out = LUTBlender._lab_to_rgb(L, A, B)
|
||||
return np.clip(out, 0.0, 1.0).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def blend_oklab(a: np.ndarray, b: np.ndarray, t: float) -> np.ndarray:
|
||||
La, aa, ba = LUTBlender._rgb_to_oklab(a)
|
||||
Lb, ab, bb = LUTBlender._rgb_to_oklab(b)
|
||||
tt = float(t)
|
||||
L = La * (1.0 - tt) + Lb * tt
|
||||
A = aa * (1.0 - tt) + ab * tt
|
||||
B = ba * (1.0 - tt) + bb * tt
|
||||
out = LUTBlender._oklab_to_rgb(L, A, B)
|
||||
return np.clip(out, 0.0, 1.0).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def blend_auto(a: np.ndarray, b: np.ndarray, t: float) -> np.ndarray:
|
||||
"""
|
||||
@@ -699,6 +846,8 @@ class LUTBlender:
|
||||
"smoothstep",
|
||||
"slerp",
|
||||
"hsv",
|
||||
"lab",
|
||||
"oklab",
|
||||
"auto",
|
||||
"multiply",
|
||||
"screen",
|
||||
@@ -720,6 +869,7 @@ class WASCombineLUT:
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("LUT",)
|
||||
RETURN_NAMES = ("lut",)
|
||||
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "WAS/Color/LUT"
|
||||
@@ -737,6 +887,10 @@ class WASCombineLUT:
|
||||
C = LUTBlender.blend_slerp(A, B, strength)
|
||||
elif mode == "hsv":
|
||||
C = LUTBlender.blend_hsv(A, B, strength)
|
||||
elif mode == "lab":
|
||||
C = LUTBlender.blend_lab(A, B, strength)
|
||||
elif mode == "oklab":
|
||||
C = LUTBlender.blend_oklab(A, B, strength)
|
||||
elif mode == "auto":
|
||||
C = LUTBlender.blend_auto(A, B, strength)
|
||||
elif mode == "multiply":
|
||||
@@ -762,6 +916,7 @@ class WASApplyLUT:
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "WAS/Color/LUT"
|
||||
@@ -815,6 +970,7 @@ class WASChannelWaveform:
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE")
|
||||
RETURN_NAMES = ("red_waveform", "green_waveform", "blue_waveform", "rgb_parade")
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "WAS/Image/Scopes"
|
||||
|
||||
@@ -861,6 +1017,7 @@ class WASChannelWaveform:
|
||||
|
||||
return {"ui": {"images": ui_entries}, "result": (red_batch, green_batch, blue_batch, parade_batch)}
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"WASLoadLUT": WASLoadLUT,
|
||||
"WASCombineLUT": WASCombineLUT,
|
||||
|
||||
Reference in New Issue
Block a user