ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

中国象棋棋盘识别实战:网格拟合与交点定位的完整方案

中国象棋棋盘识别实战:网格拟合与交点定位的完整方案 做中国象棋自动局面识别之前我一直以为“把棋盘找出来”是整条链路里最白给的一步。直到自己架好摄像头、拍了真实照片才发现即使棋盘端端正正摆在正下方想同时准确拿到90个交叉点也不是轻松事。这个项目我明确拆成了两个阶段第一阶段的工作全部是“把棋盘本身认出来”也就是后面做棋子识别、生成局面字符串的必要前提。本文不涉及完整棋局AI只讲棋盘识别这个子模块的完整思路、几何原理和调试经验如果你正在做棋盘类图像的检测、几何校正、交点定位相关的事这一段经验应该能帮你少踩不少坑。1. 棋盘识别项目的真实边界不是“画个框”那么简单1.1 我开始时低估的三个约束条件大多数人拿到“识别中国象棋棋盘”的第一反应是找外框。抓四个角、做透视变换、裁图看起来一套流程下来就完事。但中国象棋棋盘和围棋棋盘有一个根本差异很多普通象棋棋盘根本没有完整闭合的外边框最外侧那圈线就是边界线上没有加粗也没有额外包裹。这意味着依赖“闭合轮廓”的策略一开局就废掉一半。第二个被低估的点是“楚河汉界”这条横带。棋盘上下半场各有5条横向线中间隔着一个较高的空白区域横带区域里通常印着“楚河”“汉界”或一些装饰纹样。对于霍夫直线检测来说单看横向线的位置分布中间天然存在一个比正常行距大得多的间隔如果不做针对性的“断带识别”聚类时很容易把上下半场的横线当成两组独立结构甚至在拼接棋盘时丢失整体比例。第三个约束是棋盘上有大量“干扰线段”。九宫格内的斜线士角斜线、河界两侧的断线、木纹背景、棋子造成的遮挡这些都会在边缘检测阶段被放大。真正的难度不是“找不找得到线”而是“怎么从几百条线段里精确挑出属于棋盘的9条竖线、10条横线”。1.2 拆解完整识别链路棋盘定位的位置在哪里我把整套局面识别拆成四步链棋盘定位找到棋盘区域确定其边界与朝向网格拟合解算出9列10行的交点在图像中的坐标集合棋子识别在交点或其相邻邻域检测、分类棋子局面生成把棋子映射到棋规坐标系输出局面字符串。棋盘识别这个环节同时承担前两步。它输出的不是一张“好看的裁剪图”而是一组经过透视校正后的网格交点坐标。后续棋子识别完全可以基于这组坐标去做不需要重新找棋盘只需要判断每个交点上有没有棋子、是什么棋子。很多业余项目失败在第三步回头查原因十有八九是棋盘识别精度不够。比如交点偏移了半个棋子直径切割出来的子图像自然不准确如果俯视角度稍微偏一点不做透视校正就去检测棋子边缘棋子图被拉长分类器误判率会明显上升。所以棋盘识别必须当作一个精确的几何解算任务来对待而不是当作粗定位任务。1.3 识别通过的标准怎么定义我给自己定了一个容易量化的验收标准而不是含糊地说“看起来识别对了”必须稳定输出90个交叉点坐标顺序对应逻辑棋盘的第0行到第9行、第0列到第8列在没有棋子的干净棋盘图上90个点全部检出且误差小于2个像素在有棋子遮挡场景下至少保持80%以上的交点被正确确定未被遮挡部分不允许出现错位透视校正后任意相邻行间距与相邻列间距的相对误差小于5%。这个标准定出来之后后续所有调参都有了明确方向。第4条尤其关键它决定了识别结果在物理意义上是否逼近真实棋盘结构。2. 预处理中的关键取舍如何让线条从木纹背景里“干净”地露出来2.1 真实拍摄棋盘时常见的四类画面摄像头正对棋盘、光线均匀的“理想画面”在真实项目中极少见。我实际遇到的主要有四类木纹背景明显的实木棋盘棕色或深红色表面上有深色线条边缘检测后纹理边缘和棋盘线混在一起裱布棋盘或塑料棋盘表面有轻微反光斜着看时线条被高光吞掉透明玻璃板下垫棋谱的棋盘玻璃反光带出周边环境产生大量虚假边缘用手机随手拍、存在明显透视畸变的场景线条在远端逐渐密集。这四类场景对预处理的要求其实完全不同。实木棋盘需要抑制纹理反光场景需要局部对比度增强玻璃场景需要避开大块反光区域透视场景则更多依赖后续的几何校正而非预处理本身。所以预处理不能只调一套固定参数至少要区分“纹理型干扰”和“光照型干扰”两条路。2.2 滤波、边缘检测与形态学我保留的处理顺序我最终保留的预处理顺序是转灰度 → 中值滤波 → CLAHE增强 → Canny边缘检测 → 形态学膨胀。每一步都踩过不同的坑简单说一下为什么这样排。转灰度是所有后续操作的前提。这里建议使用 OpenCV 的cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)而不是直接取RGB某一通道。单独取红色通道在某些红木棋盘上效果尚可但一旦棋盘线条是黑色而木底偏红红通道反而会拉低线条与背景的对比度。灰度转换的加权公式已经很好兼顾了人眼感知亮度。中值滤波我选的核大小为5。为什么不用高斯滤波高斯滤波对高斯噪声效果好但对“椒盐型”噪声和细小纹理毛刺的抑制能力不足中值滤波在保留边缘锐度的同时能有效去点状干扰。木纹边缘是缓慢变化的纹理中值滤波对它的抑制比高斯更自然。CLAHE对比度受限自适应直方图均衡是我后来补上的。棋盘线在暗光下经常只有一种暗淡的棕色直接阈值化根本分离不出来。CLACHE 把画面局部对比度拉开之后Canny能够稳定捕捉到线的梯度。参数上我建议clipLimit2.0, tileGridSize(8, 8)太大容易出现光晕伪影反而把木纹细节放大。Canny 的阈值我用的是低阈值40、高阈值100到120这个区间比例大致落在1:2.5到1:3之间。这个比例的意义在于保留连续边缘的同时过滤零散的小梯度响应。注意阈值需要相对图像分辨率微调不建议每次都用固定值。最后做了一次3×3矩形核的膨胀。这一步很多人忽略但它对霍夫直线检测的帮助非常直接。边缘检测出来的线往往是断续的膨胀操作把相邻几像素的断口搭接起来后续 HoughLinesP 更容易把一条完整棋盘线检测出来。2.3 两条只用一次就会上瘾的增强技巧第一个技巧是针对木纹底色的。如果棋盘背景颜色比较统一比如棕色、红色、深黄可以先用颜色直方图找出主色调然后用cv2.inRange把背景色区域压低甚至直接置黑这样木纹纹理大部分被“关掉”棋盘线就凸显了。但要注意当棋盘使用浅色木材、线条也是深色时这个方法可靠如果棋盘是深色底黑色线压制背景色调会同时吞掉棋盘线不能用。第二个技巧是连续帧平均。我在做视频流识别时让程序取连续5帧做一个逐像素均值后再进入检测流程。因为棋盘是静止的而光照反射和环境噪声是随机变化的均值处理能把随机噪声打散棋盘的线条却不会丢。每帧计算量不大对稳定性的提升却非常明显。静态图片识别用不了这个技巧但如果你做实时识别强烈建议加上。3. 霍夫直线检测的实战调优以及“楚河汉界”断带问题3.1 概率霍夫优先标准霍夫备用OpenCV 里有两套常用直线检测接口cv2.HoughLines标准霍夫和cv2.HoughLinesP概率霍夫。标准霍夫输出的是极坐标公式(ρ, θ)没有线段端点概率霍夫输出的是线段起点和终点。我做棋盘网格拟合时需要知道每条线段落在画面的哪个区域还要按线段位置做空间聚类所以概率霍夫几乎是唯一选择。这里给出一个经过反复调整的起始参数组合import cv2 import numpy as np # edges 来自前面预处理得到的边缘图 lines cv2.HoughLinesP( edges, rho1, thetanp.pi / 180, threshold70, minLineLengthint(0.15 * min(h, w)), maxLineGap15, )threshold是累加器阈值表示至少要有多少像素投票支持该直线才会被接受。我遇到的大部分“检测不到线”问题都不是控制这个值太小而是太大。70是一个中庸值在分辨率约为1000×800的图像上效果尚可。minLineLength我建议写成图像短边的比例而不要用固定长度。棋盘线通常横跨整个棋盘棋盘约占整图面积的50%以上所以取短边的15%作为起始下限很稳妥。maxLineGap控制线段断裂时最多允许多大间隙被连接设为15像素对于棋盘线跨过一个棋子带来的断裂已经足够。3.2 线段分类豎线、横线与反向线段霍夫变换返回的线段角度范围覆盖0到360度。横线线段既可能是接近0度的也可能是接近180度的。如果不处理这个方向歧义分类任务会多出一倍无意义的检视线段。我的分类逻辑是计算线段的斜率角取绝对值后判断。若abs(angle) 12或abs(abs(angle) - 180) 12归为横向线段若abs(angle - 90) 12或abs(angle 90) 12归为纵向线段。12度容差能容忍拍摄时棋盘本身有轻微旋转如果画面旋转超过12度说明取景太随意建议先做一个整体旋转校正而不是无限制放宽容差。分类结果大概率会出现两类问题一是同一条棋盘线被检测成好几段分别落在不同位置二是木纹或背景中混进来一些伪线段导致某一行出现两条平行线。此时引入聚类是必要的。3.3 用一维聚类得到9条竖线和10条横线的位置对于横向线段我们关心它的纵向位置y对纵向线段关心它的横向位置x。把多条线段投影到目标轴后用一维聚类合并邻近的投影位置就能得到棋盘线的候选位置。以横线为例实现思路如下def cluster_positions(positions, gap_tol12): positions np.sort(positions) groups [] current [positions[0]] for p in positions[1:]: if p - current[-1] gap_tol: current.append(p) else: groups.append(np.mean(current)) current [p] groups.append(np.mean(current)) return np.array(groups)gap_tol的选择需要参考棋盘在画面中的实际行距。比如棋盘区域高800像素10条横线对应9个间隔正常行距约80像素那么gap_tol取12到15像素比较合理既能把同一行的碎线段合并又不会把相邻两行粘在一起。如果拍出来的图片分辨率差距很大这个阈值最好按行距比例换算。我一般用gap_tol 0.12 * 估计行距做初始值再根据聚类结果反馈修正。聚类完之后通常不会刚好得到10个横线簇可能多于10也可能少于10。多出来的多半是木纹产生的伪横线少则是棋盘线被棋子严重遮挡。解决这个问题的顺手办法是利用“期望数量”横线簇保留与期望数量最接近的10个簇远离棋盘区域的直接丢弃竖线同理保留9个。这属于先验约束的合理使用不算作弊因为象棋棋盘结构是确定已知的。3.4 断带处如何借助“河”间隔把棋盘恢复完整第一次调试时我盯着聚类结果看了半天明明10条横线应该分别落在上下两个半区聚类却把它们切成了“上半场5条下半场5条”中间一条巨大的空隙。后来意识到这不是聚类算错了而是横线在河界处天然断开霍夫检测到的横向线段本来就不覆盖河的中央区域。更大的麻烦在于如果不把上下半场的横线放在同一个坐标系里后续求网格交点时会丢掉中间5条线的总间隔整个棋盘纵向比例就错了。解决思路是显式检测“河带”。在得到横线簇的位置列表后计算相邻簇间距数组正常情况下上下半场内部的行距接近一致而第5条横线到第6条横线之间会出现“异常大的间隙”。判断标准是当某个间隙大于内部平均行距的1.6倍时就认为这里就是楚河汉界。确定河带之后直接把所有横线簇按顺序拼接为10条横线河的宽度作为正常行间距处理即可。虽然河的实际宽度通常比普通行距宽一点但这不影响后续交点坐标的求解因为我们要的是每条线的真实像素位置不是等间距理想网格。3.5 网格拟合后的残差校验拟合出9条竖线、10条横线之后我习惯做一次共线校验用每条线的所有线段端点拟合一条直线计算端点到拟合直线的平均距离。这个残差超过3像素时说明原始线段质量很差可能是边缘检测阶段吃了噪声需要回到预处理调参而不是硬把它拿去算交点。这一步看着不起眼却帮我筛掉过好几次“肉眼看不见但实际存在”的误检。画面反光造成的细小伪边缘在聚类阶段可能侥幸混了进来共线校验会直接暴露它因为伪线条往往只覆盖一小段区域很少能与完整棋盘线共享同一方向。4. 交点解算与透视校正从像素坐标到逻辑棋盘4.1 如何通过线线相交得到干净的交叉点有了9条竖线和10条横线90个交叉点其实就是线线相交的结果。不用额外检测角点也不用做模板匹配直接解析几何求交就行。两个线段交点公式用 Python 可以这样实现def line_intersect(p1, p2, p3, p4): x1, y1 p1 x2, y2 p2 x3, y3 p3 x4, y4 p4 d (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) if abs(d) 1e-8: return None t ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / d return x1 t * (x2 - x1), y1 t * (y2 - y1)把第i条竖线和第j条横线代入得到的就是逻辑坐标(i, j)对应的图像坐标。这一步输出的数组形状应为(10, 9, 2)其中第一维是行号第二维是列号第三维是(x, y)。实际操作中我会同步把90个点画在调试图上用不同颜色区分行列索引肉眼快速确认有没有交叉点错位。调试图是一切计算机视觉项目的“仪表盘”没有它很多误检要很久才能发现。4.2 透视校正在什么情况下必须做俯拍镜头正对棋盘中心时交点坐标基本是规则的不做透视校正影响不大。但只要摄像头与棋盘平面存在倾斜角远端交叉点的间距会被压缩直接拿这些坐标去切棋子图像边缘棋子的图会明显变形。更麻烦的是视角倾斜会让同一行棋子在图像中的像素间距不一致。比如开局阶段位于边线的车马炮还能看清而贴近河界的卒子被压缩得只剩一半大小后续分类器的输入尺度完全乱了。我建议无论摄像头安装角度如何都固定做一次透视校正。校正目标是把棋盘变换成一个理想化的矩形画面棋盘正好居中上下边平行行距和列距比例接近真实几何比例。这样后续棋子识别的ROI定义和分类器训练都只需要处理“正视图”一种形态。4.3 单应矩阵的估计方法与角点选择策略透视校正的数学工具是单应矩阵Homography Matrix。OpenCV 提供了cv2.getPerspectiveTransform(src, dst)和cv2.findHomography两种常用入口。前者适用于精确的4组对应点后者可以带更多对应点并用RANSAC排除异常点。在棋盘识别场景下90个交叉点都是已知的只是我们想从中选4个稳定点作为变换基准。直觉上应该选左上、右上、左下、右下四个角点但在倾斜视角下最外沿的交点可能受边缘干扰严重。更稳妥的做法是取第1列、第8列和第1行、第9行的4个交点作为基准而不是直观的“四个角”因为第1行和第1列的交点在霍夫检测中通常覆盖更长的线段定位更稳。目标点集合可以将棋盘映射到固定尺寸比如1400×1600像素即9列对应宽度140010行对应高度1600比例9比10src np.array([ pts[0][0], # 第0行第0列 pts[0][8], # 第0行第8列 pts[9][0], # 第9行第0列 pts[9][8], # 第9行第8列 ], dtypenp.float32) dst np.array([ [100, 100], [1300, 100], [100, 1500], [1300, 1500], ], dtypenp.float32) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(img, M, (1400, 1600))这样校正后棋盘的逻辑坐标与图像坐标就建立了稳定的一一对应关系。后续棋子识别只需要在固定ROI内检测不必每次重新解算棋盘。4.4 校正结果自检从90个交点校验变换质量透视变换完成之后不要急着进入棋子识别我先做三件校验第一把90个交叉点全部用cv2.perspectiveTransform映射到目标画面检查坐标是否落在预期网格上。如果某个点偏离超过2像素说明单应矩阵选点有问题。第二计算目标画面中相邻交点的行距和列距统计标准差。理想情况下同一行内相邻列距应基本一致不同行的列距也应一致如果列距差异超过5%多半是原始交点定位不够准。第三检查校正后棋盘图像的边界是否出现明显空白或拉伸。拉伸通常是因为 src 里四个基准点选得太贴近内侧目标尺寸设得过大空白则说明基准点选歪了。这步校验是我整个项目里性价比最高的一段代码它把棋盘识别的“定性正确”变成了“定量可复查”。5. 有棋子遮挡、反光与畸形棋盘时的容错处理5.1 棋子挡住棋盘线交点怎么办棋盘识别在实际局面识别中不可能等到把所有棋子拿走再检测。棋子一旦摆在交点上那条棋盘线段就被遮挡了一部分。好在概率霍夫处理断裂线段的能力足够强只要一条横线在棋子的两侧还有足够长度霍夫检测仍然能找出来。但如果棋子尺寸偏大完全把某段线盖死聚类后的候选位置就可能漏掉这条线。我的补救策略是按“行/列期望总数”反推缺失线先看现有聚类簇的位置如果已经有9条竖线中的8条缺失的那条大概率在两个已有簇的中间位置附近。按照象棋棋盘横向近似等间距的特性用左右相邻线的中点补上缺失线即可。对水平横线也同理。不过要注意在河界区域补线时不能简单等间距因为跨过河带后的两条横线位置来自不同半场间距更大。正确做法是先判断缺失线是否落在河带附近如果是则使用上下半场的局部间距分别推算而不是全局等间距。5.2 光线突变时局部对比度增强比重新调阈值更可靠棋盘识别的失败场景里因光线问题导致的多于因算法问题导致的。我在白天窗边拍摄时棋盘左侧被阳光照亮右侧落在阴影中整体阈值怎么调都顾此失彼。后来换成 CLAHE 之后左右两块区域的局部对比度被分别拉伸棋盘线在亮区和暗区都能稳定显现不再需要手工调全局阈值。另外要留心一个反直觉的问题过度增强也不是好事。CLAHE 的clipLimit调得太大时木纹和棋子的纹理会被放大成伪边缘反而干扰霍夫检测。我试过 3.0 和 4.0线条确实更清晰但木纹中的年轮也被清晰地提取了出来伪横向线段数量暴增。最终固定在 2.0既能区分线条和背景又不至于把木纹细节全盘端出来。5.3 弧线纹装饰、印字和磨损棋盘的处理建议很多工艺棋盘会在边框处做大量装饰性弧线在河界印“楚河”“汉界”大字这些内容对直线检测基本是致命干扰。弧线不会进入横线或竖线的候选集但它们会产生大量方向散射的短线段拉低霍夫检测的有效信噪比。处理办法有两个方向要么在预处理阶段把装饰区域裁剪掉只保留棋盘网格核心区域要么在聚类阶段用期望几何强制筛选只保留与网格方向高度一致的线段。磨损棋盘的问题是线条本身有些地方颜色很淡边缘检测输出会出现明显断口。断口在maxLineGap参数足够时会被霍夫连接但磨损导致线条局部变淡后断口两侧的灰度差异已经小于Canny高阈值就算maxLineGap再大也无济于事。这种情况只能降低Canny高阈值或降低霍夫threshold但这两个参数调低后会引入更多噪声。我最后的折中方案是高阈值从120降到90霍夫 threshold 从70降到55然后靠聚类和共线校验筛掉噪声。5.4 实测数据的几点体会我用手机拍了大约30张不同场景的棋盘照片覆盖光面木棋盘、亚克力棋盘、布纹棋盘和纸质棋盘。在干净棋盘图上的交点检出率稳定在95%以上有棋子遮挡时扣除被完全盖住的交点后剩余交点的定位偏差基本保持在2像素以内。最难受的其实是“半遮挡”场景一枚棋子刚好压住交点边缘但交点中心还露在外面。边缘检测时棋子圆弧会形成大量杂乱梯度导致交点坐标被向外拉偏。针对这个我在求交点后加了一个局部微调步骤以交点坐标为中心、15×15像素的窗口内寻找线条交叉强度最大的位置重新定位。这个操作把半遮挡场景的误定位率降低了大约三分之一。如果你也要做同样的棋盘识别我的建议是不要追求“一个万能的算法处理所有棋盘”而是把程序做成“场景可配置”给预处理参数、聚类容差、后处理开关分别留出配置项。我的配置里目前有木纹模式、反光模式、低对比度模式三档用起来比调一套通用参数省心得多。6. 从棋盘识别到局面识别的接口设计别给自己挖坑棋盘识别做扎实之后接后续的棋子识别会顺畅很多。但接口设计这一步出现过不少返工简单说几个我亲测有效的数据约定。第一棋盘交点数组一定用固定的行列索引存储形状固定为(10, 9, 2)。不要用列表套列表的松散结构否则下游代码每次取点都要自己处理边界。第二透视校正之后的棋盘图要规定一个统一尺寸。我选了 1400×1600这个尺寸刚好让每个交叉点周围的棋子区域大约有 140×120 像素既不浪费计算也不会因为过小损失细节。后续训练棋子分类器直接把交点附近的ROI裁剪出来做输入可以省掉大量标注对齐工作。第三把“没有检测到棋盘”这种情况也作为输出的一部分。我的接口是返回一个字典包含success、corners、grid_points、warp_matrix、debug_image五个字段。如果检测失败successFalse但debug_image依然返回便于事后定位失败原因。这个习惯帮我省了非常多调试时间。第四棋盘识别结果要做一次与棋规的一致性检查。比如第0行和第9行都排在画面最上沿说明棋盘上下颠倒第1列和第8列分别位于左右两侧这是正常状态。如果起点和终点搞反后续局面字符串的顺序就会完全错乱而且极其隐蔽。我在接口里加入了一个简单的排序校验行索引必须从上到下单调递增列索引必须从左到右单调递增不满足就触发重新校正。把棋盘识别当成一个独立模块来打磨是值得的。它虽然不产生任何“聪明结果”但直接决定后面所有步骤的上限。我现在回头去看当初最早版的代码最庆幸的是没有为了省事跳过透视校正和交点回归校验这两个东西在后面棋子识别和局面生成里帮了大忙。
返回列表