Nodes/was-node-suite-comfyui/Camera Motion Trajectory from Images
ComfyUI Node Runs on cloud

Camera Motion Trajectory from Images

A parallax camera move over one still

By WASasquatch·Created 4 years ago·Updated a day ago· 1,864
Camera Motion Trajectory from Images
  • image
  • depth_map
  • video
  • frame_count
◄num_frames60►
◄trajectory_spec{ "loop": false, "default_ease": "linear", "keyframes": [ { "frame": 0, "zoom": 1.0, "center": [0.5, 0.5], "angle": 0.0, "pan": [0.0, 0.0], "tilt": [0.0, 0.0], "dolly_strength": 0.0, "dolly_radius": [0.3, 0.3], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "ease_in_out" }, { "frame": 30, "zoom": 1.5, "center": [0.5, 0.5], "angle": 90.0, "pan": [0.0, 0.0], "tilt": [0.0, 0.15], "dolly_strength": 0.3, "dolly_radius": [0.35, 0.35], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "ease_in_out" }, { "frame": 59, "zoom": 2.0, "center": [0.6, 0.4], "angle": 360.0, "pan": [0.1, 0.0], "tilt": [0.0, 0.3], "dolly_strength": 0.6, "dolly_radius": [0.4, 0.4], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "linear" } ] }►
◄edge_modemirror►
◄enable_camera_shakefalse►
◄shake_position_amplitude0.030►
◄shake_rotation_amplitude1.5►
◄shake_seed0►

Image-to-video models will happily zoom and pan for you, and they'll also redraw the face, invent a sixth finger, and occasionally decide the wall is now a window. If all you want is a camera move - a slow push in, a pan across a landscape, a fisheye drift for a title card - you don't need a diffusion pass at all. You need a crop with keyframes.

This node does exactly that. It moves a virtual camera over a still photograph or render and emits the frames as an IMAGE batch, at the source's resolution, ready for a video writer. Nothing is generated. What's in the picture is what stays in the picture.

The spec is JSON, and that's fine

trajectory_spec is a JSON string: a keyframes list, each entry carrying a frame number and the camera properties set at that point. The default spec is a three-key slow push-in with a full rotation, ending off-centre with a mild tilt and dolly - a good thing to read before writing your own.

Each keyframe can set zoom, center, angle, pan, tilt, dolly_strength, sphereize_strength and depth_strength, plus an ease curve shaping the run to the next keyframe. default_ease covers the ones that don't say. Two behaviours worth knowing: a property a later keyframe leaves out keeps moving at the speed the earlier one gave it (so you don't have to restate zoom at every key), and "loop": true wraps the last keyframe back round to the first, which is how you get a seamless looping shot.

Empty text holds the picture still, which is a legitimately useful way to find out how much of your move is coming from the camera and how much from the easing.

The depth map is the whole parallax trick

depth_map is optional and it's the difference between a flat pan and something that reads as a real camera. Wire in a depth map - white near, black far, any size - and depth_strength in the spec holds the far parts of the scene back while the near parts move fully. Background moves less, foreground moves more, and the shot gets that three-dimensional quality that a straight zoom can never fake. Leave it disconnected and everything moves as one plane, which is fine for a slight push and obvious the moment you pan.

Depth estimation is its own solved corner of the ecosystem now - MiDaS, Depth Anything, Marigold - so a depth pass on a single image is cheap. It's the one extra step this node is really worth.

edge_mode handles what fills the frame when the camera looks past the picture: mirror reflects it back and is the least visible, border smears the edge pixels, and wrap brings the opposite edge round, which suits a seamless texture and looks bizarre on a portrait.

Handheld, because tripods look fake

enable_camera_shake (off by default) adds wobble on top of the keyframed move. shake_position_amplitude is drift as a share of the frame - 0.01 is a barely visible breath, 0.03 a natural handheld hold, 0.2 a running shot - and shake_rotation_amplitude in degrees: 1.5 reads as a steady hand, 10 as an unsteady one, 0 for drift with no roll.

shake_seed fixes the wobble so a shot can be re-rendered identically after you change something else - which matters more than it sounds, because "the handheld version of that good take" is otherwise unrepeatable.

Inputs, outputs and the batch behaviour

image is the picture; num_frames is how long the shot is (60 frames is two and a half seconds at 24fps) and keyframe numbers get clamped into that range. There's a nice detail in the batch handling: if you feed a batch that holds exactly num_frames images, each output frame uses its own source frame - otherwise the batch is cycled. So you can drive this with an animated input and get temporal behaviour too.

Outputs: video, the rendered frames in order at the input's size, and frame_count, so the number goes straight into a video writer or a duration calculation without being typed again.

Where it goes next is your call: straight into a video encode for a Ken Burns segment, or - the interesting one - as input to an image-to-video model, which gets a far easier problem from a real camera move than from a flat still. Parallax is hard to prompt and easy to fake, and camera work is still one of the places those models visibly struggle.

Installing it

It ships in WAS Node Suite v3 - MIT, by WASasquatch, 468 nodes spanning images, masks, latents, text, files, animation and video:

cd ComfyUI/custom_nodes
git clone https://github.com/WASasquatch/was-node-suite-comfyui.git

ComfyUI Manager, search WAS Node Suite v3, is the recommended install. ComfyUI 0.14.0+ and Python 3.10+; the pack installs no Python packages and downloads nothing, because the affine sampling behind this node is torch, on ComfyUI's own device. First start after install is a second slower while config.yaml and the pack's folders are written under <ComfyUI user dir>/was-node-suite/; if the animation nodes are missing from your menu, check the feature groups there before reinstalling.

The trap

Non-integer zooms on low-resolution art. Zooming past the source's actual detail gives you resampled mush and a visible softness ramp, and no amount of easing hides it. Scale the picture up first, then move the camera over it.

CategoryWAS Suite/Animation

Inputs (9)

NameTypeDefaultDescription
imageIMAGEThe picture the camera moves over. A batch is read as source frames: when it holds exactly num_frames images each output frame uses its own, otherwise the batch is cycled.
num_framesINT601–2048How many frames to render. At 24 frames per second, 60 frames is two and a half seconds. Keyframe numbers in the spec are held inside this range.
trajectory_specSTRING{ "loop": false, "default_ease": "linear", "keyframes": [ { "frame": 0, "zoom": 1.0, "center": [0.5, 0.5], "angle": 0.0, "pan": [0.0, 0.0], "tilt": [0.0, 0.0], "dolly_strength": 0.0, "dolly_radius": [0.3, 0.3], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "ease_in_out" }, { "frame": 30, "zoom": 1.5, "center": [0.5, 0.5], "angle": 90.0, "pan": [0.0, 0.0], "tilt": [0.0, 0.15], "dolly_strength": 0.3, "dolly_radius": [0.35, 0.35], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "ease_in_out" }, { "frame": 59, "zoom": 2.0, "center": [0.6, 0.4], "angle": 360.0, "pan": [0.1, 0.0], "tilt": [0.0, 0.3], "dolly_strength": 0.6, "dolly_radius": [0.4, 0.4], "dolly_feather": 0.5, "dolly_mode": "radial", "sphereize_strength": 0.0, "sphereize_radius": [0.4, 0.4], "sphereize_feather": 0.5, "depth_strength": 0.0, "ease": "linear" } ] }The move, as JSON: a 'keyframes' list, each entry carrying a 'frame' number and the camera properties it sets there. Empty text holds the picture still.
edge_modeCOMBOmirrorWhat fills the frame when the camera looks past the edge of the picture. `mirror` reflects the picture back, which is the least visible; `border` smears the edge pixels; `wrap` brings the opposite edge round, which suits a seamless texture.
enable_camera_shakeBOOLEANfalseWhether to add a handheld wobble on top of the keyframed move. Off gives a locked-off, tripod-steady result; on makes the shot feel operated by a person.
shake_position_amplitudeFLOAT0.0300–0.5How far the wobble drifts, as a share of the frame. 0.01 is a barely visible breath, 0.03 a natural handheld hold, 0.2 a running shot. Ignored while enable_camera_shake is off.
shake_rotation_amplitudeFLOAT1.50–45How far the wobble rolls, in degrees. 1.5 reads as a steady hand, 10 as an unsteady one. Set to 0 for drift without any roll. Ignored while enable_camera_shake is off.
shake_seedINT00–2147483647Seed for the wobble. The same seed always produces the same wobble, so a shot can be re-rendered identically; change it to try another take.
depth_mapoptIMAGEOptional depth map, white near and black far, at any size. With one connected, 'depth_strength' in the spec holds the far parts of the scene back while the near parts move fully, which is what makes the shot read as parallax. Leave it unconnected for a flat move.

Outputs (2)

NameTypeDescription
videoIMAGEThe rendered frames in order, all at the input's size, ready for a video writer or an image-to-video model.
frame_countINTHow many frames were rendered, for wiring straight into a video writer's frame count or a duration calculation.