Files
hello_js_reverse_skill/scripts/verify/judge_icons.py
T
yml2213 74a863c8b3 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。
2026-10-12 14:32:14 +08:00

96 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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))