1. 问题定义
输入: 任意角度拍摄包含围棋棋盘的彩色照片(倾斜、透视畸变) 输出: 正方形俯视校正图(19×19 标准棋盘,含网格线与星位标记)
2. 算法流程
flowchart TD
A["📷 加载棋盘照片"] --> B["预处理: 灰度化 + Bilateral Filter + CLAHE"]
B --> C{"自动星位检测<br/>HoughCircles + 聚类排序"}
C -->|"✅ ≥4 个星位"| D["星位身份识别<br/>确定每个点是(3/9/15)行/列"]
C -->|"❌ 检测失败"| E{"选择标记模式"}
E -->|"Full"| F["🖱️ 人工标记 9 星位"]
E -->|"⚡ Quick"| F2["🖱️ 只标记 2 斜对角星位<br/>左上(3,3) + 右下(15,15)"]
D --> G["构建源点集 src: 像素坐标"]
F --> G
F2 --> G2["仿射预估(3点: 含隐式中心点)"]
G2 --> G3["投影预测其余 7 星位"]
G3 --> G4["Guided HoughCircles 局部精搜"]
G4 --> G5{"搜到 ≥2 新星位?"}
G5 -->|"✅ ≥4 总计"| G
G5 -->|"❌ 不够"| G6["仿射兜底<br/>(2-DOF scale + 3-DOF translation)"]
G --> H["RANSAC 单应矩阵估计<br/>cv::findHomography(src, dst)"]
G6 --> I["透视变换 / 仿射变换<br/>warpPerspective → 正方形输出"]
H --> I
I --> J["后处理:网格线 + 星位标记"]
J --> K["💾 输出校正结果"]
3. 核心步骤详解
3.1 预处理
BGR → Gray → Bilateral Filter(9, 75, 75) → CLAHE(clip=2, grid=8×8)
- Bilateral Filter: 保持边缘的同时平滑噪声(棋盘线是关键边缘,不能模糊)
- CLAHE: 自适应直方图均衡,应对不均匀光照
3.2 星位坐标系统
19 路棋盘 9 个星位(0-indexed 棋盘坐标):
| 星位 | (row, col) | 归一化坐标 (u, v) |
|---|---|---|
| 右上 | (3, 3) | (3/18, 3/18) |
| 右中 | (3, 9) | (3/18, 9/18) |
| 右下 | (3, 15) | (3/18, 15/18) |
| 中右 | (9, 3) | (9/18, 3/18) |
| 天元 | (9, 9) | (9/18, 9/18) |
| 中左 | (9, 15) | (9/18, 15/18) |
| 左偏上 | (15, 3) | (15/18, 3/18) |
| 左偏中 | (15, 9) | (15/18, 9/18) |
| 左下 | (15, 15) | (15/18, 15/18) |
归一化: u = col/18, v = row/18,将棋盘坐标映射到 [0, 1] 范围
3.3 自动检测(路径 A)
flowchart LR
A["灰度图"] --> B["estimateSpacing()<br/>FFT 自相关估算网格间距"]
B --> C["自适应 HoughCircles<br/>dp=1, minDist, param1=100/60, param2=18/8<br/>minR/maxR 根据 spacing 动态计算"]
C --> D["聚类排序 identifyStarPoints()"]
D --> E["按 y 排序 → 最大 gap 分割行 → 3 行"]
E --> F["每行按 x 排序 → 最大 gap 分割列 → 3 列"]
F --> G["9 个 StarPoint{px, py, boardCol, boardRow}"]
3.4 人工标记(路径 B)
Full 模式 (9 星位)
用户按固定顺序点击棋盘上的9个星位点:
1 ─ 2 ─ 3 (上行: 左→右)
4 ─ 5 ─ 6 (中行: 左→右)
7 ─ 8 ─ 9 (下行: 左→右)
每个标记点自动分配对应的 (boardCol, boardRow):
index i → row = (i / 3) * 6 + 3 (i/3 ∈ {0,1,2} → row ∈ {3,9,15})
col = (i % 3) * 6 + 3 (i%3 ∈ {0,1,2} → col ∈ {3,9,15})
⚡ Quick 模式 (2 斜对角星位)
只需标记 2 个点: 左上星位 (3,3) 和右下星位 (15,15)
flowchart LR
A["用户点击 2 个斜对角星位"] --> B["构建 3 点对应:<br/>(3,3)→P₁ (9,9)→mid (9,9是P₁和P₂中点)(15,15)→P₂"]
B --> C["getAffineTransform → 6-DOF 仿射"]
C --> D["仿射投影预测全部 9 星位"]
D --> E["在每个预测位置 ROI 内<br/>guided HoughCircles 精搜"]
E --> F{"搜到额外 ≥2 星位?"}
F -->|"✅ 总计 ≥4"| G["升级为全单应 findHomography"]
F -->|"❌ 不足"| H["降级为仿射兜底<br/>处理旋转+缩放+平移<br/>不处理纯透视"]
关键设计:
- 两个斜对角星位提供棋盘的方向向量和尺度信息
- 隐式中心点 (天元) = 两个标记点的中点,提供第 3 个约束 → 6-DOF 仿射
- guided HoughCircles 在仿射预测位置的局部 ROI 内搜索,大幅减少误检
- 兜底: 纯仿射对正面拍摄的照片效果足够好,仅丢失纯透视(梯形)校正
关于 Quick 模式的完整原理和实现细节,参见独立文章《两点星位透视矫正:用最少标记完成棋盘校正》。
3.5 单应矩阵计算
// src: 源图像中的星位像素坐标
// dst: 归一化目标坐标
vector<Point2f> src = { (px0,py0), (px1,py1), ..., (px8,py8) };
vector<Point2f> dst = { (3/18,3/18), (3/18,9/18), ..., (15/18,15/18) };
Mat H = findHomography(dst, src, RANSAC, 5.0);
// 注意: dst→src 映射 (findHomography 的默认语义)
// 后续 warpPerspective 用 H 直接把目标坐标投影到源图像
⚠️ 方向注意:
findHomography(dst, src)返回的是 dst→src 的映射。这意味着warpPerspective配合WARP_INVERSE_MAP使用,或生成四角映射时直接用 H 正投影。
3.6 透视矫正
flowchart LR
A["源图像中的棋盘<br/>(透视变形)"] --> B["计算源四角坐标<br/>(0,0)→(18,0)→(18,18)→(0,18)<br/>通过 H 正投影到源图像"]
B --> C["getPerspectiveTransform<br/>(srcCorners, dstCorners)"]
C --> D["warpPerspective<br/>→ 正方形输出<br/>outputSize × outputSize"]
目标四角 (outputSize=600, margin=30):
dstCorners = [(30,30), (570,30), (30,570), (570,570)]
cell = (600 - 60) / 18 = 30px
3.7 后处理
1. 绘制 19×19 网格线 (灰色, 1px)
2. 绘制 9 个星位点 (深灰实心圆, r=cell/10)
3. 可选: 棋子识别 (交点 ROI 灰度均值)
4. 保存为 JPEG (95% 质量)
4. 类设计
BoardRectifier
├── setImage(cv::Mat bgr) // 设置源图像
├── setStarPoints(vector<Point2f>) // 设置人工标记的星位
├── computeHomography() // 计算单应矩阵
├── rectify(int outputSize) // 执行透视矫正
├── homography() → cv::Mat // 获取 H 矩阵
└── result() → cv::Mat // 获取校正结果
MarkWidget : QWidget
├── setImage(QImage) // 显示图像
├── markedPoints() → vector<QPointF> // 获取标记点
├── reset() // 清除标记
└── signals: allMarked() // 9个点标记完成
5. 人工标记交互协议
MarkWidget 状态机:
[Idle] ──load image──▶ [Waiting] ──click 9 times──▶ [Complete]
▲ │ │
└─── reset() ──────────┴─── right-click undo ───────┘
每次左键点击:
1. 记录像素坐标
2. 如果当前点已有标记,移动该标记
3. 如果 < 9 个点,添加新标记
4. 重绘:显示已标记点编号(1-9) + 十字准线
拖拽:
- 已标记的点可以拖拽微调
- mousePress → 检测是否命中已有标记
- mouseMove → 更新标记位置并重绘
- mouseRelease → 完成移动