Add the megapixel resize to Video Crop + Rotate + Pad → Frame

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.
This commit is contained in:
ausboss
2026-09-24 00:31:55 -05:00
parent 83b8f299f1
commit db7535751d
4 changed files with 42 additions and 9 deletions
+1 -1
View File
@@ -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 🆎
+2 -3
View File
@@ -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"]
+23 -5
View File
@@ -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,
},
)
+16
View File
@@ -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()