- 真因:dt 分布与真机严重不符(旧 50% 落 0~16ms,真机 79% 落 13~20ms)+ 点数 随机 57~65(真机 6/6 恒 61)。按 6 条真机轨迹重拟合 _sample_dt,新增 _fit_points 钉死 61 点(补齐用 [0,0,dt] 驻留点,不能复制位移点——净位移会被放大)。 - A/B:1ms 图标 0×4→0×7(前 7 连 0);残余偶发 12 为连发频控形态,低频使用即可。 - errorCode 语义标定:50=答案被否(容差实测 25~40px,我们点偏仅 2~3px), 12=包级拒答;判点位只看 50/0。 - 顺带修 build_plain:轨迹落末槽(k=37)时 chunk2 恒补前导逗号 → 非法 JSON。 - 新增 scripts/verify/send_campaign.py(批量真发+落图+统计)与 judge_icons.py (提示目标 vs 点击点并排判读);账本 #64~#74 + 专文 ICON2408.md。 复验:selftest 全绿;判读图 24/24 点位全对;mliev 图标 9/10=0。
96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""judge_icons.py —— 图标点选题的**目视判读图**生成器。
|
||
|
||
把 `send_campaign.py` 的一次实验目录变成一张对照图:每行一次跑,列出
|
||
[提示图第 j 个目标] vs [第 j 个点击点 ±32px 的背景 patch] (j = 1,2,3)
|
||
这样"点得对不对 / 顺序对不对"一眼可判 —— 顺序即红框上的 #1/#2/#3。
|
||
|
||
用法:
|
||
.venv/bin/python scripts/verify/judge_icons.py /tmp/icon_campaign/<tag> [out.png]
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from PIL import Image, ImageDraw
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
from tcaptcha.ocr.slicer import slice_prompt_image # noqa: E402
|
||
|
||
CELL = 120
|
||
PAD = 8
|
||
ROWH = CELL + 26
|
||
|
||
|
||
def build(campaign_dir: str, out_path: str) -> str:
|
||
d = Path(campaign_dir)
|
||
runs = sorted([p.name for p in d.iterdir() if re.fullmatch(r"run\d+", p.name)],
|
||
key=lambda s: int(s[3:]))
|
||
if not runs:
|
||
raise SystemExit("目录下没有 run* 子目录: %s" % d)
|
||
width = PAD + 60 + 3 * (CELL + PAD + CELL) + PAD + 150
|
||
height = PAD + len(runs) * (ROWH + PAD)
|
||
canvas = Image.new("RGB", (width, height), (245, 245, 245))
|
||
dr = ImageDraw.Draw(canvas)
|
||
|
||
y = PAD
|
||
for rn in runs:
|
||
rd = d / rn
|
||
info = json.loads((rd / "points.json").read_text(encoding="utf-8"))
|
||
ec = info.get("errorCode")
|
||
pts = info.get("points") or []
|
||
offs = info.get("centroid_offsets_px") or []
|
||
dr.text((PAD, y + 4), "%s\nec=%s" % (rn, ec), fill=(20, 20, 20))
|
||
try:
|
||
prompt = Image.open(rd / "prompt.jpg").convert("RGB")
|
||
crops = slice_prompt_image(prompt)
|
||
except Exception: # noqa: BLE001
|
||
crops = []
|
||
try:
|
||
bg = Image.open(rd / "bg.jpg").convert("RGB")
|
||
except Exception: # noqa: BLE001
|
||
bg = None
|
||
x = PAD + 60
|
||
for j in range(3):
|
||
if j < len(crops):
|
||
pc = crops[j][0].convert("RGB").resize((CELL, CELL), Image.LANCZOS)
|
||
else:
|
||
pc = Image.new("RGB", (CELL, CELL), (255, 210, 210))
|
||
canvas.paste(pc, (x, y + 20))
|
||
dr.rectangle([x, y + 20, x + CELL, y + 20 + CELL], outline=(0, 0, 0))
|
||
x2 = x + CELL + PAD
|
||
if bg is not None and j < len(pts):
|
||
px, py = pts[j]
|
||
box = (max(0, px - 32), max(0, py - 32),
|
||
min(bg.width, px + 32) + 1, min(bg.height, py + 32) + 1)
|
||
patch = bg.crop(box).resize((CELL, CELL), Image.LANCZOS)
|
||
tag = "#%d(%d,%d)" % (j + 1, px, py)
|
||
if j < len(offs) and offs[j] is not None:
|
||
tag += " 偏%.0fpx" % offs[j]
|
||
dr.text((x2 + 2, y + 4), tag, fill=(0, 0, 0))
|
||
else:
|
||
patch = Image.new("RGB", (CELL, CELL), (232, 232, 232))
|
||
canvas.paste(patch, (x2, y + 20))
|
||
dr.rectangle([x2, y + 20, x2 + CELL, y + 20 + CELL], outline=(200, 0, 0), width=3)
|
||
x = x2 + CELL + PAD
|
||
y += ROWH + PAD
|
||
|
||
canvas.save(out_path)
|
||
return out_path
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) < 2:
|
||
raise SystemExit(__doc__)
|
||
src = sys.argv[1]
|
||
dst = sys.argv[2] if len(sys.argv) > 2 else os.path.join(src, "judge.png")
|
||
print("saved", build(src, dst))
|