课程定位: 围棋打谱辅助系统系列课程的 Part 1.3。在 P1.5 和 P1.6 分别介绍了双模型的训练和落地之后,本章从架构师视角分析:为什么星位检测用 YOLO 而棋子分类用自定义 FCN——两个任务本质不同,模型选型也应不同。
前置知识: Part 1.1 的双模型方案、Part 1.2 的 C++ 推理集成
预计阅读: 15 分钟
双模型选型:YOLO(星位检测)vs FCN(棋子分类)
项目:go-board-cpp
日期:2026-05-14
1. 概览
棋盘系统使用两个独立的神经网络模型,分别处理两个本质不同的任务:
| 星位检测 | 棋子分类 | |
|---|---|---|
| 模型 | YOLOv8-nano | 自定义 FCN |
| 任务类型 | 目标检测 (Object Detection) | 网格分类 (Grid Classification) |
| ONNX 文件 | go-star-yolo.onnx |
go-stone-classifier.onnx |
| 文件大小 | 12 MB | 1.7 MB |
| 参数量 | ~3M (8.1 GFLOPs) | ~1.7M |
| 输入 | 原始相机帧 (拉伸到 640×640 BGR) | 矫正后棋盘 (缩放到 304×304 RGB) |
| 输出 | 9 个边界框 (cx, cy, w, h) |
361 个分类 . / B / W |
| 训练数据 | 55 原图 + 275 增强 = 330 张 | 矫正图数据集 (含增强) |
| 调用频率 | 标定时 1 次 | 落子检测中每帧 1 次 |
| 推理速度 | ~100-200ms (CPU) | ~10-30ms (CPU) |
| 精度 | mAP50 = 0.995 | 约 85-90% per-class |
2. 星位检测 — 为什么用 YOLO
2.1 任务分析
输入是原始相机帧——棋盘可能歪斜、旋转、缩放、光照不均。星位点(棋盘上 9 个固定位置的实心圆)在画面里是 9 个小黑圆,位置完全取决于相机角度和棋盘摆放:
输入: 800×600 原始帧(棋盘位置不固定)
┌──────────────────────────┐
│ │
│ ░░░░░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░░░░░ │
│ ░░●░░●░░●░░●░░░ │ ← 星位圆点,位置/大小随视角变化
│ ░░░░░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░░░░░ │
│ ░░●░░░░●░░░░●░░ │
│ ░░░░░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░░░░░ │
│ ░░●░░●░░●░░●░░░ │
│ ░░░░░░░░░░░░░░░░ │
│ │
└──────────────────────────┘
核心问题:在整张图里找到 9 个特定目标的位置和大小。
这是目标检测的标准场景。
2.2 YOLOv8-nano 架构适配性
YOLO 的多尺度特征金字塔(P3/P4/P5,stride 8/16/32)天然适配:
| YOLO 特性 | 对星位检测的意义 |
|---|---|
| 多尺度检测头 | 近处星位大(P3),远处星位小(P5),三个尺度同时覆盖 |
| 密集 anchor | 8400 个候选框覆盖全图,不漏掉任何位置 |
| NMS 后处理 | 每个星位多尺度重复检测 → IoU 去重,保留最优 |
| 边界框回归 | 输出 (cx, cy, w, h),直接拿到中心坐标和大小 |
2.3 训练配置
model: yolov8n.pt # 从 COCO 预训练开始迁移
epochs: 100 # 充分收敛
imgsz: 640 # 标准 YOLO 输入尺寸
nc: 1 # 单类 (star_point)
max_det: 9 # 最多 9 个检出
conf: 0.3 # 低置信阈值 (尽量多检)
训练曲线:epoch 5 即达 mAP50=0.994,epoch 97 达 mAP50-95=0.844,precision=1.0,recall=1.0。
2.4 推理流程
原始帧 (BGR, 任意分辨率)
│
▼
blobFromImage → 拉伸到 640×640, 归一化 [0,1]
│
▼
ONNX 前向推理 → (1, 5, 8400) ← [cx, cy, w, h, conf]
│
▼
conf > 0.3 筛选 → NMS (IoU=0.3) → 最多 9 个 bbox 中心
│
▼
逆拉伸变换 → 原图坐标
│
▼
空间排序 + 身份匹配 → 9 星位的棋盘坐标 (3,3)…(15,15)
3. 棋子分类 — 为什么用自定义 FCN
3.1 任务分析
输入是已矫正的 600×600 标准棋盘——透视畸变已通过单应矩阵消除,每个交叉点的像素坐标是确定的(gridPixel[] 精确给出每个格点的 (x, y))。
输入: 600×600 矫正图(位置已知) 输出: 19×19 三分类
┌──────────┬──┬──┬──┬──┐ row 0: .............
│ │ │ │ │ │ row 1: .............
│ ├──┼──┼──┼──┤ row 2: ........W....
│ │⚫│⚪│ │ │ row 3: .....B..WB...
│ ├──┼──┼──┼──┤ ...
│ │⚪│⚫│ │ │ row18: .............
│ ├──┼──┼──┼──┤
│ │ │ │ │ │ 361 个固定位置
│ └──┴──┴──┴──┘ 每个判 3 类: . / B / W
└────────────────────────────────┘
核心问题:在 361 个已知位置上,判断放的是黑子、白子还是空的。
这不是目标检测,这是逐位置分类。
3.2 自定义 FCN 架构
输入: 304×304 RGB
│
▼
ConvBlock(3→32) + MaxPool → 152×152×32
│
▼
ConvBlock(32→64) + MaxPool → 76×76×64
│
▼
ConvBlock(64→128) + Conv+BN+ReLU → 38×38×128
│
▼
AdaptiveAvgPool2d(19, 19) → 19×19×128 ← 精确映射到棋盘格
│
▼
Conv2d(128→3, 1×1) → 19×19×3 ← 三分类 logits
│
▼
argmax → 361 字符 ".BWB..W."
关键设计决策:
| 设计 | 理由 |
|---|---|
AdaptiveAvgPool2d(19,19) |
强制输出 19×19 空间网格,每个格子对应一个棋盘交叉点 |
| 1×1 卷积分类头 | 逐位置独立分类,不引入跨格子依赖(每格独立判) |
| 304×304 输入 | 304 是 19 的 16 倍,保证 19×19 池化时每个格子有 16×16 的感受野 |
| 3 层卷积 + pool | 足够的感受野,但参数控制在 1.7M(轻量) |
| 无全连接层 | 全卷积设计,保持空间对应关系 |
3.3 为什么不用 YOLO 做棋子分类
| YOLO 的劣势 | 具体影响 |
|---|---|
| 361 个框的 NMS | 计算量 O(n²) ≈ 130K 次 IoU,严重拖慢推理 |
| 密集棋子 IoU 重叠 | 相邻棋子的 bbox 高度重叠,NMS 会误杀 |
| 位置已知却重复回归 | 矫正图里每个交叉点坐标已知,YOLO 却重新回归位置,浪费算力 |
| 模型 7 倍大 | 12MB vs 1.7MB,加载慢,CPU 推理慢 5-10 倍 |
| 对矫正图过设计 | 矫正图没有旋转/缩放/遮挡,YOLO 的多尺度检测能力全浪费 |
3.4 为什么不用 FCN 做星位检测
| FCN 的劣势 | 具体影响 |
|---|---|
| 固定输出网格 | 原始帧里棋盘位置不固定,FCN 假设"每格对应固定物理位置”,不成立 |
| 无多尺度检测 | 远处星位(5px)和近处星位(40px)需要不同感受野,FCN 单一尺度覆盖不了 |
| 需要预处理定位棋盘 | FCN 只分类不定位——得先用别的方法找到棋盘区域,增加复杂度 |
4. 性能对比
4.1 推理性能
| YOLOv8-nano | Custom FCN | |
|---|---|---|
| 参数量 | ~3M | ~1.7M |
| 计算量 | 8.1 GFLOPs | ~0.3 GFLOPs (估) |
| 模型大小 | 12 MB | 1.7 MB |
| CPU 推理 (单帧) | 100-200ms | 10-30ms |
| 调用频率 | 标定时 1 次 | 每 500ms 1 次 |
| 启动加载 | ~500ms | ~50ms |
4.2 精度
| YOLOv8-nano (星位) | Custom FCN (棋子) | |
|---|---|---|
| mAP50 | 0.995 | — |
| mAP50-95 | 0.844 | — |
| 训练图命中率 | 53/55 (≥8/9点) | — |
| Per-class accuracy | — | 85-90% |
| 主要误判 | 漏检遮挡星位 | 黑/白混淆 (弱光下) |
5. 双模型协作流程
相机帧到达
│
├─→ 星位检测 (YOLO) ← 标定时跑 1 次
│ └─→ 单应矩阵 H → 持久化
│
└─→ 矫正变换 (H)
│
▼
600×600 矫正图
│
├─→ 棋子检测 (FCN) ← 每帧跑
│ └─→ 361字符局面 ".BWB..W."
│
└─→ 落子检测 (MoveDetector)
└─→ 帧差 → 5帧确认 → 落子事件
6. 选型总结
"在哪里?" "是什么?"
│ │
目标位置未知,需要在 位置已知 (361交点),
画面中搜索定位 只需逐格分类
│ │
┌─────────▼─────────┐ ┌───────────▼──────────┐
│ YOLOv8-nano │ │ Custom FCN │
│ 目标检测 │ │ 网格分类 │
│ 12 MB │ │ 1.7 MB │
│ 100-200ms │ │ 10-30ms │
│ 标定时1次 │ │ 每帧检测时1次 │
└───────────────────┘ └──────────────────────┘
│ │
9个星位坐标 361个棋子分类
(cx,cy,w,h) ".BWB.....W."
一句话:星位是"找"的问题,棋子是"判"的问题——问题本质不同,模型自然不同。用错了方向,要么算力浪费(YOLO 做棋子),要么根本搞不定(FCN 做星位)。