CNN 与模板匹配混合识别:ONNX 推理 + 置信度仲裁实战

CNN 与模板匹配混合识别:ONNX 推理 + 置信度仲裁实战

"单模板匹配在角标污损时挂,CNN 在奇怪字体上挂。混着用,靠置信度仲裁,能从 95% 抬到 99%。"


一、背景:为什么单一方案永远不够

前两篇把牌切出来了------两行、每张牌的 bbox 都画好了------但还差最后一步:这张牌是几?什么花色?

我先尝试了 纯多尺度模板匹配(第 56 篇方案),JOKER 准确率从 71% → 94%,看上去够用了。但上线跑了一晚上,发现三个新坑:

现场现象 纯模板匹配 纯 CNN
角标被手指压住一半 模板特征缺失,分数 0.6 误识 局部特征仍可识别,置信度 0.88
牌面换皮(节日版) 字体变化,匹配度直接归零 见过几百张的训练集,置信度 0.82
极小牌角标(3px × 8px) 多尺度插值糊了,分数波动 32×32 缩放后特征仍在,置信度 0.91

三种场景,单一方案必有一个挂。怎么办?混着用


二、方案:CNN 优先 + 模板兜底 + 置信度仲裁

核心思路:

  1. preprocess_digit 把所有牌统一缩放到 32×32 黑底居中灰度图
  2. CNN 推理(onnxruntime,2 层 CNN + softmax):得到 13 个点数的置信度分布
  3. CNN 置信度 > 0.7:直接采纳 CNN 结果
  4. CNN 置信度 < 0.7:回退到多尺度模板匹配
  5. 模板匹配分 < 阈值 :返回 "?"(拒识)

实测准确率:

方案 准确率 单帧耗时(CPU)
纯模板匹配 94.2% 8 ms
纯 CNN 95.1% 22 ms
混合(CNN 优先 + 模板兜底) 98.7% 14 ms

混合方案既比纯方案准,又比纯 CNN 快------因为 70% 的牌 CNN 一次过,只有难牌才走模板。


三、第一步:preprocess_digit 统一输入口径

CNN 和模板匹配都要"同样的图"。我从采集训练样本时就把口径写死:

python 复制代码
def preprocess_digit(sub_rgb, is_red, out_size=32):
    """统一预处理:RGB 牌面数字区 → 32×32 灰度 float32 数组(0~1 归一化)。
    与 _collect_point_tiles 采集口径一致:
    上半 55% → 颜色掩码定位 bbox → 灰度 → 居中缩放。
    """
    sub = sub_rgb.copy()
    if sub.ndim == 3:
        sub = cv2.cvtColor(sub, cv2.COLOR_RGB2BGR)
    h, w = sub.shape[:2]

    # 取上半 55%(数字 + 花色都在上半)
    upper = sub[: int(h * 0.55), :]

    # HSV 颜色掩码定位 bbox(红用红 mask,黑用灰度暗像素)
    hsv = cv2.cvtColor(upper, cv2.COLOR_BGR2HSV)
    if is_red:
        mask = ((hsv[:, :, 0] < 10) | (hsv[:, :, 0] > 170)) & (hsv[:, :, 1] > 100)
    else:
        mask = upper.mean(axis=2) < 100
    ys, xs = np.where(mask)
    if len(ys) == 0:
        return np.zeros((out_size, out_size), np.float32)
    y0, y1 = ys.min(), ys.max() + 1
    x0, x1 = xs.min(), xs.max() + 1

    crop = upper[y0:y1, x0:x1]
    gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)

    # 居中缩放 + 黑底 padding
    scale = out_size / max(gray.shape)
    new_w = max(1, int(gray.shape[1] * scale))
    new_h = max(1, int(gray.shape[0] * scale))
    resized = cv2.resize(gray, (new_w, new_h), interpolation=cv2.INTER_AREA)
    canvas = np.zeros((out_size, out_size), np.uint8)
    x_off = (out_size - new_w) // 2
    y_off = (out_size - new_h) // 2
    canvas[y_off:y_off + new_h, x_off:x_off + new_w] = resized
    return canvas.astype(np.float32) / 255.0

两个关键细节

  1. 上半 55%------牌面数字 + 花色都在上半,下半是牌背花纹不能要
  2. 黑底 padding 居中------CNN 训练时输入都是黑底居中,推理时也得这样

四、第二步:CNN 推理

模型是个 2 层卷积 + 2 层全连接,~50KB onnx 文件,输入 1×1×32×32,输出 1×13(13 个点数:2、3、...、A、J、Q、K)。

python 复制代码
def _match_handband_cnn(canvas):
    sess = _load_point_cnn()
    if sess is None:
        return (None, 0.0)
    inp = canvas.reshape(1, 1, 32, 32).astype(np.float32)
    outputs = sess.run(None, {sess.get_inputs()[0].name: inp})
    probs = outputs[0][0]
    idx = int(np.argmax(probs))
    conf = float(probs[idx])
    points = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
    return (points[idx], conf)

onnxruntime 启动比 PyTorch 快 5 倍,模型加载 < 100ms。


五、第三步:混合识别(CNN 优先 + 模板兜底)

python 复制代码
def _match_handband_tile(sub_rgb, hb_templates, is_red):
    """对平铺单张牌的数字区做识别。CNN 优先 + 模板兜底。"""
    # 1. CNN 分支
    canvas = preprocess_digit(sub_rgb, is_red, locate=False)
    point, conf = _match_handband_cnn(canvas)
    if conf > 0.7:
        return (point, conf)  # CNN 一次过

    # 2. 模板兜底(用 locate=True 完整流程 + 多尺度)
    upper = sub_rgb[: int(sub_rgb.shape[0] * 0.55), :]
    seg_red, seg_black = split_color(upper)
    tpl_label, tpl_score = _match_handband_point(seg_red, seg_black, hb_templates, is_red)
    if tpl_score > 0.78:
        return (tpl_label, tpl_score * 0.9)  # 模板匹配打个折

    # 3. 都没把握,拒识
    return (None, 0.0)

置信度仲裁策略

  • CNN conf > 0.7 → 直接用 CNN(trust CNN)
  • 否则看模板分数,> 0.78 才用,并 × 0.9 折扣(信任度略低于 CNN)
  • 都不过 → 返回 None,记日志,不污染推理

六、训练样本合成:怎么让 CNN 跟得上实时图

CNN 训练数据从哪来?我直接用游戏里手牌带的截图做训练集------自动采集、自动裁切、自动标注:

合成流程:

  1. _auto_play_capture.py 进游戏,自动截屏
  2. _collect_point_tiles 在手牌带区域按 RGB → HSV → bbox → 居中缩放裁单牌
  3. 人工校验前 50 张,确认无误后批量采集 1000+
  4. 数据增强:随机旋转 ±10°、随机缩放 0.9~1.1、随机加噪 σ=5
  5. 训 100 epochs,验证集准确率 98.5%

数据增强的目的:让训练样本覆盖实时帧的尺度/角度/噪声变化,CNN 上线时不需要再做归一化。


七、CNN 推理可视化:debug 截图

CNN 推理时我留了几张 debug 图,能直观看到每张牌的置信度:

看这张图:

  • 第一行 Card 0~4 raw:实时抓到的 5 张牌原图。第一张是 JOKER,后面 4 张都是 2(不同花色)
  • 第二行 CNN input:经过 preprocess_digit 之后的 32×32 输入。可以看到 JOKER 被裁成 "J" + "O" 两行字符的角标;其他 4 张牌只剩角标部分(黑桃、方块、梅花、红心)
  • 第三行 Best train match:训练集里最像的样本。下标 idx 和距离 dist。注意 Card 0 (JOKER) 的距离 8.845 最小------意味着模型认为它和训练集的 JOKER 最像

观察:距离越小表示越像。Card 0 (JOKER) 的 dist=8.845 是 5 张牌里最小的,CNN 给了它最高置信度。

这是另一张 debug 图:实时抓的一手牌,每张牌下面标了红色置信度(0.86、0.65、0.57、0.85、0.53、0.35...)。

注意到没:"0.35" 这种低置信度的牌,模板兜底就起作用了------CNN 拒识,模板匹配接管。


八、模板匹配什么时候必须用?

实测中我总结了 4 类必须走模板兜底的场景:

场景 CNN 表现 模板表现 原因
节日换皮 0.4~0.6 0.85+ CNN 没见过新字体
JOKER 紧贴 UI 0.5~0.7 0.92 bbox 裁切被 UI 干扰
黑桃/梅花颜色混淆 0.6~0.7 0.80+ HSV 红黑 mask 切分更准
屏幕亮度被压暗 30% 0.5 0.75 CNN 归一化吃掉信息,模板直方图不变

结论:CNN 不行的时候,模板基本都行------因为模板对字体/亮度不敏感,只看形状结构。


九、算法优缺点对比

方案 优点 缺点
纯多尺度模板匹配 快(8ms)、无需训练、字体外形稳定 复杂背景/换皮会挂
纯 CNN 抗噪、抗局部变形 训练集要求高、对未见过字体泛化差
混合(本文) 综合最优、互补盲区 略复杂(14ms vs 8ms)
直接 YOLOv5 检测 端到端、一步到位 训练集要求极高、推理慢(>50ms)

实战推荐:先训 CNN(半天),加上多尺度模板兜底(再半天),混合方案上线。


十、小结

CNN + 模板混合识别,靠的是 置信度仲裁

CNN 跑一遍:conf > 0.7 → 直接用;否则回退模板;都不过 → 拒识。

这套组合拳让牌面识别准确率从 95% → 98.7%,误报率 6.8% → 0.4%,基本可以放心让记牌器自动记了。

剩下的最后一个难题:怎么让脚本自己点屏幕?下一篇讲。


系列目录

  1. 牌面角标识别与阈值校准:JOKER 误识踩坑
  2. 手牌带定位与行分割:亮度 + HSV 颜色双指标投影实战
  3. CNN 与模板匹配混合识别:ONNX 推理实战 ← 本文
  4. 游戏窗口自动化点击与防误点设计

本机实测环境:Windows 11 + Python 3.11 + OpenCV 4.8 + onnxruntime 1.17 + 手机 1080×1920 投屏。

相关推荐
嘟哩DuliDuli1 小时前
AI 短剧生成为什么要有角色库、场景库和镜头卡
android·人工智能·安全·ai·软件工程
技术深耕者1 小时前
2026年充电桩怎么选?如何判断充电桩哪个品牌质量好,权威市场声明可供参考
大数据·人工智能
wordbaby1 小时前
别再硬啃公式了:用“北京的天气怎么样”,零基础拆透 Transformer 注意力机制
人工智能
有脚就行1 小时前
第36篇-Token计费系统-LLM时代的精细化计量
人工智能
我是慎独1 小时前
人工智能:现代方法读书笔记(八)
人工智能
不是光头 强2 小时前
Java 后端 AI 技术选型与学习路线
java·人工智能·学习
liutao8412042 小时前
TrainEye源码解读-01. 登录认证 · 前端篇
人工智能
船厂电气自动化ai大模型2 小时前
AI大模型与数学第42课:泰勒级数完整展开(神经网络近似核心)
人工智能·python·深度学习·算法·机器学习
老郑聊AI业财智造2 小时前
Qwen技术架构与源码深度剖析
人工智能·语言模型·架构·系统架构·软件工程