AgentCanvas / Pages / Developer Guide / Nodesets / Model / Optical Flow NodeSet
2026-07-07

The Optical Flow NodeSet (OpticalFlowNodeSet, workspace/nodesets/model/model_opticalflow.py) wraps RAFT (Recurrent All-Pairs Field Transforms, shipped in torchvision.models.optical_flow) as a dense motion primitive: a pair of consecutive RGB frames in, a per-pixel displacement field out. It fills the optical-flow coverage gap โ€” the frame-to-frame motion cue that ego-motion estimation, moving-object detection, and short-horizon dynamics all build on, without a hand-tuned pyramid.

env: ac-fm (shared FM env) ยท backend: torchvision RAFT (no HF download โ€” weights ship with torchvision) ยท variants: raft_large (default) / raft_small

Primitive Purpose
model_opticalflow__estimate_flow Dense RAFT flow between two same-resolution frames โ†’ flow (JSON envelope of an (H, W, 2) pixel-displacement field).

1. One primitive: flow A โ†’ B

The tool takes exactly two frames โ€” image_a and image_b, the "before" and "after" โ€” and returns the motion carrying each pixel of A to its match in B. This is the atomic optical-flow operation; a graph that wants flow over a clip loops it across consecutive pairs. Keeping the node to a single pair (rather than a whole video) mirrors the house "one pure primitive" style and keeps peak memory bounded to one frame pair.


2. Frames in โ€” same resolution, uint8

Each port accepts a {rgb_base64} dict (or a raw base64 string). Both frames are decoded to HWC uint8 RGB arrays and must share one resolution โ€” a size mismatch or a malformed frame degrades the node to an empty output rather than guessing. The checkpoint's own preprocessing transform (weights.transforms()) converts to float and normalizes to [-1, 1]; it does not resize.


3. Weights & the divisible-by-8 rule

RAFT requires spatial dims divisible by 8, so each frame is zero-padded up to the next multiple of 8, run, then the flow is cropped back to the original (H, W) โ€” preserving true pixel scale (no resize-and-rescale). The model returns its iterative-refinement list; the last entry is the final flow:

# weights ship with torchvision โ€” no HF download, offline-friendly.
weights = Raft_Large_Weights.DEFAULT          # or Raft_Small_Weights
net = raft_large(weights=weights).eval().to(device)
transform = weights.transforms()          # normalize to [-1,1], no resize
# โ€ฆ pad both frames to a multiple of 8 โ€ฆ
flow = net(frame_a, frame_b)[-1]       # final iterate, (1, 2, Hpad, Wpad)
flow = flow[:, :, :H, :W]              # crop padding back

raft_large is the accurate default; raft_small is ~10ร— smaller and faster (coarser). Each variant is a lazy singleton in a registry, frozen and moved to the device once; inference is single-flight per engine.


4. Flow envelope

The output is a TEXT JSON envelope โ€” the flow as a raw C-contiguous float32 buffer, base64-encoded (byte-exact across the server-mode HTTP boundary, ~4ร— smaller than a JSON float list):

{"shape": [480, 640, 2], "dtype": "float32", "b64": "<C-contiguous float32 buffer>", "variant": "raft_large"}

flow[y, x] is the displacement in pixels carrying pixel (x, y) of frame A to its match in frame B: channel 0 = dx (+ right), channel 1 = dy (+ down). The field is at the original input resolution.


5. Canvas Node

model_opticalflow__estimate_flow

FieldDetail
Inputsimage_a (ANY โ€” {rgb_base64} dict or raw base64), image_b (ANY โ€” same size as A)
Outputsflow (TEXT โ€” the ยง4 envelope; "" on degraded)
Configvariant (select raft_large | raft_small, default raft_large)
Backend callpad to /8 โ†’ model(a, b)[-1] under no_grad โ†’ crop โ†’ (H, W, 2)

6. Server Mode

parallelism = "shared" (a stateless flow estimator โ€” one server across eval workers); server_python = conda_env_python("ac-fm", "OPTICAL_FLOW_PYTHON"). Optical flow runs in the shared ac-fm env โ€” torchvision RAFT is native there and needs no external install or extra weights download. The file stays Python-3.8-parseable. Load: POST /api/components/nodesets/model_opticalflow/load?mode=server.


7. Environment Variables

VariableDefaultPurpose
OPTICAL_FLOW_PYTHONac-fm env pythonInterpreter for the server subprocess
OPTICAL_FLOW_DEVICEauto (โ†’ cuda when available)Torch device for the loaded model

8. Degraded Mode

A weight-load failure latches (_load_failed โ€” no retry storm); that, a missing/malformed frame, or a resolution mismatch between A and B yields "" on the node's output. An empty envelope is a clean "no flow this step" signal; consumers keep their own fallback and never receive a fabricated motion field.

AgentCanvas docs