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 |