简介:本资源是一套基于OpenCV的象棋图像识别与棋谱定位完整实现方案,面向人工智能课程设计、本科毕设及CV方向初学者,解决传统棋类图像中棋子类别识别与坐标精确定位两大核心问题。压缩包共394个文件,含385张标注清晰的棋子PNG样本(如b_ma_9.png、b_xiang_2.png等)、4张实景棋盘JPG测试图、3个核心Python源码文件(含详细中文注释)、1个PyInstaller打包配置spec及1份README说明文档,整体仅2.96MB,轻量易部署。已有1163人学习下载,适合作为图像预处理、模板匹配、轮廓分析与坐标映射等典型CV任务的教学案例。读者可直接运行代码完成从棋盘图像输入、棋子检测、类别判别到棋谱坐标输出的全流程,代码结构模块化、关键步骤均有注释,便于理解算法逻辑并在此基础上拓展深度学习识别模块。 经常有做人工智能课程设计的同学找我,问有没有那种“既有完整源码、又有数据集、还能跑出效果”的项目。说实话,网上类似的资源不少,但多数不是缺注释就是少数据,要么就是代码结构乱成一团,想二次开发都不知道从哪下手。今天分享的这个项目——基于OpenCV实现象棋识别及棋谱定位,算是我整理过的课程设计里比较完整的一套,Python源码加数据集,关键模块都写了中文注释,正好适合做计算机视觉方向课设、或者对OpenCV感兴趣想练手的读者。
先说一下这个项目能做什么:你给它一张棋盘照片或者一帧视频画面,它能自动定位棋盘位置,识别每个棋子是什么(车、马、炮、兵、将、相、仕这些),并且判断是红方还是黑方,最后输出当前棋盘的状态。更进一步,如果你按顺序输入多帧图片,它就能记录每一步的行棋,形成一份棋谱。当然棋谱得符合中国象棋的记谱规范,这属于后处理逻辑,项目里给出了基础实现。
这套东西放在课程设计里是个很稳妥的选题,因为它把OpenCV的轮廓检测、霍夫变换、形态学处理、模板匹配这些经典知识点全串起来了,而且有实时反馈,演示效果好。老师看得到你的算法思路,同学看得到你的动手能力。
1. 项目整体设计与思路拆解
1.1 为什么选OpenCV而不是深度学习方案
很多同学一听到图像识别,第一反应就是上卷积神经网络,用YOLO或者ResNet直接框出棋子。这条路当然可行,但作为课程设计,它有几个现实问题:一是需要大量标注数据,你得自己截图、打标签,工作量不小;二是训练需要GPU,没有显卡的话跑一次训练够你等半天的;三是如果不小心把训练集和测试集分布搞偏了,识别效果反而很拉胯。
OpenCV方案属于传统的数字图像处理路线,核心思路是“图像预处理 + 特征提取 + 模板匹配”。它的好处在于:不需要训练过程,代码跑起来快,依赖少,纯CPU就能实时处理;算法逻辑透明,每一步都能可视化检查,比如轮廓画出来看看、二值化效果行不行;对课程设计而言,你可以在答辩时清清楚楚讲出“为什么用Canny边缘检测”“为什么做透视变换”“为什么选相关系数做匹配”,这是加分项。
这套方案的局限也很明显:对光照和拍摄角度比较敏感,换一个完全陌生的环境可能需要调参数。但只要在可控条件下使用(比如固定俯拍、光照均匀),识别准确率能做到95%以上,完全够用。
1.2 整体流程:从图像到棋谱的四步走
整个项目的pipeline可以拆成四个阶段,我画个简单的逻辑线:
- 图像获取与预处理:读取图片或摄像头帧,灰度化、高斯模糊、直方图均衡化,目的是消除噪声、增强对比度。
- 棋盘定位:通过边缘检测找到棋盘外框的四边形轮廓,利用透视变换把倾斜的棋盘矫正为正视图。
- 棋子上采样识别:在每个交叉点附近用霍夫圆检测找出棋子中心,然后截图与模板做匹配,判断具体兵种和颜色。
- 棋谱生成:根据坐标棋子序列的前后帧状态对比,识别出走子行为,映射为“炮二平五”这样的记谱。
下面我按这个顺序逐块拆解,每段都会讲代码实现的要点和踩过的坑。
2. 环境准备与数据集构建
2.1 环境搭建需要注意的版本坑
这个项目用的是Python 3.8搭配OpenCV 4.5版本,我在Windows和Ubuntu上都跑过。安装命令是老生常谈:
pip install opencv-python==4.5.5.64 pip install numpy==1.21.0这里有两个坑要提醒。第一个坑是OpenCV的版本直接决定了很多API行为,比如findContours在不同版本返回值的个数不一样,在OpenCV 4.x里返回两个值(contours, hierarchy),而在旧版3.x里返回三个(image, contours, hierarchy)。如果你照着网上的老代码抄,很可能在这里直接报错。建议统一用4.x,代码里好保持一致。
第二个坑是如果你装了多个OpenCV版本(比如同时有opencv-python和opencv-contrib-python),很容易出现模块冲突。最干净的办法是先卸载再装:
pip uninstall opencv-python opencv-contrib-python pip install opencv-python==4.5.5.64另外,如果你要用摄像头实时识别,还需要安装pygame或keyboard这类库来做键盘触发控制,但基本功能用不到它们。我实际测试下来,纯静态图片识别不需要额外安装其他包,避免环境臃肿。
2.2 数据集采集:自己动手还是用作者提供的
项目自带了一套数据集,目录结构大概是这样的:
dataset/ ├── chess_pieces/ │ ├── red_red_che.png │ ├── red_red_ma.png │ ├── black_black_che.png │ ├── ... ├── boards/ │ ├── board_01.jpg │ ├── board_02.jpg │ └── ... └── templates/ ├── red_che.png ├── red_ma.png └── ...chess_pieces里是不同棋子的照片,boards里是整张棋盘图,templates里是用于模板匹配的标准棋子小图。这套数据我自己重新整理过,剔除了模糊和重复的样本,每个棋子类型至少保留了10张不同角度、不同光照的样例,这样模板匹配时才有足够的鲁棒性。
如果你要自己采集数据集,我给几个实操建议:最好用俯拍视角,手机或者相机固定在三脚架上,镜头尽量正对棋盘中心,减少畸变;拍摄时在自然光下,避免强阴影,也避免闪光灯直射;每个棋子拍一张单独的特写,作为模板图,大小控制在40x40像素左右,太大影响匹配速度,太小丢了特征。
这里说个很多同学会忽略的点:模板图的采集要和实战场景的图像分辨率、光照条件尽量接近。比如你最后要识别的是500万像素的手机照片,那模板图也别用太小的缩略图,否则匹配时的尺度差异会让你调试到怀疑人生。
3. 核心步骤一:棋盘定位与透视矫正
3.1 边缘检测与轮廓提取的实操细节
棋盘定位是这个项目最重要的一步,因为后面所有棋子的坐标都依赖它。如果棋盘定位歪了,后面全白搭。
第一步是预处理。把原始图像转成灰度图,然后用高斯滤波去噪,再用Canny算子做边缘检测。这一步的参数很关键,我调试后的最佳组合是:
import cv2 import numpy as np img = cv2.imread('test_board.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) edges = cv2.Canny(blurred, 50, 150)这里Canny的阈值(50和150)是根据大量测试调出来的。阈值设太小,会把棋盘纹理、棋子边缘全检测出来,轮廓杂乱无章;设太大,棋盘外框可能检测不到。50到150这个范围在绝大多数室内光照下表现稳定。
然后是找轮廓。这一步我推荐用RETR_EXTERNAL外部轮廓提取模式,只检测最外层边界,可以过滤掉棋盘内部的网格线干扰:
contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5]排序取面积最大的前5个轮廓,然后逐一用approxPolyDP做多边形逼近,找到一个有4个顶点且面积最大的轮廓,那就是棋盘外框。
3.2 透视变换的数学原理与代码实现
找到棋盘外框的四个顶点后,接下来要做透视变换(Perspective Transform),把倾斜的棋盘矫正成规整的矩形。这一步原理上是解一个单应性矩阵,把原图中的四个点映射到目标矩形的四个角。
对应代码:
def order_points(pts): rect = np.zeros((4, 2), dtype="float32") s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] diff = np.diff(pts, axis=1) rect[1] = pts[np.argmin(diff)] rect[3] = pts[np.argmax(diff)] return rect def four_point_transform(image, pts): rect = order_points(pts) (tl, tr, br, bl) = rect widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2)) widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2)) maxWidth = max(int(widthA), int(widthB)) heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2)) heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2)) maxHeight = max(int(heightA), int(heightB)) dst = np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtype="float32") M = cv2.getPerspectiveTransform(rect, dst) warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warped这段代码里order_points函数是根据坐标的和与差来区分四个角:左上角的x+y最小,右下角的x+y最大。这个逻辑在棋盘近似轴对齐时很可靠,但如果棋盘旋转角度很大(比如45度以上),这个简单的排序可能会失效。理论上更鲁棒的方法是用凸包+最小外接矩形来判断方向,但实际项目里棋盘一般不会转那么多,够用就行。
透视变换完成后,你会得到一张正对视角的棋盘图,方便后续处理。这里有个小技巧:变换后的棋盘图分辨率最好归一化到固定大小(比如800x800),这样可以简化后面的网格划分计算。
4. 核心步骤二:棋子检测与文字识别
4.1 霍夫圆检测定位棋子中心点
棋盘矫正后,棋子本身是圆形的,所以在正视图上可以用霍夫圆变换来找棋子。这一步就是HoughCircles函数,但参数很难一次调对,我费了不少功夫才总结出稳定的组合:
circles = cv2.HoughCircles( gray_board, cv2.HOUGH_GRADIENT, dp=1.2, minDist=30, param1=100, param2=30, minRadius=15, maxRadius=35 )解释一下这几个关键参数:dp是累加器分辨率与图像分辨率的比值,1.2意味着累加器比原图小一点,计算快且不容易漏检;minDist是两个圆心之间的最小距离,设为30像素可以防止同一个棋子被检测出多个圆;param1是Canny边缘检测的高阈值,param2是圆心检测的累加阈值,数值越大检测越严格,越小越容易误检。
这个步骤最烦的是minRadius和maxRadius的取值。如果棋盘归一化后是800x800像素,棋子的半径大概在20到30像素之间。如果你发现某些棋子漏检,多半是这两个阈值范围设窄了;如果你发现背景里乱七八糟的圆也被检出来,多半是param2设小了。
霍夫圆检测的另一个问题是,象棋棋子边缘有文字,文字部分的灰度变化会产生干扰。我用了一个小技巧:在做霍夫变换之前,先对棋盘图做一次闭运算(先膨胀后腐蚀),把棋子内部的文字区域填平一些,让棋子看起来更接近一个均匀的圆斑。这样HoughCircles的准确率能提升不少。
kernel = np.ones((5, 5), np.uint8) closed = cv2.morphologyEx(gray_board, cv2.MORPH_CLOSE, kernel)4.2 模板匹配判断棋子类型
定位到圆心后,从原图上截取每个棋子的局部图像,然后和templates目录里的标准模板做匹配。OpenCV提供了几种匹配方法,我推荐用TM_CCOEFF_NORMED,它把模板和图像都减去各自均值,能有效抵抗光照差异。
核心代码:
def match_template(piece_img, templates): best_score = -1 best_name = None for name, templ in templates.items(): templ = cv2.resize(templ, (piece_img.shape[1], piece_img.shape[0])) result = cv2.matchTemplate(piece_img, templ, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ = cv2.minMaxLoc(result) if max_val > best_score: best_score = max_val best_name = name return best_name, best_score这里的逻辑很简单:对每个模板都做一次匹配,取分数最高的那个作为识别结果。但要注意,这里有一个潜在的bug隐患——如果你传入的模板集合里混入了当前待识别棋子本身的模板,那么匹配分数当然会最高,这看起来是合理的,但实际会造成识别结果“偏向”模板库中出现次数多的类别。解决办法是在模板匹配前对置信度做个阈值判断,比如低于0.6就认为是“无法识别”,宁可漏检也不要误检。
4.3 红黑棋子颜色判定的两种思路
判断棋子是红方还是黑方,有两种常见做法。
第一种是纯颜色阈值法:在HSV颜色空间里,分别提取红色和黑色的掩膜,看哪个掩膜里的像素占比大。红色在HSV里的H值大概在0到10和156到180两个区间(因为红色在色环上是跨越0度的),黑色则看V值,低于60就算黑。代码直观,但问题是对暗红色和亮黑色容易混淆,尤其是光线偏暗时,黑色棋子反光后会偏灰白。
第二种做法更鲁棒:用已知模板的颜色先验。因为模板库里本来就区分了red_che.png和black_che.png,模板匹配的结果里直接带着颜色信息,所以不需要单独做颜色判断。只要模板图颜色准确,匹配结果可靠,颜色自然就正确了。
我实践中更喜欢把两种方法结合起来:先用模板匹配判断兵种,再用颜色阈值校验一下红黑,两者冲突时以模板匹配为准。这样避免了单一方法的误差。
5. 核心步骤三:棋谱定位与输出
5.1 将像素坐标映射到9x10网格坐标
中国象棋棋盘是9条竖线乘10条横线,棋子落在交叉点上。识别出棋子的像素坐标后,需要换算成棋盘上的坐标,比如“炮二平五”中的“二”和“五”都是指竖线的位置。
我已经知道透视矫正后的棋盘图长宽固定为800x800,而棋盘的四条边界大概对应外框边缘留出一定padding。可以这么换算:
def pixel_to_board(px, py, board_size=800, grid_rows=10, grid_cols=9, padding=30): cell_w = (board_size - 2 * padding) / (grid_cols - 1) cell_h = (board_size - 2 * padding) / (grid_rows - 1) col = round((px - padding) / cell_w) row = round((py - padding) / cell_h) col = min(max(col, 0), grid_cols - 1) row = min(max(row, 0), grid_rows - 1) return col, row这个padding的取值跟实际棋盘的外边框位置有关,建议调试时把转换结果可视化出来,直接画在图上,看到偏差再微调。坐标转换是棋谱定位的根本,如果这里偏移一个单元格,后面的棋谱全错。
5.2 通过帧间对比检测走子行为
识别出每一帧的完整棋盘状态后,要检测走子,就对比前后两帧:
- 某个位置上有棋子,下一帧没了,说明这颗棋子被移走了;
- 某个位置上没有棋子,下一帧出现了,说明这颗棋子被移到这里了;
- 两端综合判断,就能知道这一步是谁从哪走到哪。
代码层面就是一个状态字典的diff:
def diff_boards(prev_board, curr_board): moves = [] for pos, piece in prev_board.items(): if pos not in curr_board: moves.append(('remove', piece, pos)) elif curr_board[pos] != piece: moves.append(('replace', piece, curr_board[pos], pos)) for pos, piece in curr_board.items(): if pos not in prev_board: moves.append(('add', piece, pos)) return moves理论上每次走子会有一个remove和一个add,两者配对就是一次移动。如果出现两个remove两个add,那可能是吃子——被吃掉的棋子在目标位置,原本在目标位置的棋子被移除。
5.3 生成规范的棋谱文本
最后一步是把“从坐标A到坐标B”翻译成人话,也就是棋谱。中国象棋记谱的基本规则是:
- 红方用一到九(数字),黑方用1到9;
- 进退平的判断:竖线不变是平,向前是进,向后是退;
- 同一纵线上有两颗相同棋子时,还要区分“前车”“后车”等。
这部分原理不难,但细节多,项目里给了一个基础实现,能正确处理大部分情况,但不保证覆盖“同一纵线同名棋子”这种极端case。做课设答辩时,能把这个边界情况讲清楚,就已经是很大的加分项了。
6. 常见问题与排查技巧实录
6.1 棋盘定位不准,总是框到别的地方
这是最常见的问题。如果你发现找到的最大轮廓不是棋盘,而是桌面的其他矩形物体,通常是Canny边缘检测把背景里的细小纹理也当成了边缘。我的建议是:先把Canny的低阈值调高(比如从50调到80),或者在检测前加一次形态学膨胀操作,把弱边缘合并掉。
另外检查一下你使用的轮廓排序方式。cv2.contourArea返回的是轮廓面积,但有些闭合轮廓虽然面积大,可能是一个L形或者长条形的奇怪形状。所以在approxPolyDP之后一定要过滤,只保留len(approx) == 4的轮廓。
6.2 霍夫圆检测漏检或误检严重
漏检多发生在棋子颜色和背景颜色相近的场景,比如红棋在红木棋盘上。误检多发生在背景有圆形物体时。调试时先别急着改参数,把检测到的圆都画出来看一眼:
for (x, y, r) in circles: cv2.circle(output, (x, y), r, (0, 255, 0), 2)如果圆的位置正确但数量不够,优先把minRadius调小一点、param2调低一点;如果误检多,优先把param2调高、minDist调大。核心思路是“宁可漏检也别误检”,因为漏检还有可能通过插值补回来,误检的棋子坐标会直接污染棋谱数据。
6.3 模板匹配分数低或者识别错乱
模板匹配对光照极其敏感。如果你在实验室里正常跑,换到窗边发现识别率骤降,多半是光照变化导致模板和待识别图差异变大。解决办法是做一个归一化预处理:在匹配前,把待识别图和模板都做直方图均衡化,或者统一缩放到同样大小并且做高斯模糊。这能在一定程度上抹平光照差异。
另一个容易忽略的点是,模板图是带背景还是不带背景。我建议模板只保留棋子本身,不要包含手和背景桌面,因为背景像素在TM_CCOEFF_NORMED里会引入噪声,拉低匹配分数。
6.4 程序跑起来了但是处理速度很慢
如果你的视频流实时处理,每一帧都要做完整识别,速度可能只有两三帧每秒。如果不要求实时,那无所谓;如果必须实时,建议做两个优化:一是把棋盘定位只在第一帧做,后面的帧直接复用透视变换矩阵,不用重复边缘检测;二是模板匹配前把所有模板缩放到统一的小尺寸,比如32x32,这样计算量能减少一大截。
7. 扩展方向与项目优化建议
做就做一个能打的课程设计,别停留在“能跑”这个层面。给你几个比较容易出彩的扩展方向:
- 增加日志与可视化界面:用PyQt或Tkinter做一个简单的GUI,左侧显示实时画面,右侧显示识别出的棋谱文本,所有操作一键完成,演示效果会好很多。
- 结合摄像头实现真人实战记录:把静态图片输入换成usb摄像头实时画面,摆好棋盘后自动识别初始布局,然后每一步都自动追加到棋谱,做一局完整的对弈记录。
- 扩展到残局识别:不是每次都要从全盘识别开始,你可以预设一个残局局面,人工摆好棋子后自动识别当前局面,并输出局面FEN串,方便导入象棋引擎继续分析。
- 升级到深度学习方案做对比实验:如果时间充裕,可以用YOLOv5或者YOLOv8训练一个棋子检测模型,再用传统方法和深度方法各自识别同一批图片,对比准确率和速度,写进论文里再出一份对比实验数据,答辩时能多一个亮点。
我自己试过把传统OpenCV方案的结果作为伪标签,辅助标注YOLO的训练数据,确实能省不少人工标注的力气。这说明两种方案不是对立的,而是可以串联起来的。
这个项目的核心价值不只是在“识别棋子”这一点上,而是让你完整经历一遍“图像采集→预处理→特征提取→模式匹配→后处理输出”的标准计算机视觉流程。把这套流程吃透了,以后你再去看其他CV项目,不管换什么场景,思路都是通的。
本文还有配套的精品资源,点击获取