# -*- coding: utf-8 -*- """ 图标/图文点选求解器 回归测试 (AGENTS.md §5.9) 固化的性质: 1. 提示条(目标原图) -> 背景图 的**形状模板匹配**在真机样本上给出正确、有序的点击点; 2. 能处理**旋转**目标 (样本里"图表"图标被倾斜 ~-30°); 3. 能排除干扰图标 (样本里的笑脸/银行). 样本: tcaptcha/tests/fixtures/1ms_icon_click_1_{prompt,bg}.jpg 取自 1ms.run (aid=195672344) 真机页面上弹出的验证码 (2026-10-11), 题面 = "0" + 图表图标 + "7", 期望点击顺序与坐标见 EXPECTED。 运行: .venv/bin/python -m tcaptcha.tests.test_icon_click_solver """ from __future__ import annotations import sys from pathlib import Path import cv2 ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from tcaptcha.icon_click_solver import IconClickSolver # noqa: E402 FIX = ROOT / "tcaptcha" / "tests" / "fixtures" # 期望点击顺序: ("0" 右下) -> (倾斜的图表图标 左上) -> ("7" 中上),坐标是 672x480 原图口径 EXPECTED = [(520, 440), (200, 40), (360, 120)] TOL = 12 # 允许的像素误差 (连通域中心 vs 目视中心) _fails: list = [] def check(name: str, cond: bool, detail: str = "") -> None: print(f" [{'PASS' if cond else 'FAIL'}] {name}" + (f" {detail}" if detail else "")) if not cond: _fails.append(name) def main() -> int: pr = cv2.imread(str(FIX / "1ms_icon_click_1_prompt.jpg")) bg = cv2.imread(str(FIX / "1ms_icon_click_1_bg.jpg")) check("fixture 可读", pr is not None and bg is not None) print("1) 求解") res = IconClickSolver().solve(bg, pr) check("目标数=3", res.target_count == 3, str(res.target_count)) check("求解成功", res.ok, res.describe()) print("2) 点击顺序与坐标") for i, (exp, got) in enumerate(zip(EXPECTED, res.points)): dx, dy = abs(exp[0] - got[0]), abs(exp[1] - got[1]) check(f"第{i+1}个点 ~{exp}", dx <= TOL and dy <= TOL, f"got={got} (Δ={dx},{dy}) score={res.scores[i]:.3f}") print("3) 旋转与干扰项") check("命中倾斜图标 (角度非 0)", any(abs(a) >= 10 for a in res.angles), str(res.angles)) check("所有匹配分数 > 0.45", res.min_score > 0.45, f"min={res.min_score:.3f}") print() if _fails: print(f"❌ test_icon_click_solver 失败 {len(_fails)} 项: {_fails}") return 1 print("✅ test_icon_click_solver 全部通过") return 0 if __name__ == "__main__": raise SystemExit(main())