9OCR / read_game.py
ansarzeinulla's picture
Read full board diagrams, dynamic beam width, CSV metadata batch UI
1b69166
Raw History Blame Contribute Delete
4.93 kB
"""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()