核心问题:9 星位标记虽然精度最高,但用户操作繁琐。能否只用 2 个斜对角星位完成矫正,同时保持可接受的精度?答案是:可以。通过仿射预估 → 引导搜索 → 逐步升级的策略,2 点即可完成 9 点的工作。
1. 动机:为什么追求 2 点方案?
| 方案 | 标记点数 | 用户体验 | 可靠性 | 精度 |
|---|---|---|---|---|
| Full 9 星位 | 9 | 繁琐,容易点错 | 高 | 最高(全单应) |
| 点 4 角 | 4 | 中等 | 中 | 高(单应) |
| ⚡ 2 斜对角 | 2 | 极简 | 自适应 | 梯度:仿射 → 全单应 |
真实使用场景的痛点:
- 手机拍照后想快速矫正,不想反复对准 9 个点
- 老年用户/非技术人员操作,“点 9 个点"本身就是门槛
- 部分棋盘星位被棋子遮挡,只看得见对角两个
2. 算法全景
flowchart TD
A["用户点击 2 个斜对角星位<br/>P₁=(3,3) · P₂=(15,15)"] --> B["构建 3 点对应<br/>P₁ → P₁ · 中点 → (9,9) · P₂ → P₂"]
B --> C["cv::getAffineTransform()<br/>6-DOF 仿射矩阵 A"]
C --> D["A 投影预测全部 9 星位坐标"]
D --> E["在每个预测位置开 ROI 窗口<br/>guided HoughCircles 局部精搜"]
E --> F{"搜到 ≥2 个额外星位?"}
F -->|"✅ 总计 ≥4 点"| G["升级:cv::findHomography + RANSAC"]
F -->|"❌ 不足 4 点"| H["兜底:纯仿射变换 warpAffine"]
G --> I["warpPerspective → 正方形输出"]
H --> I
I --> J["绘制网格 + 星位 → 输出"]
3. 核心步骤
3.1 构建 3 点对应(关键技巧)
用户只点了 2 个点。但 getAffineTransform 需要至少 3 组对应点。怎么补齐?
天元 = 两个对角点的几何中点:
// 用户标记的两个像素坐标
Point2f p1 = markPoints[0]; // 左上星位 (3,3) 的像素位置
Point2f p2 = markPoints[1]; // 右下星位 (15,15) 的像素位置
// 天元 = 中点(这是透视投影下的近似,但对仿射足够)
Point2f p_center = (p1 + p2) * 0.5f;
// 棋盘坐标系中的 3 个对应点
vector<Point2f> src = {
Point2f(3.0f/18, 3.0f/18), // (3,3) 归一化
Point2f(9.0f/18, 9.0f/18), // (9,9) 天元
Point2f(15.0f/18, 15.0f/18) // (15,15) 归一化
};
vector<Point2f> dst = { p1, p_center, p2 };
Mat A = getAffineTransform(src, dst);
// A 是 2×3 仿射矩阵:[a b tx; c d ty]
为什么中点近似有效?
- 透视变换在远处压缩、近处放大,但对角线的中点受此影响对称抵消
- 对于中等到小角度的倾斜拍摄(≤30°),中点误差通常 < 3% 棋盘宽度
- 这个预估不需要精确,只需要把 guided search 的 ROI 窗口放到正确的大致位置
3.2 仿射投影预测
// 用仿射矩阵投影全部 9 个星位→像素坐标
vector<Point2f> predictedStars(9);
int idx = 0;
for (int row_val : {3, 9, 15}) {
for (int col_val : {3, 9, 15}) {
// 归一化棋盘坐标
float u = col_val / 18.0f;
float v = row_val / 18.0f;
// 仿射投影
float px = A.at<double>(0,0) * u + A.at<double>(0,1) * v + A.at<double>(0,2);
float py = A.at<double>(1,0) * u + A.at<double>(1,1) * v + A.at<double>(1,2);
predictedStars[idx++] = Point2f(px, py);
}
}
3.3 Guided HoughCircles 局部精搜
常规 HoughCircles 在任何地方都可能误检(棋子边缘、木纹纹理)。但有了仿射预测,我们知道每个星位大致在哪——只需要在那个位置附近搜。
// 对每个预测位置,在局部 ROI 内搜索实际星位
vector<Point2f> confirmedStars;
for (const auto& pred : predictedStars) {
// 定义搜索窗口:预测位置 ± 1.5 倍网格间距
float halfWin = estimatedSpacing * 1.5f;
Rect roi(pred.x - halfWin, pred.y - halfWin,
halfWin * 2, halfWin * 2);
// 确保 ROI 在图像范围内
roi &= Rect(0, 0, gray.cols, gray.rows);
// 在 ROI 内做 HoughCircles
Mat roiImg = gray(roi);
vector<Vec3f> circles;
HoughCircles(roiImg, circles, HOUGH_GRADIENT,
1, // dp
estimatedSpacing, // minDist
100, // param1 (Canny)
18, // param2 (累加器阈值)
minR, maxR); // 半径范围
if (!circles.empty()) {
// 取响应最强的那个,转换回全局坐标
float gx = circles[0][0] + roi.x;
float gy = circles[0][1] + roi.y;
confirmedStars.push_back(Point2f(gx, gy));
}
}
Guided Search 的优势:
| 对比 | 全局 HoughCircles | Guided HoughCircles |
|---|---|---|
| 搜索范围 | 整图 | 每个星位 ±1.5×grid ROI |
| 误检来源 | 棋子/木纹/边框 | 基本没有(窗口小且定位准) |
| param2 阈值 | 必须设高 → 漏检 | 可以设低 → 敏感但不误检 |
| 速度 | O(W×H) | O(9 × ROI_area) ≪ O(W×H) |
3.4 决策点:升级还是兜底
if (confirmedStars.size() >= 2) {
// ✅ 加上原有的 2 个标记点,总计 ≥ 4 个
// 合并并升级为全单应
vector<Point2f> allSrc;
// ... 先加入标记点的归一化坐标
// ... 再按身份匹配加入 confirmedStars
H = findHomography(allDst, allSrc, RANSAC, 3.0);
warpPerspective(srcImg, result, H, Size(outSize, outSize));
}
else {
// ❌ guided search 没找到足够的额外星位
// 降级为仿射兜底
// 仿射 = 旋转 + 缩放 + 平移,不校正梯形透视
// 对正面/接近正面拍摄的照片效果足够好
warpAffine(srcImg, result2, A, Size(outSize, outSize));
// 标记为"仿射模式"以便 UI 提示用户精度可能略低
}
升级条件: 标记点 (2) + guided found (≥2) = ≥4 个对应点,满足 findHomography 的最低要求。
兜底质量:
- 仿射处理旋转 + 缩放 + 平移 → 大部分手机拍照场景够用
- 仅丢失纯透视分量(梯形变形)
- 需要透视校正的场景(大角度侧拍),建议用户切换到 Full 9 点模式
4. 与 Full 9 星位方案的对比
| 特性 | Quick 2 星位 | Full 9 星位 |
|---|---|---|
| 用户操作 | 点击 2 次 | 点击 9 次 |
| 自动检测 | 无(手动入口) | FFT+HoughCircles |
| 容错 | 仿射兜底,≤30° 倾斜稳定 | RANSAC,任意角度 |
| 精度 | 仿射 → 单应梯度升级 | 全单应,最高精度 |
| 适用场景 | 快速矫正、棋子多、用户懒 | 高精度要求、大角度倾斜 |
| 失败模式 | 降级为仿射,梯形失真不校 | 极端遮挡时回退到手动标记 |
推荐策略: UI 默认 Quick 模式,用户点 2 下就出结果。如果结果不满意(仿射兜底时给出提示),可以切换到 Full 模式精确校正。
5. 完整 C++ 实现骨架
class QuickRectifier {
public:
void setMarkedPoints(cv::Point2f corner1, cv::Point2f corner2) {
p1 = corner1;
p2 = corner2;
}
bool rectify(const cv::Mat& input, cv::Mat& output, int outSize = 600) {
// 1. 仿射预估(3 点:P1, 中点, P2)
cv::Point2f p_center = (p1 + p2) * 0.5f;
std::vector<cv::Point2f> src = {
{3.0f/18, 3.0f/18},
{9.0f/18, 9.0f/18},
{15.0f/18, 15.0f/18}
};
std::vector<cv::Point2f> dst = {p1, p_center, p2};
cv::Mat A = cv::getAffineTransform(src, dst);
// 2. 预测 9 星位像素坐标
auto predicted = predictStars(A);
// 3. Guided HoughCircles 精搜
auto found = guidedHoughSearch(input, predicted);
// 4. 决策:升级 or 兜底
if (found.size() >= 2) {
// 升级为单应
auto allPts = mergePoints(p1, p2, found);
cv::Mat H = cv::findHomography(allPts.dst, allPts.src,
cv::RANSAC, 3.0);
cv::warpPerspective(input, output, H,
cv::Size(outSize, outSize));
return true; // 高精度模式
} else {
// 仿射兜底
cv::warpAffine(input, output, A,
cv::Size(outSize, outSize));
return false; // 降级模式
}
}
private:
cv::Point2f p1, p2;
};
6. 参数建议
| 参数 | 建议值 | 说明 |
|---|---|---|
| guided ROI 半径 | 1.5 × estimatedGridSpacing |
覆盖仿射预测误差 + 星位点偏移 |
| HoughCircles param2 | 10-15(比全局低很多) | ROI 内误检极少,可以设低 |
| RANSAC 阈值 | 3.0 px | guided search 精度通常很好 |
| 仿射兜底触发 | found < 2 | 保守策略,牺牲精度保可靠性 |
7. 总结
两点星位方案的核心洞察:不需要用户标记所有 9 个点,因为棋盘几何结构本身是已知的。 两个斜对角点提供了足够的方向 + 尺度信息,通过仿射预估桥接到 guided search,再逐步升级到全单应精度。
| 层次 | 输入 | 输出 | 精度 |
|---|---|---|---|
| L1 仿射预估 | 2 个标记点 | 9 个预测位置 | 粗(≤3% 偏移) |
| L2 Guided Search | 预测位置 + 原图 | 2-7 个精确定位 | 精(≤1px) |
| L3 全单应 | ≥4 个精确定位 | 透视校正图 | 最高 |
这个三层架构使得用户操作极简(2 次点击),系统在后台自动爬升到最高可用精度。对于绝大多数手机拍照场景,L1→L2→L3 能自然完成升级,用户甚至感知不到背后的复杂度。