
简介基于OpenCV级联分类器的中国象棋棋子识别系统是一套面向高校计算机专业学生课程设计或期末大作业的完整实践项目。系统依托Python与OpenCV视觉库通过级联分类器实现红黑棋子的自动化检测覆盖数据集准备、模型训练与识别测试全流程。压缩包共19个文件包含3个Python源码模块、训练与测试数据集、模型备份以及JPG测试结果图整体大小31.39MB结构清晰便于直接部署调参。已有76人学习浏览适合正在做图像处理、机器学习方向课题的学生参考。资源内提供可直接运行的代码与说明文档并有分色识别效果图帮助验证算法是一套可快速上手、具备完整闭环的实战范例。1. 基于OpenCV级联分类器的中国象棋棋子识别系统先想清检测和识别是两件事拿到“基于OpenCV级联分类器的中国象棋棋子识别系统”这个题目最容易踩的第一个坑是以为级联分类器能直接分清楚“车马炮”。OpenCV的CascadeClassifier落地时只做二分类它返回的是一串“可能是棋子”的矩形框真正回答“这是红车还是黑马”的是它后面的另一个识别层。所以这个项目按工作流其实拆成三块正负样本数据集、用OpenCV工具链训练出来的级联模型、以及Python推理管道里负责精读棋面文字的字符识别模块。数据集干净与否决定训练能不能收敛级联模型决定你能找到多少个棋子字符层决定最后报给用户的棋子名对不对。适合看这篇的人是想用传统图像处理而不是深度学习来做棋子定位的学生要在嵌入式或低配环境里跑棋盘识别的工程师以及刚跑通OpenCV但还不知道怎么组织训练样本的初级开发。2. 数据准备用OpenCV生成中国象棋棋子正样本与负样本2.1 正样本怎么来先用HoughCircles自动切棋子圆盘正样本的源头通常是一张棋盘照片或一段对弈视频。人工把棋子一个个裁出来可行但效率太低。更常用的做法是借“棋子是圆形”这个先验先用cv2.HoughCircles做一次预标注把棋盘上每个棋子变成圆心加半径再按半径乘系数切出正方形ROI存成待标注正样本。import cv2 import numpy as np img cv2.imread(source_board.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray cv2.medianBlur(gray, 5) # 中值滤波压掉棋盘纹路噪声 circles cv2.HoughCircles( gray, cv2.HOUGH_GRADIENT, dp1.2, # 累加器分辨率与输入图像分辨率的比值1.1~1.3常用 minDist30, # 相邻两个棋子中心的最小像素距离按棋盘格距调 param180, # Canny高阈值光线差时往下调 param270, # 圆心累加器阈值调大可以减少假圆 minRadius18, maxRadius55 ) if circles is not None: for x, y, r in np.round(circles[0]).astype(int): side int(r * 2.2) # 半径乘2.2让字符带一点外圈留白 x0, y0 x - side // 2, y - side // 2 roi img[y0:y0 side, x0:x0 side] cv2.imwrite(fdataset/positive/{x}_{y}.jpg, roi)这段代码的逻辑是先转灰度并做中值滤波防止棋盘交叉线干扰边缘检测HoughCircles把棋子变成圆再把每个圆按外接正方形裁出来。为什么半径要乘2.2而不是直接取直径中国象棋棋子外沿有一圈深色边框识别字符时真正重要的是内圈文字留一点边距能让后续二值化不把文字和边缘黏在一起。参数字典里最值得调的是param2它控制“圆”的置信度调高了会漏掉被手指遮挡的棋子调低了会把棋盘上的圆形纹理也当成棋子实际调试时建议先固定minDist和minRadius只动param2一个参数。切出来的ROI还需要人工筛一遍把误检、粘连、裁偏的图删掉剩下干净样本。这一步不能省级联分类器对噪声样本的容忍度远低于深度学习模型。2.2 生成vec用positives.txt加opencv_createsamples正样本整理好后需要把图片转成OpenCV训练用的vec文件。最稳妥的方式是写一个positives.txt标注文件每行格式是“图片路径 目标数量 x y w h”dataset/positive/100_200.jpg 1 0 0 64 64 dataset/positive/320_150.jpg 1 2 3 60 60 dataset/positive/450_80.jpg 1 0 0 68 68然后执行opencv_createsamples -info dataset/positives.txt -vec dataset/positives.vec -num 900 -w 32 -h 32-w和-h是级联训练时的样本归一化尺寸整个流程里必须始终一致后面opencv_traincascade用32×32这里就不能写成64×64。-num是生成多少个正样本通常小于等于标注文件里的目标总数。如果觉得样本量不够可以给opencv_createsamples追加-maxxangle 0.3 -maxyangle 0.3 -maxzangle 0.6让它从原始ROI随机生成旋转、缩放和透视变换的增强样本。中国象棋棋子会被随手拧到任意方向所以-maxzangle尽量给到0.5以上否则训练出来的模型对转过的棋子完全不买账。生成后建议马上看一眼opencv_createsamples -vec dataset/positives.vec -w 32 -h 32 -show弹出的窗口会逐张显示vec里的样本。这一步主要检查两件事一是字符是否完整出现在画面里二是背景是否露出太多边缘。整个正样本的重要程度高于任何参数很多项目训练失败回头看都是vec里混进了半张手指或半个棋盘。2.3 负样本组成与bg.txt组织负样本是级联分类器里更容易被忽略、但决定误报率的一半。负样本并不需要和正样本一样归一化到32×32训练器会从每张图里随机裁窗口所以你只需要提供一张张原始图。建议的负样本构成比例可以参考下面这张表负样本内容作用建议数量空棋盘与棋盘网格交叉区压住网格纹理误报800张以上棋子周围的木质/塑料桌面压住材质误报800张以上对局照片里的手、纸、笔提高泛化能力500张以上含汉字印刷体或书法字防止单个汉字被当成棋子300张以上总负样本数建议2500张起步越多越好但不要用清一色的纯色图充数。纯色负样本会让训练阶段学不到“什么不是棋子”级联在真实棋盘上一跑就全是误检。负样本路径列表写进dataset/bg.txt每行一个相对路径ls dataset/negative/*.jpg dataset/bg.txt注意bg.txt里的路径在Windows下不要把反斜杠直接丢给opencv_traincascade统一转成正斜杠否则OpenCV 4.x会把路径整个读成空文件报错信息还不明显。3. 训练级联Haar与LBP怎么选opencv_traincascade的关键参数3.1 特征类型权衡为什么中国象棋棋子优先选LBP级联分类器支持的特征类型不止一种选错特征会让训练时间差出好几倍。OpenCV里能直接在opencv_traincascade里用的主要有Haar、LBP和HOG它们是从不同角度描述图像纹理的特征类型计算方式训练耗时光照敏感度适合场景Haar相邻矩形区域像素和之差较慢中等边缘清晰的刚性物体LBP局部像素与中心像素比大小快低纹理物体、低分辨率图像HOG统计梯度方向直方图更慢中等行人与大尺寸目标中国象棋棋子的结构是“外圈深色环、中间浅色底、内部深色汉字”这是个强纹理目标而不是强边缘目标。LBP做的是局部邻域比较对光照变化和棋盘木纹干扰的容忍度远高于Haar训练速度也快不少。同样的样本量LBP在普通CPU上训练15个stage大概几十分钟Haar可能要多等一倍时间。另外LBP对低分辨率非常友好32×32的样本里汉字笔画还能辨认用Haar反而更容易被噪声带偏。但这同时带来一个代价LBP对字符间的细微差异不敏感比如“士”和“仕”这种字形相近的棋子它区分不了。所以“级联负责找到棋子、后处理负责分清字符”是个天然合理的分工别指望把识别也压进级联里。3.2 opencv_traincascade训练命令与参数卡数据齐了以后训练命令长这样opencv_traincascade \ -data model/chess_cascade \ -vec dataset/positives.vec \ -bg dataset/bg.txt \ -numPos 900 \ -numNeg 2400 \ -numStages 15 \ -featureType LBP \ -w 32 -h 32 \ -minHitRate 0.995 \ -maxFalseAlarmRate 0.5 \ -precalcValBufSize 2048 \ -precalcIdxBufSize 2048逐项说明一下这些参数在项目里的含义和调整方向参数含义建议值调整方式-numPos每轮参与训练的正样本数vec总数的90%训练中断且提示样本不够时降到85%-numNeg每轮参与的负样本数2400~3000误检偏高就往上加-numStages级联阶段总数14~16误检还很明显的就加到20-minHitRate单阶段最低检测率0.995低于0.99容易漏棋-maxFalseAlarmRate单阶段最大误检率0.5降到0.4能更准但要更多stage-featureType特征类型LBPHaar慢但可作对照组-w -h样本尺寸32 32不要大于40否则训练很慢-numPos这个参数最容易让人困惑。它不要求等于vec里的样本总数因为训练器每完成一个stage都会从vec里重新取样某些样本会被重复消耗设得太顶会在后期报Train dataset for temp stage can not be filled。我一般直接把-numPos设成vec实际样本数的85%~90%宁可少训一点也别让训练跑两小时后崩掉。-minHitRate则决定了单个stage“必须保住多少正样本”0.995意味着每个阶段允许漏掉千分之五的棋子对棋盘这种目标明确的小物体足够用了。3.3 训练中断的典型原因与对策opencv_traincascade报错信息一向很“节俭”不查日志基本看不出问题。实际项目里最常见的失败模式就是下表的几类报错现场真实原因处理办法Could not open .../bg.txt路径分隔符或文件编码统一ASCII路径、正斜杠Assertion failed (elements_read 1)vec文件损坏或尺寸不一致重新生成vec检查-w -hTrain dataset for temp stage can not be filled正样本不足以支撑本轮把-numPos调到90%以下卡在stage 3~6不动也不退出负样本太单调增加真实棋盘负样本训练完成但检测全是误报正样本里混入非棋子回头看vec清掉脏样本还有一个常被忽略的问题是内存参数。-precalcValBufSize和-precalcIdxBufSize单位是MB默认只有256MB训练到后期经常变慢。机器内存足够就给到2048级联训练会明显提速。若在OpenCV 4.x下找不到opencv_traincascade命令多半是装的Python包不包含命令行工具去装完整版OpenCV即可。4. Python源码级联分类器加载、棋子ROI精修与字符识别4.1 加载级联并生成棋子候选矩形训练完成后目录里会多出一个.xml文件推理阶段直接用cv2.CascadeClassifier加载。先看最基本的检测代码import cv2 cascade cv2.CascadeClassifier(model/chess_cascade.xml) def detect_piece_candidates(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) rects cascade.detectMultiScale( gray, scaleFactor1.05, # 尺度步长1.05比1.1准但慢 minNeighbors4, # 抑制重复框越大误检越少但漏检越多 minSize(24, 24), maxSize(180, 180) ) return rectsscaleFactor是图像金字塔的缩放步长1.05表示每次把图像缩小5%再扫一遍值越小扫的层数越多。中国象棋棋子在棋盘上的尺寸相对稳定scaleFactor1.05通常能在准确率和速度之间取得平衡。minNeighbors控制一个候选区域要被多少个邻居矩形确认才算数如果发现一枚棋子周围叠了三四个框就往大到5如果漏检严重就先降回3。equalizeHist做的是全局直方图均衡能缓解棋盘受单侧光影响导致半边过亮半边过暗的问题。4.2 候选矩形不等于棋子圆度校验过滤背景误检级联输出的矩形框只是候选尤其棋盘外的手、笔、纸也可能被框进去。常见做法是拿一个圆度校验做前置过滤因为棋子是个标准圆形而误检的矩形通常轮廓不规则import numpy as np def is_circular(roi, min_circularity0.25): gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) _, th cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY) th cv2.morphologyEx(th, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) contours, _ cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return False c max(contours, keycv2.contourArea) area cv2.contourArea(c) perimeter cv2.arcLength(c, True) if perimeter 0: return False circularity 4 * np.pi * area / (perimeter * perimeter) return circularity min_circularity圆度的取值范围最大是1越接近1越圆。棋子的二值轮廓圆度一般在0.5以上低于0.25的基本都是不规则物体。这段代码把ROI先做阈值分割再提取最外层轮廓算圆度好处是不需要额外训练坏处是对光照太暗的图会失灵所以阈值180这个值要根据具体环境微调。4.3 字符识别预旋转模板匹配的实现圆形候选确定后进入最关键的字符识别。最简单的可行方案是模板匹配但注意棋子可能被转到任何角度所以需要预先把模板旋转到多个角度再和ROI做归一化匹配。我一般把模板做成一个类初始化时离线旋转好避免在线推理时反复计算旋转矩阵import os import cv2 class ChessTemplates: def __init__(self, template_dir, step10): self.step step self.items {} for name in os.listdir(template_dir): path os.path.join(template_dir, name) if not os.path.isfile(path): continue tpl cv2.imread(path, cv2.IMREAD_GRAYSCALE) tpl cv2.resize(tpl, (64, 64)) rots [] for angle in range(0, 360, step): M cv2.getRotationMatrix2D((32, 32), angle, 1.0) rot cv2.warpAffine(tpl, M, (64, 64), flagscv2.INTER_CUBIC) rots.append(rot) self.items[os.path.splitext(name)[0]] rots def match(self, roi_bin): best_name, best_score None, -1.0 for name, rots in self.items.items(): for rot in rots: res cv2.matchTemplate(roi_bin, rot, cv2.TM_CCOEFF_NORMED) _, score, _, _ cv2.minMaxLoc(res) if score best_score: best_name, best_score name, score return best_name, best_score配套的调用逻辑分三步ROI转灰度并均衡化、OTSU二值化反色、再丢进match。直接使用cv2.matchTemplate的TM_CCOEFF_NORMED它返回的是归一化相关系数对整体亮度不敏感非常适合棋子这种“浅底深字”的二元结构。旋转模板时用INTER_CUBIC插值是因为字符笔画经过旋转后容易出现锯齿插值质量直接决定识别上限。def recognize_piece(roi, template_matcher): gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) gray cv2.resize(gray, (64, 64)) _, roi_bin cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) return template_matcher.match(roi_bin)注意二值化用了THRESH_BINARY_INV反转因为模板匹配对“白底黑字”和“黑底白字”是非常敏感的必须统一。match返回的score是最大相关系数一般在0.6~0.9之间低于0.5的匹配结果基本不可信。把该阈值留在调用处判断别硬编码在类里。4.4 组合成完整的单张图片推理函数把上面几块串成入口函数输出一个带棋子名、坐标、置信度的结构化结果def analyze_board(frame, cascade, matcher, min_score0.55): rects detect_piece_candidates(frame) result [] for x, y, w, h in rects: roi frame[y:y h, x:x w] if not is_circular(roi): continue name, score recognize_piece(roi, matcher) if score min_score: result.append({ name: name, bbox: [int(x), int(y), int(w), int(h)], score: round(float(score), 4) }) return result这个函数的执行顺序是固定的先检测再圆度过滤最后字符识别。顺序不能反因为每个候选矩形都要做一次模板匹配如果先识别再做圆度过滤等于让所有误检都白白消耗了模板匹配的计算量。min_score设为0.55在本项目里能挡住大多数不清晰匹配实际值可以画出来看几帧再调。5. 实战排错中国象棋棋子重复检测、漏检的消解与实时性能手感5.1 半枚棋子出两个框用NMS合并重叠矩形棋盘棋子密集cascade经常出现同一枚棋子被两个矩形同时框住的情况。直接调minNeighbors能压一部分但副作用是漏掉被遮挡的棋子。更可控的做法是把级联的minNeighbors先适当调低让它把候选都放出来再用非极大值抑制NMS合并重叠框import numpy as np def nms(rects, scores, threshold0.3): if len(rects) 0: return np.array([]) rects np.array(rects, dtypenp.float32) scores np.array(scores, dtypenp.float32) x1 rects[:, 0] y1 rects[:, 1] x2 rects[:, 0] rects[:, 2] y2 rects[:, 1] rects[:, 3] 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:]]) inter np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1) area_i (x2[i] - x1[i]) * (y2[i] - y1[i]) area_o (x2[order[1:]] - x1[order[1:]]) * (y2[order[1:]] - y1[order[1:]]) iou inter / (area_i area_o - inter) order order[1:][iou threshold] return rects[keep].astype(int)IoU阈值0.3意味着两个矩形重叠超过30%就只保留置信度高的那个。在棋盘场景里同一枚棋子的两个检测框往往重叠70%以上而相邻两枚棋子之间的IoU通常不到10%所以0.3是个安全值。调用时把cascade返回的候选框和每个框对应的rejectLevels或匹配分数传给nms就能在不动minNeighbors的情况下把重复框压掉。5.2 误检和漏检的调参顺序联调阶段最烦人的是“这里多一个框、那里少一个框”。我习惯按下面这张表的顺序逐项检查而不是一上来就改训练参数现象优先动作备用手段一枚棋子重复框后续NMS /minNeighbors4→6缩小maxSize把棋子外物体框进来检查负样本里是否缺少同类背景提高min_score到0.6棋盘内侧棋子漏检scaleFactor降到1.04先放大输入图再检测远景小棋子漏检调低minSize到20级联判定阈值不够回训级联误检多时首先要排除的是“负样本里根本没有这类背景”的情况。比如摄像头会拍到鼠标键盘而训练负样本全是棋盘纹理那鼠标必然会被框出来。临时提高min_score只能掩盖现象治本还是补负样本回炉。漏检则相反多半是输入分辨率太低或scaleFactor太大让特征在金字塔中间层被跳过去了。5.3 边跑边调试可视化与性能优先的取舍实时场景里如果每帧都要跑模板匹配CPU会迅速打满。级联检测本身很快瓶颈几乎全在字符识别层的旋转模板匹配上。先做一个最简单有效的优化把输入帧缩放后再送检测坐标映射回原图即可。scale 0.6 small cv2.resize(frame, None, fxscale, fyscale) rects cascade.detectMultiScale(cv2.equalizeHist(cv2.cvtColor(small, cv2.COLOR_BGR2GRAY)), scaleFactor1.05, minNeighbors4) rects np.array(rects) / scale for x, y, w, h in rects.astype(int): roi frame[y:y h, x:x w]缩放后检测更快但ROI变小字符识别的成功率会下降。压缩到0.6倍后每个棋子的ROI可能只有30×30像素模板匹配依然能跑DPI足够。再往后可以试着把step从10度改成15度模板数减少三分之一识别精度只掉一点点。最后如果需要上真实时路建议把模板匹配换成基于cv2.matchShapes的轮廓匹配或干脆把ROI归一化后交给轻量CNN不过那就是另一套系统了。6. 用颜色先验把识别收窄到七个字符误报率大幅下降的验证技巧6.1 红色像素比例判黑红模板匹配在“车、马、炮”这些跨颜色同名棋子之间极易混淆因为红车和黑车只有颜色差异字形几乎一样。一个低成本高收益的技巧是在字符识别前先判断棋子属于红方还是黑方把模板候选集从十四个直接砍到七个。判断依据很直接红棋字符偏红黑棋字符偏黑。def split_color(roi): hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) red1 cv2.inRange(hsv, (0, 50, 40), (10, 255, 255)) red2 cv2.inRange(hsv, (170, 50, 40), (180, 255, 255)) red_ratio (cv2.countNonZero(red1) cv2.countNonZero(red2)) \ / (roi.shape[0] * roi.shape[1]) black cv2.inRange(hsv, (0, 0, 0), (180, 255, 70)) black_ratio cv2.countNonZero(black) / (roi.shape[0] * roi.shape[1]) if red_ratio 0.02: return red if black_ratio 0.08: return black return NoneOpenCV里HSV色彩的H通道范围是0到180红色恰好在0附近和180附近各占一段所以要写两个inRange再做或运算。red_ratio取0.02是因为红棋字符笔画占ROI总面积的比例通常就在2%~5%之间太低会把黑棋的暗红色木纹也算进去。拿到红黑标签后只加载对应颜色的七类模板其余模板直接跳过。这一步能让匹配耗时减半因为在线匹配的主要成本就是模板数量乘以旋转角度数。6.2 用混淆矩阵量化识别质量调完颜色分流后别只靠肉眼看几张图就说“效果好了”。把测试集里每张棋子的真实名称和预测名称记下来统计成一个混淆矩阵一版模型的识别短板马上现形from collections import defaultdict confusion defaultdict(int) for true_name, pred_name in test_samples: confusion[(true_name, pred_name)] 1 print(Top confusions:) for (true_name, pred_name), cnt in sorted(confusion.items(), keylambda x: -x[1]): if true_name ! pred_name: print(f{true_name} - {pred_name}: {cnt})正常项目里最常见的混淆项是“兵”和“卒”互认、“士”和“仕”互认因为这两对字符字形高度相似。颜色分流解决的是跨颜色误报解决不了字形相似的内部混淆。想再进一步可以对这两组易混字符单独提高min_score阈值或者把这几个字符单独再训练一次小级联。数据集换成不同光线下的四十盘对局照片主对角线的变化会让你立刻知道该补训练数据还是调模板阈值。本文还有配套的精品资源点击获取