feat(easy_image_batch): source_batch + source_frames list + selected_… (#134)
* feat(easy_image_batch): source_batch + source_frames list + selected_* outputs Three-mode input system, two new "selected only" outputs, and an alpha output convention that's explicit about its role. Inputs - source_batch (IMAGE, optional): pre-batched stack, e.g. straight from Load Video. - source_frames (multiline STRING): VFX frame numbers separated by commas, newlines, spaces or any mix. Picks from source_batch and places at the same timeline position. Removes the 4-keyframe ceiling. - placeholder_color: now offers Black / Gray / White (was Black / Gray). - invert_alpha (BOOLEAN, default off = "inpaint"): flips alpha to compositing convention (selected=white) when on. - image1 is now optional (you can drive the node entirely from source_batch + source_frames). At least one of image1 or source_batch must be connected. - start_frame default changed from 0 -> 1 to match VFX 1-based numbering. Mode rules - Mode 1 -- per-slot images: connect imageN, set imageN_frame. - Mode 2 -- source_batch + 4 slots: when imageN is unconnected, imageN_frame doubles as the source pick index AND timeline target. - Mode 3 -- source_batch + source_frames list: when the list is non-empty, the 4 slots contribute ONLY where imageN is explicitly connected -- the imageN_frame defaults (0, 4, 8, 12) do NOT pick from source_batch in this mode, so the list fully controls the selection unless an imageN override is wired. - Priority: source_frames list runs first; manual slots run after and override at the same target index. Output dedup is automatic (set-based). Outputs - image_batch (IMAGE): full timeline with placeholders. Unchanged. - alpha_batch (MASK): renamed from mask_batch; same default convention (selected=0.0/black, empty=1.0/white) so existing wires work unchanged. Toggle invert_alpha for compositing-style. - selected_image_batch (IMAGE, NEW): only the keyframes that landed, packed back-to-back in timeline order. Empty (length 0) if none placed. - selected_frames (STRING, NEW): comma-separated VFX frame numbers, sorted ascending and deduped. Same format as the source_frames input -- round-trips into another easy_ImageBatch instance. Docs - Updated class docstring with input modes + worked example (surfaces in ComfyUI's node tooltip). - New per-node user guide: docs/user_guide/nodes/koolook_image/easy_image_batch.md. - docs/user_guide/README.md graduates easy_ImageBatch from "TBD" to a real link. - CHANGELOG: Unreleased Added/Changed entries cover the full change set. Backward compatibility - Existing workflows wired to image_batch + mask_batch keep working; the second output slot position is unchanged. - The 4-keyframe screenshot pattern (source_batch + 4 slots, no list) is unchanged in behaviour. - start_frame default change affects only newly added nodes; saved workflows keep the value they were saved with. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(easy_image_batch): cut window — rename start_frame -> cut_start_frame, fix source-pick math The 2nd output of Easy Image Batch (Koolook) gets a built-in cut window, replacing the workflow pattern of feeding the output through an external ImageFromBatch (batch_index, length) slicer. Conceptual rename - `start_frame` -> `cut_start_frame` everywhere. - The output now represents VFX frames [cut_start_frame .. cut_start_frame + total_frames - 1]. - Frames placed outside the cut window are dropped from the cut silently and listed in a single end-of-run summary log (replaces the previous per-frame "out of range" warnings). Math fix (real bug) The previous code conflated source pick index and output index under a single `target_index = vfx - start_frame`. That happened to work when start_frame=1 (source pick index = vfx - 1, by coincidence), but produced a wrong source pick whenever start_frame != 1. Now: source pick index = vfx_frame - 1 # hardcoded VFX 1-based output (cut) index = vfx_frame - cut_start_frame # cut window The two are validated independently: - source-side range error -> per-frame warning ("VFX frame N doesn't exist in source_batch") - output-side range -> silent drop, collected into the end-of-run summary Defaults - cut_start_frame=1 (was start_frame=1, so default behaviour for source_batch starting at VFX frame 1 is identical). - total_frames default 16 -> 81 (matches typical VFX shot length). - imageN_frame defaults 0/4/8/12 -> 1/5/9/13 (valid 1-based). - imageN_frame min 0 -> 1 (frame 0 is invalid under VFX 1-based). Console summary Always logged so it's easy to confirm what the node produced. Shape: [easy_ImageBatch] cut window: frames 41..121 (81 frames). 6 placed; 2 outside cut: 1, 27. Docs - Class docstring rewritten with the cut-window model + Examples A (cut_start_frame=1, no cut) and B (cut_start_frame=41, cut). - docs/user_guide/nodes/koolook_image/easy_image_batch.md fully updated to match. - CHANGELOG: new "Changed" entry under [Unreleased] explaining the rename, math fix, defaults bump, and migration notes. Migration notes for users of the previous PR commit - Saved workflows that had start_frame=1 (the default) keep producing identical output -- the math is unchanged in that case. - Saved workflows that set start_frame to a non-1 value will see different source picks (the old code was actually picking the wrong source frame, the new code picks correctly). Re-check those. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(easy_image_batch): shift imageN_frame defaults to 5/9/13/17 The previous 1/5/9/13 cluster put all four default keyframes inside the first 13 frames of an 81-frame timeline. Shifting to 5/9/13/17 keeps the same stride-of-4 but leaves a small gap at the start, which reads cleaner in the node UI and gives a more representative spread of where keyframes commonly land. Updates the doc table and CHANGELOG bullet to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
Cursor
parent
dc58e7b224
commit
bf02db8ad8
@@ -6,6 +6,93 @@ The format is inspired by Keep a Changelog and SemVer.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Easy Image Batch (`easy_ImageBatch`): "White" placeholder + `source_batch`
|
||||
input.** The `placeholder_color` dropdown now offers `Black / Gray / White`
|
||||
(fill values `0.0 / 0.5 / 1.0`); previously only Black and Gray were
|
||||
exposed. A new optional `source_batch` (IMAGE) input accepts a pre-batched
|
||||
image stack (e.g. straight from Load Video). When `source_batch` is
|
||||
connected and a slot's individual `imageN` is *not* connected, the node
|
||||
pulls frame `imageN_frame - start_frame` from `source_batch` — so
|
||||
`imageN_frame` controls both *which source frame to pick* and *where to
|
||||
place it on the output timeline*. This collapses the typical
|
||||
"Load Video → Get Images From Batch In Range × N → Easy Image Batch"
|
||||
helper chain into the node itself. Explicit `imageN` inputs still
|
||||
override the source-batch pick for that slot, so existing workflows keep
|
||||
working unchanged. `image1` is now optional too (you can drive the node
|
||||
entirely from `source_batch`); a clear error is raised if neither
|
||||
`image1` nor `source_batch` is connected.
|
||||
- **Easy Image Batch unlimited keyframes via `source_frames` field.** A new
|
||||
optional multiline STRING input (positioned right above `image1_frame`)
|
||||
accepts a list of VFX-numbered frames separated by commas, newlines,
|
||||
spaces, or any mix (e.g. `"1, 27, 41, 63, 85, 120"` or one number per
|
||||
line) — for sequences needing more than 4 keyframes from a
|
||||
`source_batch`. Each number both picks `source_batch[N - start_frame]`
|
||||
and places it at the same timeline position (same convention as the
|
||||
per-slot `imageN_frame` fields). Bad tokens warn and are skipped;
|
||||
out-of-range values warn and are skipped.
|
||||
|
||||
**List-vs-slot interaction rule:** when `source_frames` is non-empty,
|
||||
the 4 manual slots contribute ONLY where `imageN` is explicitly
|
||||
connected — the `imageN_frame` defaults (0, 4, 8, 12) do NOT pick from
|
||||
`source_batch` in this mode. So the list fully controls the selection,
|
||||
with explicit `imageN` connections used solely to override individual
|
||||
positions. When `source_frames` is empty, the original 4-keyframe
|
||||
behaviour is preserved (unconnected slots fall back to
|
||||
`source_batch[imageN_frame]`).
|
||||
|
||||
Priority order (later wins): `source_frames` list → 4 manual slots.
|
||||
Output dedup is automatic (set-based tracking).
|
||||
- **Easy Image Batch: two new outputs — `selected_image_batch` (IMAGE)
|
||||
and `selected_frames` (STRING).** `selected_image_batch` contains
|
||||
only the keyframes that actually landed, packed back-to-back in
|
||||
ascending timeline order with no placeholders (length 0 if nothing
|
||||
was placed). `selected_frames` is a comma-separated VFX-frame-number
|
||||
string of the placed keyframes (e.g. `"1, 27, 41, 63"`) — same
|
||||
format as the `source_frames` input, so the output round-trips
|
||||
cleanly into another `easy_ImageBatch` instance for chaining or
|
||||
re-using selections. Outputs are appended at slot positions 3 and 4,
|
||||
so existing wires on `image_batch` and `alpha_batch` are unaffected.
|
||||
|
||||
### Changed
|
||||
- **Easy Image Batch second output renamed `mask_batch` → `alpha_batch`,
|
||||
with the same value convention as before.** The default behaviour is
|
||||
unchanged — selected (occupied) frames = `0.0` (black), missing
|
||||
frames = `1.0` (white) — so existing workflows wire up identically.
|
||||
A new `invert_alpha` boolean toggle (default `false`, labels *"inpaint"*
|
||||
/ *"compositing"*) flips the alpha to compositing-style
|
||||
(`selected = 1.0, empty = 0.0`) when needed. The output slot keeps its
|
||||
position (second output) so wires re-attach automatically when the
|
||||
node reloads.
|
||||
- **Easy Image Batch: `start_frame` renamed to `cut_start_frame` and
|
||||
reinterpreted as a built-in cut window.** The output now represents
|
||||
VFX frames `[cut_start_frame .. cut_start_frame + total_frames - 1]`.
|
||||
Frames placed outside this window are silently dropped from the output
|
||||
(a single end-of-run summary lists them, replacing the previous
|
||||
per-frame "out of range" warnings). Math:
|
||||
|
||||
source pick index = vfx_frame - 1 (hardcoded VFX 1-based)
|
||||
output (cut) index = vfx_frame - cut_start_frame (cut window)
|
||||
|
||||
The two were previously conflated under `start_frame`, which produced
|
||||
the wrong source pick whenever `start_frame` differed from `1`. With
|
||||
the new model each slot picks `source_batch[vfx-1]` and places it at
|
||||
output index `vfx - cut_start_frame` independently.
|
||||
|
||||
Builds in the workflow that previously used an external
|
||||
`ImageFromBatch (batch_index, length)` to slice the Easy Image Batch
|
||||
output. Defaults: `cut_start_frame=1`, `total_frames=81` (was `16`),
|
||||
`imageN_frame` defaults bumped to `5, 9, 13, 17` (was `0, 4, 8, 12`)
|
||||
so they're valid VFX-1-based frame numbers.
|
||||
|
||||
**Migration note:** saved workflows that had `start_frame` set will
|
||||
load with the field's stored value into `cut_start_frame` (same field
|
||||
position, same units). Behaviour is preserved when `cut_start_frame=1`
|
||||
is the default and the source_batch starts at VFX frame 1; if you had
|
||||
set `start_frame` to a non-1 value, re-check that your source pick
|
||||
result is still what you want — under the old code, `start_frame=N`
|
||||
also offset the source-batch index, which the new model intentionally
|
||||
does not.
|
||||
### Fixed
|
||||
- **Snapshot Settings folder save no longer writes a snapshot.** Saving the
|
||||
library-path field in the Settings dialog no longer writes the in-memory
|
||||
|
||||
@@ -13,7 +13,7 @@ attribution is visible without needing a fork-shaped directory tree.
|
||||
| ComfyUI menu | Status | Nodes |
|
||||
|---|---|---|
|
||||
| `Koolook/Pipeline` | 🚧 page TBD | `EasyAIPipeline` |
|
||||
| `Koolook/Image` | 🚧 page TBD | `EasyResize_Koolook`, `easy_ImageBatch` |
|
||||
| `Koolook/Image` | 🚧 `EasyResize_Koolook` page TBD · ✅ [`easy_ImageBatch`](nodes/koolook_image/easy_image_batch.md) | `EasyResize_Koolook`, `easy_ImageBatch` |
|
||||
| `Koolook/Camera` | 🚧 page TBD | `KoolookLoadCameraPosesAbsolute` |
|
||||
| `Koolook/Wan_Video` | 🚧 page TBD | `EasyWan22Prompt` |
|
||||
| `Koolook/VAE` | ✅ [`nodes/radiance_koolook_v2_3_3/`](nodes/radiance_koolook_v2_3_3/) | `Easy_hdr_VAE_encode`, `Easy_hdr_VAE_decode` |
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# Easy Image Batch (Koolook)
|
||||
|
||||
| Aspect | Value |
|
||||
|---|---|
|
||||
| Display name | `Easy Image Batch (Koolook)` |
|
||||
| Node ID | `easy_ImageBatch` |
|
||||
| ComfyUI category | `Koolook/Image` |
|
||||
| Source | Koolook-native |
|
||||
| Source file | [`k_easy_image_batch.py`](../../../../k_easy_image_batch.py) |
|
||||
|
||||
## What it does
|
||||
|
||||
Places up to *N* keyframes on a 1-based VFX timeline, then outputs a
|
||||
**cut window** of that timeline as a fixed-length `IMAGE` batch. Empty
|
||||
positions are filled with a chosen placeholder colour (Black / 50 % Gray
|
||||
/ White). Pairs the image batch with an `alpha_batch` mask marking
|
||||
selected vs empty frames, and also returns just the in-cut keyframes
|
||||
packed back-to-back as a separate short-batch output.
|
||||
|
||||
Designed for sparse control sequences — e.g. feeding a video model like
|
||||
Wan 2.2 a 81-frame guide where only frames 41, 63, 78, 88, 98 and 121
|
||||
carry real image data and the rest tell the model "no latent update
|
||||
here". The cut window lets you grab any sub-range of a longer sequence
|
||||
without needing an external `ImageFromBatch` slicer.
|
||||
|
||||
## Inputs
|
||||
|
||||
### Required widgets
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `total_frames` | `INT` | `81` | Length of the cut window (= length of the output). Range: `1..1024`. |
|
||||
| `cut_start_frame` | `INT` | `1` | First VFX frame in the output. The output represents `[cut_start_frame .. cut_start_frame + total_frames - 1]`. Frames placed outside this window are dropped from the cut and listed in a single end-of-run summary log. |
|
||||
| `placeholder_color` | enum | `Black` | Fill colour for unoccupied frames in `image_batch`. `Black = 0.0`, `Gray = 0.5`, `White = 1.0`. Independent of `alpha_batch`. |
|
||||
| `invert_alpha` | `BOOLEAN` | `false` (`inpaint`) | Off = inpaint convention (selected = `0.0` black, empty = `1.0` white). On = compositing alpha (selected = `1.0`, empty = `0.0`). |
|
||||
| `source_frames` | multiline `STRING` | `""` | Optional list of VFX frame numbers to pick from `source_batch`. Tokens may be separated by commas, newlines, spaces, tabs, or any mix (`"1, 27\n41 63"` → `[1, 27, 41, 63]`). Each token picks `source_batch[N - 1]` and places it at output index `N - cut_start_frame`. Bad tokens warn and skip. Frames not present in `source_batch` warn; frames outside the cut window are dropped silently and reported in the end-of-run summary. |
|
||||
| `image1_frame` … `image4_frame` | `INT` | `5`, `9`, `13`, `17` | VFX frame number (1-based) for the slot's keyframe. In Mode 2 also used as the `source_batch` pick index. |
|
||||
|
||||
### Optional input slots
|
||||
|
||||
| Input | Type | Description |
|
||||
|---|---|---|
|
||||
| `source_batch` | `IMAGE` | Pre-batched image stack, e.g. straight from `Load Video`. `source_batch[0]` is treated as VFX frame 1; `source_batch[k]` is VFX frame `k + 1`. |
|
||||
| `image1` … `image4` | `IMAGE` | Per-slot keyframes. Each must be a single-frame `IMAGE` (batch size 1). Pre-batched stacks must come through `source_batch` instead. |
|
||||
|
||||
At least one of `image1` or `source_batch` must be connected; otherwise
|
||||
the node raises a clear `ValueError`. All connected inputs must share
|
||||
the same `H × W × C`.
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|---|---|---|
|
||||
| `image_batch` | `IMAGE` | The cut window as a `total_frames`-long batch starting at VFX frame `cut_start_frame`. Selected positions carry the picked images; everything else is the chosen placeholder colour. |
|
||||
| `alpha_batch` | `MASK` | Same length as `image_batch`. Default convention: selected = `0.0`, empty = `1.0`. Toggle `invert_alpha` to flip. |
|
||||
| `selected_image_batch` | `IMAGE` | **Only** the keyframes that actually landed inside the cut window, packed back-to-back in ascending order. Length = number of placed keyframes; empty (length 0) if nothing was placed. |
|
||||
| `selected_frames` | `STRING` | Comma-separated VFX frame numbers of the placed-inside-cut keyframes, sorted ascending and deduped (e.g. `"41, 63, 78, 88, 98, 121"`). Same format as the `source_frames` input — paste it (or wire it) into another `easy_ImageBatch` to reuse the same selection. |
|
||||
|
||||
## Frame-numbering model
|
||||
|
||||
Two indices are computed per VFX frame number `N`:
|
||||
|
||||
```text
|
||||
source pick index = N - 1 # source_batch[0] is VFX frame 1
|
||||
output (cut) index = N - cut_start_frame # cut window
|
||||
```
|
||||
|
||||
These are independent. A frame is *in the cut* iff its output index is
|
||||
in `[0, total_frames)`. The two are equal only when `cut_start_frame =
|
||||
1` (the default), so the previously-shipped behaviour is preserved by
|
||||
default.
|
||||
|
||||
There is no exposed knob to renumber `source_batch` (i.e. say "my video
|
||||
starts at VFX frame 100"). If your stack represents a different VFX
|
||||
range, renumber externally before connecting.
|
||||
|
||||
## Modes
|
||||
|
||||
The node supports three input modes that can be freely mixed.
|
||||
|
||||
### Mode 1 — Per-slot images (up to 4 keyframes)
|
||||
|
||||
Connect `imageN` and set `imageN_frame` to its VFX timeline position.
|
||||
The image lands at output index `imageN_frame - cut_start_frame` if
|
||||
that's inside the cut. No `source_batch` needed. Up to 4 keyframes
|
||||
total.
|
||||
|
||||
### Mode 2 — `source_batch` + the 4 slots (up to 4 keyframes from a video)
|
||||
|
||||
Connect a pre-batched stack to `source_batch` (e.g. from `Load Video`)
|
||||
and leave `imageN` unconnected. Each unconnected slot pulls
|
||||
`source_batch[imageN_frame - 1]` and places it at output index
|
||||
`imageN_frame - cut_start_frame` — collapsing the typical "Load Video →
|
||||
Get Images From Batch In Range × N → Easy Image Batch" helper chain
|
||||
into a single node.
|
||||
|
||||
This mode is active when `source_frames` is empty.
|
||||
|
||||
### Mode 3 — `source_batch` + `source_frames` list (unlimited keyframes)
|
||||
|
||||
Connect `source_batch` and fill `source_frames` with as many VFX frame
|
||||
numbers as you need. Removes the 4-keyframe ceiling.
|
||||
|
||||
**Important interaction rule:** when `source_frames` is non-empty, the
|
||||
4 manual slots contribute ONLY where `imageN` is explicitly connected —
|
||||
the `imageN_frame` defaults do **not** pick from `source_batch` in this
|
||||
mode. The list fully controls the selection, with explicit `imageN`
|
||||
connections used solely to override individual positions.
|
||||
|
||||
### Priority order
|
||||
|
||||
When more than one mode contributes:
|
||||
|
||||
1. `source_frames` list runs first.
|
||||
2. Manual slots (`image1` … `image4` with their `imageN_frame`) run
|
||||
after, so explicit `imageN` inputs always **override** list entries
|
||||
at the same cut-output position.
|
||||
3. The set of placed keyframes is deduped automatically (`selected_*`
|
||||
outputs reflect the final, deduped state).
|
||||
|
||||
## Conventions
|
||||
|
||||
- **1-based VFX numbering.** `imageN_frame`, `source_frames`,
|
||||
`cut_start_frame` are all 1-based. `source_batch[0]` is VFX frame 1.
|
||||
Frame `0` is invalid and out-of-source-range; frame `1` is the first
|
||||
real frame.
|
||||
- **Cut-window drops.** Frames placed outside the cut window aren't
|
||||
errors — they're intentionally absent. The console prints one summary
|
||||
line per run noting the cut range, the placed count, and any
|
||||
outside-cut drops.
|
||||
- **Alpha vs mask vocabulary.** `alpha_batch` is a `MASK`-typed output
|
||||
but follows whichever convention `invert_alpha` selects. The default
|
||||
(`inpaint`) matches the previous `mask_batch` semantics for backward
|
||||
compatibility (selected = black, empty = white).
|
||||
|
||||
## Example A — `cut_start_frame = 1` (no cut, full timeline)
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `source_batch` | `Load Video` (121 frames) |
|
||||
| `source_frames` | `"1, 27, 41, 63, 78, 88, 98, 121"` |
|
||||
| `cut_start_frame` | `1` |
|
||||
| `total_frames` | `121` |
|
||||
| `placeholder_color` | `Gray` |
|
||||
| `invert_alpha` | `false` (inpaint) |
|
||||
|
||||
| Output | What you get |
|
||||
|---|---|
|
||||
| `image_batch` | 121 frames; the 8 picks land at indices 0 / 26 / 40 / 62 / 77 / 87 / 97 / 120. Rest is gray. |
|
||||
| `alpha_batch` | 121-frame mask; those 8 indices are `0.0`, the other 113 are `1.0`. |
|
||||
| `selected_image_batch` | 8 frames, packed back-to-back in timeline order. |
|
||||
| `selected_frames` | `"1, 27, 41, 63, 78, 88, 98, 121"` |
|
||||
|
||||
Console summary: `cut window: frames 1..121 (121 frames). 8 placed.`
|
||||
|
||||
## Example B — `cut_start_frame = 41`, `total_frames = 81` (cut)
|
||||
|
||||
Same inputs as Example A, but with the cut window changed to start at
|
||||
VFX frame 41 for 81 frames.
|
||||
|
||||
| Output | What you get |
|
||||
|---|---|
|
||||
| `image_batch` | 81 frames representing VFX frames 41..121. Frames 41 / 63 / 78 / 88 / 98 / 121 land at output indices 0 / 22 / 37 / 47 / 57 / 80. Rest is gray. |
|
||||
| `alpha_batch` | 81-frame mask, 6 indices selected. |
|
||||
| `selected_image_batch` | 6 frames, packed back-to-back. |
|
||||
| `selected_frames` | `"41, 63, 78, 88, 98, 121"` |
|
||||
|
||||
Console summary: `cut window: frames 41..121 (81 frames). 6 placed; 2 outside cut: 1, 27.`
|
||||
|
||||
This replaces the workflow pattern of feeding the Easy Image Batch
|
||||
output through an external `ImageFromBatch (batch_index=40, length=81)`
|
||||
node — the cut is built in.
|
||||
|
||||
## Tips
|
||||
|
||||
- **Round-tripping selections.** Wire `selected_frames` of one node
|
||||
into `source_frames` of another to chain or fan out the same picks
|
||||
across workflows.
|
||||
- **Picking from a video without writing the list manually.** If you
|
||||
already have helper nodes producing single frames, plug them into
|
||||
`image1`…`image4` (Mode 1) and leave `source_frames` empty. That's
|
||||
the legacy 4-keyframe pattern, kept fully working.
|
||||
- **Going past 4 keyframes.** Switch to Mode 3: connect `source_batch`,
|
||||
fill `source_frames`, leave `imageN` unconnected. Use `image1`…
|
||||
`image4` only for *overrides* (e.g. patching one frame manually).
|
||||
- **Cutting an output.** Set `cut_start_frame` to the first VFX frame
|
||||
you want and `total_frames` to the length of the cut. No external
|
||||
slicer node needed.
|
||||
- **Inverted alpha for compositing.** Toggle `invert_alpha` on when you
|
||||
want to use the alpha output as a true compositing alpha (selected =
|
||||
white = visible).
|
||||
+343
-54
@@ -1,13 +1,127 @@
|
||||
import re
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
# Placeholder fill values (0..1) for unoccupied frames in the output IMAGE batch.
|
||||
# Alpha output is independent of this choice; see `invert_alpha` for convention.
|
||||
_PLACEHOLDER_FILL = {
|
||||
"Black": 0.0,
|
||||
"Gray": 0.5,
|
||||
"White": 1.0,
|
||||
}
|
||||
|
||||
|
||||
class easy_ImageBatch:
|
||||
"""
|
||||
A custom ComfyUI node for VFX workflows that takes 1-4 keyframe images with specified frame positions
|
||||
and creates an IMAGE batch sequence of a given total length, filling undefined frames with selected placeholder color (black or 50% gray).
|
||||
Also outputs a MASK batch where occupied frames are black (0.0) and empty are white (1.0), directly compatible with standard mask nodes.
|
||||
Supports VFX-style frame numbering with a start_frame offset (internal indices are 0-based).
|
||||
Useful for preparing sparse control sequences for video models like Wan 2.2, where placeholder frames indicate no latent update.
|
||||
Assumes all input images have identical dimensions.
|
||||
Place up to N keyframes on a 1-based VFX timeline, then output a
|
||||
*cut window* of that timeline as a fixed-length IMAGE batch with
|
||||
placeholder fill (Black / 50% Gray / White) for unoccupied frames.
|
||||
Returns four outputs:
|
||||
|
||||
- `image_batch` — IMAGE batch of length `total_frames`,
|
||||
representing the cut window starting at VFX frame
|
||||
`cut_start_frame` and lasting `total_frames` frames. Selected
|
||||
positions carry the picked images; everything else is the
|
||||
placeholder.
|
||||
- `alpha_batch` — MASK at the same length. Default
|
||||
convention: selected = 0.0 (black), empty = 1.0 (white). Toggle
|
||||
`invert_alpha` flips to compositing-style (selected = 1.0).
|
||||
- `selected_image_batch` — IMAGE containing only the keyframes that
|
||||
actually landed *inside the cut*, packed back-to-back in
|
||||
ascending order (no placeholders). Empty if nothing was placed.
|
||||
- `selected_frames` — STRING, comma-separated VFX frame
|
||||
numbers of the placed-inside-cut keyframes (e.g.
|
||||
`"41, 63, 78, 88, 98, 121"`). Same format as the `source_frames`
|
||||
input → round-trippable into another `easy_ImageBatch`.
|
||||
|
||||
Frame-numbering conventions
|
||||
---------------------------
|
||||
All frame numbers (`source_frames`, `imageN_frame`, `cut_start_frame`)
|
||||
are **VFX-numbered, 1-based**. Internally:
|
||||
|
||||
source pick index = vfx_frame - 1 # always
|
||||
output (cut) index = vfx_frame - cut_start_frame # cut window
|
||||
|
||||
`source_batch[0]` is treated as VFX frame 1. There is no exposed
|
||||
knob to renumber `source_batch` — if your stack represents a
|
||||
different VFX range, renumber externally before connecting.
|
||||
|
||||
Cut window
|
||||
----------
|
||||
The output represents VFX frames `[cut_start_frame ..
|
||||
cut_start_frame + total_frames - 1]`. Frames outside this window
|
||||
are placed but silently *dropped from the cut* (a single summary
|
||||
line is logged at the end listing them). Frames inside the window
|
||||
are placed at output index `vfx - cut_start_frame`. The default
|
||||
`cut_start_frame = 1` means the cut starts at VFX frame 1, so the
|
||||
full timeline shows up — equivalent to "no cut".
|
||||
|
||||
Three input modes, freely mixable
|
||||
---------------------------------
|
||||
1. Per-slot images (up to 4): connect `imageN` and set `imageN_frame`
|
||||
to its VFX timeline position. The image lands at output index
|
||||
`imageN_frame - cut_start_frame` if that's inside the cut.
|
||||
2. `source_batch` (optional pre-batched IMAGE, e.g. straight from
|
||||
Load Video) + the 4 slots: when a slot's `imageN` is *not*
|
||||
connected, the node pulls VFX frame `imageN_frame` from
|
||||
`source_batch` (i.e. `source_batch[imageN_frame - 1]`) and
|
||||
places it at output index `imageN_frame - cut_start_frame`.
|
||||
Collapses the "Load Video → Get Images From Batch In Range × N"
|
||||
helper chain into a single node.
|
||||
3. `source_batch` + `source_frames` (optional comma/newline/whitespace
|
||||
separated string like `"1, 27, 41, 63, 85, 120"`): for keyframe
|
||||
counts beyond 4. Each number picks
|
||||
`source_batch[N - 1]` and places it at output index
|
||||
`N - cut_start_frame` (if inside the cut). Bad tokens warn and
|
||||
are skipped; frames not present in `source_batch` warn; frames
|
||||
outside the cut window are dropped silently and summarised at
|
||||
the end.
|
||||
|
||||
Important: when `source_frames` is non-empty, the 4 manual slots
|
||||
contribute ONLY where `imageN` is explicitly connected — the
|
||||
`imageN_frame` defaults do NOT pick from `source_batch` in this
|
||||
mode. The list fully controls the selection, with explicit
|
||||
`imageN` connections used solely to override individual positions.
|
||||
|
||||
Priority order (later wins): `source_frames` list → 4 manual slots.
|
||||
Explicit `imageN` inputs always override list entries at the same
|
||||
cut-output position. Output dedup is automatic (set-based). At least
|
||||
one of `image1` or `source_batch` must be provided. All keyframes
|
||||
(and `source_batch` frames) must share the same H/W/C.
|
||||
|
||||
Useful for preparing sparse control sequences for video models like
|
||||
Wan 2.2, where placeholder frames indicate no latent update.
|
||||
|
||||
Example A — cut_start_frame=1 (no cut, full timeline)
|
||||
-----------------------------------------------------
|
||||
Inputs:
|
||||
source_batch = Load Video (121 frames)
|
||||
source_frames = "1, 27, 41, 63, 78, 88, 98, 121"
|
||||
cut_start_frame = 1
|
||||
total_frames = 121
|
||||
placeholder_color = "Gray"
|
||||
|
||||
Outputs:
|
||||
image_batch — 121 frames; the 8 picked frames sit at
|
||||
their natural positions (1→idx 0,
|
||||
27→idx 26, …, 121→idx 120). Rest is gray.
|
||||
selected_image_batch — 8 frames, packed back-to-back.
|
||||
selected_frames — "1, 27, 41, 63, 78, 88, 98, 121"
|
||||
|
||||
Example B — cut_start_frame=41, total_frames=81 (cut)
|
||||
-----------------------------------------------------
|
||||
Same inputs as Example A, but cut_start_frame=41 / total_frames=81.
|
||||
|
||||
Outputs:
|
||||
image_batch — 81 frames representing VFX 41..121.
|
||||
Frames 41/63/78/88/98/121 land at output
|
||||
indices 0/22/37/47/57/80. Frames 1, 27
|
||||
are *outside the cut* and dropped.
|
||||
selected_image_batch — 6 frames (the in-cut picks).
|
||||
selected_frames — "41, 63, 78, 88, 98, 121"
|
||||
Console summary: — "cut window: frames 41..121 (81 frames).
|
||||
6 placed; 2 outside cut: 1, 27."
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -15,52 +129,63 @@ class easy_ImageBatch:
|
||||
return {
|
||||
"required": {
|
||||
"total_frames": ("INT", {
|
||||
"default": 16,
|
||||
"default": 81,
|
||||
"min": 1,
|
||||
"max": 1024,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"start_frame": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"cut_start_frame": ("INT", {
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 999999,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"placeholder_color": (["Black", "Gray"], {
|
||||
"placeholder_color": (list(_PLACEHOLDER_FILL.keys()), {
|
||||
"default": "Black"
|
||||
}),
|
||||
"image1": ("IMAGE", ),
|
||||
"invert_alpha": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"label_on": "compositing",
|
||||
"label_off": "inpaint",
|
||||
}),
|
||||
"source_frames": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "extra frames from source_batch, e.g. 1, 27, 41, 63, 85 (commas and/or newlines)",
|
||||
}),
|
||||
"image1_frame": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"default": 5,
|
||||
"min": 1,
|
||||
"max": 999999,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"source_batch": ("IMAGE", ),
|
||||
"image1": ("IMAGE", ),
|
||||
"image2": ("IMAGE", ),
|
||||
"image2_frame": ("INT", {
|
||||
"default": 4,
|
||||
"min": 0,
|
||||
"default": 9,
|
||||
"min": 1,
|
||||
"max": 999999,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"image3": ("IMAGE", ),
|
||||
"image3_frame": ("INT", {
|
||||
"default": 8,
|
||||
"min": 0,
|
||||
"default": 13,
|
||||
"min": 1,
|
||||
"max": 999999,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
}),
|
||||
"image4": ("IMAGE", ),
|
||||
"image4_frame": ("INT", {
|
||||
"default": 12,
|
||||
"min": 0,
|
||||
"default": 17,
|
||||
"min": 1,
|
||||
"max": 999999,
|
||||
"step": 1,
|
||||
"display": "number"
|
||||
@@ -68,52 +193,216 @@ class easy_ImageBatch:
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", )
|
||||
RETURN_NAMES = ("image_batch", "mask_batch", )
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "IMAGE", "STRING", )
|
||||
RETURN_NAMES = ("image_batch", "alpha_batch", "selected_image_batch", "selected_frames", )
|
||||
FUNCTION = "create_batch"
|
||||
CATEGORY = "Koolook/Image"
|
||||
OUTPUT_NODE = False # Not necessarily an output node, but can be chained
|
||||
|
||||
def create_batch(self, total_frames, start_frame, placeholder_color, image1, image1_frame, image2=None, image2_frame=None, image3=None, image3_frame=None, image4=None, image4_frame=None):
|
||||
if image1.shape[0] != 1:
|
||||
raise ValueError("Each input should be a single IMAGE (batch size 1), not a pre-batched tensor.")
|
||||
def create_batch(
|
||||
self,
|
||||
total_frames,
|
||||
cut_start_frame,
|
||||
placeholder_color,
|
||||
invert_alpha,
|
||||
source_frames,
|
||||
image1_frame,
|
||||
source_batch=None,
|
||||
image1=None,
|
||||
image2=None,
|
||||
image2_frame=None,
|
||||
image3=None,
|
||||
image3_frame=None,
|
||||
image4=None,
|
||||
image4_frame=None,
|
||||
):
|
||||
# Validate per-slot image inputs (must be single-frame tensors,
|
||||
# not pre-batched). source_batch is the only accepted multi-frame input.
|
||||
for slot_name, slot_img in (
|
||||
("image1", image1),
|
||||
("image2", image2),
|
||||
("image3", image3),
|
||||
("image4", image4),
|
||||
):
|
||||
if slot_img is not None and slot_img.shape[0] != 1:
|
||||
raise ValueError(
|
||||
f"{slot_name} should be a single IMAGE (batch size 1), not a pre-batched "
|
||||
"tensor. Use the 'source_batch' input for pre-batched stacks."
|
||||
)
|
||||
|
||||
h, w, c = image1.shape[1:]
|
||||
device = image1.device
|
||||
dtype = image1.dtype
|
||||
# Determine reference H/W/C/device/dtype from the first available source.
|
||||
# Priority: image1 → source_batch → image2 → image3 → image4.
|
||||
reference = None
|
||||
for candidate in (image1, source_batch, image2, image3, image4):
|
||||
if candidate is not None:
|
||||
reference = candidate
|
||||
break
|
||||
if reference is None:
|
||||
raise ValueError(
|
||||
"easy_ImageBatch needs at least one of: image1, source_batch, image2, image3, image4."
|
||||
)
|
||||
|
||||
# Vectorized creation of image_batch
|
||||
fill_value = 0.0 if placeholder_color == "Black" else 0.5
|
||||
h, w, c = reference.shape[1:]
|
||||
device = reference.device
|
||||
dtype = reference.dtype
|
||||
|
||||
# Cross-check that all provided inputs share H/W/C.
|
||||
for label, tensor in (
|
||||
("image1", image1),
|
||||
("source_batch", source_batch),
|
||||
("image2", image2),
|
||||
("image3", image3),
|
||||
("image4", image4),
|
||||
):
|
||||
if tensor is not None and tensor.shape[1:] != (h, w, c):
|
||||
raise ValueError(
|
||||
f"{label} shape {tuple(tensor.shape[1:])} does not match reference "
|
||||
f"({h}, {w}, {c}). All inputs must share the same H/W/C."
|
||||
)
|
||||
|
||||
fill_value = _PLACEHOLDER_FILL[placeholder_color]
|
||||
image_batch = torch.full((total_frames, h, w, c), fill_value, device=device, dtype=dtype)
|
||||
# Alpha (default convention): 1.0 (white) = empty, set to 0.0 on occupied frames below.
|
||||
# `invert_alpha` flips the result to compositing-style at the end.
|
||||
alpha_batch = torch.ones((total_frames, h, w), device=device, dtype=torch.float32)
|
||||
|
||||
# Vectorized creation of mask_batch (single channel), initialized to 1.0 (white for empty)
|
||||
mask_batch = torch.ones((total_frames, h, w), device=device, dtype=torch.float32)
|
||||
# Track timeline indices that actually got a keyframe placed. Used at the
|
||||
# end to build `selected_image_batch` (just the placed frames, packed)
|
||||
# and `selected_frames` (their VFX numbers as a comma-separated string).
|
||||
placed_indices: set[int] = set()
|
||||
# VFX frame numbers that were referenced but landed outside the cut
|
||||
# window. Collected once and printed as a single summary line at the
|
||||
# end so each frame doesn't generate its own console warning.
|
||||
outside_cut: list[int] = []
|
||||
|
||||
# Collect keyframes and compute indices
|
||||
keyframes = [(image1[0], image1_frame)]
|
||||
if image2 is not None and image2_frame is not None:
|
||||
if image2.shape[1:] != (h, w, c):
|
||||
raise ValueError("All images must have the same height, width, and channels.")
|
||||
keyframes.append((image2[0], image2_frame))
|
||||
if image3 is not None and image3_frame is not None:
|
||||
if image3.shape[1:] != (h, w, c):
|
||||
raise ValueError("All images must have the same height, width, and channels.")
|
||||
keyframes.append((image3[0], image3_frame))
|
||||
if image4 is not None and image4_frame is not None:
|
||||
if image4.shape[1:] != (h, w, c):
|
||||
raise ValueError("All images must have the same height, width, and channels.")
|
||||
keyframes.append((image4[0], image4_frame))
|
||||
source_len = source_batch.shape[0] if source_batch is not None else 0
|
||||
|
||||
# Place keyframes
|
||||
for img, f in keyframes:
|
||||
index = f - start_frame
|
||||
if 0 <= index < total_frames:
|
||||
image_batch[index] = img
|
||||
mask_batch[index] = 0.0 # Black (0.0) for occupied
|
||||
# 1) source_frames list (optional, additive). Each token is a VFX-numbered
|
||||
# frame: it picks source_batch[N - 1] (VFX 1-based, hardcoded) AND
|
||||
# places it at output index N - cut_start_frame. The 4 manual slots
|
||||
# run after this and override per-position when needed.
|
||||
#
|
||||
# The field is multiline: tokens may be separated by commas, newlines,
|
||||
# spaces, tabs, or any mix of them ("1, 27\n41 63" → [1, 27, 41, 63]).
|
||||
source_frames_str = (source_frames or "").strip()
|
||||
if source_frames_str:
|
||||
if source_batch is None:
|
||||
print(
|
||||
"[easy_ImageBatch] source_frames is set but source_batch is not connected; "
|
||||
"ignoring the list (it only picks frames from source_batch)."
|
||||
)
|
||||
else:
|
||||
print(f"Warning: Frame {f} maps to index {index} which is out of range (0 to {total_frames-1}); ignoring this keyframe.")
|
||||
tokens = [t for t in re.split(r"[,\s]+", source_frames_str) if t]
|
||||
for tok in tokens:
|
||||
try:
|
||||
f = int(tok)
|
||||
except ValueError:
|
||||
print(
|
||||
f"[easy_ImageBatch] source_frames: ignoring non-integer token '{tok}'."
|
||||
)
|
||||
continue
|
||||
source_index = f - 1
|
||||
output_index = f - cut_start_frame
|
||||
if not (0 <= source_index < source_len):
|
||||
print(
|
||||
f"[easy_ImageBatch] source_frames: VFX frame {f} doesn't exist in "
|
||||
f"source_batch (size {source_len}, covers frames 1..{source_len}); "
|
||||
"skipping."
|
||||
)
|
||||
continue
|
||||
if not (0 <= output_index < total_frames):
|
||||
outside_cut.append(f)
|
||||
continue
|
||||
image_batch[output_index] = source_batch[source_index]
|
||||
alpha_batch[output_index] = 0.0
|
||||
placed_indices.add(output_index)
|
||||
|
||||
return (image_batch, mask_batch, )
|
||||
# 2) Resolve each manual slot to (frame_image, timeline_frame). A slot
|
||||
# contributes iff (a) its imageN is connected, or (b) source_batch
|
||||
# is connected (in which case imageN_frame doubles as the source
|
||||
# pick index). Manual slots run AFTER the source_frames list so an
|
||||
# explicit imageN always wins at its target position.
|
||||
slots = (
|
||||
(image1, image1_frame, "image1"),
|
||||
(image2, image2_frame, "image2"),
|
||||
(image3, image3_frame, "image3"),
|
||||
(image4, image4_frame, "image4"),
|
||||
)
|
||||
|
||||
# When the user is driving the node from `source_frames`, the 4 manual
|
||||
# `imageN_frame` defaults (0, 4, 8, 12) shouldn't sneak in extra picks
|
||||
# from source_batch — only the explicit list (and any explicitly
|
||||
# connected imageN inputs) should contribute. So the source_batch
|
||||
# fallback for an UN-connected slot is only active when the list is
|
||||
# empty (the original 4-keyframe workflow).
|
||||
slot_source_fallback = source_batch is not None and not source_frames_str
|
||||
|
||||
for slot_img, slot_frame, slot_label in slots:
|
||||
if slot_frame is None:
|
||||
continue # Optional slot with no frame value — skip.
|
||||
|
||||
output_index = slot_frame - cut_start_frame
|
||||
|
||||
if slot_img is not None:
|
||||
frame_tensor = slot_img[0]
|
||||
elif slot_source_fallback:
|
||||
# Pull VFX frame `slot_frame` from source_batch (1-based).
|
||||
source_index = slot_frame - 1
|
||||
if not (0 <= source_index < source_len):
|
||||
print(
|
||||
f"[easy_ImageBatch] {slot_label}: VFX frame {slot_frame} doesn't "
|
||||
f"exist in source_batch (size {source_len}, covers frames "
|
||||
f"1..{source_len}); skipping."
|
||||
)
|
||||
continue
|
||||
frame_tensor = source_batch[source_index]
|
||||
else:
|
||||
# Either source_batch isn't connected, or source_frames is
|
||||
# being used and no explicit imageN was wired — leave this
|
||||
# slot empty so it doesn't pollute the selection.
|
||||
continue
|
||||
|
||||
if 0 <= output_index < total_frames:
|
||||
image_batch[output_index] = frame_tensor
|
||||
alpha_batch[output_index] = 0.0
|
||||
placed_indices.add(output_index)
|
||||
else:
|
||||
outside_cut.append(slot_frame)
|
||||
|
||||
if invert_alpha:
|
||||
alpha_batch = 1.0 - alpha_batch
|
||||
|
||||
# Build the "selected only" outputs: keyframes packed back-to-back in
|
||||
# ascending timeline order, plus their VFX frame numbers as a string
|
||||
# that round-trips cleanly into the `source_frames` input.
|
||||
sorted_indices = sorted(placed_indices)
|
||||
if sorted_indices:
|
||||
selected_image_batch = image_batch[sorted_indices]
|
||||
else:
|
||||
# No keyframes placed — emit a 0-length batch with the same H/W/C.
|
||||
selected_image_batch = image_batch[:0]
|
||||
selected_frames_out = ", ".join(
|
||||
str(idx + cut_start_frame) for idx in sorted_indices
|
||||
)
|
||||
|
||||
# One-line summary of the cut window. Always logged so it's easy to
|
||||
# confirm what the node produced; mentions outside-cut drops when any.
|
||||
cut_end = cut_start_frame + total_frames - 1
|
||||
if outside_cut:
|
||||
unique_outside = sorted(set(outside_cut))
|
||||
outside_str = ", ".join(str(f) for f in unique_outside)
|
||||
print(
|
||||
f"[easy_ImageBatch] cut window: frames {cut_start_frame}..{cut_end} "
|
||||
f"({total_frames} frames). {len(sorted_indices)} placed; "
|
||||
f"{len(unique_outside)} outside cut: {outside_str}."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"[easy_ImageBatch] cut window: frames {cut_start_frame}..{cut_end} "
|
||||
f"({total_frames} frames). {len(sorted_indices)} placed."
|
||||
)
|
||||
|
||||
return (image_batch, alpha_batch, selected_image_batch, selected_frames_out, )
|
||||
|
||||
# Node mappings for ComfyUI registration
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
|
||||
Reference in New Issue
Block a user