简介:这是一套面向计算机相关专业学生与初学者的OpenCV实战项目,围绕中国象棋棋子的自动识别展开,采用级联分类器完成红黑双方共14类棋子的检测与分类,适合用作课程设计、毕业设计或Python与计算机视觉入门练手。资源包共16个文件,包含3个Python源码文件、3个压缩数据包、8张识别结果示例图,以及说明文档与授权声明,整体约20.78MB。其中数据集按红黑两色分开保存,每类棋子经灰度化与中值滤波后保留约200张多角度样本,并附有训练好的红黑分类模型文件,可直接调用或重新训练。代码注释详尽,配套项目说明与运行结果截图,便于理解级联分类器的训练流程与识别逻辑。目前已有266人学习,适合在此基础上修改扩展,完成其他棋子或目标识别类课题。
1. 从一张棋盘照片到棋子坐标:这套级联分类器资源到底能跑出什么
很多人做中国象棋识别,第一反应是上 YOLO 或者直接调大模型,结果卡在标注成本和显存上。这套资源走的是另一条路:用 OpenCV 自带的级联分类器(Cascade Classifier),把红黑双方各 7 类棋子拆成两个模型分别训练,输入是灰度化加中值滤波后的棋子小图,输出是棋子在画面里的检测框。它解决的不是"识别整盘棋局"这种大问题,而是"给定一张棋盘照片,把每个棋子的位置和颜色类别框出来"这个具体环节。适合计算机相关专业的课程设计、毕设起步,也适合想搞懂传统目标检测流程、不想一上来就被深度学习框架绑住的人。资源里带了采集好的数据集、训练脚本、识别脚本和已经训好的 yml 模型,拿到手就能先跑通再改。
2. 级联分类器识别棋子的原理与这套代码的选型逻辑
2.1 为什么用 Haar 级联而不是深度学习
级联分类器的核心是 Haar-like 特征加 AdaBoost 筛选加级联结构。简单说,它先用一堆黑白矩形模板在图像上滑动,算出明暗差异作为特征,再用 AdaBoost 从成千上万个特征里挑出对区分棋子最有用的几百个,最后把强分类器串成级联——前几级快速排除明显不是棋子的区域,后面的级才做精细判断。这套机制在棋子这种纹理固定、形状规整、背景相对可控的场景里够用,而且训练不需要 GPU,一张普通显卡甚至纯 CPU 就能跑完。
选它而不是 YOLO,理由很实际:数据集只要 200 张每类就能起步,标注只需要画框,不用做像素级分割;模型文件是 yml 文本格式,能直接打开看参数,调参过程不是黑匣子;推理时 CPU 单帧几十毫秒,嵌入式设备也能部署。代价是它对旋转和光照变化比深度学习敏感,所以资源里才专门做了灰度化和中值滤波预处理,把颜色和噪声这两个变量先摁住。
2.2 数据集的组织方式与预处理链路
资源里的 Dataset_Red_Black 文件夹按颜色分了两大块,每块下面再按棋子类别分 7 个子文件夹,每类保留 200 张各种角度的图。这个结构不是随便定的——级联分类器训练时正样本需要统一尺寸,负样本需要足够多的背景图,所以 getDataSet.py 干的事就是把原始棋子图做灰度化、中值滤波、尺寸归一化,然后生成正样本描述文件。
import cv2 import os import numpy as np # 读取原始棋子图,转灰度并做中值滤波 def preprocess_chess_image(img_path, target_size=(24, 24)): img = cv2.imread(img_path) if img is None: return None # 灰度化:去掉红黑颜色信息,只保留明暗纹理 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 中值滤波:核大小 3,去掉椒盐噪声同时保留边缘 filtered = cv2.medianBlur(gray, 3) # 统一缩放到 24x24,这是级联分类器常用的正样本尺寸 resized = cv2.resize(filtered, target_size, interpolation=cv2.INTER_AREA) return resized # 遍历每个类别文件夹,生成正样本 def build_positive_samples(root_dir, output_txt): lines = [] for label in os.listdir(root_dir): label_dir = os.path.join(root_dir, label) if not os.path.isdir(label_dir): continue for fname in os.listdir(label_dir): if not fname.lower().endswith(('.jpg', '.png', '.bmp')): continue fpath = os.path.join(label_dir, fname) processed = preprocess_chess_image(fpath) if processed is None: continue # 保存处理后的图,路径写入描述文件,格式:路径 1 0 0 24 24 save_path = os.path.join(root_dir, 'processed', label, fname) os.makedirs(os.path.dirname(save_path), exist_ok=True) cv2.imwrite(save_path, processed) lines.append(f"{save_path} 1 0 0 24 24") with open(output_txt, 'w') as f: f.write('\n'.join(lines)) print(f"生成 {len(lines)} 条正样本记录")这段代码里三个参数值得盯住:中值滤波的核大小 3 是经验值,棋子表面如果有明显反光可以试 5,但再大就会把棋子上的字糊掉;目标尺寸 24x24 是级联分类器的经典正样本尺寸,改大能保留更多细节但训练时间成倍涨;描述文件里每行末尾的1 0 0 24 24分别表示样本数量、左上角坐标和宽高,写错一个数字训练就会报错。
2.3 训练脚本的关键参数与执行流程
train.py 调用的是 OpenCV 的opencv_traincascade命令行工具,参数配置直接决定模型能不能收敛。资源里红黑两个模型分开训,是因为红色棋子和黑色棋子在灰度图上的明暗分布差异明显,混在一起训会让分类器在颜色边界上反复横跳。
# 生成负样本描述文件,负样本用棋盘背景图 find ./negative_images -name "*.jpg" > negatives.txt # 训练红色棋子模型 opencv_traincascade \ -data ./model_save/red_model \ -vec ./positive_red.vec \ -bg ./negatives.txt \ -numPos 1400 \ -numNeg 700 \ -numStages 15 \ -featureType HAAR \ -w 24 -h 24 \ -minHitRate 0.995 \ -maxFalseAlarmRate 0.5 \ -mode ALL-numPos是每级实际用的正样本数,一般设成总正样本数的 80% 左右,留一部分做验证;-numNeg是负样本数,经验值是正样本的一半到一倍;-numStages是级联层数,15 层在棋子场景下够用,层数越多误检越少但训练越慢;-minHitRate 0.995要求每级至少保留 99.5% 的正样本,这个值设低了后面级联会漏检;-maxFalseAlarmRate 0.5控制每级的误报率,两级乘起来就是 0.25,十五级下来误报率会压到很低。训练过程中如果某一级卡住不收敛,通常是负样本不够多样,往 negatives.txt 里加棋盘格、桌面、手部这些干扰图。
3. 从零跑通识别流程:环境、脚本与参数调优
3.1 环境搭建与依赖安装
这套代码依赖 OpenCV 的 Python 绑定和训练工具。常见做法是用 conda 建一个干净环境,避免和系统里的 OpenCV 版本打架。
conda create -n chess_cascade python=3.8 conda activate chess_cascade pip install opencv-python opencv-contrib-python numpy装完之后验证一下cv2.__version__能正常打印,并且cv2.CascadeClassifier和cv2.data.haarcascades都能访问。如果报ModuleNotFoundError: No module named 'cv2',八成是装到了别的 Python 环境里,用which python确认当前解释器路径。训练工具opencv_traincascade在 opencv-contrib-python 里不一定带,Linux 下可以用包管理器装opencv-data和libopencv-dev,Windows 下需要单独下载 OpenCV 的预编译包,把opencv_traincascade.exe所在目录加进 PATH。
3.2 recognize.py 的检测逻辑与参数含义
识别脚本的核心是加载 yml 模型,对输入图做同样的灰度化和中值滤波,然后调detectMultiScale拿检测框。
import cv2 import numpy as np # 加载红黑两个模型 red_cascade = cv2.CascadeClassifier('./model_save/Red_Chess.yml') black_cascade = cv2.CascadeClassifier('./model_save/Black_Chess.yml') def detect_chess(img_path): img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.medianBlur(gray, 3) # 红色棋子检测 red_boxes = red_cascade.detectMultiScale( gray, scaleFactor=1.05, # 每次缩放 5%,越小越慢但越不容易漏 minNeighbors=6, # 一个框至少被检测到 6 次才保留,压误检 minSize=(20, 20), # 最小棋子尺寸,小于这个不检 maxSize=(80, 80) # 最大棋子尺寸,大于这个不检 ) # 黑色棋子检测,参数略调 black_boxes = black_cascade.detectMultiScale( gray, scaleFactor=1.08, minNeighbors=5, minSize=(20, 20), maxSize=(80, 80) ) # 画框并标注颜色 for (x, y, w, h) in red_boxes: cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2) cv2.putText(img, 'Red', (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) for (x, y, w, h) in black_boxes: cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2) cv2.putText(img, 'Black', (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) return img result = detect_chess('./TestPicture/Test1.jpg') cv2.imwrite('./output/Test1_result.jpg', result)scaleFactor控制图像金字塔的缩放步长,1.05 意味着每次缩小 5%,检测窗口能覆盖更多尺寸但计算量上升;minNeighbors是同一个目标被重复检测多少次才输出,设太低会出一堆重叠框,设太高会漏掉边缘棋子;minSize和maxSize要根据实际拍摄距离调,拍得远棋子小就把 minSize 降到 15 左右,拍得近就往上提。红黑两个模型的参数可以不一样,因为黑色棋子在灰度图上对比度通常比红色低,需要更宽松的邻居阈值。
3.3 用测试图验证与结果判读
资源里带了 TestPicture 文件夹和 8 张分类结果图,覆盖红黑双方各 4 个测试场景。跑完 recognize.py 后,把输出图和自带的 Test1 到 Test4 结果图对比,重点看三个地方:棋子密集排列时有没有漏检、棋子边缘有没有重复框、红黑模型有没有互相误检。如果红色模型把黑色棋子也框了,说明负样本里黑色棋子放少了,需要往红色模型的负样本集里补黑色棋子图重新训。如果某个角上的棋子总是漏,检查那张图里棋子的实际像素尺寸是不是低于 minSize。
4. 避坑与排查:训练和识别阶段最容易翻车的五个点
4.1 训练到某一级报 "Required leaf false alarm rate achieved" 后卡死
现象是终端停在某一级不动,CPU 占用掉到接近零。原因是负样本被前几级筛完了,后面级没有足够的负样本继续训练。解决方法是往 negatives.txt 里补图,数量至少是 numNeg 的两倍,并且负样本要包含各种背景——棋盘格、木桌面、纯色布、手部局部,不要只用一种背景。
4.2 识别时同一个棋子出现三四个重叠框
现象是画面上一个棋子被框了好几次,框之间大量重叠。原因是 minNeighbors 设得太低,分类器在相邻尺度上都给出了响应。解决方法是把 minNeighbors 从 3 提到 6 到 8,同时把 scaleFactor 从 1.1 降到 1.05,让尺度搜索更细但合并更严格。如果还不行,在 detectMultiScale 之后加一步非极大值抑制,手动合并 IoU 超过 0.3 的框。
4.3 红色模型把黑色棋子也框进去
现象是红方模型在黑色棋子上也输出检测框,颜色分类完全乱掉。原因是训练红色模型时负样本里没有黑色棋子,分类器只学到了"圆形有字"这个特征,没学到颜色差异。解决方法是把黑色棋子的灰度图批量加入红色模型的负样本集,反之亦然,让两个模型在颜色维度上真正分开。
4.4 换了拍摄环境后检测率断崖下跌
现象是训练集里检测很好,拿到新拍的棋盘照片上漏检一大半。原因是级联分类器对光照和背景变化敏感,训练集的灰度分布和新环境不一致。解决方法是识别前先做直方图均衡化,把灰度分布拉平,或者用 CLAHE 做局部对比度增强。如果新环境背景特别花,考虑在识别前用颜色阈值先把棋盘区域抠出来,只在棋盘范围内做检测。
4.5 opencv_traincascade 命令找不到或报缺少 DLL
现象是 Windows 下运行 train.py 提示opencv_traincascade不是内部或外部命令,或者双击 exe 报缺少 opencv_worldxxx.dll。原因是训练工具不在 Python 包的 PATH 里,且依赖的 DLL 没一起放。解决方法是去 OpenCV 官方 release 页面下载对应版本的预编译包,把build\x64\vc15\bin目录加进系统 PATH,并把该目录下的所有 DLL 复制到 exe 同级目录。Linux 下用apt install libopencv-dev后工具通常在/usr/bin下,直接能调。
5. 进阶玩法:把两个模型串成完整棋局识别流水线
单靠红黑两个级联模型只能给出"这是红棋子"或"这是黑棋子",但中国象棋有 7 种棋子,帅仕相马车炮兵各有各的走法。要把识别做到能用的程度,得在检测框后面接一个分类环节。我一般会这么做:先用级联模型把棋盘上所有棋子框出来并按颜色分组,然后对每个框内的图做一次模板匹配或者小型的 CNN 分类,把具体棋子类别定下来。
# 在检测框基础上做棋子类别细分 def classify_piece(gray_crop): # 方案一:模板匹配,适合字体固定的印刷体棋子 templates = { 'shuai': cv2.imread('./templates/shuai.png', 0), 'jiang': cv2.imread('./templates/jiang.png', 0), # ... 其余 12 类 } best_label, best_score = None, 0 for label, tmpl in templates.items(): if tmpl is None: continue tmpl = cv2.resize(tmpl, (gray_crop.shape[1], gray_crop.shape[0])) res = cv2.matchTemplate(gray_crop, tmpl, cv2.TM_CCOEFF_NORMED) score = res.max() if score > best_score: best_score, best_label = score, label return best_label if best_score > 0.6 else 'unknown'模板匹配的阈值 0.6 是经验值,低于这个值说明棋子角度偏得太多或者光照差异太大,宁可标 unknown 也不要硬分。如果模板匹配效果不稳,可以拿级联模型输出的框做数据增强,训练一个 7 分类的小 CNN,输入 32x32 灰度图,三层卷积就能到 95% 以上。另一个进阶方向是把识别结果映射到棋盘坐标:检测到所有棋子后,按框的中心点做聚类,拟合出 9 列 10 行的网格,再把每个棋子填进对应格子,这样就能输出 FEN 格式的棋局字符串,后面接引擎或者走法判断都方便。
验证流水线是否靠谱,我习惯用一套固定流程:先拿训练集里的图跑一遍,确认检测率和分类准确率;再拿没参与训练的测试图跑,看泛化;最后拿手机现拍一张,看真实场景下的表现。三步都过了才算能用。从那以后我每次改完模型参数,都强制走一遍这三步验证,不再凭单张图的效果下结论。希望帮到你。
本文还有配套的精品资源,点击获取