扑克牌目标识别实战:VOC转YOLO与YOLOv8训练指南

发布时间:2026/10/1 10:40:34
扑克牌目标识别实战:VOC转YOLO与YOLOv8训练指南 简介面向目标检测入门与实战的扑克牌标注数据集专门用于识别queen、ten、nine、king、jack、ace六种常见扑克牌面。数据包含363张真实场景图片每张均配有labelimg生成的Pascal VOC格式xml标注共726个文件压缩包大小36.62MB目录中图片与xml一一对应。标注文件详细记录了每张牌的位置与类别信息可直接用于YOLO、SSD、Faster R-CNN等主流检测框架的数据加载省去手动标注的重复劳动。资源整体结构简洁使用时可快速切分训练集与验证集适合目标检测初学者了解数据集制作过程也适合算法工程师在模型对比、消融实验或部署验证中作为基准数据。目前已有395人学习下载无论是课堂实验、毕业设计还是个人项目都能帮助使用者高效打通从数据准备、标注解析到模型训练评估的完整链路快速积累目标检测实战经验。1. 扑克牌目标识别数据集363 张标注图先把六类牌面跑起来做目标检测的都知道模型好调、数据难搞。这套扑克牌目标识别数据集一共 363 张实拍图片标注类别只有六个nine、ten、jack、queen、king、ace也就是从 9 到 A 的六种牌面没有 2 到 8一看就是从特定牌局规则里裁剪出来的。标注工具是 LabelImg标签文件是 Pascal VOC 格式的 XMLYOLO 系、SSD 系、Faster R-CNN 都能直接接。它适合三类人想跑通「数据集到训练」全流程的新手、做棋牌类应用需要现成标注数据的开发者、以及要在小目标识别场景里快速验证模型效果的算法工程师。接下来我会把数据集结构、VOC 转 YOLO 的转换脚本、YOLOv8 训练参数和踩过的坑一次说清楚。2. 数据集结构拆解VOC XML 标注文件怎么读、类分布怎么看2.1 目录组织与命名规则从资源清单看图片是 cam_image26.jpg、cam_image14.jpg 这种相机连拍风格的命名按编号能大致推断采集顺序。这种命名的好处是排序稳定脚本处理方便但隐患也很明显——编号不连续cam_image7.jpg 后面直接跳到 cam_image11.jpg中间缺了帧。后续要补拍对齐的话得先做一次全量清点否则拷文件时很容易漏。最常见的整理方式是一个数据集拆两个目录JPEGImages 放原图Annotations 放同名 XML。VOC 的 XML 核心字段只有三个部分size 记录宽高object 列表记录每个目标的 name 和 bndbox 坐标。先拿一个 XML 直接解析看看结构import xml.etree.ElementTree as ET tree ET.parse(Annotations/cam_image26.xml) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) print(f图片尺寸: {w}x{h}) for obj in root.findall(object): name obj.find(name).text box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) print(f{name}: ({xmin},{ymin}) - ({xmax},{ymax}), f宽 {xmax-xmin}, 高 {ymax-ymin})VOC 的坐标是像素绝对值左上角为原点x 向右、y 向下。这段代码最大价值不是看单张图而是批量统计——把 363 个 XML 全跑一遍算出标注框的平均宽高分布训练前就能发现数据里有没有一批惊人大小的目标。不同采集批次的对焦距离不一样框的尺寸方差会非常大这个统计结果直接影响你后面 imgsz 怎么选。2.2 六类牌面的标注边界类别集合是 queen、ten、nine、king、jack、ace没有 2 到 8说明这套数据是从特定牌局规则里裁剪出来的常见于 Blackjack 记牌或扑克牌排序场景。六个类不算多但牌面之间长得太像才是真问题nine 和 king 都是大面积图案jack 和 queen 都是人物肖像模型很容易混淆所以标注质量比类别数量更关键。用 LabelImg 标牌面核心原则是「框住牌面不框桌面」。矩形框四边尽量贴住牌的边缘留 2~3 像素以内的余量牌有倾斜时 LabelImg 只能画水平矩形框里难免混入背景倾斜超过 30° 的牌宁可漏标也不要硬标否则模型学进去的是「牌背景」的混合特征。两张牌重叠时只标完全可见的那张重叠严重的直接跳过这种图进了训练集就是噪音。注意如果资源里 JPG 和 XML 混在同一层目录先按扩展名分好目录再跑校验否则 glob 会把两个类型混在一起看起来对不上账其实是被目录结构骗了。2.3 标完先做类分布统计数据到手第一步不是训练是对账。除了检查文件一一对应还要统计每类的样本量看有没有类别严重失衡from collections import Counter from pathlib import Path import xml.etree.ElementTree as ET counter Counter() for xml_path in Path(Annotations).glob(*.xml): root ET.parse(xml_path).getroot() for obj in root.findall(object): counter[obj.find(name).text] 1 for name, cnt in counter.most_common(): print(f{name:5} {cnt})如果某个类别只有个位数样本这个类在训练里基本等于不存在。363 张图均摊到六个类每类正常应该有 50 个以上的目标框低于 20 的类就要考虑是不是原始采集就没拍够。统计完类分布再决定要不要给样本少的类做裁剪增强而不是盲目加训练轮数。3. 把 VOC 转成 YOLO 格式转换脚本与四个边界坑3.1 为什么必须转、类别编号怎么定Pascal VOC 和 YOLO 的坐标体系完全是两回事VOC 存左上右下两个角点YOLO 存归一化后的中心点坐标加宽高VOC 的类别是字符串YOLO 标签文件第一列必须是整数类别 id。所以转换不是改个扩展名是真正的格式迁移。这里最容易翻车的四个点分别是类别编号顺序、坐标归一化、越界截断和过小框处理。先说第一个类别编号顺序。不要按字母序排要按你后续业务逻辑顺手的顺序固定。我要做牌面点数排序就把类别按点数降序固定为 ace0、king1、queen2、jack3、ten4、nine5。这样拿到的类别 id 直接对应点数权重后处理不用再映射一遍。把类别顺序写进一个 classes.txt转换脚本和训练配置都从它读保证全链路一致。3.2 转换脚本与参数说明import xml.etree.ElementTree as ET from pathlib import Path CLASSES [ace, king, queen, jack, ten, nine] def voc_to_yolo(xml_path: Path) - str: root ET.parse(xml_path).getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASSES: print(f[warn] {xml_path.name}: 未识别类别 {name}) continue cls_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) x_center (xmin xmax) / 2 / w y_center (ymin ymax) / 2 / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 防止标注越界导致归一化坐标超出 [0,1] bw min(bw, 1.0) bh min(bh, 1.0) if bw 0.01 or bh 0.01: print(f[warn] {xml_path.name}: 框过小 {name}) continue lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) return \n.join(lines) for xml_path in Path(Annotations).glob(*.xml): out voc_to_yolo(xml_path) if out: txt_path Path(labels) / f{xml_path.stem}.txt txt_path.write_text(out)几个参数说清楚中心点坐标必须除以图片宽高做归一化不除的话 YOLO 拿原像素值去算出来的全是几百上千的大数模型直接跑飞宽高做 min 截断是防 LabelImg 标到图片边缘外的情况框宽高小于图片 1% 的目标直接丢弃这种目标下采样几层之后就剩一两个像素训练时就是噪声。提示转换后随机打开几个 txt 抽查第一列应该是 0~5 的整数后四列应该都在 [0,1] 区间内任何一个超出区间都要回头查原始 XML。3.3 训练集和验证集划分363 张图不算多按比例随机分就行。但随机之前先固定随机种子保证不同人复现结果一致import random from pathlib import Path random.seed(42) imgs sorted(Path(JPEGImages).glob(*.jpg)) random.shuffle(imgs) split int(len(imgs) * 0.8) train_imgs imgs[:split] val_imgs imgs[split:] print(ftrain: {len(train_imgs)}, val: {len(val_imgs)})把 80% 的图片和同名标签拷到 images/train 和 labels/train剩下 20% 放 images/val 和 labels/val。注意 labels 目录结构必须和 images 完全对应YOLO 是拿图片名去查同名 txt任何一层路径没对上都会报 no labels found。拷完再跑一遍对账脚本确认两边数量一致再进训练。4. 用 YOLOv8 训练自己的数据集data.yaml、训练参数与结果解读4.1 data.yaml 与目录整理YOLOv8 训练的第一步是写 data.yaml它告诉模型数据在哪、有哪些类别。目录按官方惯例排cards/images/{train,val} 和 cards/labels/{train,val}。有一个很多新手翻车的点path 字段写相对路径配合 yaml 里其他相对目录一旦移动项目就找不到文件。我一般直接写绝对路径省得一换机器就排查半天。path: D:/workspace/cards train: images/train val: images/val names: 0: ace 1: king 2: queen 3: jack 4: ten 5: ninenames 的顺序必须和第三章转换脚本的 CLASSES 完全一致差一个位置训练出来的类别就是错位的。这种错位在混淆矩阵里很难直接发现因为模型本身没有错错的是你的标注映射——这玩意就是黑匣子最坑。我习惯把 classes.txt 和 data.yaml 的 names 用同一个 Python 脚本生成从源头杜绝手抄错。4.2 训练命令与参数说明自己的数据集模型选型我推荐从 yolov8s 起步。n 模型太轻对牌面纹理区分度不够m 及以上在这个数据规模下又容易欠拟合。命令直接跑yolo detect train \ datacards.yaml \ modelyolov8s.pt \ imgsz640 \ epochs80 \ batch16 \ projectruns/detect \ namecards参数逐个说modelyolov8s.pt 是从 COCO 预训练权重开始微调363 张图的规模不迁移学习基本训不动imgsz640 是训练输入尺寸牌在画面里普遍偏小就提到 800代价是训练速度变慢batch16 看显存决定8G 显存没问题4G 就降到 8epochs80 看起来不多但数据量小60 轮左右 loss 就平了设 80 是留余量。训练完看 weights/best.pt 和 last.pt。best 是按验证集 mAP 最优保存的部署只用它last 是最后一步的权重一般当备份。这两个别搞反用 last 部署的人最后都回来查过日志。4.3 训练结果的解读方式训练日志里最值得看的不是 loss而是 confusion matrix 和 P/R 曲线。六类都有正样本直接看各类别的 Precision 和 Recall 比看均值更直观。最容易出现的场景是 queen 和 king 互相串——两者都是人物构图印刷小差别模型分不清是正常的。遇到混淆偏高优先补这两个类的样本数量而不是盲目加 epochs。加 epochs 只能把模型在已有数据上压得更死对类别混淆基本没用。数据增强上这个项目可以做轻度 HSV 抖动和上下翻转但左右翻转要小心牌面文字是方向敏感的横拍场景下左右翻转会把 nine 和 ten 的方向特征搞乱模型学到正反两个方向推理时反而迟疑。除非你的业务里牌本来就会倒着出现否则别开。5. 标注与训练避坑五条血泪经验先看再动手5.1 图片和标注文件数量对不上现象转换脚本跑完labels 目录出现一行 no labels found或者训练时报 found 300 labels but 363 images。原因数据是多批次采集合并的有人用 LabelImg 标完没保存或者拷贝时漏了部分 XML。这套数据的命名就有端倪cam_image7 和 cam_image11 之间编号不连续说明混入了多批素材。解决转换前先跑第二章的校验脚本两个集合的差集为空再继续。缺 XML 的图直接删掉别心软留着训练时它会拖低有效样本比例还容易让 batch 计算出错。5.2 类别大小写不一致导致类别丢失现象日志里出现 [warn] 未识别类别 Ten训练出来只有五个类在跑mAP 还显示异常高。原因标注时有人手滑写成了首字母大写 Ten和标准小写 ten 不一致。XML 里 name 是自由文本LabelImg 不限制输入这种错误非常隐蔽你不去解析 XML 根本发现不了。解决转换脚本里做一次 name.lower() 归一化再和 CLASSES 比对。如果 lower 之后仍不在集合里打印警告并把该目标跳过绝不要在转换脚本里静默吞掉未知类别。整理完重新统计类分布确认六个类都在。5.3 标注框过宽、包进桌面背景现象训练 loss 降得很低但推理时同一张牌连续输出两个重叠框或者框明显比牌面大一截。原因标注时为了保险把框画大了一圈把牌桌纹理和周围牌边都包进去了。模型学到的特征是「牌背景」而不是牌本身。解决回看标注质量把框超出牌面边缘 10% 以上的样本重新标。363 张图逐张检查完全来得及这一个小时花得非常值。宁可少标不要标脏。5.4 小目标识别漏检mAP 高、实际部署漏小牌现象验证集 mAP 到 0.85但推理时画面远端的小牌完全没框出来。原因标注框面积占比过小的目标在缩放到 640 之后被压成几个像素YOLOv8 的下采样倍数决定了小目标特征保留不住。nine 和 ten 这类数字牌在远处就是一小团纹理最容易漏。解决训练 imgsz 从 640 提到 800小目标特征会更充分推理 conf 阈值从默认 0.25 降到 0.15。对棋牌场景来说漏检比误检更致命多给几个低分框交给后处理去过滤比模型直接吞掉强得多。我还加过 SAHI 切图方案把大图切块推理小目标漏检明显下降代价是推理耗时翻倍业务允许的话值得试。5.5 验证集划分泄漏同批次连续帧被拆到两边现象训练集 mAP 接近 1.0验证集表现也好但换一台新设备拍摄就崩漏检明显增多。原因这套图是连续拍摄的相邻帧之间的牌面布局几乎相同。随机切分时同一局牌的帧同时出现在训练和验证里模型其实在背答案而不是在学泛化。解决按 cam 编号或采集批次分组保证同一批次的帧只进训练或只进验证。363 张图按文件名前缀手工分桶就够分完再校验两边没有相同前缀的图。之后每次新数据进来分区逻辑考虑数据来源是第一优先不是懒得分就随机切。6. 进阶用法把检测结果按牌面点数排序输出检测跑通之后下一步就是接业务逻辑。棋牌类应用最常见的下游需求是把画面里的牌按点数排序输出再做去重。点数排序可以直接用训练时的类别 id 完成前提是你在第三章已经把类别按点数降序编号from ultralytics import YOLO model YOLO(runs/detect/cards/weights/best.pt) results model(test/cam_image12.jpg, conf0.15)[0] # 类别 id 按 0ace, 5nine 编号直接映射点数权重 RANK {0: 14, 1: 13, 2: 12, 3: 11, 4: 10, 5: 9} cards [] for box in results.boxes: cls int(box.cls[0]) conf float(box.conf[0]) x float(box.xyxy[0][0]) cards.append({name: model.names[cls], conf: conf, x: x}) # 按点数降序同点数按牌桌左右位置排 cards.sort(keylambda c: (-RANK[c[name]], c[x])) for c in cards: print(f{c[name]:6} conf{c[conf]:.2f} x{c[x]:.0f})RANK 映射是唯一要手写的部分训练时改了类别顺序这里要同步。sort 的 key 用负号实现降序x 作为次级排序保证同点数的两张牌按从左到右输出符合牌局阅读顺序。conf 阈值设 0.15 不是失误实测小目标牌在 0.2 以下就会被滤掉宁可多输出一个低分框也不要让业务层看不到牌。验证这套逻辑时别只看单张图把整个数据集跑一遍逐张输出预测到的牌数量和 XML 里手工统计的 object 数量对比能快速定位漏检集中出现在哪些构图。血泪教训是我最早把验证脚本写成只打印汇总 mAP漏了远处一张牌根本看不出来后来改成逐图比对数量才把特征不清的角牌全捞了回来。从那以后每次接新数据集我都强制先跑一遍「预测数量 vs 标注数量」的对账脚本再去做任何调参。这个习惯省下的时间远比写脚本的半小时多希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询