From image CNNs to video understanding
So far in this track you have watched pixels move: you turned stills into streams, measured per-pixel motion with optical flow, and followed objects through time with trackers. All of that answered where things go. Now we ask a harder, more human question: *what is happening?* A clip of someone swinging a racket, a wave breaking, a door opening — these are events, and recognizing them is the job of video understanding. The good news is you already own most of the toolkit: the image CNN. The one genuinely new ingredient is time.
Two closely-related tasks frame this guide. Video classification assigns a whole clip a single category — "cooking," "basketball," "a thunderstorm." Action recognition is the special, very common case where the category is a human action — "jumping," "chopping vegetables," "shaking hands." In practice they are usually the same machinery pointed at different label sets: a network that ingests a clip and outputs a probability over classes. So almost everything we build for one applies to the other. We will say "action recognition" when the example is a person doing something, and "video classification" for the general case.
Here is the one genuinely new difficulty, stated crisply: a video model must reason over a sequence, and the order of frames carries meaning. A photo of a hand near a door is ambiguous; only the progression of frames tells you whether the door is opening or closing. This is the thread that runs through the entire guide. Every architecture we meet is, at heart, a different answer to the same question: how do we let order matter?
Let's recap the image CNN in one breath, since everything below extends it. A CNN slides small learnable filters (kernels) over an image; each filter computes a weighted sum of a local patch of pixels — that is convolution — and the result is a feature map that lights up where the filter's pattern appears (an edge, a corner, later a wheel or an eye). Pooling then shrinks each feature map (e.g. by taking the max over 2×2 blocks), making the representation smaller and more tolerant to small shifts. Stack many such conv-plus-pool layers and the features grow from edges to textures to object parts to whole objects; a final classifier turns the top feature map into class scores. Hold that picture: convolution → feature map → pooling → repeat → classify.
A left-to-right diagram: an input image enters a stack of convolution and pooling layers producing progressively smaller feature maps, ending in a fully connected classifier that outputs class scores.
We will build up three big architectural ideas, each adding time in a different way. First, split appearance from motion: run two networks, one looking at a single frame's content and one looking at pre-computed motion (the two-stream idea, which is exactly why you learned optical flow). Second, convolve in space-time: extend the 2D filter into a 3D filter that also slides over time, so a single network learns motion end-to-end (3D convolution). Third, bootstrap 3D from 2D: cleverly reuse a powerful ImageNet-pretrained 2D net to initialize a 3D net (the I3D trick). By the end you will have a small decision tree for which to reach for.
The naive baselines and why they fall short
Before reaching for clever machinery, a careful engineer tries the dumbest thing that could work — that way the fancy methods have to earn their complexity. The dumbest reasonable baseline for a clip: take your already-trained image CNN, run it on each frame independently, and average the per-frame class probabilities into one prediction. This is frame-averaging, a form of late fusion (we fuse at the very end, after each frame has been fully classified on its own).
Now the killer flaw, and it is worth feeling in your gut. Averaging is order-invariant: the average of a set of numbers does not change if you shuffle the set. So a frame-averaging model literally cannot tell "opening a door" from "closing a door," or "sitting down" from "standing up," or "a glass filling" from "a glass emptying." Those pairs share the exact same frames — only the order differs — and order is precisely the information averaging throws away. Reverse the clip and the prediction is unchanged.
This pinpoints exactly what is missing: temporal modeling — the ability to represent how appearance evolves, not just what appears. A frame-averaging net has rich spatial understanding but zero temporal understanding. (To be fair, late fusion is not useless: for clips where a single key frame already gives the class away — "is this a beach?" — it is cheap and strong. It only collapses on genuinely motion-defined actions.) From here on, every architecture in this guide is a different answer to one question: how do we inject temporal order into the network?
Two-stream networks: appearance plus motion
The first strong architecture pays off everything you learned about optical flow. Its idea is a clean division of labor between two separate CNNs. The spatial stream sees a single RGB frame and answers the *"what"*: which objects, which scene, what does this look like? (A still of a person mid-air near a net already hints "volleyball.") The temporal stream sees motion and answers the *"how it moves"*: the characteristic motion pattern of the action. Each stream is a normal image CNN; together they form a two-stream network.
The crucial trick is the temporal stream's input. Instead of a raw frame, you feed it a stack of pre-computed optical flow fields spanning several consecutive frames. Recall from Guide 2 that an optical flow field gives, at every pixel, a small (dx, dy) vector saying how that pixel moved to the next frame. Stack the flow from, say, 10 frame-pairs and you get a little tensor whose channels are nothing but motion. So the temporal stream literally reads motion as its input image.
An image overlaid with a dense grid of small arrows, each arrow pointing in the direction a pixel moved between consecutive frames, with longer arrows where motion is faster.
Why is feeding optical flow so powerful? Because it hands the network clean motion information on a plate, instead of forcing a CNN to discover the very concept of motion from raw RGB pixels. Computing flow is a well-studied physics-like problem (brightness constancy, the aperture problem — all from Guide 2); doing it as a preprocessing step removes a huge burden from the learner. The network can then spend its capacity on which motion patterns mean which action, rather than on reinventing motion estimation. Empirically the temporal stream alone often beats the spatial stream on motion-heavy actions — strong evidence that motion is where the signal lives.
Finally, fusion: how to combine the two streams. The simplest is late fusion — let each stream produce its own class scores and average (or weight) them, much like the baseline but with one stream now motion-aware. A richer option is mid fusion — splice the two streams together at an intermediate layer so the network can learn joint appearance-plus-motion features (e.g. "this object moving this way"). Mid fusion usually wins because it lets the streams interact before deciding, but it is heavier to train. Either way, the two-stream design is this track's payoff for temporal modeling via optical flow, and it long held the top of the action recognition charts.
3D convolutions: learning space-time filters
The second big idea removes the precompute tax by teaching the network to find motion itself. The move is almost embarrassingly natural once you see it. A 2D filter slides over two axes — height and width — watching a small square patch (say 3×3 pixels). A 3D filter slides over three axes — height, width, AND time — so it watches a small spatio-temporal cube (say 3×3×3: three pixels tall, three wide, three frames deep). Such a cube can directly contain a motion event, like "an edge that is one position to the left in frame t and one position to the right in frame t+1" — i.e. an edge moving rightward. The filter learns this pattern end-to-end from raw frames, no precomputed flow needed.
3D convolution: one output value is a weighted sum over a small cube of space and time.
Read this against the 2D convolution you already know. In 2D you had y(i,j) = Σ w(di,dj)·x(i+di, j+dj): an output pixel at row i, column j is a weighted sum of nearby input pixels, with offsets di, dj ranging over the filter (e.g. −1,0,+1 for a 3×3). The 3D version adds one new index, t, the temporal position (which frame), and a matching new offset d_t. So: x(t+d_t, i+d_i, j+d_j) is the input pixel d_t frames away, d_i rows away, d_j columns away; w(d_t, d_i, d_j) is the corresponding learned weight of the 3D kernel; and the triple sum slides the cube over height, width, AND time, producing an output value y(t,i,j) for every space-time location. The intuition: each output summarizes a little block of space-time, so the filter can respond to motion, not just static texture.
Let's make the extra index concrete with a tiny worked sketch: a single 3×3×3 filter sweeping a 3-frame stack. To produce the output at center position (t, i, j), you line the cube up so its middle cell sits on input pixel (t, i, j), then multiply-and-add over all 27 cells. The cube reaches one frame back (t−1) and one frame forward (t+1), one pixel in each spatial direction.
# A single 3x3x3 filter over a 3-frame stack, one output value.
# x has shape (T, H, W); w has shape (3, 3, 3) indexed by (dt, di, dj) in {-1,0,1}.
# This computes y[t,i,j] for ONE center location -- the network slides this over all t,i,j.
def conv3d_one_output(x, w, t, i, j):
acc = 0.0
for dt in (-1, 0, 1): # NEW: sweep over time (previous, current, next frame)
for di in (-1, 0, 1): # sweep over rows (same as 2D)
for dj in (-1, 0, 1):# sweep over cols (same as 2D)
acc += w[dt + 1][di + 1][dj + 1] * x[t + dt][i + di][j + dj]
return acc # one number summarizing a 3x3x3 cube of space-time
# Compared to 2D conv, the ONLY new thing is the outer `for dt` loop:
# the filter now also looks one frame back and one frame forward,
# so a weight pattern like 'bright on the left at t-1, bright on the
# right at t+1' fires for an edge moving rightward -- motion, learned directly.Stacking many such layers builds a 3D convolutional network; the prototype is C3D, which stacked 3×3×3 convolutions much like a VGG net but in space-time. As you go deeper the receptive field now grows in time as well as space: a neuron in an early layer sees a few frames, but a neuron several layers up has, through the stack, an effective window of many frames — long enough to perceive a whole gesture, not just a flicker. This is the same receptive-field growth you know from 2D, now along a third axis.
A small kernel grid overlaid on an input, multiplying overlapping values and summing them into one output cell, then sliding to the next position.
A diagram showing how a neuron in a deeper layer connects back through several layers to a wide patch of the original input.
I3D: inflating 2D into 3D, reusing ImageNet
We ended the last section with a sharp problem: 3D nets are powerful but data-hungry, and training one from scratch is hard. Here is the clever resolution. We already have superb 2D nets pretrained on ImageNet — millions of labeled images taught them rich appearance features (edges, textures, object parts) that took enormous compute to learn. It would be a waste to throw that away. The Inflated 3D ConvNet (I3D) reuses it: take a proven 2D architecture and its pretrained weights, and inflate every 2D kernel into a 3D kernel by copying it across the new time dimension. The result is a 3D convolutional network that, on day one, already understands what things look like.
Kernel inflation: copy the 2D filter to all T time slices, divided by T.
Unpack this carefully. w_2D(i, j) is one weight of a pretrained 2D filter at spatial offset (i, j). The new 3D filter has an extra time axis with T slices (T is the filter's temporal depth, e.g. T = 3 or 7). We set every time slice d_t = 1…T to the same 2D weight, and divide by T. Why divide by T? Consider feeding the inflated net a 'video' that is just one image repeated T times — a frozen clip. At each spatial location the 3D filter sums its T identical slices over T identical frames: that is T copies of (w_2D/T)·x, which add back up to exactly w_2D·x — the original 2D response. So a static video run through the inflated net reproduces the 2D net's behavior exactly: a sane, faithful initialization rather than random noise.
That bootstrapping logic is the whole point. By construction, the inflated 3D net starts life behaving like a strong image classifier, so it never has to relearn appearance from zero; training on video then only has to teach it the temporal part — how those features should change over time. We are spreading known 2D knowledge evenly across time as a warm start, then fine-tuning on real (moving) video. This turns an intractable from-scratch 3D training problem into a gentle fine-tuning problem, and it is the trick that finally made deep 3D nets practical.
The final flourish unites both prior sections. The strongest I3D systems combine inflation with the two-stream idea: train one I3D on RGB frames (inflated from ImageNet) and a second I3D on stacked optical flow, then fuse their predictions — appearance and motion, each now a deep 3D net. Paired with Kinetics, a large, diverse human-action dataset built precisely to feed data-hungry 3D nets, two-stream I3D set the modern benchmark for action recognition. So our three ideas are not rivals; the field's best result for years was literally all three at once: inflation (Section 5) of 3D convs (Section 4) in a two-stream arrangement (Section 3).
Temporal modeling beyond convolution, and how we measure it
3D convolution is not the only way to do temporal modeling. A classic alternative: run a 2D CNN on each frame to get a per-frame feature vector, then feed that sequence of vectors into an RNN / LSTM. The recurrent net carries a hidden state from frame to frame — a running memory — so its prediction at frame t depends on everything seen before, which makes it order-sensitive by design (it passes the shuffle test). This cleanly separates labor: the CNN handles space, the RNN handles time. It was popular before 3D convs matured, and remains a reasonable, lightweight choice.
The more modern approach is space-time attention in video transformers. Recall attention from the transformer track: instead of only looking at fixed local neighbors (as convolution does), attention lets every position compute a weighted relationship to every other position, learning which ones to relate. Video transformers like TimeSformer and ViViT apply this across frames: a patch in frame t can attend directly to a patch in frame t+20, with no convolution stack needed to bridge the gap. The slogan is *"let the model decide which moments to relate"* — the network itself learns that, say, the wind-up and the throw are the two moments that define "pitching," however far apart they sit.
A diagram of attention: a set of query, key, and value vectors producing a weighted-sum output, with several parallel attention heads merged at the end.
This surfaces a practical axis: short-range vs long-range temporal reasoning. Convolutions and small clips excel at short-range cues (a single swing, a footstep). But many real actions are long — "assembling furniture," "making tea" — unfolding over many seconds or minutes. Long actions are hard for a sober reason: we usually feed the network only a short clip (a handful to a few dozen frames) for compute reasons, so it never sees the whole event at once. This makes sampling matter enormously: do you take consecutive frames (dense, fine motion, tiny time window) or frames spread across the whole video (sparse, coarse motion, full coverage)? That choice can decide whether the network even has the evidence to answer.
Finally, how do we measure success? Two granularities matter. Clip-level accuracy scores each short clip's prediction; video-level accuracy scores the whole video, usually by running the net on several clips sampled from it and averaging — almost always higher and the headline number. As in image recognition, we report top-1 (is the single best guess correct?) and top-5 (is the truth among the top five guesses?) on benchmarks like Kinetics. And running through all of this is the eternal accuracy-vs-compute trade-off: 3D nets and transformers are accurate but heavy, while frame-level methods are cheap but temporally blind.
- Is the class decidable from one frame (a scene, an object)? A 2D CNN with frame-averaging / late fusion is cheap and strong — start here.
- Is the action defined by motion, and is precomputed optical flow affordable? Reach for a two-stream network (or a flow stream alongside RGB).
- Want end-to-end motion learning with no flow, and have a large dataset (or an ImageNet 2D net to inflate)? Use a 3D conv net, ideally I3D-initialized.
- Need to relate distant moments or model long actions, and have compute to spare? Use a video transformer with space-time attention.
- Tight on compute or memory? Fall back to 2D-CNN-plus-LSTM, or sparse frame sampling with a light 2D backbone.
That decision tree is your working map of video classification and action recognition. You now know the four levers — frame fusion, two streams, 3D convolution, and attention — and how each one injects temporal order, plus how to evaluate and trade off the result. Next, in the final guide of this track, we push from labeling a whole clip to reasoning densely over video: segmenting and interpolating every pixel through time.