|
|
from smolagents import Tool |
|
|
from PIL import Image |
|
|
import os |
|
|
import cv2 |
|
|
import numpy as np |
|
|
import math |
|
|
import numpy |
|
|
from my_train_chess_pieces_recognition import ChessPiecesRecognition |
|
|
import traceback |
|
|
|
|
|
|
|
|
class ChessBoard(Tool): |
|
|
name = "_my_tool_chess_board" |
|
|
description = """ |
|
|
Process an image of a chess board and return board position as a list of chess pieces |
|
|
To invoke the tool use code as below |
|
|
<code> |
|
|
chess_pieces = _my_tool_chess_board(img=loaded_image) |
|
|
</code> |
|
|
""" |
|
|
|
|
|
inputs = { |
|
|
"img": { |
|
|
"type": "image", |
|
|
"description": "image of chess board to extract board position", |
|
|
} |
|
|
} |
|
|
|
|
|
output_type = "string" |
|
|
|
|
|
is_initialized = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, _chess_board_model_name, _chess_board_model_dir): |
|
|
print(f"***KS*** ChessBoard initializing ...") |
|
|
self.recognition = ChessPiecesRecognition(_chess_board_model_name, _chess_board_model_dir) |
|
|
self.is_initialized = True |
|
|
|
|
|
def __gradientx(self, img): |
|
|
|
|
|
grad_x = cv2.Sobel(img, cv2.CV_32F, 1, 0, ksize=31) |
|
|
return grad_x |
|
|
|
|
|
def __gradienty(self, img): |
|
|
|
|
|
grad_y = cv2.Sobel(img, cv2.CV_32F, 0, 1, ksize=31) |
|
|
return grad_y |
|
|
|
|
|
def __checkMatch(self, lineset): |
|
|
linediff = np.diff(lineset) |
|
|
x = 0 |
|
|
cnt = 0 |
|
|
for line in linediff: |
|
|
if abs(line - x) < 5: |
|
|
cnt += 1 |
|
|
else: |
|
|
cnt = 0 |
|
|
x = line |
|
|
return cnt == 5 |
|
|
|
|
|
def __pruneLines(self, lineset, image_dim, margin=20): |
|
|
|
|
|
lineset = [x for x in lineset if x > margin and x < image_dim - margin] |
|
|
if not lineset: |
|
|
return lineset |
|
|
linediff = np.diff(lineset) |
|
|
x = 0 |
|
|
cnt = 0 |
|
|
start_pos = 0 |
|
|
for i, line in enumerate(linediff): |
|
|
if abs(line - x) < 5: |
|
|
cnt += 1 |
|
|
if cnt == 5: |
|
|
end_pos = i + 2 |
|
|
return lineset[start_pos:end_pos] |
|
|
else: |
|
|
cnt = 0 |
|
|
x = line |
|
|
start_pos = i |
|
|
return lineset |
|
|
|
|
|
def __skeletonize_1d(self, arr): |
|
|
_arr = arr.copy() |
|
|
for i in range(len(_arr) - 1): |
|
|
if _arr[i] <= _arr[i + 1]: |
|
|
_arr[i] = 0 |
|
|
for i in range(len(_arr) - 1, 0, -1): |
|
|
if _arr[i - 1] > _arr[i]: |
|
|
_arr[i] = 0 |
|
|
return _arr |
|
|
|
|
|
def __getChessLines(self, hdx, hdy, hdx_thresh, hdy_thresh, image_shape): |
|
|
|
|
|
window_size = 21 |
|
|
sigma = 8.0 |
|
|
gausswin = cv2.getGaussianKernel(window_size, sigma, cv2.CV_64F) |
|
|
gausswin = gausswin.flatten() |
|
|
half_size = window_size // 2 |
|
|
|
|
|
|
|
|
hdx_thresh_binary = np.where(hdx > hdx_thresh, 1.0, 0.0) |
|
|
hdy_thresh_binary = np.where(hdy > hdy_thresh, 1.0, 0.0) |
|
|
|
|
|
|
|
|
blur_x = np.convolve(hdx_thresh_binary, gausswin, mode='same') |
|
|
blur_y = np.convolve(hdy_thresh_binary, gausswin, mode='same') |
|
|
|
|
|
|
|
|
skel_x = self.__skeletonize_1d(blur_x) |
|
|
skel_y = self.__skeletonize_1d(blur_y) |
|
|
|
|
|
|
|
|
lines_x = np.where(skel_x > 0)[0].tolist() |
|
|
lines_y = np.where(skel_y > 0)[0].tolist() |
|
|
|
|
|
|
|
|
lines_x = self.__pruneLines(lines_x, image_shape[1]) |
|
|
lines_y = self.__pruneLines(lines_y, image_shape[0]) |
|
|
|
|
|
|
|
|
is_match = (len(lines_x) == 7) and (len(lines_y) == 7) and \ |
|
|
self.__checkMatch(lines_x) and self.__checkMatch(lines_y) |
|
|
|
|
|
return lines_x, lines_y, is_match |
|
|
|
|
|
def __getChessTiles(self, img, lines_x, lines_y): |
|
|
stepx = int(round(np.mean(np.diff(lines_x)))) |
|
|
stepy = int(round(np.mean(np.diff(lines_y)))) |
|
|
|
|
|
|
|
|
padl_x = 0 |
|
|
padr_x = 0 |
|
|
padl_y = 0 |
|
|
padr_y = 0 |
|
|
if lines_x[0] - stepx < 0: |
|
|
padl_x = abs(lines_x[0] - stepx) |
|
|
if lines_x[-1] + stepx > img.shape[1] - 1: |
|
|
padr_x = lines_x[-1] + stepx - img.shape[1] + 1 |
|
|
if lines_y[0] - stepy < 0: |
|
|
padl_y = abs(lines_y[0] - stepy) |
|
|
if lines_y[-1] + stepy > img.shape[0] - 1: |
|
|
padr_y = lines_y[-1] + stepy - img.shape[0] + 1 |
|
|
|
|
|
img_padded = cv2.copyMakeBorder(img, padl_y, padr_y, padl_x, padr_x, cv2.BORDER_REPLICATE) |
|
|
|
|
|
setsx = [lines_x[0] - stepx + padl_x] + [x + padl_x for x in lines_x] + [lines_x[-1] + stepx + padl_x] |
|
|
setsy = [lines_y[0] - stepy + padl_y] + [y + padl_y for y in lines_y] + [lines_y[-1] + stepy + padl_y] |
|
|
|
|
|
squares = [] |
|
|
for j in range(8): |
|
|
for i in range(8): |
|
|
x1 = setsx[i] |
|
|
x2 = setsx[i + 1] |
|
|
y1 = setsy[j] |
|
|
y2 = setsy[j + 1] |
|
|
|
|
|
if (x2 - x1) != stepx: |
|
|
x2 = x1 + stepx |
|
|
if (y2 - y1) != stepy: |
|
|
y2 = y1 + stepy |
|
|
square = img_padded[y1:y2, x1:x2] |
|
|
squares.append(square) |
|
|
return squares |
|
|
|
|
|
|
|
|
def __extract_pieces_from_image_board(self, image): |
|
|
|
|
|
if image is None: |
|
|
print(f"Image not provided") |
|
|
return |
|
|
|
|
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
|
|
|
|
|
|
|
equ = cv2.equalizeHist(gray) |
|
|
norm_image = equ.astype(np.float32) / 255.0 |
|
|
|
|
|
|
|
|
grad_x = self.__gradientx(norm_image) |
|
|
grad_y = self.__gradienty(norm_image) |
|
|
|
|
|
|
|
|
Dx_pos = np.clip(grad_x, 0, None) |
|
|
Dx_neg = np.clip(-grad_x, 0, None) |
|
|
Dy_pos = np.clip(grad_y, 0, None) |
|
|
Dy_neg = np.clip(-grad_y, 0, None) |
|
|
|
|
|
|
|
|
hough_Dx = (np.sum(Dx_pos, axis=0) * np.sum(Dx_neg, axis=0)) / (norm_image.shape[0] ** 2) |
|
|
hough_Dy = (np.sum(Dy_pos, axis=1) * np.sum(Dy_neg, axis=1)) / (norm_image.shape[1] ** 2) |
|
|
|
|
|
|
|
|
a = 1 |
|
|
is_match = False |
|
|
lines_x = [] |
|
|
lines_y = [] |
|
|
|
|
|
while a < 5: |
|
|
threshold_x = np.max(hough_Dx) * (a / 5.0) |
|
|
threshold_y = np.max(hough_Dy) * (a / 5.0) |
|
|
|
|
|
lines_x, lines_y, is_match = self.__getChessLines(hough_Dx, hough_Dy, threshold_x, threshold_y, |
|
|
norm_image.shape) |
|
|
|
|
|
if is_match: |
|
|
break |
|
|
else: |
|
|
a += 1 |
|
|
|
|
|
squares_resized = [] |
|
|
if is_match: |
|
|
squares = self.__getChessTiles(gray, lines_x, lines_y) |
|
|
for square in squares: |
|
|
resized = cv2.resize(square, (32, 32), interpolation=cv2.INTER_AREA) |
|
|
squares_resized.append(resized) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return squares_resized |
|
|
|
|
|
def __detect_chess_pieces(self, images): |
|
|
return self.recognition.classify_pieces(images) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def forward(self, img: Image) -> str: |
|
|
pieces_list = "" |
|
|
try: |
|
|
print(f"***KS*** Analyzing chess board image for image: {img}") |
|
|
cv2_image = cv2.cvtColor(numpy.array(img), cv2.COLOR_RGB2BGR) |
|
|
|
|
|
|
|
|
|
|
|
squares_resized = self.__extract_pieces_from_image_board(cv2_image) |
|
|
|
|
|
|
|
|
|
|
|
pieces_list = self.__detect_chess_pieces(squares_resized) |
|
|
print(f"***KS*** Pieces list: {pieces_list}") |
|
|
except Exception as ex: |
|
|
print(traceback.format_exc()) |
|
|
print(f"***KS*** Exception invoking ChessBoard: {ex}") |
|
|
|
|
|
return pieces_list |
|
|
|