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())