python开发的军棋自动裁判软件

经过一段时间的完善,军棋自动裁判软件的开发已经基本完成。

整个系统由硬件与软件两部分构成。
硬件部分的制作请参见《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()       # 退出


为了产生图像与声音效果,还准备了一些图片与声音资源文件
在这里插入图片描述

下载地址

完整的软件包点此下载

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章