经过一段时间的完善,军棋自动裁判软件的开发已经基本完成。
整个系统由硬件与软件两部分构成。
硬件部分的制作请参见《opencv-python实际演练(二)军棋自动裁判(3)棋子图像采集设备的改进
棋子图像采集设备将军棋 棋子图片通过USB上传到PC机
python开发的自动裁判软件对图像做预处理,提取目标区域的图像,然后调用百度OCR接口识别棋子图像上的文字。收到返回的识别结果后判定两方棋子的大小。
python代码如下:
config.py
#coding:utf-8
#军棋自动裁判配置文件
#配置数据
class Config:
def __init__(self):
pass
src = "camera/piece1.png"
resizeRate = 0.5
min_area = 30000
min_contours = 8
threshold_thresh = 180
epsilon_start = 10
epsilon_step = 5
result =[]
screen=None
frame=None
isDebug =False
#------ui---------
screenWidth = 640
screenHeight= 620
medalWidth= 240
shouldPlaySound = False
imgHelper.py
#coding:utf-8
#军棋自动裁判
#图像处理相关函数
from config import *
#图像预处理所需的模块
import cv2
import numpy as np
import math
import pygame
import colorsys
from PIL import Image
import pytesseract
#导入百度的OCR包
from aip import AipOcr
import json
#在线识别
class onLineOCR:
ocr = AipOcr('17339448','GIdSsUyqyDTibSGRArPeGyNn','DmKeXGCecpb2aKjHDFjIqKXimIOZeER1')
@classmethod
def image_to_string(cls,imgFile):
with open(imgFile, 'rb') as fin:
img = fin.read()
#res = cls.ocr.basicGeneral(img)
res = cls.ocr.basicAccurate(img)
text=''
try:
text = res['words_result'][0]['words']
except Exception as e:
print(e)
return text
'''
对坐标点进行排序
@return [top-left, top-right, bottom-right, bottom-left]
'''
def order_points(pts):
# initialzie a list of coordinates that will be ordered
# such that the first entry in the list is the top-left,
# the second entry is the top-right, the third is the
# bottom-right, and the fourth is the bottom-left
rect = np.zeros((4, 2), dtype="float32")
# the top-left point will have the smallest sum, whereas
# the bottom-right point will have the largest sum
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
# now, compute the difference between the points, the
# top-right point will have the smallest difference,
# whereas the bottom-left will have the largest difference
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
# return the ordered coordinates
return rect
# 求两点间的距离
def point_distance(a,b):
return int(np.sqrt(np.sum(np.square(a - b))))
# 找出外接四边形, c是轮廓的坐标数组
def boundingBox(idx,c,image):
if len(c) < Config.min_contours:
print("the contours length is less than %d ,need not to find boundingBox,idx = %d "%(Config.min_contours,idx))
return None
epsilon = Config.epsilon_start
while True:
approxBox = cv2.approxPolyDP(c,epsilon,True)
#显示拟合的多边形
#cv2.polylines(image, [approxBox], True, (0, 255, 0), 2)
#cv2.imshow("image", image)
if (len(approxBox) < 4):
print("the approxBox edge count %d is less than 4 ,need not to find boundingBox,idx = %d "%(len(approxBox),idx))
return None
#求出拟合得到的多边形的面积
theArea = math.fabs(cv2.contourArea(approxBox))
#输出拟合信息
print("contour idx: %d ,contour_len: %d ,epsilon: %d ,approx_len: %d ,approx_area: %s"%(idx,len(c),epsilon,len(approxBox),theArea))
if theArea > Config.min_area:
if (len(approxBox) > 4):
# epsilon 增长一个步长值
epsilon += Config.epsilon_step
continue
else: #approx的长度为4,表明已经拟合成矩形了
#转换成4*2的数组
approxBox = approxBox.reshape((4, 2))
return approxBox
else:
#尝试计算外接矩形,当棋子上的笔画确到了外边缘,会造成外轮廓不再是矩形,面积缩小,这时尝试用外接矩形来包住这种外轮廓
print("try boundingRect")
x, y, w, h = cv2.boundingRect(c)
if w*h > Config.min_area:
approxBox = [[x,y],[x+w,y],[x+w,y+h],[x,y+h]]
approxBox = np.int0(approxBox)
return approxBox
else:
print("It is too small ,need not to find boundingBox,idx = %d area=%f"%(idx, theArea))
return None
#提取目标区域,并对提取的图像进行文字识别
def pickOut(srcImg=None):
Config.result =[]
if srcImg is None:
# 开始图像处理,读取图片文件
image = cv2.imread(Config.src)
else:
image =srcImg
#print(image.shape)
#获取原始图像的大小
srcHeight,srcWidth ,channels = image.shape
#对原始图像进行缩放
#image= cv2.resize(image,(int(srcWidth*Config.resizeRate),int(srcHeight*Config.resizeRate)))
#cv2.imshow("image", image)
#转成灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#cv2.imshow("gray", gray)
# 中值滤波平滑,消除噪声
# 当图片缩小后,中值滤波的孔径也要相应的缩小,否则会将有效的轮廓擦除
binary = cv2.medianBlur(gray,7)
#binary = cv2.medianBlur(gray,3)
#转换为二值图像
ret, binary = cv2.threshold(binary, Config.threshold_thresh, 255, cv2.THRESH_BINARY)
#显示转换后的二值图像
#cv2.imshow("binary", binary)
# 进行2次腐蚀操作(erosion)
# 腐蚀操作将会腐蚀图像中白色像素,可以将断开的线段连接起来
erode = cv2.erode (binary, None, iterations = 2)
#显示腐蚀后的图像
#cv2.imshow("erode", erode)
# canny 边缘检测
canny = cv2.Canny(erode, 0, 60, apertureSize = 3)
#显示边缘检测的结果
#cv2.imshow("Canny", binary)
#showImgOnScreen(canny,(640,0),False)
# 提取轮廓
contours,_ = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 输出轮廓数目
print("the count of contours is %d \n"%(len(contours)))
#显示轮廓
#cv2.drawContours(image,contours,-1,(0,0,255),1)
#cv2.imshow("image", image)
lastIdx = -1
#针对每个轮廓,拟合外接四边形,如果成功,则将该区域切割出来,作透视变换,并保存为图片文件
for idx,c in enumerate(contours):
approxBox = boundingBox(idx,c,image)
if approxBox is None:
print("\n")
continue
#显示拟合结果
#cv2.polylines(image, [approxBox], True, (0, 0, 255), 2)
#cv2.imshow("image", image)
# 待切割区域的原始位置,
# approxPolygon 点重排序, [top-left, top-right, bottom-right, bottom-left]
src_rect = order_points(approxBox)
print("src_rect:\n",src_rect)
# 获取最小矩形包络
rect = cv2.minAreaRect(approxBox)
box = cv2.boxPoints(rect)
box = np.int0(box)
box = box.reshape(4,2)
box = order_points(box)
print("boundingBox:\n",box)
w,h = point_distance(box[0],box[1]), point_distance(box[1],box[2])
print("w = %d ,h= %d "%(w,h))
# 生成透视变换的目标区域
dst_rect = np.array([
[0, 0],
[w , 0],
[w , h ],
[0, h]],
dtype="float32")
# 得到透视变换矩阵
M = cv2.getPerspectiveTransform(src_rect, dst_rect)
#得到透视变换后的图像
warped = cv2.warpPerspective(image, M, (w, h))
#warped = cv2.warpPerspective(binary, M, (w, h))
#对提取的结果进行文本识别
#codeImg = np.vstack((warped, warped))
#codeImg = np.vstack((codeImg, codeImg))
#codeImg=np.rot90(codeImg,-1)
#对局时两个放棋子的放向正好相反,为了得到水平方向的文字图片,两次旋转纠正的方向也正好要相反
if lastIdx < 0 :
codeImg=np.rot90(warped,1)
else:
codeImg=np.rot90(warped,-1)
lastIdx = idx
# 调用本地识别接口
#code = pytesseract.image_to_string(codeImg, lang='chi_sim')
#code = pytesseract.image_to_string(codeImg, lang='junqi')
#Config.result.append(code)
#print(code)
#将变换后的结果图像写入png文件
Config.src = "output/piece%d.png"%idx
cv2.imwrite(Config.src , codeImg, [int(cv2.IMWRITE_PNG_COMPRESSION), 9])
if isRedImage(Config.src) :
code ='红色:'
else:
code ='黑色:'
#调用在线识别接口(在线接口受网络的影响)
code += onLineOCR.image_to_string(Config.src)
Config.result.append(code)
if Config.isDebug :
print(code)
print("\n")
#在pygame上显示图像
def showImgOnScreen(img,pos,isBGR=True):
imgFrame=np.rot90(img)
imgFrame = cv2.flip(imgFrame,1,dst=None) #水平镜像
if isBGR:
#cv2用的是BGR颜色空间,pygame用的是RGB颜色空间,需要做一个转换
imgFrame=cv2.cvtColor(imgFrame,cv2.COLOR_BGR2RGB)
#pygame不能直接显示numpy二进制数组数据,需要转换成surface才能正常显示
imgSurf=pygame.surfarray.make_surface(imgFrame)
Config.screen.blit(imgSurf, pos)
#查找图像的主要颜色
def findDominantColor(image):
image = image.convert('RGBA')
#生成缩略图,减少计算量
image.thumbnail((200, 200))
max_score = 0
dominantColor = None
for count, (r, g, b, a) in image.getcolors(image.size[0] * image.size[1]):
# 跳过纯黑色
if a == 0:
continue
saturation = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)[1]
y = min(abs(r * 2104 + g * 4130 + b * 802 + 4096 + 131072) >> 13, 235)
y = (y - 16.0) / (235 - 16)
# 忽略高亮色
if y > 0.9:
continue
score = (saturation + 0.1) * count
if score > max_score:
max_score = score
dominantColor = (r, g, b)
return dominantColor
#判断是否红色图片
def isRedImage(imageFile):
image = Image.open(imageFile)
r=255
try:
r,g,b = findDominantColor(image)
except Exception as e:
print(e)
if r > 200:
return True
else:
return False
judge.py
#coding:utf-8
#军棋自动裁判 判断类
from config import *
piecePower={
'工兵':1,
'排长':2,
'连长':3,
'营长':4,
'团长':5,
'旅长':6,
'师长':7,
'军长':8,
'司令':9,
'地雷':-1,
'炸弹':-2,
'军旗':200,
}
#裁判类
class Judger:
def __init__(self):
pass
def judge(self):
if type(Config.result)==type('正在处理'):
return Config.result
if Config.isDebug:
tip = "测试结果"
for code in Config.result:
tip+=" : "+code
return tip
return self.judgeOcrResut()
#识别ocr识别的结果, ocr识别的结果是一个列表,存放于 Config.result中
def judgeOcrResut(self):
red=''
black=''
for code in Config.result:
a=code.split(':')
if a[0] == '红色':
red = a[1]
if a[0] == '黑色':
black = a[1]
#print(red,black)
if red not in piecePower.keys():
return '红色棋子不能识别,请旋转后重试'
if black not in piecePower.keys():
return '黑色棋子不能识别,请旋转后重试'
return self.compare(red,black)
#比较两个棋子棋力的大小
def compare(self,red,black):
if piecePower[red] > 100 or piecePower[black] > 100 :
return '军旗不能被裁判'
if piecePower[red] < 0 or piecePower[black] < 0 :
if piecePower[red]==1 :
return '红方获胜'
if piecePower[black]==1 :
return '黑方获胜'
return '同归于尽'
if piecePower[red] > piecePower[black] :
return '红方获胜'
if piecePower[red] < piecePower[black] :
return '黑方获胜'
return '同归于尽'
bfButton.py
# -*- coding=utf-8 -*-
import threading
import pygame
from pygame.locals import MOUSEBUTTONDOWN
class BFControlId(object):
_instance_lock = threading.Lock()
def __init__(self):
self.id = 1
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(BFControlId, "_instance"):
BFControlId._instance = BFControlId(*args, **kwargs)
return BFControlId._instance
def get_new_id(self):
self.id += 1
return self.id
CLICK_EFFECT_TIME = 100
class BFButton(object):
def __init__(self, parent, rect, text='Button', click=None):
self.x,self.y,self.width,self.height = rect
self.bg_color = (225,225,225)
self.parent = parent
self.surface = parent.subsurface(rect)
self.is_hover = False
self.in_click = False
self.click_loss_time = 0
self.click_event_id = -1
self.ctl_id = BFControlId().instance().get_new_id()
self._text = text
self._click = click
self._visible = True
self.init_font()
def init_font(self):
#font = pygame.font.Font(None, 28)
font = pygame.font.Font("C:\Windows\Fonts\STSONG.TTF", 20)
white = 100, 100, 100
self.textImage = font.render(self._text, True, white)
w, h = self.textImage.get_size()
self._tx = (self.width - w) / 2
self._ty = (self.height - h) / 2
@property
def text(self):
return self._text
@text.setter
def text(self, value):
self._text = value
self.init_font()
@property
def click(self):
return self._click
@click.setter
def click(self, value):
self._click = value
@property
def visible(self):
return self._visible
@visible.setter
def visible(self, value):
self._visible = value
def update(self, event):
if self.in_click and event.type == self.click_event_id:
if self._click: self._click(self)
self.click_event_id = -1
return
x, y = pygame.mouse.get_pos()
if x > self.x and x < self.x + self.width and y > self.y and y < self.y + self.height:
self.is_hover = True
if event.type == MOUSEBUTTONDOWN:
pressed_array = pygame.mouse.get_pressed()
if pressed_array[0]:
self.in_click = True
self.click_loss_time = pygame.time.get_ticks() + CLICK_EFFECT_TIME
self.click_event_id = pygame.USEREVENT+self.ctl_id
pygame.time.set_timer(self.click_event_id,CLICK_EFFECT_TIME-10)
else:
self.is_hover = False
def draw(self):
if self.in_click:
if self.click_loss_time < pygame.time.get_ticks():
self.in_click = False
if not self._visible:
return
if self.in_click:
r,g,b = self.bg_color
k = 0.95
self.surface.fill((r*k, g*k, b*k))
else:
self.surface.fill(self.bg_color)
if self.is_hover:
pygame.draw.rect(self.surface, (0,0,0), (0,0,self.width,self.height), 1)
pygame.draw.rect(self.surface, (100,100,100), (0,0,self.width-1,self.height-1), 1)
layers = 5
r_step = (210-170)/layers
g_step = (225-205)/layers
#for i in range(layers):
# pygame.draw.rect(self.surface, (170+r_step*i, 205+g_step*i, 255), (i, i, self.width - 2 - i*2, self.height - 2 - i*2), 1)
else:
self.surface.fill(self.bg_color)
#pygame.draw.rect(self.surface, (0,0,0), (0,0,self.width,self.height), 1)
#pygame.draw.rect(self.surface, (100,100,100), (0,0,self.width-1,self.height-1), 1)
#pygame.draw.rect(self.surface, self.bg_color, (0,0,self.width-2,self.height-2), 1)
self.surface.blit(self.textImage, (self._tx, self._ty))
main.py
#coding:utf-8
#军棋自动裁判 主文件
#将两个棋子的内容从棋子图像采集器中提取出来,调用tesseract或百度OCR识别出文字后判断两个棋子的棋力大小
#当前系统中安装的是 tesseract5.0
import pygame
from imgHelper import *
from bfButton import BFButton
from judge import Judger
#按扭事件处理:测试
def test_click(btn):
Config.isDebug = True
if Config.frame is None:
print('Config.frame is None')
else:
pickOut(Config.frame)
#按扭事件处理:裁判
def judeg_click(btn):
Config.isDebug = False
showBusy()
if Config.frame is None:
print('Config.frame is None')
else:
pickOut(Config.frame)
Config.shouldPlaySound =True
#显示背景图片
def showBackGround(x,y):
screen.blit(bgImg, (x, y))
def showJudgeImg(x,y,result):
#print(result)
if result =='红方获胜':
screen.blit(redImg, (x, y))
if Config.shouldPlaySound:
wave_red.play()
Config.shouldPlaySound = False
if result =='黑方获胜':
screen.blit(blackImg, (x, y))
if Config.shouldPlaySound:
wave_black.play()
Config.shouldPlaySound = False
if result =='同归于尽':
screen.blit(evenImg, (x, y))
if Config.shouldPlaySound:
wave_even.play()
Config.shouldPlaySound = False
def showBusy():
if Config.frame is None:
Config.result='图像采集仪异常'
else:
Config.result='正在思考......'
screen.fill(BLACK)
tip = Config.result
text = font.render(tip, True, WHITE)
text_rect = text.get_rect()
x= int(640/2-text_rect.width/2)
text_rect.x = x
text_rect.y = 15
showBackGround(0,0)
screen.blit(text, text_rect)
pygame.display.update() #刷新窗口
#-----------------------------------------------------------------------------------------------
pygame.mixer.init() # 初始化混音器
pygame.init()
screen = pygame.display.set_mode([Config.screenWidth,Config.screenHeight]) #设置图形窗口大小
Config.screen = screen
pygame.display.set_caption("军棋自动裁判") #设置图形窗口标题
RED = (255,0,0) # 用RGB值定义红色
BLACK = (0,0,0) # 用RGB值定义黑色
WHITE = (255,255,255) # 用RGB值定义白色
BROWN = (166,134,95) # 用RGB值定义棕色
#加载图片资源
bgImg = pygame.image.load("res/tank.png")
redImg = pygame.image.load("res/red.png")
blackImg = pygame.image.load("res/black.png")
evenImg = pygame.image.load("res/even.png")
#加载声音资源
wave_red = pygame.mixer.Sound("res/red.wav")
wave_black = pygame.mixer.Sound("res/black.wav")
wave_even = pygame.mixer.Sound("res/even.wav")
#界面控件列表
UIControllerList=[]
#生成按钮对象
button1 = BFButton(screen, (Config.screenWidth/4-120/2,Config.screenHeight-60,120,40))
button1.text = '测试'
button1.click = test_click
UIControllerList.append(button1)
button2 = BFButton(screen, (Config.screenWidth*3/4-120/2,Config.screenHeight-60,120,40))
button2.text = '裁判'
button2.click = judeg_click
UIControllerList.append(button2)
#生成一个裁判员对象
theJudger = Judger()
Config.result = '人工智能裁判员已就位'
#准备捕捉摄像头内容
camera = cv2.VideoCapture(0)
#设置显示中文所用的字体
font = pygame.font.Font("C:\Windows\Fonts\STSONG.TTF", 24)
#窗口背景
screen.fill(BLACK)
#生成一个定时器对象
timer = pygame.time.Clock()
keepGoing = True
while keepGoing: # 事件处理循环
screen.fill(BLACK)
# 自动裁判后输出提示信息
tip = theJudger.judge()
#print(tip)
if Config.isDebug:
text = font.render(tip, True, WHITE)
else:
text = font.render(tip, True, WHITE)
text_rect = text.get_rect()
x= int(Config.screenWidth/2-text_rect.width/2)
text_rect.x = x
text_rect.y = 15
#显示摄像头内容
success, frame = camera.read()
Config.frame = frame
if Config.isDebug:
if frame is None:
Config.result ='图像采集仪异常'
else:
showImgOnScreen(frame,(0,text_rect.y+text_rect.height+10),True)
else:
showBackGround(0,0)
showJudgeImg((Config.screenWidth-Config.medalWidth)/2,(Config.screenHeight-Config.medalWidth)/2,tip)
screen.blit(text, text_rect)
for event in pygame.event.get():
if event.type == pygame.QUIT:
keepGoing = False
if event.type == pygame.KEYDOWN: # 如果按下了键盘上的键
if event.key == pygame.K_t: # 如果按下't'
pickOut(frame)
elif event.key == pygame.K_RIGHT: #如果按下了向右的方向键
pickOut(frame)
for c in UIControllerList:
c.update(event)
#重绘控件
for c in UIControllerList:
c.draw()
pygame.display.update() #刷新窗口
timer.tick(30) #设置帧率
pygame.quit() # 退出
为了产生图像与声音效果,还准备了一些图片与声音资源文件
下载地址
完整的软件包点此下载