资讯中心

52类别扑克牌YOLOV5数据集制作全指南:从目录格式到避坑实战

📅 2026/9/28 8:43:24
52类别扑克牌YOLOV5数据集制作全指南:从目录格式到避坑实战
简介面向目标检测与计算机视觉学习者这份大型扑克牌图像数据集按YOLOV5标准目录格式组织可直接用于模型训练与验证。数据覆盖扑克牌全部52种牌型图像为720×720 RGB图片训练集16000张、验证集4000张并配有对应txt标注文件无需额外格式转换即可接入YOLO系列训练流程。资源包共2000个文件包含1999个txt标注及类别字典另有1个可直接运行的show.py脚本支持随机读取图片绘制边界框并保存到当前目录便于快速检查标注质量。包体约947.74MB7z压缩目录结构清晰适合目标检测入门练习、模型评估、教学演示以及数据增广研究等场景。目前已有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, ] # 花色前缀SSpade黑桃, HHeart红桃, CClub梅花, DDiamond方块 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), encodingutf-8) # 对某个目录下所有XML执行转换 xml_dir path/to/xml_annotations out_dir path/to/labels/train os.makedirs(out_dir, exist_okTrue) for xml_file in Path(xml_dir).glob(*.xml): xml_to_yolo(str(xml_file), out_dir)脚本里保留6位小数是因为YOLOV5在增强阶段会做随机缩放、平移归一化精度太低会让标注框在放大后偏移好几个像素。img_w必须取XMLsize节点里的原始尺寸不能用加载图片后的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也不高mAP0.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(encodingutf-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), encodingutf-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.5hsv_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(parentsTrue, exist_okTrue) # 按文件名前缀分组这里以 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(encodingutf-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曲线震荡不定或完全没有下降趋势先检查数据增强参数是不是和数据集特征冲突。第三验证集mAP0.5至少应该在训练结束时达到0.2以上如果还是0几乎可以断定数据划分或标签索引有严重错误而不是模型训练时间不够。用yolov5s而不是更大的模型做验证因为推理快、显存占用低小模型能出结果说明数据没问题如果小模型完全学不动换大模型只是放大计算成本不会解决数据本身的错误。--img 640是速度和精度的中间值如果数据集里的牌面普遍很小可以直接用--img 1280先看标注框是否有效这一步显存不够就调小batch。5. 扑克牌数据集避坑训练不出效果先查这五个地方5.1 训练时提示 No labels foundmAP恒为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一直为0recall也低。 原因扑克牌数据集里点数牌的数字区域和花色符号在远距离拍摄时只有几个像素这些小于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_thres0.25对这个数据集偏低。牌面之间的视觉差异比COCO类别差异小得多在相同IoU下分类分支给出的分数可能只差0.1。实际部署时我习惯把conf_thres调到0.35到0.45用少量误检换分类置信度。如果模型仍然混淆红桃和方块我会在检测后加一个“多帧投票”后处理对视频流里同一张牌在连续5帧的检测结果取值最大的类别单帧瞬时抖动在这种投票机制下基本能被过滤掉。另一个更省力的方向是迁移。52类扑克牌模型训练完后backbone对“小面积、高纹理、强对称”目标的特征提取能力已经很强了。如果后续要识别UNO牌、麻将牌甚至简化版的扑克牌数字识别不需要重新训练整个网络。把最后一层检测头的类别数改成新任务的目标数冻结backbone和前两个检测层只训练新类别输出层200张新牌面数据的标注量就能在现有权重基础上稳定收敛。朴克牌检测做到最后会发现数据质量决定一切的说法一点不夸张。跑通YOLOV5只是第一步把数据集检查、索引映射、增强策略这些基本功做扎实模型效果才能真正贴近实际场景。整个方案干下来我自己最深的感受是先用小模型快速验证数据比盲目堆训练时间有用得多。希望这些步骤和踩坑记录能帮你在这个方向少走几轮弯路。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案