作者 | 天元浪子  责编 | 张文

头图 | CSDN 下载自视觉中国

出品 | CSDN(ID:CSDNnews)

前言

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

谈到视觉识别,一定离不开 OpenCV,而 OpenCV 的图像数据结构,就是 NumPy 的多维数组(numpy.ndarray)。在后面的讲解中,我会穿插一些 OpenCV 的基本概念和技术细节,但不会对 NumPy 做过多介绍。

准备工作

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

标准的 19 路围棋盘上有 361 个交叉点可以落子,每个交叉点有三种状态:无子、黑子、白子,不难算出围棋共有 3 的 361 次方种不同的局面。

对于围棋局面,用什么样的数据结构来表示最合理呢?

我以前写过象棋和国际跳棋的代码,对于这两种棋,是用字符串来表示一个局面。考虑到围棋终局时需要统计黑白双方的棋子数量和围空数量,这里选择使用二维的 NumPy 数组来表示围棋局面,数组元素 0 表示无子,1 表示黑子,2 表示白子。下面是我手动输入的一个围棋局面。

import numpy as npphase = 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 osimport 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:            breakblanks.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] = vblack = temp[temp==1].size + temp[temp==3].size    white = temp[temp==2].size + temp[temp==4].size    common = temp[temp==9].sizereturn 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))

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

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

处理流程

为了便于查看各种数据结构、对象的信息,整个处理流程我放在 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 像素的正方形、且棋盘格子一定位于棋盘中心,基于这个条件,我们可以简化定位棋盘格子的思路如下:

  • 借助于圆检测,找出棋盘上的每一颗棋子,但要尽量避免找到实际并不存在的棋子

  • 对全部棋子的 x 坐标排序,找出最左侧和最右侧的两列棋子,分别计算其x坐标排序的平均值 x_min 和 x_max

  • 对全部棋子的 y 坐标排序,找出最上方和最下方的两行棋子,分别计算其y坐标排序的平均值 y_min 和 y_max

  • 考察 x 和 y 坐标极值差,最接近 600 者,为棋盘格子的宽度和高度

  • 在分辨率为 660x660 像素的图像上计算出 19x19 的网格四角的坐标

  • 将棋盘映射到 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_maxelse:  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 # 白棋>>> phasearray([[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.1 统计棋子和围空数量的脚本文件

stats.py

# -*- coding: utf-8 -*-
"""根据围棋局面,计算对局结果
使用NumPy二维ubyte数组存储局面,约定:0 - 空1 - 黑子2 - 白子3 - 黑空4 - 白空5 - 黑子统计标识6 - 白子统计标识7 - 黑残子8 - 白残子9 - 单官/公气/统计标识"""
import numpy as npimport 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:            breakblanks.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] = vblack = temp[temp==1].size + temp[temp==3].size    white = temp[temp==2].size + temp[temp==4].size    common = temp[temp==9].sizereturn 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 cv2import numpy as np
from stats import show_phase, statsclass 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:            returncircles = 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 += 1y_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_maxpts1 = 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:            returnmesh = 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_bgrif 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()更多精彩推荐
☞Linux 能否拿下苹果 M1 阵地?
☞Firefox 终于对退格键“下手”了!
☞25 款软件上榜,2020“最佳开源奖” 出炉!
☞量子计算还没搞懂,光子计算又要来统治世界?
☞程序员为教师妻子开发专属应用;2020 最佳开源项目出炉;中国构建全星地量子通信网|开发者周刊☞“干掉”程序员饭碗后,OpenAI 又对艺术家下手了!
☞开考!狮子,老虎,企鹅,技术圈的这些飞禽走兽你认识多少?
点分享点收藏点点赞点在看

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

  1. TensorFlow深度学习算法原理与编程实战 人工智能机器学习技术丛书

    作者:蒋子阳 著 出版社:中国水利水电出版社 品牌:智博尚书 出版时间:2019-01-01 TensorFlow深度学习算法原理与编程实战 人工智能机器学习技术丛书 ISBN:97875170682 ...

  2. 大规模分布式存储系统:原理解析与架构实战 (大数据技术丛书) - 电子书下载 -(百度网盘 高清版PDF格式)...

    大规模分布式存储系统:原理解析与架构实战 (大数据技术丛书)-杨传辉 在线阅读                   百度网盘下载(89hy) 书名:大规模分布式存储系统:原理解析与架构实战 (大数据技 ...

  3. CoreOS容器云企业实战(3)--Docker技术实践

    0x1 Docker概述 1)Docker介绍 Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源. Docker 可以让开发者打包他们的应用以及依赖包到一 ...

  4. 实战 | 基于 Serverless 技术的视频截帧架构如何实现?

    前言 视频直播是一种创新的在线娱乐形式,具有多人实时交互特性,在电商.游戏.在线教育.娱乐等多个行业都有着非常广泛的应用.随着网络基础设施的不断改善以及社交娱乐需求的不断增长,视频直播在持续渗透进大家 ...

  5. Keras蚂蚁金服大赛实战——自然语言处理技术

    之前在自然语言处理技术系列的第一篇NER实战的结语中介绍过:序列标注(分词,NER),文本分类(情感分析),句子关系判断(语意相似判断),句子生成(机器翻译)是NLP领域的四大任务,之后我又陆续简单介 ...

  6. 大数据开发实战:数据仓库技术

    1.OLTP和OLAP OLTP的全称是 Online Transaction Processing, OLTP主要用传统的关系型数据库来进行事务处理.OLTP最核心的需求是单条记录的高效快速处理,索 ...

  7. 乾坤 微前端_最全汇总之微前端知识和实战(EMP技术方案)

    我们团队在早早聊的B站直播间分享了EMP微前端---团队半年以来的技术果实.分享的内容全在这里,会讲述微前端的由来,解决的问题,以及EMP微前端方案的不同之处,更有四个实战项目的总结,欢迎大家一起探讨 ...

  8. 软件测试实战(微软技术专家经验总结)--第九、十章(团队工作、个人管理)读书笔记

    第九章 团队管理 本章目标是一线的测试人员,分析测试人员如何面对项目管理的一些挑战. 9.1工作风格 9.1.1测试人员通过服务团队来体现自己的价值 首先,测试人员应该设定正确的工作目标.为了设定工作 ...

  9. 【MDCC 2016】iOS开发峰会回顾:实战Coding演示 技术大牛带你起飞

    9月23日-24日,由CSDN.创新工场联合主办的MDCC 2016中国移动开发者大会(Mobile Developer Conference China)在北京·国家会议中心隆重举行.本次大会以移动 ...

最新文章

  1. 下载Android源码流程(完整版)
  2. Vue中使用Openlayers加载Geoserver发布的TileWMS时单击获取shp文件的坐标信息
  3. linux rpm 没有返回,容易忘记的linux命令之rpm
  4. 二叉排序树与文件操作的设计与实现_堆排序就这么简单
  5. ubuntu16.04安装zabbix-server3.4
  6. apache的server-status如何分析的技术说明
  7. Miller Robbin测试模板(无讲解)
  8. OpenCV 利用MFC的Picture控件显示和处理图像
  9. 前端每日实战:98# 视频演示如何用纯 CSS 创作一只愤怒小鸟中的绿猪
  10. SQL进阶:数据中间表,多表取身份证号-整理-匹配多表-合并整理
  11. 半连续性:上半连续与下半连续
  12. 病毒RNA分离:病毒RNA提取试剂盒方案
  13. URL中 # ? 是什么意思
  14. 非标自动化设备设计制造的13个步骤 || 技巧总结
  15. 彻底掌握基于HTTP网络层的 “前端性能优化“
  16. matlab求近似解,matlab求近似解
  17. conemu 打开wsl 报错
  18. 【CCF会议期刊推荐】中国计算机协会(CCF)推荐计算领域高质量科技期刊分级目录(T1类)
  19. 七星彩长奖表图_够力七星彩奖表长条图最新版
  20. 博客园实时同步更新【阅读感受更佳】

热门文章

  1. 敏捷开发般若敏捷系列之五:如何推广敏捷(中)(无寿者,回报,破我执)...
  2. CSS学习笔记----选择器与字体(字系)
  3. 浅析SVM中的对偶问题
  4. 技术文档(12)-- Linux权限总结
  5. ipython介绍及使用
  6. 获得驱动器信息卷设备Ring3得到磁盘文件系统(NTFS WIN10)
  7. http session 基础知识
  8. 结对编程——paperOne基于java的四则运算 功能改进
  9. margin和padding的四种写法
  10. 《Linux编程》学习笔记 ·002【Linux常用工具GCC、GDB、Make】