Chess piece detection On chessboard Opencv

Question:

I am trying to detect enter image description here

on my chessboardenter image description here

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?

enter image description 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()
Asked By: Cesar

||

Answers:

I have tried several setups and experimentally found this to work best:

  1. Do shape matching ignoring the piece color.
  2. 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:
Board image after Morphological gradient

Template image after Morphological gradient

Answered By: Baraa

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.

enter image description here

I used a trick from this answer and this answer that overwrites the results so you can find more matches

Answered By: bfris
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.