Extracting Chess Square Coordinates Dynamically with OpenCV Image Processing Methods

→ Extracting Chess Square Coordinates with OpenCV

siromer
5 min readJun 18, 2024

OpenCV provides a great number of image processing functions. By using these functions, a lot of information can be extracted from images. In this article, I will discuss how to extract square positions on a chess board by using classical image processing techniques that OpenCV provides, such as :

  • Gaussian Blurring, Otsu Threshold , Canny Edge Detection , Dilation , Hough Lines …
Main Process

You can see all the steps from the image above. Now, I will talk about this steps one by one, show you the code, and explain how it works. So lets get started.

Github Link : https://github.com/siromermer/Dynamic-Chess-Board-Piece-Extraction

1. Import Libraries

import cv2
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

2. Read Image and Convert it to Grayscale Image

image = cv2.imread(r"images/chess3.jpeg") # opencv reads images as BGR format

gray_image=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
rgb_image=cv2.cvtColor(image,cv2.COLOR_BGR2RGB)

plt.imshow(rgb_image) # matplotlib expects RGB format
RGB & Grayscale

3. Gaussian Blur

Gaussian Blur reduces the amount of noise and smooths the image.

gaussian_blur = cv2.GaussianBlur(gray_image,(5,5),0)

plt.imshow(gaussian_blur,cmap="gray")
Gaussian Blur

4. OTSU Threshold

For creating a binary image, Otsu thresholding is used. There is no need to set the thresholding parameters, it automatically finds best values.

ret,otsu_binary = cv2.threshold(gaussian_blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

plt.imshow(otsu_binary,cmap="gray")
OTSU Threshold

5. Canny Edge Detection

The Canny edge detector is an edge detection operator that uses a multi-stage algorithm to detect a wide range of edges in images (wikipedia).

canny = cv2.Canny(otsu_binary,20,255)

plt.imshow(canny,cmap="gray")

6. Dilation

Dilation adds pixels to the boundaries of objects in an image. Dilation increases the object area.

kernel = np.ones((7, 7), np.uint8) 

img_dilation = cv2.dilate(canny, kernel, iterations=1)

plt.imshow(img_dilation,cmap="gray")
Dilation

7. Hough Lines

Hough Lines is used for strengthening straight lines

lines = cv2.HoughLinesP(img_dilation, 1, np.pi/180, threshold=200, minLineLength=100, maxLineGap=50)

if lines is not None:
for i, line in enumerate(lines):
x1, y1, x2, y2 = line[0]

# draw lines
cv2.line(img_dilation, (x1, y1), (x2, y2), (255,255,255), 2)

plt.imshow(img_dilation,cmap="gray")
Hough Lines

8. Dilation

kernel = np.ones((3, 3), np.uint8) 

img_dilation_2 = cv2.dilate(img_dilation, kernel, iterations=1)

plt.imshow(img_dilation_2,cmap="gray")
Dilation

9. Find and Filter Contours

cv.2findContours” function finds contours and returns some useful values for processing.

  • Here, after finding contours , I filtered them by using contours area values. If they fall between predefined area values , I draw them on the “canny” image (contours is extracted from “img_dilation_2” image). Only for better visualization I used “canny” image.
  • square_centers” list contains all the information about squares , I am going to use this list in the following steps.
import cv2
import matplotlib.pyplot as plt

# find contours --> img_dilation_2
board_contours, hierarchy = cv2.findContours(img_dilation_2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

square_centers=list()

# draw filtered rectangles to "canny" image for better visualization
board_squared = canny.copy()

for contour in board_contours:
if 4000 < cv2.contourArea(contour) < 20000:
# Approximate the contour to a simpler shape
epsilon = 0.02 * cv2.arcLength(contour, True)
approx = cv2.approxPolyDP(contour, epsilon, True)

# Ensure the approximated contour has 4 points (quadrilateral)
if len(approx) == 4:
pts = [pt[0] for pt in approx] # Extract coordinates

# Define the points explicitly
pt1 = tuple(pts[0])
pt2 = tuple(pts[1])
pt4 = tuple(pts[2])
pt3 = tuple(pts[3])

x, y, w, h = cv2.boundingRect(contour)
center_x=(x+(x+w))/2
center_y=(y+(y+h))/2

square_centers.append([center_x,center_y,pt2,pt1,pt3,pt4])



# Draw the lines between the points
cv2.line(board_squared, pt1, pt2, (255, 255, 0), 7)
cv2.line(board_squared, pt1, pt3, (255, 255, 0), 7)
cv2.line(board_squared, pt2, pt4, (255, 255, 0), 7)
cv2.line(board_squared, pt3, pt4, (255, 255, 0), 7)


plt.imshow(board_squared,cmap="gray")

10. Sort coordinates

Sort coordinates by their center values , “square_centers” list is used.

  • Sort squares for chess format → 8x8
sorted_coordinates = sorted(square_centers, key=lambda x: x[1], reverse=True)

groups = []
current_group = [sorted_coordinates[0]]

for coord in sorted_coordinates[1:]:
if abs(coord[1] - current_group[-1][1]) < 50:
current_group.append(coord)
else:
groups.append(current_group)
current_group = [coord]

# Append the last group
groups.append(current_group)

# Step 2: Sort each group by the second index (column values)
for group in groups:
group.sort(key=lambda x: x[0])

# Step 3: Combine the groups back together
sorted_coordinates = [coord for group in groups for coord in group]

sorted_coordinates[:10]
Sorted Coordinates

11. Fill undetected Squares

Sometimes all the squares cannot be detected correctly, because of this some additional step may required. I created a simple algorithm for finding undetected squares and add them to “sorted_coordinates” list.

  • Here , I compared nearby squares coordinates, if there is a missing square, I add it to “sorted_coordinates” list.
  • Look at the image in STEP 9 , sqaure number 63 is missing , algorithm is going to fill missing square.
 
for num in range(len(sorted_coordinates)-1):
if abs(sorted_coordinates[num][1] - sorted_coordinates[num+1][1])< 50 :
if sorted_coordinates[num+1][0] - sorted_coordinates[num][0] > 150:
x=(sorted_coordinates[num+1][0] + sorted_coordinates[num][0])/2
y=(sorted_coordinates[num+1][1] + sorted_coordinates[num][1])/2
p1=sorted_coordinates[num+1][5]
p2=sorted_coordinates[num+1][4]
p3=sorted_coordinates[num][3]
p4=sorted_coordinates[num][2]
sorted_coordinates.insert(num+1,[x,y,p1,p2,p3,p4])

12. Display Result

  • Now , I will write square number for every square
square_num=1
for cor in sorted_coordinates:
cv2.putText(img = board_squared,text = str(square_num),org = (int(cor[0])-30, int(cor[1])),
fontFace = cv2.FONT_HERSHEY_DUPLEX,fontScale = 1,color = (125, 246, 55),thickness = 3)
square_num+=1

plt.imshow(board_squared,cmap="gray")
RESULT

--

--