牌面角标识别与阈值校准: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 实时数据的尺度/形态不一致问题。修复套路是:
- 多尺度匹配 → 跟上实时变化
- 阈值偏严 → 宁可漏报
- JOKER 形状预判 + 颜色分类 → 单独通道
- 稀有大牌保护 → 反向校验
四招组合后 JOKER 准确率 71% → 94%,误报率 6.8% → 0.4%。剩下的 6% 错误交给 CNN(下一篇讲)。
九、下一篇预告
手牌带识别比单牌识别难 10 倍:两行牌紧贴着,还经常被 UI 按钮(首充礼包/挂小王/过牌)干扰。
下一篇讲 《手牌带定位与行分割:亮度/颜色投影实战》——怎么从一行花花绿绿的 UI 里,把"我的 12 张牌"切出来。
系列目录
本机实测环境:Windows 11 + Python 3.11 + OpenCV 4.8 + 手机 1080×1920 投屏。
浙公网安备 33010602011771号