ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

扑克牌检测实战:从VOC XML到YOLO训练格式的转换与避坑指南

扑克牌检测实战:从VOC XML到YOLO训练格式的转换与避坑指南 简介用于扑克牌目标检测与识别任务的数据集资源面向计算机视觉学习者、算法工程师及目标检测方向的研究人员解决扑克牌类别定位与分类训练数据不足的问题。图片均使用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 folderimages/folder filenamecam_image26.jpg/filename source databaseUnknown/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object namequeen/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin523/xmin ymin301/ymin xmax698/xmax ymax512/ymax /bndbox /object object nameten/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin724/xmin ymin288/ymin xmax907/xmax ymax505/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, encodingutf-8) as f: f.write(\n.join(lines)) def convert_batch(xml_dir, out_dir): os.makedirs(out_dir, exist_okTrue) 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.yamlYOLOv5/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现象训练到第三个 epochloss 突然变成 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]], dtypefloat32) 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 的对比确认没有明显的类间混淆才公布结论。希望帮到你。本文还有配套的精品资源点击获取
返回列表