96% full-board accuracy on chess recognition — what the papers say and what actually worked

This was built for an online chess-recognition challenge: given rendered chessboard images from a held-out test set, predict the FEN string for each position. The evaluator scores two things — per-square accuracy and full-board accuracy — and returns results in real time. Here is what the submission page showed:

Online challenge evaluator output: ACCURACY 96.00%, PIECE ACCURACY 99.85%, IMAGES EVALUATED 3072, TOTAL TIME 175.28s
The online challenge evaluator output — scores computed on 3,072 held-out images the model had never seen.
MetricResult
Per-square accuracy (val)99.85%
Full-board accuracy (val)96%
Inference latency (GPU FP16)~40 ms
Inference latency (CPU)~500 ms
Training epochs30
Dataset size12,252 train / 3,072 test

96% full-board means roughly 96 out of 100 boards have every single square correct — one wrong square fails the whole board. The per-square rate (99.85%) puts the per-cell error at about 1.5 in 1,000. This post is a walkthrough of every design decision that got there, with the paper that justified each one.


The problem: pixels in, FEN string out

The task: given one top-down rendered PNG of a chessboard, predict the FEN piece-position string.

What is FEN? FEN (Forsyth–Edwards Notation) is a compact way to describe where every piece sits on a chessboard. A string like rnbqkbnr/pppppppp/8/8/2P5/8/PP1PPPPP/RNBQKBNR encodes all 64 squares — uppercase for white pieces, lowercase for black, digits for runs of empty squares. One FEN string = one complete board position.

There are 13 possible classes per square: empty, plus the 12 piece types (K Q R B N P in upper and lower case).

Three-panel view of training data: raw perspective image (left), warped top-down canonical view (centre), augmented version (right)
A sample from the training set. Left: the raw 1920×1080 input from a fixed camera angle. Centre: after homography warp to 512×512. Right: augmented variant used during training (colour jitter + horizontal flip with mirrored FEN labels).

Why is full-board accuracy the right metric? A model that is 99% per-square accurate but consistently wrong on one square per board scores 0% full-board. Every square must be correct for the board to be usable. Per-square accuracy is a training signal; full-board accuracy is the real-world score.

The full pipeline at a glance

1. Input PNG1920 × 1080 × 3H × W × RGB uint82. Warp (homography)512 × 512 × 31 square = 64×64 px3. Normalizetensor (1, 3, 512, 512)÷255, ImageNet mean/std4. ResNet-50 backbonefeatures (1, 2048, 16, 16)extracts visual features5. Head (pool + linear)logits (1, 64, 13)13 class scores per square6. Softmaxprobs (1, 64, 13)scores → probabilities (sum=1)7. Argmaxclasses (64,)pick highest-prob class per square8. Legality rulesclasses (64,) fixed2 kings? pawn on back rank?9. FEN string”rnbqkbnr/pppppppp/…“final outputPixels (millions) → Features → Text (bytes)~40 ms total on GPU FP16 / ~500 ms on CPUThe actual model is only steps 4–5 (ResNet-50 + Head).Everything else is pre/post-processing that runs outside the neural network.

What the papers said before writing a line of code

My principle going in: every component must have a paper behind it, or I need my own ablation experiment to justify it. That kept me from chasing intuitions.

PaperYearKey resultRole in this project
CVChess (Abeykoon et al.)202598.93% per-square on real photosArchitecture blueprint
ResNet (He et al.)2015Residual connections enable 50+ layer trainingBackbone
ImageNet (Russakovsky et al.)20151.3M labeled images for pretrainingPretrained weights
LiveChess2FEN (Mallasén Quintana et al.)202092% per-piece + legality Algorithm 2Post-processing rules
Neural Chessboard (Czyzewski et al.)2017~95% per-piece + chess rule enumerationHard chess constraints
Chesscog (Wölflein & Arandjelović)202199.77% per-square on synthetic dataAccuracy baseline
End-to-End (Masouris & van Gemert)2024DETR “failed to converge”Negative result — ruled out Transformers

Architecture blueprint — CVChess (Abeykoon et al., 2025)

CVChess is the direct blueprint for the model architecture. The paper proposes a pipeline: ResNet-50 backbone → AdaptiveAvgPool(8, 8) → reshape to (64, 2048)Linear(2048 → 13). They report 98.93% per-square accuracy on real photographs — a significantly harder domain than rendered images. If this architecture handles real photos at 98.9%, it should handle clean synthetic renders well above that.

The key insight CVChess demonstrates is per-square classification with a shared head. One linear layer predicts all 64 squares simultaneously, sharing the same weights.

Why shared weights instead of 64 separate classifiers? A white king looks the same whether it stands on e1 or d4. You want the same detector to fire wherever a white king appears, regardless of which square it occupies. Shared weights enforce that — the model learns “what a white king looks like” once, not 64 times. This uses 2,048 × 13 = 26,624 parameters instead of 64 times as many.

I adopted CVChess’s architecture verbatim, adding only ImageNet pretrained weights and horizontal flip augmentation.

Backbone — ResNet (He et al., 2015)

What is a residual network? Before 2015, adding more layers to a neural network reliably made it train worse, not better. The problem is vanishing gradients: during backpropagation, the error signal multiplies through dozens of weight matrices on its way to the early layers, shrinking exponentially until the gradient is nearly zero. Early layers stop learning.

ResNet’s fix is a shortcut connection. Instead of output = F(x), a residual block computes output = F(x) + x. The plain input is added back to the block’s output. This gives gradients an express lane to early layers — instead of multiplying through every weight matrix, they can flow directly through the addition. He et al. showed that a 152-layer network could train reliably with this trick, and their CVPR 2016 Best Paper award reflects how transformative it was.

Residual Block — the core of ResNetinput xConv 1×1Conv 3×3Conv 1×1F(x)shortcut (skip connection) — x passes through unchanged+ReLUy = ReLU(F(x) + x)The “+x” shortcut lets gradients flow directly to early layers.Without it, a 50-layer network trains worse than a 20-layer one.ResNet-50 has 16 residual blocks across 4 layer groups.

Why pretrained on ImageNet? Russakovsky et al. (2015) created ImageNet: 1.3 million labeled photographs across 1,000 categories. A ResNet-50 trained on ImageNet has already learned to detect edges, textures, gradients, and shapes from natural images. Fine-tuning from this starting point costs a fraction of training from scratch — the early layers already know how to look at images. We load these weights via torchvision.models.ResNet50_Weights.IMAGENET1K_V2.

Legality post-processing — LiveChess2FEN (Mallasén Quintana et al., 2020)

LiveChess2FEN runs on a Jetson Nano for real-time physical board recognition, reporting 92% per-piece accuracy and 95% board detection. Their contribution relevant here is a fixed-point legality post-processor (Algorithm 2) that applies hard chess rules after inference:

  • R1: each side has exactly one king — if the model predicts two, demote the lower-confidence one
  • R2: pawns cannot appear on the back rank (row 1 or row 8)
  • R3: piece count limits — each side has at most 8 pawns, 2 rooks, 2 bishops, 2 knights, 1 queen

Why post-process instead of adding these rules to the loss function? Hard count constraints in a loss function create conflicting gradients. If the model predicts two white kings, the loss term that penalizes this fires on both squares simultaneously, pushing each square’s prediction in opposite directions. Training becomes unstable. Post-processing with confidence-based demotion is cleaner: after inference, if two white kings appear, keep the square with higher softmax probability and replace the other with its second-most-likely class. The model’s own confidence distribution makes the call.

I implemented Algorithm 2 directly in chess_recognition/legality.py.

Hard chess rule set — Czyzewski et al. (2017)

Czyzewski et al. report ~95% per-piece accuracy and 99.57% lattice point detection. Their contribution relevant here is the enumeration of hard chess constraints, which I used to populate the R3 piece count rules above.

Why not a Transformer? — Masouris & van Gemert (2024)

Masouris and van Gemert tried a DETR variant (a Transformer-based object detection architecture) for chess recognition and reported it “failed to converge”.

Why is this a valuable result? DETR was designed to detect a variable number of objects at unknown locations. A chessboard is the opposite problem: exactly 64 predetermined locations, no need to find anything. The per-square CNN classification approach from CVChess is the right inductive bias for a fixed grid. DETR’s flexibility is a liability when your output space is fully rigid. This negative result saved several days of investigation.

Synthetic images are tractable — Chesscog (Wölflein & Arandjelović, 2021)

Chesscog achieves 99.77% per-square accuracy on synthetic data, confirming that CNNs can classify rendered chess pieces to high accuracy. I did not use their board detection pipeline because in this dataset the camera is fixed — but their accuracy number set the expectation for what was achievable.


The key insight from EDA: the camera does not move

Before building the model, I averaged 200 training images pixel-by-pixel. If the camera moves between shots, the average blurs everywhere. If the camera is fixed, the static parts (the board surface) stay sharp while the moving parts (the pieces) blur out.

The average image showed a crystal-clear board outline and completely blurred pieces. One observation, two architectural decisions:

Pixel-average of 200 training images showing sharp board surface and ghost-like blurred pieces
Pixel average of 200 training images. The board surface is razor-sharp (camera never moves). The pieces are transparent ghosts — they appear in different positions across images, so they average out. This single observation confirmed a fixed camera and unlocked the cached homography optimisation.

1. Cached homography. A homography is a 3×3 matrix that maps points on a tilted plane to a flat top-down view. cv2.getPerspectiveTransform computes the matrix from the four board corners, mapping the board to a 512×512 canonical view where each square occupies exactly 64×64 pixels. Because the camera never moves, compute this matrix once and reuse it for every image.

Homography: tilted board → canonical 512×512Photo (perspective view)board (trapezoid)a8h8h1a1H · x(3×3 matrix)Warped (512×512, top-down)board (square)(0,0)(512,0)(512,512)(0,512)
Training image with four red corner dots marking the board corners and a green grid overlay showing the 8×8 square grid
Corner detection output: the four red dots are the board corners fed to cv2.getPerspectiveTransform. The green grid shows the resulting 8×8 projection — each cell maps to exactly one chess square in the warped 512×512 image. This matrix is computed once and cached for all subsequent frames.

Chesscog needed a full detection pipeline for board localization. This project skips it entirely — the cached homography does the job in microseconds.

2. Hash-based insurance. Compute a SHA-256 hash of the top strip of the image (rows y=0..300, which shows only the background — no pieces). Same hash → camera in same position → use cached matrix. Different hash → camera moved → fall back to classical edge detection. Pieces occupy y=348 and below, so the hash is never influenced by piece positions.


The piece misalignment problem (and why full-board classification solves it)

Homography corrects the board surface — a flat 2D plane. Chess pieces are 3D objects. After warping, a tall piece like the king has its base (the ground truth label location) in square e4, but its crown can project into e5 or beyond.

The Piece Misalignment ProblemSide view (physical reality)📷 cameraboard surfacepawn (short)king (tall)king’s crownprojects here ↓adjacent square!After warp (top-down view)base (label here)♔ topcrown “falls into” adjacent square!

The naive approach fails. Cropping 64 individual squares and running 64 separate classifiers makes this unsolvable: the most distinctive pixels for identifying the king in its square might be sitting in the adjacent square’s crop.

The CVChess solution. Classify all 64 squares from the full 512×512 image in one forward pass. ResNet-50 with a 32× stride has a receptive field of roughly 483×483 pixels at the deepest layers — nearly the full image. Each feature cell encodes context from a large patch of the board, not just one square.

Analogy: imagine a panel of judges assessing 64 candidates simultaneously by watching them all work in the same room, rather than calling each one into a separate booth. The panel can see that the person labeled as sitting in seat 12 is actually reaching into seat 13 — because they have the full-room view. The per-booth interviewer would just see an empty seat.


Architecture: from pixels to FEN

The full inference pipeline with actual tensor shapes at each step:

Input:  PNG  →  1080 × 1920 × 3    (H × W × BGR uint8)

BGR→RGB + cv2.warpPerspective

Warped: 512 × 512 × 3              (1 square = 64×64 px)

Normalize (/255, ImageNet mean/std)

Tensor: 1 × 3 × 512 × 512          (B, C, H, W float32)

ResNet-50 (Layer 4 output)

Features: 1 × 2048 × 16 × 16       (NOT pixels — feature cells)

AdaptiveAvgPool(8, 8)

Pooled: 1 × 2048 × 8 × 8           (1 cell = 1 chess square)

Reshape → 1 × 64 × 2048

Linear(2048 → 13)

Logits: 1 × 64 × 13                (13 scores per square)

Softmax + argmax + legality rules

FEN string: "rnbqkbnr/pppppppp/..."

Understanding the three unit types

The 16×16 after ResNet-50’s layer 4 is not pixels. It is 16×16 feature cells, each holding 2048 channels of extracted features. One feature cell covers 32×32 pixels of the warped image. After pooling to 8×8, each cell covers 64×64 pixels — exactly one chess square.

Head: Feature Map → 64 square predictionsFeature Map(2048, 16, 16)16 × 16AvgPool(8)Pooled(2048, 8, 8)8 × 81 cell = 1 squarereshape64 vectors(64, 2048)a8 → vec₁b8 → vec₂h1 → vec₆₄Linear(2048→13)Logits(64, 13)13 scores per square:[empty, K, Q, R, B,N, P, k, q, r, b, n, p]e.g. [−2.1, 5.3, −1, …]softmax+argmaxClassK(white king)Key: the Linear layer uses the SAME weights for all 64 cells.2048 × 13 = 26,624 parameters classify all 64 squares. Spatial differences come entirely from the backbone’s feature vectors.

The 2048 numbers per cell are not color values; they are learned representations of shapes, textures, and spatial relationships that ResNet extracted from that region.

Model parameters. ~23M in the ResNet-50 backbone; 26,624 in the linear head (2048 × 13).


Training: two-phase fine-tuning

Why two phases? When you attach a randomly initialized head to a pretrained backbone, the head produces large, noisy gradients early in training — it has no idea what to do yet. If the backbone is unfrozen from the start, those noisy gradients corrupt the ImageNet features the backbone already learned before the head has stabilized.

Training (done once)teaches the model — takes hours on GPUInput: 12,252 labeled imageseach PNG + corresponding FEN stringForward → Loss → Backward → Updaterepeat 30 epochs over all 12K imagescode: chess_recognition/train.pyOutput: weights.pt (~90 MB)23M learned parametersthis file IS “what the model knows”Like a teacher correcting homework until the student learnsInference (per image)uses the model — ~40 ms on GPUInput: 1 unlabeled imageno FEN — the model must predict itWarp → Normalize → Forwardno backward pass, no weight updatecode: submission/predict.pyOutput: FEN string”rnbqkbnr/pppppppp/…“returned to callerLike a student sitting an exam — applying what they learned
Phase 1 (epochs 1–3)Phase 2 (epochs 4–30)
BackboneFrozen — 23M params unchangedUnfrozen — gently fine-tuned
Head LR1e-31e-3
Backbone LRN/A1e-4
LR scheduleConstantCosineAnnealingLR
PurposeAlign head to existing ImageNet featuresAdapt backbone features to chess textures
Full-board acc. at end10.4%96%

The effect of unfreezing is stark. At the end of epoch 3 (frozen), full-board accuracy was 10.4%. At the end of epoch 4 (first epoch unfrozen), it jumped to 62.9% — a 52-point gain from a single epoch of backbone fine-tuning.

Loss function: weighted cross-entropy. The dataset is imbalanced — roughly half of all squares are empty in any position. Without weighting, the model’s easiest path is to predict “empty” everywhere and score ~50% per-square accuracy while producing useless FENs. Inverse-frequency class weights (capped at 5× to prevent rare pieces like queens from dominating training) fix this by making mistakes on occupied squares more costly.

Augmentation. Horizontal flip with symmetric FEN label remapping (column a↔h, b↔g, etc. — a flipped board is still a valid board with mirrored piece positions), plus color jitter ±10% on brightness, contrast, and saturation.


Legality post-processing in practice

The neural network does not know chess rules. It has seen 12,252 boards during training and learned statistical patterns, but it is not guaranteed to output a legal board. In testing, the most common illegal predictions were:

  • Two kings of the same color (model was close to 50/50 on a low-contrast square)
  • A pawn on the back rank (class confusion between pawn and empty near the board edge)

The LiveChess2FEN fixed-point algorithm resolves these with confidence-based demotion. When the model predicts two white kings, keep the square with the highest softmax probability as king, and replace the other with its second-most-likely class. Iterate until no rules are violated.

In practice, this never ran more than 2 iterations on the validation set.


Results

Evaluation output showing 96.00% accuracy, 99.85% piece accuracy, 3072 images evaluated, 175.28s total time
Final evaluation run against the 3,072-image held-out test set. All numbers in this post come from this output.

Training results from runs/v1/history.json:

EpochPer-square (val)Full-board (val)
189.3%6.8%
389.6%10.4%
4 ✦ unfreeze96.0%62.9% (+52 pts in 1 epoch)
1099.3%83.0%
1799.6%90.3%
2599.8%95.0%
30 (final)99.85%96.0%
Training curve: full-board accuracy by epoch0%25%50%75%100%13410172530Epochfrozenunfrozen backbone+52 ptsin 1 epoch!96%

Why is full-board accuracy lower than per-square accuracy? The math explains the gap. If each square has an independent 0.15% error rate, the probability that all 64 are correct is 0.9985^64 ≈ 0.907, or about 90.7%. The actual 96% is better than this naive independence model — errors are not uniformly distributed across all boards and squares. The model gets most boards perfectly right and clusters its mistakes on a subset of hard positions. If errors were random, we’d expect ~91%; the actual 96% means the model has learned to handle most positions reliably.

Comparison. Chesscog (Wölflein & Arandjelović, 2021) reports 99.77% per-square on synthetic data with a more complex pipeline including separate board detection. This project reaches 99.85% with a simpler approach — no separate board detection, no per-square crop model, one unified forward pass.


What did not work

The Masouris & van Gemert (2024) DETR-based approach explicitly failed to converge, as documented in their paper. The structural reason: DETR is built to solve “how many objects are there, and where are they?” A chessboard has exactly 64 fixed locations — there is nothing to locate. The fixed-grid inductive bias of per-square CNN classification is simply the correct fit for this problem.


Summary of design decisions and their sources

DecisionJustificationSource
ResNet-50 backboneResidual connections enable deep training; ImageNet pretraining provides strong featuresHe et al. (2015)
Per-square Linear headShared weights, fixed-grid output — 98.93% on real photosCVChess
512×512 inputDivisibility by 32 (ResNet stride), alignment to 8×8 grid, memory budgetCVChess (adapted)
ImageNet pretrained weightsMatch the backbone’s expected input distributionRussakovsky et al. (2015)
Fixed-point legality rulesStable post-processing without loss instabilityLiveChess2FEN
Hard chess constraints (R1/R2/R3)Enumerated rule setCzyzewski et al. (2017)
No TransformerDETR failed to converge on this taskMasouris & van Gemert (2024)
Cached homographyCamera fixed → compute once, reuseEDA observation (this project)
Two-phase fine-tuningPrevent head’s initial gradients from corrupting backboneStandard practice, confirmed empirically
Weighted cross-entropy~50% empty squares cause class imbalanceStandard practice

References

  1. Abeykoon et al. (2025). CVChess: A Robust Chess Recognition Pipeline. arXiv:2511.11522
  2. He, K., Zhang, X., Ren, S., & Sun, J. (2015). Deep Residual Learning for Image Recognition. arXiv:1512.03385. CVPR 2016 Best Paper.
  3. Mallasén Quintana et al. (2020). LiveChess2FEN: a Framework for Classifying Chess Pieces based on CNNs. arXiv:2012.06858
  4. Czyzewski et al. (2017). Neural Chessboard. arXiv:1708.03898
  5. Russakovsky et al. (2015). ImageNet Large Scale Visual Recognition Challenge. arXiv:1409.0575
  6. Wölflein, G. & Arandjelović, O. (2021). Recognizing Chessboards and Their Pieces in the Wild. arXiv:2103.07919
  7. Masouris, A. & van Gemert, J. (2024). End-to-End Chess Recognition. arXiv:2310.04086