File size: 4,927 Bytes
20d7fde
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b69166
 
 
 
20d7fde
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b69166
 
 
20d7fde
1b69166
20d7fde
 
 
1b69166
20d7fde
 
 
 
 
 
 
 
 
 
1b69166
 
 
 
 
 
 
20d7fde
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
"""Read an entire Togyzkumalak scoresheet photo into game records (ONNX).

    python read_game.py "data/2026-07-06 00.00.20.jpg" --out out/sheet1 --result 0-1

Outputs in --out:
    game.json      per ply: bbox, probabilities for all 163 classes, top-k,
                   raw argmax, legal move set, legality flag
    raw.pgn        pure classifier argmax for every ply (even if illegal)
    legal.pgn      replayed under the rules; STOPS at the first illegal argmax,
                   the first empty cell, or when the game is over
    beam.pgn       best fully-legal reconstruction (beam search + kazan/result
                   evidence)
    annotated.jpg  sheet with cell boxes and the beam reconstruction labels
    cells/         every scanned cell crop, e.g. 07_W.png

Inference runs on the exported ONNX models (torch-free). Regenerate them with
`python scripts/export_onnx.py` after training. PGN move annotations: '+' =
capture, 'x' = tuzdyk creation; strip '+' to feed the moves to the 9Q engine.
"""

import argparse
import json
from pathlib import Path

from togyz.pipeline import RESULT_CODES, load_classifier, run_pipeline


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("image", help="scoresheet photo")
    parser.add_argument("--onnx", default="checkpoints/best.onnx",
                        help="move classifier ONNX (with a .classes.json sidecar)")
    parser.add_argument("--diagram-onnx", default="checkpoints/diagram/best.onnx",
                        help="board-diagram ONNX (kazan boxes + pit cells); "
                             "checkpoint matching is skipped if the file "
                             "does not exist")
    parser.add_argument("--out", default=None, help="output dir (default: out/<image stem>)")
    parser.add_argument("--topk", type=int, default=5)
    parser.add_argument("--beam-width", type=int, default=1024,
                        help="hypotheses kept during beam decoding")
    parser.add_argument("--beam-top", type=int, default=9,
                        help="legal continuations considered per ply (9 = all)")
    parser.add_argument("--result", choices=sorted(RESULT_CODES),
                        help="known game result from the sheet footer "
                             "(1-0 = Bast./White won); re-ranks the beam pool")
    parser.add_argument("--no-tta", action="store_true",
                        help="disable test-time augmentation (7 shifted views/cell)")
    parser.add_argument("--temperature", type=float, default=1.0,
                        help="softmax temperature; >1 softens overconfident cells")
    args = parser.parse_args()

    out_dir = Path(args.out or Path("out") / Path(args.image).stem)
    out_dir.mkdir(parents=True, exist_ok=True)

    moves_clf = load_classifier(args.onnx)
    diagram_clf = None
    if Path(args.diagram_onnx).exists():
        diagram_clf = load_classifier(args.diagram_onnx)
    else:
        print(f"No diagram classifier at {args.diagram_onnx} - checkpoint matching off.")

    print(f"Reading {args.image} ...")
    out = run_pipeline(
        args.image, moves_clf, diagram_clf,
        result=args.result, topk=args.topk,
        beam_width=args.beam_width, per_ply=args.beam_top,
        temperature=args.temperature, tta=not args.no_tta,
        save_cells_dir=out_dir / "cells",
    )

    if out["low_resolution"]:
        print(f"WARNING: median cell height is only {out['median_cell_height']}px - "
              "accuracy suffers at this resolution; re-photograph at full camera "
              "resolution if possible.")
    if out["checkpoint_report"]:
        kaz = [(r["move"], r["side"], r["read"])
               for r in out["checkpoint_report"] if r["kind"] == "kazan"]
        pits = sum(1 for r in out["checkpoint_report"] if r["kind"] == "pit")
        print(f"Diagram checkpoints read: kazans {kaz}, {pits} pit cells")
    for warning in out["warnings"]:
        print(f"WARNING: {warning}")

    result = {"image": args.image, "onnx": args.onnx, **out["game_json"]}
    (out_dir / "game.json").write_text(json.dumps(result, indent=1))
    (out_dir / "raw.pgn").write_text(out["raw_pgn"])
    (out_dir / "legal.pgn").write_text(out["legal_pgn"])
    (out_dir / "beam.pgn").write_text(out["beam_pgn"])
    out["annotated_image"].save(out_dir / "annotated.jpg", quality=90)

    beam = out["game_json"]["beam"]
    agree = sum(d["agrees_with_raw"] for d in beam["moves"])
    print(f"Scanned {out['plies_scanned']} plies; strict legal replay covers {out['legal_plies']}.")
    print(f"Beam decode: {out['beam_plies']} fully legal plies "
          f"(log-prob {beam['log_prob']:.1f}, agrees with raw argmax on {agree}/{out['beam_plies']}).")
    print(f"Stopped: {out['stopped']}")
    print(f"Outputs in {out_dir}/: game.json, raw.pgn, legal.pgn, beam.pgn, annotated.jpg, cells/")


if __name__ == "__main__":
    main()