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()       # 退出


爲了產生圖像與聲音效果,還準備了一些圖片與聲音資源文件
在這裏插入圖片描述

下載地址

完整的軟件包點此下載

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