Chess piece detection On chessboard Opencv
Question:
But can’t get it to detect the right piece. The piece is 59×83. It should be detected, but isn’t.
I guess i’m missing something here?
import cv2
import numpy as np
# Load the chess board and chess piece images
img_board = cv2.imread('ccom.png')
img_piece = cv2.imread('bbis.png')
# Convert both images to grayscale
img_board_gray = cv2.cvtColor(img_board, cv2.COLOR_BGR2GRAY)
img_piece_gray = cv2.cvtColor(img_piece, cv2.COLOR_BGR2GRAY)
# Apply morphological operations to extract the chess piece from the board
kernel = np.ones((5, 5), np.uint8)
img_piece_mask = cv2.erode(img_piece_gray, kernel, iterations=1)
img_piece_mask = cv2.dilate(img_piece_mask, kernel, iterations=1)
# Find the matching location on the board
result = cv2.matchTemplate(img_board_gray, img_piece_mask, cv2.TM_SQDIFF)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# Draw a rectangle around the matching location
top_left = min_loc
bottom_right = (top_left[0] + img_piece.shape[1], top_left[1] + img_piece.shape[0])
cv2.rectangle(img_board, top_left, bottom_right, (0, 0, 255), 2)
# Show the result
cv2.imshow('Result', img_board)
cv2.waitKey(0)
cv2.destroyAllWindows()
Answers:
I have tried several setups and experimentally found this to work best:
- Do shape matching ignoring the piece color.
- Do color matching only on matching pieces.
1- Shape matching:
In a similar method to what you have listed, but using the morphological gradient
to stabilize the matching, a response map is created. The map is thresholded to get the most likely locations (4 in our case as color is not checked).
2- Color matching:
Using the absolute pixel-wise difference followed by a threshold (needs tuning) we can decide whether the studied piece’s color matches the provided template image.
import cv2
import numpy as np
def draw_results(img, rects):
for r in rects:
print(r)
cv2.rectangle(img, (r[0], r[1]), (r[0] + r[2], r[1] + r[3]), (0, 0, 255), 2)
def check_color(img, temp, rect):
y0, y1, x0, x1 = rect[1], rect[1] + rect[3], rect[0], rect[0] + rect[2]
crop = (img[y0 : y1, x0 : x1]).copy()
diff = cv2.absdiff(temp, crop)
avg_diff = cv2.mean(diff)[0] / 255
return avg_diff < 0.4 # a tricky threshold
def find_template_multiple(img, temp):
rects = []
w, h = temp.shape[1], temp.shape[0]
result = cv2.matchTemplate(img, temp, cv2.TM_CCOEFF_NORMED)
threshold = 0.5 # matching threshold, relatively stable.
loc = np.where( res >= threshold)
for pt in zip(*loc[::-1]):
rects.append((pt[0], pt[1], w, h))
#Perform a simple non-max suppression
rects, _ = cv2.groupRectangles(rects, 1, 1)
#Flatten list of list to list of elements
rects = [r for r in rects]
return rects
# Load the chess board and chess piece images
img_board = cv2.imread('board.png')
img_piece = cv2.imread('template.png')
# Convert both images to grayscale
img_board_gray = cv2.cvtColor(img_board, cv2.COLOR_BGR2GRAY)
img_piece_gray = cv2.cvtColor(img_piece, cv2.COLOR_BGR2GRAY)
s = 3
kernel = np.ones((s, s), np.uint8)
#morphological gradient stabilizes the template matching by focusing on the shape's edges rather than its content.
img_board_gray_grad = cv2.morphologyEx(img_board_gray, cv2.MORPH_GRADIENT, kernel)
img_piece_gray_grad = cv2.morphologyEx(img_piece_gray, cv2.MORPH_GRADIENT, kernel)
rects = find_template_multiple(img_board_gray_grad, img_piece_gray_grad)
matching_color_list = [check_color(img_board_gray, img_piece_gray, r) for r in rects]
#Keep only matching color rectangles.
matching_color_rects = [r for (r, is_matching) in zip(rects, matching_color_list) if is_matching]
draw_results(img_board, matching_color_rects)
# Show the result
cv2.imshow('Result', img_board)
cv2.waitKey(0)
cv2.destroyAllWindows()
Images:
Board image and template image after morphological gradient:

I’m not sure any morphological operations are required. You did a good job making the template image and it has a transparent background. You can use the transparency channel of the file to make a mask for the template.
import cv2
import numpy as np
# Load the chess board and chess piece images
img_board = cv2.imread('ccom.png')
img_piece = cv2.imread('bbis.png', cv2.IMREAD_UNCHANGED)
mask = img_piece[:,:,3] # use the inverted transparency channel for mask
# Convert both images to grayscale
img_board_gray = cv2.cvtColor(img_board, cv2.COLOR_BGR2GRAY)
img_piece_gray = cv2.cvtColor(img_piece, cv2.COLOR_BGR2GRAY)
h, w = img_piece_gray.shape
# Apply morphological operations to extract the chess piece from the board
#kernel = np.ones((5, 5), np.uint8)
#img_piece_mask = cv2.erode(img_piece_gray, kernel, iterations=1)
#img_piece_mask = cv2.dilate(img_piece_mask, kernel, iterations=1)
result = cv2.matchTemplate(img_board_gray, img_piece_gray, cv2.TM_SQDIFF_NORMED, mask=mask)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
while min_val < 0.1:
# Draw a rectangle around the matching location
top_left = min_loc
bottom_right = (top_left[0] + img_piece.shape[1], top_left[1] + img_piece.shape[0])
cv2.rectangle(img_board, top_left, bottom_right, (0, 0, 255), 2)
#overwrite the portion of the result that has the match:
h1 = top_left[1]-h//2
h1 = np.clip(h1, 0, result.shape[0])
h2 = top_left[1] + h//2 + 1
h2 = np.clip(h2, 0, result.shape[0])
w1 = top_left[0] - w//2
w1 = np.clip(w1, 0, result.shape[1])
w2 = top_left[0] + w//2 + 1
w2 = np.clip(w2, 0, result.shape[1])
result[h1:h2, w1:w2] = 1 # poison the result in the vicinity of this match so it isn't found again
# look for next match
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
cv2.imwrite('output.png', img_board)
# Show the result
cv2.imshow('Result', img_board)
cv2.waitKey(0)
cv2.destroyAllWindows()
I added the IMREAD_UNCHANGED
flag to imread
to cause OpenCV to read the transparency data. This is super helpful for you because you can now match both black bishops.
It used to be that you could only use a template mask for TM_SQDIFF
and TM_CCORR_NORMED
, but I think those restrictions were remove in ver 4.x. I replaced TM_SQDIFF
with TM_SQDIFF_NORMED
. The NORMED versions convert the result values to be between 0 and 1 and it feels a lot like a percentage score.
I used a trick from this answer and this answer that overwrites the results
so you can find more matches
But can’t get it to detect the right piece. The piece is 59×83. It should be detected, but isn’t.
I guess i’m missing something here?
import cv2
import numpy as np
# Load the chess board and chess piece images
img_board = cv2.imread('ccom.png')
img_piece = cv2.imread('bbis.png')
# Convert both images to grayscale
img_board_gray = cv2.cvtColor(img_board, cv2.COLOR_BGR2GRAY)
img_piece_gray = cv2.cvtColor(img_piece, cv2.COLOR_BGR2GRAY)
# Apply morphological operations to extract the chess piece from the board
kernel = np.ones((5, 5), np.uint8)
img_piece_mask = cv2.erode(img_piece_gray, kernel, iterations=1)
img_piece_mask = cv2.dilate(img_piece_mask, kernel, iterations=1)
# Find the matching location on the board
result = cv2.matchTemplate(img_board_gray, img_piece_mask, cv2.TM_SQDIFF)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# Draw a rectangle around the matching location
top_left = min_loc
bottom_right = (top_left[0] + img_piece.shape[1], top_left[1] + img_piece.shape[0])
cv2.rectangle(img_board, top_left, bottom_right, (0, 0, 255), 2)
# Show the result
cv2.imshow('Result', img_board)
cv2.waitKey(0)
cv2.destroyAllWindows()
I have tried several setups and experimentally found this to work best:
- Do shape matching ignoring the piece color.
- Do color matching only on matching pieces.
1- Shape matching:
In a similar method to what you have listed, but using the morphological gradient
to stabilize the matching, a response map is created. The map is thresholded to get the most likely locations (4 in our case as color is not checked).
2- Color matching:
Using the absolute pixel-wise difference followed by a threshold (needs tuning) we can decide whether the studied piece’s color matches the provided template image.
import cv2
import numpy as np
def draw_results(img, rects):
for r in rects:
print(r)
cv2.rectangle(img, (r[0], r[1]), (r[0] + r[2], r[1] + r[3]), (0, 0, 255), 2)
def check_color(img, temp, rect):
y0, y1, x0, x1 = rect[1], rect[1] + rect[3], rect[0], rect[0] + rect[2]
crop = (img[y0 : y1, x0 : x1]).copy()
diff = cv2.absdiff(temp, crop)
avg_diff = cv2.mean(diff)[0] / 255
return avg_diff < 0.4 # a tricky threshold
def find_template_multiple(img, temp):
rects = []
w, h = temp.shape[1], temp.shape[0]
result = cv2.matchTemplate(img, temp, cv2.TM_CCOEFF_NORMED)
threshold = 0.5 # matching threshold, relatively stable.
loc = np.where( res >= threshold)
for pt in zip(*loc[::-1]):
rects.append((pt[0], pt[1], w, h))
#Perform a simple non-max suppression
rects, _ = cv2.groupRectangles(rects, 1, 1)
#Flatten list of list to list of elements
rects = [r for r in rects]
return rects
# Load the chess board and chess piece images
img_board = cv2.imread('board.png')
img_piece = cv2.imread('template.png')
# Convert both images to grayscale
img_board_gray = cv2.cvtColor(img_board, cv2.COLOR_BGR2GRAY)
img_piece_gray = cv2.cvtColor(img_piece, cv2.COLOR_BGR2GRAY)
s = 3
kernel = np.ones((s, s), np.uint8)
#morphological gradient stabilizes the template matching by focusing on the shape's edges rather than its content.
img_board_gray_grad = cv2.morphologyEx(img_board_gray, cv2.MORPH_GRADIENT, kernel)
img_piece_gray_grad = cv2.morphologyEx(img_piece_gray, cv2.MORPH_GRADIENT, kernel)
rects = find_template_multiple(img_board_gray_grad, img_piece_gray_grad)
matching_color_list = [check_color(img_board_gray, img_piece_gray, r) for r in rects]
#Keep only matching color rectangles.
matching_color_rects = [r for (r, is_matching) in zip(rects, matching_color_list) if is_matching]
draw_results(img_board, matching_color_rects)
# Show the result
cv2.imshow('Result', img_board)
cv2.waitKey(0)
cv2.destroyAllWindows()
Images:
Board image and template image after morphological gradient:
I’m not sure any morphological operations are required. You did a good job making the template image and it has a transparent background. You can use the transparency channel of the file to make a mask for the template.
import cv2
import numpy as np
# Load the chess board and chess piece images
img_board = cv2.imread('ccom.png')
img_piece = cv2.imread('bbis.png', cv2.IMREAD_UNCHANGED)
mask = img_piece[:,:,3] # use the inverted transparency channel for mask
# Convert both images to grayscale
img_board_gray = cv2.cvtColor(img_board, cv2.COLOR_BGR2GRAY)
img_piece_gray = cv2.cvtColor(img_piece, cv2.COLOR_BGR2GRAY)
h, w = img_piece_gray.shape
# Apply morphological operations to extract the chess piece from the board
#kernel = np.ones((5, 5), np.uint8)
#img_piece_mask = cv2.erode(img_piece_gray, kernel, iterations=1)
#img_piece_mask = cv2.dilate(img_piece_mask, kernel, iterations=1)
result = cv2.matchTemplate(img_board_gray, img_piece_gray, cv2.TM_SQDIFF_NORMED, mask=mask)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
while min_val < 0.1:
# Draw a rectangle around the matching location
top_left = min_loc
bottom_right = (top_left[0] + img_piece.shape[1], top_left[1] + img_piece.shape[0])
cv2.rectangle(img_board, top_left, bottom_right, (0, 0, 255), 2)
#overwrite the portion of the result that has the match:
h1 = top_left[1]-h//2
h1 = np.clip(h1, 0, result.shape[0])
h2 = top_left[1] + h//2 + 1
h2 = np.clip(h2, 0, result.shape[0])
w1 = top_left[0] - w//2
w1 = np.clip(w1, 0, result.shape[1])
w2 = top_left[0] + w//2 + 1
w2 = np.clip(w2, 0, result.shape[1])
result[h1:h2, w1:w2] = 1 # poison the result in the vicinity of this match so it isn't found again
# look for next match
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
cv2.imwrite('output.png', img_board)
# Show the result
cv2.imshow('Result', img_board)
cv2.waitKey(0)
cv2.destroyAllWindows()
I added the IMREAD_UNCHANGED
flag to imread
to cause OpenCV to read the transparency data. This is super helpful for you because you can now match both black bishops.
It used to be that you could only use a template mask for TM_SQDIFF
and TM_CCORR_NORMED
, but I think those restrictions were remove in ver 4.x. I replaced TM_SQDIFF
with TM_SQDIFF_NORMED
. The NORMED versions convert the result values to be between 0 and 1 and it feels a lot like a percentage score.
I used a trick from this answer and this answer that overwrites the results
so you can find more matches