Skillversitas

Video Capture and Processing

Video processing is the image pipeline in a loop: open a capture source (a file or a camera index), read frames one at a time, apply the per-frame operations, and write or display the results. The API is VideoCapture for input and VideoWriter for output, and the frame loop - read, process, write, check the key - is the same skeleton as the display chapter with a source attached. The new concerns are the frame rate, the codec, and the per-frame budget: every operation you add is multiplied by 30 frames a second.

Reading Video

import cv2

# from a file
cap = cv2.VideoCapture("clip.mp4")
print(cap.isOpened())
print("fps:", cap.get(cv2.CAP_PROP_FPS))
print("frames:", cap.get(cv2.CAP_PROP_FRAME_COUNT))
print("size:", cap.get(cv2.CAP_PROP_FRAME_WIDTH),
      cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

while True:
    ret, frame = cap.read()          # ret False at end of file
    if not ret:
        break
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(cv2.GaussianBlur(gray, (5, 5), 0), 100, 200)
    cv2.imshow("Edges", edges)
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()

cap.read() returns (ret, frame) - check ret, because at end-of-file (and on a failed open) the frame is None and processing it is the classic crash. The CAP_PROP_* getters report fps, frame count, and resolution; setting them (width, fps) works for some backends and is ignored by others, which is why "set the camera to 1280x720" silently does nothing on certain drivers. For a webcam, pass the index (0 for the default device) instead of a path; on Linux you can also pass a v4l2 device node directly.

Writing Video

import cv2

cap = cv2.VideoCapture("clip.mp4")
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0

# the fourcc encodes the codec; mp4v works in most mp4 outputs
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
out = cv2.VideoWriter("out.mp4", fourcc, fps, (w, h))

while True:
    ret, frame = cap.read()
    if not ret:
        break
    out.write(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))

cap.release()
out.release()

VideoWriter takes a codec fourcc, the fps, and the frame size - the size must match every frame you write, or the writer misbehaves. mp4v in an .mp4 container is the safe default; avc1 (H.264) gives smaller files but is not always available in the bundled build, and on Linux the XVID/FLV codecs in .avi are the reliable fallbacks. A gray frame written into a color-sized writer works but check the container - codec and extension must agree, which is the source of most "the file is 0 bytes" reports.

Per-Frame Processing and the Budget

import cv2
import time

cap = cv2.VideoCapture(0)
last = time.perf_counter()
fps_actual = 0.0

while True:
    ret, frame = cap.read()
    if not ret:
        break

    # a realistic per-frame chain, kept deliberately cheap
    small = cv2.resize(frame, (640, 360))
    gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    edges = cv2.Canny(blurred, 100, 200)

    now = time.perf_counter()
    dt = now - last
    last = now
    fps_actual = 0.9 * fps_actual + 0.1 * (1.0 / dt if dt > 0 else 0)

    cv2.putText(edges, f"{fps_actual:.0f} fps", (10, 24),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
    cv2.imshow("Video", edges)
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()

Every operation in the chain runs 30 times a second, so the budget discipline differs from stills: downscale early (processing at 640x360 instead of 1920x1080 is a 9x reduction), keep kernels small, and measure - the running fps in the corner is the one widget every video tool should have. When the fps drops, the fix is almost always resolution or a heavy filter (bilateral, non-local means, DNN inference), not the Canny or the color conversion. Frame timing with perf_counter and an exponential moving average is the standard instrumentation.

Frame Differencing: A Free Detector

import cv2

cap = cv2.VideoCapture(0)
prev = None

while True:
    ret, frame = cap.read()
    if not ret:
        break
    gray = cv2.cvtColor(cv2.resize(frame, (640, 360)), cv2.COLOR_BGR2GRAY)
    blur = cv2.GaussianBlur(gray, (5, 5), 0)

    if prev is not None:
        diff = cv2.absdiff(blur, prev)
        _, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
        thresh = cv2.dilate(thresh, None, iterations=2)
        contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL,
                                       cv2.CHAIN_APPROX_SIMPLE)
        for c in contours:
            if cv2.contourArea(c) > 500:
                x, y, w, h = cv2.boundingRect(c)
                cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
    prev = blur

    cv2.imshow("Motion", frame)
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()

Frame differencing - the absolute difference between consecutive blurred frames, thresholded, dilated, and run through findContours - is the zero-model motion detector: anything that changed becomes a contour. It is the first "video understanding" result you can get in twenty lines, the same six-line contour pipeline from earlier applied to a difference image. Its limits are well known - lighting changes trigger it, and it sees motion but not objects - which is exactly what motivates the DNN detectors in the next chapters.

Summary

Build the loop: VideoCapture (file path or device index), a while that reads (ret, frame) and checks ret, processes, and writes via VideoWriter with a matching codec, fps, and size - releasing both at the end. Treat the per-frame budget as the design constraint: downscale early, keep filters light, and instrument with a running fps. The frame-differencing recipe (absdiff, threshold, dilate, findContours) is the entry point to video understanding - motion as contours - and it sets the stage for the learned detectors that follow.