核心思想:围棋棋盘有 9 个固定位置的星位标记点(座子点),它们构成一个已知的 3×3 网格。检测这 9 个点即可直接求解透视变换,一步到位地映射出全部 361 个棋盘点。
为什么用星位点?
| 对比 | 传统边缘/直线方案 | 星位点方案 |
|---|---|---|
| 依赖 | 棋盘边框清晰 | 星位圆点可见 |
| 遮挡容忍度 | 边缘被棋子遮挡就失败 | 只被遮 1-2 个星位仍可工作 |
| 计算量 | 边缘检测+直线提取+交点 | 斑点检测+单应矩阵 |
| 参数数量 | 大量阈值需要调 | 基本只需要斑点大小 |
| 棋盘类型 | 必须可见边框 | 任意标准围棋棋盘 |
| 精度 | 取决于直线拟合质量 | 单应矩阵内插,天然平滑 |
整体流程
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐
│ 摄像头采集 │ ─→ │ 预处理 │ ─→ │ 星位点检测 │ ─→ │ 点身份识别 │ ─→ │ 透视校正 │
└──────────┘ │ · 灰度化 │ │ · 二值化 │ │ · 排序+分组 │ │ 单应矩阵 │
│ · CLAHE 均衡 │ │ · 斑点检测 │ │ · 模式匹配 │ │ H(3×3) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────┬───┘
│
▼
┌──────────────┐
│ 交点网格生成 │
│ 361 个交点坐标 │
└──────────────┘
第一步:预处理
目标:增强星位黑点与棋盘底色的对比度。
输入: BGR 图像
→ cv::cvtColor(BGR → GRAY)
→ cv::GaussianBlur(ksize=5, sigma=1.0)
→ cv::createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
输出: 对比度增强的灰度图
关键点:
- 高斯模糊先做,避免木纹纹理被 CLAHE 放大
- CLAHE 是必须的——真实场景光照往往不均匀(窗边/灯光/阴影)
第二步:星位点检测
星位点是棋盘上的黑色实心圆点,直径约为棋盘格子宽度的 1/6 ~ 1/5。
方法 1:SimpleBlobDetector(推荐起步方案)
# OpenCV SimpleBlobDetector 参数
params = cv2.SimpleBlobDetector_Params()
params.filterByArea = True
params.minArea = 10 # 根据分辨率调整
params.maxArea = 200
params.filterByCircularity = True
params.minCircularity = 0.7 # 圆形度
params.filterByConvexity = True
params.minConvexity = 0.8
params.filterByColor = True
params.blobColor = 0 # 检测黑色圆形(深色)
detector = cv2.SimpleBlobDetector_create(params)
keypoints = detector.detect(gray)
方法 2:自适应阈值 + 轮廓筛选(更可控)
二值化 (自适应阈值, 反色, 使得黑点变白)
→ cv::findContours
→ 对每个轮廓筛选:
· area 在 [min_area, max_area] 之间
· 圆形度 ≈ 1 (4π * area / perimeter²)
· convexHull 面积 / 原始面积 ≈ 1
→ 取轮廓质心作为星位点坐标
两者对比:
| 特性 | SimpleBlobDetector | 自适应阈值+轮廓 |
|---|---|---|
| 实现难度 | 一行调用 | 需要手动调参 |
| 抗光照 | 一般 | 好(自适应阈值) |
| 误检率 | 棋子边缘可能误检 | 圆形度筛选更准 |
| 推荐 | 快速原型 | 生产级 |
方法 3:归一化互相关模板匹配(最强鲁棒)
创建星位点模板 (黑色圆盘, 半径 = 预估值)
→ cv::matchTemplate(image, template, TM_CCOEFF_NORMED)
→ 找相关系数 > 0.6 的局部极大值
→ 非极大值抑制 (NMS)
→ 得到候选星位点
第三步:星位点身份识别
检测到 ~9 个候选点后,需要确定"第 i 个点对应棋盘上哪个星位”。
3.1 坐标排序
已知: 9 个像素坐标 {(x_i, y_i)}
未知: 每个点对应的棋盘坐标 (row, col) ∈ {3, 9, 15} × {3, 9, 15}
算法:
1. 计算候选点的 2D 质心 C = (mean(x_i), mean(y_i))
2. 对 9 个点按到质心的距离排序,最近的点 = 天元 (9, 9)
或:按 y 坐标排序分 3 组(上/中/下)
每组内按 x 坐标排序分 3 组(左/中/右)
3. 赋予棋盘坐标:
row ∈ {3, 9, 15} → 从上到下
col ∈ {3, 9, 15} → 从左到右
3.2 最小二乘验证
用 9 组 (pixel, board) 对应点拟合仿射变换:
pixel = A · board + b
计算拟合残差:
如果某个点的残差 > 2×标准差 → 误检或偏差过大
→ 剔除该点,用剩余点重新拟合
如果剩余点数 ≥ 4:
→ 满足 findHomography 的最低要求
→ 降级为单应矩阵计算
3.3 特殊情况的处理
检测到 10+ 个点:
→ 棋子或木纹被误检
→ 用 RANSAC findHomography 自动剔除离群点
检测到 5-8 个点:
→ 部分星位被棋子遮挡
→ 最少 4 个即可计算 H
→ 但会降低精度,建议用 ≥ 6 个点
检测到 < 4 个点:
→ 极差光照或棋盘无星位标记
→ 降级到 Hough 直线检测方案(回退策略)
第四步:透视校正
有了 ≥4 组对应点,计算单应矩阵 H:
棋盘坐标 (归一化到 [0,1]):
star_board[i] = (col/18, row/18) 其中 col,row ∈ {3,9,15}
像素坐标:
star_pixel[i] = 检测到的斑点中心
求解:
H = cv::findHomography(star_board, star_pixel, cv::RANSAC)
然后用 H 计算全部 361 个棋盘点:
grid_points = [] # 棋盘坐标 → 像素坐标
grid_pixel = [] # 像素坐标列表
for row in range(19):
for col in range(19):
# 归一化棋盘坐标
board_pt = np.array([col/18.0, row/18.0, 1.0])
# 映射到像素坐标
pixel_pt = H @ board_pt
pixel_pt /= pixel_pt[2] # 齐次坐标归一化
grid_points.append((row, col))
grid_pixel.append((pixel_pt[0], pixel_pt[1]))
此时得到:361 个 (row, col) → (px, py) 的完整映射表。
第五步:后验证(可选)
检查投影网格是否合理:
验证项:
1. 网格线是否近似等间距?
→ 计算相邻水平线间距的标准差 < 阈值
2. 四个角点是否在图像范围内?
→ (0,0) (18,0) (0,18) (18,18) 的像素位置
3. 天元(9,9)是否落在预期的棋盘中心区域?
→ 用初始星位检测的天元位置交叉验证
第六步:棋子识别(可选,但有了网格后很自然)
有了 361 个精确的坐标点,棋子识别变得极其简单:
对每个 (row, col):
1. 取 5×5 或 7×7 像素的 ROI
2. 计算 ROI 的灰度均值
3. 分类:
mean < T_black → 黑子 (B)
mean > T_white → 白子 (W)
否则 → 空位 (.)
改进:用 HSV 的 V 通道代替灰度,对光照更鲁棒。
与原始方案的对比
原始思路:
预处理 → Canny → 找轮廓 → 透视校正 → Hough 直线 → 361 交点
问题:依赖棋盘边框、受棋子遮挡影响大、参数多
星位点方案:
预处理 → 斑点检测 → 身份识别 → 单应矩阵 → 361 交点
优势:9 个已知点直接求解、抗遮挡、参数少、精度高
OpenCV API 速查
// 预处理
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(gray, gray, cv::Size(5,5), 1.0);
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(8,8));
clahe->apply(gray, gray);
// 斑点检测
cv::Ptr<cv::SimpleBlobDetector> detector = cv::SimpleBlobDetector::create(params);
detector->detect(gray, keypoints);
// keypoint.pt → (x, y) 像素坐标
// 单应矩阵
std::vector<cv::Point2f> src(star_board); // 棋盘归一化坐标 (9个)
std::vector<cv::Point2f> dst(star_pixel); // 像素坐标 (9个)
cv::Mat H = cv::findHomography(src, dst, cv::RANSAC);
// 投影交点
cv::Mat boardPt = (cv::Mat_<double>(3,1) << col/18.0, row/18.0, 1.0);
cv::Mat pixelPt = H * boardPt;
pixelPt /= pixelPt.at<double>(2);
// 棋子检查 (每个交点)
cv::Rect roi(pixel.x - 4, pixel.y - 4, 8, 8);
double meanGray = cv::mean(gray(roi))[0];
参数调优指南
| 参数 | 含义 | 建议初始值 | 如何调整 |
|---|---|---|---|
blob_minArea |
星位点最小像素面积 | 10 | 太小时出现噪点误检 |
blob_maxArea |
星位点最大像素面积 | 200 | 太小会漏检近处的星位 |
minCircularity |
最小圆形度 | 0.7 | 降低可获得更多候选,但误检增加 |
CLAHE_clipLimit |
对比度增强幅度 | 2.0 | 光照越差越大 |
RANSAC_thresh |
离群点距离阈值 | 3.0 px | 星位检测精度越高可设越小 |
总结
9 星位方案的核心优势:
- 9 个天然标记点 — 不需要贴标定板,棋盘自带
- 几何约束强 — 9 个点的空间关系完全已知 {(3,3), (3,9), (3,15), …}
- 一步到位 — 检测 → 单应 → 361 交点,中间没有复杂的直线拟合
- 容错性好 — RANSAC 自动剔除误检/遮挡点,6 个点就够用
- 计算量小 — 适合 ESP32-CAM 实时处理(斑点检测可用神经网加速)
典型精度: 星位像素误差 < 2px 时,内插交点误差 < 1px,足够棋子识别使用。