Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use OpenCV when you need a straightforward Python loop: open the file with cv2.VideoCapture, call read() until its success flag is false, and write each decoded frame with cv2.imwrite(). The loop below extracts every frame without relying only on the file’s reported frame count.

import cv2
from pathlib import Path

video_path = "input.mp4"
out_dir = Path("frames")
out_dir.mkdir(parents=True, exist_ok=True)

cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
    raise RuntimeError(f"Could not open {video_path}")

index = 0
try:
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        if not cv2.imwrite(str(out_dir / f"frame_{index:06d}.jpg"), frame):
            raise IOError(f"Could not write frame {index}")
        index += 1
finally:
    cap.release()

print(f"Wrote {index} frames to {out_dir}")

Install OpenCV with python -m pip install opencv-python. OpenCV documents read() as acquisition plus decoding, with a false flag when no frame is grabbed; its VideoCapture reference also covers opening and releasing captures.

Install the library and verify the input

Create an isolated environment if this is a project rather than a one-off script:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell: .venvScriptsActivate.ps1
python -m pip install --upgrade pip
python -m pip install opencv-python

Use a real path, not a directory or a URL that OpenCV cannot open through your installed backend. Always test cap.isOpened() before entering the loop. A failed open commonly means the path is wrong, the file is inaccessible, or the installed OpenCV build lacks a usable backend for that media.

Extract every frame with OpenCV

The complete example in the introduction writes sequential JPEG files named frame_000000.jpg, frame_000001.jpg, and so on. The six-digit padding keeps directory listings in numeric order. The counter records frames that were actually decoded, not merely the container’s advertised count.

Why the success flag controls the loop

read() returns a pair: a Boolean indicating whether a frame was obtained and the decoded image. Stop on False. Metadata properties such as frame count can be useful for progress displays, but they are not a safe replacement for checking the return value at end of file. OpenCV’s API and video-I/O property documentation describe these position and capture properties, while also noting that behavior depends on the selected backend: VideoCapture and video-I/O flags.

Choose an output format

Change the suffix passed to imwrite to select a supported encoder, for example .png for lossless output or .webp where your build provides that encoder. JPEG is usually much smaller, but it is lossy. Check the Boolean result from imwrite if failed writes must be detected immediately.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Save only selected frames

Every Nth decoded frame

To sample a video while reading it sequentially, increment the counter for every decoded frame and write only when the counter meets your interval. This avoids retaining all images in memory.

import cv2
from pathlib import Path

video_path = "input.mp4"
out_dir = Path("sampled")
out_dir.mkdir(exist_ok=True)
step = 10                         # save frames 0, 10, 20, ...

cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
    raise RuntimeError(f"Could not open {video_path}")

index = 0
saved = 0
try:
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        if index % step == 0:
            destination = out_dir / f"frame_{index:06d}.jpg"
            if not cv2.imwrite(str(destination), frame):
                raise IOError(f"Could not write {destination}")
            saved += 1
        index += 1
finally:
    cap.release()

print(f"Decoded {index} frames and saved {saved}")

This is the right pattern for requests such as “every 10 frames” or for processing a live-style stream while intentionally skipping intermediate images. It still decodes each intervening frame, because the loop must advance through the stream.

Sample by elapsed time

If you need one image approximately every few seconds, use a time target while iterating rather than assuming that a nominal frame rate is exact:

import cv2
from pathlib import Path

cap = cv2.VideoCapture("input.mp4")
if not cap.isOpened():
    raise RuntimeError("Could not open input.mp4")

out_dir = Path("time_samples")
out_dir.mkdir(exist_ok=True)
interval_seconds = 2.0
next_capture = 0.0
index = 0
saved = 0

try:
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        position_ms = cap.get(cv2.CAP_PROP_POS_MSEC)
        position_seconds = position_ms / 1000.0
        if position_seconds >= next_capture:
            cv2.imwrite(str(out_dir / f"t_{position_seconds:010.3f}.jpg"), frame)
            saved += 1
            next_capture += interval_seconds
        index += 1
finally:
    cap.release()

print(f"Decoded {index}; saved {saved} time samples")

Position properties are backend-dependent. Treat the resulting timestamp as an estimate for sampling, not proof that every format supports frame-perfect seeking.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capture one frame at a requested timestamp

OpenCV exposes frame and time-position properties, but exact seeking can vary with the media, codec and backend. If a precise FFmpeg-oriented operation is central to your workflow, ffmpegio documents both single-image timestamp capture and multi-frame reads.

python -m pip install ffmpegio
import ffmpegio

# Capture the image nearest 4 minutes, 25.3 seconds.
image = ffmpegio.image.read("input.mp4", ss="4:25.3")
image.tofile("at-time.raw")  # choose an appropriate image conversion for your pipeline

The ffmpegio documentation shows the timestamp form ffmpegio.image.read(..., ss='4:25.3'). For a sequence, its video reader accepts a start position and frame count and returns a frame rate plus a NumPy array; see the ffmpegio 0.11.0 documentation for the exact return values and supported arguments. In production, use its documented image-writing helpers or convert the returned array with Pillow/OpenCV rather than writing raw bytes unless your consumer expects that format.

Use PyAV when you need FFmpeg-level control

PyAV exposes containers, streams, packets and decoded frames directly. Its basic workflow is to open a container, select a video stream, decode frames and convert each frame to a PIL image or NumPy array.

python -m pip install av pillow numpy
import av
from pathlib import Path

out_dir = Path("pyav_frames")
out_dir.mkdir(exist_ok=True)

with av.open("input.mp4") as container:
    stream = container.streams.video[0]
    for index, frame in enumerate(container.decode(stream)):
        image = frame.to_image()       # PIL Image; Pillow is required
        image.save(out_dir / f"frame_{index:06d}.png")

VideoFrame.to_image() creates a PIL image and to_ndarray() creates a NumPy array; those conversions require their respective dependencies. The PyAV 18.1.0 documentation contains the decoding example and API details. Choose PyAV when packet, stream or codec control matters more than the smallest possible loop.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Other useful Python interfaces

Library Best fit Important constraint
OpenCV Sequential decode, image processing and saving in one familiar API. Seeking and available codecs depend on the backend and build; see the video-I/O flags.
PyAV Direct FFmpeg container, stream, packet and frame access; PIL or NumPy conversion. Install Pillow or NumPy for those conversions.
imageio-ffmpeg Generator-style frame reads through an FFmpeg subprocess. read_frames() accepts filenames rather than file-like objects, and frames move through pipes; details are in the project documentation.
ffmpegio Timestamp image capture and requested multi-frame NumPy reads. Requires an FFmpeg-oriented setup and follows the arguments documented at ffmpegio.
ImageIO A higher-level iteration interface, including its PyAV video plugin. Plugin and backend availability follow the installed ImageIO setup; see the video examples.

Decide by access pattern (sequential versus timestamp), desired representation (OpenCV array, PIL image or NumPy array), how much FFmpeg control you need, and which backend you can install and reproduce.

Performance, memory and reliability

  • Stream instead of accumulating: write or process each frame inside the decode loop. Holding thousands of full-resolution arrays can exhaust memory.
  • Reduce output volume: save every Nth frame or sample by time when you do not need every image. The decoder still has to advance through skipped frames in a sequential loop.
  • Separate decode from expensive processing: if resizing, OCR or neural inference is slow, queue bounded batches rather than allowing unlimited frames to accumulate.
  • Keep the capture lifecycle explicit: check isOpened(), use try/finally, and call release() even when writing or processing raises an exception.
  • Do not promise universal seek precision: container indexes, codecs and backends differ. Validate timestamps on representative files before building a frame-accurate editor around random access.
  • Plan disk space: lossless PNG output and full-resolution extraction can be much larger than the source video’s compressed stream. Use a temporary directory and monitor free space for long videos.

Troubleshooting common failures

“Could not open” or isOpened() is false

Print the absolute path and verify permissions. Try a file known to play locally. If only a particular codec fails, compare another OpenCV build or use PyAV/FFmpeg-based tooling; the available documentation does not establish one codec matrix that works on every operating system.

The loop stops before the expected number of frames

The reported frame count may be approximate or unavailable for the selected backend. The authoritative event for this loop is read() returning false. If decoding fails mid-file, test the same input with another backend and inspect the media with your normal FFmpeg utilities.

Timestamp selection is slightly early or late

Random access is not guaranteed to be frame-perfect across formats and backends. Decode sequentially and select based on observed positions when you need deterministic sampling, or use ffmpegio’s documented timestamp operation and verify its result on your media.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Images are missing or the output directory is empty

Create the directory before the loop, use a writable path, and check the Boolean returned by cv2.imwrite. A successful decode does not guarantee a successful write.

Memory usage keeps growing

Do not append every frame to a Python list. Process and release each image, save only the required samples, and keep any work queue bounded.

Colors look wrong after conversion

Keep track of the representation expected by the next library. OpenCV’s processing pipeline and PIL/NumPy conversions have different conventions; convert once at the boundary and avoid unnecessary round trips.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is for capturing webpages, not decoding a local video file. If the “video” you need is actually a page or an online visual you want rendered, its API can return a screenshot with one GET request. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

See the ScreenshotNeo API documentation for parameters and response handling.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Every plan includes the full feature set. The free tier includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account if webpage capture is the part of your workflow you want to automate.

Which approach should you choose?

  • Choose OpenCV for the default sequential “decode, process, save” script.
  • Choose PyAV when you need FFmpeg objects or direct PIL/NumPy frame conversion.
  • Choose ffmpegio for documented timestamp and multi-frame array operations.
  • Choose imageio-ffmpeg or ImageIO when a generator or higher-level iteration API fits your application.

Whichever library you select, validate the actual media and backend you will deploy, stop on a failed decode, release resources, and avoid assuming that metadata or seeking behaves identically for every file.

Frequently Asked Questions

Can I extract frames from an MP4 every 10 frames?

Yes. Decode sequentially with OpenCV, increment a counter for every successful read(), and save when index % 10 == 0.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do I get a frame at an exact time?

Use a documented timestamp operation such as ffmpegio.image.read(..., ss="4:25.3"), then verify the result because seek precision depends on the media and backend.

Should I use OpenCV or PyAV?

OpenCV is the simpler default for sequential processing. PyAV is a better fit when direct FFmpeg stream/container access or PIL/NumPy frame conversion is central.

Why does my script decode fewer frames than the metadata count?

Metadata counts are backend-dependent. Treat the read() success flag as the loop’s end-of-file signal and investigate any mid-file decode failure with another backend.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.