ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

扑克牌识别数据集实战:从COCO JSON转换到YOLOv8训练与部署

2026/9/28 5:53:46 拓冰建站 浏览量
扑克牌识别数据集实战:从COCO JSON转换到YOLOv8训练与部署 简介扑克牌识别数据集是面向计算机视觉入门与进阶开发者的目标检测训练资源覆盖A到K全部牌面字母正确识别率可达98.7%。整个压缩包共1853个文件包含1850张jpg格式原始扑克牌图片和3个json标注文件包体大小约110.21MBjpg图像提供了不同拍摄角度与光照条件下的牌面样本json文件则按COCO格式记录每张图的类别与边界框信息便于加载到YOLO、MMDetection等主流框架直接开展训练与评估。对于需要快速构建扑克牌识别系统的工程师这套数据免去了自行采集和标注的繁琐流程既可用于模型精度基线测试也可作为数据增强、格式转换或调参实验的基础。目前已有259人学习下载适合希望以规范化标注数据提升项目效率的开发者是一套标注完整、精度有保障的实用数据集。1. 扑克牌识别数据集到底解决什么问题一张扑克牌放到摄像头前模型要立刻说出这是A、K还是Q听起来是入门级目标检测可真去搭数据时你会发现市面上的数据集要么类别不全要么标注格式七零八落。这套扑克牌识别数据集的特殊之处在于它用1850张原始图覆盖A到K全部13个牌面等级标注做成COCO JSON标称正确识别率98.7%。它能解决的问题很直接省掉从零拍照、标注、格式整理的苦力活让你直接跳到模型训练与效果验证。适合正在做棋牌识别、桌面感知或者想练习COCO JSON转YOLO格式的工程师和数据标注同学。按后面的步骤走你不需要依赖作者原稿也能把整套流程在本地复现清楚。2. 从COCO JSON标注看懂“A-K”扑克牌数据集COCO JSON本身不复杂但很多人拿到手先解压看图片再用文本编辑器打开JSON看几行就晕。这里我按“找牌、看框、查分布”三步拆开帮你把这个数据集的内在结构摸透。2.1 COCO JSON五个关键字段先找到“牌在哪、牌是什么”一个完整的COCO JSON里通常有info、licenses、images、annotations、categories五块。训练真正依赖的是后三块images保存每张图的id、file_name、width、heightannotations保存每一个标注实例用image_id指向对应图片用bbox给出左上角坐标和宽高用category_id指向类别categories则把id映射成“A”“K”这样的名字。很多扑克牌数据集还会带segmentation但目标检测用不到可以先不管。拿到数据后我习惯先跑一遍解析脚本确认类别数和实例数是不是符合预期import json from collections import Counter with open(annotations.json, r, encodingutf-8) as f: coco json.load(f) cat_id_to_name {c[id]: c[name] for c in coco[categories]} print(categories:, cat_id_to_name) anns coco[annotations] print(total images:, len(coco[images])) print(total instances:, len(anns)) cls_counter Counter(cat_id_to_name[ann[category_id]] for ann in anns) for name, cnt in sorted(cls_counter.items(), keylambda x: x[1], reverseTrue): print(f{name}: {cnt})这段代码能回答两个关键问题类别是否覆盖A-K共13个等级每类实例数是否均衡。注意“1850张原始图”不直接等于“1850个样本”——如果每张图里有2到3张牌实例数可能接近4000甚至更多。如果某个类别只有几十个实例比如“10”特别少那训练时就要加大这个类的权重或做针对性增强。再检查一下COCO的bbox形态。它的格式是[x, y, width, height]x、y是框左上角在原始图像中的像素坐标不是中心点也不是右下角。很多人转YOLO格式出错根源就在这个坐标定义上。转换之前最好遍历一遍所有框看看有没有宽高为0或者越界的异常值img_map {im[id]: im for im in coco[images]} bad 0 for ann in anns: x, y, w, h ann[bbox] im img_map[ann[image_id]] if w 0 or h 0 or x w im[width] or y h im[height]: bad 1 print(bad bbox:, ann[id], ann[bbox]) print(bad count:, bad)扑克牌识别数据集如果是在统一环境下采集的标注通常比较干净但二手数据往往有边界框贴边的情况早发现早处理别等到训练报错再回头找。2.2 用脚本可视化COCO标注别信统计表信画框统计表只能告诉你“有没有”画框才能告诉你“标得准不准”。我每次拿到新数据集都会先把前几十张图的可视化结果导出来一张张看框有没有包住整张牌、有没有把牌面字母切开、有没有把牌边当成背景。可视化脚本不复杂import json import os import cv2 with open(annotations.json, r, encodingutf-8) as f: coco json.load(f) img_info {im[id]: im for im in coco[images]} cat_name {c[id]: c[name] for c in coco[categories]} anns coco[annotations] os.makedirs(vis, exist_okTrue) for ann in anns[:60]: im img_info[ann[image_id]] path os.path.join(images, im[file_name]) img cv2.imread(path) if img is None: continue x, y, w, h [int(v) for v in ann[bbox]] cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2) label cat_name[ann[category_id]] cv2.putText(img, label, (x, max(0, y - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imwrite(fvis/{ann[id]}.jpg, img)注意这里直接用cat_name从category_id取名字如果JSON里category_id不连续也不会报错因为字典查找天然安全。画框之后重点看三类问题框是否过松把相邻两张牌的边缘都包进去框是否过紧裁掉了牌面外缘框是否标错了类别比如把J标成I。即使数据集的标称正确率很高也不代表每一个框都完美你的模型最终学的是标注所表达的目标区域。如果不想写代码用CVAT或Labelme这类数据标注工具打开JSON也能可视化但脚本更灵活可以批量跑还能顺带统计框面积占比。我一般会在脚本里加一行输出把所有框的最小面积打出来因为扑克牌在1080p大图里往往只占一小块区域下采样到640后牌面字符可能只有十几像素这时候小目标增强就很有必要。2.3 检查类别与实例分布1850张图不等于1850个样本假设这套数据集覆盖A、2到10、J、Q、K共13个类别那么每一类平均至少要有130多张牌面实例才支撑得起13个类别的检测任务。如果某张图同时出现3张牌那实例总数会比1850大不少。这里最关键的是类别平衡如果“Q”只有几十张而“7”有三百张模型会天然偏向“7”导致Q的漏检率偏高。处理办法是给稀有类别增加样本或者在训练时使用类别权重。还要看图像尺寸分布。扑克牌识别数据集如果统一为1920x1080训练时直接resize到640会导致牌面长宽比变化虽然YOLO对长宽比不敏感但过度的非等比缩放会影响小字符的清晰度。可以统计所有图像的长宽比再做一次中心裁剪或固定resize。常见做法是保持原始比例用640x640的灰色边填充或直接拉伸但适当提高imgsz到768。这里把COCO JSON、YOLO TXT、VOC XML三种常见标注格式放在一起对比能很清楚看到为什么转换时要小心特性COCO JSONYOLO TXTVOC XMLbbox表示[x, y, w, h] 像素坐标[cx, cy, w, h] 归一化[xmin, ymin, xmax, ymax]类别id可以不连续必须从0开始连续用字符串名称解析方式json.load每行splitxml.etree典型风险类别映射易错归一化截断易忘坐标方向易反这套数据集既然是COCO JSON标注转YOLO时就要把类别id重新映射到0到12同时把像素坐标转换成归一化中心坐标。后面第三章会写完整转换脚本这里先把原理立住COCO的category_id是数据标注时的人为编号YOLO的class id是训练时的连续编号两者没有天然对应关系。3. 把COCO JSON喂给YOLOv8数据划分、格式转换与训练参数拿到COCO JSON后最快跑通的路径是转成YOLO格式再用YOLOv8训练。这节我按顺序来先划分数据集再转换格式然后给训练命令和参数选择逻辑。3.1 按图像划分数据集一张牌不能同时出现在训练和验证集很多人划分数据时直接随机打乱标注框把同一张原始图里的不同牌分别划进训练集和验证集这是最常见的翻车方式。扑克牌检测数据里的牌往往是多张同框如果按框划分模型在验证时见到的背景纹理和牌面组合已经在训练时瞄过一眼指标虚高现场部署立刻露馅。正确做法是按image_id整图划分。我习惯用80/10/10的比例数据量小的时候可以改成85/15但至少要留出一个完全独立的test子集import json import os import random import shutil random.seed(42) src_images images dest_root poker_split with open(annotations.json, r, encodingutf-8) as f: coco json.load(f) image_ids [im[id] for im in coco[images]] random.shuffle(image_ids) n len(image_ids) train_ids set(image_ids[:int(n * 0.8)]) val_ids set(image_ids[int(n * 0.8):int(n * 0.9)]) test_ids set(image_ids[int(n * 0.9):]) for split_name, ids in [(train, train_ids), (val, val_ids), (test, test_ids)]: split_dir os.path.join(dest_root, split_name, images) os.makedirs(split_dir, exist_okTrue) for im in coco[images]: if im[id] in ids: shutil.copy(os.path.join(src_images, im[file_name]), split_dir)这段代码会生成三个目录poker_split/train/images、poker_split/val/images、poker_split/test/images。注意检查是否有图片文件名重复但id不同的情况如果同张图有两个id上面按id划分就会把同一文件拷到不同集合。稳妥做法是先按哈希值去重再划分。划分完还要把COCO JSON同步拆成三份这样后续转YOLO时不用反复读原文件for split_name, ids in [(train, train_ids), (val, val_ids), (test, test_ids)]: split_anns { images: [im for im in coco[images] if im[id] in ids], annotations: [a for a in coco[annotations] if a[image_id] in ids], categories: coco[categories], } out_path os.path.join(dest_root, split_name, annotations.json) with open(out_path, w, encodingutf-8) as f: json.dump(split_anns, f)划分比例不是死的。如果原始数据里同一个场景拍了好几帧这种划分方式下验证集里仍然可能出现与训练集高度相似的连续帧所以更严格的验证应该按“拍摄批次”划分——把同一批次的所有图像放进同一个集合。这一条我在第五章的坑5里会再展开。3.2 COCO JSON转YOLO TXT一行归一化三个细节要命YOLO训练需要的标注是一张图对应一个同名txt每行格式为class_id center_x center_y width height其中坐标和宽高都相对图像宽高归一化到0到1之间。转换代码不长但有三个细节会让人翻车类别id必须从0连续、坐标必须归一化、越界框必须先截断。import json import os def coco_to_yolo(labels_dir, json_path): os.makedirs(labels_dir, exist_okTrue) with open(json_path, r, encodingutf-8) as f: coco json.load(f) img_map {im[id]: im for im in coco[images]} cat_map {c[id]: i for i, c in enumerate(coco[categories])} for ann in coco[annotations]: im img_map[ann[image_id]] img_w, img_h im[width], im[height] x, y, w, h ann[bbox] # 截断越界框避免训练时出现空框或负宽高 x max(0, min(x, img_w - 1)) y max(0, min(y, img_h - 1)) w max(1, min(w, img_w - x)) h max(1, min(h, img_h - y)) cx (x w / 2) / img_w cy (y h / 2) / img_h nw w / img_w nh h / img_h label cat_map[ann[category_id]] txt_name im[file_name].rsplit(., 1)[0] .txt txt_path os.path.join(labels_dir, txt_name) with open(txt_path, a, encodingutf-8) as f: f.write(f{label} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n)为什么cat_map要用enumerate而不是直接用ann[category_id]因为COCO的category_id可能从1开始也可能跳号而YOLO要求类别从0开始连续递增。如果直接沿用A会变成1而不是0所有类别整体偏移一位训练出来的模型会把A识别成2。转换后抽看几个txt文件第一行第一个数字应该在0到12之间如果出现13说明映射没做对。截断越界框也很关键。标注人员习惯把框贴着牌边缘画偶尔会让x w超出图像宽度一个像素训练时图像增强一做这个像素误差会被放大成负面积框直接报all boxes are zero。转换脚本里的max和min就是在出厂前把这个问题修掉。3.3 用YOLOv8训练扑克牌识别模型命令与参数选择转好格式后先写一个data.yaml描述数据集路径和类别名这是YOLO训练的统一入口path: poker_split train: train/images val: val/images test: test/images nc: 13 names: 0: A 1: 2 2: 3 3: 4 4: 5 5: 6 6: 7 7: 8 8: 9 9: 10 10: J 11: Q 12: K注意names的顺序必须和转换脚本里cat_map的枚举顺序一致。如果原数据把类别定义为“Ace”“King”这种英文全称建议统一成单字母省得后面画图时标签太长。然后启动训练。我一般用YOLOv8n起步因为这套数据量不算大大模型更容易过拟合yolo detect train \ datapoker_split/data.yaml \ modelyolov8n.pt \ epochs200 \ batch32 \ imgsz640 \ patience20 \ projectruns/poker \ nameexp1 \ device0几个参数的选择逻辑如下参数我常用的值理由modelyolov8n.pt参数少1850张图级别的小数据更稳epochs200让学习率充分衰减配合早停不浪费时间batch32或16显存允许时尽量大BN统计更稳定imgsz640平衡牌面字符清晰度和显存占用patience20验证集连续20轮不提升就停防过拟合如果显存只有8Gbatch就降到16如果牌面字符在图中特别小可以考虑imgsz768代价是训练和推理时间变长。不要一上来就调学习率YOLOv8默认的lr00.01在这个量级的数据集上通常够用。真正需要关注的是过拟合而不是收敛太慢。3.4 训练过程的监控点loss曲线与早停训练开始后看runs/poker/exp1/results.png里的box_loss、cls_loss、dfl_loss三条曲线。正常情况前30轮下降很快之后进入平台期。如果训练loss还在降但验证集mAP开始掉头就是过拟合前兆。这时不用手动断patience20会替你早停。另外要看训练集和验证集的类别映射是否一致。有的数据集names写成[10,2,3,4,...]顺序和转换脚本不一致导致训练时A的标签学到的实际是10。检查方法很简单训练结束后打开runs/poker/exp1/confusion_matrix.png看对角线是否足够亮。如果对角线外出现明显的规则偏移基本可以断定是标签映射错了。4. 98.7%正确率是怎么算出来的评估口径与复现验证标题里的“正确识别率98.7%”看起来是一个很确定的数字但在目标检测里“正确识别率”这个说法很模糊。复现之前先搞清楚它可能的统计口径否则你会发现怎么调都到不了那个数还以为是代码写错了。4.1 目标检测的正确率不是分类准确率先分清mAP、AP50和“牌识对率”如果问题只是“给一张裁好的牌面图片输出它属于哪一张”那正确率可以简单算成分类准确率。但这套数据集是COCO JSON标注意味着它要解决的是目标检测问题先找牌在哪再判断是什么牌。目标检测的常用指标是mAP其中AP50表示IoU阈值取0.5时的平均精度map50-95是IoU从0.5到0.95按0.05步长取平均。同样一个模型AP50可能到0.98map50-95可能只有0.75。很多人把“正确识别率”理解为“预测类别正确且IoU大于0.5的样本比例”这其实接近AP50而不是严格的COCO mAP。所以复现时先看验证集输出里map50这一列如果它接近0.987说明你和数据集的报告口径大概率一致。如果报告写的是map50-95那复现难度就大很多因为定位稍微偏一点都会扣分。还可以用YOLO训练过程自动生成的CSV文件直接读取最后一个epoch的验证指标import pandas as pd df pd.read_csv(runs/poker/exp1/results.csv) last_row df.iloc[-1] print(mAP50:, last_row[val/box_map50]) print(mAP50-95:, last_row[val/box_map50-95])results.csv里每个epoch一行最后的val/box_map50就是早停前的AP50。如果训练早停发生CSV最后一行可能是最后一个可用的验证指标而不是最优指标。严格来说应该从best.pt重新跑一次yolo detect val拿到的最佳指标在输出日志里。4.2 用CLI跑验证集conf、iou阈值如何影响数字训练完成后用官方验证命令对自己的权重做独立评估yolo detect val \ datapoker_split/data.yaml \ modelruns/poker/exp1/weights/best.pt \ conf0.25 \ iou0.5conf是置信度阈值低于0.25的预测会被丢弃iou是NMS用的IoU阈值。这两个参数会直接影响最终指标conf调高precision上升、recall下降mAP通常也会变iou调高会保留更多重叠框mAP50-95可能变化。所以复现98.7%时要先确认你用的conf和iou是否和数据标注者报告时用的一致。常见做法是先在conf0.25下跑一遍记下所有类别各自的AP50。再写一个简单脚本统计“检测框和真值框IoU0.5且类别一致”的样本占比这个占比更接近口语里的“正确识别率”。但要注意一张图里多张牌同时出现时漏检和重复检测都会影响这个比例光看一个数字容易误判。4.3 画出混淆矩阵找到A-K里的“双胞胎牌”YOLOv8训练结束后会在权重目录下生成confusion_matrix.png。这张图能直观显示类别之间怎么互相混淆。扑克牌识别里最常见的混淆是“6”和“9”旋转180度后互换、“5”和“S”但这里没有S、“Q”和“O”也没有O更实际的是“A”和“4”“J”和“1”。看混淆矩阵时别只看对角线要看对角线之外的亮点。如果“6”那一行里“9”的格子特别亮说明模型总是把6认成9。处理办法有两个给训练数据增加轻度旋转增强让模型学会“牌是歪着放也能认”或者在推理时保留原始方向不做180度随机翻转。我个人的习惯是训练时只开degrees10加fliplr0.5这样能覆盖桌面摆放的角度偏差又不会把6和9强行教成同一类。如果你想把“正确识别率”复现得更严谨建议自己搭一个推理评估循环加载best.pt对验证集每张图做预测用torchvision.ops.box_iou把预测框和真值框一一匹配匹配成功的再比较类别是否一致。这个循环能帮你计算任意置信度下的准确率还能画出PR曲线比只信任CLI输出更能理解模型行为。5. 复现扑克牌识别模型的5个避坑记录这一章来自我实际跑这类数据集的踩坑记录。每一条都按“现象→原因→解决”写你可以对照自己的日志来排查。5.1 坑1类别编号从1开始导致所有牌面标签串位现象训练完成后推理时把A识别成2把2识别成3整体偏了一位。但验证集mAP却显示正常因为验证集也用了同样错误的标注映射。原因COCO JSON里的category_id通常从1开始命名而YOLO格式要求类别从0开始连续。转换脚本里直接写了ann[category_id]没有重新映射导致所有类别整体偏移。解决转换时用cat_map {c[id]: i for i, c in enumerate(coco[categories])}并且在转换后随机检查10个txt文件。看每行第一个数字是否都落在[0, 12]区间。如果看到数字13立刻回查映射逻辑。5.2 坑2bbox越界训练时出现“all boxes are zero”现象训练到第几十轮时突然报错日志提示all boxes are zeroloss变成NaN。重启训练后问题又随机出现。原因某些标注框的x width超出了图像宽度一两个像素或y height超出图像高度。YOLO的Mosaic增强会把这些框随机裁剪一旦裁剪区域落到越界的部分就生成宽度或高度为0的框。解决在COCO转YOLO的脚本里加截断处理把x、y限制在图像范围内把w、h的最小值设为1。同时转换前跑一遍异常检测脚本统计坏框数量。如果坏框超过总标注数的1%建议回炉修标注而不是靠截断硬抗。5.3 坑3按标注框而不是按图像划分验证集严重泄露现象训练集mAP0.95验证集mAP0.93看起来没问题换到现场摄像头却漏检一半尤其多张牌叠放时。原因划分数据集时按标注框随机切分同一张图里有3张牌可能1张进了训练集、2张进了验证集。模型在读验证集时看到的背景纹理和牌面组合已经在训练时出现过相当于提前泄露了答案。解决严格按image_id整图划分并做一次泄露检查把两个集合的image_id求交集如果交集不为空就是泄露。代码很简单if len(train_ids val_ids) 0: print(leak:, len(train_ids val_ids))对于同一场景连续拍摄的帧建议按批次ID合并后再划分否则验证集只是“同场景重采样”不是真正的新场景。5.4 坑4背景单一导致模型学到“桌面”而不是“牌”现象训练集里全是同一张绿色桌面AP50到了0.98换到深色木质桌面直接漏检甚至把桌面的木纹纹理当成牌。原因1850张图如果都来自同一个拍摄环境模型的低层特征会学习“这块绿色区域就是桌面桌面上的白色矩形就是牌”而不是真正学习牌的边缘纹理和字体特征。COCO JSON标注只是把目标框出来没有强制模型关注牌面内部特征。解决给训练集补充不同背景、不同光照的现场图片这是最有效的方案。如果暂时没有新数据就在训练参数里加大颜色抖动和模糊增强hsv_h0.015 hsv_s0.5 hsv_v0.4同时开启scale0.5和fliplr0.5。但增强只能缓解不能替代真实场景多样性。5.5 坑5验证集来自同一批原始图离线指标虚高但现场翻车现象离线验证mAP50稳定在0.98部署到摄像头后只有0.85而且误检很多。原因验证集是从1850张图里随机抽取的但原始图可能是同一时间、同一相机、同一环境拍下来的光线和角度的变化范围很小。随机抽取只能保证统计随机不能保证场景多样。模型在分布内的数据上表现好一遇到分布外的桌面纹理、反光、模糊就崩。解决单独留出一个“完全独立拍摄”的test set最好换相机、换分辨率、换桌面、换光照。如果组织不了新拍摄至少按拍摄批次划分给每一批连续拍摄的图一个场景ID划分数据集时保证同一个场景ID只进一个集合。这会让你的离线指标“变差”但这个变差才是真实水平。6. 让模型在真实牌桌上站稳旋转鲁棒性与推理加速6.1 给训练补上旋转增强但别转成“倒牌”扑克牌在桌面上不会总是正对着镜头实际使用经常带一点角度。给训练加旋转增强能提升鲁棒性但角度范围要克制。牌面字母“6”旋转180度就成了“9”如果训练时开满随机旋转模型会被迫把同一张牌的正反两个方向都当作同一个类别最终要么两头都学不精要么置信度被拉低。我一般只开degrees10左右再配fliplr0.5做水平翻转就能覆盖手持拍摄的角度抖动。如果你的场景真有“牌倒扣在桌上”的情况那是另一类旋转不变识别问题需要单独在数据集里补充上下翻转样本并重新定义类别语义。6.2 导出ONNX/TensorRT把检测延迟压到几毫秒模型训练好后部署时我通常导出ONNX或TensorRT而不是直接跑pt权重。ONNX方便跨平台TensorRT在NVIDIA卡上能压到几毫秒。以YOLOv8为例yolo export modelruns/poker/exp1/weights/best.pt formatonnx dynamicFalse imgsz640 yolo export modelbest.pt formatengine device0 imgsz640第二条命令需要机器有TensorRT环境没有的话用ONNX Runtime也能跑只是延迟稍高。导出后一定要做精度回归在验证集上分别用pt和onnx推理50张图对比预测框的坐标和类别。坐标误差超过1像素就要警惕算子在当前硬件上做了意外优化类别不一致则说明精度损失过大考虑关掉FP16或INT8量化。我自己的教训是第一次把模型接到桌面摄像头时离线指标从98.7%掉到85%折腾半天才发现是训练数据全是正对镜头的高清大图现场却是俯拍的歪斜小图。后来把旋转和背景补强做进训练才算真正落地。数据集的数字再好看也只有经过独立测试集和真实场景双重考验才配得上“可用”两个字。希望帮到你。本文还有配套的精品资源点击获取