课程定位: 围棋打谱辅助系统系列课程的 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 做星位)。