核心思想:围棋棋盘有 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 星位方案的核心优势:

  1. 9 个天然标记点 — 不需要贴标定板,棋盘自带
  2. 几何约束强 — 9 个点的空间关系完全已知 {(3,3), (3,9), (3,15), …}
  3. 一步到位 — 检测 → 单应 → 361 交点,中间没有复杂的直线拟合
  4. 容错性好 — RANSAC 自动剔除误检/遮挡点,6 个点就够用
  5. 计算量小 — 适合 ESP32-CAM 实时处理(斑点检测可用神经网加速)

典型精度: 星位像素误差 < 2px 时,内插交点误差 < 1px,足够棋子识别使用。