课程定位: 围棋打谱辅助系统系列课程的 Part 1.1,衔接 Part 0(基础视觉系统)和 Part 2(游戏引擎)。本章聚焦棋子检测的模型训练全流程:从标注管线设计、YOLO 星位检测方案,到模型训练实战与性能调优。
前置知识: Part 0 棋盘矫正管线、Python 基础、PyTorch 基础
预计阅读: 40 分钟 | 实战耗时: 标注 5-7h + 训练 1-2h
目录
1. 概述:为什么需要模型训练
Part 0 完成了棋盘矫正——从倾斜的摄像头画面中提取 600×600 的标准俯视图。下一步是识别棋盘上 361 个交点是否有棋子,以及棋子的颜色。
我们可以用传统 CV 方法(ROI 采样 + 双阈值)做初步判断,但准确率受光照影响大。为了达到生产级可靠性,需要训练一个轻量神经网络来做 361 分类。
同时,手工点击 4 角星位的标定流程也可以用 YOLO 自动检测替代——9 个星位标记点的检测不仅能自动标定,还能提供 RANSAC 鲁棒性。
本章覆盖这两个模型的完整生命周期:数据采集 → 标注 → 训练 → ONNX 导出 → C++ 集成。
2. 标注管线设计
依赖: Phase 0 棋盘矫正已完成 | 目标: 产出 YOLO 格式训练数据集
2.1 整体流程
Magic_GT 代码实现 手工操作
════════════════ ══════════════
① 采集端改造 → CalibWindow 加采集模式
→ ② 连接 ESP32-CAM + 标定
→ ③ 摆放棋子 + 变化光照
→ ④ 逐批拍摄 500 张
⑤ auto_label.py → ROI 初判生成 stone_state
→ ⑥ 用 review_labels.py 逐张纠正
⑦ synthetic_data.py → 空棋盘合成 2000 张(自动)
⑧ batch_label.py → 转 YOLO 格式 → dataset/
→ ⑨ 抽查 10% 标注质量
→ ⑩ 确认 dataset 就绪
2.2 手工操作步骤
按执行顺序列出所有需要手动操作的步骤:
| 序号 | 操作 | 预估耗时 | 说明 |
|---|---|---|---|
| ② | 连接 ESP32-CAM,标定棋盘 | 2 分钟 | 打开 go-board-calib.exe,Connect,标记 4 角星位 |
| ③ | 摆棋子,布置不同局面 | 分批进行 | 至少包含:空盘、开局、中盘、官子四种阶段 |
| ④ | 在 5 种光照下拍摄 500 张 | 2-3 小时 | 每换一种光照 → 摆 3-4 种局面 → 每种局面拍约 25 张 |
| ⑥ | 人工纠正 ROI 误判 | 2-3 小时 | 用 review_labels.py 逐张检查,键盘快捷键修改 |
| ⑨ | 抽查标注质量 | 30 分钟 | 从 dataset 随机抽 25-50 张,肉眼确认 bbox 贴合棋子 |
| ⑩ | 确认 dataset 就绪 | 5 分钟 | 运行训练脚本前的最后一次检查 |
总计约耗时 6-8 小时,可分多次完成。其余步骤(①⑤⑦⑧)由脚本自动实现,全自动。
2.3 采集端改造
CalibWindow 新增采集模式:
工具栏新增 checkbox: [☐ Collect] [📸 Capture]
开启 Collect 模式后:
- onFrame() 中矫正图自动保存到 pic/collect/
- 文件名: raw_YYYYMMDD_HHmmss_sss.jpg
- 同时调用 StoneDetector 生成初判 stone_state
- 初判写入: raw_xxx_state.txt
- 底部状态栏显示: "Collected: 47/500"
节流控制: 每 500ms 最多保存一帧(约 2fps),避免同一局面产生大量重复帧。
2.4 采集矩阵
覆盖 5 种光照 × 4 种局面阶段:
| 空盘 | 开局(20手) | 中盘(80手) | 官子(150手) | |
|---|---|---|---|---|
| 自然光(白天窗边) | 25 张 | 25 张 | 25 张 | 25 张 |
| 台灯顶部直射 | 25 张 | 25 张 | 25 张 | 25 张 |
| 侧光 | 25 张 | 25 张 | 25 张 | 25 张 |
| 暗光/傍晚 | 20 张 | 20 张 | 20 张 | 20 张 |
| 顶灯(室内) | 25 张 | 25 张 | 25 张 | 25 张 |
每格 20-25 张不是重复拍摄同一画面——每张之间稍微移动棋盘位置(±5cm)或轻微旋转摄像头角度(±3°),让矫正管线产生不同的像素分布。
2.5 标注生成
auto_label.py
# 核心逻辑
for each 交点 i:
if stone_state[i] != '.':
class_id = 0 if 'B' else 1
bbox = 以 gridPixel[i] 为中心, 半径 = cell_size * 0.35
→ 归一化为 YOLO 格式: class cx/w cy/h bw/w bh/h
review_labels.py 交互界面
┌─────────────────────┬──────────────────┐
│ │ File: 12 / 500 │
│ 矫正图 + │ Progress: ██░░ │
│ 检测叠加 │ │
│ (黑子蓝圈 │ 当前交点: (3,3) │
│ 白子红圈 │ ROI 判断: B │
│ 空位灰点) │ │
│ │ 修改: │
│ ← 鼠标悬停交点 │ [B] [W] [空] │
│ → 高亮 │ │
│ │ 快捷键: │
│ │ B=黑 W=白 │
│ │ Space=空 │
│ │ S 保存并翻页 │
└─────────────────────┴──────────────────┘
纠正优先级: 不用每张都仔细检查 361 个点。重点看:
- 棋盘边缘(四边第一/二行)— 光线衰减,ROI 容易误判
- 有手指/手的区域 — 遮挡污染 ROI
- 强反光区域 — 空位误判为白子
- 棋子密集区(中盘)— 相邻棋子互相影响
2.6 合成增强数据
python scripts/synthetic_data.py --count 2000 --output dataset/
逻辑:空棋盘矫正图 → 随机放置 10-200 个黑白棋子 → 绘制带位置噪声的圆(σ=2px)→ 随机亮度 ±20%、对比度 ±15% → 随机高斯噪声/模糊 → 输出 YOLO 格式标注。
优势:不限量、标注完美(自己放子 → stone_state 精确已知)、覆盖棋子密度全范围。
2.7 最终数据集目录
go-board-cpp/
└── dataset/
├── data.yaml
├── images/
│ ├── train/ # ~1750 张
│ ├── val/ # ~375 张
│ └── test/ # ~375 张
└── labels/
├── train/ # 每张对应一个 .txt
├── val/
└── test/
3. YOLO 星位检测方案
3.1 目标
用 YOLO 检测 19 路棋盘上的 9 个星位标记点,自动计算单应矩阵完成棋盘矫正,替代当前手工点击 4 角的流程。
九星位坐标(19 路棋盘):
(3,3) (3,9) (3,15)
(9,3) (9,9) (9,15)
(15,3) (15,9) (15,15)
3.2 整体流程
ESP32-CAM 原始帧
→ YOLO 推理 (ONNX)
→ 9 个星位 bounding box + 置信度
→ NMS + 置信度过滤 (≥0.5)
├─ ≥4 个点 → findHomography(RANSAC) → 自动标定 ✅
└─ <4 个点 → 退化为手动点击标定
3.3 模型选择
| 选项 | 参数量 | 推理速度 (CPU) | 适用 |
|---|---|---|---|
| YOLOv8-n | 3.2M | ~50ms | ✅ 推荐 |
| YOLOv11-n | 2.6M | ~40ms | 更轻量 |
| YOLOv8-s | 11.2M | ~120ms | 精度更高 |
推荐 YOLOv8-n:单类检测(星位),目标小且特征一致,nano 足够。
训练配置:
| 参数 | 值 |
|---|---|
| 输入 | 640×640 |
| 类别 | 1 (star_point) |
| Epochs | 100 |
| 增强 | mosaic, hsv, flip, translate |
| 输出 | ONNX (opset 12, OpenCV dnn 兼容) |
3.4 自动/手动对比
| 对比 | 当前 (4 点手工) | 新方案 (YOLO 9 星位) |
|---|---|---|
| 操作 | 连接 → 手动依次点击 4 角 | 连接 → 点击"自动标定” |
| 时间 | ~15-30 秒 | ~100ms |
| 鲁棒性 | 4 点精确解 | 9 点 RANSAC,容忍遮挡/误检 |
| 复用性 | — | 星位检测可用于后续落子定位 |
3.5 实施计划
| 阶段 | 内容 | 工时估计 |
|---|---|---|
| P1 数据标注 | 扩展 MarkWidget 支持 9 星位标注 + YOLO 格式导出 | 2-3h |
| P2 采集标注 | 拍摄 50-100 张棋盘图 + 标注 | 30min |
| P3 训练 | Python 训练 YOLOv8-n + 导出 ONNX | 1-2h |
| P4 C++ 集成 | StarPointDetector + 自动标定逻辑 | 2-3h |
| P5 测试 | 实机测试 + 精度调优 | 1h |
4. 模型训练实战
4.1 V1 问题诊断
- 白子误识别: 棋盘网格线被错误分类为白色棋子
- 根因分析: 矫正图中棋盘网格线始终在固定像素位置。模型过度拟合了网格纹理特征
4.2 V2 改进:随机平移增强
核心思路: 对图片做 ±12px 随机平移 → 网格线不再固定在像素位置 → 模型被迫学习石子本身的形状/颜色特征
4.3 数据集
| 来源 | 数量 | 说明 |
|---|---|---|
| ESP32-CAM 实拍 | 180 张 | 手工采集,AI 模型预标注 |
| 格式 | 600×600 JPEG + 361 字符标签 |
标签分布 (180 张原始图):
- 空位
.: 81.3% - 白子
W: 10.4% - 黑子
B: 8.3%
4.4 增强策略 (5x → 900 张)
| 序号 | 增强类型 | 参数 | 说明 |
|---|---|---|---|
| 1 | shift |
平移 ±8px + 亮度 ±10 | 新增: 破坏网格位置锚定 |
| 2 | bc |
对比度 0.85-1.15, 亮度 ±20 | 光照变化 |
| 3 | hsv |
H±5, S±25, V±20 | 色调抖动 |
| 4 | noise |
高斯噪声 σ=2-6 | 传感器噪声 |
| 5 | combo |
shift + bc + hsv + noise + blur | 组合增强 |
4.5 训练配置
| 参数 | 值 |
|---|---|
| 架构 | 轻量 FCN (435K 参数) |
| 输入 | 304×304 RGB → 19×19×3 logits |
| 优化器 | AdamW, lr=1e-3, weight_decay=1e-4 |
| 调度器 | CosineAnnealingLR (15 epochs, eta_min=1e-5) |
| 损失函数 | CrossEntropyLoss + Class Weights |
| 批大小 | 32 |
| Epochs | 15 |
| 训练/验证 | 720/180 (按原始图分组拆分) |
| 计算设备 | CPU |
类别权重(处理样本不平衡):
Empty (.): 0.412
White (W): 3.175
Black (B): 3.883
4.6 训练结果
Epoch 1 val_acc = 0.8259 *
Epoch 2 val_acc = 0.9491 *
Epoch 3 val_acc = 0.9954 *
Epoch 4 val_acc = 0.9943
Epoch 5 val_acc = 0.9895
Epoch 6 val_acc = 0.9921
Epoch 7 val_acc = 0.9949
Epoch 8 val_acc = 0.9949
Epoch 9 val_acc = 0.9960 *
Epoch 10 val_acc = 0.9955
Epoch 11 val_acc = 0.9956
Epoch 12 val_acc = 0.9977 *
Epoch 13 val_acc = 0.9984 * ← 最佳
Epoch 14 val_acc = 0.9975
Epoch 15 val_acc = 0.9982
最佳模型 (Epoch 13) 分类准确率:
| 类别 | 准确率 | 样本数 |
|---|---|---|
空位 . |
99.81% | 53,665 |
白子 W |
100.00% | 6,505 |
黑子 B |
99.90% | 4,810 |
| 整体 | 99.84% | 64,980 |
4.7 V1 vs V2 对比
| 指标 | V1 (之前) | V2 (现在) |
|---|---|---|
| 输入图 | 164 张 | 180 张 |
| 增强数 | 820 张 | 900 张 |
| 白子准确率 | 97.5% | 100.0% ✅ |
| 整体准确率 | 99.2% | 99.8% ✅ |
| 平移增强 | 无 | 离线 + 在线 ✅ |
4.8 模型文件
| 文件 | 大小 | 用途 |
|---|---|---|
models/go-stone-classifier.pth |
1.7 MB | PyTorch checkpoint |
models/go-stone-classifier.onnx |
1.7 MB | OpenCV dnn 推理 (单文件, opset 12) |
模型接口:
输入: image (1, 3, 304, 304) float32 [0,1] RGB
输出: logits (1, 3, 19, 19) float32
推理: argmax(dim=1) → 19×19 网格 → 映射 {0:'.', 1:'W', 2:'B'}
5. C++ 推理集成
5.1 棋子检测 (StoneDetector)
C++ 源码无需修改,仅替换 ONNX 模型文件:
models/go-stone-classifier.onnx ← 自动加载 (main_calib.cpp 启动时)
StoneDetector 类支持双模式:
- AI 模式 (默认): ONNX 推理 (304×304 → 19×19)
- 阈值模式 (fallback): ROI 采样 + 双阈值
5.2 星位检测 (StarPointDetector)
新增类:StarPointDetector
class StarPointDetector {
bool loadModel(const std::string &onnxPath);
std::vector<cv::Point2f> detect(const cv::Mat &rawBgr, float confThresh = 0.5);
private:
cv::dnn::Net m_net;
static constexpr int INPUT_SIZE = 640;
};
推理流程:
// 1. YOLO 前向
blobFromImage(rgb, 1/255.0, 640, 640, Scalar(), true, false);
m_net.setInput(blob);
cv::Mat output = m_net.forward(); // [1, 8400, 6] (x,y,w,h,conf,cls)
// 2. 解析 + NMS
for each detection with cls==0 && conf >= 0.5:
boxes.push_back(xywh2rect)
confs.push_back(conf)
cv::dnn::NMSBoxes(boxes, confs, 0.5, 0.4, indices);
// 3. 取 box 中心作为星位坐标 → 匹配到棋盘坐标
// 4. RANSAC 单应矩阵
if (starPoints.size() >= 4) {
cv::Mat H = cv::findHomography(boardCoords, starPoints, cv::RANSAC, 3.0);
m_calibrator->setHomography(H); // 自动标定完成 ✅
}
5.3 文件清单
go-board-cpp/
├── models/
│ ├── go-stone-classifier.onnx (棋子分类)
│ └── go-star-yolo.onnx (星位检测)
├── scripts/
│ ├── auto_label.py
│ ├── review_labels.py
│ ├── batch_label.py
│ ├── synthetic_data.py
│ ├── augment_dataset.py
│ ├── retrain.py
│ └── train_star_yolo.py
├── src/detect/
│ ├── StoneDetector.h/cpp
│ ├── StarPointDetector.h/cpp (新增)
│ └── PersistentCalibrator.h (新增 setHomography)
└── src/main_calib.cpp (集成自动标定按钮)
6. 小结与下一步
这一章我们完成了从数据采集到模型推理的完整闭环:
- 标注管线 — 定义了采集-标注-纠正-合成-导出的五阶段流水线,约 6-8h 手工操作即可产出高质量训练集
- YOLO 星位检测 — 用 9 点 RANSAC 替代 4 点手工标定,省时且鲁棒
- 模型训练 — 435K 参数的轻量 FCN,在 900 张增强数据上达到 99.84% 整体准确率
- OpenCV DNN 推理 — 1.7MB ONNX 模型,304×304 输入,CPU 实时推理
关键设计原则:
- 数据与代码分离: 标注管线完全独立于训练脚本,数据集可复用、可版本管理
- 双模式推理: AI 模式优先,阈值模式兜底 — 没有模型也能跑
- 渐进式增强: 平移增强是解决网格线过拟合的最小化干预
下一步 → Part 2: 将检测结果接入 GoGame 游戏引擎,实现命令模式 + AI 对弈 + SGF 打谱的完整交互系统。
相关文档: