Files
Ewald a4154ccf05 Fix P0 bugs, remove Append V2/Lens Artifacts, and fix crop reload reset
- Fix batch bypass tuple handling, memory budget device-awareness, grain
  RNG device, roto payload limits, preview temp-file cleanup, and route
  double-registration.
- Add native VIDEO output (fps/audio) to effect nodes that previously
  only returned IMAGE/MASK when fed a VIDEO input.
- Split the oversized src/preview/ops/geometry.ts and add a starting
  golden-fixture test harness (spherize, transform) comparing backend
  and frontend math.
- Remove ImageOpsAppendV2 (Autogrow) and ImageOpsLensArtifacts; inline
  Append's helper back into its single caller.
- Collapse the Color Correct curve widget behind a toggle so the node
  no longer grows too tall.
- Fix Crop/generic-node video preview playback running at the RAF rate
  instead of the source video's real fps.
- Fix Crop losing its saved output size after a workflow reload:
  syncCropWidgets recomputed height from width and a preset aspect
  ratio on every resync (including onConfigure on load), discarding
  the restored value. It now only refreshes aspect-ratio bookkeeping
  on a resync.
2026-09-30 12:49:42 +02:00

97 lines
3.4 KiB
Python

import json
from pathlib import Path
import torch
import pytest
from nodes.transform import ImageOpsTransform, _transform_batch_affine, _padding_mode_from_fill
def test_transform_batch_affine_matches_golden_fixture_used_for_frontend_parity():
"""Regression guard for tests/golden/transform.json, which
tests/frontend/transform.test.cjs uses to check applyTransform for parity.
If this test needs updating, regenerate the golden and re-check the JS side."""
golden_path = Path(__file__).resolve().parents[1] / "golden" / "transform.json"
golden = json.loads(golden_path.read_text(encoding="utf-8"))
meta = golden["_meta"]
width, height = meta["width"], meta["height"]
params = golden["params"]
frame = torch.tensor(golden["input_rgba"], dtype=torch.float32).unsqueeze(0) / 255.0
padding_mode = _padding_mode_from_fill(params["fill_mode"])
out = _transform_batch_affine(
frame, params["filter_mode"], params["translate_x"], params["translate_y"],
params["rotate_deg"], params["scale"], padding_mode=padding_mode,
)
actual = (out[0] * 255.0).round().clamp(0, 255).byte()
expected = torch.tensor(golden["expected_rgba"], dtype=torch.uint8)
assert actual.shape == (height, width, 4)
max_diff = (actual.int() - expected.int()).abs().max().item()
# tolerance=1: the fixture round-trips through uint8 storage, so re-deriving
# the input tensor from it reintroduces ~1/255 of quantization noise versus
# the exact floats used when the golden was generated.
assert max_diff <= 1, f"_transform_batch_affine no longer matches its own golden fixture (max diff {max_diff})"
def test_transform_expand():
node = ImageOpsTransform()
# 100x100 white image
image = torch.ones((1, 100, 100, 3))
# Translate by 50 in X, and expand
# Center of original is at (50, 50).
# Translation shifts center by 50 to the right.
# New X coordinates will span from -50+50 = 0 to 50+50 = 100.
# So max_x = 100, min_x = 0.
# Expand is currently disabled (fixed-size canvas).
# W_out = 100, H_out = 100.
# New image should be 100x100.
result, mask, video = node.execute(
image=image,
translate_x=50.0,
translate_y=0.0,
rotate_deg=0.0,
scale=1.0,
expand=True,
fill_mode="transparent",
filter="nearest"
)
assert result.shape == (1, 100, 100, 3)
# Rotate by 90 degrees, W=100, H=200
image2 = torch.ones((1, 200, 100, 3))
# After 90 deg rotation, corners swap. W_out should be 200, H_out should be 100.
result2, mask2, video2 = node.execute(
image=image2,
translate_x=0.0,
translate_y=0.0,
rotate_deg=90.0,
scale=1.0,
expand=True,
fill_mode="transparent",
filter="nearest"
)
assert result2.shape == (1, 200, 100, 3)
@pytest.mark.parametrize("fill_mode", ["mirror", "expand"])
def test_masked_transform_keeps_color_bounded_at_edges(fill_mode):
"""Color and matte must use the same padding, or edge alpha divides non-zero color."""
torch.manual_seed(0)
image = torch.rand((1, 96, 128, 4))
mask = torch.rand((1, 96, 128))
result, _, _ = ImageOpsTransform().execute(
image=image,
mask=mask,
translate_x=7.0,
rotate_deg=3.0,
scale=1.05,
fill_mode=fill_mode,
)
assert float(result[..., :3].max()) <= 1.0 + 1e-3