Alina Kuznetsova Project 2 of 4 · Back to index
II. Project 2 · MSc project · UAL Creative Computing Institute · 2026 · Graded A

The Rematch

A computer vision system for playing a chess engine on a real board. An overhead camera reads my moves, Stockfish replies, and its pieces are projected onto the board as light. I performed it live, restaging the game in which a machine first beat a world champion.

Role
Solo: concept, computer vision, engine integration, performance
Hardware
Overhead camera, projector, printed board
Software
Python, OpenCV, python-chess, Stockfish
Code
git.arts.ac.uk, performing-project
II.1What I built

Chess against a machine, without giving up the board.

I play chess seriously, and without regular opponents nearby most of my games happen online. A screen loses what makes the game: the pieces in your hand, the presence of the person across the table.

So I built a way to play an engine on a physical board. A camera above the board watches my moves. The system works out which move I made, sends it to Stockfish, and projects the reply as glowing pieces on the real squares. No sensors in the board and no special pieces: just a camera, a projector and software.

The full performance. The engine mated me in about three minutes; the point was never to win, but to see how else the game could go.
II.2The concept

Taking Kasparov's seat, one move before it went wrong.

In 1997 IBM's Deep Blue beat Garry Kasparov, the reigning world champion, over six games: the first time a computer had beaten a world champion under standard tournament conditions. The deciding sixth game turned on Kasparov's seventh move, widely regarded as the error that lost him the match.

I start from the position just before that move, take Kasparov's seat, and play something else. Keeping my pieces physical and the engine's projected was practical, but it was also the point: I am visibly present at the board, against an opponent that exists only as light and code.

7…h6

The move Kasparov played, and the one I don't.

Diagram of the starting position: Deep Blue v Kasparov, 1997, Game 6, before Black's seventh move
Fig. II.1 Game 6, before Black's 7…h6.
The board in the dark: physical black pieces, projected white pieces and the words checkmate, white wins
Fig. II.2 Checkmate, projected onto the real board.
II.3The system

Seven modules, one live loop.

Six Python modules each handle one stage of the path from camera to projector, and main.py conducts them. Two calibration scripts run once before play, so the camera and the projector agree on exactly where the squares are.

  1. No.StageInOut
  2. 01CameraiPhone via CamoThe boardRaw frame
  3. 02Motionmotion_detector.pyRaw frameMoving, settling or stable; fires once when a move ends
  4. 03Board warpboard_calibration.pyRaw frameFlat, top-down board
  5. 04Occupancyoccupancy_detector.pyTop-down board and empty-board baselineThe set of occupied squares
  6. 05Game stategame_state.pyOccupied squaresMy move, matched against every legal move, then Stockfish's reply
  7. 06Renderrenderer.pyBoard positionWhite pieces on a transparent 800 × 800 layer
  8. 07Compositemain.pyPiece layer and status textPieces on pure black, turned 180° to face me
  9. 08Projector warpprojector_calibration.pyBoard imageImage aligned to the physical board
  10. 09Projectorfull-screen windowAligned imageLight on the board

main.py runs the loop, keeps a 10-minute clock per side, and projects check, checkmate and time-out messages.

Decision 1

Never read a half-finished move

The camera watches continuously, and a pause mid-move looks like a finished position. A three-state machine solves it: the board is only read once it has been still for a full second, and only once per move.

MOVING→SETTLING→STABLE
Decision 2

Brightness, not piece recognition

Each square is compared to its own empty baseline as a proportional drop, (baseline - current) / baseline, so a black piece on a dark square still registers. The threshold of 0.25 was tuned in the lab: the value where all 15 black pieces read solidly with no false positives.

Decision 3

Infer the move, don't read it

game_state.py works backwards: it tries every legal move and keeps the one that produces the occupancy the camera saw. Almost always exactly one matches. The exception is promotion, where piece type is invisible, accepted as a known limit against a 2750 engine.

Decision 4

Black means no light

The engine's pieces are composited onto pure black, because a projector emits nothing for black. The real board shows through untouched and only the pieces appear, as light, exactly where the squares are.

Calibration view: the board warped to a flat top-down grid with lines between squares
Fig. II.3 Top-down warp; grid lines fall between squares.
Occupancy debug view: rectangles mark every square detected as occupied
Fig. II.4 Occupancy: every black piece found.
II.4What I learnt

Two problems, two lessons worth keeping.

Problem 1

Know when to fix the room, not the code

My first board's dark squares were too dark for the occupancy detector. I tried to solve it in software, with texture-based detection and by masking projected squares, and every fix created a new problem.

Reprinting the board with lighter dark squares fixed it outright, and the proportional threshold then worked across every square. Knowing when to stop tuning code and change the physical setup is the lesson I kept.

Problem 2

A bug only other people could find

The projected check and checkmate text worked in my own testing, because I drift towards the same kinds of positions. User testing found the one that broke it: a pawn under the text was lit above the detection threshold, vanished from the read, and the game froze.

The quick fix was smaller, dimmer text. The real fix removed the cause: main.py now hides the overlay on the single frame it reads the board, so the text can never interfere with detection.