简介:用于扑克牌目标检测与识别任务的数据集资源,面向计算机视觉学习者、算法工程师及目标检测方向的研究人员,解决扑克牌类别定位与分类训练数据不足的问题。图片均使用labelimg手工标注,涵盖queen、ten、nine、king、jack、ace六种常见牌面,输出标准xml标签文件,适配YOLO、SSD、Faster R-CNN等检测框架,便于直接替换或补充自有数据集。整个资源包共726个文件,包括363张jpg原图与363个对应的xml标注文件,压缩后约36.62MB,体量轻便、目录结构清晰,适合开展小规模模型训练与算法验证。资源中的图片与标注一一对应,内容直观,便于进行训练集/验证集划分及数据预处理,能够帮助使用者快速建立检测流程。目前已有395人学习使用,可广泛用于模型训练、精度评测、课程设计及教学演示,显著减少数据采集和标注环节的重复投入,提升实验迭代效率。
1. 扑克牌目标识别数据集到手:6类363张图,为什么我拿它练手
扑克牌牌面检测在目标识别里看着是“最玩具”的任务,真跑一遍你会发现坑一点不少:王后和国王的牌面都顶着人头,9和10全是密集花色点,新手模型在这几对类别上频繁翻车。这份数据集恰好只保留六个类别——ace、nine、ten、jack、queen、king,共363张真实拍摄的扑克牌图片,数据标注由 labelimg 完成,交付的是 VOC XML 标签。如果你在练手目标检测流程,或者要给棋牌桌游类产品做快速验证,拿它比自己拍牌面再逐张标注省事得多,而且类间相似度高这件事本身,正好把检测器的软肋全部暴露出来。
2. 数据集结构解析:labelimg 打出的 XML 里到底存了什么
2.1 先别急着训练,把图片和标签的体检做掉
从交付的文件清单能看出,图片统一叫 cam_image26.jpg、cam_image14.jpg 这种格式,总数是 363 张,标签是同名 XML。命名里的 cam 大概率说明是相机连续抓帧后保留的有效帧,编号跳号是正常现象——采集过程中有模糊、遮挡的帧在人工筛帧时被剔掉了,不用纠结编号为什么不连续。
我拿到任何目标识别数据集的第一件事不是开训,而是先做三件体检:第一,统计所有图片的宽高分布,确认有没有尺寸悬殊的极端样本;第二,统计每个类别的标注框数量,看是否存在类别极度不均衡;第三,随机抽十几张图把标注框叠加回原图,肉眼看一遍框和牌面的贴合程度。这三步加起来不到十分钟,但能避免后面百分之九十的无效训练。以这个扑克牌数据集为例,363 张图六个类别,平均下来每类约六十个样本,属于典型的“小样本但类别语义清晰”的项目,后续数据增强和训练集划分要格外上心。
2.2 labelimg 的 XML 字段逐个拆:bndbox 与 size 的对应关系
labelimg 默认输出的是 Pascal VOC 格式 XML,这份数据集交付的就是这种格式。一个典型的标注文件结构如下,具体坐标数值因图而异,但字段组织方式是固定的:
<annotation> <folder>images</folder> <filename>cam_image26.jpg</filename> <source> <database>Unknown</database> </source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>queen</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>523</xmin> <ymin>301</ymin> <xmax>698</xmax> <ymax>512</ymax> </bndbox> </object> <object> <name>ten</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>724</xmin> <ymin>288</ymin> <xmax>907</xmax> <ymax>505</ymax> </bndbox> </object> </annotation>这里有几个关键点值得专门说明。size 节点下的 width、height、depth 是原图真实尺寸,depth 为 3 表示 RGB 三通道,bndbox 里存的是目标框左上角 (xmin, ymin) 和右下角 (xmax, ymax),单位是像素绝对值,不是归一化坐标。训练前转换格式时,必须以每张图自己 annotation 里的 size 为准做归一化,而不是全局套一个固定宽高——不同图片尺寸不一致时,套错尺寸会导致所有框集体偏移。
object 节点里还有一个容易忽略的字段是 difficult。如果这个值被标成 1,表示该目标在训练时通常会被忽略或降低权重。扑克牌检测里常见的情况是桌面边缘被截断的半张牌被标成 difficult,转换格式时如果不过滤,会让模型学到去检测残缺目标,推理阶段容易出现一堆假阳框。
2.3 六个类别之间的隐性难度差异
六个类别名字看起来平平无奇,但作为目标识别训练的素材,它们之间的难度差异非常明显。ace、nine、ten 这三类在牌面四角都有醒目的数字角标,中远距离下模型靠角标就能给出高置信度判断。jack、queen、king 则完全是另一回事,它们的主体都是人物肖像,在分辨率不足或牌面倾斜时,三个人头之间的纹理差异很小,模型极易把 king 看成 queen。
还有一个客观存在的干扰因素:扑克牌有黑桃、红心、梅花、方块四种花色,所以同一类别在不同图片里呈现的颜色和形状差异很大。比如 ace 既可能是黑桃 A 也可能是红桃 A,模型需要从少量样本里学到“花色不影响类别判断”这一层抽象。检测方法上,如果只跑水平框检测,一张斜放的牌面框内会混入大量背景,类别特征被稀释;想省事就直接用旋转框检测器,想简单就靠数据增强把角度变化撑起来,这个数据集正好能逼你把这一步想清楚。
3. 从 VOC XML 到 YOLO 训练格式:转换脚本与类别映射
3.1 为什么 labelimg 标好的 XML 不能直接拿去训练
很多新手拿到这个扑克牌数据集的第一反应是打开训练脚本,结果发现 YOLOv5、YOLOv8 的训练入口根本不认 XML。原因在于 YOLO 系列使用的是纯文本标签,每张图对应一个同名 txt 文件,里面每一行代表一个目标,格式固定为:类别id、归一化中心点x、归一化中心点y、归一化宽度w、归一化高度h。XML 里是绝对像素坐标,YOLO 要的是归一化相对坐标,差了一个完整的坐标变换环节。
有人会问,那 labelimg 不是能直接保存成 YOLO 格式吗,为什么这份数据集的标签偏偏是 XML?这其实是数据标注交付时的常见状态:标注员用 labelimg 统一作业时,项目预设的导出格式就是 VOC XML,因为它在多工具之间通用性最好,后面要给哪个框架用再单独转。你收到的 XML 只是一个中间产物,转换这一步绕不开,与其手工改不如直接写脚本批量处理。
3.2 批量转换脚本:XML 转 YOLO txt 的完整实现
下面的脚本可以处理整个标注目录,把所有 XML 转换成 YOLO 训练直接可用的 txt 文件。类别顺序必须先定死,因为转换结果里写的 class id 就是按这个列表顺序排的,和后面训练配置里的 names 列表必须一一对应。
import xml.etree.ElementTree as ET import os # 类别顺序即训练时的 id,修改后要与 data.yaml 保持一致 CLASSES = ["ace", "nine", "ten", "jack", "queen", "king"] def convert_one_xml(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() # 以每张图自己的 size 为准做归一化 size_node = root.find("size") img_w = int(size_node.find("width").text) img_h = int(size_node.find("height").text) lines = [] for obj in root.iter("object"): # difficult 为 1 的目标默认跳过,避免把残缺目标教给模型 difficult = int(obj.find("difficult").text) if difficult == 1: continue name = obj.find("name").text.strip() if name not in CLASSES: print(f"跳过未知类别: {name} in {xml_path}") continue class_id = CLASSES.index(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) # 坐标边界保护,防止标注框越界造成 nan xmin = max(0.0, min(xmin, img_w)) ymin = max(0.0, min(ymin, img_h)) xmax = max(0.0, min(xmax, img_w)) ymax = max(0.0, min(ymax, img_h)) # 转换为 YOLO 的 cx, cy, w, h 相对坐标 cx = ((xmin + xmax) / 2.0) / img_w cy = ((ymin + ymax) / 2.0) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") if lines: xml_name = os.path.basename(xml_path) txt_name = os.path.splitext(xml_name)[0] + ".txt" txt_path = os.path.join(out_dir, txt_name) with open(txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) def convert_batch(xml_dir, out_dir): os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(xml_dir): if fname.lower().endswith(".xml"): convert_one_xml(os.path.join(xml_dir, fname), out_dir) print("转换完成,输出目录:", out_dir) if __name__ == "__main__": convert_batch("annotations", "labels")这段代码里最容易被忽略的是 difficult 过滤和坐标边界保护。坐标边界保护是把 xmin、ymax 等值强制钳制到图片宽高范围内,防止标注员手滑把框拖出画面边界,导致后续归一化出现负值或大于 1 的值,YOLO 训练时直接 nan。打印时保留六位小数是为了精度,训练时坐标误差会被整体放大到原图分辨率,精度不足会导致框整体偏移几个像素。
3.3 类别文件与数据配置:一次配对,处处配对
转换完成后,还需要生成类别清单和训练数据配置文件。训练配置里的 names 列表必须与转换脚本里的 CLASSES 列表顺序完全一致,这是最常见的翻车点——两边顺序一错,所有标注框的类别标签就整体错位,训练过程看起来正常,推理结果全乱。
printf "ace\nnine\nten\njack\nqueen\nking\n" > classes.txt# data.yaml,YOLOv5/v8 通用 path: /your/path/poker_dataset train: images/train val: images/val nc: 6 names: ["ace", "nine", "ten", "jack", "queen", "king"]这里有一个隐性要求:train 和 val 对应的目录里放的是图片,但 YOLO 读取标签的规则是自动把图片路径里的 images 替换成 labels,也就是它会去 images 的同级目录找 labels/train 和 labels/val。所以转换脚本输出的 txt 标签目录结构必须与图片目录结构镜像对齐,图片在 images/train 下,标签就要放在 labels/train 下。
3.4 转换结果可视化验证:画框回看最稳
脚本跑完不代表转换一定正确,我一般会在动手训练前做一次抽样可视化,把 txt 里的归一化坐标还原成像素框,叠加到原图上用 OpenCV 画出来,肉眼检查类别和位置是否对齐。
import cv2 def draw_yolo_box(image_path, label_path): img = cv2.imread(image_path) h, w = img.shape[:2] with open(label_path, "r") as f: for line in f.readlines(): cid, cx, cy, bw, bh = map(float, line.split()) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, CLASSES[int(cid)], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return img # 随机抽查三张,按 (图片路径, 标签路径) 传入 img = draw_yolo_box("images/train/cam_image26.jpg", "labels/train/cam_image26.txt") cv2.imshow("check", img) cv2.waitKey(0)画框脚本里的坐标还原用到了 w 和 h 保存的图片原始尺寸,注意这里读取的是 OpenCV 的宽高维度,不要和标注时的 size 字段搞混。抽查满足两个标准才算通过:框要严丝合缝贴合牌面边缘,类别名称要与牌面真实内容一致。只要有任意一张错位,就回到转换脚本检查归一化逻辑,不要硬着头皮开训。
4. 避坑指南:标注质量、类别混淆与训练集划分
4.1 类别张冠李戴:king 被标成 queen,训练出来的模型也跟着瞎认
现象:训练时 loss 下降很正常,mAP 也能到 0.9 以上,但把模型部署到实际场景,明明是 K 的牌面它总给出 queen 的高置信度结果,K 和 Q 之间反复横跳。
原因:我在可视化抽查时发现,有些牌面因为拍摄角度斜、牌面反光,人像区域纹理不清晰,标注员只能靠牌角文字判断类别,但标注框本身框的是整张牌,人像区域和角标区域外观差异过大,模型学到的是“这个人像是 Q”的错误关联。加上 king 和 queen 的牌面构图本来就相似,标注错误会被训练过程放大。
解决:转换格式前先做一轮类别校准。具体做法是用脚本把每个类别的标注图各拼成一张九宫格大图,人眼快速扫一遍,把明显标错的框揪出来重新标注。扑克牌这种强先验的目标,拿真牌对比着看,一张图几秒钟就能判断对错,这个时间花得最值。
4.2 标签里混进了奇怪文本:未知类别被静默跳过
现象:训练完发现某个类别完全没学到东西,比如 ten 的 AP 只有 0.3,查看训练日志时发现该类别目标数明显偏少。
原因:标注员偶尔会把类别写成 “10” 而不是 “ten”,或者把 ”Queen“ 写成大写,转换脚本里用了严格匹配,这些标签被静默跳过,导致对应目标等于没有训练样本。目标识别数据集里的类别命名一旦出现多套写法,最终吃亏的都是模型。
解决:转换脚本里遇到不在 CLASSES 里的名字时,不只要打印提示,还要把文件名和错误类别名写进一个 report.txt,转换完之后统一查看修正。我一般会再补一个统计脚本,输出每个类别转换前后的目标数量对比,数量对不上就说明有标签被过滤了。
4.3 标注框越界:坐标超宽高,loss 直接变成 nan
现象:训练到第三个 epoch,loss 突然变成 nan,调小学习率也没用,换初始化权重还是不稳。
原因:标注框的 xmax 或 ymax 超出了图片实际宽高,转换脚本没有做边界保护,归一化后出现大于 1 的宽高值。YOLO 在计算损失的宽高项时对异常值极其敏感,一个越界框就能让整个 batch 的梯度爆炸。这属于数据标注质量问题的典型后遗症。
解决:转换脚本里对标注框坐标做钳制到 [0, img_w] 和 [0, img_h] 的操作。如果不想直接修改数据,也可以在训练配置里开启 YOLO 自带的坐标裁剪参数,但最干净的做法还是在转换环节就堵住。顺带检查一下标注框是否有 w 或 h 为 0 的退化框,这类目标按照面积阈值过滤掉。
4.4 全量数据直接训练,验证结果虚高到失真
现象:训练集和验证集用的是同一批 363 张图,验证 mAP 高达 0.98,换成新环境拍摄的图片测试,mAP 掉到 0.7,差距大得离谱。
原因:这是最典型的训练集划分错误——先划分后训练,而不是先训练后划分。很多新手图省事,把所有图直接喂进去训练,模型把训练图像里的背景、光照、拍摄角度都背下来了,验证集又来自同一个分布,准确率自然虚高。这个扑克牌数据集只有 363 张图,不划分的话过拟合问题尤其严重。
解决:按 8:1:1 或 9:0.5:0.5 的比例划分训练集、验证集、测试集,划分时保证每个类别在三个集合中的数量比例与总体分布基本一致,避免某个类别在验证集里一个样本都没有。划分完再做数据增强,增强只作用于训练集,验证和测试集保持原图。
4.5 花色干扰:同类别不同花色导致特征学习偏移
现象:模型对黑桃类别的识别明显好于红桃,单独看红桃牌面的检测结果,置信度普遍偏低,漏检率偏高。
原因:扑克牌数据集里同一类别的花色分布并不均匀,比如 queen 大量是黑桃,红桃 Queen 样本很少,模型学到的主要是黑桃花色与类别之间的关联,而不是“牌面数值 + 标志”的通用特征。这在目标识别数据集的标注阶段很难被注意到,因为标注时关注的是类别,不是花色。
解决:训练前统计每个类别下的花色分布,如果发现某个类别只有单一花色,就要么补拍样本,要么在数据增强阶段把颜色扰动调大,通过色调偏移削弱模型对花色颜色的依赖。实战中我用 HSV 通道随机扰动配合灰度化增强,效果立竿见影。
5. 把 363 张用出 3630 张的效果:数据增强与划分边界
5.1 先划分再增强,别让验证集被“剧透”
数据增强最忌讳的是先增强后划分,那样同一张原图的多个增强版本会同时出现在训练集和验证集里,验证结果虚高到失去参考价值。正确流程是:先把 363 张图按比例切分,训练集只保留原始图,验证集和测试集永远是纯净的原图,然后只对训练集做增强。
以这个扑克牌数据集的实际规模来说,我建议按 85:10:5 划分,也就是大约 308 张训练、36 张验证、19 张测试。比例倾斜是因为测试集只用来做最终评估, 36 张验证集已经足够反映整体训练状态。划分时用脚本强制保证每个类别在三个集合中的出现次数与总体比例一致,避免某类牌面恰好全部集中到训练集。
5.2 扑克牌场景适合哪几类增强
通用的翻转、旋转、缩放在这里都能用,但有额外三个针对牌面的增强值得优先做。第一是透视变换,模拟牌面倾斜摆放;第二是亮度对比度扰动,模拟室内灯光不均匀和牌面反光;第三是随机擦除,模拟被手指或筹码遮挡的牌面局部。角度方面要注意,旋转 90 度或 180 度后牌面正反颠倒,但类别不变,所以旋转增强可以做满整个圆周,前提是模型推理时也要能处理旋转后的牌面方向。
光照扰动要控制幅度。扑克牌的牌面颜色本身是强区分的,红桃和黑桃靠颜色就能分开,亮度扰动过大会让红桃和黑桃在灰度空间变得几乎一样,反而加剧类别混淆。我一般把亮度偏移控制在 ±30 以内,饱和度偏移控制在 ±25 以内。
5.3 用 ultralytics 内置增强还是外部脚本增强
现在 YOLOv8 已经在训练阶段内置了 mosaic、mixup、hsv 扰动等增强策略,训练时开启即可,外部再增强一份会引入数据冗余。但内置增强对分布极为偏斜的小数据集不够充分,我的做法是双管齐下:外部脚本先做翻转、旋转、透视变换这种几何增强,生成新的图片和标签文件,内置增强再在训练时做色彩层面扰动,两者叠加效果最好。
import cv2 import numpy as np def perspective_augment(image, label_path, out_img_path, out_txt_path): h, w = image.shape[:2] # 四个角点随机偏移,模拟牌面透视形变 src = np.float32([[0, 0], [w, 0], [0, h], [w, h]]) offset = int(w * 0.08) dst = np.float32([ [np.random.randint(0, offset), np.random.randint(0, offset)], [w - np.random.randint(0, offset), np.random.randint(0, offset)], [np.random.randint(0, offset), h - np.random.randint(0, offset)], [w - np.random.randint(0, offset), h - np.random.randint(0, offset)], ]) matrix = cv2.getPerspectiveTransform(src, dst) warped = cv2.warpPerspective(image, matrix, (w, h)) # 标签框坐标同步做透视变换 with open(label_path, "r") as f: lines = f.readlines() new_lines = [] for line in lines: cid, cx, cy, bw, bh = map(float, line.split()) x1, y1 = (cx - bw / 2) * w, (cy - bh / 2) * h x2, y2 = (cx + bw / 2) * w, (cy + bh / 2) * h pts = np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtype="float32") new_pts = cv2.perspectiveTransform(pts.reshape(-1, 1, 2), matrix).reshape(-1, 2) nx1, ny1 = new_pts[:, 0].min(), new_pts[:, 1].min() nx2, ny2 = new_pts[:, 0].max(), new_pts[:, 1].max() # 透视后的框可能会越界,必须裁剪 nx1, ny1 = max(0, nx1), max(0, ny1) nx2, ny2 = min(w, nx2), min(h, ny2) nw = (nx2 - nx1) / w nh = (ny2 - ny1) / h ncx = (nx1 + nx2) / 2 / w ncy = (ny1 + ny2) / 2 / h new_lines.append(f"{cid} {ncx:.6f} {ncy:.6f} {nw:.6f} {nh:.6f}") cv2.imwrite(out_img_path, warped) with open(out_txt_path, "w") as f: f.write("\n".join(new_lines))这里最关键的是标注框坐标必须跟着透视矩阵一并对齐,框选区域要从矩形变成任意四边形后再次取外接矩形。透视变换后牌面目标可能被拉大或缩小,外接矩形可能包含额外背景,但只要原始框贴合牌面,增强后的误差在可接受范围内。每个增强副本生成后都要做一次面积过滤,如果变换后框面积小于原面积的 40%,说明该目标已经被严重压缩,直接丢弃这行标签。
5.4 增强之后再看一眼数据分布
增强做完,训练集从 300 多张膨胀到一千多张,这时候最容易出问题的是增强后的标注框坐标漂移。我最后都会跑一遍分布检查:打印增强前后每个类别的目标数量、每张图的平均目标数、标注框宽高的分布直方图。如果增强后某个类别的目标数量是其他类别的五倍以上,就要减少该类别的增强倍数,否则模型会被不平衡的类别带偏。这个小习惯能让你在开训前就发现八成以上的数据问题,省下的调试时间远比写检查脚本的时间多。
6. 模型验证的实操技巧:mAP 之外还要看混淆矩阵
训练完别急着收工,YOLO 训练结束后会在 runs/detect 目录下生成一系列指标文件,大多数人只看 mAP,但我建议把 confusion_matrix.png 单独拎出来放大看。对扑克牌检测来说,mAP 拉到 0.95 以上并不意味着模型能用,真正决定质量的是类间混淆分布。
扑克牌这种强规则目标,混淆矩阵里最值得盯的三条线是:king 与 queen 之间、jack 与 king 之间、ten 与 nine 之间。前两对是因为人像牌面纹理相似,后一对是因为数字牌中央图案密集且花色干扰强。正常情况下这几对之间的混淆比例应该在 3% 以下,超过 10% 就说明模型处于“能检测出是花牌,但分不清是哪张花牌”的半吊子状态。
python train.py --img 640 --batch 16 --epochs 100 --data data.yaml --weights yolov8s.pt训练完成后跑验证集,用下面这段 Python 读取混淆矩阵和每类 AP,对比各类别差异:
import pandas as pd # 读取 YOLO 输出的每类指标 df = pd.read_csv("runs/detect/val/results.csv") ap_cols = [c for c in df.columns if "ap" in c.lower()] print(df[ap_cols].to_string())每类 AP 的差异比总 AP 更诚实地反映模型短板。如果 queen 的 AP 明显低于其他类别,回看标注数据中 queen 的样本量和标注质量;如果某两类 AP 都不低但混淆矩阵里互相串脸,那就是特征抽象层次不够,优先考虑加深网络而非加数据。
验证通过后,我会做一次更接近真实场景的鲁棒性测试:把训练集里没有出现过的牌面照片,比如不同牌桌、不同灯光下拍的牌局,用训练好的模型直接推理,看置信度阈值调低到 0.4 时漏检率是否失控。这个扑克牌数据集本身只有 363 张图,训练出的模型只能保证在接近它拍摄分布的场景下可靠,迁移到新环境前务必重采一组验证图,不要指望一个模型打天下。
最早我做目标识别只看 mAP,有次模型 mAP 到了 0.96,实际桌面推演时明明摆着 Q,模型硬输出成 K,气到翻桌。后来才意识到混淆矩阵里 queen 和 king 的互相串扰率高达 17%,mAP 被大量易分类样本稀释了,问题样本全被掩盖。从那以后每次训练完,除了 mAP,我都会强制走一遍混淆矩阵和每类 AP 的对比,确认没有明显的类间混淆才公布结论。希望帮到你。
本文还有配套的精品资源,点击获取