Files
atm/tests/test_vision.py
Marius Mutu 45ed502b3d feat(telegram): /ss + /resume cu verify multi-bulină și header FSM step
/ss și /resume afișează acum markerii top-3 buline sub ROI (cercuri pline,
r=7, culoarea clasificată) cu tick vertical roșu pe pick-ul FSM (rightmost).
Caption compact: `N/3 STATE` header + `emoji c1/c2/c3: name ← pick`.
FIRE_{BUY|SELL} afișat ca 3/3 când fire_ts e în ultimele 30s.

/resume face capture ÎNAINTE de clearing state → zero race cu FSM tick
simultan. Capture fail → title marchează "⚠️ captură eșuată", resume-ul
rulează oricum.

config: <version> mutat din caption în /status (acolo are sens pentru
verificare de calibrare, nu la fiecare /ss).

Adaugă find_top_dots în vision.py (top-N variantă a find_rightmost_dot,
tie-break determinist pe y). 5 teste sintetice noi + 4 teste noi pentru
dispatcher resume (screenshot inline, capture-fail, order-of-ops,
parity /ss <-> fire path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:38:29 +03:00

74 lines
2.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Unit tests for vision primitives (synthetic BGR masks, fast, deterministic)."""
from __future__ import annotations
import cv2
import numpy as np
from atm.vision import find_top_dots
BG_RGB = (18, 18, 18) # background in RGB
def _make_frame(h: int = 30, w: int = 100) -> np.ndarray:
"""Blank BGR frame filled with BG_RGB."""
bgr_bg = (BG_RGB[2], BG_RGB[1], BG_RGB[0])
frame = np.zeros((h, w, 3), dtype=np.uint8)
frame[:, :] = bgr_bg
return frame
def _paint_dot(frame: np.ndarray, cx: int, cy: int, radius: int = 5,
bgr: tuple[int, int, int] = (0, 255, 0)) -> None:
# radius ≥ 5 keeps blob above min_cluster_px after 2× erosion by 3x3 kernel.
cv2.circle(frame, (cx, cy), radius, bgr, -1)
def test_find_top_dots_happy_three_blobs_sorted_desc():
frame = _make_frame()
_paint_dot(frame, 10, 15)
_paint_dot(frame, 30, 15)
_paint_dot(frame, 50, 15)
result = find_top_dots(frame, BG_RGB, n=3)
assert len(result) == 3
# Sorted by right edge descending → x=50 first, then 30, then 10.
xs = [pt[0] for pt in result]
assert xs[0] > xs[1] > xs[2]
assert xs[0] >= 48 and xs[2] <= 12 # allow ±2px wobble from centroid
def test_find_top_dots_zero_blobs_returns_empty():
frame = _make_frame()
assert find_top_dots(frame, BG_RGB, n=3) == []
def test_find_top_dots_one_blob_n3_returns_one():
frame = _make_frame()
_paint_dot(frame, 25, 15)
result = find_top_dots(frame, BG_RGB, n=3)
assert len(result) == 1
cx, _cy = result[0]
assert 23 <= cx <= 27
def test_find_top_dots_fused_wide_blob_anchors_to_right_edge():
frame = _make_frame()
# Paint a wide stripe (width > 12) — simulates fused anti-aliased dots.
cv2.rectangle(frame, (20, 13), (60, 17), (0, 255, 0), -1)
result = find_top_dots(frame, BG_RGB, n=1)
assert len(result) == 1
cx, _cy = result[0]
# Anchor should be near right edge (~58 = 60-2), not centroid (~40).
assert cx >= 55
def test_find_top_dots_tie_break_by_y_ascending():
frame = _make_frame(h=40)
# Two dots at same right-edge x=50, different y.
_paint_dot(frame, 50, 10) # upper — should come first
_paint_dot(frame, 50, 30) # lower
result = find_top_dots(frame, BG_RGB, n=2)
assert len(result) == 2
# Tie-break: smaller y first.
assert result[0][1] < result[1][1]