Blind Connect 4

Project · Computer Vision · 05/11/2024

pythonopencvcomputer visionhough transformk-meanswebcamgames
Project

A webcam points at a physical Connect 4 board and the program works out the state of the game: where every disc is, which colour it is, and whether anybody has won. The name comes from the goal, letting someone play the real board against a computer, or against a remote opponent, without the computer ever being told the moves.

Finding the board

Each frame goes through a Hough circle transform tuned to the disc radius, which finds the 42 holes in the board whether they are empty or filled. Overlapping detections are dropped, and the survivors are sorted into six rows with k-means clustering on their y coordinates, then by x within each row, which gives a 6 x 7 grid regardless of a bit of camera tilt. If the count isn't 42 the frame is rejected rather than guessed at.

The 42 holes found by the Hough transform, numbered in row order after the k-means row correction

Reading the discs

The pixel at the centre of each circle is classified as red, yellow or empty with simple RGB thresholds, which turned out to be good enough under room lighting once the thresholds were widened to cope with the glossy plastic. The board state is a 6 x 7 array of R, Y and O, and a win check scans rows, columns and both diagonals for four in a line.

Original image, detected discs coloured by the sampled centre pixel, and the reconstructed board state

Seeing what it sees

The viewer tiles four images: the raw frame, the detected circles, the circles filled with the colour that was sampled at their centre, and the reconstructed board. Watching all four side by side was the fastest way to find out why a detection went wrong, usually a reflection or a hand in shot. Helper classes let you crop a region of interest with the mouse and scale the input down so the Hough transform runs at a sensible frame rate, and a notebook holds the experiments that set the parameters.

circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=1, minDist=average_radius,
                           param1=200, param2=20,
                           minRadius=piece_radius - radius_delta,
                           maxRadius=piece_radius + radius_delta)
kmeans = KMeans(n_clusters=6, n_init=10)   # six rows
rows = kmeans.fit_predict(circles[:, 1].reshape(-1, 1))
screenshots
Detected circles Board representation