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>
134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
r"""Diagnose why detect_strips finds 1 strip when there are 2 TS windows.
|
|
|
|
Reuses the most recent raw capture under logs/repro/. For each strip detected,
|
|
prints the connected-components vivid mask, gaps, and the contiguous run lengths
|
|
so we can see exactly where the threshold is killing the second strip.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SRC = ROOT / "src"
|
|
if str(SRC) not in sys.path:
|
|
sys.path.insert(0, str(SRC))
|
|
|
|
import cv2 # noqa: E402
|
|
import numpy as np # noqa: E402
|
|
|
|
from atm.config import Config # noqa: E402
|
|
from atm.layout import VIVID_COLORS, detect_strips # noqa: E402
|
|
from atm.vision import crop_roi, find_top_dots, classify_pixel, pixel_rgb # noqa: E402
|
|
|
|
|
|
def _latest_raw() -> Path:
|
|
candidates = sorted((ROOT / "logs" / "repro").glob("*_raw.png"))
|
|
if not candidates:
|
|
raise SystemExit("No *_raw.png in logs/repro — run scripts/repro_ss_resume.py first.")
|
|
return candidates[-1]
|
|
|
|
|
|
def main() -> int:
|
|
cfg = Config.load_current(ROOT / "configs")
|
|
raw_path = _latest_raw()
|
|
print(f"Using raw frame: {raw_path}")
|
|
frame = cv2.imread(str(raw_path), cv2.IMREAD_COLOR)
|
|
if frame is None:
|
|
raise SystemExit(f"Could not read {raw_path}")
|
|
|
|
palette = {
|
|
name: (spec.rgb, spec.tolerance)
|
|
for name, spec in cfg.colors.items()
|
|
if name != "background"
|
|
}
|
|
full_crop = crop_roi(frame, cfg.dot_roi)
|
|
h, w = full_crop.shape[:2]
|
|
print(f"dot_roi crop shape: {h}x{w} (cfg.dot_roi={cfg.dot_roi})")
|
|
|
|
# 1) Build the vivid mask used by detect_strips
|
|
mask = np.zeros((h, w), dtype=np.uint8)
|
|
img_f = full_crop.astype(np.float32)
|
|
per_color_pixels = {}
|
|
for name in VIVID_COLORS:
|
|
if name not in palette:
|
|
continue
|
|
rgb, tol = palette[name]
|
|
bgr = np.array([rgb[2], rgb[1], rgb[0]], dtype=np.float32)
|
|
diff = np.linalg.norm(img_f - bgr, axis=2)
|
|
m = (diff < tol).astype(np.uint8)
|
|
per_color_pixels[name] = int(m.sum())
|
|
mask |= m
|
|
|
|
print("\nVIVID_COLORS pixel counts in dot_roi crop (pre-morphology):")
|
|
for n, c in per_color_pixels.items():
|
|
print(f" {n:12s} {c:>7d} px")
|
|
print(f" total mask: {int(mask.sum()):>7d} px")
|
|
|
|
# 2) Column projection: which x columns have any vivid pixel?
|
|
col_has = mask.any(axis=0).astype(np.uint8)
|
|
runs = []
|
|
i = 0
|
|
while i < w:
|
|
if col_has[i] == 0:
|
|
i += 1
|
|
continue
|
|
j = i
|
|
while j < w and col_has[j] == 1:
|
|
j += 1
|
|
runs.append((i, j - 1, j - i))
|
|
i = j
|
|
print(f"\nContiguous vivid-column runs (raw, no morphology): {len(runs)}")
|
|
for x0, x1, run_w in runs[:25]:
|
|
print(f" x={x0:>4d}..{x1:>4d} width={run_w}")
|
|
if len(runs) > 25:
|
|
print(f" ... +{len(runs) - 25} more")
|
|
|
|
# 3) Apply same morphology + connected components as detect_strips
|
|
strip_h = cfg.dot_roi.h
|
|
min_strip_px = max(150, strip_h * 8)
|
|
min_gap_px = max(20, int(strip_h * 0.8))
|
|
kw = max(3, min_gap_px // 2)
|
|
print(f"\ndetect_strips params: min_strip_px={min_strip_px} min_gap_px={min_gap_px} morphology kw={kw}")
|
|
|
|
kernel = np.ones((1, kw), dtype=np.uint8)
|
|
closed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
|
n_labels, _labels, stats, _centroids = cv2.connectedComponentsWithStats(closed, connectivity=8)
|
|
print(f"\nConnected components after CLOSE (kw={kw}): n={n_labels - 1} (excluding bg)")
|
|
for i in range(1, n_labels):
|
|
x = int(stats[i, cv2.CC_STAT_LEFT])
|
|
y = int(stats[i, cv2.CC_STAT_TOP])
|
|
ww = int(stats[i, cv2.CC_STAT_WIDTH])
|
|
hh = int(stats[i, cv2.CC_STAT_HEIGHT])
|
|
passes = "PASS" if ww >= min_strip_px else "drop"
|
|
print(f" [{i:>2d}] x={x:>4d} y={y:>3d} w={ww:>4d} h={hh:>3d} -> {passes}")
|
|
|
|
strips = detect_strips(full_crop, palette, min_gap_px, min_strip_px)
|
|
print(f"\ndetect_strips() final result: {len(strips)} strip(s)")
|
|
for i, s in enumerate(strips):
|
|
print(f" strip[{i}] x={s.x} y={s.y} w={s.w} h={s.h}")
|
|
|
|
# 4) Run find_top_dots on the FULL dot_roi (single-chart fallback path used
|
|
# by /ss when ctx.charts < 2). This is what users see when the layout
|
|
# detector hasn't promoted the layout to multi-chart yet.
|
|
bg_rgb = cfg.colors["background"].rgb if "background" in cfg.colors else (18, 18, 18)
|
|
bg_tol = cfg.colors["background"].tolerance if "background" in cfg.colors else 15.0
|
|
full_dots = find_top_dots(full_crop, bg_rgb, bg_tol, n=3)
|
|
print(f"\nfind_top_dots on FULL dot_roi (n=3, single-chart fallback path):")
|
|
for i, (cx, cy) in enumerate(full_dots):
|
|
rgb = pixel_rgb(full_crop, cx, cy)
|
|
m = classify_pixel(rgb, palette)
|
|
print(f" c{i + 1}: pos=({cx + cfg.dot_roi.x},{cy + cfg.dot_roi.y}) rgb={rgb} -> {m.name} d={m.distance:.1f}")
|
|
|
|
# 5) Save the binary mask + closed mask for visual inspection
|
|
out_dir = ROOT / "logs" / "repro"
|
|
cv2.imwrite(str(out_dir / "diag_mask_raw.png"), (mask * 255).astype(np.uint8))
|
|
cv2.imwrite(str(out_dir / "diag_mask_closed.png"), (closed * 255).astype(np.uint8))
|
|
print(f"\nWrote diag_mask_raw.png + diag_mask_closed.png to {out_dir}")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|