ca59546711
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).
87 lines
2.7 KiB
Python
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)
|