基于OpenCV的围棋终局胜负自动判定系统开发实战

发布时间:2026/8/2 9:52:52

基于OpenCV的围棋终局胜负自动判定系统开发实战 1. 项目概述当围棋遇上计算机视觉作为一名在计算机视觉领域摸爬滚打了多年的开发者我最近完成了一个特别有意思的业余项目用Python和OpenCV也就是PyOpenCV做了一个围棋终局胜负判定的工具。这个想法的起因很简单我和朋友在网上下棋有时会因为终局数目产生分歧尤其是当棋盘上棋子密密麻麻空点目和死子纠缠不清时手动数目既耗时又容易出错。我就想能不能让电脑来干这个“裁判”的活儿这个项目的核心就是利用计算机视觉技术让程序“看懂”一张围棋终局的照片或截图自动识别出棋盘、棋子并最终计算出黑棋和白棋各自的目数从而判定胜负。听起来像是图像处理的基础应用但真做起来里面涉及到的细节和坑点可不少。它不仅仅是调用几个OpenCV函数那么简单更需要你对围棋规则有清晰的理解并将这些规则转化为图像算法可以执行的逻辑。对于想入门计算机视觉实战的朋友来说这个项目是个绝佳的练手机会。它涵盖了图像预处理、轮廓检测、形态学操作、颜色空间转换、逻辑判断等多个经典CV模块。而对于围棋爱好者而言这或许能帮你解决一个实实在在的痛点。接下来我就把自己从零搭建这个项目的完整思路、关键步骤、踩过的坑以及最终解决方案毫无保留地分享出来。2. 核心思路与方案设计2.1 问题拆解从图像到胜负的路径要实现“看图判胜负”我们需要把整个问题分解成一系列可执行的子任务。这就像下棋一样要有清晰的步骤。首先输入是一张包含完整围棋棋盘的图像。这张图可能来自手机拍摄的实体棋盘也可能是围棋软件的对局截图。图像质量参差不齐可能有透视畸变拍摄角度不正、光照不均、背景杂乱、棋子反光等问题。我们的目标是输出一个明确的胜负结果比如“黑胜3.5目”。那么从A点到B点我们需要经过哪些关键站点呢我将其梳理为以下四个核心阶段棋盘定位与矫正无论图片怎么拍首先要找到棋盘在图像中的精确位置并将其矫正为一个标准的“正方形”俯视图。这是所有后续处理的基础如果这一步歪了后面全错。棋子检测与识别在矫正后的棋盘图像上找出所有棋子的位置并判断每个位置上是黑子、白子还是空点。死子判定与目数计算这是围棋规则的核心。并非所有被围住的空点都算目也并非所有棋子都是活棋。程序需要根据围棋规则区分出“活棋”、“死子”以及属于各方的“目”地盘。胜负判定与输出根据中国规则数子法或日韩规则数目法结合贴目贴子设置计算出最终胜负结果。2.2 技术选型为什么是OpenCV在这个项目中我选择了Python OpenCV的组合这是经过深思熟虑的。OpenCV (Open Source Computer Vision Library)这是一个功能极其强大且成熟的计算机视觉开源库。它提供了从最基本的图像读写、色彩转换到高级的特征检测、机器学习接口等几乎所有你需要的工具。对于棋盘检测、轮廓查找、透视变换这些任务OpenCV有现成的、高度优化的函数比如cv2.findContours,cv2.warpPerspective能让我们免于重复造轮子。Python作为胶水语言Python的语法简洁开发效率高。它拥有庞大的科学计算生态如NumPy OpenCV本身也依赖它进行数组操作非常适合进行算法原型快速开发和验证。后期如果需要集成更复杂的规则引擎或用户界面Python也有丰富的框架支持。为什么不直接用现成的深度学习模型如YOLO检测棋子对于这个特定问题传统图像处理的方法在大多数场景下已经足够可靠、快速且不需要大量的标注数据和训练成本。深度学习方法更适合处理极端光照、严重形变或非常规棋盘样式等复杂情况可以作为后续优化的方向但并非本项目初版的核心需求。2.3 方案设计中的关键决策在动手写代码之前有几个设计层面的决策至关重要规则选择围棋规则有中国规则、日韩规则等主要区别在于计算胜负的方式数子 vs 数目和贴目值。我选择了更常见的中国数子法作为基础因为它逻辑上更直接最终比较的是棋盘上活子与围得的空点的总和。程序中需要预留一个贴目贴子的设置接口通常设为3.75子相当于7.5目。输入假设为了降低初版复杂度我对输入图像做了一些合理假设棋盘是标准的19路棋盘19x19条线。棋盘颜色与棋子颜色有较明显对比例如木质黄色棋盘配黑白棋子。图像中棋盘区域占据主要部分没有其他相似形状的强烈干扰。这些假设在真实手机拍摄的家用棋盘上绝大部分情况是成立的。“死子”判定逻辑这是最大的挑战。在视觉层面我们只能看到棋子的颜色和位置无法直接知道一块棋是否“有两只真眼”。因此在终局前提下我们采用一个简化但实用的策略默认所有棋子都是“活子”除非它被对方棋子完全包围且内部没有形成眼的可能。在实际算法中这通常通过“气”的计算来实现遍历每个棋子所在的连通块检查其相邻的空点“气点”数量。如果某个连通块的所有“气点”都被对方棋子占据则判定为死子。这个逻辑需要精细的棋盘坐标映射和状态搜索。3. 核心模块实现详解3.1 棋盘检测与透视矫正这是整个流程的基石如果棋盘区域找不准或者矫正不正后面的棋子坐标会全部错位。步骤一图像预处理与边缘检测首先将彩色图像转为灰度图这是为了简化信息。接着使用高斯模糊 (cv2.GaussianBlur) 稍微平滑图像以减少噪声对边缘检测的干扰。然后使用Canny边缘检测算法找出图像中所有的边缘。这里的参数如阈值需要根据图像对比度进行调整目标是将棋盘网格的线条清晰地提取出来。import cv2 import numpy as np def preprocess_image(image_path): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) # Canny边缘检测阈值需要微调 edges cv2.Canny(blurred, 50, 150) return img, edges步骤二寻找棋盘轮廓使用cv2.findContours在边缘图中寻找轮廓。我们需要找到那个最大的、近似四边形的轮廓它很可能就是棋盘的外边框。可以通过轮廓面积和轮廓近似 (cv2.approxPolyDP) 来筛选。找到四边形后获取它的四个角点。注意拍摄角度可能导致棋盘呈现梯形或其他四边形。cv2.approxPolyDP的epsilon参数控制近似的精度太小可能得不到四边形太大则形状失真。通常设为轮廓周长的某个百分比如0.02倍进行尝试。步骤三透视变换获得源图像的四个角点后我们需要定义目标图像的四个角点。目标图像应该是一个标准的正方形。假设我们处理的是19路棋盘我们可以将目标图像大小设为(600, 600)像素这是一个示例值可以根据需要调整。然后使用cv2.getPerspectiveTransform计算变换矩阵再用cv2.warpPerspective应用变换得到一张“摆正”的、俯视的棋盘图像。def warp_perspective(img, contour): # 假设从contour中已提取出四个有序角点 src_pts (左上右上右下左下) src_pts np.array(src_pts, dtypenp.float32) # 定义目标点 side_length 600 dst_pts np.array([[0, 0], [side_length, 0], [side_length, side_length], [0, side_length]], dtypenp.float32) matrix cv2.getPerspectiveTransform(src_pts, dst_pts) warped cv2.warpPerspective(img, matrix, (side_length, side_length)) return warped, matrix实操心得透视变换的质量直接影响后续棋子定位的精度。务必确保提取的四个角点顺序与目标点顺序一致通常是顺时针或逆时针。可以通过在原图上画出角点和连线来可视化验证。3.2 棋子检测与颜色识别得到矫正后的棋盘正视图后接下来就是在361个交叉点上“找棋子”。步骤一网格划分在600x600像素的矫正图像上19路棋盘有18个格子20条线包括边框。我们可以粗略地计算出每个格子的像素宽度约为600 / 18 ≈ 33.33像素。但更稳健的方法是利用霍夫线变换 (cv2.HoughLinesP) 检测出棋盘的水平线和垂直线通过它们的交点来确定精确的交叉点坐标。对于质量较好的矫正图像也可以直接用等分估算坐标但霍夫变换能应对轻微的畸变。步骤二兴趣区域ROI提取对于每一个交叉点坐标 (i, j)我们以其为中心截取一个小的正方形区域例如20x20像素作为ROI。这个区域应该足够包含整个棋子。步骤三棋子存在性与颜色判断这是关键步骤。如何判断一个ROI里是否有棋子以及是什么颜色存在性判断可以计算ROI内像素颜色的方差或标准差。如果是一个空点棋盘格颜色通常比较均匀比如木色方差较小。如果存在棋子由于棋子的颜色黑或白与棋盘对比明显且棋子是圆形边缘会有颜色变化方差会显著增大。设定一个经验阈值即可区分。颜色判断如果判定有棋子接下来判断颜色。将ROI从BGR色彩空间转换到HSV色彩空间。HSV对光照变化比RGB更鲁棒。识别黑子黑色在HSV中表现为低亮度Value。可以设定一个较低的V值阈值例如V 50并统计ROI内满足条件的像素比例。比例高则可能是黑子。识别白子白色表现为高亮度Value和低饱和度Saturation。可以设定S 30且V 200的阈值范围。需要注意的是棋子的颜色可能不是纯黑或纯白反光会导致局部高亮。因此更可靠的做法是计算ROI在某个颜色通道如灰度图上的平均强度。与棋盘背景的平均强度对比显著暗的是黑子显著亮的是白子。def detect_stone_at_point(warped_img, center_x, center_y, roi_size20): half roi_size // 2 roi warped_img[center_y-half:center_yhalf, center_x-half:center_xhalf] if roi.size 0: return empty # 转换为灰度计算对比度 gray_roi cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) avg_intensity np.mean(gray_roi) # 需要预先获取棋盘背景的平均灰度值 bg_gray if avg_intensity bg_gray - 25: # 阈值需调整 return black elif avg_intensity bg_gray 25: return white else: return empty # 与背景相近视为空点踩坑记录光照和阴影是颜色识别的主要敌人。同一颗白棋在光源直射下可能接近255纯白在阴影里可能只有200。单纯依赖固定阈值很容易误判。解决方案有1) 在矫正图像上执行更复杂的光照均衡化如CLAHE2) 采用自适应阈值或机器学习分类器如SVM对每个ROI进行分类3) 利用棋盘格背景色相对均匀的先验知识进行局部背景减除。在初版中我采用了一种简单有效的办法在棋盘四个角附近的空白区域采样动态估算当前图像下的“棋盘背景色”范围以此作为基准进行判断效果提升明显。3.3 死子判定与目数计算算法这是将图像信息转化为围棋知识的核心也是算法中最复杂的部分。步骤一构建棋盘状态矩阵经过上一步我们得到了一个19x19的矩阵board_state其中每个元素是‘black’,‘white’, 或‘empty’。这个矩阵代表了从图像中“看到”的棋子分布。步骤二计算“气”与判定死子围棋中一块棋的“气”是指与其相邻的空白交叉点。我们需要为棋盘上的每一个连通块同色棋子相连的区域计算气。连通块标记使用深度优先搜索DFS或广度优先搜索BFS遍历board_state将相连的同色棋子标记为同一个连通块ID。计算气数对于每个连通块检查其所有棋子相邻的四个方向上、下、左、右。如果相邻点是‘empty’则这块棋就有一口“气”。统计该连通块所有相邻空点的集合去重集合的大小就是这块棋的“气数”。初步死子判定在终局状态下如果一块棋的“气数”为0意味着它被对方棋子完全包围且没有眼位理论上就是死棋。将所有气数为0的连通块标记为“死子”。步骤三目数计算目数计算需要区分“实空”和“虚空”。标记死子将上一步判定为死子的连通块从棋盘状态中暂时移除视为空点因为死子最终要提走不算作对方的地盘。空点归属判定对于棋盘上所有的空点‘empty’需要判断它属于黑方还是白方或者为“公气”双方共享不计入任何一方。判定原则是一个空点如果它只与黑棋或黑棋的地域相邻则属于黑空只与白棋相邻则属于白空如果与双方都相邻则是公气。实现上可以对每个空点进行“泛洪填充”Flood Fill探索由空点连接的整个区域。在探索过程中记录这个区域接触到的棋子颜色。如果只接触到一种颜色则该区域全部属于该方。如果接触到两种颜色则为公气。计算最终目数黑方总子数 黑方活子数 黑方围得的空点数黑空白方总子数 白方活子数 白方围得的空点数白空根据中国数子法规则棋盘总交叉点数为361。因此黑方最终结果 黑方总子数 - 贴子数如3.75。如果结果大于180.5则黑胜反之则白胜。胜负差就是abs(黑方结果 - 180.5) * 2转换成目数。def calculate_liberty(board_state, group): 计算一个棋子连通块的气相邻空点集合 liberties set() for (x, y) in group: for dx, dy in [(0,1),(0,-1),(1,0),(-1,0)]: nx, ny xdx, ydy if 0 nx 19 and 0 ny 19: if board_state[ny][nx] empty: liberties.add((nx, ny)) return liberties def find_and_remove_dead_stones(board_state): 查找并移除死子 dead_stones [] visited [[False]*19 for _ in range(19)] for y in range(19): for x in range(19): if not visited[y][x] and board_state[y][x] in [black, white]: # BFS/DFS找到连通块 group, color find_connected_group(board_state, x, y, visited) lib calculate_liberty(board_state, group) if len(lib) 0: dead_stones.append((color, group)) # 移除死子 for color, group in dead_stones: for (x, y) in group: board_state[y][x] empty return board_state, dead_stones核心难点与技巧上述死子判定算法“气为零即死”在大多数终局场景下是有效的但它无法处理“有眼活棋”被紧完外气后、但眼内气未紧的情况这在终局时通常不会发生因为双方会同意跳过。然而它可能误判“双活”共活的情况即两块棋共享公气双方都无法紧气。处理双活需要更复杂的规则逻辑一个实用的简化方法是在终局数目时默认双活区域的公气为双方各占一半或根据具体规则不计入任何一方。对于业余爱好级别的工具可以暂时忽略或提示用户手动确认双活区域。4. 系统集成与结果可视化4.1 构建完整处理流水线将上述模块串联起来形成一个完整的函数process_go_board(image_path)。其流程如下读取图像预处理检测并矫正棋盘。在矫正图像上划分网格遍历361个点检测棋子颜色生成初始board_state。对board_state执行死子判定与移除。对移除死子后的board_state进行空点归属判定计算黑空和白空。统计黑/白活子数初始棋子数 - 死子数。根据数子法公式计算胜负。4.2 可视化与交互设计为了让结果更直观也便于调试可视化至关重要。结果叠加显示使用OpenCV的绘图功能在原图或矫正图上进行标注。用实心圆点在每个检测到的棋子位置进行重绘颜色用程序判断的结果蓝圈代表黑子红圈代表白子。用不同的颜色如浅黑色、浅白色填充被判定为黑空和白空的区域。用醒目的标记如“X”标出被程序判定为死子的位置。在图像顶部或底部添加文字显示计算结果“黑子XX 白子XX 黑空XX 白空XX 贴目后结果黑胜X.5目”。调试视图在开发过程中可以创建多个显示窗口分别展示边缘检测结果、透视变换后的棋盘、棋子检测热力图等帮助定位问题。简易交互可以增加简单的交互来修正算法的误判。例如允许用户用鼠标点击来切换某个点的状态黑/白/空修正后再重新计算。这对于处理复杂边界或算法不确定的情况非常有用。def visualize_result(original_img, warped_img, board_state, black_area, white_area, dead_stones, result_text): # 在矫正图warped_img上绘制 display_img warped_img.copy() grid_size warped_img.shape[0] // 19 # 1. 绘制空点归属 for area in black_area: cv2.fillPoly(display_img, [np.array(area)], (200, 200, 255)) # 浅紫代表黑空 for area in white_area: cv2.fillPoly(display_img, [np.array(area)], (255, 255, 200)) # 浅黄代表白空 # 2. 绘制棋子 for y in range(19): for x in range(19): center (x*grid_size grid_size//2, y*grid_size grid_size//2) if board_state[y][x] black: cv2.circle(display_img, center, grid_size//3, (0, 0, 0), -1) # 黑子 cv2.circle(display_img, center, grid_size//3, (255, 255, 255), 1) # 白边 elif board_state[y][x] white: cv2.circle(display_img, center, grid_size//3, (255, 255, 255), -1) # 白子 cv2.circle(display_img, center, grid_size//3, (0, 0, 0), 1) # 黑边 # 3. 标记死子 for color, group in dead_stones: for (x, y) in group: center (x*grid_size grid_size//2, y*grid_size grid_size//2) cv2.drawMarker(display_img, center, (0, 0, 255), markerTypecv2.MARKER_CROSS, thickness2) # 红色十字 # 4. 添加结果文字 cv2.putText(display_img, result_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) # 并排显示原图和结果图 combined np.hstack([cv2.resize(original_img, (warped_img.shape[1], warped_img.shape[0])), display_img]) cv2.imshow(Go Board Result, combined) cv2.waitKey(0) cv2.destroyAllWindows()4.3 性能优化与代码组织当算法基本跑通后可以考虑一些优化网格计算缓存棋盘网格坐标和ROI位置是固定的只需计算一次。并行处理361个点的棋子检测是独立的可以使用Python的concurrent.futures库进行并行处理显著提升速度。模块化将棋盘检测、棋子识别、规则计算分别写成独立的类或模块使代码结构清晰易于测试和维护。参数配置文件将Canny阈值、颜色判断阈值、贴目值等参数放在配置文件如JSON或YAML中方便针对不同棋盘、光照条件进行调整而无需修改代码。5. 常见问题与实战调试技巧在实际测试中你一定会遇到各种各样的问题。下面是我遇到的一些典型问题及解决方法希望能帮你少走弯路。5.1 棋盘检测失败或不准问题cv2.findContours找不到棋盘轮廓或者找到的是错误的四边形。排查检查边缘图首先可视化Canny边缘检测的结果。棋盘网格线是否清晰连续如果线条断裂尝试调整高斯模糊的核大小和Canny的高低阈值。背景干扰如果图片背景杂乱可能有其他强边缘干扰。可以在边缘检测后进行形态学操作如闭运算cv2.morphologyEx使用cv2.MORPH_CLOSE来连接断线并过滤掉面积过小的轮廓。透视畸变严重如果棋盘倾斜角度太大检测到的轮廓可能不是凸四边形。可以尝试使用cv2.convexHull来获取轮廓的凸包然后再进行多边形近似。技巧如果自动检测实在不稳定特别是对于项目演示或固定场景可以考虑提供手动标定的选项。让用户在图片上依次点击棋盘的四个角点然后进行透视变换。这虽然增加了用户操作但保证了100%的准确率。5.2 棋子颜色识别错误问题白棋被认成空点黑棋被认成白棋或者反光处识别混乱。排查动态背景估计不要用固定的颜色阈值。如前所述在图像四周的棋盘边缘区域采样多个点计算棋盘背景颜色的平均强度和范围。所有判断都基于与这个动态背景的对比。使用HSV色彩空间在判断黑白时HSV比BGR更可靠。重点关注亮度V通道。可以尝试在ROI内计算V通道的直方图观察其分布。形态学去噪对于棋子ROI可能会包含棋子边缘的阴影或反光。可以对ROI的灰度图进行一下形态学开运算消除小的亮斑或暗斑。采用机器学习如果条件允许可以收集几百个不同光照下的棋子ROI黑、白、空图片手动标注训练一个简单的分类模型如使用scikit-learn的SVM。即使特征只用像素灰度值的均值和方差效果也会比固定阈值稳定很多。技巧实现一个“置信度”输出。对于每个点的判断除了给出类别还计算一个置信度分数例如与背景的差异度。在最终可视化时可以用不同透明度或圈圈大小来显示置信度方便用户一眼看出哪些点的判断可能存疑。5.3 死子判定逻辑的边界情况问题双活共活被误判为死子或者大块棋内部有眼但外气被紧后程序误判。解决方案终局前提我们的算法基于“终局”假设即双方都同意不再落子。在这种情况下真正的“有眼活棋”不会处于外气被紧的状态因为对方不会去紧气送死。因此简单的“无气即死”在终局时对于活棋是安全的。主要风险在于双活。双活处理双活的典型特征是存在一个或多个“公气”空点这些空点同时是两块棋唯一的气。可以在“气数计算”后增加一个检查如果一个连通块的气数为0但这些气点实际上是公气同时也属于对方另一个连通块且对方连通块的气数也为0且双方都只有这些公气那么这两块棋形成双活都不应被判死。实现这个逻辑需要更精细的“气点归属关系”记录。提供手动修正对于无法通过算法完美解决的复杂棋形最实用的方法是在可视化界面中允许用户手动将某些棋子标记为“活”或“死”。程序根据用户的修正重新计算目数。5.4 性能与精度权衡问题处理一张图片速度慢或者网格点定位有微小偏移导致目数计算误差。优化分辨率选择不需要用原始高清图进行处理。将输入图像缩放到一个固定宽度如1000像素再进行检测可以大幅提升速度且对棋盘检测精度影响不大。网格精校使用霍夫线变换检测到的直线交点作为交叉点坐标比等分法更准。可以对这些交点坐标进行排序和多项式拟合纠正可能存在的非线性畸变。ROI大小自适应根据棋盘图像的分辨率动态计算ROI的大小使其始终能覆盖一个交叉点区域。5.5 实战调试流程建议当你遇到结果不对时建议按照以下步骤隔离问题保存中间结果把每一步处理后的图片灰度图、边缘图、轮廓图、矫正图都保存下来看看。打印关键数据打印出检测到的棋盘四个角点坐标、矫正后的图像尺寸、前几个交叉点的ROI图像和其判断结果颜色、置信度。构造测试用例不要一开始就用复杂的实拍图。用围棋软件生成一张标准的、无背景的终局截图作为“黄金标准”测试图。先用它验证你的算法流程是否正确然后再挑战实拍图。单元测试为棋盘检测、棋子识别、气计算等核心函数编写单元测试用模拟数据验证其逻辑。这个项目从构思到实现我花了大约两个周末的时间。最大的收获不是做出了一个多完美的工具而是在解决一个个具体问题的过程中对OpenCV的各种函数有了更深刻的理解并且将抽象的围棋规则转化为了具体的代码逻辑。它可能无法替代职业裁判但对于日常对弈的终局确认已经足够可靠。如果你也感兴趣不妨动手试试从一张简单的棋盘截图开始逐步增加复杂度这个过程本身就像解一道有趣的谜题。

相关新闻