mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-09-09 02:26:31 +08:00
test(compositor): pin blend behaviour to a golden fixture; fix luminosity (#15373)
Blending exists in two implementations - the numpy compositor and the layerBlend.frag shader that drives the live preview - with nothing holding them together. They have already diverged once (the safeDiv operand), and a divergence only shows up to the user as 'the render does not match the preview'. Adds compositor_blend_golden.json: every mode, at both endpoints, the midpoint and inside each epsilon guard. Any implementation of these 26 modes must reproduce it. compositor_blend_test.py pins the numpy side to it and additionally spells out the boundary rules by hand, so the guards cannot be re-broken by regenerating the fixture. Diffing the shader against the numpy implementation over that grid leaves exactly one mismatch: luminosity. safe_div guards the denominator and returns 0, so a luminosity layer over a black or near-black backdrop disappears. The backdrop has no hue or saturation to preserve there, so the result should be a neutral grey at the layer's luminance - which is also the analytic limit of i * lum(l)/lum(i) as the backdrop approaches black. The matching four-line shader change is proposed on the frontend PR; with both applied all 26 modes agree. Also clamps layer opacity to [0, 1]. The layer state round-trips through the saved workflow and is accepted verbatim on /prompt, so it is untrusted input; the canvas is only clamped once, after the last layer, so an out-of-range coverage multiplier changes the blend of every layer above it. _parse_background already clamps the same field.
This commit is contained in:
@@ -146,8 +146,22 @@ def _blend_color(i: np.ndarray, l: np.ndarray) -> np.ndarray:
|
||||
|
||||
|
||||
def _blend_luminosity(i: np.ndarray, l: np.ndarray) -> np.ndarray:
|
||||
ratio = safe_div(luminance(l), luminance(i))
|
||||
return i * ratio[..., None]
|
||||
# Scale the backdrop so it carries the layer's luminance. Where the backdrop
|
||||
# has no luminance to scale there is no hue or saturation to preserve either,
|
||||
# so the result is a neutral grey at the layer's luminance - which is also the
|
||||
# analytic limit of i * lum(l)/lum(i) as a grey backdrop approaches black.
|
||||
# Guarding the numerator here instead (returning black) makes a luminosity
|
||||
# layer disappear over dark backdrops; see tests-unit/comfy_extras_test/
|
||||
# compositor_blend_golden.json.
|
||||
lum_i = luminance(i)
|
||||
lum_l = luminance(l)
|
||||
degenerate = lum_i <= EPSILON
|
||||
ratio = np.where(degenerate, 0.0, lum_l / np.where(degenerate, 1.0, lum_i))
|
||||
return np.where(
|
||||
degenerate[..., None],
|
||||
np.broadcast_to(lum_l[..., None], i.shape),
|
||||
i * ratio[..., None],
|
||||
)
|
||||
|
||||
|
||||
HSL_BLEND = {
|
||||
|
||||
@@ -272,7 +272,12 @@ def _layer_params(entry, natural_w: int, natural_h: int) -> dict:
|
||||
blend = entry.get("blend")
|
||||
return {
|
||||
"visible": bool(entry.get("visible", True)),
|
||||
"opacity": _number(entry, "opacity", 1.0),
|
||||
# The layer state is untrusted input: it round-trips through the saved
|
||||
# workflow and can be posted directly to /prompt. An out-of-range opacity
|
||||
# would otherwise reach blend_composite as a raw coverage multiplier and
|
||||
# produce negative or greater-than-white RGB. _parse_background already
|
||||
# clamps the same field.
|
||||
"opacity": min(max(_number(entry, "opacity", 1.0), 0.0), 1.0),
|
||||
"blend": blend if isinstance(blend, str) else "normal",
|
||||
"x": _number(transform, "x", 0.0),
|
||||
"y": _number(transform, "y", 0.0),
|
||||
|
||||
Reference in New Issue
Block a user