Skip to content

Video particles

Turn a movie into trajectories, then inspect their motion as continuous polylines in Field. The optional PyTorch tracker follows frame-to-frame motion; the Kotlin reader and visualizer play the recorded result without a Python process or a neural model.

Open the particle_video starting point to scrub the example or track your own movie.

A dance in 24,000 particles

Each polyline joins consecutive recorded positions of one tracked particle (one identity). The recent path shows bends, stalls and sudden jumps that dots alone conceal. The source dance is on the left; stable identity-colored trajectories are on the right. These colors stay fixed through the film's intentional recoloring, so the motion is easier to follow.

The example uses 24,000 target particles at the source's full 400 × 300 resolution, 25 frames per second, and 20 RAFT updates in each direction. Increasing the particle count adds sampling density without running a separate neural prediction per particle. Tracking runs offline; playback uses the recorded positions.

For readable individual paths, the comparison shows identities with id % 16 == 0, about 1,500 at a time, with 25 frames (one second) of history. The underlying run still tracks 24,000. The full-density polyline movie draws every live trajectory with 12 frames of history and source colors. The source video and original dot-only movie are also available. The footage is public domain on Wikimedia Commons. Its historical filename refers to Loïe Fuller; the performer is not verified as Fuller herself.

Each path ends at its current recorded position; earlier vertices belong to earlier frames. Watch the same polylines over a dimmed source to check the endpoints against the fabric:

Scrub recorded tracks

The starting point's second box loads a small, representative subset of the same trajectories. It keeps every sample of each selected identity, so time, births, retirement and colors are preserved. Move movie time, adjust history (frames) and line width, or switch identity colors to inspect motion independently of the source's changing tint. Lines are the default; points adds endpoint dots. Trajectory stride thins displayed identities without changing the tracks.

Actual Kotlin particle reader and scrubber with movie-time, identity-color and trail controls

At another time, a longer history reveals the shape of the recorded paths:

Identity colors and trails at 4.8 seconds

A loaded video exposes positions in processed image pixels, with x right and y down. Velocity is displacement per processed frame; multiply by video.fps to obtain pixels per second. The reader validates finalized files and complete lifecycles, and indexes records that arrived out of order while the tracker checked whether a new trajectory was long enough to keep.

Query one time or one identity, and make ordinary Field geometry from those samples:

val frame = video.at(2.35) // interpolate matching IDs between recorded frames
val points = frame.samples.map { it.position }
val rgb = frame.samples.map { vec(it.red, it.green, it.blue, 1) }
val trajectory = video.trajectory(frame.samples.first().id)
val paths = video.trails(frame.index, length = 25, stride = 16)
val gpuPoints = frame.points(pointSize = 2.0, color = ParticleColor.SAMPLED)

points() returns a single FLine with per-node colors for a node-color-aware renderer such as Field's Vulkan drawing. The 2D particleVideoView() rasterizes sampled colors directly in Kotlin. video.image(frame) draws polylines into a BufferedImage; video.renderMovie(path) exports that same Kotlin drawing through FFmpeg. Reading tracks and making geometry need no FFmpeg; movie export does.

Track your own movie

Prepare the optional environment once from a checkout. It uses pinned Torch/Torchvision packages; the first tracker call fetches the approximately 21 MB pretrained RAFT-large checkpoint. A fresh CUDA Python environment needs several GB of disk space. FFmpeg, FFprobe and a C++17 compiler must be available on PATH.

uv sync --project optional/particle-tracker --python 3.11 --frozen

On an Apple Silicon Mac, install FFmpeg and the Xcode command-line tools, then use the same uv sync command with a native arm64 Python. Choose device = "mps" in ParticleTrackingOptions for Metal, or device = "cpu" explicitly. The default is CUDA. The pinned Torch 2.6 packages do not include Intel Mac wheels, but recorded tracks can still be read and visualized on an Intel Mac without installing the tracking environment. Tracking on a Mac is untested.

If you already have a compatible environment, set FIELD_PARTICLE_TRACKER_PYTHON to its Python executable before starting Field. The tracker is optional: Field startup and normal builds do not install these packages or fetch weights.

The starting point's third box runs the long job in a coroutine, then opens the complete recorded result. Change the input filename and particle count for your own movie:

val tracker = particleTracker()
suspend fun process() {
    val run = tracker.track(
        "my-movie.mp4",
        ParticleTrackingOptions(particles = 24000, colorLimit = 1.0),
    )
    println("cached=${run.cacheHit}")
    particleVideoView(run.load(), stride = 16)
}
start(::process)

Stopping or rerunning that box cancels its subprocess work. Repeating the same successful request reuses checksummed tracks in ~/.afield/particle-tracker. Video content, all options, source, compiler, model checkpoint and environment versions contribute to the cache key. A file edited in place gets a new result. Logs and provenance live beside the cached binary.

Option Effect
width = 0 Preserve native source resolution; set a positive width to resize with aspect ratio retained.
particles = 3000 Target live population; local spacing and confidence can leave holes.
frames = 0, fps = null Process the whole clip at the source's average frame rate.
colorLimit = .18 Retire a point after sustained RGB RMS change from its birth color; 1.0 disables this rule.
minTrack = 10, grace = 5 Omit trajectories shorter than ten samples; require five bad frames before retirement.
spacing = 0.0 Choose birth spacing from image area and target population. Set pixels explicitly to change density.
stillSpeed = 0.0 Disable retirement of still particles; set a positive speed in pixels/frame to enable it.
mergeDistance = 0.0 Disable merging; enable with a positive pixel radius for close, similarly moving mature tracks.
device = "cuda" Use CUDA; request "cpu" or "mps" explicitly for another prepared backend.

For the dance, colorLimit = 1.0 is intentional: recoloring should change the sampled color rather than retire an otherwise useful point. For stable-color footage, the birth-color rule can help retire points that drift into a different surface. Treat it as an artistic choice about when points die; it does not measure tracking accuracy.

Confidence combines forward/backward agreement and local photometric consistency. An uncertain point holds its position during the grace interval; crowding may retire younger points as fabric compresses. Fresh identities fill available space. Stable identity colors make these events visible, but do not imply recovery of the same material point after an occlusion.

Dense tracks are sizeable: this twelve-second example is about 575 MB before compression. The Kotlin reader memory-maps uncompressed files and decodes queried frames on demand. Its current limit is 2 GiB per file; split longer dense work into clips. Portable gzip copies can inflate to at most 256 MiB. Old cache entries can be deleted when you no longer need them.

Example and validation

The example is seconds 4–16 of the Commons file, tracked on a Linux RTX 4090. It keeps the source's 400 × 300 pixels rather than inventing detail by upscaling. The original 1,500-particle test and the denser 24,000-particle version use the same bidirectional RAFT-large engine. The manifest records model versions, options, hashes, cache reuse, duration and the representative-subset rule. The source is also available losslessly for rerunning the exact request.

The imported engine came from the shared tracker conversation. Optional tool source and setup retain its origin and component hashes. Linux CUDA was executed here, and a short Linux CPU smoke passed. Mac/MPS execution and Mac performance remain unverified.

For GPU-resident flow between Field render targets, see Optical flow. For deforming a canonical sheet to learned image densities, see Stipple transport. Video tracking instead follows recorded frame-to-frame motion and recycles identities as needed.