Frame Delay
Frame Delay Is One Small Stateful Trick — and It Powers Every Motion Trick in This Pack
- image
- previous_image
Frame Delay is the least glamorous node in comfyui-frame-step and the one everything else leans on. It's a one-sample delay - signal-processing people write it z^-1 - which means it holds onto the image you fed it last run and gives you back that one, while storing the current image for next time. Two of the pack's other tricks (DetectMovingArea, and the frame-difference lesson) are built directly on this.
Why would you want the previous frame? Because motion is only visible in the difference between frames, and difference needs two frames at once. Feed the current frame and the delayed one into a comparison node and suddenly you can detect what changed. It's the same reason video encoders keep a reference frame around - the idea is older than diffusion models and it still does most of the work.
How it works (this is the interesting part)
In most frameworks a node like this would be trivial: store state, return it. In ComfyUI it's a small feat of trickery, because ComfyUI caches node results and skips re-running anything whose inputs look unchanged. A delay line whose output depends on call history breaks that assumption.
The fix is IS_CHANGED returning NaN - the same idiom a random picker or a clock uses. NaN != NaN, so ComfyUI can never consider this node unchanged, and it re-runs every time. That's exactly right for a delay line, and it's worth knowing for two reasons: you can't reproduce a "previous" result by re-running with identical inputs (that's inherent to the concept), and this node will always show as dirty in the cache - that's by design, not a bug.
The node also relies on ComfyUI keeping the same node instance alive across runs (cached by its unique_id). If you delete it from the graph and add it fresh, its memory of the previous frame is gone and it starts over.
Inputs and output
One input, one output, both plain IMAGE:
image- the current frame.previous_image- the frame from the last run.
That's the whole thing. On the very first call there's no previous frame yet, so it returns an all-zero image of the same shape - the standard DSP convention for a delay line's zero initial condition. Your first output will look black. That's not a bug, it's the node telling you "I have no memory yet."
Wiring it
The canonical pattern is a Video Frame Reader stepping through a clip:
LoadVideo → KU_VideoFrameReader → KU_FrameDelay ──► (previous_image)
└────────────────► (current frame, straight from reader)
Both wires go into DetectMovingArea: the reader's current frame as image_a, the delay's previous_image as image_b. You can also build a motion echo by feeding the delayed output back into an animation loop. Because it's just IMAGE in and IMAGE out, it chains anywhere an image chain already works.
Installing it
It ships in comfyui-frame-step, so install the pack once for all four nodes. Via ComfyUI Manager, search "comfyui-frame-step", or:
cd ComfyUI/custom_nodes
git clone https://github.com/NobutakaKuroki/comfyui-frame-step
cd comfyui-frame-step && pip install -r requirements.txt
Restart ComfyUI after. Dependencies are only av and opencv-python-headless, and there are no model downloads anywhere in the pack - it's teaching code from Dr. Nobutaka Kuroki at Kobe University, MIT-licensed. The bundled workflows (Frame Delay, Motion Echo, Frame Difference) come with a sample clip in examples/ to copy into your input folder.
Where people get burned
The black first frame trips everyone up once - it's the zero initial condition, move past it. The bigger gotcha is re-adding the node mid-workflow: if you delete and re-add it, its memory resets, and suddenly your difference looks wrong for one run. Keep the node in place once the graph is wired.
Inputs (1)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| previous_image | IMAGE | — |