Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ Run tests:
python -m pytest
```

When adding, removing, or renaming a packaged file, update the explicit expected file list in
`src/tests/test_setup.py::test_sdist` and, for files included in binary distributions, in
`src/tests/test_setup.py::test_wheel`. Run the focused packaging tests:

```shell
python -m pytest src/tests/test_setup.py::test_sdist src/tests/test_setup.py::test_wheel
```

On headless GNU/Linux environments, run tests with a virtual display:

```shell
Expand Down
2 changes: 1 addition & 1 deletion demos/cat-detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def main() -> None:
monitor = sct.monitors[1]

# Compute the minimum size, in square pixels, that we'll consider reliable.
img_area = monitor["width"] * monitor["height"]
img_area = monitor.width * monitor.height
min_box_area = MIN_AREA_FRAC * img_area

# We start a new line of the log if the cat visibility status changes. That way, your terminal will show
Expand Down
16 changes: 8 additions & 8 deletions demos/tinytv-stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
from serial.tools import list_ports

import mss
from mss.models import Region

from common.pipeline import Mailbox, PipelineStage

Expand Down Expand Up @@ -330,21 +331,20 @@ def _scale_stretch(img: Image.Image, size: tuple[int, int]) -> Image.Image:
def capture_image(
*,
monitor: int | None = None,
capture_area: dict[str, int] | None = None,
capture_area: Region | None = None,
) -> Generator[Image.Image]:
"""Continuously capture images from the specified monitor.

Either monitor or capture_area must be used, but not both.

:param monitor: Monitor number to capture from, using the standard
MSS convention (all screens=0, first screen=1, etc.).
:param capture_area: Capture rectangle dict with 'left', 'top',
'width', 'height'.
:param capture_area: Capture region.
:yields: PIL Image objects from the captured monitor.
"""
with mss.MSS() as sct:
rect = capture_area if capture_area is not None else sct.monitors[monitor]
LOGGER.debug("Capture area: %i,%i, %ix%i", rect["left"], rect["top"], rect["width"], rect["height"])
LOGGER.debug("Capture area: %i,%i, %ix%i", rect.left, rect.top, rect.width, rect.height)

while True:
sct_img = sct.grab(rect)
Expand Down Expand Up @@ -510,14 +510,14 @@ def _quality_type(value: str) -> int:
raise argparse.ArgumentTypeError(msg)


def _capture_area_type(value: str) -> dict[str, int]:
"""Validate and return a capture area dict.
def _capture_area_type(value: str) -> Region:
"""Validate and return a capture region.

Expected format is ``left,top,width,height`` where all values are
integers.

:param value: The capture area string to validate.
:returns: Dict with 'left', 'top', 'width', 'height' keys.
:returns: Capture region.
:raises argparse.ArgumentTypeError: If the format is invalid or extents
are non-positive.
"""
Expand All @@ -536,7 +536,7 @@ def _capture_area_type(value: str) -> dict[str, int]:
msg = "Capture area width and height must be positive"
raise argparse.ArgumentTypeError(msg)

return {"left": left, "top": top, "width": width, "height": height}
return Region(left=left, top=top, width=width, height=height)


def main() -> None:
Expand Down
17 changes: 11 additions & 6 deletions demos/video-capture-simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from PIL import Image

import mss
from mss.models import Region

# These are the options you'd give to ffmpeg that would affect the way the video is encoded. There are comments in
# the full demo that go into more detail.
Expand Down Expand Up @@ -72,17 +73,21 @@ def main() -> None:
monitor = sct.monitors[1]

# Because of how H.264 video stores color information, libx264 requires the video size to be a multiple of
# two.
monitor["width"] = (monitor["width"] // 2) * 2
monitor["height"] = (monitor["height"] // 2) * 2
# two. Keep the monitor description unchanged and capture a slightly smaller region when necessary.
capture_region = Region(
left=monitor.left,
top=monitor.top,
width=(monitor.width // 2) * 2,
height=(monitor.height // 2) * 2,
)

with av.open(FILENAME, "w") as avmux:
# The "avmux" object we get back from "av.open" represents the MP4 file. That's a container that holds
# the video, as well as possibly audio and more. These are each called "streams". We only create one
# stream here, since we're just recording video.
video_stream = avmux.add_stream(CODEC, rate=FPS, options=CODEC_OPTIONS)
video_stream.width = monitor["width"]
video_stream.height = monitor["height"]
video_stream.width = capture_region.width
video_stream.height = capture_region.height
# There are more options you can set on the video stream; the full demo uses some of those.

# Count how many frames we're capturing, so we can log the FPS later.
Expand Down Expand Up @@ -121,7 +126,7 @@ def main() -> None:
print(".", end="", flush=True)

# Grab a screenshot.
screenshot = sct.grab(monitor)
screenshot = sct.grab(capture_region)
frame_count += 1

# There are a few ways to get the screenshot into a VideoFrame. The highest-performance way isn't
Expand Down
27 changes: 12 additions & 15 deletions demos/video-capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,14 @@
from typing import Any

# Install the necessary libraries with "pip install av mss numpy si-prefix".
# Note that numpy is needed even if we don't explicitly import it here.
import av
import numpy as np
from si_prefix import si_format

import mss
from common.pipeline import Mailbox, PipelineStage
from mss.models import Region

from common.pipeline import Mailbox, PipelineStage

# These are the options you'd give to ffmpeg that it sends to the video codec. Because ffmpeg and PyAV both use the
# libav libraries, you can get the list of available flags with `ffmpeg -help encoder=libx264`, or whatever encoder
Expand Down Expand Up @@ -165,7 +166,7 @@
def video_capture(
fps: int,
sct: mss.MSS,
monitor: mss.models.Monitor,
capture_region: Region,
shutdown_requested: Event,
) -> Generator[tuple[mss.screenshot.ScreenShot, float], None, None]:
# Keep track of the time when we want to get the next frame. We limit the frame time this way instead of sleeping
Expand All @@ -183,7 +184,7 @@ def video_capture(
time.sleep(next_frame_at - now)

# Capture a frame, and send it to the next processing stage.
screenshot = sct.grab(monitor)
screenshot = sct.grab(capture_region)
yield screenshot, now

# We try to keep the capture rate at the desired fps on average. If we can't quite keep up for a moment (such
Expand Down Expand Up @@ -436,14 +437,10 @@ def main() -> None:
with mss.MSS() as sct:
if args.region:
left, top, right, bottom = args.region
monitor = {
"left": left,
"top": top,
"width": right - left,
"height": bottom - top,
}
capture_region = Region(left=left, top=top, width=right - left, height=bottom - top)
else:
monitor = sct.monitors[args.monitor]
capture_region = monitor.as_region()

# Some codecs, such as libx264, require the region to be a multiple of 2, to get the chroma subsampling right.
# Others, such as h264_nvenc, do not; they'll pad to get the subsampling region, and add flags to the stream
Expand All @@ -453,8 +450,8 @@ def main() -> None:
# it (at least, when using 4:2:0 subsampling).
region_crop_to_multiple_of_two = codec in {"libx264", "libx265"}
if region_crop_to_multiple_of_two:
monitor["width"] = (monitor["width"] // 2) * 2
monitor["height"] = (monitor["height"] // 2) * 2
capture_region.width = (capture_region.width // 2) * 2
capture_region.height = (capture_region.height // 2) * 2

# We don't pass the container format to av.open here, so it will choose it based on the extension: .mp4, .mkv,
# etc.
Expand Down Expand Up @@ -493,8 +490,8 @@ def main() -> None:
# so some video encoders will tag it as AVCOL_TRC_BT709 (1) instead.
video_stream.color_trc = 13

video_stream.width = monitor["width"]
video_stream.height = monitor["height"]
video_stream.width = capture_region.width
video_stream.height = capture_region.height
# There are multiple time bases in play (stream, codec context, per-frame). Depending on the container
# and codec, some of these might be ignored or overridden. We set the desired time base consistently
# everywhere, so that the saved timestamps are correct regardless of what format we're saving to.
Expand Down Expand Up @@ -535,7 +532,7 @@ def main() -> None:
video_capture,
fps,
sct,
monitor,
capture_region,
shutdown_requested,
),
out_mailbox=mailbox_screenshot,
Expand Down
6 changes: 3 additions & 3 deletions docs/source/examples/custom_cls_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any

import mss
from mss.models import Monitor
from mss.models import Region
from mss.screenshot import ScreenShot


Expand All @@ -17,9 +17,9 @@ class SimpleScreenShot(ScreenShot):
or add new methods.
"""

def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None:
def __init__(self, data: bytearray, region: Region, **_: Any) -> None:
self.data = data
self.monitor = monitor
self.region = region


with mss.MSS() as sct:
Expand Down
5 changes: 3 additions & 2 deletions docs/source/examples/fps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from PIL import ImageGrab

import mss
from mss.models import Region


def screen_record() -> int:
Expand All @@ -36,15 +37,15 @@ def screen_record() -> int:

def screen_record_efficient() -> int:
# 800x600 windowed mode
mon = {"top": 40, "left": 0, "width": 800, "height": 640}
region = Region(left=0, top=40, width=800, height=640)

title = "[MSS] FPS benchmark"
fps = 0
sct = mss.MSS()
last_time = time.time()

while time.time() - last_time < 1:
img = np.asarray(sct.grab(mon))
img = np.asarray(sct.grab(region))
fps += 1

cv2.imshow(title, img)
Expand Down
5 changes: 3 additions & 2 deletions docs/source/examples/fps_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@

import mss
import mss.tools
from mss.models import Region


def grab(queue: Queue) -> None:
rect = {"top": 0, "left": 0, "width": 600, "height": 800}
region = Region(left=0, top=0, width=600, height=800)

with mss.MSS() as sct:
for _ in range(1_000):
queue.put(sct.grab(rect))
queue.put(sct.grab(region))

# Tell the other worker to stop
queue.put(None)
Expand Down
4 changes: 2 additions & 2 deletions docs/source/examples/from_pil_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
monitor = sct.monitors[1]

# Capture a bbox using percent values
left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left
top = monitor["top"] + monitor["height"] * 5 // 100 # 5% from the top
left = monitor.left + monitor.width * 5 // 100 # 5% from the left
top = monitor.top + monitor.height * 5 // 100 # 5% from the top
right = left + 400 # 400px width
lower = top + 400 # 400px height
bbox = (left, top, right, lower)
Expand Down
5 changes: 3 additions & 2 deletions docs/source/examples/opencv_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@
import cv2

import mss
from mss.models import Region

with mss.MSS() as sct:
# Part of the screen to capture
monitor = {"top": 40, "left": 0, "width": 800, "height": 640}
region = Region(left=0, top=40, width=800, height=640)

while "Screen capturing":
last_time = time.time()

# Get raw pixels from the screen, save it to a NumPy array.
# Note that OpenCV expects colors in BGR order.
img = sct.grab(monitor).to_numpy(channels="BGR")
img = sct.grab(region).to_numpy(channels="BGR")

# Display the picture
cv2.imshow("OpenCV/NumPy normal", img)
Expand Down
7 changes: 4 additions & 3 deletions docs/source/examples/part_of_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

import mss
import mss.tools
from mss.models import Region

with mss.MSS() as sct:
# The screen part to capture
monitor = {"top": 160, "left": 160, "width": 160, "height": 135}
output = "sct-{top}x{left}_{width}x{height}.png".format(**monitor)
region = Region(left=160, top=160, width=160, height=135)
output = f"sct-{region.top}x{region.left}_{region.width}x{region.height}.png"

# Grab the data
sct_img = sct.grab(monitor)
sct_img = sct.grab(region)

# Save to the picture file
mss.tools.to_png(sct_img.rgb, sct_img.size, output=output)
Expand Down
20 changes: 10 additions & 10 deletions docs/source/examples/part_of_screen_monitor_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@

import mss
import mss.tools
from mss.models import Region

with mss.MSS() as sct:
# Get information of monitor 2
monitor_number = 2
mon = sct.monitors[monitor_number]
monitor = sct.monitors[monitor_number]

# The screen part to capture
monitor = {
"top": mon["top"] + 100, # 100px from the top
"left": mon["left"] + 100, # 100px from the left
"width": 160,
"height": 135,
"mon": monitor_number,
}
output = "sct-mon{mon}_{top}x{left}_{width}x{height}.png".format(**monitor)
region = Region(
left=monitor.left + 100, # 100px from the left
top=monitor.top + 100, # 100px from the top
width=160,
height=135,
)
output = f"sct-mon{monitor_number}_{region.top}x{region.left}_{region.width}x{region.height}.png"

# Grab the data
sct_img = sct.grab(monitor)
sct_img = sct.grab(region)

# Save to the picture file
mss.tools.to_png(sct_img.rgb, sct_img.size, output=output)
Expand Down
10 changes: 9 additions & 1 deletion docs/source/release-history/v10.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ In 11.0, monitor dictionaries will become a dedicated **`Monitor` class**.

To maintain compatibility:

- dictionary-style access will continue to work
- string-key access will temporarily continue to work

```python
monitor["left"]
Expand All @@ -269,6 +269,14 @@ monitor["top"]

- `grab()` will continue accepting dictionaries

The compatibility access does not make `Monitor` a complete mapping. Migrate dictionary methods, membership tests, and
unpacking to attribute access:

```python
monitor.left
monitor.top
```

If you use type annotations, you can switch to the provided `Monitor` type:

```python
Expand Down
Loading