YOLOV5扑克牌检测实战:52类数据集构建与训练全指南
2026/9/22 2:33:43 网站建设 项目流程

简介:一份面向目标检测实战的大型扑克牌图像数据集,已按YOLOV5目录结构整理,涵盖训练集与验证集,下载后可直接用于模型训练,无需额外格式转换。数据包含52种扑克牌类别(四种花色的A到K),训练集16000张、验证集4000张,共20000张720×720 RGB图片,每张均配有同名的txt标注文件,同时提供类别字典txt文件,方便映射标签。压缩包总大小约947.74MB,内部共2000个文件,除1999个txt标注与字典外,还附赠一个可直接运行的Python可视化脚本,随机传入一张图片即可绘制边界框并保存在当前目录,能直观检查标注质量。无论你是刚接触检测算法的学习者,还是正在训练自定义YOLO模型的开发者,这份数据量和类别粒度都比较充足。当前已有133人学习下载,适合作为扑克牌识别、卡牌检测等场景的基准数据集使用。

1. 52类扑克牌检测先过数据集这一关

做棋牌识别的工程师都有一个共同体会:模型结构不是瓶颈,数据集才是。真正的扑克牌检测涉及 52 个类别,公开数据里要么只有 13 种牌面的分类集,要么是单张牌特写的分类图,直接拿去训练多目标检测器根本不成立。YOLOV5 目录格式的价值,是把图片、标签、类别清单三者的绑定关系用目录结构固定下来;拿到一个符合规范的大型扑克牌检测数据集,从解压到跑通训练可以控制在半天内。

这次按目录规范、标签转换、数据校验、训练参数、难类处理五个环节推进,每个环节都有直接可复制的命令和脚本,覆盖从拿到数据集到产出权重的完整路径。不少团队在这条路上踩过同一个坑:数据集格式不对,训练日志只给一个 warning,却能让人白跑好几天。

适合读的人,是准备做牌面识别、AR 发牌引导或牌桌自动化监控的工程师。新手能照着目录结构把数据集理顺,熟手可以重点看第 3 章的按组划分和第 5 章的难类过采样,这两处是扑克牌这类语义高度相似的目标与通用检测差异最大的地方。

2. 扑克牌数据集目录结构:YOLOV5格式的硬规范

2.1 images与labels的目录对应关系

YOLOV5 对数据集目录的要求比很多人以为的更严格。它不允许 images 与 labels 用不同的子目录命名,也不支持把标注文件全部堆在一个文件夹里。标准结构是:

poker_dataset/ ├── images/ │ ├── train/ │ │ ├── frame_000001.jpg │ │ ├── frame_000002.jpg │ │ └── ... │ └── val/ │ ├── table_001_0001.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── frame_000001.txt │ │ ├── frame_000002.txt │ │ └── ... │ └── val/ │ ├── table_001_0001.txt │ └── ... └── data.yaml

YOLOV5 的 dataloader 构建样本列表时,会取 images 下每个图片的相对路径,把路径中的 images 替换成 labels、把扩展名替换成 .txt,然后检查这个文件是否存在。目录树里任何一级名字写错,比如写成 label 或 annotations,样本会被静默跳过,训练日志里只有一行 dataset warnings,不注意就漏掉。所以拿到扑克牌数据集的第一步不是看图片质量,而是用 tree 命令核对目录层级。

文件名规范同样容易被忽略。图片名必须是纯文件名加扩展名,不能含空格和中文,路径深度控制在 images/train 这种两级以内。我遇到过一张名为 IMG_0421 copy.jpg 的样本,对应的 txt 也带空格,Windows 和 Linux 对路径空格的处理不一致,同一份数据换机器后大量标签加载失败。批量重命名脚本是拿到数据集后第一个要跑的脚本。

提示:统一改成纯数字前缀命名,例如 frame_000001.jpg,比保留原始照片名省去后续大量路径问题。

标签文件每行一个目标,格式固定为五列:

<class_id> <x_center> <y_center> <width> <height>

四个坐标全是归一化到 [0,1] 的浮点数,不能写像素坐标。扑克牌数据集的标签里最典型的错误是直接写0 320.0 240.0 100.0 150.0这类像素值,模型不会报错,但训练时 xy 损失爆炸,置信度永远不收敛。3.1 的校验脚本能提前暴露这类问题,别等训练跑完才查。

2.2 52类别索引与data.yaml的绑定

52 个类别的命名常见做法是「点数+花色首字母」,例如 Ah 表示红桃 A、Ks 表示黑桃 K,组成固定顺序后与 class_id 一一对应:

class_id牌面class_id牌面
0Ah(红桃A)266h(红桃6)
1Ad(方块A)276d(方块6)
2Ac(梅花A)286c(梅花6)
3As(黑桃A)296s(黑桃6)
4Kh(红桃K)......
5Kd(方块K)512s(黑桃2)

类别顺序一旦定下来,整个项目生命周期都不能改。训练时 YOLOV5 从 data.yaml 的 names 列表读类别名,推理时模型输出的 class_id 再反向查表换算成牌面字符串,两端用的必须是同一份顺序。中途改动 names 排序而不动标签文件,模型不会报错,只会让全部预测结果整体错位,这种错误在牌桌场景里最难排查。

data.yaml 里最容易踩的坑是 nc 与 names 数量不一致,或者 train、val 路径写成了 labels 目录。YOLOV5 6.0 之后对路径检查比较宽松,path 写错时只打印 warning 并回退到默认训练集,训练照常跑,等发现时已经白耗几小时。

注意:训练前打印一次数据集统计信息,确认 images 和 labels 数量一致再启动,这个习惯能省掉最多的无效训练时间。

2.3 把VOC/COCO标注转成YOLOV5标签

大型扑克牌标注的原始来源一般是 VOC XML 或 COCO JSON,转换脚本的本质是坐标换算。VOC 的 bndbox 给左上和右下角点像素坐标,COCO 的 bbox 给左上角坐标加宽高,转成中心点格式的公式不同。下面这段脚本处理 VOC XML:

import cv2 from xml.etree import ElementTree as ET POKER_CLASSES = ['Ah','Ad','Ac','As','Kh','Kd','Kc','Ks','Qh','Qd','Qc','Qs', 'Jh','Jd','Jc','Js','Th','Td','Tc','Ts','9h','9d','9c','9s', '8h','8d','8c','8s','7h','7d','7c','7s','6h','6d','6c','6s', '5h','5d','5c','5s','4h','4d','4c','4s','3h','3d','3c','3s', '2h','2d','2c','2s'] def voc_xml_to_yolo(xml_path, img_w, img_h): """ 读VOC XML,把绝对像素坐标换算成YOLO归一化坐标。 返回 [(class_id, xc, yc, w, h), ...],每个元素都是浮点。 """ tree = ET.parse(xml_path) boxes = [] for obj in tree.getroot().iter('object'): name = obj.find('name').text if name not in POKER_CLASSES: continue b = obj.find('bndbox') xmin = float(b.find('xmin').text) ymin = float(b.find('ymin').text) xmax = float(b.find('xmax').text) ymax = float(b.find('ymax').text) w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h boxes.append((POKER_CLASSES.index(name), (xmin + xmax) / 2 / img_w, (ymin + ymax) / 2 / img_h, w, h)) return boxes # 使用前用 cv2.imread 读图拿真实宽高,不要信任XML里的尺寸

这段脚本里三个位置要格外注意。第一,img_w 和 img_h 必须用 cv2.imread 读出的真实尺寸,因为不少数据集缩放过但没有同步更新 XML,用 XML 里记录的分辨率会导致所有框的位置偏移。第二,POKER_CLASSES 的索引顺序必须与 data.yaml 的 names 一致,脚本里用 index(name) 天然保证这一点。第三,name not in POKER_CLASSES的样本被直接跳过,这是有意为之,源数据里常见的 JOKER 和背面标记不进入 52 类,但要单独记录数量,避免类别凭空消失。

COCO JSON 的换算不同,它的 bbox 是 [x, y, width, height] 左上角坐标:

x, y, w, h = ann['bbox'] xc = (x + w / 2) / img_w yc = (y + h / 2) / img_h # class_id 需要经过 coco_category_id -> 牌面名 -> POKER_CLASSES.index 两级映射

转换之后不要急着删原始标注,先随机抽 20 张图,把 YOLO txt 画回图片上人工核对一遍框是否贴边。扑克牌的角标很小,标注时容易把整张牌框进去,这种框不是错,但会让模型学习到「带牌边的完整牌面」特征,部署时遇到裁切过的牌面会漏检。

3. 训练前必做的数据集校验与划分

3.1 图片标签对齐与越界框检查

大型数据集翻车通常不是标注质量,而是文件缺失和坐标越界。一个五万张的扑克牌数据集,缺几百个 txt 不会让训练崩溃,只会让 mAP 莫名下降。训练前跑一遍完整性校验:

python - <<'EOF' import os from pathlib import Path base = Path('poker_dataset') issues = [] for split in ('train', 'val'): img_dir = base / 'images' / split lbl_dir = base / 'labels' / split for img in sorted(img_dir.iterdir()): if img.suffix.lower() not in ('.jpg', '.jpeg', '.png'): continue lbl = lbl_dir / (img.stem + '.txt') if not lbl.exists(): issues.append(f'{img}: label missing') elif lbl.stat().st_size == 0: issues.append(f'{img}: empty label') print(f'issues: {len(issues)}') for msg in issues[:50]: print(msg) EOF

校验通过后还要查越界框。归一化坐标理论上都在 [0,1] 内,标注工具偶尔会生成 x_center + w/2 超过 1 的框。YOLOV5 训练时会把越界部分直接裁剪掉,导致实际参与学习的区域跟标注意图不一致。写几行统计把所有框的最小最大坐标打出来,越界比例超过 0.5% 就得回源数据修,不建议在训练时用裁剪逻辑掩盖。

3.2 类别不平衡与牌面目标尺寸分析

52 类扑克牌天然存在不平衡。按真实牌局采集的数据里,亮出来的多是出过的牌,A、K 这类大牌占比偏高;合成数据集则取决于生成程序是否均匀抽样。统计脚本如下:

import os from collections import Counter def analyze_labels(label_dir): cls = Counter() size_ratio = [] total = 0 for f in os.listdir(label_dir): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f)) as fh: for line in fh: parts = line.split() if len(parts) != 5: continue cls[int(parts[0])] += 1 w = float(parts[3]) h = float(parts[4]) if h > 0: size_ratio.append(w / h) total += 1 return cls, size_ratio, total cls_cnt, ratios, total = analyze_labels('poker_dataset/labels/train') print('total boxes:', total) print('class num:', len(cls_cnt)) print('most common:', cls_cnt.most_common(3)) print('least common:', cls_cnt.most_common()[-3:])

两个指标决定后续策略。第一,最多类与最少类的数量比超过 10:1 时,训练会明显偏向高频类,最少类的 AP 长期为零;第二,框宽高比如果普遍大于 2,说明数据集里横向特写偏多,模型对竖屏远距离牌的召回会差。前者用第 5 章的难类过采样解决,后者需要在验证集里补充竖构图样本,或者在数据增强时调高旋转概率。

3.3 按牌局组划分训练验证集

扑克牌检测的划分策略与通用检测不一样。常见做法是不按图片随机划分,而是按拍摄场景分组:同一张桌子、同一台相机连续拍的帧必须进同一个集合。随机划分会让验证集和训练集包含同一牌局的相邻帧,牌面位置和光线几乎相同,val mAP 虚高 3 到 5 个点,部署到新牌桌立刻现原形。

import random random.seed(42) videos = [f'video_{i:03d}' for i in range(100)] random.shuffle(videos) cut = int(len(videos) * 0.85) train_groups, val_groups = set(videos[:cut]), set(videos[cut:]) # 按文件名的 group 前缀把图片和标签移入对应目录 # 移动前先复制一份到 staging 目录,不要原地挪,便于反复调整划分

划分比例的参考值:

数据规模训练集比例验证集比例说明
1万张以下0.900.10样本少,验证集只做趋势判断
1万至5万0.850.15扑克牌数据集常见区间
5万以上0.800.20验证集足够大,单类 AP 更可信

划分完之后检查一个容易漏的点:同一个牌局里 52 类牌不会全部出现,如果验证集恰好缺少某几个类,YOLOV5 在计算该类 AP 时会直接跳过,最终的 mAP 统计偏乐观。用 3.2 的统计脚本分别跑 train 和 val,确认两边都覆盖 52 类;如果 val 缺类,把该类所属的视频组整体划回训练集,再补录一段该类的数据做验证。这种按组划分的做法,后续新增数据时只需要维护一个 group 到 split 的映射文件,比每次重新随机划分的可重复性高得多。

4. 用YOLOV5训练52类扑克牌检测的参数清单

4.1 data.yaml的写法与类别顺序冻结

52 类扑克牌数据集的配置文件长这样:

path: ../poker_dataset train: images/train val: images/val nc: 52 names: 0: Ah 1: Ad 2: Ac 3: As 4: Kh 5: Kd 6: Kc 7: Ks # 其余类别按第2章的顺序写满到 51: 2s

train 和 val 指向 images 子目录,YOLOV5 会自动推导 labels 的对应路径,不需要再写 labels 字段。names 顺序一旦训练开始就不允许改,建议把这份 yaml 放进 git,并在 CI 里加一条断言,检查 len(names) 等于 nc 等于 52,且每个名字在 POKER_CLASSES 里能找到。这个断言看起来很笨,但实际项目里改错 names 顺序的概率比我预想的高得多。

类别顺序冻结还有一个连带影响:换模型规模时不要重建 yaml。从 yolov5s 切到 yolov5l,data.yaml 原样复用,只换 weights 参数,否则训练出来的权重和推理端类别映射对不上,线上会输出错牌面。

4.2 训练命令与扑克牌场景的超参数

52 类扑克牌检测不需要从零训练,常见做法是加载 COCO 预训练权重后冻结浅层微调。扑克牌数据集的规模和场景多样性通常远小于 COCO,从零训练收敛慢三倍以上,而且容易过拟合到数据集的背景。推荐命令:

cd yolov5 python train.py \ --weights yolov5s.pt \ --data ../poker_dataset/data.yaml \ --epochs 150 \ --batch-size 32 \ --img 640 \ --freeze 10 \ --patience 30 \ --save-period 10

参数说明:用 --freeze 10 冻结前 10 层,保留 COCO 预训练的通用纹理特征,只训练高层的牌面图案匹配部分,能明显抑制过拟合;--patience 30 是 EarlyStopping 耐心值,mAP 连续 30 轮不提升自动停;--img 640 是输入尺寸,扑克牌属于中小目标,下到 512 会明显损失角标细节,上到 1280 收益有限且训练时间翻倍。显存不够时优先降 batch-size 到 16,并同步把学习率减半,不要直接改 imgsz。

超参数建议表:

参数默认值扑克牌数据集建议原因
imgsz640640牌面角标是小细节,512 以下明显掉点
batch816~3252 类收敛需要足够多样本参与每轮更新
lr00.010.005微调场景下调一半,避免扰动预训练权重
optimizerSGDAdamW牌面数据噪声低,AdamW 在验证集上更稳
hsv_h0.0150.0色相改变会混淆红桃与方块,必须关掉
degrees0.010模拟牌在桌面上的任意旋转角
fliplr0.50.0牌面左右翻转会改变类别语义,不能开
mosaic1.00.8全开让合成图与真实桌面布局偏差变大

这张表里最容易被忽略的是 hsv_h 和 fliplr。红桃和方块靠形状与颜色区分,随机色相偏移后两类特征混在一起;左右翻转则直接把 Qh 变成 Qd 的镜像,属于语义级错误增强。这两项不关,52 个类别里至少有两三类 AP 会无解地低,排查半天最后发现是增强策略的问题,浪费的时间比训练本身还长。

4.3 训练监控、混淆类别与提前终止

训练启动后不能只看 loss。YOLOV5 每轮输出 P、R、mAP@0.5、mAP@0.5:0.95 四项,对 52 类模型,重点盯 mAP@0.5 的单类最低值和类别混淆矩阵。总 mAP 容易被高频大目标拉高,个别难类的 AP 长期为零时总指标看不出来。

训练过程的三个检查点:

  1. 第 10 轮:看 train 和 val 的 P、R 是否同时上升。如果 train 的 loss 下降但 val 不动,先确认验证集是否按 3.3 做了按组划分,排除数据泄漏导致的虚高。
  2. 第 50 轮:把混淆矩阵导出看相邻类混淆。黑桃与梅花在低分辨率下最容易互相认错,方块与红桃在灰度场景容易混,这两组混淆是数据增强问题的信号。
  3. 第 100 轮:mAP 增速变缓是正常现象,patience 30 会在连续 30 轮无提升时停训。此时用 --save-period 10 保存的中间权重里,取 val mAP 最高的一版,不要用最后一个 epoch 的权重。

训练结束时把 results.png 里的 P、R、mAP 曲线和类别 AP 分布一起归档,连同 data.yaml 和 commit hash 打成一个版本标签。后续调参对比时,只看这些归档就能定位是数据变了还是参数变了,避免两个月后对着三个相似的 results 文件夹无从下手。

5. 难类过采样、牌背负样本与端到端验证

模型训练完,第一步不是看测试集指标,而是跑一次真实牌桌视频的批量推理,把置信度低于 0.3 的检测框和原图一起导出来。这批低置信样本里,占比最高的是角标被手指遮挡的牌、洗牌运动模糊帧、以及浅色背景上白色牌边缘反光的图。把这些样本单独归一个目录,作为下一版数据集扩充的起点。

难类过采样我常用带权采样而不是简单复制。对最高的 A、K 类随机丢弃 30% 的训练样本,对最少的 2、3 类除了保留原图外,每张再做两次随机透视变换加亮度扰动,两张增广图与原图一并进入训练。这样 52 类的样本量被人工拉平,比依赖 YOLOV5 内置增强更可控。注意过采样只作用于训练集,验证集保持原始分布,否则 mAP 统计会失真。

牌背负样本是扑克牌场景特有的一环。很多数据集的标注只覆盖牌面正面,但真实摄像头拍到的牌经常背面朝上。这些未标注的背面区域会被当作背景参与训练,模型学到的是把背面误判成某种正面牌。处理方法是往训练集混入 10% 只含牌背或空桌面的图,这类图不写任何标签。目标检测的损失函数里背景项天然存在,混入负样本图等价于给分类分支强化了一个隐式的「无目标」输出,推理时背面被正确过滤掉。

端到端验证建议用一段完整的发牌动作视频而不是随机抽帧。逐帧跑推理,把每帧的检测框数量、置信度、类别序列输出成 CSV,再和真实记录逐帧对比,统计漏检集中在哪个动作阶段。这个环节能顺带暴露 NMS 的问题:扑克牌相邻排布时,两张牌的 IoU 经常大于 NMS 默认的 0.45,导致相邻牌被合并成一个框。遇到这种情况把 NMS 阈值降到 0.3,同时在验证脚本里加一条断言,断言每帧最多输出 54 个框且不存在 class_id 相同又相邻重叠的框。等漏检率降到 1% 以下,再考虑转 TensorRT 或 ONNX 做边缘部署。部署侧的类别映射表直接复用训练时的 data.yaml,52 类检测的推理开销本身不是瓶颈,预处理分辨率和多路并发吞吐才是真正决定线上帧率的地方。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询