PyOpenCV实战:借助视觉识别技术实现围棋终局的胜负判定

1 前言

学习一种技能,最好的方式就是与实际应用相结合,也就是人们常说的学以致用。很多的Python初学者在学完基础语法、基本技能之后,都会感到迷茫,主要原因就是不知道自己的一身本领可以用在何处,接下来又该向哪个方向发展。如果你恰好处在这样一个阶段,又对视觉识别技术感兴趣的话,不妨拿这个项目来练练手。

谈到视觉识别,一定离不开OpenCV,而OpenCV的图像数据结构,就是NumPy的多维数组(numpy.ndarray)。在后面的讲解中,我会穿插一些OpenCV的基本概念和技术细节,但不会对NumPy做过多介绍。如果不熟悉NumPy的话,建议先读一下《如果不懂NumPy,请别说自己是Python程序员》这篇文章。

2 准备工作

2.1 约定围棋局面的数据结构

标准的19路围棋盘上有361个交叉点可以落子,每个交叉点有三种状态:无子、黑子、白子,不难算出围棋共有 3 361 3^{361} 3361种不同的局面。这究竟是一个多大的数字?用Python可以很轻松地计算出来。

>>> pow(3,361)
17408965065903192790718823807056436794660272495026354119482811870680105167618464984116279288988714938612096988816320780613754987181355093129514803369660572893075468180597603

1.74 × 1 0 172 1.74\times10^{172} 1.74×10172,竟然长达173位!要知道,地球上全部物质的原子总数,也不过是 1.28 × 1 0 47 1.28\times10^{47} 1.28×1047,难怪围棋被认为是复杂度最高的棋种。

有点跑题了,咱们言归正传。对于围棋局面,用什么样的数据结构来表示最合理呢?我以前写过象棋和国际跳棋的代码,对于这两种棋,是用字符串来表示一个局面。考虑到围棋终局时需要统计黑白双方的棋子数量和围空数量,这里选择使用二维的NumPy数组来表示围棋局面,数组元素0表示无子,1表示黑子,2表示白子。下面是我手动输入的一个围棋局面。

import numpy as np
phase = np.array([
    [0,0,2,1,1,0,1,1,1,2,0,2,0,2,1,0,1,0,0],
    [0,0,2,1,0,1,1,1,2,0,2,0,2,2,1,1,1,0,0],
    [0,0,2,1,1,0,0,1,2,2,0,2,0,2,1,0,1,0,0],
    [0,2,1,0,1,1,0,1,2,0,2,2,2,0,2,1,0,1,0],
    [0,2,1,1,0,1,1,2,2,2,2,0,0,2,2,1,0,1,0],
    [0,0,2,1,1,1,1,2,0,2,0,2,0,0,2,1,0,0,0],
    [0,0,2,2,2,2,1,2,2,0,0,0,0,0,2,1,0,0,0],
    [2,2,2,0,0,0,2,1,1,2,0,2,0,0,2,1,0,0,0],
    [1,1,2,0,0,0,2,2,1,2,0,0,0,0,2,1,0,0,0],
    [1,0,1,2,0,2,1,1,1,1,2,2,2,0,2,1,1,1,1],
    [0,1,1,2,0,2,1,0,0,0,1,2,0,2,2,1,0,0,1],
    [1,1,2,2,2,2,2,1,0,0,1,2,2,0,2,1,0,0,0],
    [2,2,0,2,2,0,2,1,0,0,1,2,0,2,2,2,1,0,0],
    [0,2,0,0,0,0,2,1,0,1,1,2,2,0,2,1,0,0,0],
    [0,2,0,0,0,2,1,0,0,1,0,1,1,2,2,1,0,0,0],
    [0,0,2,0,2,2,1,1,1,1,0,1,0,1,1,0,0,0,0],
    [0,2,2,0,2,1,0,0,0,0,1,0,0,0,0,1,1,0,0],
    [0,0,2,0,2,1,0,1,1,0,0,1,0,1,0,1,0,0,0],
    [0,0,0,2,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0]
], dtype=np.ubyte)

2.2 显示一个围棋局面

怎样直观地显示一个围棋局面呢?下面的代码使用Python的内置函数print()就可以在控制台上打印出像照片一样的彩色棋盘。

import os
import numpy as np

os.system('')

def show_phase(phase):
    """显示局面"""
    
    for i in range(19):
        for j in range(19):
            if phase[i,j] == 1: 
                chessman = chr(0x25cf)
            elif phase[i,j] == 2:
                chessman = chr(0x25cb)
            elif phase[i,j] == 9:
                chessman = chr(0x2606)
            else:
                if i == 0:
                    if j == 0:
                        chessman = '%s '%chr(0x250c)
                    elif j == 18:
                        chessman = '%s '%chr(0x2510)
                    else:
                        chessman = '%s '%chr(0x252c)
                elif i == 18:
                    if j == 0:
                        chessman = '%s '%chr(0x2514)
                    elif j == 18:
                        chessman = '%s '%chr(0x2518)
                    else:
                        chessman = '%s '%chr(0x2534)
                elif j == 0:
                    chessman = '%s '%chr(0x251c)
                elif j == 18:
                    chessman = '%s '%chr(0x2524)
                else:
                    chessman = '%s '%chr(0x253c)
            print('\033[0;30;43m' + chessman + '\033[0m', end='')
        print()
    
phase = np.array([
    [0,0,2,1,1,0,1,1,1,2,0,2,0,2,1,0,1,0,0],
    [0,0,2,1,0,1,1,1,2,0,2,0,2,2,1,1,1,0,0],
    [0,0,2,1,1,0,0,1,2,2,0,2,0,2,1,0,1,0,0],
    [0,2,1,0,1,1,0,1,2,0,2,2,2,0,2,1,0,1,0],
    [0,2,1,1,0,1,1,2,2,2,2,0,0,2,2,1,0,1,0],
    [0,0,2,1,1,1,1,2,0,2,0,2,0,0,2,1,0,0,0],
    [0,0,2,2,2,2,1,2,2,0,0,0,0,0,2,1,0,0,0],
    [2,2,2,0,0,0,2,1,1,2,0,2,0,0,2,1,0,0,0],
    [1,1,2,0,0,0,2,2,1,2,0,0,0,0,2,1,0,0,0],
    [1,0,1,2,0,2,1,1,1,1,2,2,2,0,2,1,1,1,1],
    [0,1,1,2,0,2,1,0,0,0,1,2,0,2,2,1,0,0,1],
    [1,1,2,2,2,2,2,1,0,0,1,2,2,0,2,1,0,0,0],
    [2,2,0,2,2,0,2,1,0,0,1,2,0,2,2,2,1,0,0],
    [0,2,0,0,0,0,2,1,0,1,1,2,2,0,2,1,0,0,0],
    [0,2,0,0,0,2,1,0,0,1,0,1,1,2,2,1,0,0,0],
    [0,0,2,0,2,2,1,1,1,1,0,1,0,1,1,0,0,0,0],
    [0,2,2,0,2,1,0,0,0,0,1,0,0,0,0,1,1,0,0],
    [0,0,2,0,2,1,0,1,1,0,0,1,0,1,0,1,0,0,0],
    [0,0,0,2,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0]
], dtype=np.ubyte)

show_phase(phase)

这段代码的运行结果如下:

在这里插入图片描述

2.3 计算黑白双方的棋子和围空

判断一个围棋局面是否为终局,是有一定难度的。为了简化代码,约定提交判决的围棋局面必须是双方死棋、残子均已提清,无任何异议。如此,代码就非常简单了。

import numpy as np

def find_blank(phase, cell):
    """找出包含cell的成片的空格"""
    
    def _find_blank(phase, result, cell):
        i, j = cell
        phase[i,j] = 9
        result['cross'].add(cell)
        
        if i-1 > -1:
            if phase[i-1,j] == 0:
                _find_blank(phase, result, (i-1,j))
            elif phase[i-1,j] == 1:
                result['b_around'].add((i-1,j))
            elif phase[i-1,j] == 2:
                result['w_around'].add((i-1,j))
        if i+1 < 19:
            if phase[i+1,j] == 0:
                _find_blank(phase, result, (i+1,j))
            elif phase[i+1,j] == 1:
                result['b_around'].add((i+1,j))
            elif phase[i+1,j] == 2:
                result['w_around'].add((i+1,j))
        if j-1 > -1:
            if phase[i,j-1] == 0:
                _find_blank(phase, result, (i,j-1))
            elif phase[i,j-1] == 1:
                result['b_around'].add((i,j-1))
            elif phase[i,j-1] == 2:
                result['w_around'].add((i,j-1))
        if j+1 < 19:
            if phase[i,j+1] == 0:
                _find_blank(phase, result, (i,j+1))
            elif phase[i,j+1] == 1:
                result['b_around'].add((i,j+1))
            elif phase[i,j+1] == 2:
                result['w_around'].add((i,j+1))
    
    result = {'cross':set(), 'b_around':set(), 'w_around':set()}
    _find_blank(phase, result, cell)
    
    return result

def find_blanks(phase):
    """找出所有成片的空格"""
    
    blanks = list()
    while True:
        cells = np.where(phase==0)
        if cells[0].size == 0:
            break
        
        blanks.append(find_blank(phase, (cells[0][0], cells[1][0])))
    
    return blanks

def stats(phase):
    """统计结果"""
    
    temp = np.copy(phase)
    for item in find_blanks(np.copy(phase)):
        if len(item['w_around']) == 0:
            v = 3 # 黑空
        elif len(item['b_around']) == 0:
            v = 4 # 白空
        else:
            v = 9 # 单官或公气
        
        for i, j in item['cross']:
            temp[i, j] = v
    
    black = temp[temp==1].size + temp[temp==3].size
    white = temp[temp==2].size + temp[temp==4].size
    common = temp[temp==9].size
    
    return black, white, common

if __name__ == '__main__':
    phase = np.array([
        [0,0,2,1,1,0,1,1,1,2,0,2,0,2,1,0,1,0,0],
        [0,0,2,1,0,1,1,1,2,0,2,0,2,2,1,1,1,0,0],
        [0,0,2,1,1,0,0,1,2,2,0,2,0,2,1,0,1,0,0],
        [0,2,1,0,1,1,0,1,2,0,2,2,2,0,2,1,0,1,0],
        [0,2,1,1,0,1,1,2,2,2,2,0,0,2,2,1,0,1,0],
        [0,0,2,1,1,1,1,2,0,2,0,2,0,0,2,1,0,0,0],
        [0,0,2,2,2,2,1,2,2,0,0,0,0,0,2,1,0,0,0],
        [2,2,2,0,0,0,2,1,1,2,0,2,0,0,2,1,0,0,0],
        [1,1,2,0,0,0,2,2,1,2,0,0,0,0,2,1,0,0,0],
        [1,0,1,2,0,2,1,1,1,1,2,2,2,0,2,1,1,1,1],
        [0,1,1,2,0,2,1,0,0,0,1,2,0,2,2,1,0,0,1],
        [1,1,2,2,2,2,2,1,0,0,1,2,2,0,2,1,0,0,0],
        [2,2,0,2,2,0,2,1,0,0,1,2,0,2,2,2,1,0,0],
        [0,2,0,0,0,0,2,1,0,1,1,2,2,0,2,1,0,0,0],
        [0,2,0,0,0,2,1,0,0,1,0,1,1,2,2,1,0,0,0],
        [0,0,2,0,2,2,1,1,1,1,0,1,0,1,1,0,0,0,0],
        [0,2,2,0,2,1,0,0,0,0,1,0,0,0,0,1,1,0,0],
        [0,0,2,0,2,1,0,1,1,0,0,1,0,1,0,1,0,0,0],
        [0,0,0,2,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0]
    ], dtype=np.ubyte)
    
    show_phase(phase)
    black, white, common = stats(phase)
    print('--------------------------------------')
    print('黑方:%d,白方:%d,公气:%d'%(black, white, common))

代码运行结果如下图所示。

在这里插入图片描述
这段代码在查找成片的空地时使用了递归的方式,这是解决此类问题的有效手段。

3 处理流程

为了便于查看各种数据结构、对象的信息,整个处理流程我放在IDLE中运行。除了标准库,全部代码仅需另外导入PyOpenCV和NumPy模块。

>>> import cv2
>>> import numpy as np

3.1 图像预处理

做视觉识别,通常要在读入图像之后对读入的图像做一些预处理。比如,在边缘检测或直线检测、圆检测之前,要将彩色图像转为灰度图像,如有必要,还要进行滤波降噪处理,甚至是侵蚀、膨胀处理。

下图是一幅围棋局面的照片,整个处理过程就用这张照片来演示。这个局面是随便摆出来的,并不是一个终局。

在这里插入图片描述
下面的代码使用opencv打开这张照片,转为灰度图像,对图像进行滤波降噪处理后做边缘检测。

>>> pic_file = r'D:\temp\go\res\pic_0.jpg'
>>> im_bgr = cv2.imread(pic_file) # 读入图像
>>> im_gray = cv2.cvtColor(im_bgr, cv2.COLOR_BGR2GRAY) # 转灰度
>>> im_gray = cv2.GaussianBlur(im_gray, (3,3), 0) # 滤波降噪
>>> im_edge = cv2.Canny(im_gray, 30, 50) # 边缘检测
>>> cv2.imshow('Go', im_edge) # 显示边缘检测结果

下图是灰度图、滤波降噪后的灰度图和边缘检测的结果。
在这里插入图片描述
看起来杂乱无章,但整个棋盘的边缘还是非常清晰完整的。如果边缘检测效果不佳,可以考虑在边缘检测之前做锐化处理,或者在边缘检测之后做膨胀处理。

3.2 识别并定位棋盘

基于以下约定,我们可以从边缘检测的结果中识别并提取出棋盘的位置信息:

  • 在检测结果中棋盘边缘是清晰完整的
  • 棋盘边缘围成的封闭区域是所有封闭区域中面积最大的

识别棋盘的第一步是使用OpenCV的findContours()函数提取轮廓,该函数返回全部轮廓的列表contours,以及每个轮廓的属性hierarchy。contours中的每一个轮廓由该轮廓的各个点的坐标来描述,而hierarchy则表示每一个轮廓和其他轮廓的相对关系。仅使用轮廓数据列表contours就可以找到棋盘。

>>> contours, hierarchy = cv2.findContours(im_edge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 提取轮廓
>>> rect, area = None, 0 # 找到的最大四边形及其面积
>>> for item in contours:
	hull = cv2.convexHull(item) # 寻找凸包
	epsilon = 0.1 * cv2.arcLength(hull, True) # 忽略弧长10%的点
	approx = cv2.approxPolyDP(hull, epsilon, True) # 将凸包拟合为多边形
	if len(approx) == 4 and cv2.isContourConvex(approx): # 如果是凸四边形
		ps = np.reshape(approx, (4,2))
		ps = ps[np.lexsort((ps[:,0],))]
		lt, lb = ps[:2][np.lexsort((ps[:2,1],))]
		rt, rb = ps[2:][np.lexsort((ps[2:,1],))]
		a = cv2.contourArea(approx) # 计算四边形面积
		if a > area:
			area = a
			rect = (lt, lb, rt, rb)

>>> if rect is None:
	print('在图像文件中找不到棋盘!')
else:
	print('棋盘坐标:')
	print('\t左上角:(%d,%d)'%(rect[0][0],rect[0][1]))
	print('\t左下角:(%d,%d)'%(rect[1][0],rect[1][1]))
	print('\t右上角:(%d,%d)'%(rect[2][0],rect[2][1]))
	print('\t右下角:(%d,%d)'%(rect[3][0],rect[3][1]))

	
棋盘坐标:
	左上角:(111,216)
	左下角:(47,859)
	右上角:(753,204)
	右下角:(823,859)

下面的代码将这4个点用红色的十字标注在原始的彩色图像上。

>>> im = np.copy(im_bgr)
>>> for p in rect:
	im = cv2.line(im, (p[0]-10,p[1]), (p[0]+10,p[1]), (0,0,255), 1)
	im = cv2.line(im, (p[0],p[1]-10), (p[0],p[1]+10), (0,0,255), 1)
	
>>> cv2.imshow('go', im)

用红色十字标注的棋盘四角如下图所示。
在这里插入图片描述
很显然,因为拍摄的角度问题,大多数情况下棋盘并不是一个矩形,因此需要对棋盘做透视矫正,使其看起来更像一块真正的棋盘。

3.3 透视矫正

假定透视变换后生成的棋盘大小为640x640像素,加上四边各有10个像素的空白,图像分辨率为660x660像素。

>>> lt, lb, rt, rb = rect
>>> pts1 = np.float32([(10,10), (10,650), (650,10), (650,650)]) # 预期的棋盘四个角的坐标
>>> pts2 = np.float32([lt, lb, rt, rb]) # 当前找到的棋盘四个角的坐标
>>> m = cv2.getPerspectiveTransform(pts2, pts1) # 生成透视矩阵
>>> board_gray = cv2.warpPerspective(im_gray, m, (660, 660)) # 对灰度图执行透视变换
>>> board_bgr = cv2.warpPerspective(im_bgr, m, (660, 660)) # 对彩色图执行透视变换
>>> cv2.imshow('go', board_gray)

下图是经过透视矫正后的棋盘。
在这里插入图片描述
现在,我们找到了棋盘的四个角,还需要进一步确定棋盘上每一个交叉格点的坐标,也就是定位棋盘格子。

3.4 定位棋盘格子

透视矫正后的棋盘是边长为660像素的正方形、且棋盘格子一定位于棋盘中心,基于这个条件,我们可以简化定位棋盘格子的思路如下:

  1. 借助于圆检测,找出棋盘上的每一颗棋子,但要尽量避免找到实际并不存在的棋子
  2. 对全部棋子的x坐标排序,找出最左侧和最右侧的两列棋子,分别计算其x坐标排序的平均值x_min和x_max
  3. 对全部棋子的y坐标排序,找出最上方和最下方的两行棋子,分别计算其y坐标排序的平均值y_min和y_max
  4. 考察x和y坐标极值差,最接近600者,为棋盘格子的宽度和高度
  5. 在分辨率为660x660像素的图像上计算出19x19的网格四角的坐标
  6. 将棋盘映射到620x620像素的图像上,网格四角的坐标分别是(22, 22)、 (22, 598)、 (598, 22)和 (598, 598),网格的水平和垂直间距都是32像素
>>> circles = cv2.HoughCircles(board_gray, cv2.HOUGH_GRADIENT, 1, 20, param1=90, param2=16, minRadius=10, maxRadius=20) # 圆检测
>>> xs = circles[0,:,0] # 所有棋子的x坐标
>>> ys = circles[0,:,1] # 所有棋子的y坐标
>>> xs.sort()
>>> ys.sort()
>>> k = 1
>>> while xs[k]-xs[:k].mean() < 15:
	k += 1
	
>>> x_min = int(round(xs[:k].mean()))
>>> k = 1
>>> while ys[k]-ys[:k].mean() < 15:
	k += 1
	
>>> y_min = int(round(ys[:k].mean()))
>>> k = -1
>>> while xs[k:].mean() - xs[k-1] < 15:
	k -= 1
	
>>> x_max = int(round(xs[k:].mean()))
>>> k = -1
>>> while ys[k:].mean() - ys[k-1] < 15:
	k -= 1

>>> y_max = int(round(ys[k:].mean()))
>>> x_min, x_max, y_min, y_max
(32, 629, 29, 622)
>>> if abs(600-(x_max-x_min)) < abs(600-(y_max-y_min)):
	v_min, v_max = x_min, x_max
else:
	v_min, v_max = y_min, y_max
	
>>> v_min, v_max
(32, 629)
>>> lt = (v_min, v_min) # 棋盘网格左上角
>>> lb = (v_min, v_max) # 棋盘网格左下角
>>> rt = (v_max, v_min) # 棋盘网格右上角
>>> rb = (v_max, v_max) # 棋盘网格右下角
>>> pts1 = np.float32([[22, 22], [22, 598], [598, 22], [598, 598]])  # 棋盘四个角点的最终位置
>>> pts2 = np.float32([lt, lb, rt, rb])
>>> m = cv2.getPerspectiveTransform(pts2, pts1)
>>> board_gray = cv2.warpPerspective(board_gray, m, (620, 620))
>>> board_bgr = cv2.warpPerspective(board_bgr, m, (620, 620))
>>> cv2.imshow('go', board_gray)
>>> im = np.copy(board_bgr)
>>> series = np.linspace(22, 598, 19, dtype=np.int)
>>> for i in series:
	im = cv2.line(im, (22, i), (598, i), (0,255,0), 1)
	im = cv2.line(im, (i, 22), (i, 598), (0,255,0), 1)
	
>>> cv2.imshow('go', im)

定位棋盘格子之后的效果如下图所示。

在这里插入图片描述
确定了棋盘上每一个格子的位置,接下来就是识别无子、黑子、白子三种状态了。

3.5 识别棋子及其颜色

识别棋子之前,再次做圆检测。和上次的圆检测不同,这次要尽可能找到所有的棋子,即使找出来并不存在的棋子也没关系。另一个准备工作是将彩色图像的BGR模式转换为HSV,以便在判断颜色时可以消除明暗、色温等干扰因素。

识别过程就是遍历每一个检测到的圆,在圆心处取10x10像素的图像,计算S通道和H通道的均值,并判断是否存在棋子以及棋子的颜色。代码中的阈值为经验值。识别棋子颜色,亦可考虑使用机器学习的方式实现。

>>> mesh = np.linspace(22, 598, 19, dtype=np.int)
>>> rows, cols = np.meshgrid(mesh, mesh)
>>> circles = cv2.HoughCircles(board_gray, cv2.HOUGH_GRADIENT, 1, 20, param1=40, param2=10, minRadius=12, maxRadius=18) # 再做一次圆检测
>>> circles = np.uint32(np.around(circles[0]))
>>> phase = np.zeros_like(rows, dtype=np.uint8)
>>> im_hsv = cv2.cvtColor(board_bgr, cv2.COLOR_BGR2HSV_FULL)
>>> for circle in circles:
	row = int(round((circle[1]-22)/32))
	col = int(round((circle[0]-22)/32))
	hsv_ = im_hsv[cols[row,col]-5:cols[row,col]+5, rows[row,col]-5:rows[row,col]+5]
	s = np.mean(hsv_[:,:,1])
	v = np.mean(hsv_[:,:,2])
	if 0 < v < 115:
		phase[row,col] = 1 # 黑棋
	elif 0 < s < 50 and 114 < v < 256:
		phase[row,col] = 2 # 白棋

		
>>> phase
array([[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1],
       [0, 1, 1, 1, 2, 2, 2, 1, 2, 1, 1, 2, 0, 2, 1, 2, 1, 1, 1],
       [0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 1, 0],
       [1, 1, 1, 2, 2, 0, 2, 0, 0, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0],
       [1, 2, 2, 1, 1, 2, 2, 2, 0, 0, 2, 2, 1, 0, 1, 0, 0, 0, 0],
       [2, 2, 1, 1, 1, 2, 1, 1, 2, 2, 1, 1, 0, 1, 0, 1, 1, 0, 0],
       [0, 2, 2, 1, 0, 1, 1, 0, 1, 2, 2, 1, 0, 0, 1, 1, 1, 1, 0],
       [0, 0, 2, 1, 1, 2, 1, 1, 1, 2, 1, 1, 0, 1, 2, 2, 1, 0, 1],
       [0, 2, 1, 1, 1, 2, 2, 1, 1, 1, 2, 1, 0, 1, 0, 2, 2, 0, 0],
       [0, 0, 2, 2, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 0, 0, 0],
       [0, 2, 2, 1, 1, 2, 2, 0, 2, 2, 2, 2, 1, 1, 2, 1, 0, 2, 1],
       [2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 1, 0, 1],
       [1, 2, 1, 1, 2, 2, 2, 1, 0, 1, 2, 2, 1, 2, 2, 2, 1, 1, 0],
       [1, 1, 0, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 2, 0],
       [0, 0, 1, 2, 1, 2, 2, 2, 1, 0, 1, 2, 1, 2, 1, 1, 1, 2, 1],
       [1, 1, 2, 2, 2, 2, 0, 2, 2, 0, 2, 2, 1, 2, 2, 2, 1, 2, 1],
       [1, 1, 2, 0, 0, 1, 2, 2, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 0],
       [1, 2, 2, 0, 0, 2, 2, 0, 2, 1, 1, 2, 1, 1, 1, 0, 0, 1, 1],
       [2, 2, 2, 0, 0, 0, 0, 2, 2, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0]],
      dtype=uint8)

4 源码文件

4.1 统计棋子和围空数量的脚本文件

stats.py

# -*- coding: utf-8 -*-

"""
根据围棋局面,计算对局结果

使用NumPy二维ubyte数组存储局面,约定:
0 - 空
1 - 黑子
2 - 白子
3 - 黑空
4 - 白空
5 - 黑子统计标识
6 - 白子统计标识
7 - 黑残子
8 - 白残子
9 - 单官/公气/统计标识
"""

import numpy as np
import os

os.system('')

def show_phase(phase):
    """显示局面"""
    
    for i in range(19):
        for j in range(19):
            if phase[i,j] == 1: 
                chessman = chr(0x25cf)
            elif phase[i,j] == 2:
                chessman = chr(0x25cb)
            elif phase[i,j] == 9:
                chessman = chr(0x2606)
            else:
                if i == 0:
                    if j == 0:
                        chessman = '%s '%chr(0x250c)
                    elif j == 18:
                        chessman = '%s '%chr(0x2510)
                    else:
                        chessman = '%s '%chr(0x252c)
                elif i == 18:
                    if j == 0:
                        chessman = '%s '%chr(0x2514)
                    elif j == 18:
                        chessman = '%s '%chr(0x2518)
                    else:
                        chessman = '%s '%chr(0x2534)
                elif j == 0:
                    chessman = '%s '%chr(0x251c)
                elif j == 18:
                    chessman = '%s '%chr(0x2524)
                else:
                    chessman = '%s '%chr(0x253c)
            print('\033[0;30;43m' + chessman + '\033[0m', end='')
        print()
        
def find_blank(phase, cell):
    """找出包含cell的成片的空格"""
    
    def _find_blank(phase, result, cell):
        i, j = cell
        phase[i,j] = 9
        result['cross'].add(cell)
        
        if i-1 > -1:
            if phase[i-1,j] == 0:
                _find_blank(phase, result, (i-1,j))
            elif phase[i-1,j] == 1:
                result['b_around'].add((i-1,j))
            elif phase[i-1,j] == 2:
                result['w_around'].add((i-1,j))
        if i+1 < 19:
            if phase[i+1,j] == 0:
                _find_blank(phase, result, (i+1,j))
            elif phase[i+1,j] == 1:
                result['b_around'].add((i+1,j))
            elif phase[i+1,j] == 2:
                result['w_around'].add((i+1,j))
        if j-1 > -1:
            if phase[i,j-1] == 0:
                _find_blank(phase, result, (i,j-1))
            elif phase[i,j-1] == 1:
                result['b_around'].add((i,j-1))
            elif phase[i,j-1] == 2:
                result['w_around'].add((i,j-1))
        if j+1 < 19:
            if phase[i,j+1] == 0:
                _find_blank(phase, result, (i,j+1))
            elif phase[i,j+1] == 1:
                result['b_around'].add((i,j+1))
            elif phase[i,j+1] == 2:
                result['w_around'].add((i,j+1))
    
    result = {'cross':set(), 'b_around':set(), 'w_around':set()}
    _find_blank(phase, result, cell)
    
    return result

def find_blanks(phase):
    """找出所有成片的空格"""
    
    blanks = list()
    while True:
        cells = np.where(phase==0)
        if cells[0].size == 0:
            break
        
        blanks.append(find_blank(phase, (cells[0][0], cells[1][0])))
    
    return blanks

def stats(phase):
    """统计结果"""
    
    temp = np.copy(phase)
    for item in find_blanks(np.copy(phase)):
        if len(item['w_around']) == 0:
            v = 3 # 黑空
        elif len(item['b_around']) == 0:
            v = 4 # 白空
        else:
            v = 9 # 单官或公气
        
        for i, j in item['cross']:
            temp[i, j] = v
    
    black = temp[temp==1].size + temp[temp==3].size
    white = temp[temp==2].size + temp[temp==4].size
    common = temp[temp==9].size
    
    return black, white, common

if __name__ == '__main__':
    phase = np.array([
        [0,0,2,1,1,0,1,1,1,2,0,2,0,2,1,0,1,0,0],
        [0,0,2,1,0,1,1,1,2,0,2,0,2,2,1,1,1,0,0],
        [0,0,2,1,1,0,0,1,2,2,0,2,0,2,1,0,1,0,0],
        [0,2,1,0,1,1,0,1,2,0,2,2,2,0,2,1,0,1,0],
        [0,2,1,1,0,1,1,2,2,2,2,0,0,2,2,1,0,1,0],
        [0,0,2,1,1,1,1,2,0,2,0,2,0,0,2,1,0,0,0],
        [0,0,2,2,2,2,1,2,2,0,0,0,0,0,2,1,0,0,0],
        [2,2,2,0,0,0,2,1,1,2,0,2,0,0,2,1,0,0,0],
        [1,1,2,0,0,0,2,2,1,2,0,0,0,0,2,1,0,0,0],
        [1,0,1,2,0,2,1,1,1,1,2,2,2,0,2,1,1,1,1],
        [0,1,1,2,0,2,1,0,0,0,1,2,0,2,2,1,0,0,1],
        [1,1,2,2,2,2,2,1,0,0,1,2,2,0,2,1,0,0,0],
        [2,2,0,2,2,0,2,1,0,0,1,2,0,2,2,2,1,0,0],
        [0,2,0,0,0,0,2,1,0,1,1,2,2,0,2,1,0,0,0],
        [0,2,0,0,0,2,1,0,0,1,0,1,1,2,2,1,0,0,0],
        [0,0,2,0,2,2,1,1,1,1,0,1,0,1,1,0,0,0,0],
        [0,2,2,0,2,1,0,0,0,0,1,0,0,0,0,1,1,0,0],
        [0,0,2,0,2,1,0,1,1,0,0,1,0,1,0,1,0,0,0],
        [0,0,0,2,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0]
    ], dtype=np.ubyte)
    
    show_phase(phase)
    black, white, common = stats(phase)
    print('--------------------------------------')
    print('黑方:%d,白方:%d,公气:%d'%(black, white, common))

4.2 视觉识别的脚本文件

go.py

# -*- coding: utf-8 -*-

"""
识别图像中的围棋局面
"""

import cv2
import numpy as np

from stats import show_phase, stats


class GoPhase:
    """从图片中识别围棋局面"""
    
    def __init__(self, pic_file, offset=3.75):
        """构造函数,读取图像文件,预处理"""
        
        self.offset = offset # 黑方贴七目半
        self.im_bgr = cv2.imread(pic_file) # 原始的彩色图像文件,BGR模式
        self.im_gray = cv2.cvtColor(self.im_bgr, cv2.COLOR_BGR2GRAY) # 转灰度图像
        self.im_gray = cv2.GaussianBlur(self.im_gray, (3,3), 0) # 灰度图像滤波降噪
        self.im_edge = cv2.Canny(self.im_gray, 30, 50) # 边缘检测获得边缘图像
        
        self.board_gray = None # 棋盘灰度图
        self.board_bgr = None # 棋盘彩色图
        self.rect = None # 棋盘四个角的坐标,顺序为lt/lb/rt/rb
        self.phase = None # 用以表示围棋局面的二维数组
        self.result = None # 对弈结果
        
        self._find_chessboard() # 找到棋盘
        self._location_grid() # 定位棋盘格子
        self._identify_chessman() # 识别棋子
        self._stats() # 统计黑白双方棋子和围空
        
    def _find_chessboard(self):
        """找到棋盘"""
        
        contours, hierarchy = cv2.findContours(self.im_edge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 提取轮廓
        area = 0 # 找到的最大四边形及其面积
        for item in contours:
            hull = cv2.convexHull(item) # 寻找凸包
            epsilon = 0.1 * cv2.arcLength(hull, True) # 忽略弧长10%的点
            approx = cv2.approxPolyDP(hull, epsilon, True) # 将凸包拟合为多边形

            if len(approx) == 4 and cv2.isContourConvex(approx): # 如果是凸四边形
                ps = np.reshape(approx, (4,2)) # 四个角的坐标
                ps = ps[np.lexsort((ps[:,0],))] # 排序区分左右
                lt, lb = ps[:2][np.lexsort((ps[:2,1],))] # 排序区分上下
                rt, rb = ps[2:][np.lexsort((ps[2:,1],))] # 排序区分上下
                
                a = cv2.contourArea(approx)
                if a > area:
                    area = a
                    self.rect = (lt, lb, rt, rb)
        
        if not self.rect is None:
            pts1 = np.float32([(10,10), (10,650), (650,10), (650,650)]) # 预期的棋盘四个角的坐标
            pts2 = np.float32(self.rect) # 当前找到的棋盘四个角的坐标
            m = cv2.getPerspectiveTransform(pts2, pts1) # 生成透视矩阵
            self.board_gray = cv2.warpPerspective(self.im_gray, m, (660, 660)) # 执行透视变换
            self.board_bgr = cv2.warpPerspective(self.im_bgr, m, (660, 660)) # 执行透视变换
        
    def _location_grid(self):
        """定位棋盘格子"""
        
        if self.board_gray is None:
            return
        
        circles = cv2.HoughCircles(self.board_gray, cv2.HOUGH_GRADIENT, 1, 20, param1=90, param2=16, minRadius=10, maxRadius=20) # 圆检测
        xs, ys = circles[0,:,0], circles[0,:,1] # 所有棋子的x坐标和y坐标
        xs.sort()
        ys.sort()
        
        k = 1
        while xs[k]-xs[:k].mean() < 15:
            k += 1
        x_min = int(round(xs[:k].mean()))
        
        k = 1
        while ys[k]-ys[:k].mean() < 15:
            k += 1
        
        y_min = int(round(ys[:k].mean()))
        
        k = -1
        while xs[k:].mean() - xs[k-1] < 15:
            k -= 1
        x_max = int(round(xs[k:].mean()))
        
        k = -1
        while ys[k:].mean() - ys[k-1] < 15:
            k -= 1
        y_max = int(round(ys[k:].mean()))
        
        if abs(600-(x_max-x_min)) < abs(600-(y_max-y_min)):
            v_min, v_max = x_min, x_max
        else:
            v_min, v_max = y_min, y_max
            
        pts1 = np.float32([[22, 22], [22, 598], [598, 22], [598, 598]])  # 棋盘四个角点的最终位置
        pts2 = np.float32([(v_min, v_min), (v_min, v_max), (v_max, v_min), (v_max, v_max)])
        m = cv2.getPerspectiveTransform(pts2, pts1)
        self.board_gray = cv2.warpPerspective(self.board_gray, m, (620, 620))
        self.board_bgr = cv2.warpPerspective(self.board_bgr, m, (620, 620))
        
    def _identify_chessman(self):
        """识别棋子"""
        
        if self.board_gray is None:
            return
        
        mesh = np.linspace(22, 598, 19, dtype=np.int)
        rows, cols = np.meshgrid(mesh, mesh)

        circles = cv2.HoughCircles(self.board_gray, cv2.HOUGH_GRADIENT, 1, 20, param1=40, param2=10, minRadius=12, maxRadius=18)
        circles = np.uint32(np.around(circles[0]))
        
        self.phase = np.zeros_like(rows, dtype=np.uint8)
        im_hsv = cv2.cvtColor(self.board_bgr, cv2.COLOR_BGR2HSV_FULL)
        
        for circle in circles:
            row = int(round((circle[1]-22)/32))
            col = int(round((circle[0]-22)/32))
            
            hsv_ = im_hsv[cols[row,col]-5:cols[row,col]+5, rows[row,col]-5:rows[row,col]+5]
            s = np.mean(hsv_[:,:,1])
            v = np.mean(hsv_[:,:,2])

            if 0 < v < 115:
                self.phase[row,col] = 1 # 黑棋
            elif 0 < s < 50 and 114 < v < 256:
                self.phase[row,col] = 2 # 白棋
        
    def _stats(self):
        """统计黑白双方棋子和围空"""
        
        self.result = stats(self.phase)
        
    def show_image(self, name='gray', win="GoPhase"):
        """显示图像"""
        
        if name == 'bgr':
            im = self.board_bgr
        elif name == 'gray':
            im = self.board_gray
        else:
            im = self.im_bgr
        
        
        if im is None:
            print('识别失败,无图像可供显示')
        else:
            cv2.imshow(win, im)
            cv2.waitKey(0)
            cv2.destroyAllWindows()
        
    def show_phase(self):
        """显示局面"""
        
        if self.phase is None:
            print('识别失败,无围棋局面可供显示')
        else:
            show_phase(self.phase)
        
    def show_result(self):
        """显示结果"""
        
        if self.result is None:
            print('识别失败,无对弈结果可供显示')
        else:
            black, white, common = self.result
            B = black+common/2-self.offset
            W = white+common/2+self.offset
            result = '黑胜' if B > W else '白胜'
            
            print('黑方:%0.1f,白方:%0.1f,%s'%(B, W, result))
        

if __name__ == '__main__':
    go = GoPhase('res/pic_0.jpg')
    go.show_image('origin')
    go.show_image('gray')
    go.show_phase()
    go.show_result()
<p> <span><span style="color:#337FE5;">【为什么前端都要学习Vue<span style="color:#337FE5;">】</span></span></span> </p> <span> </span> <p style="color:#666666;"> <span style="color:#1A1A1A;">这几年Vue.js成为前端框架中最火一个。越来越多网站前端开始采用Vue.js开发。是开源世界华人骄傲,作者是我国尤雨溪大神。相对于其他前端框架,<span style="color:#1A1A1A;">Vue 更容易上手!正因为它简单易学,很多前端开发工程师可以很快掌握并且应用到实际开发中。如果说你想用最短时间来学习一个框架,快速上手项目,Vue是不二之选。</span></span> </p> <p style="color:#666666;"> <span style="color:#1A1A1A;"><span style="color:#1A1A1A;"><br /> </span></span> </p> <p style="color:#666666;"> <span style="color:#337FE5;">【学员收益】</span> </p> <p style="color:#666666;"> <span>1)大部分学员想要学习Vue,但是无奈缺少一个好老师,</span><span>董老师将手把手带领你学习,让你彻底掌握Vue框架。</span> </p> <p style="color:#666666;"> <span>2)课程将会长期维护,<span style="color:#666666;">内容更超值,</span>本课程基于最新版本进行讲解,并且老师会更新升级</span><span>到3.0稳定版本。</span> </p> <p> <span>3)学完该课程后不仅能学到Vue设计和开发技能,还能培养市场思维、用户思维、设计思维,并能够利用掌握技术开发Vue项目,获取额外收益。<br /> </span><span></span> </p> <p> <span><br /> </span> </p> <p style="color:#666666;"> <span><span style="color:#337FE5;">【课程收获】</span></span> </p> <p> <span><span style="color:#666666;">1、</span>从基础知识到项目实战,内容涵盖Vue各个层面知识和技巧<br /> <span style="color:#666666;">2、</span>学习曲线平缓,前端新人也可以看得懂<br /> 3、贴近企业项目,按照企业级代码标准和工程开发流程进行讲解<br /> 4、让你能够独立开发高颜值项目<br /> </span> </p> <p> <span>5、项目涉及14大功能组件,从基础组件到业务组件,一站式全掌握</span> </p> <p> <span><img src="https://img-bss.csdnimg.cn/202007160700105241.png" alt="" /><br /> </span> </p> <p> <img src="https://img-bss.csdnimg.cn/202007160649295686.png" alt="" /> </p> <p style="color:#666666;"> <span><span style="color:#337FE5;">【项目效果】</span></span> </p> <p> 本课程打造是高颜值美团外卖项目。不仅界面美观,而且涉及到了众多页面。多说无益,请大家扫码查看课程效果。<img src="https://img-bss.csdnimg.cn/202007081115567138.png" alt="" /> </p> <p> <img src="https://img-bss.csdnimg.cn/202007161118172782.png" alt="" /> </p> <p> <img src="https://img-bss.csdnimg.cn/202007161118403086.png" alt="" /> </p>
相关推荐
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付 15.20元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值