简介:面向目标检测与计算机视觉学习者,这份大型扑克牌图像数据集按YOLOV5标准目录格式组织,可直接用于模型训练与验证。数据覆盖扑克牌全部52种牌型,图像为720×720 RGB图片,训练集16000张、验证集4000张,并配有对应txt标注文件,无需额外格式转换即可接入YOLO系列训练流程。资源包共2000个文件,包含1999个txt标注及类别字典,另有1个可直接运行的show.py脚本,支持随机读取图片绘制边界框并保存到当前目录,便于快速检查标注质量。包体约947.74MB(7z压缩),目录结构清晰,适合目标检测入门练习、模型评估、教学演示以及数据增广研究等场景。目前已有133人学习,数据量充足、类别覆盖完整,对需要扑克牌检测数据的开发者而言是一份开箱即用的高质量数据集。
1. 扑克牌检测为什么值得做成一个52类别YOLOV5格式数据集
如果你做过摄像头识别扑克牌的娱乐应用、桌面游戏辅助或棋牌机器人,大概率卡在同一个问题上:检测模型不难跑通,难的是数据。目标检测数据集(YOLOV5目录格式):大型扑克牌图像检测数据集(52类别),指的是把一整副扑克牌的52张牌面(黑桃A到黑桃K、红桃、梅花、方块共52类)按YOLOV5的目录约定整理成可直接训练的数据集。它区别于常见的二分类“有牌/无牌”检测,也区别于只区分12张数字牌的简化版,真正把每一种花色的每一张点数当成独立类别去做。
这套数据集的难点在于:52个类别数量偏多,牌面之间区分度低,红桃和方块在低分辨率下几乎只差一个形状,加上拍照时牌面倾斜、反光、手指遮挡,检测器的分类分支很容易翻车。而“YOLOV5目录格式”又是一个硬约定:images和labels怎么放、data.yaml怎么写、类别索引从几开始,任何一环错了,训练脚本要么报找不到标签,要么mAP永远上不去。这篇文章按我实际处理类似数据集的经验,从目录拆解、转换脚本、数据体检到训练验证,把能抄作业的细节全部写出来。
2. YOLOV5目录格式逐层拆解:images、labels、data.yaml 三者怎么对上
2.1 目录树与文件命名对齐规则
YOLOV5对数据集的读取并不依赖一个全局索引文件,而是靠目录结构和文件名同名匹配。一个标准的扑克牌数据集目录长这样:
poker_dataset/ ├── data.yaml ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ ├── 000002.jpg │ │ └── ... │ └── val/ │ ├── 001001.jpg │ └── ... └── labels/ ├── train/ │ ├── 000001.txt │ ├── 000002.txt │ └── ... └── val/ ├── 001001.txt └── ...匹配规则只有一条:images/train/000001.jpg对应labels/train/000001.txt,目录层级完全一致,文件名完全一致(不含扩展名)。图片可以是.jpg/.jpeg/.png,标签统一是.txt。如果你的数据集里某张图有对应标注,但文件名多了空格或后缀大小写不一致,YOLOV5在训练时会直接跳过这张图并输出WARNING: No labels found。
每个.txt文件的内容是若干行目标记录,每行格式为:
class_id x_center y_center width height以黑桃A为例,类别索引为0,标注框中心在图像坐标(0.5, 0.5),宽高各占整图的0.8和0.9,那么这一行就是:
0 0.500000 0.500000 0.800000 0.900000坐标全部归一化到0到1之间,用原图宽高除以像素坐标得到。这里有一个新手常犯的错:把像素坐标直接写进txt。YOLOV5的loss计算会按归一化坐标乘回图的尺寸,如果标注里出现大于1的数值,训练时边框回归会直接异常,表现为loss爆炸或mAP完全不动。
2.2 从标注工具导出到YOLO txt:一个可复制的转换脚本
现在主流标注工具像labelimg、x-anylabeling、Roboflow都支持直接导出YOLO格式,但很多存量扑克牌数据集是用VOC XML或COCO JSON保存的。我处理这类转换的经验是写一个一次性脚本,把XML解析成YOLO txt,类别名统一从class_names列表取索引,而不是让工具帮你生成索引。
import os import xml.etree.ElementTree as ET from pathlib import Path class_names = [ "SA", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "S10", "SJ", "SQ", "SK", "HA", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "H10", "HJ", "HQ", "HK", "CA", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10", "CJ", "CQ", "CK", "DA", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "DJ", "DQ", "DK", ] # 花色前缀:S=Spade黑桃, H=Heart红桃, C=Club梅花, D=Diamond方块 def xml_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text.strip() if cls_name not in class_names: continue cls_id = class_names.index(cls_name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 归一化坐标 x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 过滤无效框:宽度或高度为0,或越界 if w <= 0 or h <= 0 or w > 1 or h > 1: continue lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if lines: out_path = Path(out_dir) / (Path(xml_path).stem + ".txt") out_path.write_text("\n".join(lines), encoding="utf-8") # 对某个目录下所有XML执行转换 xml_dir = "path/to/xml_annotations" out_dir = "path/to/labels/train" os.makedirs(out_dir, exist_ok=True) for xml_file in Path(xml_dir).glob("*.xml"): xml_to_yolo(str(xml_file), out_dir)脚本里保留6位小数是因为YOLOV5在增强阶段会做随机缩放、平移,归一化精度太低会让标注框在放大后偏移好几个像素。img_w必须取XML<size>节点里的原始尺寸,不能用加载图片后的shape,因为XML里可能记录的是缩放前的大图尺寸。
2.3 data.yaml 的类别顺序:52类索引是从0开始算的
data.yaml是YOLOV5训练入口读取的配置文件,里面指定了训练集、验证集路径、类别数和类别名。扑克牌数据集至少要这样写:
path: /absolute/path/to/poker_dataset train: images/train val: images/val nc: 52 names: [ "SA", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "S10", "SJ", "SQ", "SK", "HA", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "H10", "HJ", "HQ", "HK", "CA", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10", "CJ", "CQ", "CK", "DA", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "DJ", "DQ", "DK" ]nc和names必须长度一致,names的顺序决定了txt文件里数字索引对应的真实类别。比如类别索引37,就要去列表里数第38个元素,得到的是方块2。一旦训练中切换了不同顺序的names文件,之前标注的索引就全部错位,这就是“类别平行错位”问题,后面避坑章节会具体展开。
路径写法上我推荐用path字段指定数据集根目录绝对路径,train和val写相对路径。这样项目拷到别的机器,只需改path一行。如果直接在train字段写绝对路径,也能跑,但换机器容易忘改。这套目录约定后来被YOLOV8、YOLO11延续,所以把数据整理成YOLOV5格式,后续换新版模型也不需要重新做数据集处理。
3. 52类别扑克牌数据集的三个难点:类别命名、小目标、标注策略
3.1 类别命名与索引映射表:先把52个类固定下来
扑克牌数据集的第一个坑来自命名。直接叫A,2,K这种名字会立刻撞车,因为黑桃A和红桃A都叫A。我见过有人用S-A、H-A这样带花色前缀的方式,也有人用spade_A、heart_A的长命名,都可以,关键是定下来就不能改。
建议一开始写好一个映射脚本,把标注文件里的旧类名统一映射到新索引,而不是靠手工改txt。比如旧标注里用了Ace_of_Spades,新names里叫SA,转换逻辑这样写:
old_to_new = { "Ace_of_Spades": "SA", "Two_of_Spades": "S2", # ... 全部52个映射关系 }映射表的意义在于:当你想做子集实验(比如只保留黑桃类、或只保留数字牌到红心类),你可以从映射表直接过滤生成新的labels目录,不用重新标注。另外类别名的字符集要避开中文和特殊符号,因为names会写进训练日志、TensorBoard分类标签,中文在部分可视化工具里会显示成乱码,排查时很费劲。
52个类别意味着网络输出层维度显著增大。YOLOV5的检测头每个anchor输出nc + 5个值(类别数加框坐标和置信度),三个尺度乘三个anchor,类别数从常见的20类变成52类后,分类分支的参数量和FLOPs都会涨。这不影响训练,但显存占用会比COCO 80类略高,训练时batch size要适当缩一档。
3.2 小目标与遮挡:旋转矩形标注 vs 轴对齐框
扑克牌检测有别于一般物体的关键点在于:牌面经常是旋转的。YOLOV5默认的标注格式是轴对齐矩形,也就是x_center y_center width height只能描述一个不旋转的水平框。当牌旋转45度时,轴对齐框会把牌面四周的空白桌面一起框进来,标注框面积可能比牌面真实面积大40%以上。
这个现象带来的直接后果是:IoU计算时,预测框和真实框即使中心点完全重合,IoU也不高,mAP@0.5:0.95这类指标会被明显压低。但训练本身不会失败,因为网络在回归框坐标时会学到一个“把牌完整包住”的折中框。如果后续只做牌面识别不做点数OCR,这个精度损失可以接受。
如果对贴合度有硬要求,或者之后要做牌面点数识别,再去考虑旋转框。常见替代方案是换成mmrotate这类旋转检测训练框架,标注格式要从四角点坐标描述旋转框,数据增强逻辑和YOLO系完全不同。对于标题里这个数据集,我的判断是:先用轴对齐框跑通,如果实际测试中发现旋转牌误检框过大,再针对旋转样本做专门增强或升级框架,不要一上来就换模型结构。
小目标是另一个更实际的问题。当整张图像分辨率是1920×1080,牌面只有80×120像素时,归一化后目标宽高约0.04和0.11,属于小目标。YOLOV5在640×640输入下,下采样到输出特征层分别是80×80、40×40、20×20,小目标主要靠80×80那一层检测,但经过Mosaic等增强后,小目标容易被裁出图。建议在数据预处理阶段过滤掉过小的标注框,实际操作里我会用一组阈值:原图分辨率下小于12×12像素的框直接剔除或提升分辨率。
import numpy as np from pathlib import Path MIN_W = 12 # 原图坐标系下的最小框宽 MIN_H = 12 # 原图坐标系下的最小框高 label_dir = Path("path/to/labels/train") image_size = (1920, 1080) # 当前数据集统一分辨率 for txt_path in label_dir.glob("*.txt"): lines = txt_path.read_text(encoding="utf-8").strip().splitlines() valid_lines = [] for line in lines: parts = line.strip().split() cls_id = int(parts[0]) x_center, y_center, w_norm, h_norm = map(float, parts[1:]) w_pixel = w_norm * image_size[0] h_pixel = h_norm * image_size[1] if w_pixel >= MIN_W and h_pixel >= MIN_H: valid_lines.append(line) txt_path.write_text("\n".join(valid_lines), encoding="utf-8")过滤脚本跑完后,要重新统计一下类别分布,确认不是某一类牌的样本全被过滤掉导致类别缺失。这个场景在扑克牌里很典型:J、Q、K这种带人像的牌面面积大,点数牌的面积小,如果拍摄距离远,点数区域可能只有5×5像素。
3.3 增强策略要为“识别花色”服务:翻转和旋转的坑
YOLOV5自带的超参数文件里有两个关于翻转的设置:fliplr和flipud。对自然场景目标来说,左右翻转通常无害,但扑克牌有一个特殊性:牌面上印有英文字母,J、Q、K这些花牌有不对称图案,左右翻转等于把一张“黑桃J正常方向”的图像变成一张“黑桃J镜像方向”的样本。模型会把两种完全不同的视觉特征当成同一个类别去学,如果翻转样本占比过大,分类置信度会被拉低。
我的做法是:如果你的数据集标注时统一按牌面正立方向标注,把fliplr从默认的0.5调到0.2或0。如果标注时本身就包含了各个旋转方向的牌,翻转开不开都行,因为旋转增强已经在制造方向变化。flipud我建议直接设为0,上下翻转后的牌面在真实应用里几乎不会出现(牌面上下颠倒属于特殊情况,一般靠旋转增强来覆盖)。
颜色增强对扑克牌识别很关键。红桃和方块在灰度或低饱和度下区分度下降,但训练时hsv_h、hsv_s这类抖动参数不能太低也不能太高,太高会让红色和橙色混在一起,把红桃学成方块。我用扑克牌数据集时一般把hsv_s从默认的0.7降到0.5,hsv_v保持0.4,这样能模拟不同光线环境,又不会破坏花色边界。
4. 拿到数据集先别训练:标签体检与快速验证训练
4.1 数据划分:按文件哈希或采集序列分组,杜绝验证集泄漏
扑克牌数据集如果来自视频抽帧或连续拍摄,相邻帧间的画面几乎相同,同一张牌会出现在多帧里。如果划分train/val时随机打乱,验证集里就会出现大量和训练集高度重叠的帧,mAP虚高,换一批真实场景数据直接崩溃。
稳妥做法是按“采样组”划分。假如文件名里包含拍摄批次信息,比如video_01_0051.jpg中的video_01,那整个video_01批次只能全进训练集或全进验证集:
from pathlib import Path import random import shutil image_dir = Path("path/to/all_images") label_dir = Path("path/to/all_labels") train_img_dir = Path("path/to/images/train") val_img_dir = Path("path/to/images/val") train_lbl_dir = Path("path/to/labels/train") val_lbl_dir = Path("path/to/labels/val") # 在目标目录不存在时创建 for d in [train_img_dir, val_img_dir, train_lbl_dir, val_lbl_dir]: d.mkdir(parents=True, exist_ok=True) # 按文件名前缀分组(这里以 video_01 为例) groups = {} for img_path in image_dir.glob("*.jpg"): prefix = img_path.stem.rsplit("_", 1)[0] # video_01_0051 -> video_01 groups.setdefault(prefix, []).append(img_path) group_names = list(groups.keys()) random.seed(42) random.shuffle(group_names) val_count = max(1, int(len(group_names) * 0.2)) val_groups = set(group_names[:val_count]) for prefix, img_paths in groups.items(): target_img_dir = val_img_dir if prefix in val_groups else train_img_dir target_lbl_dir = val_lbl_dir if prefix in val_groups else train_lbl_dir for img_path in img_paths: txt_path = label_dir / (img_path.stem + ".txt") if not txt_path.exists(): continue shutil.copy(img_path, target_img_dir / img_path.name) shutil.copy(txt_path, target_lbl_dir / txt_path.name)如果文件名没有批次信息,退而求其次是用文件名的hash值取模分组,保证同一图像的标签和图像永远进同一个set。Hash分组有一个额外的好处:重新跑实验时只要seed固定,划分结果可复现。
4.2 六维标签体检:画像对应、坐标越界、空标签
我拿到任何YOLO格式数据集的第一件事不是开训,而是跑一个体检脚本。扑克牌数据集结构简单,但就是容易在细节上翻车。下面这个脚本检查六个维度,全部通过后再进训练环节。
from pathlib import Path from collections import Counter img_dir = Path("path/to/images/train") lbl_dir = Path("path/to/labels/train") class_counter = Counter() invalid_box_count = 0 empty_label_count = 0 missing_label_count = 0 for img_path in img_dir.glob("*.jpg"): txt_path = lbl_dir / (img_path.stem + ".txt") if not txt_path.exists(): missing_label_count += 1 continue lines = txt_path.read_text(encoding="utf-8").strip().splitlines() if not lines: empty_label_count += 1 continue for line in lines: parts = line.strip().split() if len(parts) != 5: print(f"非法行: {txt_path}: {line}") invalid_box_count += 1 continue cls_id = int(parts[0]) x_center, y_center, w, h = map(float, parts[1:]) if w <= 0 or h <= 0 or x_center < 0 or x_center > 1 or y_center < 0 or y_center > 1: invalid_box_count += 1 print(f"越界框: {txt_path}: {line}") continue class_counter[cls_id] += 1 print(f"图片数: {len(list(img_dir.glob('*.jpg')))}") print(f"缺失标签: {missing_label_count}") print(f"空标签: {empty_label_count}") print(f"非法行/越界框: {invalid_box_count}") print(f"类别数: {len(class_counter)}, 应为52") print(f"每类最少样本: {min(class_counter.values())}, 最多: {max(class_counter.values())}")重点看两个输出:缺失标签如果是几十上百的数量级,说明数据整理时漏拷了labels子目录;类别数如果不足52,说明某个花色的牌一张没标或全部被过滤。每类样本数min/max差距过大比如超过5倍,就要考虑类别不平衡,后面训练时分类分支会偏向样本量大的类。
4.3 快速验证:像训练自己的数据集一样跑10个epoch
体检通过后,先不要直接上完整训练。我用一个快速验证流程,10到20个epoch就能暴露八成数据问题,节省大量时间:
python train.py \ --data /path/to/poker_dataset/data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 15 \ --device 0关键在于观察三件事。第一,训练日志里有没有WARNING: No labels found的提示,有就要回到第2章的目录对齐规则排查。第二,每个epoch结束时输出的cls_loss和box_loss是否在稳步下降,如果loss曲线震荡不定或完全没有下降趋势,先检查数据增强参数是不是和数据集特征冲突。第三,验证集mAP@0.5至少应该在训练结束时达到0.2以上,如果还是0,几乎可以断定数据划分或标签索引有严重错误,而不是模型训练时间不够。
用yolov5s而不是更大的模型做验证,因为推理快、显存占用低,小模型能出结果说明数据没问题,如果小模型完全学不动,换大模型只是放大计算成本,不会解决数据本身的错误。--img 640是速度和精度的中间值,如果数据集里的牌面普遍很小,可以直接用--img 1280先看标注框是否有效,这一步显存不够就调小batch。
5. 扑克牌数据集避坑:训练不出效果先查这五个地方
5.1 训练时提示 No labels found,mAP恒为0
现象:日志里出现大量WARNING: No labels found in /path/to/images/val,模型一路训练到结束,验证集mAP都是0。 原因:images目录和labels目录虽然都有train/val子目录,但data.yaml里的train字段指向images/train,而训练脚本期望在对应路径下找到labels/train。如果数据集把labels直接放在labels/根目录,或者在data.yaml里写了绝对路径却漏了一层目录,就会出现这个问题。 解决:先确认labels目录和images目录在同一个父目录下,且子目录名完全一致;再确认data.yaml里train: images/train,不要写train: ./images/train这类相对路径歧义写法。改完重新启动训练。
5.2 类别索引错位:把红桃3识别成方块3
现象:训练loss正常下降,mAP也挺高,但实际推理时某个类别的牌总是被预测成相邻类别的牌,尤其集中在同点数的不同花色之间。 原因:标注txt文件里的类别索引当初是按一套旧names顺序写的,后来调整了data.yaml里names的排列,但labels里的数字没有被重新映射。网络训练时强制学习“索引5=红桃3”,但推理时索引5变成方块3,所有预测整体错位。 解决:重新生成names和labels,禁止手工修改txt。建议写一套独立的映射脚本,从旧索引到新索引按旧类名转换,转换后随机抽查20个txt的前三行,确认数字语义正确再开训。
5.3 验证集mAP虚高,换实测环境掉一半
现象:训练结束验证mAP达到0.9,但把模型拿到另一个房间、另一张桌子上测试,mAP只剩0.4左右。 原因:数据划分时没有按采集组分离,同一个视频片段或连续拍摄的相近画面被随机分到了train和val,验证集与训练集高度相似。扑克牌数据集中“同一副牌、同一桌面纹理、同一光源”就是最大的隐藏变量,模型学到的可能是背景和拍摄条件,不只是牌面特征。 解决:按4.1的按组划分方法重新切分数据,划分后检查是否存在同一张牌的画面同时出现在两个集合中。如果数据集是单次拍摄的,宁可在划分时做严格的时间窗隔离,比如前80%帧做train,后20%帧做val。
5.4 标注了成百上千个小目标,但loss不降,precision始终为0
现象:训练集和验证集的标签体检都没问题,训练时检测出的目标却很少,precision一直为0,recall也低。 原因:扑克牌数据集里点数牌的数字区域和花色符号在远距离拍摄时只有几个像素,这些小于8×8像素的框一旦经过Mosaic缩放或随机裁剪,目标区域被裁出图像,网络根本没机会学到这些特征,还容易把白色背景误学成目标。 解决:用3.2的清洗脚本过滤过小框,或者直接把输入分辨率提到--img 1280,让原本十几个像素的小目标在特征层上占据更多网格。同时用YOLOV5仓库里的kmeans_anchors.py重新聚类anchor,默认anchor是按COCO目标尺寸设计的,对扑克牌这种小尺寸目标不够匹配。
5.5 模型把桌面纹理、手指、牌背误检成扑克牌
现象:验证mAP可以接受,但实时检测时画面里出现桌面木纹、手指夹牌、牌背朝上等情况,模型都会输出大量置信度较高的预测框。 原因:训练集正样本过于单一,所有图片都是牌面朝上的完整牌面,模型没有见过“非牌面”的负样本。它学到的是“白色矩形区域出现就可能是牌”,桌面纹理和牌背也被当成类似特征。 解决:收集一批不包含任何牌面朝上的背景图,单独放在images/train目录下,并给每张图配一个内容为空的.txt文件。YOLO训练时遇到空标签文件会跳过目标loss,只算背景loss,等同于告诉模型“这个画面里没有牌”。负样本数量控制在训练集的10%到20%比较合适,过多会把模型带偏成保守预测,凡是小目标都判为背景。
6. 进阶用法:调整置信度后处理和用52类模型做迁移
训练结束后真正要投入使用的阶段,才是扑克牌检测和通用目标检测拉开差距的地方。常用的detect.py默认conf_thres=0.25,对这个数据集偏低。牌面之间的视觉差异比COCO类别差异小得多,在相同IoU下,分类分支给出的分数可能只差0.1。实际部署时我习惯把conf_thres调到0.35到0.45,用少量误检换分类置信度。如果模型仍然混淆红桃和方块,我会在检测后加一个“多帧投票”后处理:对视频流里同一张牌在连续5帧的检测结果取值最大的类别,单帧瞬时抖动在这种投票机制下基本能被过滤掉。
另一个更省力的方向是迁移。52类扑克牌模型训练完后,backbone对“小面积、高纹理、强对称”目标的特征提取能力已经很强了。如果后续要识别UNO牌、麻将牌甚至简化版的扑克牌数字识别,不需要重新训练整个网络。把最后一层检测头的类别数改成新任务的目标数,冻结backbone和前两个检测层,只训练新类别输出层,200张新牌面数据的标注量就能在现有权重基础上稳定收敛。
朴克牌检测做到最后会发现,数据质量决定一切的说法一点不夸张。跑通YOLOV5只是第一步,把数据集检查、索引映射、增强策略这些基本功做扎实,模型效果才能真正贴近实际场景。整个方案干下来,我自己最深的感受是:先用小模型快速验证数据,比盲目堆训练时间有用得多。希望这些步骤和踩坑记录,能帮你在这个方向少走几轮弯路。
本文还有配套的精品资源,点击获取