核心问题: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 能自然完成升级,用户甚至感知不到背后的复杂度。