feat(icon): 轨迹时序指纹修复 —— 1ms 图标 4/8→7/8=0,errorCode 语义与轨迹对拍收口
- 真因: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。
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# -*- 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))
|
||||
Reference in New Issue
Block a user