前言
这篇写的是象棋机器人的识别代码部分
工程代码:https://github.com/darkfan-cheese/Chinese-chess-robot
一、识别方法
识别的方法一般包括目标检测、分类或者使用opencv处理图像。这三种方法我都有尝试,总的来说目标检测的精度需要大量的数据集作为支撑,分类任务所需的数据集较少,但是对棋盘的位置要求比较严格,opencv的图像处理方法不容易想到,也有一定的局限性。
1.目标检测
这里就不放代码了,也没什么好放的,毕竟都是使用的网上开源的项目,直接打好数据集进行训练就是了。
但是局限性也特别明显,我一开始使用yolov3,后来换了其他的,但效果都不理想,因为一共32个棋子,拍摄下来都是小目标,想要一个不落地检测还是不太现实,特别是当时时间和精力有限的情况下。
2.分类
分类就是将每个可能包含棋子的区域先分割出来,再分别进行分类,棋盘处理部分的代码如下:
import cv2 as cv
import os
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from numpy import unicode
from classification import Classification
class chess(object):
def __init__(self):
self.a0 = [192, 393]
self.a9 = [195, 104]
self.i0 = [460, 396]
self.parallel = {'a': '黑士', 'b': '黑象', 'c': '黑炮', 'empty': '空', 'k': '将', 'n': '黑马', 'p': '卒', 'r': '黑车',
'R_A': '红士', 'R_B': '红相', 'R_C': '红炮', 'R_K': '帅', 'R_N': '红马', 'R_P': '红兵', 'R_R': '红车'}
self.parallel_en = {'a': 'a', 'b': 'b', 'c': 'c', 'empty': '0', 'k': 'k', 'n': 'n', 'p': 'p', 'r': 'r',
'R_A': 'A', 'R_B': 'B', 'R_C': 'C', 'R_K': 'K', 'R_N': 'N', 'R_P': 'P', 'R_R': 'R'}
def getzb(self):
a0 = self.a0
a9 = self.a9
i0 = self.i0
x = []
y = []
w = (int(i0[0]) - int(a0[0])) / 8
h = (int(i0[1]) - int(a0[1])) / 8
width = (int(a0[0]) - int(a9[0])) / 9
height = (int(a0[1]) - int(a9[1])) / 9
# print(a0[0], type(a0[0]), width, type(width), a0[1], type(a0[1]), height, type(height))
count_1 = 0
count_2 = 0
for i in range(90):
x.append(int(int(a0[0]) + w * count_2 - width * count_1))
y.append(int(int(a0[1]) + h * count_2 - height * count_1))
# print("计算到:", i)
if count_1 == 9:
count_1 = 0
count_2 += 1
else:
count_1 = count_1 + 1
return x, y
def puttxt(self, frame, zb, wenzi):
img_PIL = Image.fromarray(cv.cvtColor(frame, cv.COLOR_BGR2RGB)) # 图像从OpenCV格式转换成PIL格式
# font = ImageFont.truetype('ziti.ttf', 20) # 40为字体大小,根据需要调整
font = ImageFont.load_default().font
fillColor = (255, 255, 0)
draw = ImageDraw.Draw(img_PIL)
draw.text(zb, wenzi, font=font, fill=fillColor)
return cv.cvtColor(np.asarray(img_PIL[:, :, ::-1]), cv.COLOR_RGB2BGR) # 转换回OpenCV格式
其实这就是最基本的分类任务,不过需要保证棋盘的位置固定,这样便可以根据棋盘的三个顶点(a0,a9,i0)位置,将棋盘分割为90个方格,棋子都是放置在这些方格中的,这样每张图片分为90个小尺寸图像的分类任务,执行起来速度也不慢,精度也有一定的保证,不过当时由于时间问题没有采用。
3.Opencv图像处理
使用opencv是总体代码量和所需精力最少的,最难的是算法设计部分。
最终实现的步骤主要包括:使用行棋前拍摄的图像和行棋后拍摄的图像进行做差对比,计算得到变化后的坐标,也就是起点和终点的坐标(但还无法分辨哪个是起点哪个是终点),由于我们固定机械臂走黑棋,因此可以通过坐标中棋子的颜色判断起点与终点,这样便能够获得行棋信息更新棋局的FEN码。
代码如下:
def jiance(img1, img2, num=50, length=10):
"""
:param img1:第一张图
:param img2:第二张图
:param num:设置白色像素的阈值
:param length:设置先验框的一半边长
:return:变化的坐标
"""
print('识别中...')
# red_1 = hongqi(img1)
# red_2 = hongqi(img2)
# if red_2 < red_1*hong:
# chizi = 1
# else:
# chizi = 0
a_org = img1
b_org = img2
a_gray = cv2.cvtColor(a_org, cv2.COLOR_BGR2GRAY)
b_gray = cv2.cvtColor(b_org, cv2.COLOR_BGR2GRAY)
before = cv2.subtract(a_gray, b_gray) #
ret, before = cv2.threshold(before, 50, 255, cv2.THRESH_BINARY)
before_list = []
dic_list = []
old_qp = qipan.qipan()
count = 0
while True:
count += 1
# print('count', count)
if count >= 40:
print('识别失败,请重新走棋!!')
return False
for i in range(90):
sum = 0
sum_gray1 = 0
sum_gray2 = 0
zb = old_qp.qp[old_qp.qpd[i]][0]
zb = eval(zb)
x = zb[0]
y = zb[1]
x1 = x - length
y1 = y - length
x2 = x + length
y2 = y + length
for j in range(x1, x2):
for k in range(y1, y2):
sum = sum + before[k][j]
for j in range(x1-5, x2-5):
for k in range(y1+5, y2+5):
sum_gray1 = sum_gray1 + a_gray[k][j]
sum_gray2 = sum_gray2 + b_gray[k][j]
sub_gray = abs(sum_gray1 - sum_gray2)
print('gray', sub_gray, sum_gray2)
if sum > 255*num and sub_gray > 0.005*sum_gray2:
before_list.append((x, y))
if len(before_list) == 2:
break
elif len(before_list) > 2:
num += 2
before_list = []
elif len(before_list) < 2:
num -= 5
before_list = []
#cv2.circle(b_gray, (x, y), 30, 255)
#cv2.imshow('b_gray', b_gray)
#cv2.waitKey(0)
# cv2.circle(before, before_list[0], 10, color=(255, 255, 255))
# cv2.circle(before, before_list[1], 10, color=(255, 255, 255))
# cv2.imshow('before', before)
for (ok, op) in old_qp.qp.items():
op = op[0].split(',')
p = (int(op[0]), int(op[1]))
if p in before_list:
dic_list.append(ok)
#print('dic_list:', dic_list)
# cv2.waitKey(0)
return dic_list
代码中还包含了,判断阈值自动迭代,并且提高了检测的鲁棒性,在走棋的时候不小心误碰其他棋子,或者悔棋重新走棋,也能够识别正确,不过具体参数需要自行调参。