news 2026/10/1 10:40:34

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
扑克牌目标识别实战: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 格式的 XML,YOLO 系、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。所以转换不是改个扩展名,是真正的格式迁移。

这里最容易翻车的四个点分别是:类别编号顺序、坐标归一化、越界截断和过小框处理。先说第一个:类别编号顺序。不要按字母序排,要按你后续业务逻辑顺手的顺序固定。我要做牌面点数排序,就把类别按点数降序固定为 ace=0、king=1、queen=2、jack=3、ten=4、nine=5。这样拿到的类别 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(f"train: {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: nine

names 的顺序必须和第三章转换脚本的 CLASSES 完全一致,差一个位置,训练出来的类别就是错位的。这种错位在混淆矩阵里很难直接发现,因为模型本身没有错,错的是你的标注映射——这玩意就是黑匣子,最坑。我习惯把 classes.txt 和 data.yaml 的 names 用同一个 Python 脚本生成,从源头杜绝手抄错。

4.2 训练命令与参数说明

自己的数据集,模型选型我推荐从 yolov8s 起步。n 模型太轻,对牌面纹理区分度不够;m 及以上在这个数据规模下又容易欠拟合。命令直接跑:

yolo detect train \ data=cards.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=80 \ batch=16 \ project=runs/detect \ name=cards

参数逐个说:model=yolov8s.pt 是从 COCO 预训练权重开始微调,363 张图的规模不迁移学习基本训不动;imgsz=640 是训练输入尺寸,牌在画面里普遍偏小就提到 800,代价是训练速度变慢;batch=16 看显存决定,8G 显存没问题,4G 就降到 8;epochs=80 看起来不多,但数据量小,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", conf=0.15)[0] # 类别 id 按 0=ace, 5=nine 编号,直接映射点数权重 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(key=lambda 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 标注数量」的对账脚本,再去做任何调参。这个习惯省下的时间远比写脚本的半小时多,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:40:21

YOLOv5数据集格式详解:自动驾驶目标检测训练避坑指南

简介&#xff1a;面向自动驾驶目标检测任务&#xff0c;提供YOLOV5目录格式的标注数据集&#xff0c;覆盖卡车、行人、交通信号灯等11类道路目标&#xff0c;配套图像为512512 RGB&#xff0c;每帧含多个目标且边界框清晰&#xff0c;适合车辆检测、密集场景识别与自动驾驶感知…

作者头像 李华
网站建设 2026/10/1 10:40:20

针织品瑕疵检测YOLOv9数据集深度解析与实战避坑指南

简介&#xff1a;面向针织品瑕疵检测的YOLOv9标注数据集&#xff0c;专门为训练目标检测模型而整理&#xff0c;适合计算机视觉工程师、算法研究人员及工业质检技术选型者使用&#xff0c;可解决产线质检中标注样本缺失、模型难以收敛的问题。压缩包共105个文件&#xff0c;包含…

作者头像 李华
网站建设 2026/10/1 10:40:20

【AI大模型接入SDK】ChatSDK 集成测试概述

&#x1f3ac; 个人主页&#xff1a;艾莉丝努力练剑❄专栏传送门&#xff1a;《C语言》《数据结构与算法》《C/C干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法&#xff1a;从基础到进阶》《Python干货分享》⭐️为天地立心&#xff0c;为生民立命…

作者头像 李华
网站建设 2026/10/1 10:40:15

从零实现Java局域网聊天室:Socket通信、多线程与Swing实战

简介&#xff1a;面向计算机相关专业毕业设计或课程设计的Java局域网聊天室系统完整项目&#xff0c;提供源代码与配套论文&#xff0c;涵盖客户端与服务端通信、用户界面和消息收发等典型功能&#xff0c;可作为选题参考与二次开发基础。资源包共239个文件&#xff0c;大小约1…

作者头像 李华
网站建设 2026/10/1 10:40:06

DeepSeek Harness实战:从API调用到Agent工作流编排的完整落地指南

这次我们来看一个和 DeepSeek 强相关的 agent 开发话题&#xff1a;DeepSeek Harness。它不是一个单纯的聊天客户端&#xff0c;而是一类面向 agent 工程化的 harness 工作流框架&#xff0c;把模型调用、工具编排、批量任务和评估测试收拢到一套可配置的系统里。如果你最近在看…

作者头像 李华
网站建设 2026/10/1 10:34:05

OpenCV+Dlib实时陌生人检测系统:从算法到可部署落地

简介&#xff1a;本资源是一套完整可用的Python毕业设计项目——基于OpenCV的视频人脸识别与陌生人报警系统&#xff0c;面向计算机及相关专业本科生&#xff0c;适用于课程设计、期末大作业及项目实战训练。系统支持实时视频流人脸检测与识别&#xff0c;对未授权人员触发声音…

作者头像 李华