Skip to content

Repository files navigation

2D Chess OCR

Extract FEN positions from screenshots of 2D chessboards — fully open-source, handles multiple boards per image, detects orientation and turn.

The goal isn't just the tool. It's a recipe for tackling 2D chessboard recognition with near-zero manual labeling: classical CV plus open-source models autolabel a dataset that trains a single end-to-end detector (YOLO / RF-DETR).

banner

What it does

Give it any image containing 2D chessboards (screenshots, book scans, video frames) and it returns:

  • FEN piece-placement string for each board
  • Board orientation (white or black at bottom)
  • Whose turn it is (inferred from highlighted last-move tiles)
  • Per-square confidence scores

Quick start

Installation

git clone git@github.com:AndrewSpano/2d-chess-ocr.git
cd 2d-chess-ocr
uv venv
echo "export PYTHONPATH=\"$(pwd):\$PYTHONPATH\"" >> .venv/bin/activate
source .venv/bin/activate
uv sync

Download finetuned models

Pre-trained weights, ready to use — no training required.

uv run download_models.py

Run inference

uv run model_inference.py \
    --model finetuned-models/yolo26m-finetuned.pt \
    --image images/input-example.png \
    --output annotated.png
Found 2 chessboard(s):
  Board 0: orientation=unknown  turn=w  turn_conf=0.50
    FEN: r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/2P2N2/PP1P1PPP/RNBQK2R
  Board 1: orientation=unknown  turn=w  turn_conf=0.50
    FEN: r1bqk1nr/pppp1ppp/2n5/2b1p3/4P3/2N2N2/PPPP1PPP/R1BQKB1R
Saved annotated image to: annotated.png

How it works

A single YOLO model detects all objects in one pass — no multi-stage pipeline at inference time:

Class ID What Count
0 Chessboard bounding box 1 per board
1–12 Individual pieces (6 black + 6 white) ~32 per board
13 Row-1 label (orientation marker) 0–1 per board
14–15 Last-move start/end tiles 0–2 per board

Postprocessing assigns pieces to boards by position, reads the grid into a FEN string, determines orientation from the row-1 marker, and infers turn from the piece on the last-move end tile.

Training pipeline

The YOLO training dataset is built through a multi-step autolabeling pipeline — see dataset_creation/README.md for the full walkthrough:

  1. Detect boards — classical CV detector
  2. Classify piecesSAM3 detects generic pieces, an ensemble of 3 DNNs classifies them with uncertainty estimation
  3. Detect orientationSAM3 locates the "1" digit near corners, verified by a small CNN binary classifier
  4. Label last move — manual annotation via a tkinter app (2 clicks required per board --> ~1s annotation time per image)

The trained YOLO model is then fine-tuned on this dataset — see model_training/README.md.

Dataset

A precomputed version of the autolabeled training dataset is provided in public-datasets/. It contains YOLO-format label files and a mapping.jsonl recording each label's provenance (images are not provided, due to copyright). A small binary dataset for the digit-one orientation classifier is also included.

See public-datasets/README.md for details and unzip commands.

Project structure

2d-chess-ocr/
├── download_models.py          # fetch the finetuned models from Hugging Face
├── model_inference.py          # run inference on images (CLI + importable classes)
├── chess_ocr_detection.py      # detection results --> FEN conversion logic
│
├── dataset_creation/           # dataset creation and autolabeling pipeline
│   ├── chessboards/            # board detection/autolabeling
│   ├── chess_piece_sets/       # piece tile dataset, classifier, and piece autolabeling
│   ├── orientation/            # rank-one orientation dataset/classifier/autolabeling
│   ├── synthetic_data/         # synthetic assets and YOLO synthetic generation
│   ├── turn/                   # last-move tile annotation tool
│   ├── board_corners.py        # Hough-transform tile-intersection detector
│   └── yolo_to_coco.py         # YOLO --> COCO dataset converter (for RF-DETR)
│
├── model_training/             # training, evaluation, export (YOLO + RF-DETR)
│   ├── train_yolo.py
│   ├── evaluate_yolo.py
│   ├── export_yolo.py
│   ├── train_rfdetr.py
│   ├── evaluate_rfdetr.py
│   └── fen_eval.py             # shared FEN-reconstruction evaluation
│
├── public-datasets/            # shipped precomputed datasets (zipped)
├── baselines/                  # baseline benchmark scripts
│
├── pyproject.toml
└── uv.lock

Benchmarks

Evaluated on 245 images from an out-of-distribution subset of the generated dataset. Latency = mean end-to-end per-image wall-clock time (including preprocessing and postprocessing) on an NVIDIA RTX 4090.

Method Detection rate Full-board acc¹ Latency Orientation Turn Multi-board
chess-ocr 100% 6.94%² 13.3 ms (CPU)
tensorflow_chessbot 49.8%³ 66.12% 12.4 ms (CPU+GPU⁴)
YOLO-m (this repo) 100% 95.93% 5.8 ms (GPU)
RF-DETR-L (this repo) 100% 95.19% 10.4 ms (GPU)⁵

¹ Best-orientation: a board is counted correct if the predicted FEN or its 180° rotation matches ground truth — this decouples piece recognition accuracy from orientation detection.
² Trained on synthetic SVG diagrams; the low accuracy reflects domain gap, not architecture capability.
³ Low detection rate: the Hough-line board detector fails on a significant fraction of images.
⁴ Board detection (Hough lines) runs on CPU; piece classification runs on GPU via ONNX Runtime. The CPU-bound detector is the bottleneck.
⁵ RF-DETR latencies are the native PyTorch model with optimize_for_inference() — unlike the YOLO numbers, there is no ONNX/TensorRT export yet, so further speedups are likely available.

See baselines for example scripts on how to run the benchmark.

Model comparison by board size (this repo)

Board size = the board's bounding box area as a fraction of min(img_h, img_w). Accuracies are full-board best-orientation. No boards in the evaluation set had side lengths below 25% of the shortest image dimension.

Model Large (≥50%, n=222) Medium (25–50%, n=47) Latency (mean)
YOLO-m 97.75% 89.36% 5.8 ms (GPU)*
YOLO-n 83.33% 40.43% 4.2 ms (GPU)
RF-DETR-L 98.20% 82.98% 10.4 ms (GPU)†
RF-DETR-2XL 97.75% 82.98% 16.2 ms (GPU)†

*If using TensorRT, the YOLO-m model has a latency of ~2.1ms.
†RF-DETR latencies are the native PyTorch model with optimize_for_inference() (no ONNX/TensorRT export yet).

Error analysis

A single misclassified piece is enough to make a board count as wrong under the full-board metric. The most common failure mode is piece-color confusion. The example below shows a white queen predicted as black:

example mistake

However, post-processing (e.g. enforcing chess-legality constraints, or simple heuristics) could catch many such mistakes.

Limitations (and how to address them)

  1. This method assumes cropped, axis-aligned, uniform 2D chessboards, since detected boards are split into an 8×8 grid mechanically. If this assumption does not hold, find_chessboard_corners() from board_corners.py can detect tile intersections via the Hough transform, though this adds a ~5 ms latency penalty due to the CPU-bound implementation.
  2. This method assumes chessboards are axis-aligned. For rotated or warped boards (e.g., photos taken with a phone), the problem can instead be framed as instance segmentation of the board region, followed by a homography to map it to an axis-aligned view before resuming the normal pipeline.

Ideas for future improvement

  1. Model size: The best performing model (yolo26-medium, onnx version) is 80MB, which is not really deployable on edge devices. Ideas such as compression and quantization could help in this regard, and they're worth exploring.
  2. Improving performance: Generally, more data could help close the gap and reach near 100% accuracy on unseen, OOD boards. A simple idea would be to finetune a foundation model on this task, and then use it to pseudolabel more images. If the foundation model can reach near 100% accuracy (with the current amount of data), it likely will aid the smaller, specialized YOLO models.
  3. Predict tile coordinates as auxiliary labels: Extend the YOLO/RF-DETR heads to predict two extra outputs per detection, alongside the class — the tile's rank (0–7) and file (0–7). This would localize each piece on the board directly, removing the geometric grid-assignment step currently done during FEN conversion. It would likely require more data, as well as adapting the loss function to ignore these outputs for classes where they don't apply (e.g. the chessboard box or the orientation marker). This detection-based framing (segment each piece, regress its tile) is also why the approach should scale to 3D / photographed boards, where two-stage "split into tiles, then classify each tile" pipelines break down under perspective and piece occlusion.

Ethical use

This tool is intended for game analysis, accessibility, and research. Do not use it for cheating in online chess. As a matter of fact, do not cheat. In life. In general.

License

See LICENSE.

About

Extract FEN positions from images of 2D chessboards. Multi-board, orientation & turn detection, near-zero manual labeling.

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages