ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

363张扑克牌XML标注数据集:目标检测实战训练完整指南

363张扑克牌XML标注数据集:目标检测实战训练完整指南 简介面向目标检测入门与扑克牌识别场景这份数据集包含363张已标注扑克牌图像覆盖六种牌面王后、十、九、国王、杰克、A可用于训练主流目标检测模型也适合作为分类或识别任务的基准数据。资源共726个文件含363个jpg原图与363个xml标注xml采用LabelImg工具生成符合Pascal VOC格式能直接搭配常见检测框架使用压缩包约36.62MB便于下载和本地部署。目前已有395人学习下载适合计算机视觉新手、竞赛选手和需要快速构建扑克牌识别原型的开发者。通过它可以省去自行采集与标注的时间把精力放到模型调参和效果验证上每个jpg均有同名xml文件便于理解图像与标签的对应关系也能参考标注中的目标框坐标与类别信息练习VOC格式的解析、增强与数据划分从而系统掌握目标检测数据准备的标准流程。1. 363 张扑克牌照片就能训目标检测这套 XML 标注数据集够用吗“你手里有没有大牌”这个判断本质就是一个 6 类目标识别问题。这套扑克牌目标识别数据集收录 363 张实拍画面用 labelimg 标注出 queen、ten、nine、king、jack、ace 六类牌面标签全部是 XML 格式。它的规模不大却把“数据集 → 标注格式 → 训练 → 验证”的完整流程要素都凑齐了。对还没跑通过一次检测训练的新手它比动辄几万张的公开数据集友好得多对熟手它又是一个测试数据增强、调优流程的轻量载体。关键是单张图片和 XML 都很小整套数据几十 MB迭代一次训练很快很适合拿来做课程设计或验证自己的想法。下面是我实际拆这套数据集后记录的文件结构、转换脚本和训练时会踩的坑。2. 数据集的物理解构图片命名、XML 标注与对象分布2.1 图片文件与标注文件的组织方式这套数据的图片命名带明显的相机采集痕迹前缀统一是cam_image加编号比如cam_image26.jpg、cam_image14.jpg、cam_image34.jpg。编号不是从 1 连续排到 363 的中间有跳号说明采集时做了筛选把模糊、无牌面的帧删掉了。这个细节对后续脚本有个直接影响不能靠文件名编号推断总数一切以实际存在的文件为准。标注工具是 labelimg默认输出 Pascal VOC 风格的 XML。如果你之前只碰过 YOLO 的 txt 标注第一次打开这种 XML 会有点不习惯但其实字段很固定。一个标准 XML 里至少有 filename、size、object 三块object 可以重复出现每张牌一个。下面是一个典型的标注内容annotation folderJPEGImages/folder filenamecam_image26.jpg/filename source databaseUnknown/database /source size width640/width height480/height depth3/depth /size object namequeen/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin142/xmin ymin97/ymin xmax374/xmax ymax288/ymax /bndbox /object /annotationXML 里size记录的是原图宽高单位是像素bndbox下四个值分别代表目标框左上角和右下角的绝对坐标。注意这里的坐标没有归一化训练 YOLO 之前必须转成相对宽高的中心点格式。truncated表示目标是否被截断difficult表示是否难例这两个字段 labelimg 默认写 0但如果标的时候手动勾选过后面转换脚本里要留个心眼。2.2 object 字段解读六类牌面怎么写的六个类名是queen、ten、nine、king、jack、ace全是小写字符串对应扑克牌里的 Q、10、9、K、J、A。这里有两个值得注意的点。第一没有 2 到 8 这些点数所以这套数据的任务不是“识别任意扑克牌”而是“识别玩家手里的关键大牌”业务目标要按这个来重新定义。第二标注里不区分花色黑桃 Q 和红心 Q 都算queen这其实是合理的——同一张牌面不同花色对后续牌型判断没有影响模型只需要知道“这张牌是 Q”就够了。从目标框来看这套数据的标注框基本都紧贴牌面没有给牌面留太多边缘。好处是训练出来的框会很干净坏处是如果拍照时牌边有手指遮挡truncated字段可能会被忽略。我在统计时发现同一张图里出现 2 到 3 个 object 的情况很常见说明场景大多是翻牌或手牌特写不是整桌扑克的全景。对小目标检测来说这种牌面占画面比例较大的样本其实更友好模型不需要在很小的像素区域里找牌。3. 数据划分与类别统计别让一张牌出现在两个目录里3.1 划分脚本先按图切分再复制标注训练检测模型之前第一件事不是写训练代码而是把数据划分做好。很多人习惯直接写个循环把图片和 XML 按 9:1 随机塞到两个文件夹里但这里有个非常隐蔽的坑如果同一张图片的多个标注对象因为随机数拆分在两个集合里等于同一张图的标注信息被撕裂。比如cam_image22.jpg里有两张牌一张划分到训练集另一张划分到验证集验证集的评估结果就毫无意义。我习惯的做法是先拿到所有图片的完整文件名列表以图片为单位整体划分然后连同对应的 XML 一起复制。下面这个脚本就是按这个思路写的import os import random import shutil image_dir JPEGImages xml_dir Annotations train_dir images/train val_dir images/val random.seed(0) val_ratio 0.1 all_images [f for f in os.listdir(image_dir) if f.lower().endswith(.jpg)] random.shuffle(all_images) val_count int(len(all_images) * val_ratio) val_images set(all_images[:val_count]) train_images all_images[val_count:] for split, imgs in [(train, train_images), (val, val_images)]: os.makedirs(f{split}_labels, exist_okTrue) for img in imgs: xml_name os.path.splitext(img)[0] .xml if os.path.exists(os.path.join(xml_dir, xml_name)): shutil.copy(os.path.join(image_dir, img), fimages/{split}/{img}) shutil.copy(os.path.join(xml_dir, xml_name), f{split}_labels/{xml_name}) else: print(fmissing xml: {xml_name})脚本里random.seed(0)很关键意思是固定随机源保证每次运行产生的划分结果一致。如果你不固定随机种子同一个数据集两次划分出不同的训练集/验证集训练结果就没有可比性这也是后面调参时最大的不确定性来源。val_ratio 0.1对 363 张图来说大概划出 36 张做验证剩下的全给训练验证集小了点但对检测任务来说够用。划分完成后建议做一次“反向校验”也就是统计一下训练集和验证集的图片有没有重复。直接对比文件名交集即可train_files set(os.listdir(images/train)) val_files set(os.listdir(images/val)) print(overlap:, train_files val_files)输出为空说明切分没问题如果输出非空就要回到前面的划分逻辑检查通常是因为val_images用了set去重但train_images里仍保留着未去重的副本。这个小脚本我每次都会跑一遍因为它能直接发现文件名重复、复制遗漏这类低级错误。3.2 类别统计与样本均衡性划分之后要看各类别的目标框数量分布。扑克牌检测有个特点六类样本不可能完全均衡因为采集者拍摄时不会刻意去凑数量。对 363 张图来说统计目标框数量用下面这段脚本很方便import xml.etree.ElementTree as ET import os annotations_dir Annotations class_counter {} for xml_name in os.listdir(annotations_dir): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(annotations_dir, xml_name)) root tree.getroot() for obj in root.findall(object): cls obj.findtext(name) class_counter[cls] class_counter.get(cls, 0) 1 for cls, count in sorted(class_counter.items(), keylambda x: -x[1]): print(f{cls}: {count})实际统计出来的分布大致是 queen、king、ace 会偏多ten、nine 相对少一些。这是扑克牌识别数据集很常见的状态因为采集时更关注大牌。只要最少的类别不是只有几张图对训练的影响就有限如果某个类别少于 20 个框那就要考虑做针对性增强或者去补拍。更需要注意的是验证集里六类是否都覆盖到如果验证集里恰好没有nine那模型在nine上的表现就是一个黑匣子mAP 再高也不能说明问题。4. XML 转 YOLO 格式与避坑清单转换脚本和五个翻车点4.1 转换脚本把 bndbox 归一化成相对坐标YOLO 系列训练时读的是 txt 标注每行一个目标格式是class_id x_center y_center width height其中后四个值都是相对于图片宽高的比例。从 XML 转过去时很多人直接套公式结果训练出来的框要么偏到角落要么宽高和实际目标差好几倍问题大多出在坐标换算上。避开这些问题的转换脚本如下import os import xml.etree.ElementTree as ET classes [ace, jack, king, nine, queen, ten] class_to_id {name: i for i, name in enumerate(classes)} xml_dir Annotations out_dir yolo_labels os.makedirs(out_dir, exist_okTrue) def convert(xml_path, out_txt_path): tree ET.parse(xml_path) root tree.getroot() img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) with open(out_txt_path, w) as f: for obj in root.findall(object): cls obj.findtext(name) if cls not in class_to_id: continue bbox obj.find(bndbox) xmin float(bbox.findtext(xmin)) ymin float(bbox.findtext(ymin)) xmax float(bbox.findtext(xmax)) ymax float(bbox.findtext(ymax)) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h width (xmax - xmin) / img_w height (ymax - ymin) / img_h if x_center 0 or x_center 1 or y_center 0 or y_center 1: print(fout of range: {xml_path}, {cls}) continue f.write(f{class_to_id[cls]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) out_txt os.path.join(out_dir, xml_name.replace(.xml, .txt)) convert(xml_path, out_txt)注意classes列表的顺序不是乱写的它决定了class_to_id的映射。这个顺序必须和后面训练用的data.yaml保持一致否则会出现类别错位。另一个容易忽略的点是findtext(size/width)这种写法它依赖 XML 里的层级结构labelimg 输出的标准格式没问题但如果别人手工改过 XML这里拿不到值就会直接报错。转换完最好随机抽查几行 txt拿第一行去和原 XML 的 bndbox 做一次反向验证。比如cam_image26.jpg转换后第一行是0 0.4031 0.4010 0.3625 0.3979那你就要打开原图看左上角 (142, 97) 到右下角 (374, 288) 是不是一张完整的 queen。这个反向验证动作只需花 5 分钟却能过滤掉一大半标注格式问题。4.2 五个翻车点现象、原因与解决翻车点 1模型训练 loss 降下去但预测框整体偏移。现象是检测框比实际牌面大一圈或者中心点往右下角偏。原因出在归一化时除的不是原图宽高而是目标框自身的宽高。解决方法是回到转换脚本检查是否用了xmax - xmin作为除数并把每条 txt 的值打印出来看是否都在 0 到 1 之间。翻车点 2训练时类别互相混淆ace 和 queen 常常互相误检。现象是混淆矩阵里 ace 那一行有相当一部分框被判成 queen。原因往往不是模型问题而是classes列表顺序和 YOLO 的data.yaml里names顺序不一致模型把 ID 3 当成 ace实际标签 ID 3 是 queen。解决方法是只在转换脚本里定义一次classes然后让data.yaml的 names 直接用这个顺序生成不要手动在 YAML 里再敲一遍。翻车点 3Linux 服务器上训练时大量图片提示找不到标注文件。现象是 windows 本机跑没问题一上服务器就报 xml 缺失。原因是 XML 里filename记录的和实际文件名大小写不一致比如图片是cam_image7.jpgXML 里写的是cam_image7.JPG。Windows 文件系统不区分大小写Linux 严格区分。解决方法是转换脚本里统一用os.path.splitext得到的小写扩展名去匹配不要直接用 XML 里的filename字段。翻车点 4标注里difficult1的目标被保留导致验证集评估结果虚高。现象是 mAP 看起来很高但实际丢到新场景里误检一堆尤其是牌面被遮挡、只露出一角的图。原因是转换脚本没有过滤 difficult 样本而这些难例在训练时被模型捡漏“背下来”验证时又作为正样本出现。解决方法是转换时判断difficult字段要么全保留要么全丢弃并确保训练集和验证集的处理逻辑完全一致。翻车点 5同一点数的红黑花色被当成不同类别人为切分。现象是自己拍的补充数据里只标了红桃和方块模型对黑桃、梅花泛化差。原因是对“类别”的定义没想清楚。这套数据把 Q、K、A 归为不带花色的类补充数据时也要保持同样口径不要在 class 名里加heart_queen、spade_queen这样的细分否则类别数量会失控。5. 用 YOLOv8 训练自己的扑克牌数据集data.yaml、增强与批次调参5.1 data.yaml 与模型选型数据和标签准备好之后就要开始训练。现在训练目标检测的主流方式是用 YOLOv8 这类框架直接读你自己的数据集。第一步是写data.yaml内容里指定 train、val 路径、类别数量和类别名下面是这套扑克牌数据对应的配置path: /your/absolute/path/to/poker train: images/train val: images/val names: 0: ace 1: jack 2: king 3: nine 4: queen 5: tennames的顺序必须和转换脚本里classes列表完全一致这是整个训练流程里最容易出错但也最容易检查的地方。path建议写绝对路径因为 ultralytics 在解析相对路径时是以当前运行目录为基准不同机器上很容易因为路径解析不一致而报错。train和val是相对于path的路径这里指向的是划分好的图片目录而不是 labels 目录YOLO 会自动去同级找 labels所以前面把 txt 放在yolo_labels后建议把目录重命名成与图片目录对应的labels/train和labels/val结构这一点从val配置就能看出来。模型选择上363 张图这个规模用yolov8n.pt起步最合适。nano 版本参数量小不容易过拟合训练速度快跑完一个 epoch 只要几十秒调参迭代周期短。不要一上来就选yolov8x在 363 张图的小数据集上模型容量越大越容易把训练集背下来验证集表现反而更差。我的经验是先用 nano 跑通全流程再根据结果考虑是否换 s 或 m。训练命令可以这样写yolo detect train datadata.yaml modelyolov8n.pt epochs100 imgsz640 batch16 device0 seed0参数含义epochs100对应小数据集的常规迭代轮数如果 60 轮左右 mAP 已经不再涨提前停止即可imgsz640是输入分辨率扑克牌牌面本身不算小目标640 是平衡速度和精度的常见选择batch16看显卡显存显存不够就降到 8检测任务 batch 大小对最终精度的影响没有分类任务那么剧烈。5.2 数据增强参数怎么调小数据集最怕的就是过拟合YOLOv8 自带的增强策略能帮你把 363 张图“变出”更多有效样本。增强参数在训练时可以用命令行覆盖也可以在yolov8n.yaml里改下面这几个是扑克牌场景里效果最明显的参数默认值建议值说明hsv_h0.0150.02色相抖动模拟不同灯光下的牌面颜色偏移hsv_s0.70.8饱和度抖动并不过分影响牌面纹理hsv_v0.40.5亮度抖动牌面反光和阴影都能覆盖flipud0.00.5上下翻转扑克牌上下翻后类别语义不变fliplr0.50.5左右翻转对牌面识别无影响scale0.50.6随机缩放模拟不同拍摄距离mosaic1.01.0四图拼接增强小目标检测能力flipud对扑克牌来说是安全的因为 J、Q、K、A 这些牌面上下翻转后还是同一张牌除非牌面上有明确的方向性水印。mosaic增强在这个数据上效果显著它能把四张不同环境下拍的牌拼成一张图让模型天然适应“牌出现在画面不同位置”的场景。但要注意这些增强参数只在训练时生效验证时用的是原图所以不用担心验证集被“污染”。调增强参数时要控制变量。一次只调一个参数固定其他参数和随机种子观察验证集 mAP 的变化。我见过有人同时改五个增强参数然后说效果“变好了”其实根本说不清是哪个参数起的作用。扑克牌检测对颜色不太敏感因为纸牌颜色本身就局限在红、白、黑几个色系所以hsv_h不要调太大0.02 已经足够反之对尺度变化比较敏感因为拍摄距离不稳定scale可以适当放宽。6. 验证一套推理链路从权重到业务判断的第二个门槛6.1 用训练好的权重跑一次完整预测训练完成后验证集上的 mAP 只是一个数字真正要确认的是模型在真实牌桌上能不能用。我习惯写一个极简的推理脚本把模型输出的框画到原图上直接肉眼看效果这比只看 mAP 更能发现问题from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict(cam_image21.jpg, conf0.35, iou0.5) for r in results: for box in r.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) names model.names print(f{names[cls_id]} {conf:.2f} {box.xyxy[0].tolist()})这里conf0.35是关键参数。YOLOv8 默认的置信度阈值是 0.25但扑克牌检测场景里牌与牌之间的纹理差异不大背景又常常是绿呢桌面阈值设得太低容易把桌面纹理当牌面。我一般会在 0.3 到 0.5 之间扫一遍找到误检和漏检的平衡点。box.xyxy输出的是绝对像素坐标可以直接画框或换算成业务坐标。6.2 从 mAP 到业务判断的最后一公里有了检测结果后续怎么用取决于业务目标。如果是做扑克牌识别计数那要写一层业务逻辑把多个检测框聚合起来判断是否有同一点数的对子、三条如果是做牌型记录就要把每一帧的检测结果序列化存储再做时间维度上的去重。这一步和模型训练是割裂的但最容易暴露数据集标注口径的问题。我最初拿这套数据跑通训练后直接按 mAP 0.87 就认为任务完结结果把模型端上实景测试时发现逆光拍摄场景下有一半的ten被漏检。回头检查才发现数据集里ten的样本大多来自顺光近距拍摄逆光样本几乎没有——这不是模型的错是数据分布和真实场景不一致。从那以后我每次拿到新数据都会先想清楚“模型预测结果会被谁消费”然后强制走一遍端到端推理用十张现场重拍的图做冒烟测试再回头调训练而不是只看验证集指标。这套 363 张的 XML 标注数据集作为入门链路验证和算法实验足够扎实希望帮到你。本文还有配套的精品资源点击获取
返回列表