Files
ausboss-ComfyUI-AusBoss/tests/test_resize_helpers.py
T
ausboss 3f715a4af8 Ship the crush-list build-out: 7 new node files, replace button, video and LoRA polish
- New nodes: Image Resize plus the Utility group (Text, Integer, Float,
  Show Text, Math Expression, Select Every Nth, Split Batch, Merge
  Batches, Free Memory) - the dependency-free gaps that let shared
  workflows drop heavy packs.
- Replace with AusBoss nodes (prototype): preview-then-confirm graph
  rewrite swapping known third-party nodes for pack equivalents, with a
  tested translation map, per-node opt-out, and refuse-with-reason for
  nodes the pack cannot stand in for.
- Load Video: single-frame FRAME picker, an honest "what will load"
  label from the probed source rate, drop-a-video-to-restore. Save
  Video: format-aware widgets and a file_path output. Stitch Inpaint
  gained a blend_mask output, Select Frame negative from-the-end
  indexing, Image Size a batch count - all appended, saved workflows
  load unchanged.
- LoRA Loader: drag-to-reorder rows, hover thumbnails on rows, file
  size and date in the info card.
- Appearance: flagship scheme deepened to slate-teal; the retired
  bright-teal pair upgrades automatically via LEGACY_SCHEME_PAIRS.
  Optional queue-completion chime, off by default.
- Image Resize invariant: pixels are only invented on an explicit
  width+height pad request - scale-derived modes resolve divisible_by
  snaps with a sub-half-step resize, never bars.
- Docs refreshed for six stale pages, stale text and metadata fixed
  across the pack, the panel-height rule codified in AGENTS.md and the
  brand skill, and ausboss_node_tour.json added - a model-free example
  exercising 17 node types in one graph.

Validated: scripts/validate_nodes.py (24 files), 300 JS tests, all
Python suites under the ComfyUI embedded interpreter.
2026-08-27 13:30:35 -05:00

453 lines
19 KiB
Python

from __future__ import annotations
from pathlib import Path
import sys
import unittest
import torch
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
if "nodes" in sys.modules and not hasattr(sys.modules["nodes"], "__path__"):
del sys.modules["nodes"]
from nodes._resize_helpers import (
INTERPOLATION_MODES,
PROPORTION_MODES,
TARGET_MODES,
apply_resize,
plan_resize,
resample_batch,
resolve_target,
snap_to_multiple,
)
def rand_image(batch: int, height: int, width: int, seed: int = 0) -> torch.Tensor:
generator = torch.Generator().manual_seed(seed)
return torch.rand((batch, height, width, 3), generator=generator, dtype=torch.float32)
class SnapToMultipleTests(unittest.TestCase):
def test_snaps_to_the_nearest_multiple_half_up(self):
self.assertEqual(snap_to_multiple(1000, 16), 1008) # 62.5 rounds up
self.assertEqual(snap_to_multiple(23, 16), 16)
self.assertEqual(snap_to_multiple(24, 16), 32) # 1.5 rounds up
self.assertEqual(snap_to_multiple(16, 16), 16)
def test_never_returns_below_one_step(self):
self.assertEqual(snap_to_multiple(0, 16), 16)
self.assertEqual(snap_to_multiple(3, 16), 16)
def test_multiple_one_is_a_passthrough(self):
self.assertEqual(snap_to_multiple(7, 1), 7)
self.assertEqual(snap_to_multiple(7, 0), 7) # normalized to 1
class ResolveTargetTests(unittest.TestCase):
def test_width_height_zero_keeps_the_source(self):
self.assertEqual(resolve_target(1920, 1080, "width+height", 0, 0), (1920, 1080))
def test_width_height_one_value_preserves_aspect(self):
self.assertEqual(resolve_target(1920, 1080, "width+height", 960, 0), (960, 540))
self.assertEqual(resolve_target(1920, 1080, "width+height", 0, 540), (960, 540))
def test_width_height_both_values_are_exact(self):
self.assertEqual(resolve_target(1920, 1080, "width+height", 512, 512), (512, 512))
def test_longest_edge_lands_exactly_on_the_request(self):
self.assertEqual(
resolve_target(1920, 1080, "longest_edge", edge_length=960), (960, 540)
)
self.assertEqual(
resolve_target(1080, 1920, "longest_edge", edge_length=960), (540, 960)
)
self.assertEqual(
resolve_target(1920, 1080, "longest_edge", edge_length=0), (1920, 1080)
)
def test_shortest_edge_lands_exactly_on_the_request(self):
self.assertEqual(
resolve_target(1920, 1080, "shortest_edge", edge_length=540), (960, 540)
)
self.assertEqual(
resolve_target(1080, 1920, "shortest_edge", edge_length=540), (540, 960)
)
self.assertEqual(
resolve_target(1920, 1080, "shortest_edge", edge_length=0), (1920, 1080)
)
def test_megapixels_hits_the_pixel_budget(self):
width, height = resolve_target(1920, 1080, "megapixels", megapixels=1.0)
self.assertEqual((width, height), (1333, 750))
self.assertLess(abs(width * height / 1e6 - 1.0), 0.01)
# Aspect survives the scale.
self.assertAlmostEqual(width / height, 1920 / 1080, places=2)
self.assertEqual(
resolve_target(1920, 1080, "megapixels", megapixels=0.0), (1920, 1080)
)
def test_scale_factor_multiplies_and_zero_keeps_the_source(self):
self.assertEqual(
resolve_target(1920, 1080, "scale_factor", scale_factor=0.5), (960, 540)
)
self.assertEqual(
resolve_target(1920, 1080, "scale_factor", scale_factor=2.0), (3840, 2160)
)
self.assertEqual(
resolve_target(1920, 1080, "scale_factor", scale_factor=0.0), (1920, 1080)
)
def test_dimensions_never_collapse_to_zero(self):
self.assertEqual(
resolve_target(2000, 10, "scale_factor", scale_factor=0.01), (20, 1)
)
def test_unknown_mode_and_empty_source_are_rejected(self):
with self.assertRaises(ValueError):
resolve_target(64, 64, "diagonal")
with self.assertRaises(ValueError):
resolve_target(0, 64, "width+height")
class PlanResizeTests(unittest.TestCase):
def test_stretch_resizes_straight_to_the_target(self):
plan = plan_resize(1920, 1080, 512, 512, "stretch")
self.assertEqual(
plan,
{
"resize_width": 512,
"resize_height": 512,
"canvas_width": 512,
"canvas_height": 512,
"crop_x": 0,
"crop_y": 0,
"offset_x": 0,
"offset_y": 0,
},
)
def test_fit_shrinks_the_target_box_to_the_source_aspect(self):
plan = plan_resize(1920, 1080, 512, 512, "fit")
self.assertEqual((plan["canvas_width"], plan["canvas_height"]), (512, 288))
self.assertEqual((plan["resize_width"], plan["resize_height"]), (512, 288))
self.assertEqual((plan["offset_x"], plan["offset_y"]), (0, 0))
def test_fit_snaps_the_fitted_size_to_the_multiple(self):
plan = plan_resize(1920, 1080, 500, 500, "fit", divisible_by=16)
self.assertEqual((plan["canvas_width"], plan["canvas_height"]), (496, 288))
self.assertEqual(plan["canvas_width"] % 16, 0)
self.assertEqual(plan["canvas_height"] % 16, 0)
def test_cover_crop_overshoots_and_centers_the_crop(self):
plan = plan_resize(1920, 1080, 512, 512, "cover_crop")
self.assertEqual((plan["canvas_width"], plan["canvas_height"]), (512, 512))
self.assertEqual((plan["resize_width"], plan["resize_height"]), (911, 512))
self.assertEqual((plan["crop_x"], plan["crop_y"]), (199, 0))
def test_cover_crop_exact_scale_does_not_overshoot_by_a_pixel(self):
# scale is exactly 0.5; floating-point noise must not ceil past it.
plan = plan_resize(100, 50, 50, 25, "cover_crop")
self.assertEqual((plan["resize_width"], plan["resize_height"]), (50, 25))
self.assertEqual((plan["crop_x"], plan["crop_y"]), (0, 0))
def test_pad_fits_inside_and_centers_the_bars(self):
plan = plan_resize(1920, 1080, 512, 512, "pad")
self.assertEqual((plan["canvas_width"], plan["canvas_height"]), (512, 512))
self.assertEqual((plan["resize_width"], plan["resize_height"]), (512, 288))
self.assertEqual((plan["offset_x"], plan["offset_y"]), (0, 112))
def test_pad_snaps_the_canvas_then_refits_the_source(self):
plan = plan_resize(1920, 1080, 500, 500, "pad", divisible_by=16)
self.assertEqual((plan["canvas_width"], plan["canvas_height"]), (496, 496))
self.assertEqual((plan["resize_width"], plan["resize_height"]), (496, 279))
self.assertEqual((plan["offset_x"], plan["offset_y"]), (0, 108))
def test_matching_target_is_an_identity_plan(self):
for mode in PROPORTION_MODES:
with self.subTest(mode=mode):
plan = plan_resize(640, 480, 640, 480, mode)
self.assertEqual((plan["resize_width"], plan["resize_height"]), (640, 480))
self.assertEqual((plan["canvas_width"], plan["canvas_height"]), (640, 480))
self.assertEqual((plan["crop_x"], plan["crop_y"]), (0, 0))
self.assertEqual((plan["offset_x"], plan["offset_y"]), (0, 0))
def test_divisible_by_applies_even_when_the_target_is_the_source(self):
plan = plan_resize(1000, 750, 1000, 750, "stretch", divisible_by=16)
self.assertEqual((plan["canvas_width"], plan["canvas_height"]), (1008, 752))
def test_invalid_inputs_are_rejected(self):
with self.assertRaises(ValueError):
plan_resize(640, 480, 512, 512, "letterbox")
with self.assertRaises(ValueError):
plan_resize(640, 480, 0, 512, "fit")
class ResampleBatchTests(unittest.TestCase):
def test_every_mode_returns_the_requested_shape(self):
image = rand_image(2, 16, 16, seed=1)
for mode in INTERPOLATION_MODES:
for width, height in ((24, 20), (8, 8)):
with self.subTest(mode=mode, size=(width, height)):
out = resample_batch(image, width, height, mode)
self.assertEqual(tuple(out.shape), (2, height, width, 3))
self.assertFalse(bool(out.isnan().any()))
def test_matching_size_returns_the_input_untouched(self):
image = rand_image(1, 12, 10, seed=2)
self.assertIs(resample_batch(image, 10, 12, "lanczos"), image)
def test_nearest_invents_no_new_values(self):
image = torch.tensor(
[[[0.0, 0.25], [0.5, 1.0]]], dtype=torch.float32
).unsqueeze(-1)
out = resample_batch(image, 8, 8, "nearest")
self.assertEqual(
set(torch.unique(out).tolist()), set(torch.unique(image).tolist())
)
def test_overshooting_filters_stay_inside_the_value_range(self):
# A hard checkerboard makes lanczos and bicubic ring past 0..1.
checker = torch.zeros((1, 16, 16, 3), dtype=torch.float32)
checker[:, ::2, ::2, :] = 1.0
checker[:, 1::2, 1::2, :] = 1.0
for mode in ("lanczos", "bicubic"):
with self.subTest(mode=mode):
out = resample_batch(checker, 24, 24, mode)
self.assertGreaterEqual(float(out.min()), 0.0)
self.assertLessEqual(float(out.max()), 1.0)
def test_lanczos_keeps_a_constant_image_constant(self):
image = torch.full((1, 10, 14, 3), 0.375, dtype=torch.float32)
out = resample_batch(image, 7, 5, "lanczos")
self.assertTrue(torch.allclose(out, torch.full_like(out, 0.375), atol=1e-5))
def test_area_downscale_averages_exactly(self):
checker = torch.zeros((1, 4, 4, 1), dtype=torch.float32)
checker[:, ::2, ::2] = 1.0
checker[:, 1::2, 1::2] = 1.0
out = resample_batch(checker, 2, 2, "area")
self.assertTrue(torch.allclose(out, torch.full_like(out, 0.5)))
def test_bad_arguments_are_rejected(self):
image = rand_image(1, 8, 8)
with self.assertRaises(ValueError):
resample_batch(image, 8, 8, "mitchell")
with self.assertRaises(ValueError):
resample_batch(image, 0, 8, "bilinear")
with self.assertRaises(ValueError):
resample_batch(image[0], 8, 8, "bilinear")
def run_resize(image, mask=None, **overrides):
values = {
"target_mode": "width+height",
"width": 0,
"height": 0,
"edge_length": 0,
"megapixels": 0.0,
"scale_factor": 0.0,
"keep_proportion": "fit",
"fill_color": "#000000",
"divisible_by": 1,
"interpolation": "nearest",
}
values.update(overrides)
return apply_resize(image, mask, **values)
class ApplyResizeTests(unittest.TestCase):
def test_stretch_hits_the_exact_target_and_reports_it(self):
image = rand_image(2, 24, 32, seed=3)
out, mask, width, height = run_resize(
image, width=16, height=12, keep_proportion="stretch"
)
self.assertEqual(tuple(out.shape), (2, 12, 16, 3))
self.assertEqual(tuple(mask.shape), (2, 12, 16))
self.assertEqual((width, height), (16, 12))
self.assertEqual(float(mask.sum()), 0.0)
def test_fit_keeps_the_aspect_instead_of_the_box(self):
image = rand_image(1, 24, 32, seed=4)
out, _, width, height = run_resize(image, width=16, height=16)
self.assertEqual((width, height), (16, 12))
self.assertEqual(tuple(out.shape), (1, 12, 16, 3))
def test_zero_widgets_are_a_bit_identical_passthrough(self):
image = rand_image(1, 24, 32, seed=5)
out, mask, width, height = run_resize(image)
self.assertIs(out, image)
self.assertEqual((width, height), (32, 24))
self.assertEqual(tuple(mask.shape), (1, 24, 32))
self.assertEqual(float(mask.sum()), 0.0)
def test_zero_widgets_still_respect_divisible_by(self):
image = rand_image(1, 20, 30, seed=6)
out, _, width, height = run_resize(
image, keep_proportion="stretch", divisible_by=8
)
self.assertEqual((width, height), (32, 24))
self.assertEqual(tuple(out.shape), (1, 24, 32, 3))
def test_scale_derived_modes_never_invent_pixels(self):
# 855x480 at longest_edge 512 fits to 512x287 but snaps to 512x288 —
# the case where pad used to answer divisible_by with an invented
# bottom row. The box is derived from the source's own aspect, so
# every proportion mode must resolve the snap like fit does: a
# sub-half-step resize, a black mask, and not one invented pixel.
image = rand_image(1, 480, 855, seed=9)
for proportion in ("stretch", "fit", "cover_crop", "pad"):
out, mask, width, height = run_resize(
image,
target_mode="longest_edge",
edge_length=512,
keep_proportion=proportion,
divisible_by=16,
)
self.assertEqual((width, height), (512, 288), proportion)
self.assertEqual(tuple(out.shape), (1, 288, 512, 3), proportion)
self.assertEqual(float(mask.max()), 0.0, proportion)
def test_pad_fills_the_bars_and_marks_them_in_the_mask(self):
image = rand_image(1, 24, 32, seed=7)
out, mask, width, height = run_resize(
image, width=32, height=32, keep_proportion="pad", fill_color="#ff0000"
)
self.assertEqual((width, height), (32, 32))
self.assertEqual(tuple(out.shape), (1, 32, 32, 3))
# The source resamples to itself, so it lands bit-identical at y=4.
self.assertTrue(torch.equal(out[:, 4:28, :, :], image))
red = torch.tensor([1.0, 0.0, 0.0])
self.assertTrue(torch.allclose(out[0, 0, 0], red))
self.assertTrue(torch.allclose(out[0, -1, -1], red))
# Generated-area contract: bars 1.0, source region 0.0.
self.assertEqual(float(mask[:, 4:28, :].sum()), 0.0)
self.assertEqual(float(mask.sum()), float(2 * 4 * 32))
def test_pad_carries_the_input_mask_inside_the_bars(self):
image = rand_image(1, 24, 32, seed=8)
mask = torch.full((1, 24, 32), 0.5, dtype=torch.float32)
_, out_mask, _, _ = run_resize(
image, mask, width=32, height=32, keep_proportion="pad"
)
self.assertTrue(
torch.allclose(out_mask[:, 4:28, :], torch.full((1, 24, 32), 0.5))
)
self.assertEqual(float(out_mask[:, :4, :].min()), 1.0)
self.assertEqual(float(out_mask[:, 28:, :].min()), 1.0)
def test_cover_crop_slices_image_and_mask_identically(self):
image = rand_image(1, 24, 32, seed=9)
mask = torch.zeros((1, 24, 32), dtype=torch.float32)
mask[:, :, :16] = 1.0
out, out_mask, width, height = run_resize(
image, mask, width=24, height=24, keep_proportion="cover_crop"
)
self.assertEqual((width, height), (24, 24))
# Cover scale is exactly 1.0 here, so the crop is a pure slice.
self.assertTrue(torch.equal(out, image[:, :, 4:28, :]))
self.assertTrue(torch.equal(out_mask, mask[:, :, 4:28]))
def test_mask_keeps_its_own_batch_size(self):
image = rand_image(3, 24, 32, seed=10)
mask = torch.ones((1, 24, 32), dtype=torch.float32)
out, out_mask, _, _ = run_resize(
image, mask, width=32, height=32, keep_proportion="pad"
)
self.assertEqual(out.shape[0], 3)
self.assertEqual(out_mask.shape[0], 1)
def test_scaling_modes_flow_through_end_to_end(self):
image = rand_image(2, 24, 32, seed=11)
_, _, width, height = run_resize(
image, target_mode="longest_edge", edge_length=16
)
self.assertEqual((width, height), (16, 12))
_, _, width, height = run_resize(
image, target_mode="shortest_edge", edge_length=12
)
self.assertEqual((width, height), (16, 12))
_, _, width, height = run_resize(
image, target_mode="scale_factor", scale_factor=0.5
)
self.assertEqual((width, height), (16, 12))
def test_bad_tensors_are_rejected(self):
with self.assertRaises(ValueError):
run_resize(torch.zeros((24, 32, 3)))
with self.assertRaises(ValueError):
run_resize(rand_image(1, 8, 8), torch.zeros((1, 8, 8, 1)))
class ImageResizeNodeTests(unittest.TestCase):
def make_node(self):
from nodes.node_image_resize import (
NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS,
)
self.assertIn("AUSBOSS_NODES_ImageResize", NODE_CLASS_MAPPINGS)
self.assertEqual(
NODE_DISPLAY_NAME_MAPPINGS["AUSBOSS_NODES_ImageResize"],
"Image Resize 🆎",
)
return NODE_CLASS_MAPPINGS["AUSBOSS_NODES_ImageResize"]
def test_contract(self):
cls = self.make_node()
self.assertIn("AusBoss/Image", cls.CATEGORY)
self.assertIn("ausboss", cls.SEARCH_ALIASES)
self.assertTrue(cls.DESCRIPTION)
self.assertEqual(cls.RETURN_TYPES, ("IMAGE", "MASK", "INT", "INT"))
self.assertEqual(cls.RETURN_NAMES, ("image", "mask", "width", "height"))
self.assertEqual(len(cls.OUTPUT_TOOLTIPS), 4)
def test_every_input_is_tooltipped_and_combos_match_the_helpers(self):
cls = self.make_node()
inputs = cls.INPUT_TYPES()
for section in ("required", "optional"):
for name, (kind, options) in inputs[section].items():
with self.subTest(input=name):
self.assertTrue(options.get("tooltip"))
self.assertEqual(inputs["required"]["target_mode"][0], list(TARGET_MODES))
self.assertEqual(inputs["required"]["keep_proportion"][0], list(PROPORTION_MODES))
self.assertEqual(inputs["required"]["interpolation"][0], list(INTERPOLATION_MODES))
self.assertIn("mask", inputs["optional"])
def run_node(self, cls, image, mask=None, **overrides):
values = {
"target_mode": "width+height",
"width": 0,
"height": 0,
"edge_length": 0,
"megapixels": 0.0,
"scale_factor": 0.0,
"keep_proportion": "fit",
"fill_color": "#000000",
"divisible_by": 1,
"interpolation": "bilinear",
}
values.update(overrides)
return getattr(cls(), cls.FUNCTION)(image=image, mask=mask, **values)
def test_resizes_by_longest_edge(self):
cls = self.make_node()
image = rand_image(1, 24, 32, seed=12)
out, mask, width, height = self.run_node(
cls, image, target_mode="longest_edge", edge_length=16
)
self.assertEqual((width, height), (16, 12))
self.assertEqual(tuple(out.shape), (1, 12, 16, 3))
self.assertEqual(tuple(mask.shape), (1, 12, 16))
def test_defaults_keep_the_source(self):
cls = self.make_node()
image = rand_image(1, 24, 32, seed=13)
out, _, width, height = self.run_node(cls, image)
self.assertIs(out, image)
self.assertEqual((width, height), (32, 24))
if __name__ == "__main__":
unittest.main()