Bug critic: _strips_match(tol=10) trip pe pulsații naturale de lățime ~18px între ticks (ex. 792↔810px). Fiecare trip → _commit_layout_change → reset FSM + alert Telegram + scheduler stop. Logul 2026-05-04.jsonl arăta 576 evenimente layout_change/zi, plus prime alerts repetate la dark_red/dark_green (FSM resetat înghite lockout-ul) și sincronizare cross-chart pe ambele FSM-uri simultan. Fix: - main.py:1511 — gate doar pe count change (len(new) != len(current)); count stabil → silent update sub_roi indiferent de jitter - main.py:1438 — silent=True pe alert layout_change (Telegram fără sunet) - 2 teste regresie noi: width oscillation 792↔810 + silent assertion - 2 teste async reparate: bootstrap _detect_strips_for_ctx pentru ScriptedDetector (regresie după ce _run_multi_tick a devenit unica cale de detecție) Plus refactor multi-chart pre-existent: layout.py modul nou, _detect_strips_for_ctx, ChartState per-chart FSM/Detector, ROI per-strip pe screenshots, scripts/diag_*. Verificat: 292 passed, 2 skipped în 10s. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
r"""Inspect ATM strip pixels without relying on shell pipelines.
|
|
|
|
Usage:
|
|
.\.venv\Scripts\python.exe scripts\inspect_image_pixels.py 6033117943853423831.jpg
|
|
.\.venv\Scripts\python.exe scripts\inspect_image_pixels.py image.jpg --point 1780 725
|
|
|
|
The script intentionally parses only the config fields needed for pixel inspection,
|
|
so it does not require Discord/Telegram secrets to be valid.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "src"
|
|
if str(SRC) not in sys.path:
|
|
sys.path.insert(0, str(SRC))
|
|
|
|
from atm.config import ROI # noqa: E402
|
|
from atm.vision import classify_pixel, crop_roi, find_top_dots, pixel_rgb # noqa: E402
|
|
|
|
|
|
def _load_probe_config(path: Path) -> dict:
|
|
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
colors = {
|
|
name: (tuple(int(c) for c in spec["rgb"]), float(spec["tolerance"]))
|
|
for name, spec in data["colors"].items()
|
|
}
|
|
background = colors.pop("background", ((18, 18, 18), 15.0))
|
|
return {
|
|
"dot_roi": ROI(**data["dot_roi"]),
|
|
"colors": colors,
|
|
"background_rgb": background[0],
|
|
"background_tol": background[1],
|
|
}
|
|
|
|
|
|
def _as_jsonable_match(match) -> dict:
|
|
return {
|
|
"name": match.name,
|
|
"distance": round(float(match.distance), 3),
|
|
"confidence": round(float(match.confidence), 3),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Inspect ATM strip pixels in a JPG/PNG frame.")
|
|
parser.add_argument("image", type=Path, help="Frame image path.")
|
|
parser.add_argument(
|
|
"--config",
|
|
type=Path,
|
|
default=ROOT / "configs" / "2026-04-21-recalib.toml",
|
|
help="ATM TOML config path.",
|
|
)
|
|
parser.add_argument("--top", type=int, default=3, help="Number of rightmost dots to report.")
|
|
parser.add_argument(
|
|
"--point",
|
|
nargs=2,
|
|
type=int,
|
|
metavar=("X", "Y"),
|
|
help="Optional absolute pixel coordinate to sample.",
|
|
)
|
|
parser.add_argument("--box", type=int, default=3, help="Sampling radius for mean RGB.")
|
|
args = parser.parse_args()
|
|
|
|
frame = cv2.imread(str(args.image), cv2.IMREAD_COLOR)
|
|
if frame is None:
|
|
raise SystemExit(f"Could not read image: {args.image}")
|
|
|
|
probe = _load_probe_config(args.config)
|
|
roi = probe["dot_roi"]
|
|
roi_img = crop_roi(frame, roi)
|
|
dots = find_top_dots(
|
|
roi_img,
|
|
bg_rgb=probe["background_rgb"],
|
|
bg_tol=probe["background_tol"],
|
|
n=args.top,
|
|
)
|
|
|
|
result = {
|
|
"image": str(args.image),
|
|
"image_size": {"w": int(frame.shape[1]), "h": int(frame.shape[0])},
|
|
"config": str(args.config),
|
|
"dot_roi": {"x": roi.x, "y": roi.y, "w": roi.w, "h": roi.h},
|
|
"dots": [],
|
|
}
|
|
|
|
for x, y in dots:
|
|
rgb = pixel_rgb(roi_img, x, y, box=args.box)
|
|
match = classify_pixel(rgb, probe["colors"])
|
|
result["dots"].append(
|
|
{
|
|
"roi_xy": [int(x), int(y)],
|
|
"abs_xy": [int(roi.x + x), int(roi.y + y)],
|
|
"rgb": list(rgb),
|
|
"match": _as_jsonable_match(match),
|
|
}
|
|
)
|
|
|
|
if args.point:
|
|
px, py = args.point
|
|
if not (roi.x <= px < roi.x + roi.w and roi.y <= py < roi.y + roi.h):
|
|
raise SystemExit(f"Point {px},{py} is outside dot_roi")
|
|
rx, ry = px - roi.x, py - roi.y
|
|
rgb = pixel_rgb(roi_img, rx, ry, box=args.box)
|
|
result["point"] = {
|
|
"roi_xy": [rx, ry],
|
|
"abs_xy": [px, py],
|
|
"rgb": list(rgb),
|
|
"match": _as_jsonable_match(classify_pixel(rgb, probe["colors"])),
|
|
}
|
|
|
|
print(json.dumps(result, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|