From one picture to many pictures a second
In the last four parts you learned to read a single image with OpenCV, clean it up with filters, find its edges and contours, and pick out regions of interest with thresholds and masks. Every one of those operations worked on a single NumPy array: rows, columns, and (usually) three colour channels in BGR order. Video does not change that picture at all. A video is simply a long list of those same arrays, shown one after another fast enough that your eyes perceive continuous motion instead of a slideshow.
That is the whole conceptual leap for this part: a webcam or a video file gives you a stream of frames, and each frame is an image exactly like the ones you have been working with since part 4. Once you can grab a single frame into a variable called frame, everything from parts 5, 6 and 7 applies unchanged. Blur it, convert it to HSV, threshold it, find its contours, draw on it. The only new things to learn are how to get the frames out of a camera or file in the first place, how fast they arrive, and how to write a new video back to disk.
This part sets up the machinery (VideoCapture, VideoWriter, the capture loop, frame timing) that the final project in this path, a webcam motion detector, will build on directly. If anything here feels familiar from still images, that is intentional: video in OpenCV is mostly still-image processing done in a loop.
What is actually inside a video
Before writing code it helps to know what a video file or a camera stream is made of, because a few of its properties (frame rate, resolution, codec) will show up directly as numbers you read and set in OpenCV.
Frames and frame rate
A frame is one still image in the sequence. The frame rate, usually written as fps (frames per second), tells you how many of these images are shown per second. Common values you will meet are 24, 25, 30 and 60 fps. At 30 fps, 30 separate images flash past every second; because your visual system blends successive images that are similar enough, you perceive smooth motion rather than a flicker of still pictures. A webcam typically reports something close to 30 fps, though the true achievable rate depends on the camera, the requested resolution, lighting, and how fast your computer can pull frames off the USB bus.
Why raw video is huge, and what a codec does
Recall from part 2 that a colour image is a grid of pixels, each pixel usually stored as three bytes, one per channel. A modest webcam frame of 640 by 480 pixels therefore takes 640 times 480 times 3, which is 921,600 bytes, just under one megabyte, for a single still image. At 30 frames per second that is roughly 27.6 megabytes of raw pixel data every second, which would make a one minute clip over a gigabyte if stored with no compression at all. Nobody ships video that way. A codec (short for coder-decoder) is an algorithm that compresses this sequence of frames, exploiting the fact that most pixels barely change from one frame to the next and that neighbouring pixels within a frame are often similar. Common codecs you will meet include H.264, MJPG and XVID. A container format such as MP4, AVI or MOV is the file wrapper that holds the compressed video data (and often audio and metadata) together; the file extension tells you the container, not necessarily the codec used inside it.
You do not need to understand codec internals to use OpenCV, but you will meet the words fourcc, container and codec again in this part when you save video to disk, so it helps to know that a fourcc (four character code, such as 'mp4v' or 'XVID') is simply a short label that tells OpenCV which codec to use when writing a file.
Opening a camera or a video file: VideoCapture
OpenCV uses one class, cv2.VideoCapture, for both live cameras and video files on disk. The only difference is what you pass it: an integer index for a camera, or a filename (or path) for a video file.
import cv2
# 0 usually refers to the first (often built-in) webcam.
# If that does not open your camera, try 1, 2, and so on.
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise IOError("Cannot open webcam. Try a different index, e.g. 1 or 2.")
print("Webcam opened successfully")
cap.release()
A few things worth noticing. The index passed to VideoCapture is not a universal ID for a physical camera; it is the position in the list of camera devices that your operating system currently sees, so it can change if you plug in a new camera or if the built-in one is numbered differently on your machine. That is why it is normal to simply try 0, then 1, then 2 until one opens. Second, cap.isOpened() is the standard way to check that the camera or file actually opened; always check it before trying to read frames, because a VideoCapture object that failed to open will not raise an error by itself, it will just quietly fail to produce frames later. Third, call cap.release() when you are finished. This frees the camera so that other programs (or a second run of your own script) can use it; forgetting this is one of the most common beginner mistakes with webcams, covered later in this part.
Opening a video file uses exactly the same function, with a path instead of an index:
import cv2
cap = cv2.VideoCapture("sample.mp4") # replace with a video file on your computer
if not cap.isOpened():
raise IOError("Cannot open video file. Check the path and that the file exists.")
print("Video file opened successfully")
cap.release()
If you do not have a sample video file handy, you can create one yourself later in this part using the VideoWriter example, or use any short clip already on your computer, for example one recorded on a phone and copied over. OpenCV relies on codecs available on your system to decode files, so an unusual or very new codec may fail to open even though the file plays fine in a media player; if that happens, try a different, more common file such as a plain MP4 encoded with H.264.
The capture loop: read, show, wait, release
Reading frames from a VideoCapture object always follows the same four-step pattern, whether the source is a webcam or a file: read a frame, do something with it, wait briefly and check for a quit key, and finally release the capture when the loop ends. Here is the smallest useful version, just showing the live webcam feed:
import cv2
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise IOError("Cannot open webcam")
while True:
ret, frame = cap.read()
if not ret:
print("Failed to grab a frame, stopping.")
break
cv2.imshow("Webcam", frame)
# waitKey(1) pauses for 1 millisecond and lets the window redraw;
# it also returns the key code of any key pressed during that time.
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
Walking through this line by line: cap.read() returns two things, a boolean ret and the frame itself. ret is True if a frame was successfully grabbed and False otherwise; frame is a NumPy array in BGR order, the same format you met in part 4, with shape (height, width, 3). Checking ret before using frame matters because, when reading from a file, read() will eventually return False once the file runs out of frames; if you skip the check, your next line of code will try to process None and crash. When reading from a live webcam, ret being False usually means something went wrong with the camera connection.
cv2.imshow("Webcam", frame) opens (or updates) a window named "Webcam" and displays the current frame in it, exactly like it did for a single static image in part 4. The difference here is that it is called once per loop iteration, so the window content keeps changing as new frames arrive.
cv2.waitKey(1) is doing two jobs at once. It pauses the program for up to 1 millisecond, which gives the operating system's window manager a chance to actually paint the updated image (without any call to waitKey, OpenCV windows do not refresh and appear frozen or blank). It also checks whether a key was pressed during that pause and returns its code, or -1 if no key was pressed. The expression & 0xFF masks the result down to the last 8 bits, which avoids platform-specific quirks in how key codes are returned, and ord("q") converts the character 'q' to its numeric code so you can compare them. The net effect of the line cv2.waitKey(1) & 0xFF == ord("q") is "wait briefly, and if the user pressed q, stop the loop".
Finally, cap.release() tells OpenCV you are done with the camera or file, and cv2.destroyAllWindows() closes any windows that imshow opened. Both should run even if something in the loop went wrong, which is why some developers wrap the loop in a try/finally block; for short scripts like these, placing the release calls immediately after the loop is usually enough, since break always falls through to them.
Saving a single frame: the bridge back to still images
Sometimes you do not want continuous video at all, just one good still image from a live camera, for example to capture a reference photo before running the filters or thresholds from earlier parts. Because each frame is an ordinary image, you can save it with cv2.imwrite exactly as you did in part 4:
import cv2
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise IOError("Cannot open webcam")
ret, frame = cap.read()
if ret:
cv2.imwrite("snapshot.png", frame)
print("Saved snapshot.png")
else:
print("Could not read a frame from the webcam")
cap.release()
One subtlety worth knowing: some webcams need a frame or two to adjust exposure and white balance after being opened, so the very first frame read immediately after VideoCapture(0) can occasionally look darker or more washed out than frames read a moment later. If your snapshots look odd, try reading and discarding a handful of frames (for example, call cap.read() five times in a loop) before saving the one you actually want.
Useful properties: width, height, fps, frame count
A VideoCapture object exposes a set of properties through cap.get(property_id), using constants defined in OpenCV such as cv2.CAP_PROP_FRAME_WIDTH. These are handy for understanding what you are working with, and for setting up a VideoWriter later with matching dimensions.
import cv2
cap = cv2.VideoCapture("sample.mp4") # or cv2.VideoCapture(0) for a webcam
if not cap.isOpened():
raise IOError("Cannot open video source")
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
print(f"Width: {width}, Height: {height}")
print(f"FPS: {fps}")
print(f"Frame count: {frame_count}")
if fps > 0:
print(f"Duration: {frame_count / fps:.1f} seconds")
cap.release()
For a video file, frame_count and fps together let you estimate the duration, since duration in seconds is simply the number of frames divided by how many appear each second. For a live webcam, frame_count is usually meaningless (there is no fixed end), and fps may report 0, a rough estimate, or even a value that does not match what the camera can truly sustain once you add processing; treat it as a hint rather than a guarantee.
You can also try to request a specific width, height or frame rate with cap.set(), for example cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280). This is only a request: some cameras and drivers will honour it exactly, others will round to the nearest resolution they support, and some will ignore it entirely. Always call cap.get() again afterwards to see what was actually applied, rather than assuming your set() call succeeded.
Seeking within a video file
When working with a file (not a live camera), you can jump directly to a particular frame instead of reading through everything before it, using cv2.CAP_PROP_POS_FRAMES:
import cv2
cap = cv2.VideoCapture("sample.mp4")
if not cap.isOpened():
raise IOError("Cannot open video file")
# jump to frame 100 and read it
cap.set(cv2.CAP_PROP_POS_FRAMES, 100)
ret, frame = cap.read()
if ret:
cv2.imwrite("frame_100.png", frame)
print("Saved frame 100")
cap.release()
This is useful if you want to sample every tenth or every hundredth frame of a long recording for analysis rather than process every single one, which also saves a lot of time. Note that seeking to an exact frame can occasionally be imprecise for some compressed formats, because the decoder may only be able to start cleanly from certain points in the file (called keyframes) and has to decode forward a little from there; for most everyday use this imprecision is a frame or two at most and does not matter.
Processing every frame with what you already know
This is the part where the earlier chapters in this path pay off directly. Because frame is just a NumPy array in BGR order, any still-image operation from parts 3, 5, 6 and 7 can be dropped straight into the capture loop. Here is a live demonstration that converts each frame to greyscale (part 3), blurs it slightly (part 5), and finds its edges with Canny (part 6), showing both the original and the processed version side by side in separate windows:
import cv2
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise IOError("Cannot open webcam")
while True:
ret, frame = cap.read()
if not ret:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 50, 150)
cv2.imshow("Original", frame)
cv2.imshow("Edges", edges)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
Nothing in this code is new on its own; cv2.cvtColor, cv2.GaussianBlur and cv2.Canny behave exactly as they did on single images, because they are being called on a single image, it just happens to be refreshed many times a second inside a loop. The same would be true if you dropped in cv2.inRange to build an HSV colour mask (part 7), cv2.findContours to outline shapes (part 6), or cv2.threshold to binarise the frame (part 7). The webcam motion detector project later in this path will combine several of these ideas, comparing each new frame against a previous one to decide whether something moved.
A note on region of interest and resizing in video
Cropping a frame with array slicing (frame[y1:y2, x1:x2]) and resizing it with cv2.resize work exactly as they do on still images, and both are worth using in video because they reduce the amount of data your processing code has to work through on every single frame. Shrinking a 1920 by 1080 frame down to, say, 640 by 360 before running an expensive operation like contour detection can make the difference between a smooth live preview and a sluggish, lagging one, often without any visible loss in the result if you only need an approximate answer such as roughly where motion occurred.
Writing video to a file: VideoWriter
To save a sequence of frames as a video file, OpenCV provides cv2.VideoWriter. Creating one requires four pieces of information: the output filename (whose extension picks the container, for example .mp4 or .avi), a fourcc code identifying the codec, the frame rate to record at, and the frame size as a (width, height) tuple. The frame size you pass must exactly match the size of the frames you will write to it; writing a frame of a different size will fail or produce a broken file.
import cv2
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise IOError("Cannot open webcam")
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
if not fps or fps != fps: # covers 0 and NaN, which some webcams report
fps = 20.0
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
writer = cv2.VideoWriter("output.mp4", fourcc, fps, (width, height))
print("Recording. Press q to stop.")
while True:
ret, frame = cap.read()
if not ret:
break
writer.write(frame)
cv2.imshow("Recording (press q to stop)", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
writer.release()
cv2.destroyAllWindows()
cv2.VideoWriter_fourcc(*"mp4v") builds the four-character codec code from the string "mp4v" (the asterisk unpacks the string's four characters as separate arguments, which is simply how this function expects them). If this particular codec is not available on your system, you will usually notice immediately: the writer object is created without an obvious error, but the output file ends up empty or will not open afterwards. In that case, try a different combination, such as fourcc = cv2.VideoWriter_fourcc(*"XVID") together with a filename ending in .avi instead of .mp4. Which codecs are available depends on how OpenCV was built and what is installed on your operating system, which is why there is no single combination guaranteed to work everywhere; if one fails, trying XVID with AVI is the most broadly supported fallback.
Also notice the small safety check around fps: some webcams report a frame rate of 0 or an invalid value through cap.get(cv2.CAP_PROP_FPS) before any frames have actually been read, especially right after opening. Passing 0 to VideoWriter would produce a file with no usable timing, so the code above falls back to a reasonable default of 20 frames per second when that happens. The line fps != fps looks unusual, but it is a standard trick for detecting NaN (not a number), because NaN is the one value in floating point arithmetic that is never equal to itself.
Timing, dropped frames, and what real time actually means
A live webcam produces frames at its own pace, roughly governed by its reported fps, whether or not your code is ready for them. A video file, by contrast, has no such pressure: cv2.read() on a file will hand you frames as fast as your loop can ask for them, with no built-in pause to match the original recording speed. This distinction matters in two common situations.
First, if you read a video file in a tight loop purely to process it (for example, to count something in every frame) and you do not need to watch it play at normal speed, that is fine, and often faster than real time, since there is no need to wait between frames. But if you want to display the file to a human viewer at a natural pace, reading as fast as possible will make it look sped up, because frames will be shown far quicker than the original recording intended. In that case you need to pace the display yourself, for example by using cv2.waitKey with a delay computed from the frame rate (roughly 1000 divided by fps, in milliseconds) between showing each frame.
Second, and more important for live webcam work, is what happens when your processing is slower than the camera's frame rate. If the camera produces 30 frames a second but your loop, including all its filtering, edge detection or contour finding, takes longer than about 33 milliseconds to process one frame, your program simply falls behind: cap.read() will keep returning the newest frame available each time you call it, so you effectively skip frames rather than see video in slow motion. This is usually fine for exploratory work and for the kind of beginner projects in this path, but it is worth knowing about, because it explains why a live preview sometimes feels laggy or jumpy once you add heavier processing steps, and why reducing frame size before processing (mentioned earlier) is a quick and effective fix.
If you want to see this effect directly, the snippet below counts how many frames your loop manages to process each second and prints that number, which will usually be close to your camera's native rate for a light loop (just displaying the frame) and noticeably lower once you add expensive operations:
import cv2
import time
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise IOError("Cannot open webcam")
frame_count = 0
start_time = time.time()
while True:
ret, frame = cap.read()
if not ret:
break
frame_count += 1
elapsed = time.time() - start_time
if elapsed >= 1.0:
print(f"Frames per second: {frame_count / elapsed:.1f}")
frame_count = 0
start_time = time.time()
cv2.imshow("Webcam", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
Try running this as it is, note the printed rate, then add the Canny edge pipeline from earlier into the loop and run it again. On most laptops the number will drop somewhat once you add the extra processing steps, which is a direct, measurable illustration of the real-time constraint described above. This kind of timing measurement is also the basis of later, more advanced techniques such as running the camera read and the processing on separate threads so that a slow processing step does not block frame capture, a refinement the motion detector project later in this path can build on if you want to take it further; it is not needed for anything in this part.
Common mistakes
- Forgetting cap.release(): the camera device stays locked, so a second run of your script (or another program) may fail to open it until the first process exits. If your webcam suddenly refuses to open, check for other running scripts or windows still holding it open.
- Not checking ret before using frame: once a video file ends, or if a webcam briefly drops its connection, cap.read() returns (False, None), and any code that assumes frame is always a valid array will crash. Always test ret first.
- Calling cv2.imshow without a following cv2.waitKey: without waitKey, the window manager never gets a chance to paint the image, so the window appears frozen, blank or unresponsive even though your code is running correctly.
- Assuming cap.set() always works: requests for a specific width, height or frame rate are only requests. Some cameras ignore values they do not support. Always read back the actual value with cap.get() if the exact number matters to your code, for example before constructing a VideoWriter.
- Mismatched VideoWriter frame size: if the (width, height) you pass to VideoWriter does not exactly match the shape of the frames you call writer.write() with, the file may fail to be created properly or may end up empty. Take the size from cap.get() on the same capture object you are reading frames from.
- Picking the wrong camera index: index 0 is a common default but is not guaranteed on every machine, especially laptops with multiple cameras or external webcams attached. If VideoCapture(0) fails or opens the wrong camera, try 1, 2 and so on.
- Expecting a video file to play back at its recorded speed automatically: reading frames from a file in a loop pulls them as fast as your code allows, with no built-in pause, so a preview window can appear to run in fast motion unless you add your own timing.
- Doing expensive per-frame work without resizing first: full-resolution contour finding, heavy blurring or colour mask searches on every frame of a 1920 by 1080 feed can make live video noticeably laggy; shrinking the frame first, as discussed above, is usually the simplest fix.
- Mixing up BGR and RGB when combining OpenCV video with other libraries, for example matplotlib, which expects RGB. This is the same colour order issue covered in part 4 for still images, and it applies identically to every frame of video.
Summary and what comes next
Video in OpenCV is a sequence of ordinary images delivered through a loop. cv2.VideoCapture opens either a live camera (by index) or a file (by path) with the same interface; cap.read() returns a success flag and a frame that is a plain NumPy array in BGR order, exactly like the images from part 4. Properties such as width, height, frame rate and frame count are available through cap.get(), with the caveat that live cameras do not always report them accurately and that cap.set() requests are not guaranteed to be honoured. Everything you learned for still images, colour conversion, blurring, edge detection, thresholding and masking, applies unchanged inside the capture loop, frame by frame. cv2.VideoWriter lets you save a processed stream back to disk, provided you give it a matching frame size and a fourcc code the installed codecs support. Finally, live video adds a genuinely new constraint that still images never had: your processing has to keep up with the camera's frame rate, or you start dropping frames, which is why resizing frames before heavy processing and measuring your actual achieved frame rate are both useful habits.
The next part in this path, From Hand-Made Features to Learned Features, steps back from the mechanics of capturing pixels and starts asking a different question: instead of hand-designing filters and thresholds for a specific task, how can a system learn, from examples, what features matter. That part works mostly with still images again, building the conceptual bridge toward the kind of models used in modern computer vision. After that, the three hands-on projects put everything from this entire path together, including a webcam motion detector that reuses the capture loop, frame differencing and thresholding techniques introduced in this part and in part 7.
Comments (0)
No comments yet. Be the first to share your thoughts.