Files
ledgrab/server/tests/test_capture_roi.py
T
alexei.dolgolyov ca59546711 feat(capture): region-of-interest (ROI) crop for screen sampling
Sample only a sub-rectangle of the captured frame instead of the whole display,
so a taskbar, game HUD, or letterbox bars don't pollute the border colours — the
first functional gap a reviewer hits (capture was full-display only).

- New pure crop_screen_capture() returns a numpy view (no copy), fast-paths the
  full-frame case, and clamps degenerate/out-of-range ROIs to >=1px.
- ROI lives on CalibrationConfig (simple mode) as fractions 0..1 with a has_roi
  helper; applied in the picture color-strip stream just before border
  extraction, clamping border_width to the cropped size. Additive + backward
  compatible (full-frame default, omitted from serialization when unset -> no
  migration).
- Round-trips through the calibration schema automatically; frontend adds an
  X/Y/Width/Height (%) 'Capture region' group to the calibration editor with
  i18n (en/ru/zh).

10 unit tests (crop geometry, view-not-copy, clamping, ROI round-trip, legacy
default); full suite green (1946 passed).
2026-06-05 11:58:26 +03:00

87 lines
2.7 KiB
Python

"""Tests for capture region-of-interest (ROI) cropping."""
import numpy as np
from ledgrab.core.capture.calibration import (
CalibrationConfig,
calibration_from_dict,
calibration_to_dict,
)
from ledgrab.core.capture.screen_capture import ScreenCapture, crop_screen_capture
def _cfg(**kw) -> CalibrationConfig:
return CalibrationConfig(layout="clockwise", start_position="bottom_left", leds_top=10, **kw)
def _sc(w: int = 100, h: int = 80) -> ScreenCapture:
return ScreenCapture(
image=np.zeros((h, w, 3), dtype=np.uint8), width=w, height=h, display_index=0
)
def test_full_frame_returns_same_object():
sc = _sc()
assert crop_screen_capture(sc, 0.0, 0.0, 1.0, 1.0) is sc
def test_center_crop_dimensions():
out = crop_screen_capture(_sc(100, 80), 0.25, 0.25, 0.5, 0.5)
assert out.width == 50 and out.height == 40
assert out.image.shape[:2] == (40, 50)
assert out.display_index == 0
def test_crop_returns_a_view_of_the_source():
sc = _sc(100, 80)
out = crop_screen_capture(sc, 0.0, 0.0, 0.5, 0.5)
out.image[0, 0] = (9, 9, 9)
assert (sc.image[0, 0] == 9).all() # mutating the view touches the source pixels
def test_partial_width_only_keeps_full_height():
out = crop_screen_capture(_sc(100, 80), 0.1, 0.0, 0.8, 1.0)
assert out.width == 80 and out.height == 80
def test_degenerate_roi_clamped_to_at_least_one_pixel():
out = crop_screen_capture(_sc(100, 80), 0.999, 0.999, 0.0, 0.0)
assert out.width >= 1 and out.height >= 1
def test_out_of_range_roi_is_clamped():
out = crop_screen_capture(_sc(100, 80), -0.5, -0.5, 2.0, 2.0)
# x<=0,y<=0,w>=1,h>=1 hits the full-frame fast path
assert out.width == 100 and out.height == 80
# --- CalibrationConfig ROI serialization ---
def test_has_roi_property():
assert _cfg().has_roi is False
assert _cfg(roi_width=0.5).has_roi is True
assert _cfg(roi_x=0.1).has_roi is True
assert _cfg(roi_height=0.9).has_roi is True
def test_roi_round_trips_through_dict():
cfg = _cfg(roi_x=0.1, roi_y=0.2, roi_width=0.6, roi_height=0.7)
d = calibration_to_dict(cfg)
assert d["roi_x"] == 0.1 and d["roi_width"] == 0.6
back = calibration_from_dict(d)
assert (back.roi_x, back.roi_y, back.roi_width, back.roi_height) == (0.1, 0.2, 0.6, 0.7)
def test_full_frame_roi_omitted_from_dict():
d = calibration_to_dict(_cfg())
assert "roi_x" not in d and "roi_width" not in d
def test_legacy_dict_without_roi_defaults_to_full_frame():
cfg = calibration_from_dict(
{"mode": "simple", "layout": "clockwise", "start_position": "bottom_left", "leds_top": 10}
)
assert cfg.has_roi is False
assert (cfg.roi_x, cfg.roi_y, cfg.roi_width, cfg.roi_height) == (0.0, 0.0, 1.0, 1.0)