AI & Computer Vision, lesson 5 of 52
Video Basics: Frames, FPS, Reading & Saving Video
Introduction
Every match replay, every VAR clip and every highlight reel is the same thing underneath: a stack of still pictures. Today you'll open video files frame by frame, record your own webcam to a file with the time stamped on every frame, and make a slow-motion copy, and find out why it looks jerky.
By the end of this lesson you'll be able to:
- Explain that video is a stack of still frames shown quickly, and say what FPS and resolution mean
- Read a video file frame by frame with
cv2.VideoCapture - Read a video's FPS, frame count and size from its properties
- Save frames to a new file with
cv2.VideoWriter, with the right codec and frame size - Put a timestamp on every frame
- Explain how slowing a clip down differs from recording at a higher frame rate
Working principle
Video is frames
- A frame is one still image, exactly the array you met in lesson 3.
- FPS is how many frames are shown per second. Webcams usually give 15 to 30. Phones give 30 or 60.
- Resolution is the frame width and height in pixels, for example 640 by 480 or 1920 by 1080.
- A codec compresses the frames to save space (for example H.264). A container (
.mp4,.avi) is the file that holds them. OpenCV picks the codec with a four-letter code called a fourcc.mp4vis the safe choice for.mp4files.
Each page is a frame. This kick took 8 frames. Page 1 of 8.
At 30 FPS, a 10-second clip is 300 frames. At 1920 by 1080 in colour, uncompressed, that is almost 2 GB. Compression matters.
Reading a video file
import cv2
cap = cv2.VideoCapture("clip.mp4") # a file name instead of 0 for the webcam
fps = cap.get(cv2.CAP_PROP_FPS)
frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"{w}x{h}, {fps:.1f} FPS, {frames} frames, {frames / fps:.1f} seconds")
count = 0
while True:
ret, frame = cap.read()
if not ret: # no more frames: the file has ended
break
count += 1
cap.release()
print("Frames read:", count)For a file, ret becoming False means the video ended. For a webcam it can also mean the camera was disconnected.
Writing a video file
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
out = cv2.VideoWriter("copy.mp4", fourcc, fps, (w, h)) # size is (width, height)
...
out.write(frame) # the frame MUST be exactly (w, h)
...
out.release() # without this the file is broken or emptyThe three classic mistakes: giving the size as (height, width), writing a frame that is a different size, and forgetting release(). All three give you a file that won't play.
Timestamps and slow motion
- Wall-clock timestamp:
datetime.now().strftime("%H:%M:%S.%f")[:-3] - Video timestamp:
frame_index / fpsseconds into the clip. Use this when reading a file, because the wall clock is when you ran the script, not when the video was filmed. - Slow motion (cheap): write the same frames with a lower FPS. 30 FPS written as 15 FPS plays at half speed. It will look choppy.
- Time-lapse: the opposite. Keep every 10th frame and write at 30 FPS.
Many webcams report an FPS of 0. If so, measure it: count frames over a few seconds, and use 30 as a fallback.
What you'll need
- Your
cv_courseenvironment from lesson 2 - A webcam
- A short video clip (
match.mp4). No clip?demo_clip.pybelow makes a pretend one.
Build it
Helper: demo_clip.py (a fake match clip)
Save this once. Every later lab can call it, so nobody is stuck without footage. The orange ball and the blue and red "players" move across a green pitch.
# demo_clip.py: makes a fake match clip so every lab works without real footage
import cv2
import numpy as np
def make_demo_clip(path="demo_clip.mp4", seconds=6, fps=30, size=(640, 360)):
w, h = size
out = cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*"mp4v"), fps, size)
total = seconds * fps
for i in range(total):
t = i / total
f = np.zeros((h, w, 3), np.uint8)
f[:] = (34, 142, 28) # grass (B, G, R)
cv2.rectangle(f, (20, 20), (w - 20, h - 20), (255, 255, 255), 2)
cv2.line(f, (w // 2, 20), (w // 2, h - 20), (255, 255, 255), 2)
cv2.circle(f, (w // 2, h // 2), 40, (255, 255, 255), 2)
bx = int(40 + t * (w - 80)) # the orange ball
by = int(h / 2 + 60 * np.sin(t * 6.28 * 2))
cv2.circle(f, (bx, by), 9, (0, 140, 255), -1)
teams = [(160, (200, 60, 30)), (300, (200, 60, 30)), # blue shirts
(420, (30, 30, 210)), (520, (30, 30, 210))] # red shirts
for k, (x0, colour) in enumerate(teams):
px = int(x0 + 25 * np.sin(t * 6.28 + k))
py = int(h / 2 + 80 * np.cos(t * 6.28 * 1.5 + k))
cv2.circle(f, (px, py), 14, colour, -1)
out.write(f)
out.release()
return path
if __name__ == "__main__":
print("Made", make_demo_clip())Step 1: inspect a clip
import os
import cv2
from demo_clip import make_demo_clip
path = "match_clip.mp4"
if not os.path.exists(path): # no real clip? make the fake one
path = make_demo_clip()
cap = cv2.VideoCapture(path)
fps = cap.get(cv2.CAP_PROP_FPS)
frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"{path}: {w}x{h}, {fps:.1f} FPS, {frames} frames, {frames / fps:.1f} s")
count = 0
while True:
ret, frame = cap.read()
if not ret:
break
count += 1
cap.release()
print("Counted frames:", count)Question: is the counted number the same as CAP_PROP_FRAME_COUNT? Why might they differ a little?
Step 2: record 10 seconds from the webcam, with a timestamp
import time
from datetime import datetime
import cv2
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise SystemExit("Cannot open webcam. Try index 1 or 2.")
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
cam_fps = cap.get(cv2.CAP_PROP_FPS)
fps = cam_fps if cam_fps and cam_fps > 1 else 30.0 # many webcams report 0
out = cv2.VideoWriter("my_clip.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
start = time.time()
frames_written = 0
while time.time() - start < 10: # record for 10 seconds
ret, frame = cap.read()
if not ret:
break
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
cv2.putText(frame, stamp, (10, h - 12), cv2.FONT_HERSHEY_SIMPLEX,
0.6, (0, 255, 255), 2, cv2.LINE_AA)
out.write(frame)
frames_written += 1
cv2.imshow("Recording", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
elapsed = time.time() - start
cap.release()
out.release()
cv2.destroyAllWindows()
real_fps = frames_written / elapsed
print(f"{frames_written} frames in {elapsed:.1f} s = {real_fps:.1f} real FPS")Question: the real FPS may be lower than the camera's claimed FPS. Why does that matter when the clip is played back at the claimed FPS?
Step 3: slow-motion copy, and one frame as a picture
import cv2
src = "my_clip.mp4"
cap = cv2.VideoCapture(src)
fps = cap.get(cv2.CAP_PROP_FPS)
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Same frames, written at half the FPS: plays at half speed
slow = cv2.VideoWriter("my_clip_slow.mp4", cv2.VideoWriter_fourcc(*"mp4v"),
fps / 2, (w, h))
index = 0
while True:
ret, frame = cap.read()
if not ret:
break
seconds = index / fps # video time, not wall-clock time
cv2.putText(frame, f"t = {seconds:5.2f} s", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2, cv2.LINE_AA)
slow.write(frame)
index += 1
slow.release()
# Jump to frame 30 and save it as a picture
cap.set(cv2.CAP_PROP_POS_FRAMES, 30)
ret, frame = cap.read()
if ret:
cv2.imwrite("frame_30.jpg", frame)
cap.release()
print("Done:", index, "frames")Question: how long is the slow-motion clip compared with the original? What would you change to make it 4 times slower?
If it goes wrong
| Error | Cause | Fix |
|---|---|---|
| Output file is 0 KB or won't play | out.release() missing, or frame size doesn't match | Call release(). Make sure each frame is exactly (w, h). |
cv2.VideoWriter seems to work but writes nothing | Size given as (height, width) | Use (width, height) |
CAP_PROP_FPS returns 0 | The webcam doesn't report its FPS | Use 30 as a fallback, or measure it |
| Clip plays too fast or too slow | Written FPS doesn't match the real capture rate | Measure real FPS, and write with that |
cap.read() returns False immediately | Wrong file path, so the file didn't open | Check cap.isOpened() and the path |
| Slow-motion looks jerky | You slowed the playback, not the capture | Expected. Explain it. |
| Resizing a frame breaks the writer | The size changed after the writer was created | Resize before writing and create the writer with the new size |
Try it
- Make a time-lapse: keep every 5th frame and write at the original FPS
- Add a frame counter and a red recording dot to the overlay
- Resize frames to half size before writing and compare file sizes
Check what you learnt
How many frames are in a 10-second clip at 30 FPS?
While reading a file, ret comes back False. What does that mean?
Why must VideoWriter's frame size match the frames you write?
What's the difference between a codec and a container?
Which gives smooth slow motion?
Conclusion
Video is frames shown quickly. cv2.VideoCapture reads them one at a time from a camera or a file, its properties tell you the FPS and size, and cv2.VideoWriter saves frames back out (same size, a codec and a container). Smooth slow motion needs more frames recorded, not slower playback.
What next?
Next: JS for CV: browser vision basics. You'll do the same in a web browser, with JavaScript.

