OpenCV级联分类器实战:中国象棋棋子识别与棋谱生成
2026/9/23 13:55:03 网站建设 项目流程

简介:这份资源面向计算机相关专业的在校学生、教师及企业员工,提供基于OpenCV级联分类器识别中国象棋棋子的完整项目源码与配套数据集,适合课程设计、毕业设计、作业或项目初期立项演示使用。压缩包共16个文件,约20.78MB,包含3个Python脚本分别负责数据采集、模型训练与棋子识别,8张jpg图片展示红黑双方分类结果,另有3个zip数据与模型压缩包、1个md说明文档和1个txt授权声明,注释拉满便于理解。数据集按红黑两色分开保存,每色7类棋子、每类约200张经灰度化与中值滤波的多角度图片,并附训练好的Red_Chess.yml与Black_Chess.yml模型。目前已有266人学习,代码均经测试运行成功,答辩评审平均分达96分,读者可据此掌握级联分类器训练流程、图像预处理与识别思路,也可在源码基础上修改扩展其他功能。

1. 从一张棋盘照片到棋子坐标:级联分类器识别中国象棋到底在做什么

拍一张中国象棋残局照片,想让程序自动说出“红方车在二路、黑方炮在五路”,这件事听起来像是一个完整的视觉系统,但拆开看,第一步其实非常朴素:把棋盘上每一个棋子从背景里抠出来,判断它是红帅、黑卒还是红马。这一步做不稳,后面的走子识别、棋谱生成全是空中楼阁。而 OpenCV 的级联分类器(Cascade Classifier)恰好是解决“小目标、固定形状、样本可控”这类问题的老牌工具,它不依赖 GPU,不依赖深度学习框架,一台普通笔记本就能跑,特别适合刚接触 OpenCV 图像处理项目、想拿一个完整闭环练手的开发者。

这个标题里的“中国象棋棋子识别”并不是一个玩具任务。棋子是圆形、有汉字、红黑两色、棋盘背景有网格线和河界文字,光照一变,阈值分割就翻车。级联分类器用 Haar-like 特征加 AdaBoost 训练,推理时用积分图加速,对圆形轮廓和汉字纹理的区分能力比很多人想象中强。配套的数据集和注释拉满的源码,价值在于让你不用从零标注几千张图,直接跑通“采集→标注→训练→检测→后处理”这条链路。适合谁?适合已经会 Python 基础语法、装过 OpenCV、但还没完整做过一个自定义目标检测项目的人。下面按我实际复现的顺序,把每一步的参数和坑讲清楚。

2. 级联分类器的训练链路:从棋子样本到 XML 模型

2.1 为什么棋子识别适合用级联分类器而不是 YOLO

很多人第一反应是“都什么年代了还用级联分类器,直接上 YOLOv8 训练自己的数据集不香吗”。这话对一半。YOLO 系列在通用目标检测上确实碾压级联分类器,但放到中国象棋棋子这个具体场景,级联分类器有三个现实优势。第一,棋子是刚性物体,没有姿态变化,圆形轮廓在 32×32 的窗口里非常稳定,Haar-like 特征里的边缘特征和线特征足以描述“圆边+内部汉字笔画”的模式。第二,级联分类器的推理速度在 CPU 上极快,一张 1920×1080 的棋盘图,多尺度滑动窗口检测通常 200ms 内出结果,不需要 CUDA 环境,这对“linunx安装cuda版本opencv”踩过坑的人来说是解脱。第三,训练样本需求小,每个棋子类别 200~500 张正样本就能收敛,而 YOLO 通常要上千张且标注框要更精细。

当然,级联分类器的边界也很清楚:它做的是“有没有”的二分类滑窗检测,不是端到端的多类别回归。所以实际方案是“每个棋子类别训练一个级联分类器”,检测时并行跑多个分类器,再用非极大值抑制合并重叠框。红方七个兵种加黑方七个兵种,一共 14 个分类器,听起来多,但每个训练 20 分钟以内,总时间可控。

2.2 正负样本准备:棋子数据集怎么切、怎么增强

拿到“象棋分类数据集”后,第一件事不是直接扔进训练,而是检查样本分布。常见的数据集结构是dataset/red_che/dataset/black_ma/这样的文件夹,每个文件夹里是棋子的小图。正样本要求:只包含棋子本体,尽量不带棋盘网格线,尺寸统一缩放到 24×24 或 32×32。负样本要求:从棋盘背景、河界文字、空白格、手部遮挡区域随机裁剪,数量至少是正样本的 3 倍。

我一般会写一个预处理脚本,把正样本统一灰度化、直方图均衡化,再做小角度旋转(±10°)和亮度扰动(±20%),扩充到每类 400 张左右。负样本从原始棋盘大图里用滑动窗口裁,裁的时候避开棋子区域。这一步的代码不复杂,但决定了后面模型是不是“玄学收敛”。

import cv2 import os import numpy as np import random def augment_positive(src_dir, dst_dir, target_size=(32, 32), augment_num=3): """ 对正样本做灰度化、均衡化、旋转和亮度扰动 src_dir: 原始棋子小图目录 dst_dir: 增强后输出目录 target_size: 统一尺寸 augment_num: 每张原图生成几张增强图 """ if not os.path.exists(dst_dir): os.makedirs(dst_dir) for fname in os.listdir(src_dir): img = cv2.imread(os.path.join(src_dir, fname)) if img is None: continue gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) gray = cv2.resize(gray, target_size) cv2.imwrite(os.path.join(dst_dir, fname), gray) for i in range(augment_num): angle = random.uniform(-10, 10) M = cv2.getRotationMatrix2D((target_size[0]//2, target_size[1]//2), angle, 1.0) rotated = cv2.warpAffine(gray, M, target_size, borderMode=cv2.BORDER_REPLICATE) beta = random.uniform(-20, 20) bright = cv2.convertScaleAbs(rotated, alpha=1.0, beta=beta) out_name = f"{os.path.splitext(fname)[0]}_aug{i}.png" cv2.imwrite(os.path.join(dst_dir, out_name), bright) # 调用示例 augment_positive("dataset/red_che/raw", "dataset/red_che/pos", augment_num=3)

这段代码的逻辑是:先灰度化和直方图均衡化,消除红黑颜色差异对 Haar 特征的干扰;再统一尺寸,因为opencv_traincascade要求所有正样本尺寸一致;最后做旋转和亮度扰动,模拟拍摄角度和光照变化。参数上,target_size建议用 24×24 或 32×32,太小会丢失汉字笔画,太大训练慢且容易过拟合。augment_num取 3 到 5 之间,太多会导致样本高度相似,反而降低泛化。

负样本准备用另一个脚本,从棋盘大图里随机裁 32×32 的窗口,但要用一个简单的颜色判断跳过棋子区域(棋子区域饱和度或红色通道明显偏高)。

def generate_negatives(board_img_path, dst_dir, num=2000, win_size=(32, 32)): """ 从棋盘大图中随机裁剪负样本,跳过棋子区域 """ img = cv2.imread(board_img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) h, w = gray.shape count = 0 while count < num: x = random.randint(0, w - win_size[0]) y = random.randint(0, h - win_size[1]) patch = gray[y:y+win_size[1], x:x+win_size[0]] # 简单跳过过亮或过暗区域,保证负样本有纹理 if patch.mean() < 30 or patch.mean() > 220: continue cv2.imwrite(os.path.join(dst_dir, f"neg_{count}.png"), patch) count += 1

负样本的关键是“多样性”。如果只从空白棋盘格裁,模型会把所有带网格线的区域都当成负样本,遇到河界文字就误检。所以负样本里要混入河界文字、棋盘边框、甚至手指边缘。数量上,每个棋子分类器对应 2000 张负样本是底线,我一般用 3000 张。

2.3 用 opencv_traincascade 训练:参数怎么设、日志怎么看

OpenCV 自带的opencv_traincascade是命令行工具,训练前需要生成正样本描述文件(.vec)和负样本列表文件(neg.txt)。正样本描述文件用opencv_createsamples生成,命令如下:

# 生成正样本 vec 文件,-num 是正样本数量,-w -h 是样本尺寸 opencv_createsamples -info pos.txt -vec pos.vec -num 400 -w 32 -h 32 # 开始训练,-data 是输出目录,-numPos 要略小于实际正样本数 opencv_traincascade -data cascade_red_che -vec pos.vec -bg neg.txt \ -numPos 380 -numNeg 3000 -numStages 15 -featureType HAAR \ -w 32 -h 32 -minHitRate 0.995 -maxFalseAlarmRate 0.5 \ -mode ALL -precalcValBufSize 1024 -precalcIdxBufSize 1024

参数逐个说。-numPos不能等于实际正样本数,因为训练过程中每级会淘汰一部分,设成实际数量的 90%~95% 比较稳。-numNeg是每级使用的负样本数,3000 是经验值,太少会导致误检率高。-numStages是级联层数,15 级对棋子这种简单目标够了,层数越多误检越低但训练越慢。-minHitRate 0.995表示每级至少保留 99.5% 的正样本,这个值设太高会导致层数涨不上去,设太低会漏检。-maxFalseAlarmRate 0.5是每级的最大误报率,级联的总体误报率是各级乘积,15 级下来理论误报率是 0.5^15,非常低。

训练日志里重点看两个指标:N | HR | FA。HR 是当前级的命中率,FA 是误报率。如果某一级的 FA 降不下去,说明负样本不够或者特征区分度不足,需要补负样本。如果训练到第 8 级就停了,说明-minHitRate设太高,正样本被淘汰完了。我踩过的坑是-precalcValBufSize-precalcIdxBufSize设太小导致训练极慢,设成 1024MB 以上,内存够就设 2048。

训练完成后,cascade_red_che目录里会生成cascade.xml,这就是最终模型文件。14 个棋子类别重复这个过程,得到 14 个 XML。

3. 检测与后处理:把 14 个分类器串成完整识别流程

3.1 多分类器并行检测与非极大值抑制

有了 14 个cascade.xml,检测时用cv2.CascadeClassifier逐个加载,对同一张棋盘图跑detectMultiScale。每个分类器返回一组矩形框,但同一个棋子可能被多个分类器同时检出(比如红车和黑车的圆形轮廓相似),也可能同一个棋子被同一分类器检出多个重叠框。所以需要两步后处理:先按类别做非极大值抑制(NMS),再跨类别做置信度筛选。

import cv2 import numpy as np def nms(boxes, scores, iou_thresh=0.3): """ 标准 NMS,boxes 格式 [x, y, w, h] """ if len(boxes) == 0: return [] boxes = np.array(boxes) scores = np.array(scores) x1 = boxes[:, 0] y1 = boxes[:, 1] x2 = boxes[:, 0] + boxes[:, 2] y2 = boxes[:, 1] + boxes[:, 3] areas = (x2 - x1) * (y2 - y1) order = scores.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) xx1 = np.maximum(x1[i], x1[order[1:]]) yy1 = np.maximum(y1[i], y1[order[1:]]) xx2 = np.minimum(x2[i], x2[order[1:]]) yy2 = np.minimum(y2[i], y2[order[1:]]) w = np.maximum(0, xx2 - xx1) h = np.maximum(0, yy2 - yy1) inter = w * h iou = inter / (areas[i] + areas[order[1:]] - inter) inds = np.where(iou <= iou_thresh)[0] order = order[inds + 1] return boxes[keep].tolist() def detect_chess_pieces(img_path, cascade_dir, piece_names, scaleFactor=1.1, minNeighbors=5): """ 加载所有棋子分类器,检测并做 NMS """ img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) all_boxes = [] all_scores = [] all_labels = [] for idx, name in enumerate(piece_names): xml_path = f"{cascade_dir}/{name}/cascade.xml" cascade = cv2.CascadeClassifier(xml_path) if cascade.empty(): print(f"分类器加载失败: {xml_path}") continue boxes = cascade.detectMultiScale(gray, scaleFactor=scaleFactor, minNeighbors=minNeighbors, minSize=(24, 24), maxSize=(64, 64)) for (x, y, w, h) in boxes: all_boxes.append([x, y, w, h]) # 用 minNeighbors 作为粗略置信度,实际可用分类器输出权重 all_scores.append(minNeighbors) all_labels.append(idx) # 按类别分组做 NMS final_results = [] for label in set(all_labels): idxs = [i for i, l in enumerate(all_labels) if l == label] boxes = [all_boxes[i] for i in idxs] scores = [all_scores[i] for i in idxs] kept = nms(boxes, scores) for b in kept: final_results.append((b, piece_names[label])) return final_results

这段代码的核心是detectMultiScale的三个参数。scaleFactor=1.1表示每次图像缩小 10%,值越小检测越细但越慢,1.1 是速度和精度的平衡点。minNeighbors=5表示一个候选框周围至少要有 5 个重叠框才保留,值越大误检越少但漏检越多,棋子场景建议 4~6。minSizemaxSize根据棋盘图里棋子的实际像素大小设,一般 24 到 64 之间。

NMS 的iou_thresh=0.3是经验值,棋子之间间距较大,0.3 能有效合并同一棋子的多个框,又不会把相邻棋子误合并。

3.2 棋盘坐标映射:从像素框到“二路五进一”

检测出棋子框只是第一步,要生成棋谱还得把像素坐标映射到棋盘九宫十线的逻辑坐标。常见做法是先用霍夫圆检测或轮廓检测找到棋盘外框,做透视变换把棋盘摆正,然后按 9 列 10 行均匀划分网格,每个棋子框的中心点落在哪个网格就映射到哪个坐标。

def map_to_board_coord(box, board_rect, grid_cols=9, grid_rows=10): """ box: [x, y, w, h] 棋子框 board_rect: [bx, by, bw, bh] 棋盘外接矩形 返回 (col, row),col 0-8,row 0-9 """ cx = box[0] + box[2] / 2 cy = box[1] + box[3] / 2 bx, by, bw, bh = board_rect col = int((cx - bx) / (bw / grid_cols)) row = int((cy - by) / (bh / grid_rows)) col = max(0, min(grid_cols - 1, col)) row = max(0, min(grid_rows - 1, row)) return col, row

棋盘外接矩形可以用cv2.findContours找最大轮廓,再用cv2.minAreaRect得到旋转矩形,最后用cv2.warpPerspective做透视校正。这一步的坑是棋盘边框可能被棋子遮挡,导致轮廓不完整,我一般会先做边缘检测再膨胀,把边框连起来。

坐标映射的精度取决于棋盘校正的精度。如果校正后网格线不直,映射就会偏一格。验证方法是把映射结果画回原图,看棋子中心是否落在网格交点上。

4. 避坑与排查:棋子识别项目里最容易翻车的 5 个地方

4.1 训练时正样本 vec 文件生成失败,报错 “Unable to open pos.txt”

现象:运行opencv_createsamples时提示找不到pos.txt或格式错误。原因:pos.txt每行的格式是图片路径 目标数量 x y w h,路径必须是相对路径或绝对路径且不能有中文,目标数量通常是 1。很多人直接从标注工具导出,格式对不上。解决:用脚本统一生成pos.txt,确保每行只有一张图、一个目标,路径用正斜杠。

4.2 检测结果全是误检,棋盘网格线被当成棋子

现象:detectMultiScale返回大量框,但框里没有棋子。原因:负样本里缺少棋盘网格线和河界文字的样本,分类器没学会区分“圆形棋子”和“直线网格”。解决:补 1000 张以上包含网格线和文字的负样本,重新训练。另一个快速缓解办法是提高minNeighbors到 8,但会漏检小棋子。

4.3 红方棋子和黑方棋子互相误识别

现象:红车被识别成黑车,或者红帅被识别成黑将。原因:灰度化后红黑颜色差异消失,Haar 特征只看到圆形和汉字轮廓,而红车和黑车的汉字结构相似。解决:不要完全灰度化,保留颜色通道信息,或者对红黑分别用不同的预处理(红色通道增强 vs 绿色通道增强)。另一个办法是训练时把红黑棋子混在一起做多类别,但级联分类器不支持多类别,只能靠后处理按颜色投票。

4.4 训练到一半报 “Train dataset for temp stage can not be filled”

现象:opencv_traincascade在某一级停止,提示无法填充训练数据集。原因:-numPos设得太大,接近或等于实际正样本数,前几级淘汰后剩余正样本不够下一级用。解决:把-numPos降到实际正样本数的 85%~90%,或者增加正样本数量。我一般会预留 20% 的余量。

4.5 检测速度慢,一张图跑好几秒

现象:14 个分类器串行跑detectMultiScale,每张图耗时超过 3 秒。原因:scaleFactor设太小(比如 1.01),或者图像分辨率太高没有预缩放。解决:先把棋盘图缩放到宽度 800 像素左右再检测,scaleFactor用 1.1 到 1.2,minSize根据缩放后的棋子大小调整。如果还慢,可以用多线程并行跑分类器,Python 的concurrent.futures就能做。

5. 进阶技巧:用检测结果反推棋局并验证识别准确率

5.1 从检测框生成 FEN 字符串

识别出所有棋子的类别和坐标后,可以按中国象棋的 FEN 格式生成棋局字符串。FEN 的规则是:从黑方底线开始,从左到右,每个棋子用字母表示(红方大写、黑方小写),空位用数字表示连续空格数,行之间用/分隔。比如红方车在二路五、黑方炮在五路五,对应 FEN 里的Rc

def generate_fen(pieces, grid_cols=9, grid_rows=10): """ pieces: [(col, row, piece_name), ...] piece_name 映射到 FEN 字母 """ fen_map = { "red_che": "R", "red_ma": "N", "red_xiang": "B", "red_shi": "A", "red_jiang": "K", "red_pao": "C", "red_bing": "P", "black_che": "r", "black_ma": "n", "black_xiang": "b", "black_shi": "a", "black_jiang": "k", "black_pao": "c", "black_zu": "p" } board = [["1" for _ in range(grid_cols)] for _ in range(grid_rows)] for col, row, name in pieces: if name in fen_map: board[row][col] = fen_map[name] fen_rows = [] for row in board: fen_row = "" empty = 0 for cell in row: if cell == "1": empty += 1 else: if empty > 0: fen_row += str(empty) empty = 0 fen_row += cell if empty > 0: fen_row += str(empty) fen_rows.append(fen_row) return "/".join(fen_rows)

这个 FEN 字符串可以直接喂给象棋引擎做走法分析,或者和标准棋谱对比验证识别准确率。参数上,grid_cols=9grid_rows=10是中国象棋标准,如果棋盘校正后行列反了,需要交换。

5.2 用混淆矩阵定位哪个棋子最容易错

验证识别效果不能只看“感觉准不准”,要跑一个带标注的测试集,统计每个类别的准确率和召回率。我一般会标 50 张棋盘图,每张图里所有棋子的类别和坐标都人工标好,然后跑检测脚本,用 IoU 大于 0.5 且类别一致作为正确匹配,生成混淆矩阵。

真实类别 \ 预测类别红车红马红炮黑车黑马黑炮
红车4801100
红马0470021
红炮1046003
黑车2004512
黑马0300461
黑炮0021047

从这张表能看出,红炮和黑炮互相误识别最多,因为炮的汉字结构和圆形轮廓在灰度化后非常接近。改进办法是给炮类单独增加负样本,把对方颜色的炮样本作为负样本喂进去。另一个发现是红车和黑车也有少量互误,同样用颜色特征做后处理投票可以压下去。

5.3 我踩过的最大的坑:别在训练集上验证

刚做这个项目时,我用训练用的正样本直接跑检测,准确率 99%,兴奋得不行。结果换了一张新拍的棋盘图,准确率掉到 60%。血泪经验是:训练集和验证集必须来自不同的拍摄批次,光照、角度、棋盘材质都要有差异。我后来固定用 70% 样本训练、30% 样本验证,并且验证集里至少包含 5 张完全没参与训练的棋盘图。这个习惯让我在后面做任何 OpenCV 图像处理项目时都少走了很多弯路。

如果你打算把这个方案落地到实际对弈记录场景,建议先把检测准确率做到 95% 以上再考虑棋谱生成,否则后面每一步都在还债。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询