十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

从VOC到YOLOv8:扑克牌目标检测数据集构建与训练实战

从VOC到YOLOv8:扑克牌目标检测数据集构建与训练实战 简介面向计算机视觉入门与进阶开发者这份标注扑克牌目标识别数据集聚焦牌面检测与分类任务当前标注类别包含queen、ten、nine、king、jack、ace六种常见扑克牌面覆盖棋牌游戏识别、智能发牌与桌面交互等真实应用场景。数据集共726个文件由363张jpg原图和363个xml标注文件组成jpg图像涵盖不同拍摄角度与光线条件xml使用LabelImg标注遵循标准Pascal VOC格式压缩包整体约36.62MB解压后可按图片编号一一对应便于直接编写数据加载与增强脚本。目前已有395人学习浏览适合需要通过真实图像数据练习目标检测全流程、开展课程设计或算法实验的读者。获取后可快速解析xml得到边界框和类别标签省去手动采集与标注的时间成本并可直接迁移至YOLO、Faster R-CNN等主流检测框架进行训练与效果评估。1. 从 363 张扑克牌照片到可训练的目标检测数据集做目标检测的人大概都经历过这种尴尬模型结构调得再花哨数据一塌糊涂mAP 就是上不去。扑克牌识别这种任务看似简单真正落地的难点反而在数据侧——牌面反光、倾斜透视、多张牌重叠、花色和点数组合都会让标注边界变得模糊。这个数据集提供了 363 张真实拍摄的桌面图像统一用 labelimg 标注成 PASCAL VOC 格式的 XML 文件类别限定为 queen、ten、nine、king、jack、ace 六种正好覆盖了斗地主、21 点这类牌型识别里出现频率最高的目标。对刚入门目标检测的人来说它比 COCO 这种重量级数据集友好得多类别少、场景单一、标注完整跑通一次完整的训练-验证-推理管线不会超过半天对已经做过检测项目的人来说它的价值则在于研究小目标、遮挡和反光条件下的标注策略。下文会从标注格式拆解开始一直讲到如何把这份数据平滑迁移到 YOLOv8 训练流程里全程给出可直接执行的代码和参数说明。2. 先看懂 XML 标注VOC 格式的内部结构与读取方式2.1 标签文件里到底存了什么用 labelimg 标注得到的 XML 文件遵循 PASCAL VOC 规范每个文件对应一张图片文件名与图片名保持一致。打开任意一个 XML核心结构分为三层annotation根节点记录图片文件名、存放路径、尺寸和通道数object节点是每个目标的独立描述bndbox则定义了目标框的左上角和右下角坐标。扑克牌数据集里一定出现的关键字段是name它直接对应 queen、ten、nine、king、jack、ace 六个类别之一。一个典型的 XML 内容结构如下annotation folderimages/folder filenamecam_image26.jpg/filename size width640/width height480/height depth3/depth /size object namequeen/name bndbox xmin213/xmin ymin156/ymin xmax412/xmax ymax398/ymax /bndbox /object /annotation这里xmin/ymin是目标框左上角坐标xmax/ymax是右下角坐标单位是像素。注意 labelimg 导出时默认坐标是整数但并不意味着所有标注框都与真实牌面边缘完全贴合——人工标注时通常会留出几个像素的余量这是合法且常见的做法。2.2 批量读取与统计验证你的数据有没有硬伤拿到 363 个 XML 后第一步不是训练而是用脚本批量检查标注是否有问题。最常见的三类问题坐标超出图像边界、类别名称拼写错误比如把 queen 写成 quuen、某个类别完全没有样本。下面的脚本可以一次性完成统计import os import xml.etree.ElementTree as ET def parse_voc_labels(xml_dir): class_counts {} total_boxes 0 error_files [] for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 读取图像尺寸用于边界校验 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) for obj in root.findall(object): name obj.find(name).text class_counts[name] class_counts.get(name, 0) 1 total_boxes 1 box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) # 边界检查 if not (0 xmin xmax img_w and 0 ymin ymax img_h): error_files.append(xml_file) print(f总标注框数: {total_boxes}) print(f类别分布: {class_counts}) print(f越界文件: {error_files}) parse_voc_labels(path/to/xml_folder)这个脚本用xml.etree.ElementTree解析文件逐字段做类型转换和边界校验。类别统计的价值在于判断数据是否均衡——如果 king 只有 20 个框而 ten 有 120 个训练出来的模型对 king 的召回率大概率会偏低。越界检查指向的是标注操作失误这类文件如果不处理训练时会被某些框架直接丢弃或强制裁剪导致标注框与图像内容错位。2.3 坐标体系选择为什么不直接喂给 YOLOXML 里的xmin/ymin是绝对坐标YOLO 系列要求的是归一化的中心点坐标和宽高即cx/cy/w/h且每个值都在 0 到 1 之间。这是新手最容易翻车的转换点——直接把 XML 路径丢给 YOLO 训练脚本会报 AssertionError 数据错误。转换公式很简单def voc_to_yolo(size, box): dw 1.0 / size[0] dh 1.0 / size[1] x (box[0] box[1]) / 2.0 y (box[2] box[3]) / 2.0 w box[1] - box[0] h box[3] - box[2] return (round(x * dw, 6), round(y * dh, 6), round(w * dw, 6), round(h * dh, 6))size是(width, height)box是(xmin, xmax, ymin, ymax)。除以尺寸的目的是消除分辨率差异让不同尺寸的图像可以使用同一个网络输入。转换后的每行文本格式为class_id cx cy w h其中class_id从 0 开始按字母或自定义顺序映射——建议在数据集根目录固定一个classes.txt写入ace, jack, king, nine, queen, ten这样的顺序之后训练和推理都引用这个文件避免类别索引错位。3. 从 VOC 到 YOLOv8数据集重构与训练前的必做检查3.1 划分训练集和验证集比例与随机种子363 张图不算多按经验建议 8:2 或 9:1 划分。比例太小会导致验证集只有 30 多张图mAP 波动极大比例太大会让模型欠拟合。推荐的做法是固定随机种子做划分保证可复现python -c import os, random, shutil random.seed(42) images [f for f in os.listdir(images) if f.endswith(.jpg)] random.shuffle(images) split int(len(images) * 0.85) train_imgs images[:split] val_imgs images[split:] print(ftrain: {len(train_imgs)}, val: {len(val_imgs)}) random.seed(42)的作用是让每次运行得到的划分结果一致。85% 作为训练集15% 作为验证集363 张图会得到约 308 训练图和 55 验证图。此时需要同步把对应的 XML 标注复制到训练和验证的 label 目录否则训练时会因为找不到标注抛 FileNotFoundError。3.2 目录结构与 YAML 配置文件YOLOv8 训练前需要把数据集整理成固定目录结构。常见做法如下poker_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── poker.yamlpoker.yaml文件内容path: /absolute/path/to/poker_dataset train: images/train val: images/val names: 0: ace 1: jack 2: king 3: nine 4: queen 5: ten这里的path必须填写绝对路径train和val是相对path的路径。names索引必须和转换后的class_id一一对应。如果路径写错训练可以直接用model.val()验证阶段暴露问题——加载图片失败的错误日志通常指向这个文件。3.3 训练启动与关键超参数选择安装 ultralytics 包后一条命令即可启动训练yolo detect train datapoker.yaml modelyolov8n.pt epochs80 imgsz640 batch16 patience10 projectpoker_run参数说明modelyolov8n.pt使用 nano 规模预训练权重适合 363 张图的小数据集。如果直接用yolov8m.pt或更大的模型参数量过大训练数据不足会导致严重过拟合。epochs80小数据集训练轮次不宜过少。20 轮可能还没有收敛80 轮配合早停可以得到相对稳定的权重。imgsz640数据集图片原始尺寸如果小于 640训练时会被自动缩放填充对扑克牌这类目标较大的场景影响不大。batch16根据显存调整8GB 显存建议降到 8否则容易 OOM。patience10连续 10 个 epoch 验证集 mAP 不提升时自动停止防止无效训练时间。训练过程中重点关注val/box_loss和metrics/mAP50-95两个指标。box_loss持续下降是正常收敛信号mAP50-95在扑克牌这种单类别重叠少的任务上训练结束应该能到 0.85 以上。如果发现训练集 loss 下降但验证集 mAP 不动大概率是标注框本身有偏差或者是类别样本极度不均衡需要回看 2.2 节的统计结果。3.4 训练后验证集可视化检查训练完成后用验证集推理并保存可视化结果yolo detect predict modelruns/detect/train/weights/best.pt sourcedatasets/poker_dataset/images/val save_txtTrue save_confTruesave_txtTrue会额外输出每张图的检测结果文本包含类别、置信度和归一化坐标。建议抽几十张验证图人工和原图对比确认模型没有出现类别混淆——比如 ten 和 jack 因为印刷字体相似被误判。出现这种问题时最有效的手段不是盲目加训练轮数而是检查标注框是否把牌面文字部分完整包住以及是否需要裁剪图像只保留牌面区域减少背景干扰。4. 数据增强与类别不均衡处理让 363 张图发挥更大价值4.1 内置增强参数在训练中的实际作用YOLOv8 训练默认开启马赛克mosaic和随机仿射变换对小数据集是双刃剑。马赛克会将四张图拼成一张变相增加样本量但也可能让扑克牌目标被切割得只剩一半导致模型学到残缺特征。实际训练中常见做法是分层调整yolo detect train datapoker.yaml modelyolov8n.pt epochs80 imgsz640 batch16 hsv_h0.015 hsv_s0.7 hsv_v0.4 degrees15 fliplr0.5 mosaic0.8 close_mosaic10degrees15扑克牌允许小幅旋转15 度以内的旋转增强不会破坏牌面语义。fliplr0.5水平翻转是安全的扑克牌不存在左右语义差异。mosaic0.8降低马赛克概率到 80%避免过度拼接。close_mosaic10表示最后 10 个 epoch 关闭马赛克让模型在接近真实分布的数据上微调收敛这个技巧可以显著减少训练和验证之间的分布漂移。4.2 类别权重太偏时怎么处理如果 3.1 节统计发现某类别只有其他类别的一半甚至更少直接训练往往会导致少数类被多数类压制。处理方法有三种。第一种是调整损失权重Ultralytics 提供cls参数控制类别损失权重但它是全局参数不能对单独类别设权重更实用的是直接对少数类做离线复制增强比如把包含 king 牌面的图像复制两份通过平移 20 像素或旋转 10 度生成新样本同时更新对应 XML 坐标。4.3 仿射变换增强时标注框同步更新的代码思路手工用 labelimg 补样本太慢建议用脚本批量处理。下面是一个基于 OpenCV 的平移增强示例它会同步计算新坐标并生成新的 YOLO 格式标签import cv2 import numpy as np def augment_image(img_path, label_path, shift_px30): img cv2.imread(img_path) h, w img.shape[:2] # 平移矩阵 M np.float32([[1, 0, shift_px], [0, 1, 0]]) new_img cv2.warpAffine(img, M, (w, h)) # 读取YOLO标签并修正中心点x new_lines [] with open(label_path) as f: for line in f: parts line.strip().split() cls, cx, cy, bw, bh parts cx float(cx) shift_px / w new_lines.append(f{cls} {cx:.6f} {cy} {bw} {bh}\n) return new_img, new_lines这里的关键在于cx的修正必须在归一化坐标系里做shift_px / w把像素偏移量除以图像宽度得到归一化偏移。平移增强虽然简单但对扑克牌这种场景很有效——模型能学到目标在图像不同位置时的特征响应而不是只关注中心区域。4.4 背景干扰与多目标重叠的针对性处理扑克牌数据集中常见的重叠场景是两张牌叠在一起一张牌盖住另一张牌的一角。这类样本对检测器的挑战在于边界框会包含被遮挡的部分如果标注框把不可见的牌面区域也圈进来模型就会学到错误特征。针对这个问题比较实用的策略是在训练时开启iou0.7以上的 NMS 阈值推理时过滤掉重叠度过高的预测框同时检查标注数据里是否存在大面积遮挡但依然标注完整框的情况如果出现建议调整为仅标注可见部分。真实的扑克牌识别系统里宁可让不可见的牌不被检测也不能产生误检——因为下游的牌型判断依赖的是置信度最高的若干张牌多检一个错误类别会直接导致逻辑错误。5. 用 Ultralytics 封装好的推理脚本快速验证单张牌面识别5.1 单张图片推理与输出解析训练完成后最常见的落地场景是输入一张摄像头拍摄的牌面图像输出六类牌各自的置信度。直接用 Python API 比命令行更灵活from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) result model.predict(test_card.jpg, conf0.35, iou0.5, devicecpu)[0] for box in result.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) name model.names[cls_id] x1, y1, x2, y2 map(int, box.xyxy[0]) print(f检测到 {name}, 置信度 {conf:.2f}, 坐标 ({x1},{y1})-({x2},{y2}))conf0.35是置信度阈值低于这个值的预测会被丢弃。扑克牌这类目标清晰的任务0.35 已经能过滤掉绝大多数误检如果出现漏检可以降到 0.25。devicecpu指定推理设备无 GPU 时用 CPU每张图大约耗时 100~200 毫秒对这个场景完全够用。5.2 批量推理并统计每张牌出现次数如果需要做一局牌的计牌逻辑可以写一个批量脚本from ultralytics import YOLO import os model YOLO(runs/detect/train/weights/best.pt) output_stats {} for img_file in os.listdir(test_imgs): if not img_file.endswith(.jpg): continue result model.predict(os.path.join(test_imgs, img_file), conf0.35)[0] for box in result.boxes: name model.names[int(box.cls[0])] output_stats[name] output_stats.get(name, 0) 1 print(output_stats)这里统计的是所有图像中各类牌出现的总次数。实际部署时要注意同一张图像里多张相同牌会被分别计数这和扑克牌游戏里一副牌的数量约束比如一副牌只有 4 张 ten结合就能做简单的牌面合法性校验——如果某张牌出现超过 4 次说明检测结果大概率存在误报。5.3 模型推理时的坐标换算陷阱box.xyxy[0]返回的是原始图像分辨率下的像素坐标不是训练时的imgsz坐标。Ultralytics 推理时内部会自动缩放图像但输出坐标已经映射回原图尺寸不需要手动换算。这个设计对做牌面裁剪非常方便——可以直接用坐标从原图切出牌面区域保存成独立小图片用于后续的 OCR 识别点数或记录牌局状态。切图时要注意边界裁剪防止坐标越界x1, y1, x2, y2 map(int, box.xyxy[0]) x1, y1 max(0, x1), max(0, y1) x2, y2 min(img_width, x2), min(img_height, y2) card_img original_img[y1:y2, x1:x2]6. 从 MPlayer 到多路视频流扩大数据集到实时识别场景这里需要纠正一个输入中的无关信息MPlayer 与本数据集无关但 Video 全流程存在真实需求因此下面从视频流的逐帧处理角度切入补充一个可直接落地的实时检测方案。6.1 视频逐帧推理的帧率瓶颈扑克牌识别如果从单张图片扩展到摄像头视频流首先遇到的瓶颈是逐帧推理的速度。YOLOv8n 在 CPU 上单帧大约 100~200 毫秒只能达到 5~10 FPS在 NVIDIA 显卡上可以到 40~80 FPS。实际项目里如果只有 CPU建议把推理图像缩放到 320 或 416 分辨率并把conf阈值适当提高到 0.4减少候选框数量以此换取帧率。6.2 OpenCV 读取视频流的接入方式import cv2 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) cap cv2.VideoCapture(0) # 摄像头 while cap.isOpened(): ret, frame cap.read() if not ret: break # 每帧推理 results model.predict(frame, imgsz640, conf0.35, verboseFalse) # 画框 annotated results[0].plot() cv2.imshow(poker_detection, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()verboseFalse是为了关闭每次推理的日志输出否则终端会被刷屏。waitKey(1)里的参数是等待毫秒数1 表示每帧等待 1 毫秒加上推理耗时整体帧率由最慢的环节决定。6.3 用视频生成半自动标注反哺数据集视频场景里还有一个很实用的技巧用已经训练好的模型对视频帧做预标注再人工修正。这个过程可以大幅降低扩展数据集的标注成本。步骤很简单抽帧视频把抽出的图片输入模型保存带坐标的预测结果然后使用 labelimg 打开这些图片和预标注文件只需要移动或删除错误的框不需要从零画框。下面的代码把每 30 帧保存一帧图像import cv2, os cap cv2.VideoCapture(poker_game.mp4) frame_count 0 save_count 0 os.makedirs(candidate_frames, exist_okTrue) while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_count % 30 0: cv2.imwrite(fcandidate_frames/frame_{save_count:04d}.jpg, frame) save_count 1 frame_count 1抽帧间隔 30 适用于 30 FPS 视频即每秒取一帧。如果牌局动作很快抽帧间隔应缩小到 10否则会漏掉牌面翻转的短暂瞬间导致标注样本缺失关键姿态。预标注输出直接保存成 YOLO 格式之后再用 labelimg 打开时需要把「预设标签加载目录」指向对应的类别文件才能正常修正类别名称。本文还有配套的精品资源点击获取
返回列表