资讯动态

OpenCV级联分类器实战:中国象棋棋子识别与棋谱生成

发布时间:2026/9/23 13:55:05 来源:尧图企业网站定制
简介这份资源面向计算机相关专业的在校学生、教师及企业员工提供基于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”踩过坑的人来说是解脱。第三训练样本需求小每个棋子类别 200500 张正样本就能收敛而 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_num3): 对正样本做灰度化、均衡化、旋转和亮度扰动 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, borderModecv2.BORDER_REPLICATE) beta random.uniform(-20, 20) bright cv2.convertScaleAbs(rotated, alpha1.0, betabeta) 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_num3)这段代码的逻辑是先灰度化和直方图均衡化消除红黑颜色差异对 Haar 特征的干扰再统一尺寸因为opencv_traincascade要求所有正样本尺寸一致最后做旋转和亮度扰动模拟拍摄角度和光照变化。参数上target_size建议用 24×24 或 32×32太小会丢失汉字笔画太大训练慢且容易过拟合。augment_num取 3 到 5 之间太多会导致样本高度相似反而降低泛化。负样本准备用另一个脚本从棋盘大图里随机裁 32×32 的窗口但要用一个简单的颜色判断跳过棋子区域棋子区域饱和度或红色通道明显偏高。def generate_negatives(board_img_path, dst_dir, num2000, 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:ywin_size[1], x:xwin_size[0]] # 简单跳过过亮或过暗区域保证负样本有纹理 if patch.mean() 30 or patch.mean() 220: continue cv2.imwrite(os.path.join(dst_dir, fneg_{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_thresh0.3): 标准 NMSboxes 格式 [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, scaleFactor1.1, minNeighbors5): 加载所有棋子分类器检测并做 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, scaleFactorscaleFactor, minNeighborsminNeighbors, 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的三个参数。scaleFactor1.1表示每次图像缩小 10%值越小检测越细但越慢1.1 是速度和精度的平衡点。minNeighbors5表示一个候选框周围至少要有 5 个重叠框才保留值越大误检越少但漏检越多棋子场景建议 46。minSize和maxSize根据棋盘图里棋子的实际像素大小设一般 24 到 64 之间。NMS 的iou_thresh0.3是经验值棋子之间间距较大0.3 能有效合并同一棋子的多个框又不会把相邻棋子误合并。3.2 棋盘坐标映射从像素框到“二路五进一”检测出棋子框只是第一步要生成棋谱还得把像素坐标映射到棋盘九宫十线的逻辑坐标。常见做法是先用霍夫圆检测或轮廓检测找到棋盘外框做透视变换把棋盘摆正然后按 9 列 10 行均匀划分网格每个棋子框的中心点落在哪个网格就映射到哪个坐标。def map_to_board_coord(box, board_rect, grid_cols9, grid_rows10): box: [x, y, w, h] 棋子框 board_rect: [bx, by, bw, bh] 棋盘外接矩形 返回 (col, row)col 0-8row 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.2minSize根据缩放后的棋子大小调整。如果还慢可以用多线程并行跑分类器Python 的concurrent.futures就能做。5. 进阶技巧用检测结果反推棋局并验证识别准确率5.1 从检测框生成 FEN 字符串识别出所有棋子的类别和坐标后可以按中国象棋的 FEN 格式生成棋局字符串。FEN 的规则是从黑方底线开始从左到右每个棋子用字母表示红方大写、黑方小写空位用数字表示连续空格数行之间用/分隔。比如红方车在二路五、黑方炮在五路五对应 FEN 里的R和c。def generate_fen(pieces, grid_cols9, grid_rows10): 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_cols9、grid_rows10是中国象棋标准如果棋盘校正后行列反了需要交换。5.2 用混淆矩阵定位哪个棋子最容易错验证识别效果不能只看“感觉准不准”要跑一个带标注的测试集统计每个类别的准确率和召回率。我一般会标 50 张棋盘图每张图里所有棋子的类别和坐标都人工标好然后跑检测脚本用 IoU 大于 0.5 且类别一致作为正确匹配生成混淆矩阵。真实类别 \ 预测类别红车红马红炮黑车黑马黑炮红车4801100红马0470021红炮1046003黑车2004512黑马0300461黑炮0021047从这张表能看出红炮和黑炮互相误识别最多因为炮的汉字结构和圆形轮廓在灰度化后非常接近。改进办法是给炮类单独增加负样本把对方颜色的炮样本作为负样本喂进去。另一个发现是红车和黑车也有少量互误同样用颜色特征做后处理投票可以压下去。5.3 我踩过的最大的坑别在训练集上验证刚做这个项目时我用训练用的正样本直接跑检测准确率 99%兴奋得不行。结果换了一张新拍的棋盘图准确率掉到 60%。血泪经验是训练集和验证集必须来自不同的拍摄批次光照、角度、棋盘材质都要有差异。我后来固定用 70% 样本训练、30% 样本验证并且验证集里至少包含 5 张完全没参与训练的棋盘图。这个习惯让我在后面做任何 OpenCV 图像处理项目时都少走了很多弯路。如果你打算把这个方案落地到实际对弈记录场景建议先把检测准确率做到 95% 以上再考虑棋谱生成否则后面每一步都在还债。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价