← worksCourse project · Computer vision202520/20

Chessboard photo to FEN

A digital twin of a physical chess game, comparing a classical OpenCV pipeline against YOLOv8 and U-Net corner heatmaps.

OpenCVUltralytics YOLOPyTorchOptuna

With Félix Martins, Francisco da Ana and João Torre Pereira · Computer Vision, FEUP

Photograph a chessboard, return the position as a FEN string. The work splits into three tasks: a pipeline built from OpenCV primitives with nothing learned, a CNN that counts the pieces, and a full digital twin built on YOLO. Detecting the pieces turned out to be the easy half, at 0.99 mAP50 from a baseline YOLOv8n. Nearly all of the remaining difficulty was in finding the four corners of the board.

Task 1: no learned models

Everything downstream depends on a clean perspective warp, so the pipeline spends most of its effort on the board rather than on the pieces. Every figure below follows one photo, G006_IMG048, from the raw frame through to the boxes.

Finding the board. Two detectors run independently on the grayscale image. One blurs and thresholds for white; the other runs Canny edge detection directly. Each keeps the contour whose convex hull has the largest area, then absorbs its neighbours: any contour lying within a distance threshold of that hull is merged in, the hull is recomputed, and the sweep repeats until nothing else merges.

The two results are then intersected, and cv2.approxPolyDP simplifies the intersection to a four-vertex polygon, whose corners sort into board order by coordinate. That intersection is where the robustness comes from. White thresholding breaks under strong lighting, Canny latches onto nearby pieces or the floor, and the two rarely fail on the same image, so their overlap is far steadier than either alone.

The two detectors run independently and agree on the board. The checkered floor in the top-left corner is exactly the kind of structure Canny alone latches onto; thresholding for white ignores it, and the intersection inherits that.

Orientation comes from template matching. A crop of the knight logo printed on the board is rotated to each of the four possible corner orientations and matched against the warped image. The best-scoring rotation names the alignment.

The grid. CLAHE for local contrast, Gaussian blur, Canny, then dilation to close the broken edges. A Hough transform pulls out straight lines, which are filtered by angle and clustered to merge duplicates. Lines still go missing, so the gaps are filled by measuring the median spacing between the surviving lines and inferring where the absent ones belong. Intersecting the horizontal and vertical sets gives candidate points; keeping the 81 closest to the board centre, subject to a minimum separation, gives the 9×9 lattice. Without that separation constraint the selection collapses into a ring of points around the centre.

Hough returns 217 segments; clustering by angle and position leaves 14 vertical and 13 horizontal lines, because the printed border contributes its own. Nothing needed inferring on this photo. The work is in selecting 81 points out of 182, which is what the minimum-separation constraint is for.

Occupancy. Each square is cropped and handed to cv2.grabCut with a hint mask of two concentric ellipses, sitting slightly above the square's centre because a chess piece is tall and narrow. The inner ellipse is labelled GC_FGD, the outer GC_PR_FGD. A square counts as occupied only if the resulting foreground ratio falls inside a band. GrabCut tends to either flood the whole square or return nothing when it fails, and both failure modes land outside that band.

The hint ellipses sit slightly above each square's centre because a piece is tall and narrow. The per-square foreground ratio makes the band argument visible: occupied squares land at 20–44%, empty ones at 5% when GrabCut returns nothing and 83–96% when it floods. All 64 squares are called correctly on this photo.

Bounding boxes were attempted twice. The first version thresholds in HSV and cleans up with erosion and dilation. It works for white pieces, whose hue separates cleanly, but produces many false positives and negatives on black ones; a distance-transform threshold and watershed improve it without fixing it. The second version instead starts from the GrabCut mask and iterates watershed on it, letting the mask grow past the square boundary and rebuilding the hint mask each round by eroding for sure foreground and dilating for sure background. Boxes are then read off by inverse-warping the contours back to the original image and fitting boxes there, rather than warping the boxes themselves.

The second method scores slightly better and is the one used, but it leans on an accurate warp and degrades badly when corner detection goes wrong. A bad corner fit is detectable, so those images fall back to the HSV method, which never needed the warp in the first place.

The GrabCut masks seed a watershed that is iterated ten times, eroding for sure foreground and dilating for sure background each round so the mask can grow past the square it started in. The surviving contours are inverse-warped to the original frame, and boxes are fitted there rather than warped as boxes.

On the 50-image test subset, occupancy reaches a mean F1 of 0.9039 (median 0.9387). Bounding boxes, scored by F1 at an IoU threshold of 0.5, reach 0.7336 (median 0.7560). The gap between the two is the expected one: deciding whether a square holds a piece is a much easier question than drawing a box around that piece under overlap and perspective distortion.

Task 2: counting the pieces

The second task predicts one number, the count of pieces on the board, as a regression scored by MAE.

The baseline is a ResNet50 with pre-trained weights, fine-tuned end to end for 30 epochs, MSE loss, AdamW at 1e-4 with weight decay 0.01, and a sigmoid head rescaled into the legal range with out = 30·σ(x) + 2. Images were resized to 256 and cropped to 224. That setup reaches 0.73 validation MAE, and everything after it is a greedy sweep: try one change, keep it if it helps, carry it into the next experiment.

ChangeResult
Baseline: ResNet50, MSE, sigmoid head0.73 val MAE
Manual augmentation (rotation, perspective, blur, jitter)0.62
RandAugment in place of manual augmentationBeat both manual and AutoAugment
EfficientNetV2-S in place of ResNet500.47
ReLU, then scale·ReLU(x) + bias, in place of sigmoidImproved at each step
L1 and SmoothL1 in place of MSEBoth beat MSE
Optuna, 50 trials, 17 of them pruned0.154

Random cropping was deliberately left out of the augmentation set, since a crop that clips the board edge removes pieces and silently corrupts the label.

EfficientNetV2-S wins on all four metrics, at 0.4695 MAE against ResNet50's 0.6206. Swin V2-S, the only transformer in the set, lands between the two ResNet variants on every metric.

Optuna settled on L1 loss, a learning rate of 5.26e-4, weight decay 0.0326 and cosine annealing. That configuration was then trained for 30, 100 and 200 epochs, with the 100 and 200 epoch runs saving their best checkpoint by rounded MAE rather than plain MAE. The 200-epoch model is the best of them, at 0.0027 rounded MAE on validation and 0.0056 on test.

Rounded predictions against truth for counts from 2 to 32, and the unrounded error distribution. Off-diagonal cells hold a handful of images at most, and nearly every raw error falls between −0.3 and +0.2.

A board holds a whole number of pieces, and the raw predictions land close enough to the truth that rounding almost always recovers the exact count: on validation, a plain MAE of 0.0665 becomes 0.0027. Selecting checkpoints on the rounded metric is also the likely reason validation and test disagree slightly.

One result from Task 3 cuts against the premise of this one. The best YOLO detector, run over the full validation set at a confidence threshold of 0.5 and asked only to count its own detections, reaches 0.0233 validation and 0.0202 test MAE with no rounding at all, better than the dedicated regressor's unrounded numbers. The report puts this down to the detector's size and more robust training. The specialised model only wins once rounding is applied.

Task 3: the digital twin

Piece detection was never the bottleneck. YOLOv8n was the baseline, trained 100 epochs at batch size 64 with images resized to 960px and otherwise default settings, and reached mAP50 0.99 and mAP50-95 0.86. YOLOv8x and YOLO11s were trained afterwards; YOLOv8x came out on top at mAP50 0.995 and mAP50-95 0.875.

Mapping those boxes onto squares is where it got hard. An end-to-end approach following Masouris et al., the authors of the ChessReD dataset, classified each square directly through a fully connected head under cross-entropy loss. It overfit badly and performed poorly on anything beyond common openings.

Validation loss goes flat at epoch two and never moves again while training loss keeps falling. A semantic segmentation variant over the squares was tried as well and did no better.

So detection and board geometry were split apart: YOLO finds the pieces, a separate step finds the board, and each detection collapses to a single anchor point: horizontally centred, 90% of the way down the box, so it sits at the piece's base rather than on its body. Warping that point onto the rectified grid names a square, and that assignment is enough to write out a FEN string.

The same photo as Task 1, end to end. YOLO finds all 24 pieces, each box reduces to one point, that point names a square, and template matching on the knight logo rotates the matrix into FEN order. The output is r4rk1/5pn1/bpN3pp/p1pP4/4PPPq/P6P/2Q3B1/3R1RK1, at edit distance 0 against the annotation. Detections here come from the trained YOLO11s checkpoint rather than YOLOv8x, which scores marginally higher.

The pipeline is scored by edit distance between the expanded FEN and the ground truth: the minimum number of single-character insertions, deletions and substitutions needed to turn one into the other, which is a direct count of misplaced, wrong and missing pieces.

The first version of this pipeline used Task 1's classical corner detection, and across the whole of ChessReD2k it averaged 1.59 edits per board. Storing the mismatched strings and reading through them showed where the edits were coming from, and the size of the edit distance turned out to identify the cause:

Edit distanceWhat went wrong
1 – 3One occluded or ambiguous piece misread by the detector. The floor of the pipeline, set by YOLO.
Above 10Orientation. The corners are found, applied the wrong way round, and every piece shifts together.
Above 40Corner detection. The warp is wrong, so almost nothing lands on the right square.

The distribution is bimodal rather than spread out: a board is usually either exactly right or badly wrong, and the badly wrong ones are mostly orientation, where the corners are found well enough but applied the wrong way round, rotating the entire position.

That made corner detection the real task. Five approaches were built and compared.

YOLO handles the pieces in all five. The variation is entirely in how corners and orientation get found, and whether one model covers both.

Corner quality is measured as mean Euclidean distance (MED) between predicted and true corner coordinates, normalised, where 0% would be perfect.

ApproachCorner methodVal MEDTest MED
AClassical CV, as in Task 1not measurednot measured
BYOLO11s-pose keypoints6.9%3.6%
C1ResNet50 regression, frozen backbone14.91%13.19%
C2EfficientNetV2-S, all layers, plus augmentation0.58%0.45%
DU-Net mask plus ResNet18 orientation2.23%0.49%
EU-Net corner heatmaps, ResNet34 encoder0.50%0.31%

Approach A is the Task 1 pipeline reused, and it is the one that motivated the rest: it relies on Canny edges, polygon approximation and template matching, and it struggles under occlusion and difficult lighting.

Augmentation made the difference for the CNN regressor. C1 and C2 are the same idea: a CNN predicting four corner coordinates directly. C1 froze a ResNet50 backbone, trained a few added layers with MSE and Adam behind a sigmoid output, and was still unreliable after 150 epochs at 13.19% test MED. C2 switched to EfficientNetV2-S, unfroze every layer and added manual augmentation. Most augmentation libraries transform the image and leave the labels behind, which is useless when the label is a set of coordinates, so this needed tv_tensors.KeyPoints from a pre-release torchvision 0.23 to carry the corners through each rotation and perspective shift. That reached 0.45%.

Ground truth in green, prediction in red, zoomed because at full-frame scale the difference is a few pixels. Without augmentation the predictions sit 5–10% of the image away from the corners; with it they land on them. Two different photos, so the comparison is between the two models and not the two scenes.

YOLO11s-pose found the board but not its corners. Trained 100 epochs at batch size 64 and 640px, it located the board almost perfectly, with mAP50 of 1.0 on the board's bounding box, but its predicted keypoints rarely coincided with the true corners.

The segmentation route hit an annotation problem. D segments the board with a U-Net, reads the corners off the mask, and hands orientation to a separate ResNet18 classifying 0°, 90°, 180° or 270°. A mask says nothing about which corner is which, so the classifier is doing the work. But the corner annotations mark only the board's internal corners and crop out the knight logo, the one feature that settles orientation. The fix was to feed the classifier a deliberately wider crop with enough margin to keep the logo in frame.

The heatmap U-Net scored best and still mislabels corners. Predicting a probability distribution per corner rather than a coordinate gave well-defined peaks sitting on the true corners, at 0.31% test MED. But it sometimes swaps the heatmaps for the top-left and bottom-right corners. It learns to find corner-like features without reliably assigning the right label from the board's global orientation, which is the same failure the edit-distance analysis had already pointed at.

Corner heatmap peaks on the physical board, and the digital twin generated from the resulting FEN string.
A board where it goes wrong. The top-left heatmap lights up in two places and the bottom-right one never resolves into a peak at all, so the labels come out scrambled even though the board's corners are all found. A distribution carries that uncertainty, and reading only the brightest pixel discards it.

The final pipeline pairs YOLOv8x with the heatmap U-Net: find the corners, warp the board, drop each detection to its anchor point, read off the square, emit FEN.

Run over the whole of ChessReD2k, that pipeline averages 0.0197 edits per board, roughly two misplaced pieces per hundred boards, against 1.59 for the version that used classical corners. Almost all of the eighty-fold gap comes from the catastrophic cases disappearing: better corners mean fewer boards where the warp is wrong and nothing lands on the right square. The two written reports stop at 1.59 and never re-measure, so that final figure comes from the delivery notebook rather than from either PDF.

The set with annotated corners is small, which caps how well any of these generalise across varied conditions. Augmentation was only ever applied in the simplest of the corner methods, where it clearly helped, and the labelling problem calls for an explicit orientation module rather than asking the corner model to resolve it implicitly.

loading 11 projects 0%