diwen.dev  ·  learning notes  ·  August 2026
optical-flow

Optical Flow from First Principles

Two consecutive video frames. One question: for every single pixel in the first frame, where did it go in the second? The answer is a field of arrows. Almost everything interesting about the problem comes from the places where that question has no good answer.

Figure 1: Frame 1 → Frame 2 → flow field. A synthetic driving scene rendered by this page. The camera moves forward while a car passes on the left. The middle panel draws each pixel's motion as an arrow. The right panel draws the same numbers as colour, which is the standard way to show a field this dense. If you have not read one of these pictures before, the box below explains how.
How to read the colour pictures on this page

Half a million arrows will not fit on a page, so the field gets drawn as an image instead. One pixel of the picture is one motion vector, and its colour says which way that pixel moved.

Hue is direction. Red is rightward, cyan is leftward, yellow is downward, purple is upward, and the shades between them are the directions between them. The small colour wheel in the corner of each panel is the key: find your colour on the wheel, and the direction you had to travel from the centre to reach it is the direction those pixels moved.

How strong the colour is tells you the speed. A faint, washed-out colour means the pixel barely moved; a deep, vivid one means it moved fast; and pure white means it did not move at all. So a pale pink pixel and a vivid red one moved the same way, the vivid one just moved further. (Papers call this the saturation of the colour, which is the same idea.) Each picture is scaled to its own fastest pixel, so colours are comparable in direction between figures but not in speed. That is why every panel states its maximum.

Two habits make these quick to read. A patch of flat, even colour is a surface moving as one piece. A sharp colour boundary is the edge of something moving differently from whatever is behind it, which usually traces an object's outline. In Figure 1 the colours fan out around a single still, white point on the horizon. That point is where the camera is heading, and everything streams away from it.

1What optical flow actually is

The intuition

Put a finger on a pixel in frame 1, say a freckle on someone's cheek. Now find that same freckle in frame 2. It has moved four pixels right and one pixel down. Write that down as a little arrow, (+4, +1), and pin it to the original location.

Do that for every pixel in the image. What you get is optical flow: a two-channel image, the same width and height as the input, where instead of storing a color at each location you store a displacement.

So an RGB image is a function from pixel coordinates to color, and a flow field is a function from pixel coordinates to motion. Same grid, different payload.

image     I : ℤ² → ℝ³    (x, y) ↦ (r, g, b)
flow      F : ℤ² → ℝ²    (x, y) ↦ (u, v)
Figure 2: The data structure, literally: hover the image to read the numbers. Left: frame 1 with a cursor. Middle: the 5×5 patch of grayscale intensities around the cursor in frame 1 and frame 2. These are the raw numbers any algorithm sees. Right: the flow field's own 5×5 patch of (u,v) pairs. Flow is stored as float32, not integers: sub-pixel motion is normal and meaningful.

What lives in memory

Concretely, for one Sintel frame pair at 1024×436, here is every array involved and its exact shape:

ArrayShapeDtypeBytesRange of values
frame1, frame2(436, 1024, 3)uint81,339,392 ea.0 … 255
flow_gt(436, 1024, 2)float323,571,712−230.6 … +198.4 px
valid mask(436, 1024)bool / uint8446,464{0, 1}
occlusion mask(436, 1024)bool / uint8446,464{0, 1}
RAFT feature map f₁(256, 54, 128)float327,077,888≈ −4 … +4
RAFT all-pairs cost volume(54, 128, 54, 128)float32191,102,976≈ −20 … +20
EPE map(436, 1024)float321,785,8560 … ∞
the score( ) scalarfloat41.48  (current Sintel-final SOTA)
Figure 3: Shapes and sizes for one 1024×436 frame pair. The whole field collapses to that last row. Note the cost volume: 191 MB for a single frame pair, and it grows as the fourth power of resolution. That one number drives a large fraction of the architectural decisions in Section 5.

The file on disk

Flow is shipped in the Middlebury .flo format, which is about as simple as a binary format gets, and worth seeing, because it tells you exactly what a "ground truth" file is:

byte offset
0x00   50 49 45 48  ASCII "PIEH" = 202021.25f, the magic number
0x04   00 04 00 00  width = 1024 (int32, little-endian)
0x08   B4 01 00 00  height = 436
0x0C   … … …     w·h·2 float32s, interleaved u,v, row-major

total = 12 + 1024·436·2·4 = 3,571,724 bytes

Invalid or unknown pixels are stored as any value with magnitude > 1e9. KITTI instead uses a 16-bit PNG: u = (R − 2¹⁵)/64, v = (G − 2¹⁵)/64, and the blue channel is a {0,1} validity flag, which is how a benchmark encodes "we don't know what happened here."

The mathematics

The single assumption underneath all of classical optical flow is brightness constancy: a point in the world keeps the same intensity as it moves.

I(x, y, t)  =  I(x + u,  y + v,  t + 1)

That equation is exact but unusable: it relates a pixel to an unknown location. So take a first-order Taylor expansion of the right side around (x, y, t), assuming the motion is small:

I(x+u, y+v, t+1) ≈ I(x,y,t) + Ix·u + Iy·v + It

⟹   Ix u + Iy v + It = 0   the optical flow constraint equation

First, the notation, because it is genuinely misleading. I is the intensity, the brightness of a pixel on some scale like 0 to 255. But the subscript in Ix is not multiplication and not a coordinate. It means rate of change with respect to, so Ix is shorthand for ∂I/∂x: how much the brightness changes when you step one pixel to the right. Nobody is adding an x coordinate to a brightness.

Once you read the subscripts that way, the units work out. Each term is a brightness change per frame:

quantity               meaning                                units
I              brightness                         intensity
Ix, Iy       brightness change per pixel    intensity / pixel
It            brightness change per frame    intensity / frame
u, v          how far the pixel moved        pixels / frame

so the pixels cancel:

  Ix·u  →  (intensity/pixel) × (pixels/frame)  =  intensity/frame
  Iy·v  →  (intensity/pixel) × (pixels/frame)  =  intensity/frame
  It    →                                          intensity/frame

Every term is a brightness change per frame, so they are the same kind of thing and adding them is legitimate. Read aloud, the equation says: the brightness change you would expect from sliding this pixel through the image, plus the brightness change actually observed, comes to zero. The two cancel, which is another way of saying the pixel kept its brightness while it moved.

The two unknowns are u and v, the horizontal and vertical displacement of this one pixel, which is exactly what we are trying to find. Everything else is measured straight off the two images.

Here is the whole problem in one worked pixel. Take a 3×3 patch sitting on a vertical edge, where brightness climbs by 20 per pixel to the right and does not change at all going down:

frame 1, around pixel p       frame 2, same place
120  140  160            80  100  120
120  140  160            80  100  120
120  140  160            80  100  120

Now measure the three quantities, using nothing but those numbers:

Ix = (160 − 120) / 2 = +20  intensity per pixel — right neighbour minus left, over 2 px
Iy = (140 − 140) / 2 =   0  intensity per pixel — the rows are identical, so no vertical gradient
It = 100 − 140     = −40  intensity per frame — centre pixel, frame 2 minus frame 1

substitute into Ixu + Iyv + It = 0:

   20u + 0v − 40 = 0   ⟹   u = 2, and v cancels out entirely

Check the units on those numbers before going on. If the true motion is (2, 2), then moving 2 px right through a gradient of 20 intensity per pixel should darken this pixel by 20 × 2 = 40 intensity per frame. Moving 2 px down through a gradient of 0 changes nothing: 0 × 2 = 0. And the brightness actually measured did drop by 40 per frame. The predicted change and the observed change cancel, which is the whole content of the equation.

That is the one equation, and u and v are the two unknowns. The equation fixes u = 2, and then it has nothing left to say: because Iy is zero, the v term vanishes, so v can be anything at all. These all satisfy it exactly:

u = 2, v =   0  →  20(2) + 0(  0) − 40 = 0  ✓
u = 2, v =   3  →  20(2) + 0(  3) − 40 = 0  ✓
u = 2, v = −7  →  20(2) + 0(−7) − 40 = 0  ✓
u = 2, v = 99  →  20(2) + 0(99) − 40 = 0  ✓

And they are not just algebraically valid, they are visually indistinguishable. Slide that patch straight down and the numbers do not change, because every row is identical. The image genuinely does not record how far it moved vertically. If the true motion was (2, 2), nothing in this pixel can tell you the 2.

The fix is to find a second, different equation. A nearby pixel sitting on a horizontal edge gives one, and its blind spot is the opposite:

Ix = 0,  Iy = +25,  It = −50
⟹  0u + 25v − 50 = 0  ⟹  v = 2, and now u is the free one

Put the two together and you get u = 2 from the first and v = 2 from the second: a single answer, (2, 2). Two equations, two unknowns, solved. That is the entire idea behind Lucas–Kanade in Section 5, which gathers one equation per pixel over a whole window and solves them together.

Figure 4: One equation is a line, not a point. Plotting every (u,v) pair that satisfies a pixel's constraint equation gives a line of equally valid answers, not a single answer. Left: the vertical-edge pixel worked through above pins u = 2 but leaves v completely free, so its solution set is a vertical line. Middle: a horizontal-edge pixel has the opposite blind spot, fixing v = 2 and leaving u free. Right: the two lines cross at exactly one point, and that intersection is the true motion. Every algorithm in Section 5 is a different strategy for collecting enough lines to pin down the crossing.

The aperture problem

Look at a moving edge through a small hole and you cannot tell how it is moving along its own length. The constraint equation says the same thing algebraically: it pins down only the component of motion perpendicular to the edge (called normal flow) and says nothing about the component parallel to it.

Figure 5: The aperture problem: one equation cannot pin down two unknowns. Both panels show identical pixels inside the circle. The bar's true motion (white arrow) can point anywhere along the dashed constraint line and produce exactly the same appearance through the hole; the local evidence only supports the normal flow (teal arrow). Drag the direction slider. The teal arrow barely moves while the white one swings wildly. Reveal the bar's ends and the ambiguity disappears, because corners constrain both components at once. Every algorithm in Section 5 is, at bottom, a different answer to "where do I get the second equation?"

There are only two families of answers, and every method is a blend of them:

  • Assume neighboring pixels move together. Collect the constraint equations of a whole window and solve them jointly. That is Lucas–Kanade. Local, fast, fails on textureless regions.
  • Assume the flow field is smooth overall and solve for the entire image at once, trading data fidelity against a smoothness penalty. That is Horn–Schunck, and it is the ancestor of every dense method since.

Deep networks replace both assumptions with a third: learn, from millions of examples, what plausible motion fields look like.

What the vector (u, v) actually represents

It is worth being pedantic here, because the units trip people up constantly.

F(x, y) = (u, v)

u = horizontal displacement, pixels per frame, positive = rightward
v = vertical displacement, pixels per frame, positive = downward

the pixel at (x, y) in frame 1 is at (x+u, y+v) in frame 2

Four things follow that are easy to get wrong:

  • The arrow is anchored in frame 1, not frame 2. F is indexed by frame-1 coordinates. This is forward flow. Backward flow, indexed by frame 2 and pointing back, is a different array, and the two are not simply negatives of each other wherever anything is occluded.
  • Positive v is down, because image rows increase downward. Plotting libraries that assume y-up will render your entire flow field upside down.
  • The units are pixels per frame, not pixels per second and definitely not metres per second. Double the frame rate and every number in the array halves. Halve the image resolution and every number halves again. A flow field is meaningless without knowing the resolution and frame interval it was computed at.
  • The values are continuous. u = 3.72 is a normal, correct answer. Sub-pixel precision is what separates the top of the leaderboards: first and tenth place on Sintel are about half a pixel apart.

Typical magnitudes, so you have a feel for the scale: on Sintel about 60% of pixels move less than 10 px/frame, roughly 10% move more than 40 px/frame, and the maximum displacement in the dataset exceeds 400 px. On KITTI, driving at 50 km/h, road pixels near the bottom of the frame move 20–30 px/frame while pixels near the horizon move less than 1.

Dense vs. sparse

Same problem, two different output data structures, and that difference alone changes which algorithms are even applicable.

Figure 6: Sparse flow tracks a chosen set of points; dense flow answers for every pixel. Sparse methods first ask "which pixels are even trackable?" The answer is corners, where the local structure tensor has two large eigenvalues. Toggle the corner-strength map to see that most of an image is simply not trackable in isolation. Dense methods must produce an answer for those pixels anyway, by propagating information in from places that are trackable.

Sparse flow

list[(x, y, u, v)], typically 100–2000 entries. Classic pipeline: Shi–Tomasi corner detection, then pyramidal Lucas–Kanade on each corner. Milliseconds on a CPU. Used for visual odometry, SLAM, video stabilisation, rolling-shutter correction.

cv2.calcOpticalFlowPyrLK

Dense flow

array[H, W, 2], 446,464 vectors for one Sintel frame. Every pixel gets an answer, including pixels where there is no evidence at all. Used for video interpolation, action recognition, video compression, and as a supervision signal for other models.

RAFT · PWC-Net · Farnebäck

Flow ≠ tracking ≠ physical velocity

These three get conflated constantly, and the distinctions are load-bearing.

Figure 7: Three ways optical flow lies about the physical world. Left: a featureless sphere rotating rapidly. Real motion everywhere, zero optical flow. Nothing in the image changes. Middle: a completely static sphere with a moving light source. Zero real motion, strong optical flow. Right: the barber pole. The stripes physically move straight up; the flow reads as motion to the right, because the aperture-ambiguous stripe motion gets resolved by the pole's vertical boundaries. Optical flow is a property of the image sequence, not of the world.
Optical flowObject trackingPhysical velocity
Output[H,W,2] per pairbox or mask per frame3D vector, m/s
Unitspx / framepx (position)m / s
Time spanexactly 2 framesthe whole sequencecontinuous
Identitynone, a pixel has no IDthe point of itattached to matter
Survives occlusion?no, by definitionyes, that's the hard partyes, trivially
Needs camera model?nonoyes, plus depth

The bridge from flow to physics is scene flow: 3D motion per pixel, in metres. Getting there requires depth and camera intrinsics, because a small nearby object and a large distant one moving proportionally produce identical optical flow. This is also why KITTI and Spring are jointly stereo, flow, and scene-flow benchmarks. They carry the depth needed to make the conversion.

And point tracking (TAP-Vid, CoTracker) is the modern middle ground: track a set of points across hundreds of frames, through occlusion, with an explicit visibility flag. As of 2026 the flow and point-tracking literatures have substantially merged. See Section 5.

Why this is hard: the six failure modes

Every one of these is a case where the question "where did this pixel go?" is ill-posed, not merely difficult.

Figure 8: The standard failure modes, each rendered as an actual frame pair. Each tile shows frame 1, frame 2, and what goes wrong. These are the categories benchmarks deliberately stress. Section 3 explains how each one gets its own column on the leaderboard.
  • Occlusion. A pixel visible in frame 1 is hidden in frame 2. There is no correct answer in the image data; the ground truth still specifies one (where the surface went), so the network must hallucinate it from context. On Sintel, error on occluded pixels runs roughly 10× higher than on visible ones, 0.69 vs 7.89 EPE for the current leader. Occlusions are perhaps 5–10% of pixels and dominate the score.
  • Motion blur. Brightness constancy assumes a point keeps its intensity. A blurred point is smeared across dozens of pixels and its intensity is a mixture. This is exactly what Sintel's "final" pass adds over "clean," and it costs the leaders about 0.5 EPE.
  • Large motion. Displacement beyond the algorithm's search radius. Coarse-to-fine methods handle it by shrinking the image until the motion is small, but see the next item.
  • Small, fast objects. The pathological interaction: coarse-to-fine downsampling makes large motion tractable, but a thin fast object disappears at coarse levels, so the pyramid propagates the background's motion onto it and later levels cannot recover. This failure is the one RAFT was designed to eliminate.
  • Textureless regions. Sky, walls, road surface. The structure tensor is singular; there is no local evidence whatsoever. The answer must be interpolated from distant boundaries.
  • Camera motion. When the camera moves, every pixel moves, and the field is dominated by the ego-motion pattern: expansion from a focus of expansion when driving forward, near-uniform translation when panning. Flow magnitude then encodes depth, not object motion, which is why "is that car moving?" is not answerable from flow alone.

2How to read a flow field

Section 1 gave the short version of the colour code. This is the full one, including the exact convention, so you can hold the figures here up against the ones in any paper.

Hover the wheel to read off the vector it encodes.
Figure 9: The Middlebury color code: hue is direction, saturation is speed. The mapping is fixed by convention so that flow images from different papers are comparable: angle = atan2(−v, −u) indexes a 55-entry hand-tuned color wheel, and ‖(u,v)‖ divided by the field's maximum controls how far the color is pushed from white. White means stationary. In the standard implementation that means red = moving right, cyan = moving left, yellow = moving down, purple = moving up, green = moving down-left. Because normalisation is per-image, two flow visualisations are only comparable in direction unless the max is stated.

The practical reading skill: flat regions of uniform color are rigid surfaces moving together; sharp color boundaries are motion boundaries, usually object silhouettes; smooth gradients are surfaces slanting away from the camera or rotating. A good flow field ends up looking a lot like a segmentation of the scene.

Figure 10: The same field, four ways: arrows, color, magnitude, and the raw numbers. Four canonical motion fields, each shown as an arrow plot, a color image, a magnitude heat map, and (for a 4×4 corner of the grid) the literal float32 values. Learn to recognise these four and you can decompose most real flow fields by eye. Expansion is what forward camera motion looks like; the white point it radiates from is the focus of expansion, the direction you are heading.

The same motion at different frame rates

This is the clearest way to see why "large motion" is an algorithmic problem rather than a physical one. The ball's velocity never changes. Only the sampling interval does, and the numbers in the array scale linearly with it.

Figure 11: One ball, one velocity, three frame rates. At 120 fps the displacement is a few pixels and a 5×5 Lucas–Kanade window contains the answer. At 30 fps it exceeds the window and you need a pyramid. At 8 fps the ball in frame 2 does not overlap its position in frame 1 at all. There is no local gradient connecting them, and any method based on linearising the image around the current estimate is finished. Only a method that searches globally, or matches learned features, can recover it. Watch the ball vanish at coarse pyramid levels as you raise the speed.

A real-world case: ego-motion versus object motion

The most common practical use of flow is separating "the camera moved" from "something moved." Because forward camera motion produces a highly structured expansion field, anything that doesn't fit that pattern is an independently moving object. This is the basis of motion segmentation, and of KITTI's foreground/background metric split.

Figure 12: Dashcam scene decomposed: total flow = ego-motion + residual. The measured flow looks chaotic. Fit a two-parameter expansion model to the background (a focus of expansion and a rate), subtract it, and the residual isolates what the camera's own motion cannot explain. The model was fitted to the road plane, and over the road the residual goes to essentially zero, so that region is fully explained. The moving car lights up, which is the point. But so do two things that are not moving at all: the distant buildings across the top, and the near sign post. That is the honest caveat: both sit at a different depth from the plane the model was fitted to, and a single-plane ego model has no way to know that. Real motion-segmentation systems use depth or an epipolar constraint for exactly this reason. The plot at the bottom shows the same fact from the other side: flow magnitude on the ground plane is inversely proportional to depth, rising steadily toward the bottom of the frame while the horizon barely moves, with nothing in the world moving at different speeds.

3How benchmarks know the truth

To score a method you need the true displacement of every pixel. Nobody can label that by hand. A single Sintel frame would need 446,464 sub-pixel-accurate annotations. So the field has four tricks, and each one shapes the dataset built on it.

Figure 13: Four ways to obtain ground-truth flow, and what each one costs you. Every optical-flow dataset in existence uses one of these four. There is no fifth.
  1. Render it. In a 3D renderer you know where every surface point is at both times, so you project both positions and subtract. Blender's vector pass does exactly this. dense, exact, free, unlimited synthetic
    Used by: Sintel, FlyingThings3D, Spring, AutoFlow, Monkaa, Driving.
  2. Warp it. Take a real photo and apply a known transform to it. The flow is the transform, analytically. real textures, exact no real 3D motion, no real occlusion
    Used by: FlyingChairs (2D affine), and every data-augmentation pipeline.
  3. Measure it with a laser. Mount a LiDAR and a GPS/IMU on a car. Accumulate scans into a 3D point cloud, register consecutive poses, and project. Moving objects break the static-world assumption, so KITTI 2015 additionally fits 3D CAD models to every moving vehicle. genuinely real imagery sparse (~19–50% of pixels), no sky, no thin structures
    Used by: KITTI 2012, KITTI 2015.
  4. Hide the texture. The Middlebury trick, and still the most elegant idea in the literature: paint the scene in fluorescent paint that is invisible under normal light, then alternate between visible-light and UV illumination under computer control. The UV frames are covered in dense high-contrast random texture that makes matching trivial and near-exact; the visible-light frames are what you hand to the algorithm. real, dense, non-rigid lab-only, tiny, slow motion
    Used by: Middlebury 2007/2011.

Endpoint Error

The primary metric. It is exactly what it sounds like: the Euclidean distance between where you said the pixel went and where it actually went.

EPE(x,y)  =  ‖ (upred, vpred) − (ugt, vgt) ‖2
             =  √( (upred − ugt)² + (vpred − vgt)² )

AEPE  =  mean of EPE(x,y) over all valid pixels   the reported number

A worked example on four pixels:

px   ground truth    prediction      difference     EPE
A   ( 4.0,  3.0)   ( 4.0,  3.0)   ( 0.0,  0.0)   0.00
B   ( 4.0,  3.0)   ( 4.6,  3.8)   ( 0.6,  0.8)   1.00  √(0.36+0.64)
C   ( 4.0,  3.0)   ( 1.0, −1.0)   (−3.0, −4.0)   5.00
D   (30.0,  0.0)   ( 0.0,  0.0)   (−30.0, 0.0)  30.00 missed a fast object entirely

AEPE = (0.00 + 1.00 + 5.00 + 30.00) / 4 = 9.00 px

Look at what happened: three of the four pixels are decent, one is catastrophic, and the mean is dominated entirely by the catastrophe. EPE is a mean of an extremely heavy-tailed distribution. That single property explains most of the metric design that follows, and why the leaderboards also report robust alternatives.

Figure 14: Drag the prediction and watch EPE and the outlier verdicts change. The white vector is ground truth; drag the tip of the teal vector to change the prediction. The rings show the 1 px threshold used by Spring and the 3 px threshold used by KITTI. Note how the KITTI criterion widens as the ground-truth motion gets faster. It forgives 5% relative error, so a 100 px displacement gets a 5 px allowance rather than 3.

The other metrics, and why they exist

MetricDefinitionUsed byAnswers
AEPEmean ‖err‖Sintel, Middleburyaverage sub-pixel accuracy
Fl-all% of px with ‖err‖ > 3 and ‖err‖/‖gt‖ > 5%KITTI 2015fraction of pixels that are simply wrong
Fl-bg / Fl-fgsame, split by static background vs moving objectsKITTI 2015are you failing on the cars or the road?
1px outlier rate% of px with ‖err‖ > 1Spring (primary)fraction not sub-pixel accurate
EPE matched / unmatchedEPE restricted to visible / occluded pixelsSintelhow much is occlusion hurting you?
s0-10 / s10-40 / s40+EPE bucketed by ground-truth speedSintelslow detail vs large displacement
d0-10 / d10-60 / d60-140EPE bucketed by distance to nearest motion boundarySinteledge sharpness vs interior smoothness
WAUCweighted area under the accuracy-vs-threshold curverobustness challengesa threshold-free summary
AEangular error in (u,v,1) spacelegacy (pre-2011)mostly historical; distorts near zero motion

The KITTI outlier condition is a conjunction, and that matters: a pixel counts as an outlier only if the error exceeds 3 px and exceeds 5% of the true displacement's magnitude. So for a pixel that truly moved 80 px, an error of 3.5 px is forgiven. Fl-all is reported as a percentage, and the current best on KITTI 2015 is around 2.84%. That is: on 97 of every 100 pixels the leading method is essentially right, and the entire competition happens in the remaining three.

Spring went the opposite direction. At 1920×1080 with ground truth rendered at 4× super-resolution, a 3 px threshold is far too coarse to be informative, so its headline metric is the 1 px outlier rate, and the leaders sit around 3.3%, meaning roughly one pixel in thirty is not yet sub-pixel accurate.

What makes a benchmark hard

Difficulty is engineered, not incidental. The levers:

  • Occlusion fraction. Sintel's unmatched-pixel EPE is ~11× its matched EPE. Adding fast foreground objects to a scene raises the difficulty faster than anything else.
  • Displacement distribution. Not the maximum but the tail. A dataset where 5% of pixels move over 40 px punishes coarse-to-fine methods far more than one where everything moves 8 px.
  • Thin structures and small objects. Hair, foliage, fences, wires. This is Spring's entire thesis, and why it renders ground truth at 4× resolution. Sub-pixel detail is invisible at 1× and methods were being scored on a blurred version of the truth.
  • Nuisance imagery. Motion blur, defocus, atmospheric haze, fog, rain, sensor noise, low light. Sintel's clean/final split isolates this variable exactly; RobustSpring adds 20 corruption types on top of Spring.
  • Non-rigid and non-Lambertian content. Water, smoke, fire, cloth, reflections, transparency. Brightness constancy fails outright and there is no correct pixel correspondence even in principle.
  • Domain gap. The hardest property to design for. Nearly all training data is synthetic; the test set that matters may be real. A method can win Sintel and fall apart on KITTI, which is why generalisation results (train on Chairs+Things, test on Sintel/KITTI without fine-tuning) are reported separately and taken seriously.
  • A withheld test set with a submission limit. The unglamorous but decisive one. Sintel, KITTI, and Spring all keep test ground truth secret and cap submissions to prevent tuning against the leaderboard.

4The five datasets that define the field

Two of them are training sets nobody evaluates on, three are benchmarks nobody trains on, and the standard recipe uses all five in a fixed order.

A note on the pictures below Every dataset panel on this page is a schematic recreation drawn by this page, built to convey composition, motion type, and ground-truth density, not the actual dataset frames, which I can't embed here. Links to the official sample imagery are given with each one, and you should look at them.
Figure 15: What each dataset looks like, and what its ground truth covers. Left column: a representative frame. Right column: the ground truth, in the Middlebury color code, with black marking pixels where no ground truth exists. Note how much of KITTI is black. That is what "semi-dense laser ground truth" means in practice.

MPI Sintel 2012 · benchmark

Built from Sintel, an open-source animated short by the Blender Foundation. The authors took the actual production movie files and re-rendered them with the motion vector pass enabled, which is why the imagery is dramatically more complex than anything purpose-built: dragons, flowing capes, blowing snow, water, hair, characters running toward camera.

  • Size: 35 sequences, 1628 frames, 1024×436. 23 sequences train / 12 test.
  • Two render passes from the same geometry: clean (full shading, no blur or atmosphere) and final (adds motion blur, depth of field, and atmospheric effects). Same ground truth for both, a controlled experiment in how much nuisance imagery costs you.
  • Ground truth: Blender render pass. Dense, exact, plus occlusion masks and invalid masks.
  • Motion tested: very large displacement (>400 px), non-rigid deformation, severe occlusion, motion blur, specular and transparent materials.
  • Metric: AEPE, split into matched/unmatched, plus s0-10/s10-40/s40+ and d0-10/d10-60/d60-140.
  • Where it stands: the current final-pass leader, FreeFlow-L, is at 1.480 EPE overall, 0.689 on matched pixels but 7.894 on unmatched. Fourteen years in, occlusion is still where the error lives. Official leaderboard →

KITTI 2012 / 2015 · benchmark

The only one of the five with real photographs. A station wagon drove around Karlsruhe with two stereo camera pairs, a Velodyne HDL-64E laser scanner, and a GPS/IMU unit. Ground truth comes from registering accumulated laser point clouds using the measured vehicle pose and projecting them into the image.

  • Size: KITTI 2015 has 200 training and 200 test pairs, roughly 1242×375.
  • Ground truth: semi-dense. Laser returns cover only part of the frame: no sky, nothing beyond ~80 m, nothing on reflective or transparent surfaces. Roughly 19–50% of pixels have labels; the rest are excluded from scoring.
  • The 2015 upgrade: KITTI 2012 assumed a completely static world, so moving cars had no valid ground truth at all. KITTI 2015 fixed this by fitting 3D CAD models from a library of car shapes to every moving vehicle, recovering their motion, which is exactly why the leaderboard splits Fl-bg from Fl-fg.
  • Motion tested: forward camera motion (large expansion fields), independently moving vehicles, real sensor noise, real lighting, shadows, saturated sky.
  • Metric: Fl-all. A pixel is an outlier if EPE > 3 px and > 5% of the true magnitude. Current leaders sit at Fl-all ≈ 2.84–2.94% (GeoMFlow, MS-RAFT-3D+, MEMFOF). Official leaderboard →

FlyingChairs 2015 · training only

Created for FlowNet because no training set of the required size existed. Deliberately, almost aggressively unrealistic: take a Flickr photo as a background, composite renders of 3D chair models on top, and apply independently sampled 2D affine transforms to the background and to each chair.

  • Size: 22,872 image pairs at 512×384. 809 chair models × 62 viewpoints.
  • Ground truth: analytic. The affine transforms are chosen, not measured, so the flow is exact by construction and costs nothing to produce.
  • Motion tested: 2D translation, rotation, and scale only. No 3D structure, no perspective, no realistic occlusion relationships. Chairs simply slide over a background.
  • Why it still matters: it demonstrated that a network trained on obviously fake data generalises to real video, which is the premise the whole deep-flow literature rests on. It remains stage one of the standard training curriculum, because its simple motions teach matching before the harder data teaches 3D.

FlyingThings3D 2016 · training only

The 3D successor, part of the Scene Flow Datasets (with Monkaa and Driving). Random ShapeNet objects are launched along randomised 3D trajectories through a scene with a moving camera, rendered in Blender.

  • Size: ~22,000 stereo frame pairs at 960×540, ~2,250 sequences.
  • Ground truth: Blender render passes: optical flow, disparity, disparity change, object segmentation, camera pose. Enough for full scene flow.
  • Motion tested: genuine 3D motion with perspective, real occlusion and disocclusion, very large displacements, objects entering and leaving frame.
  • Why it still matters: stage two of the curriculum. The canonical schedule, C → T → S/K fine-tune, meaning Chairs, then Things, then Sintel or KITTI, was established by FlowNet 2.0 and is still what nearly every 2026 paper reports. Training on Things first actively hurts; the easy-to-hard order is load-bearing.

Spring 2023 · benchmark

The modern benchmark, and a direct response to a specific complaint: Sintel is only 1024×436, so sub-pixel structure (hair, foliage, thin branches) is not even representable in the ground truth, and methods were being penalised for being more accurate than the labels.

  • Size: 6,000 frame pairs from 47 sequences of the Blender open movie Spring, at 1920×1080. Roughly 60× more annotated pixels than Sintel.
  • Ground truth: Blender, rendered at 4× super-resolution (3840×2160) specifically so that sub-pixel detail survives. Provides forward and backward flow for both stereo views, plus disparity and disparity change: four flow maps per frame, enabling stereo, flow, and scene flow on one dataset.
  • Motion tested: high-detail thin structures, high resolution (so large pixel displacements even for modest real motion), and via RobustSpring, 20 corruption types (blur, noise, weather, compression) applied temporally consistently.
  • Metric: 1 px outlier rate as the headline, with detail-focused and matched/unmatched breakdowns. The current leader MEMFOF is at 3.289%. Official benchmark →
DatasetYearPairsResolutionImageryGT sourceGT densityHeadline metric
MPI Sintel20121,628 fr1024×436animated filmBlender pass100%AEPE ≈ 1.48
KITTI 201520154001242×375real photosLiDAR + CAD fit~19–50%Fl-all ≈ 2.84%
FlyingChairs201522,872512×3842D compositeknown affine100%training only
FlyingThings3D2016~22,000960×540random 3D objectsBlender pass100%training only
Spring20236,0001920×1080animated filmBlender @ 4×100%1px ≈ 3.29%
Figure 16: Side by side. Leaderboard figures are the best published entries as of mid-2026 and move regularly, so check the official sites. Also widely used but not covered here: Middlebury (2011, the fluorescent-paint set), HD1K (2016, 1080p driving), AutoFlow (2021, learned rendering parameters), and TAP-Vid / Kubric for point tracking.

5The algorithms

Every method here answers the same question, where do I get the second equation?, and the history is a steady march from hand-written assumptions toward learned ones, with one structural idea (iterative refinement at a single resolution) turning out to matter more than any of them.

Lucas–Kanade (1981)

The core idea: one pixel gives one equation. So take a 5×5 window, assume all 25 pixels share the same motion, and you have 25 equations for 2 unknowns. Solve by least squares.

minimise   Σ(x,y)∈W [ Ixu + Iyv + It

⟹   A · (u,v)ᵀ = b,   where

       ⎡ ΣIx²   ΣIxIy ⎤         ⎡ −ΣIxIt
A = ⎢                   ⎥   b = ⎢          ⎥
       ⎣ ΣIxIy   ΣIy² ⎦         ⎣ −ΣIyIt

A is the structure tensor, the same matrix as the Harris corner detector, which is not a coincidence. Its eigenvalues tell you whether the window contains enough structure to solve for both components:

flat region (blank wall)
A = ⎡ 0.4   0.1 ⎤   λ = (0.45, 0.35)  both tiny → no information at all
     ⎣ 0.1   0.4 ⎦

straight edge → the aperture problem, in matrix form
A = ⎡ 8100    0 ⎤   λ = (8100, 2)   rank-deficient → u solvable, v is not
     ⎣    0    2 ⎦                            κ(A) = 4050

corner
A = ⎡ 5200  900 ⎤   λ = (5750, 3350) both large → fully determined
     ⎣ 900  3900 ⎦

min(λ₁, λ₂) is the Shi–Tomasi "good feature to track" score. This is why sparse flow tracks corners: they are precisely the pixels where the 2×2 system is well-conditioned. It is also why plain LK handles only small motion. The Taylor expansion is only valid for displacements under about a pixel. The fix is the pyramid: downsample until the motion is sub-pixel, solve, upsample the estimate, warp, and solve for the residual. Four levels turn a 1 px capability into roughly 16 px.

Farnebäck (2003)

The core idea: stop linearising the image and fit it to a quadratic instead. Approximate the neighbourhood of every pixel by a 2D polynomial:

f(x) = xAx + bx + c   6 coefficients per pixel, fitted by weighted least squares

Now the trick. If the image is displaced by d, substitute x → x − d and expand. The quadratic term is unchanged, and the linear term shifts in a way that isolates d exactly:

A₂ = A₁      b₂ = b₁ − 2Ad

⟹  d = −½ A−1 (b₂ − b₁)   a closed-form displacement, no iteration

In practice A is averaged over a neighbourhood for stability, and the whole thing runs coarse-to-fine. The result is fully dense, runs in real time on a CPU, and was the default in OpenCV (calcOpticalFlowFarneback) for a decade. Its signature weakness: the quadratic model is a smoothness assumption in disguise, so motion boundaries get blurred and small objects are absorbed into the background.

The other classical branch Horn–Schunck (1981), published the same year as Lucas–Kanade, took the global route: minimise ∬ (Ixu + Iyv + It)² + α²(‖∇u‖² + ‖∇v‖²) over the whole image. The second term is an explicit smoothness prior that lets information diffuse from textured pixels into textureless ones. Everything dense since, including the "context network" at the end of PWC-Net and the convex upsampler in RAFT, is a descendant of that regularisation term.

FlowNet (2015): flow as supervised learning

The core idea: stop designing the matching function; learn it. Two variants shipped in the same paper:

  • FlowNetS ("simple"): stack the two frames into a 6-channel input and hand it to a plain encoder–decoder CNN. It works, which was itself the surprising result.
  • FlowNetC ("correlation"): process each frame through its own weight-shared tower, then insert an explicit correlation layer that computes the dot product between a feature in frame 1 and features in a neighbourhood of frame 2. This bakes "matching" into the architecture rather than hoping the network invents it.

FlowNet 2.0 (2017) made it competitive by stacking multiple FlowNets, each one warping frame 2 by the previous estimate and predicting a residual, plus a dedicated small-displacement branch and a fusion network. It also established the Chairs → Things3D training curriculum that everyone still uses.

PWC-Net (2018): three classical principles, made learnable

The core idea: take the three things classical methods always did (, Warping, cost volume) and make each one a learned module. At every pyramid level, from coarse to fine:

  1. Warp frame 2's feature map (not the image) by the upsampled flow from the level above.
  2. Build a cost volume between frame 1's features and the warped features, but only over a ±4 pixel search radius, because after warping the remaining motion is small. That's 81 channels instead of a full all-pairs volume.
  3. Run a small CNN to predict the residual flow, then pass to the next level.

The payoff was efficiency: PWC-Net is 17× smaller than FlowNet2 and more accurate. The cost is structural, and it is the flaw RAFT was built to fix. Coarse-to-fine cannot recover an object that vanished at a coarse level. A thin, fast-moving limb is gone by level 4, the pyramid assigns it the background's motion, and the ±4 search at fine levels can never find it again.

RAFT (2020): the architecture that reset the field

Best paper at ECCV 2020, and still the backbone that most subsequent methods modify. It has three parts.

  1. Feature encoders. One weight-shared encoder maps both frames to 256-channel features at 1/8 resolution. A separate context encoder runs on frame 1 only.
  2. An all-pairs correlation volume. Compute the dot product between every feature in frame 1 and every feature in frame 2, a full 4D tensor (H/8, W/8, H/8, W/8). No search radius, no coarse-to-fine, no assumption about how far anything moved. It is then average-pooled into a 4-level pyramid over the last two dimensions only, so large displacements are covered without ever downsampling frame 1.
  3. A recurrent update operator. Start from flow = 0. Repeat 12–32 times: look up correlation values in a small window around the current flow estimate, feed them plus context into a GRU, and get an increment Δf. Add it. The weights are shared across all iterations. This is a learned optimiser rather than a deeper network.

The key structural difference: RAFT maintains a single flow field at a single resolution and refines it repeatedly, instead of building it up across a pyramid. Nothing ever disappears, so small fast objects are recoverable, because the entire correlation volume was computed at full 1/8 resolution before iteration began.

Figure 17: RAFT, part 1: hover frame 1 to see a real slice of the all-pairs cost volume. This is genuinely computed: for the pixel under your cursor, the panel shows the normalised correlation of its patch against every location in frame 2, one 2D slice of the 4D volume. Bright means "this could be where it went." Over a corner you get a single sharp peak. Over an edge you get a bright ridge: the aperture problem, visible as the shape of the cost surface. Over blank sky you get a broad plateau and no information. The update operator's job is to resolve ridges and plateaus using its neighbours' evidence.
Figure 18: RAFT, part 2: the estimate converging over iterations. Starting from an all-zero flow field, each step performs a local cost-volume lookup around the current estimate and takes a damped step toward the minimum, then smooths. This is a hand-coded stand-in for RAFT's learned GRU update, the same loop, with arithmetic where RAFT has trained weights. The convergence behaviour is characteristic: large errors collapse in the first three or four iterations, then the tail is spent on sub-pixel refinement and on propagating motion into textureless regions. Real RAFT shows the same curve, which is why papers report accuracy at 12, 24, and 32 iterations.

2021 → 2026: what happened after RAFT

Five threads, all still active. Almost everything below keeps RAFT's iterative-refinement skeleton and changes what feeds it.

1 · Global context for occlusion

GMA (2021) added a transformer that aggregates motion features across the whole image, on the reasoning that an occluded pixel's motion is usually knowable from the visible parts of the same object. Directly targets Sintel's unmatched-pixel error.

2 · Matching instead of iterating

GMFlow (2022) reformulated flow as global feature matching, a softmax over all-pairs similarity, computed in one forward pass. FlowFormer (2022) encoded the cost volume itself into latent tokens with a transformer, then decoded with cost memory.

3 · More than two frames

VideoFlow (2023) fuses three or five frames, since temporal context disambiguates occlusion. MemFlow (2024) keeps a running memory buffer for real-time video. MEMFOF (2025) made multi-frame practical at 1080p (2.09 GB at inference, by shrinking the correlation volume) and currently leads Spring (3.289% 1px), Sintel-clean (0.963 EPE) and KITTI (2.94% Fl-all).

4 · Simplify and speed up

SEA-RAFT (2024) is the pragmatist's pick: direct regression of an initial flow instead of starting at zero, a mixture-of-Laplace loss instead of L1, and rigid-motion pretraining on TartanAir. About 2.3× faster than comparable methods at better accuracy, and the usual baseline in 2026 papers. MS-RAFT+ holds up on the robustness challenges.

5 · Borrowed vision priors

The 2026 direction. MegaFlow (Zhang et al., ETH Zürich / Microsoft) drops task-specific encoders and uses frozen pre-trained ViT features, since DINO-style representations already match across huge displacements. It poses flow as global matching over them, then adds light iterative refinement for sub-pixel accuracy. It reaches state-of-the-art zero-shot results on Sintel, KITTI and Spring without fine-tuning on any of them.

6 · Convergence with point tracking

Two-frame flow and long-range point tracking (CoTracker, TAP-Vid) are collapsing into one problem. MegaFlow being competitive on point-tracking benchmarks is the clearest signal: the useful object is a correspondence model, and two-frame flow is a special case of it.

The through-line: the twelve years from FlowNet to MegaFlow moved the source of the "second equation" from a hand-written smoothness term, to a task-specific network trained on synthetic chairs, to a general-purpose visual representation trained on everything. What stayed constant is RAFT's insight that you should refine one full-resolution estimate iteratively rather than build it up a pyramid.

6End to end, with real numbers

Two frames in, a benchmark score out. Everything below runs for real in your browser: the scene is rendered as separate layers with exactly known motion, so the ground truth is analytic, and a genuine pyramidal Lucas–Kanade solver produces the prediction.

Figure 19: Step 1–2 · The input pair, and the ground truth we can compute exactly. The scene is built from four layers, each with an analytically known transform: distant buildings and road expand from the vanishing point at different rates (the camera is driving forward, and flow magnitude scales inversely with depth), the car translates left, and the near sign post expands fast and leaves the frame. Because the transforms are chosen rather than measured, the ground-truth flow at every pixel is exact. This is method 1 from Section 3, in miniature. The occlusion mask marks pixels that are visible in frame 1 but covered in frame 2; no algorithm can get those right from image evidence alone.
Figure 20: Step 3–4 · Run the algorithm, compare against truth. Left: the predicted flow field. Middle: the ground truth, same color scale. Right: the per-pixel EPE map, where dark is accurate and bright is wrong. Move the sliders and watch where the errors move. Drop to 1 pyramid level and the fast-moving sign post and car fail completely, because the displacement exceeds what a single-scale gradient step can capture. But push to 5 levels and the error climbs again. At that depth the coarsest image is about 20×11 px, mostly noise, and whatever it gets wrong is amplified 16× on the way back up. The sweet spot here is 3 levels, and that non-monotonic curve is the coarse-to-fine trade-off in one control, the exact thing RAFT was built to sidestep. Widening the window smooths the field but blurs motion boundaries.
computing…
Figure 21: Step 5 · The benchmark score. The error histogram is the point of this figure. Bar heights are on a square-root scale, without which the tail would be invisible next to the spike. It is not a bell curve. It is a spike near zero with a long right tail, and the reported AEPE is pulled far above the median by a small number of catastrophically wrong pixels. That is why the outlier-rate metrics exist alongside it, and why the occluded/visible split is reported separately.

Two things are worth taking away from the numbers above. First, the errors are not spread evenly. They concentrate on occlusion boundaries, on the fastest-moving layer, and in the textureless road and sky where the structure tensor is singular. Second, this classical solver would place nowhere near a modern leaderboard, and the gap is almost entirely in exactly those regions. Every architectural idea in Section 5 (the pyramid, the all-pairs volume, global aggregation, multi-frame context, borrowed ViT features) is aimed at one of the bright patches in that error map.

Where to go next

  • Run the real thing: torchvision.models.optical_flow.raft_large ships with pretrained weights and takes about ten lines. Compare its output on your own video against cv2.calcOpticalFlowFarneback. The difference at motion boundaries is startling.
  • Download Sintel's training split (it includes ground truth) and compute EPE yourself. Then compute it separately on the occluded and non-occluded masks, and you'll feel the 10× gap directly.
  • Read the RAFT paper before any of the newer ones; nearly everything since is a modification of it.

Sources

Benchmark figures were checked against the official leaderboards while writing this page (August 2026); they change frequently.

Every figure on this page is generated procedurally at load time: the scenes, the flow fields, the cost volumes, the solver, and the scores. Dataset panels in Section 4 are schematic recreations, not real dataset frames.

7Practice problems

Twenty problems, all doable on paper in under five minutes each. Work them before opening the solution. The arithmetic is the point, because the quantities are exactly the ones you'll be printing to a console when something goes wrong.

A · The constraint equation

At one pixel you measure the gradients below. The motion is known to be purely horizontal. Find u.

Ix = 20   Iy = −10   It = −40
Solution
Substitute v = 0 into Ixu + Iyv + It = 0: 20u + (−10)(0) + (−40) = 0
20u = 40
u = 2
u = 2 px/frame. Note what made this solvable: an extra assumption supplied from outside the equation. That is what Section 1 is about.

Same pixel, same numbers, but now you don't get to assume anything. Compute the normal flow, the unique flow vector parallel to the image gradient that satisfies the constraint.

Ix = 20   Iy = −10   It = −40
Solution
‖∇I‖ = √(20² + (−10)²) = √500 ≈ 22.36
magnitude = −It / ‖∇I‖ = 40 / 22.36 ≈ 1.789
direction = ∇I / ‖∇I‖ = (20, −10)/22.36 = (0.894, −0.447)
n = 1.789 × (0.894, −0.447) = (1.6, −0.8)
Check: 20(1.6) + (−10)(−0.8) + (−40) = 32 + 8 − 40 = 0
Normal flow = (1.6, −0.8). The true flow could be any vector on the line through this point perpendicular to ∇I. This is the shortest one.

The true motion at that pixel turns out to be (4.0, 3.6). Verify it satisfies the constraint, and say why the normal flow from P2 was still "correct."

Solution
20(4.0) + (−10)(3.6) + (−40) = 80 − 36 − 40 = 4 ≈ 0 ✓ (to rounding) The true vector (4.0, 3.6) and the normal flow (1.6, −0.8) differ by roughly (2.4, 4.4), which is parallel to the edge. Both are consistent with the single equation available. The local data genuinely cannot distinguish them. An EPE of 5.0 here would not be a bug in your solver; it is the aperture problem being measured.

B · Lucas–Kanade and the structure tensor

A 3-pixel window has these gradients. Build the structure tensor A, compute its eigenvalues, and classify the window.

(Ix, Iy) = (3, 0), (4, 0), (5, 0)
Solution
ΣIx² = 9 + 16 + 25 = 50
ΣIxIy = 0
ΣIy² = 0

A = ⎡ 50   0 ⎤   det(A) = 0
     ⎣  0   0 ⎦   λ = 50, 0
Singular: a perfect vertical edge. u is recoverable, v is not, and A−1 does not exist. In code this shows up as a divide-by-zero or a wildly large flow vector, which is why every implementation adds a small λI to the diagonal or tests min(λ) > τ before solving.

Now a window from a corner. Same task: build A, find its eigenvalues, and give the Shi–Tomasi score.

(Ix, Iy) = (3, 0), (0, 4), (3, 4)
Solution
ΣIx² = 9 + 0 + 9 = 18
ΣIy² = 0 + 16 + 16 = 32
ΣIxIy = 0 + 0 + 12 = 12

A = ⎡ 18  12 ⎤   trace = 50,  det = 18·32 − 12² = 576 − 144 = 432
     ⎣ 12  32 ⎦

λ = (50 ± √(50² − 4·432)) / 2 = (50 ± √772) / 2 = (50 ± 27.79) / 2
λ₁ ≈ 38.9  λ₂ ≈ 11.1
Shi–Tomasi score = min(λ) ≈ 11.1, comfortably non-zero, so both flow components are determined. This is exactly the test goodFeaturesToTrack applies.

Solve the Lucas–Kanade system for (u, v).

A = ⎡ 4  2 ⎤   b = ⎡ 10 ⎤
     ⎣ 2  3 ⎦        ⎣  9 ⎦
Solution
Cramer's rule on A(u,v)ᵀ = b: det(A) = 4·3 − 2·2 = 8

u = (10·3 − 2·9) / 8 = (30 − 18) / 8 = 12/8 = 1.5
v = (4·9 − 2·10) / 8 = (36 − 20) / 8 = 16/8 = 2.0
(u, v) = (1.5, 2.0) px/frame. Sanity check: 4(1.5) + 2(2) = 10 ✓, 2(1.5) + 3(2) = 9 ✓.

Single-scale Lucas–Kanade is reliable up to about 1 px of displacement, and each pyramid level halves the motion. How many levels do you need for a 40 px displacement, and what is the catch?

Solution
capacity with L levels ≈ 2^(L−1) px
2^(L−1) ≥ 40  ⟹  L − 1 ≥ log₂40 = 5.32  ⟹  L = 7
7 levels. The catch: at level 7 a 1024×436 image is 16×7 pixels. A 20 px-wide object occupies a third of a pixel there. It has been averaged out of existence, so the coarse level confidently assigns it the background's motion and the finer levels, which only search ±1 px around that, can never recover. This is the exact failure RAFT's all-pairs volume was designed to remove.

Farnebäck: given the polynomial coefficients of the same neighbourhood in both frames, compute the displacement.

A₁ = ⎡ 2  0 ⎤   b₁ = (6, −4)
      ⎣ 0  4 ⎦      b₂ = (2,  4)
Solution
Use d = −½ A₁−1(b₂ − b₁): b₂ − b₁ = (2 − 6, 4 − (−4)) = (−4, 8)

A₁−1 = ⎡ 0.5    0 ⎤   (diagonal, so just reciprocals)
         ⎣   0  0.25 ⎦

A₁−1(b₂ − b₁) = (0.5·−4, 0.25·8) = (−2, 2)
d = −½ · (−2, 2) = (1, −1)
d = (1, −1) px/frame. No iteration, no linearisation of the image, just a closed form. That is the appeal of Farnebäck's method.

C · Metrics

Compute the endpoint error.

ground truth = (6, 8)   prediction = (3, 4)
Solution
difference = (3 − 6, 4 − 8) = (−3, −4)
EPE = √(9 + 16) = √25 = 5.0
EPE = 5.0 px. Note the prediction has the right direction. It is exactly half the true vector. EPE does not care; a systematically under-scaled flow field is penalised the same as a randomly wrong one.

Five pixels have the endpoint errors below. Report the AEPE and the median, then say which one a paper would print.

0.2  0.5  0.3  0.4  23.6
Solution
sum = 0.2 + 0.5 + 0.3 + 0.4 + 23.6 = 25.0
AEPE = 25.0 / 5 = 5.0
sorted: 0.2, 0.3, 0.4, 0.5, 23.6  ⟹ median = 0.4
AEPE = 5.0, median = 0.4, a factor of 12.5 apart. Papers report the mean, which is why a method can look terrible because of one badly-handled object while being sub-pixel accurate on 80% of the frame. It is also why outlier-rate metrics exist as a counterweight.

Apply the KITTI criterion (outlier if EPE > 3 px and EPE > 5% of ‖gt‖) to all three pixels. Then compute Fl-all.

a)  gt = (10, 0)   pred = (13.5, 0)
b)  gt = (100, 0)  pred = (104, 0)
c)  gt = (2, 0)    pred = (4, 0)
Solution
a) EPE = 3.5  >3? yes  3.5/10 = 35% >5%? yes  ⟹ OUTLIER
b) EPE = 4.0  >3? yes  4/100 = 4%  >5%? no  ⟹ inlier
c) EPE = 2.0  >3? no    (2/2 = 100%)       ⟹ inlier

Fl-all = 1/3 = 33.3%
Only (a) is an outlier; Fl-all = 33.3%. The two escapes are deliberate: (b) is forgiven because a 4% error on fast motion is proportionally fine, and (c) is forgiven because 2 px absolute error is below the noise floor of the laser ground truth itself. The and makes KITTI far more lenient than a naive reading suggests.

Twenty pixels have these endpoint errors. Compute Spring's 1px outlier rate, and KITTI's Fl-all on the same data.

0.2 0.4 0.9 1.1 0.3 0.7 2.4 0.5 0.8 1.0
0.6 0.3 5.2 0.4 0.9 0.2 1.4 0.7 0.5 0.6

all ground-truth magnitudes are large (> 100 px)
Solution
EPE > 1.0:  1.1, 2.4, 5.2, 1.4  → 4 pixels  (1.0 is not > 1.0)
1px outlier rate = 4/20 = 20%

EPE > 3 and > 5% of ‖gt‖ (>5 px here):  only 5.2
Fl-all = 1/20 = 5%
Spring: 20%. KITTI: 5%. Same predictions, a 4× difference in reported error. Metrics are not comparable across benchmarks, and Spring chose the stricter one deliberately, because at 1080p a 3 px threshold hides almost everything worth measuring.

A method reports Sintel-final EPE of 0.70 on matched pixels and 7.90 on unmatched. Occluded pixels are 8% of the frame. Compute the overall EPE, and the share of total error contributed by that 8%.

Solution
overall = 0.92 × 0.70 + 0.08 × 7.90
        = 0.644 + 0.632
        = 1.276

share from occluded = 0.632 / 1.276 = 49.5%
Overall EPE ≈ 1.28; occluded pixels contribute 49.5% of it. 8% of the pixels produce half the score. This single arithmetic fact explains why GMA, VideoFlow, MemFlow and MEMFOF are all, in different ways, occlusion-reasoning methods.

D · Fields, frame rates and geometry

An object moves 90 pixels per second across the image. Give its flow magnitude at 120, 30 and 10 fps. Which are within reach of a 4-level pyramidal LK (capacity ≈ 8 px)?

Solution
120 fps → 90/120 = 0.75 px/frame  ✓ even single-scale LK
 30 fps → 90/30  = 3.0  px/frame  ✓ needs the pyramid
 10 fps → 90/10  = 9.0  px/frame  ✗ just past capacity
Nothing about the object changed except the sampling interval. "Large motion" is not a property of the world; it is a property of your frame rate, your resolution, and your algorithm's search range, and any one of the three can create or remove the problem.

A forward-driving camera produces a pure expansion field, F(x) = s·(x − c). You measure flow at two pixels. Find the focus of expansion c and the scale s.

F(600, 400) = (12,  8)
F(100, 150) = (−8, −2)
Solution
Take the x-components and subtract to eliminate cx: s(600 − cx) = 12
s(100 − cx) = −8
─────────────────
s(500) = 20  ⟹  s = 0.04

0.04(600 − cx) = 12 ⟹ 600 − cx = 300 ⟹ cx = 300
0.04(400 − cy) = 8  ⟹ 400 − cy = 200 ⟹ cy = 200
FOE = (300, 200), s = 0.04. The FOE is the image point the camera is heading toward, and it is where flow magnitude is zero. Anything whose flow doesn't fit this two-parameter model is independently moving. That is motion segmentation in one subtraction.

Warping needs sub-pixel sampling. Compute I₂(10.3, 20.6) by bilinear interpolation.

I₂(10, 20) = 100   I₂(11, 20) = 140
I₂(10, 21) =  60   I₂(11, 21) =  80
Solution
Interpolate along x first (fraction 0.3), then along y (fraction 0.6): top    = 100 + 0.3(140 − 100) = 100 + 12 = 112
bottom =  60 + 0.3( 80 −  60) =  60 +  6 =  66

result = 112 + 0.6(66 − 112) = 112 − 27.6 = 84.4
84.4. This runs once per pixel per iteration per pyramid level inside every warping-based method, and its differentiability is exactly what lets PWC-Net and RAFT backpropagate through the warp.

Forward–backward consistency is the standard occlusion test: a pixel is occluded if ‖F(p) + B(p + F(p))‖ exceeds a threshold (take 3 px). Classify both pixels.

p = (100, 50)  F(p) = (10, 0)  B(110, 50) = (−10,  0)
q = (200, 80)  F(q) = (10, 0)  B(210, 80) = ( −3,  4)
Solution
p:  (10, 0) + (−10, 0) = (0, 0)  ‖·‖ = 0.0  ≤ 3  ⟹ VISIBLE
q:  (10, 0) + (−3, 4)  = (7, 4)  ‖·‖ = √(49+16) = √65 ≈ 8.06  > 3 ⟹ OCCLUDED
p visible, q occluded. The logic: if you follow the flow forward and the backward flow at the destination doesn't bring you home, then the pixel you landed on belongs to a different surface. Something moved in front. This costs one extra forward pass and is how most methods produce an occlusion mask for free.

E · Sizing the data structures

How large is a .flo ground-truth file for one Sintel frame at 1024×436?

Solution
header = 4 (magic) + 4 (width) + 4 (height) = 12 bytes
payload = 1024 × 436 × 2 channels × 4 bytes (float32)
        = 446,464 × 8 = 3,571,712 bytes

total = 3,571,724 bytes ≈ 3.41 MiB
≈ 3.4 MiB per frame, larger than the two PNG images it describes. Ground truth is not a small side-file; the Sintel training set's flow alone runs to several gigabytes.

RAFT builds an all-pairs correlation volume at 1/8 resolution. Size it for a 1024×436 input, in float32. Then say what happens at 2048×872.

Solution
feature grid = 1024/8 × 436/8 = 128 × 54 = 6,912 positions
all-pairs entries = 6,912 × 6,912 = 47,775,744
bytes = 47,775,744 × 4 = 191,102,976 ≈ 182 MiB

at 2× resolution: 4× the positions ⟹ 16× the entries
= 764,411,904 entries = 3,057,647,616 bytes ≈ 2.85 GiB
≈ 182 MiB at 1024×436, ≈ 2.85 GiB at 2048×872. It scales as resolution to the fourth power. This single quantity is why RAFT pools the volume into a pyramid, why methods for 1080p (MEMFOF) advertise their memory footprint on the front page, and why 2026 methods are moving toward global matching over compact ViT features instead of dense all-pairs volumes.

A sparse tracker follows 500 corners; a dense method covers a 1024×436 frame. Compare the number of output values, and the ratio.

Solution
sparse: 500 points × 2 = 1,000 floats  (4 KB)
dense:  1024 × 436 × 2 = 892,928 floats  (3.4 MB)

ratio = 892,928 / 1,000 ≈ 893×
Roughly 900× more output. And the extra 99.9% is the hard part. Those are precisely the pixels where the structure tensor is singular (P4) and no local evidence exists, so every value must be inferred rather than measured.

Back to diwen.dev

Written with Claude Code. Every figure is generated procedurally at load time: the scenes, the flow fields, the cost volumes, the Lucas–Kanade solver, and the benchmark scores, so the numbers quoted in Section 6 are computed on your machine, not transcribed. The dataset panels in Section 4 are schematic recreations drawn by this page, not real dataset frames; each links to the official imagery. Benchmark leaderboard figures were checked against the official sites in August 2026 and change frequently.