ScreenshotNeo

BlogScreenshots on your device

How to Take Screenshots with Python and DXcam

Capture a Windows desktop screenshot with Python and DXcam, crop regions, read frames continuously, and handle common capture issues.

By the ScreenshotNeo team4 October 20269 min read

To take a one-time screenshot with DXcam, install the package, create a camera, and call grab(). DXcam is a Windows-focused Python library; the returned frame is a NumPy array. If you need the latest frame even when the screen has not changed, pass new_frame_only=False.

pip install dxcam

import dxcam

with dxcam.create() as camera:
    frame = camera.grab()

if frame is None:
    print("No new frame was available")
else:
    print(frame.shape, frame.dtype)

The example captures pixels from the local Windows desktop. It does not capture a webpage by URL. For the project’s documented options and current installation notes, see the DXcam README and DXcam on PyPI.

1. Install DXcam and check your environment

Install DXcam into the Python environment that will run your script:

python -m pip install dxcam

DXcam is for Windows desktop capture. The project README lists official Windows wheels for CPython 3.10 through 3.14; package compatibility can change, so check the current README and PyPI files if installation fails. The README describes DXcam as a high-performance Python screenshot and capture library for Windows based on the Desktop Duplication API. Microsoft’s documentation explains the underlying desktop duplication API in more detail: Desktop Duplication.

Verify that Python can import the package:

python -c "import dxcam; print('DXcam import succeeded')"

2. Take a one-time screenshot

Create a camera with dxcam.create(), then call grab(). The camera can be used as a context manager, which releases capture resources when the block ends.

import dxcam

with dxcam.create() as camera:
    frame = camera.grab()

    if frame is None:
        print("No new frame has appeared yet")
    else:
        print("Frame shape:", frame.shape)
        print("Data type:", frame.dtype)

frame is a NumPy array. By default, grab() can return None if no new frame has appeared since the last capture. This is useful when you only want changed frames, but it can surprise a one-shot script. Ask for the current frame regardless of whether it changed:

frame = camera.grab(new_frame_only=False)

To save the array as an image, install OpenCV and use an output format that OpenCV expects. DXcam’s README recommends BGR for OpenCV video writing; this example converts to BGR and writes a PNG:

python -m pip install opencv-python
import cv2
import dxcam

with dxcam.create(output_color="BGR") as camera:
    frame = camera.grab(new_frame_only=False)

if frame is None:
    raise RuntimeError("DXcam did not return a frame")

if not cv2.imwrite("screenshot.png", frame):
    raise RuntimeError("OpenCV could not write screenshot.png")

DXcam’s processor backend can handle color conversion. The README recommends cv2 when OpenCV is installed and numpy otherwise. If you want the leanest dependency path and can consume BGRA data yourself, use output_color="BGRA" and avoid adding OpenCV just for capture.

3. Capture part of the screen

Pass a region as (left, top, right, bottom). These are desktop coordinates, and the right and bottom values mark the far edge of the requested region.

import dxcam

left, top, right, bottom = 100, 100, 900, 700

with dxcam.create() as camera:
    region_frame = camera.grab(
        region=(left, top, right, bottom),
        new_frame_only=False,
    )

if region_frame is None:
    raise RuntimeError("No frame was returned")

print("Cropped frame shape:", region_frame.shape)

Choose coordinates that fit the target display and account for your desktop layout. Do not assume every monitor is 1920×1080. For multiple displays or unusual layouts, inspect the desktop configuration and confirm that your requested coordinates land on the intended screen. If the crop is shifted or empty, first test a full-screen grab and then adjust the bounds.

4. Keep reading the newest frame

For continuous capture, start DXcam’s polling thread, read frames from its ring buffer, and stop capture when the consumer is done. Put cleanup in a finally block so an exception does not leave capture running.

import time
import dxcam

camera = dxcam.create(output_color="BGR")
camera.start(target_fps=60)

try:
    for _ in range(300):
        frame = camera.get_latest_frame()
        if frame is not None:
            # Replace with image processing, inference, or another consumer.
            print(frame.shape)
        time.sleep(1 / 60)
finally:
    camera.stop()
    camera.release()

get_latest_frame() reads the newest available frame. For a frame paired with its capture time, use get_latest_frame(with_timestamp=True), as documented by the project. Choose the exact handling based on the installed DXcam version and the return value documented for that version.

latest = camera.get_latest_frame(with_timestamp=True)
if latest is not None:
    frame, timestamp = latest
    print("Captured at:", timestamp)

The ring buffer has a documented default size of 8 and can be adjusted with max_buffer_len. It uses memory, and newer frames overwrite older ones when the buffer fills. Increase the buffer only when a consumer needs more time to catch up; a larger buffer uses more memory and still does not guarantee that a slow consumer processes every frame.

Choose whether to repeat unchanged frames

With video_mode=True, DXcam fills the ring buffer at the target FPS by repeating the previous frame when the desktop has not rendered a new one. This can suit a fixed-rate video or training pipeline that expects a frame at every interval. If your work should process only actual desktop updates, use the normal mode and handle the possibility that no new frame is available.

camera = dxcam.create(output_color="BGR")
camera.start(target_fps=30, video_mode=True)
try:
    frame = camera.get_latest_frame()
    # Consume the frame according to your pipeline's timing needs.
finally:
    camera.stop()
    camera.release()

5. Select a capture backend and output format

Choice When to start with it What to consider
dxgi Default starting point, especially for one-shot grabs Uses the Desktop Duplication path. Start here for most workloads, then assess behavior on your application and machine.
winrt When cursor rendering is needed or its application fit is better Windows Graphics Capture is the alternative backend. The project does not establish one universal performance winner.
BGRA When minimizing extra dependencies matters DXcam obtains BGRA frames; BGRA can avoid requiring OpenCV for conversion.
RGB, RGBA, BGR, GRAY When the consumer expects one of these formats Conversion is handled by a processor backend. The README recommends OpenCV when installed and NumPy otherwise.

Set the backend when creating the camera, using the option documented by the installed release. A typical DXGI selection is:

camera = dxcam.create(backend="dxgi")

For a workload that needs cursor rendering, try the documented WinRT backend and compare the result on the target machine:

camera = dxcam.create(backend="winrt")

Backend behavior and option availability can be version-dependent. Consult the current DXcam README before relying on an option in a deployed script.

6. DXcam configuration at a glance

Need DXcam setting or method Practical note
One screenshot camera.grab() Can return None if there is no new frame.
Latest frame even if unchanged camera.grab(new_frame_only=False) Useful for one-shot capture and polling workflows.
Crop region=(left, top, right, bottom) Use desktop coordinates that fit the intended display.
Continuous capture start(target_fps=...), get_latest_frame(), stop() Read at a pace your consumer can handle.
Timestamp get_latest_frame(with_timestamp=True) Use when associating frames with capture times.
Repeat previous frame at target rate video_mode=True Useful for fixed-rate consumers; repeated images are not new desktop updates.
Buffer capacity max_buffer_len Default documented size is 8; larger buffers consume more memory.
Capture backend backend="dxgi" or backend="winrt" DXGI is the default starting point; evaluate WinRT for cursor rendering or application fit.
Output color output_color Documented formats: RGB, RGBA, BGR, BGRA, and GRAY.
Resource cleanup Context manager or release() A released instance cannot be reused.

7. Common errors and fixes

Symptom Likely cause What to do
ModuleNotFoundError: No module named 'dxcam' DXcam was installed into a different Python environment. Run python -m pip install dxcam with the same Python executable that runs your script. In a virtual environment, activate it before installing.
Installation reports no compatible distribution or wheel The Python version, architecture, or Windows environment may not match an available package build. Check the current project README and PyPI files for supported wheels. Confirm which interpreter runs with python --version and whether it is the intended Windows Python.
camera.grab() returns None No new frame has appeared since the last capture. Use camera.grab(new_frame_only=False) when you need the current screen even if unchanged. In a continuous loop, skip or wait on missing frames as appropriate.
Crop is empty, shifted, or the wrong size Bounds do not match desktop coordinates or the target display. Try a full-screen capture, inspect the returned array dimensions, and adjust (left, top, right, bottom) to the actual display layout.
Saved image has unexpected colors The consumer expects a different channel order. Use an output format suited to the consumer. OpenCV commonly expects BGR for writing; DXcam documents BGRA, RGB, RGBA, BGR, and GRAY choices.
Cursor is missing The selected capture path may not fit the cursor-rendering requirement. Try the WinRT backend and check the current DXcam documentation for the installed version.
Capture stops after an exception or hangs during shutdown Continuous capture was not stopped reliably. Call camera.stop() in a finally block, then release the camera. Do not reuse an instance after releasing it.
Consumer seems to skip frames The ring buffer has filled and newer frames overwrite older ones, or the consumer is slower than capture. Reduce capture rate or processing cost, or increase max_buffer_len if memory allows and the consumer needs more backlog. A larger buffer cannot make processing faster.

8. Performance, reliability, and cost

DXcam’s project README promotes capture throughput of “240+fps on 1080p.” That is a project-published capability claim, not an independent benchmark or a guarantee for a particular computer. The gathered project material does not provide a benchmark protocol or independent replication. Measure the full pipeline on the target machine, including capture, color conversion, copying, and downstream processing.

For lower overhead, capture only the region you need, choose the output format your consumer already accepts, and avoid work inside a tight capture loop that can be done later. For continuous capture, tune target_fps and buffer capacity to the consumer rather than assuming it will process every acquired frame. Use a context manager for one-shot work; use try/finally to stop and release a running camera.

DXcam is a local software library installed with pip; the documented workflow does not require a paid screenshot service, capture card, or particular monitor model. Hardware and application behavior still affect results, so verify coordinates, cursor behavior, and sustained throughput on the Windows system where the script will run.

9. Or skip the browser setup

DXcam captures the local Windows desktop. If the task is to capture a webpage from its URL, ScreenshotNeo provides a website screenshot API and MCP server. See the ScreenshotNeo API documentation for request options.

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,
)
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(`ScreenshotNeo returned ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
  • Cookie banners are accepted and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before capture; each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers identify the page verdict and billing status.
  • An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf.
  • The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

10. Frequently asked questions

Can DXcam capture a website by URL?

No. DXcam captures the Windows desktop. To render a webpage from a URL, use a browser automation workflow or a website screenshot API such as ScreenshotNeo.

Does DXcam work on macOS or Linux?

The project describes DXcam as a Windows capture library. This guide does not treat it as cross-platform; use a platform-specific capture method outside Windows.

Can I reuse a camera after calling release()?

No. The project documentation says a released instance cannot be reused. Create a new camera when another capture session is needed.

Should I choose DXGI or WinRT?

Start with DXGI, the default path recommended by the project for most workloads and one-shot grabs. Try WinRT when cursor rendering or application constraints make it a better fit, then compare behavior on your own system.

Will DXcam always produce a new frame on every call?

No. In the default one-shot behavior, grab() can return None when no new frame has appeared. Request the latest frame with new_frame_only=False when an unchanged frame is still useful.

Sources