牌面角标识别与阈值校准:JOKER 误识踩坑实录(多尺度模板匹配实战)

牌面角标识别与阈值校准:JOKER 误识踩坑实录(多尺度模板匹配实战)

"识别牌面难的不是找出 13 个数字,难的是让 JOKER 不再被认成错,有时候大小王会被一些玩家编号之类的挡住、手遮住又或者广告遮住,这次遇到了两个一个是玩家编号36的有时候会贴在牌上,另一个则是时不时蹦出来的广告和对话。"

在这里插入图片描述


一、背景:为什么先写 JOKER

最近在写一个够级记牌器,要从手机画面里识别"我手里还剩什么牌"。整套链路里,最先遇到、也是最难修的,就是 JOKER(大小王)。

它们的角标长这样:

在这里插入图片描述

看着人眼一目了然,但模板匹配一上,先把 JOKER 认成了 36,再过一会儿又认成 38。同一张牌,前后两帧识别不一致——这种 bug 在记牌器里直接致命:还剩什么牌全乱套。


二、现场对比:JOKER 实时 vs 训练模板

我做了 17 张"实时牌 vs 训练模板"对比图,问题一目了然:

在这里插入图片描述

关键差异:实时牌 50×70,训练模板 32×32。

人眼看都是"王",但 cv2.matchTemplate 是逐像素比对,50×70 的高瘦"王"和 32×32 的矮胖"王"算出来归一化相关系数只有 0.71——刚好卡在我之前的 0.72 阈值上下,于是匹配到的下一个最高分是 "3" 或 "6" 的角标。

我又看了一眼训练样本集:

在这里插入图片描述

训练样本全是规整的 32×32 居中图。但运行时牌面大小会随屏幕分辨率、缩放、滚动条浮动 50%~150%,根本不固定。

问题清楚了:实时图尺度变,模板不跟着变,匹配分数就崩。


三、根因复盘

我把踩坑原因整理成四类:

现象 直接原因 深层原因
JOKER 认成 36/38 matchTemplate 分数被 3/6 反超 实时 50×70 vs 模板 32×32 尺度差 1.5×
同一张牌前后两帧不一致 阈值卡在分数边界 单一尺度匹配,没有候选排序
红心♥ vs 方块♦混淆 HSV 红 mask 没分开 实时红比训练样本更"湿"(饱和度低)
大王小王互相认错 没有颜色/形状校验 JOKER 角标只有"JOKER"四个字,花色靠底部"圆"/"方"区分

四、修复方案:四招组合拳

4.1 多尺度模板匹配(核心修复)

思路:把模板做 0.5~1.5 共 5 个尺度缩放,每个尺度算一次分数,取最高。

def match_point(img_gray, templates, scales=(0.5, 0.7, 0.85, 1.0, 1.2, 1.5)):
    """多尺度模板匹配,返回 (标签, 最高分)。"""
    best = ("", -1.0)
    for label, tpl in templates.items():
        for s in scales:
            new_w = max(4, int(tpl.shape[1] * s))
            new_h = max(4, int(tpl.shape[0] * s))
            tpl_s = cv2.resize(tpl, (new_w, new_h), interpolation=cv2.INTER_AREA)
            if tpl_s.shape[0] > img_gray.shape[0] or tpl_s.shape[1] > img_gray.shape[1]:
                continue
            res = cv2.matchTemplate(img_gray, tpl_s, cv2.TM_CCOEFF_NORMED)
            _, score, _, _ = cv2.minMaxLoc(res)
            if score > best[1]:
                best = (label, float(score))
    return best

实测:JOKER 准确率从 71% → 94%。

4.2 阈值校准(宁可漏报不误报)

记牌器场景里,"漏报一张牌"只是少记,"误报一张牌"会污染推理。所以阈值偏严。

我做了个简单校准表:

牌类型 阈值 理由
普通数字(2~10) 0.78 角标形状差异大,太低会和 JOKER 混
大写字母(J/Q/K/A) 0.80 字母比数字稳,阈值可放松
JOKER 0.85 字符长、形状窄,宁可拒识不误识
大王/小王分类 0.90 二选一,靠底部圆形/方形区分

低于阈值的牌直接标 "?" 而不是猜一个最像的——日志里能看到,但不会进推理。

4.3 JOKER 形状预判 + 颜色分类

JOKER 的角标有一个特征:aspect ratio > 1.4(瘦高)。先用形状预判,再走专门分类器:

def _is_joker_shaped(crop_gray):
    h, w = crop_gray.shape[:2]
    if h < 20 or w < 6:
        return False
    ar = h / w
    # JOKER 角标高瘦比 1.5~2.0,普通牌 0.9~1.2
    return 1.4 < ar < 2.5

def _classify_joker_color(crop_bgr):
    """JOKER 颜色:大王红/小王黑,靠 HSV 区分。"""
    hsv = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2HSV)
    red_mask = ((hsv[:, :, 0] < 10) | (hsv[:, :, 0] > 170)) & (hsv[:, :, 1] > 100)
    return "大王" if red_mask.sum() > 20 else "小王"

4.4 稀有大牌保护(逆向校验)

一局牌里,每种花色最多 4 张(A、2、3...)。我维护了一个"已识别花色-点数"集合,识别出新牌时反向校验:

  • 若识别为"方块 A"但已经识别过 4 张方块 A → 拒识
  • 若识别为 JOKER 但已经是第 3 张王 → 拒识

这一条规则直接把误报率压到 0.5% 以下。


五、完整调用链

def identify_card(crop_bgr, templates, identified_set):
    # 1. 预处理(详见 58 篇)
    preproc = preprocess_digit(crop_bgr)  # -> 32×100 黑底居中
    gray = preproc if len(preproc.shape) == 2 else cv2.cvtColor(preproc, cv2.COLOR_BGR2GRAY)

    # 2. 多尺度模板匹配
    label, score = match_point(gray, templates)

    # 3. JOKER 专门路径
    if _is_joker_shaped(gray):
        color = _classify_joker_color(crop_bgr)
        if score > 0.85 and (color, "JOKER") not in identified_set:
            identified_set.add((color, "JOKER"))
            return f"{color}王", score

    # 4. 阈值过滤
    if score < 0.78:
        return "?", score

    # 5. 逆向校验
    if label in identified_set and identified_set.count(label) >= 4:
        return "?", score

    identified_set.add(label)
    return label, score

六、实测数据

跑了 3 局真实牌(每局约 80 张手牌):

牌型 旧方案 新方案
普通数字(2~10) 92.1% 97.3%
大写字母(J/Q/K/A) 88.7% 95.8%
JOKER 71.4% 94.2%
误报率(不该识的牌) 6.8% 0.4%

JOKER 提升最大(+22.8 个百分点),符合预期。


七、算法优缺点对比

方案 优点 缺点
单尺度模板匹配 快、代码短 实时尺度变化直接挂
多尺度模板匹配 鲁棒性大幅提升 计算量 ×5,CPU 吃紧
CNN 推理 抗噪抗旋转 需要训练样本、模型部署
多尺度 + CNN + 阈值 + 逆向校验 准确率最高 工程复杂,慢 30~50ms

多尺度模板匹配 vs CNN:实测这个场景两者精度差不多(94% vs 95%),但 CNN 需要 onnxruntime + 训练集,多尺度方案无需训练、当天就能上线。所以前期优先多尺度。


八、小结

JOKER 误识不是算法问题,是训练数据 vs 实时数据的尺度/形态不一致问题。修复套路是:

  1. 多尺度匹配 → 跟上实时变化
  2. 阈值偏严 → 宁可漏报
  3. JOKER 形状预判 + 颜色分类 → 单独通道
  4. 稀有大牌保护 → 反向校验

四招组合后 JOKER 准确率 71% → 94%,误报率 6.8% → 0.4%。剩下的 6% 错误交给 CNN(下一篇讲)。


九、下一篇预告

手牌带识别比单牌识别难 10 倍:两行牌紧贴着,还经常被 UI 按钮(首充礼包/挂小王/过牌)干扰。

下一篇讲 《手牌带定位与行分割:亮度/颜色投影实战》——怎么从一行花花绿绿的 UI 里,把"我的 12 张牌"切出来。


系列目录

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

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

posted @ 2026-08-18 23:25  橘和柠  阅读(8)  评论(0)    收藏  举报