Phase: P1 — 棋子识别 + 局面感知 | 依赖: P0 棋盘矫正

输入: 透视矫正后的 600×600 正方形棋盘图,已知 361 个交点像素坐标 输出: 19×19 局面字符串 (B/W/.) + 落子检测事件


1. 方法对比

方法 原理 准确率 速度 光照敏感度 实现复杂度
ROI 灰度阈值 交点 ROI 均值 → 阈值分类 ~85% 极快
HSV 颜色空间 H/S/V 三维阈值 ~90%
HoughCircles 圆检测 → 形状匹配 ~80%
CNN 深度卷积 ~98% 慢(GPU)
ROI 均值+SVM 特征向量 + 分类器 ~93%
本项目选用: ROI 双阈值 + 反光补偿 统计特征 + 自适应 ~90% 极快 低-中

选型理由: ROI 均值在矫正后的标准棋盘图上效果足够好。灰度均值天然区分黑白(黑子<80, 白子>200, 空=120-170)。加上反光补偿和帧间一致性校验,准确率可达 ~90%。


2. 算法流程

flowchart TD
    INPUT["输入: 矫正棋盘图 600x600<br>+ 361 交点坐标 gridPixel"] --> PRE["预处理<br>BilateralFilter 去噪<br>CLAHE 均衡光照"]
    PRE --> LOOP["遍历 361 交点"]
    LOOP --> ROI["取交点 ROI<br>r = cellSize x 0.38"]
    ROI --> FEAT["提取特征<br>mean, stddev, min, max"]
    FEAT --> CLASSIFY{"分类"}
    CLASSIFY -->|"mean lt M_DARK<br>AND std lt S_DARK"| BLACK["⚫ 黑子"]
    CLASSIFY -->|"mean gt M_LIGHT<br>AND std lt S_LIGHT"| WHITE["⚪ 白子"]
    CLASSIFY -->|"其他"| EMPTY["· 空"]
    BLACK --> STABLE{"连续 N 帧<br>一致?"}
    WHITE --> STABLE
    EMPTY --> STABLE
    STABLE -->|"✅"| OUTPUT["19x19 局面字符串<br>BWB...W."]

2.1 为什么用 stddev?

黑子在某些光照下会产生镜面反射高光,ROI 内有白斑 → 均值被拉高 → 误判为空。但高光使 stddev 异常大。所以:

黑子判定:  mean < 90 AND std < 45  (无高光暗区)
白子判定:  mean > 200 AND std < 40  (明亮均匀)
反光黑子:  mean ≥ 90 AND std ≥ 45  (暗但色散大 → 大概率反光黑子)
空交叉点:  120 ≤ mean ≤ 180 AND std < 25 (均匀棋盘色)

3. 帧差法落子检测

sequenceDiagram
    participant Cam as 摄像头
    participant Det as StoneDetector
    participant State as 局面状态机
    participant Game as GoGame

    Cam->>Det: frame(t) — 矫正图
    Det->>Det: classify() → state_t
    Det->>State: diff = state_t ⊕ state_{t-1}

    alt diff == 无变化
        State->>State: stableCount++ (无变化计数)
    else diff == 1 增加 + 0 减少
        State->>State: stableCount++
        State->>State: if stableCount >= K → confirm 落子
        State->>Game: emit stonePlaced(row,col,color)
    else diff 有增减
        State->>State: stableCount = 0 (抖动, 等待稳定)
    end

3.1 防抖参数

参数 默认值 说明
STABLE_FRAMES 5 连续一致帧数才确认
DETECT_INTERVAL 500ms 检测间隔 (减少 CPU)

4. 类设计

classDiagram
    class StoneDetector {
        -double m_darkMean, m_darkStd
        -double m_lightMean, m_lightStd
        -int m_stableFrames
        +classify(dewarpedBgr, gridPixel) string
        +setThresholds(darkMean, darkStd, lightMean, lightStd)
        +preprocess(bgr) Mat
        +extractFeatures(roi) Features
    }

    class MoveDetector {
        -string m_prevState
        -string m_currentState
        -int m_stableCount
        +processFrame(dewarpedBgr, gridPixel) vector~DetectedMove~
        +reset()
    }

    class DetectedMove {
        +int row, col
        +Stone stone
        +bool isPlacement
        +int stableCount
    }

    class StoneRecognizer {
        <<CvGoBoardDetector 扩展>>
        +recognizeStones(gray, gridPixel) string
    }

    MoveDetector --> StoneDetector : uses
    MoveDetector --> DetectedMove : produces

4.1 StoneDetector

class StoneDetector {
public:
    struct Params {
        double darkMeanMax  = 90;    // 黑子均值上限
        double darkStdMax   = 45;    // 黑子标准差上限
        double lightMeanMin = 200;   // 白子均值下限
        double lightStdMax  = 40;    // 白子标准差上限
        double roiRatio     = 0.38;  // ROI 半径 = cell * ratio
        double reflectMean  = 100;   // 反光补偿: mean≥此值且std大→判黑
        double reflectStd   = 50;
    };

    std::string classify(const cv::Mat &dewarpedBgr,
                         const std::vector<cv::Point2f> &gridPixel);
    void setParams(const Params &p) { m_params = p; }

private:
    struct Features { double mean, stddev, minVal, maxVal; };
    Features extractFeatures(const cv::Mat &roi);
    char classifyRoi(const Features &f);
    Params m_params;
};

4.2 MoveDetector

class MoveDetector {
public:
    struct Config {
        int stableFrames = 5;
        int detectIntervalMs = 500;
    };

    std::vector<DetectedMove> processFrame(
        const cv::Mat &dewarpedBgr,
        const std::vector<cv::Point2f> &gridPixel);

    void reset();
    std::string currentState() const { return m_currentState; }

private:
    StoneDetector m_detector;
    std::string m_prevState;
    std::string m_currentState;
    std::string m_stableState;
    int m_stableCount = 0;
    Config m_config;
    QTimer *m_timer;
};

5. 参数自适应

实际使用中光照条件不确定,提供参数调节接口:

暗光环境:
  降低 darkMeanMax (60-70)
  降低 lightMeanMin (170-180)
  增大 roiRatio (0.40-0.42) → 更大的采样区

强光环境:
  增大 darkMeanMax (100-110)
  增大 lightMeanMin (210-220)
  减小 roiRatio (0.35-0.38)

默认:
  通过界面 Slider 手动调节, 实时预览分类效果

6. 与现有系统集成

IBoardDetector (detect/IBoardDetector.h)
    ↑
    │
StoneDetector + MoveDetector
    │
    ├──→ 矫正图 ← BoardRectifier
    ├──→ 出力局面 → GoGame (via CommandInvoker)
    └──→ 帧差检测 → emit stonePlaced(row,col,color)

集成点:

  • GoBoardDetector::recognizeStones() — 已有骨架,替换为 StoneDetector 实现
  • MainWindow::onStreamFrame() — 增加检测模式的局面更新
  • GoGame::executeCommand() — 检测到的落子通过命令模式注入

7. 实现计划

步骤 内容 文件
1 ROI 特征提取 + 阈值分类 detect/StoneDetector.h/cpp
2 帧差法落子检测 + 防抖 detect/MoveDetector.h/cpp
3 替换 GoBoardDetector::recognizeStones() 修改现有 detect/GoBoardDetector.cpp
4 MainWindow 集成:实时局面刷新 修改 ui/MainWindow.cpp
5 可视化反光:识别结果叠加到 LiveView 修改 detect/LiveView.cpp