From db7535751dbddb1b4b6a7d76de547bb9db884dee Mon Sep 17 00:00:00 2001 From: ausboss <102309304+ausboss@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:31:55 -0500 Subject: [PATCH] =?UTF-8?q?Add=20the=20megapixel=20resize=20to=20Video=20C?= =?UTF-8?q?rop=20+=20Rotate=20+=20Pad=20=E2=86=92=20Frame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The image and clip transform nodes could resize their output to a pixel budget; the frame picker, whose single frame often feeds an image or video model, could not. It takes the same appended resize inputs, off by default, and its face and editor show them like the other two. --- README.md | 2 +- nodes/_transform_inputs.py | 5 ++--- nodes/node_video_crop_rotate_pad.py | 28 +++++++++++++++++++++++----- tests/test_transform_outputs.py | 16 ++++++++++++++++ 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9fca075..9b050e2 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Returns frames, lazy audio, frame count, fps, width, height, duration, and a cor ### Video Crop + Rotate + Pad → Frame 🆎 -Find **one frame** in an uploaded or local video, then apply the image transform controls. A playhead rail on the node scrubs to the frame; the editor adds exact frame stepping and playback. Returns one transformed `image`, its generated-area `mask`, a `stitcher`, the selected `original` frame, and the output `width` and `height`. Use the Clip variant for an entire sequence. +Find **one frame** in an uploaded or local video, then apply the image transform controls and an optional megapixel resize. A playhead rail on the node scrubs to the frame; the editor adds exact frame stepping and playback. Returns one transformed `image`, its generated-area `mask`, a `stitcher`, the selected `original` frame, and the output `width` and `height`. Use the Clip variant for an entire sequence. ### Video Crop + Rotate + Pad → Clip 🆎 diff --git a/nodes/_transform_inputs.py b/nodes/_transform_inputs.py index 788a9dd..89b7e72 100644 --- a/nodes/_transform_inputs.py +++ b/nodes/_transform_inputs.py @@ -166,9 +166,8 @@ def transform_inputs(*, feather: int = 24, fill_color: str = "#808080") -> dict[ } -# The image and clip nodes only (the frame node keeps its source geometry): -# resize the transformed output to a pixel budget, core -# ImageScaleToTotalPixels-style. +# Every transform node, appended after its other widgets: resize the +# transformed output to a pixel budget, core ImageScaleToTotalPixels-style. RESIZE_METHODS = ["lanczos", "area", "bicubic", "bilinear", "nearest-exact"] diff --git a/nodes/node_video_crop_rotate_pad.py b/nodes/node_video_crop_rotate_pad.py index c2fb19d..fa0ab7c 100644 --- a/nodes/node_video_crop_rotate_pad.py +++ b/nodes/node_video_crop_rotate_pad.py @@ -9,8 +9,13 @@ from ._media_helpers import ( resolve_video_path, ) from ._inpaint_crop_helpers import build_transform_stitcher -from ._transform_engine import original_image_batch, stable_file_fingerprint, transform_pil_batch -from ._transform_inputs import spec_from_values, transform_inputs +from ._transform_engine import ( + original_image_batch, + resize_batch_to_megapixels, + stable_file_fingerprint, + transform_pil_batch, +) +from ._transform_inputs import resize_inputs, spec_from_values, transform_inputs class AusBossVideoCropRotatePad: @@ -71,6 +76,9 @@ class AusBossVideoCropRotatePad: ), } required.update(transform_inputs()) + # Appended AFTER the stable widgets, so saved workflows' positional + # widgets_values keep loading; missing values fall back to defaults. + required.update(resize_inputs()) return {"required": required} # Appended outputs only: saved links ride slot indices. @@ -80,9 +88,9 @@ class AusBossVideoCropRotatePad: "The selected and transformed frame as a one-image BHWC batch.", "BHW generated-area mask: rotation corners and padding.", "Full-canvas stitcher: restores kept source pixels over an outpaint result; wire to Stitch Inpaint.", - "The picked source frame before rotation, crop, or padding, as a BHWC RGB batch.", - "Output width after the transform.", - "Output height after the transform.", + "The picked source frame before rotation, crop, padding, or resize, as a BHWC RGB batch.", + "Output width after the transform and any resize.", + "Output height after the transform and any resize.", ) FUNCTION = "load_transform" @@ -94,11 +102,19 @@ class AusBossVideoCropRotatePad: seek_mode: str, frame_index: int, frame_time: float, + resize_to_megapixels=False, + megapixels=1.0, + resize_method="lanczos", + resolution_steps=1, **values, ): path = resolve_video_path(source_mode, video, local_path) frame, _, _ = decode_video_frame(path, seek_mode, frame_index, frame_time) output, mask, geometry = transform_pil_batch([frame], spec_from_values(**values)) + if resize_to_megapixels: + output, mask = resize_batch_to_megapixels( + output, mask, float(megapixels), str(resize_method), int(resolution_steps) + ) stitcher = build_transform_stitcher( output, mask, geometry, 32, source="Video Crop + Rotate + Pad -> Frame" ) @@ -131,6 +147,7 @@ class AusBossVideoCropRotatePad: except Exception: path = local_path if source_mode == "local path" else video spec = spec_from_values(**values) + resize = {name: values.get(name) for name in resize_inputs()} return stable_file_fingerprint( path, { @@ -140,6 +157,7 @@ class AusBossVideoCropRotatePad: "frame_index": int(frame_index), "frame_time": round(float(frame_time), 6), **spec.__dict__, + **resize, }, ) diff --git a/tests/test_transform_outputs.py b/tests/test_transform_outputs.py index ddf58dd..fec5797 100644 --- a/tests/test_transform_outputs.py +++ b/tests/test_transform_outputs.py @@ -55,6 +55,22 @@ class TransformOutputTests(unittest.TestCase): self.assertEqual(tuple(original.shape), (1, 96, 128, 3)) self.assertEqual(tuple(apply_stitch(stitcher, image).shape), tuple(image.shape)) + @unittest.skipUnless(COMFY_ROOT, "Set AUSBOSS_COMFY_ROOT for core resize integration") + def test_video_frame_resizes_to_a_budget_like_the_image_node(self): + with patch('nodes.node_video_crop_rotate_pad.resolve_video_path', return_value=Path('source.mp4')), patch('nodes.node_video_crop_rotate_pad.decode_video_frame', return_value=(self.source, 0, 0)): + image, mask, stitcher, original, width, height = AusBossVideoCropRotatePad().load_transform( + 'source.mp4', 'input folder', '', 'frame index', 0, 0, + resize_to_megapixels=True, megapixels=.05, resolution_steps=16, pad_left=32, feather=0, + ) + self.assertEqual((width, height), (int(image.shape[2]), int(image.shape[1]))) + # 0.05 MP x 1024 x 1024 is larger than the 160 x 96 canvas: the + # budget scales the frame to it either way, aspect kept, to steps of 16. + budget = 0.05 * 1024 * 1024 + self.assertLess(abs(width * height - budget) / budget, 0.1) + self.assertEqual((width % 16, height % 16), (0, 0)) + self.assertEqual(tuple(stitcher['canvas'].shape), tuple(image.shape)) + self.assertEqual(tuple(original.shape), (1, 96, 128, 3)) + if __name__ == '__main__': unittest.main()