☰
扑克牌识别数据集:COCO转YOLO训练与98.7%准确率复现指南
2026/9/28 5:53:43 网站建设 项目流程

简介:该扑克牌识别数据集面向计算机视觉、目标检测及深度学习初学者,完整覆盖A至K所有扑克牌面字母,可用于训练扑克牌检测与识别模型,也可作为分类任务基准。数据集包含1850张原始jpg图像,均采用COCO json格式标注,提供类别标签、边界框坐标等标准信息,可直接导入YOLO、MMDetection等主流检测框架,无需额外转换。据描述,模型正确识别率可达98.7%,验证了数据标注质量与可用性。压缩包共1853个文件(1850个jpg图片、3个json标注文件),整体大小110.21MB,文件结构清晰,便于按批次训练与测试。数据集图像涵盖不同拍摄角度、光照条件和背景样式,有助于提升模型在真实场景中的泛化能力。目前已有259人学习下载,适合课程设计、算法实验或扑克牌相关应用开发。

1. 扑克牌识别数据集是什么:1850张原始图如何撑起A-K全类别识别

扑克牌识别数据集的核心卖点很直白:1850张真实牌面照片,A、2、3一直到K共13个类别都有独立标注,配套coco json格式的标注文件,标称正确识别率能达到98.7%。这个规模放在目标检测领域不算大,但扑克牌自身类别清晰、形变有限,只要标注一致、训练参数得当,小数据集反而比通用大场景更容易收敛到高正确率。

它解决的典型问题是桌面视觉计数、牌局过程复盘、魔术教学拆解这类场景里的“牌面状态”自动盘点。适合已经有yolo基础、但缺干净数据集的开发者拿来练手,也适合做自动发牌机和牌桌辅助系统的原型验证。

这组数据最容易被低估的是coco json标注带来的格式成本。接下来我按读json、转yolo、训练、复现指标、避坑、复核标注这条线展开,把命令和参数都摆出来,尽量让你能照着跑通,并且理解98.7%这个数字到底在什么条件下成立。

2. 拆解coco json标注:字段含义、类别映射与扑克牌bbox的统计体检

2.1 为什么扑克牌识别数据集偏偏用coco json标注

目标检测的标注格式五花八门,Pascal VOC用xml,yolo用txt,还有各类工具的自定义格式。常见做法是选coco json,原因不复杂:第一,coco json能同时承载检测框、实例分割多边形和关键点三种信息,扑克牌虽然是矩形物体,但牌面出现透视形变时标注员偶尔会画多边形,json格式两种都能装;第二,主流数据标注工具对它的支持最完整,cvat标注工具和labelme都能直接导出coco json,labelimg标注工具也可以通过脚本互转,省去手工改文件;第三,coco官方提供了pycocotools和统一的评估脚本,后面复现98.7%的识别率时不必自己从头写评估逻辑。

当数据集只有1850张原始图时,标注成本本身是可控的,所以更值得把精力花在格式规范化上。扑克牌这个物体的特点是:牌面是刚性平面,绝大多数场景下用一个水平检测框就能包住整张牌,不需要像素级分割;但A、K、Q、J这类牌面上的花体与角落数字辨识度差异大,标注一致性比格式本身更重要。也就是说,coco json在这里并不是因为它复杂才被选中,而是因为它在“类别、位置、图像尺寸、分割掩码”之间提供了一个结构化载体,后续无论转到yolo训练还是做切片推理,都有现成的解析路径。

2.2 coco json的五个核心字段:images、annotations、categories不能只看名字

拿到扑克牌数据集的标注文件,我一般先把它当普通json处理,用编辑器里的json格式化功能打开看结构。顶层固定是五个键:info、licenses、images、annotations、categories。对训练真正起作用的是后三个,info是版本信息,licenses是版权声明,可以忽略。

images数组里每个元素描述一张原始图,常用字段是id、file_name、width、height。annotations数组是核心,每个元素对应一个目标实例,最关键的是image_id、category_id、bbox、area、iscrowd。bbox写法是[x, y, width, height],x和y是框左上角在原始图上的绝对像素坐标,width和height是框宽高。area在检测任务里基本用不到,但分割标注中它会作为掩码面积被记录。iscrowd在扑克牌目标上通常全是0,只有遇到一堆牌叠在一起、无法逐张框时才会标成1,如果出现iscrowd=1,训练前要单独处理。

一张牌对应几条annotation?大多数情况下一个实例对应一条,包含一个外接框。一张图里有两张牌就有两条annotation;牌被转成背面而又需要识别花色时,就得按数据集说明决定是否单独建类。这里有个容易忽略的点:coco的category_id是自定的正整数,不要求从0开始,而yolo要求从0连续编号,转换脚本里必须显式做一次映射,否则后续训练成绩全错。很多人第一次跑这类数据集翻车就翻在这里。

2.3 读取coco json做体检:先看类别分布和bbox尺寸再谈训练

训练前我习惯写一段最短的python脚本,把coco json读进来,统计三类信息:各类别的标注条数、每张图的平均目标数、bbox相对图片的尺寸分布。这一步能提前暴露长尾类别、漏标和框画得异常的情况。

import json from collections import Counter, defaultdict with open('cards_coco/annotations/instances_train.json', 'r', encoding='utf-8') as f: data = json.load(f) # 顶层键名确认,防止拿到非标准coco文件 print(data.keys()) # 类别表映射:coco的category_id -> 牌面名称 cats = {c['id']: c['name'] for c in data['categories']} print(cats) # 每个类别的实例数 cls_cnt = Counter() # 每张图对应的目标数量 img_cnt = defaultdict(int) for ann in data['annotations']: cls_cnt[cats[ann['category_id']]] += 1 img_cnt[ann['image_id']] += 1 print('每个类别的实例数:', cls_cnt.most_common()) print('每张图目标数分布:', Counter(img_cnt.values()))

这段代码的逻辑很直白:加载json,用字典推导把category_id映射成牌面名称,遍历annotations同时统计类别和每张图的目标数。如果某个类别明显偏少,比如A只有80张而K有200张,说明数据集自带长尾,后面针对少数类做增强。如果每张图目标数基本都是1,说明1850张图主要是单牌照片,复现时就不要指望模型能稳定处理多张牌堆叠的实拍画面。

再看bbox尺寸分布,用来发现标注框是否异常:

import json import numpy as np with open('cards_coco/annotations/instances_train.json', 'r', encoding='utf-8') as f: data = json.load(f) img_size = {im['id']: (im['width'], im['height']) for im in data['images']} rel_w, rel_h = [], [] out_of_bound = [] for ann in data['annotations']: x, y, w, h = ann['bbox'] iw, ih = img_size[ann['image_id']] rel_w.append(w / iw) rel_h.append(h / ih) # 显式检查标注框是否越过图像边界 if x < 0 or y < 0 or x + w > iw or y + h > ih: out_of_bound.append(ann['id']) print('rel_w 百分位:', np.percentile(rel_w, [5, 50, 95])) print('rel_h 百分位:', np.percentile(rel_h, [5, 50, 95])) print('越界框数量:', len(out_of_bound))

大多数扑克牌照片中,单张牌的框宽高比接近牌的实际比例,约2.5:1到3.5:1。如果rel_w的中位数在0.2到0.6之间、rel_h在0.1到0.3之间,说明标注尺度正常。越界框一旦出现,yolo训练时不会报错,但会把背景噪声带进标签,复现准确率时被无端拖低。注意x、y可以等于0,但不能是负数,x+w也不能大于图片宽度,这是最基础的coco标注合法性标准。

注意:json文件用什么打开本身也是个常见问题。别用记事本直接编辑大json,会卡而且容易把编码改坏。用VS Code或Notepad++的json格式化功能看结构就够了,真正处理交给python脚本。

3. coco json转yolov8训练格式:转换脚本、data.yaml与必调参数

3.1 coco转yolo的最小脚本:归一化坐标和类别重映射是两件独立的事

yolov8训练自己的数据集时,默认不直接接收coco json,它要的标签是每张图对应一个同名txt文件,每行五个值:class x_center y_center width height,全部归一化到0和1之间。因此中间必须加一层转换。我每次处理类似数据集都保留coco json原文件不动,只生成一套新的txt标签,避免来回编辑把原始标注改坏。

import json import os def coco_to_yolo(json_path, out_dir): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) # coco类别id -> yolo类别id(从0连续编号) cat_id_map = {c['id']: i for i, c in enumerate(data['categories'])} img_map = {im['id']: im for im in data['images']} os.makedirs(out_dir, exist_ok=True) for im in data['images']: base = os.path.splitext(im['file_name'])[0] txt_path = os.path.join(out_dir, base + '.txt') lines = [] for ann in data['annotations']: if ann['image_id'] != im['id']: continue x, y, w, h = ann['bbox'] # bbox左上角 -> 中心坐标 cx = (x + w / 2) / im['width'] cy = (y + h / 2) / im['height'] nw = w / im['width'] nh = h / im['height'] lines.append(f"{cat_id_map[ann['category_id']]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") with open(txt_path, 'w', encoding='utf-8') as out: out.write('\n'.join(lines)) coco_to_yolo('cards_coco/annotations/instances_train.json', 'labels/train') coco_to_yolo('cards_coco/annotations/instances_val.json', 'labels/val')

这段脚本有三个细节值得较真。第一,类别映射不能直接拿category_id当class用,coco的id通常从1开始,而yolo的class必须从0开始连续编号,差一个序号就会导致所有类别整体错位。第二,归一化用的是框中心,不是左上角x/y,照搬VOC写法会生成偏移半张图的错误框。第三,txt文件名必须和图片文件名完全一致只换后缀,yolo通过stem匹配图片和标签,IMG_001.jpg对应IMG_001.txt,多一个空格都不行。

3.2 data.yaml与训练命令:六个影响收敛的参数

转换完标签,下一步组织目录结构并写data.yaml。常见做法是images和labels分开放,train和val各一份,在data.yaml里写相对路径,避免换机器后绝对路径失效。

path: ./cards_dataset train: images/train val: images/val nc: 13 names: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']

names顺序必须和上一步转换脚本里cat_id_map枚举的顺序完全一致。转换脚本是以categories数组为基准生成的,这里如果手写names时按字母A到K排序,而categories数组是另一个顺序,就会造成A变成K之类的混乱。稳妥的办法是把coco json里categories打印出来复制过去,不要凭印象写。

训练命令如下,建议先用yolov8n跑基线:

yolo detect train \ data=cards.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=15 \ augment=True \ project=runs/cards

六个参数按影响程度排一下。imgsz决定输入分辨率,扑克牌花体笔画细,640起步,显存足够就试960,分辨率对牌面边缘细节帮助明显。epochs在1850张图规模下120轮基本收敛,跑满300轮容易过拟合到背景纹理。batch与lr0联动,调大batch时lr0要跟着放大,否则收敛很慢。patience设为15能早停省时间,但不要设为0,那会让模型即使不再收敛也一直跑满全部epochs。augment默认开启Mosaic和色彩抖动,对扑克牌识别是把双刃剑,后面避坑章细说。

3.3 小数据集下的收敛策略:为什么这套参数能把准确率推上去

小数据集训练最怕的是“loss很低但测试很差”。98.7%级别的高正确率依赖三件事:类别均衡、增强温和、早停合理。在1850张图下,如果直接用yolo默认增强,Mosaic会频繁把四张牌拼成一张,模型见过更多组合,但也容易被拼贴的边界干扰单牌轮廓。我通常会把Mosaic概率降到0.5以下,把hsv饱和度增强幅度调低,因为扑克牌的红桃黑桃颜色差异是重要的类别线索,过度调色会让模型混淆红色与黑色的判断。

训练过程中盯住三条曲线:train/box_loss持续下降,val/box_loss同步下降,metrics/mAP50稳步上升。如果train_loss降而val_loss不降,说明过拟合,先降epochs或调大正则项。如果mAP50涨到0.95之后反复跳动,不必追求最后1%的涨幅,拿best.pt去跑实拍验证比继续调参更有意义。

另一个容易被忽略的地方是数据增强里的旋转角度。扑克牌识别中,旋转增强角度范围建议控制在正负20度以内。超过30度之后,水平框内会混入大量牌桌背景,模型学到的不再是“牌面特征”而是“框内纹理”,反而不利于真实桌面的任意角度识别。

4. 如何复现98.7%的识别准确率:测试集划分、指标口径与预测验证

4.1 先搞清楚98.7%说的是哪种准确率

拿到数据集先冷静一下:页面里写的98.7%,和训练日志里的mAP50不是同一个东西。目标检测的mAP50是把每个类别的PR曲线按50%IoU阈值平均,数值到0.9以上已算不错;而98.7%更像“整体识别正确率”或“类别正确率”,也就是预测框与真实框匹配后,类别正确的样本数占总样本数的比例。两种口径对数据和评估方式的要求完全不同。

所以第一步是先跑一次验证预测,打印类别层面的正确率,不要因为和页面宣传对不上就怀疑模型坏了。常规做法是把IoU阈值定在0.5,对每个预测框和真实框做匹配,只统计置信度高于0.35的框,计算正确分类的样本数除以总牌数。如果这个指标在验证集上能稳定到95%以上,说明数据集标注质量是过硬的,98.7%大概率是在特定划分和置信度阈值下测出来的。养成先看指标口径的习惯,能少走弯路。

4.2 划分测试集:按“牌面实例”而不是按“照片”随机化

朴素的随机划分在这里容易翻车。假设1850张图中,同一张牌在不同角度、不同光照下重复出现多次,随机划分会把同一张牌的多个视角同时分进train和val,模型在val上的成绩会被“记忆”抬高。真实桌面识别关心的是没见过的牌面,不是同一个A换个角度换个亮度再认一次。

建议划分的粒度是“牌面实例”而不是“照片”。如果数据集没有提供图像分组字段,一个可行近似是按采集批次或连续文件名前缀拆分,保证同一个批次只在train或只在val。另一种更稳的做法是KFold交叉验证,折数建议5,每个fold单独训练并上报平均准确率,这样复现出来的数字才有说服力。

4.3 用best.pt跑预测并统计单牌正确率:一段能落地的评估脚本

用yolov8的python接口做一次类目级评估,比看训练曲线直观得多。

from ultralytics import YOLO import os, json model = YOLO('runs/cards/weights/best.pt') # 读取val集的coco json,按image_id收集真实类别 gt_by_img = {} with open('cards_coco/annotations/instances_val.json', encoding='utf-8') as f: data = json.load(f) cats = {c['id']: c['name'] for c in data['categories']} for ann in data['annotations']: gt_by_img.setdefault(ann['image_id'], []).append(cats[ann['category_id']]) img_paths = {im['id']: im['file_name'] for im in data['images']} correct = 0 total = 0 for img_id, gts in gt_by_img.items(): path = os.path.join('cards_dataset/images/val', img_paths[img_id]) res = model(path, conf=0.35, iou=0.45, verbose=False)[0] # 只取类别名,忽略空框 preds = [model.names[int(b.cls[0])] for b in res.boxes] # 按整张图片的类别集合比对,忽略数量差异 correct += len(set(preds) & set(gts)) total += len(gts) print(f'类别正确率: {correct}/{total} = {correct/total:.3f}')

这段脚本有意做得简单,只统计每张图里预测类别集合和真实类别集合的交集,没有做逐框IoU匹配。真正要较真精度时,需要用IoU把预测框映射到真实框上,再看类别是否一致,否则会出现“预测出一个真值里没有的K”但照样计入正确的情况。做基线评估时这个简化够用,因为先回答的是“类别方向对不对”,定位误差的具体来源留给后续分析。

跑完这步,基本就能判断98.7%能否复现。如果类别正确率明显低于98.7%,先查两件事:一是coco json里category_id和yolo class的映射是否错位,二是少数类样本是否过少。这两个因素的排查方法写在下一章。

5. 扑克牌数据集避坑记录:类别错位、长尾样本与旋转牌面的血泪教训

5.1 类别ID错位:明明是A,预测出来却是K

现象:训练结束后mAP50很高,但可视化预测时发现模型把A牌整体识别成K或2,错得非常均匀。原因:转换脚本的cat_id_map和data.yaml的names顺序不一致。比如转换脚本用enumerate后category_id映射到class 0对应A,而data.yaml里names写成了['K', 'Q', ...],yolo的class 0被分配给了另一个类别。这个错误在训练日志里完全不报错,最容易发现的方式是混淆矩阵对角线整体偏移。

解决:把coco json里categories数组的原始顺序打印出来,直接复制到data.yaml的names。我的习惯是在转换脚本里多打印一行映射表,写完data.yaml后再读回来和映射表做一次断言,两者完全一致才开始训练。用python脚本生成data.yaml比手写更不容易出错。

注意:不要用“看起来顺眼”的排序去重写names。扑克牌数据集的categories可能是按牌面出现顺序录入的,和字母表顺序无关,一切以json内容为准。

5.2 长尾样本:K、Q、J的识别率为什么总垫底

现象:整体mAP在0.98左右,单独统计每个类别的准确率,发现A、2、3这些数字牌很高,K、Q、J相对偏低,个别花色尤其差。原因:1850张图按13类均分时每类约140张,但实际数据往往有偏重,普通数字牌的数量多于人头牌,而且K的轮廓与A相似,牌面转角更复杂,类别间的类间距离更小。

解决:先做类别计数,如果差距超过两倍,考虑两件事。一是对少数类做复制粘贴增强,二是按类别加权采样,让少数类在每次迭代中有更高概率被抽中。我常用的是把Q、K这些类的原始图做轻度旋转和亮度增强后再补进训练集,不直接复制同一张图,否则等于重复加权,会让模型记住具体图片的噪声而不是类别的规律。判定标准很简单:单独统计每个类的预测召回率,人头牌低于92%就需要干预。

5.3 旋转牌与水平框的冲突:mAP好看,实拍总漏检

现象:验证指标和数据集内部测试都不错,放到真实牌桌上,牌被斜着拿、任意旋转角度时,漏检率立刻升高。原因:数据集的标注框是水平外接框,yolo学到的是水平矩形内的牌面花纹;一旦牌旋转超过45度,水平框内混入大量背景,特征被背景冲淡,尤其红心、方块这类中心对称花色更容易误判。

解决:轻度旋转时外接框变化不大,超过30度之后标注工具通常不会自动计算旋转后的外接框,只是简单拉伸原框。我一般对训练图做正负20度以内的旋转增强,推理时用更大的imgsz弥补旋转带来的特征衰减。如果业务目标是全向识别,那就该换旋转目标检测器,而不是抱着水平框硬调。这个坑最隐蔽,因为模型在原数据集上的分数始终很漂亮,只有到真实牌桌前才能暴露。

5.4 json编码与转义符:看着能打开,脚本却读不进去

现象:json.loadf直接报JSONDecodeError,或者标注内容读出来是一串乱码。原因:标注文件里混了注释、没有用utf-8编码、某些牌面名称里含有引号或emoji但没有转义。coco json是严格json,注释、单引号、尾部多逗号都是非法写法;在Windows下保存为ANSI编码后,python默认utf-8读取就会报错。

解决:打开文件统一写encoding='utf-8',遇到JSONDecodeError时用json.tool或编辑器格式化检查具体行。实际经验是,标注工具导出的json本身通常没问题,问题大多出在中间手工编辑环节。所以我在转换前从不在原文件上另存,只读不改。如果确实要修改,复制一份再改,避免引入编码偏差。

6. 复核coco json标注质量的三个自查脚本:越界、漏标、重复框与牌面回显

扑克牌识别数据集值不值得拿来训练,最终取决于原始标注怎么维护。下面是组合自查脚本,我每一轮训练前都会跑一遍。

import json, cv2, os with open('cards_coco/annotations/instances_train.json', encoding='utf-8') as f: data = json.load(f) images = {im['id']: im for im in data['images']} ann_map = {} for ann in data['annotations']: ann_map.setdefault(ann['image_id'], []).append(ann) bad = [] for img_id, anns in ann_map.items(): im = images[img_id] boxes = [] for ann in anns: x, y, w, h = ann['bbox'] # 检查越界与非法宽高 if x < 0 or y < 0 or w <= 0 or h <= 0 or \ x + w > im['width'] or y + h > im['height']: bad.append(img_id) # 检查重复框:中心位置和尺寸完全一致视为重复 key = (round(x, 1), round(y, 1), round(w, 1), round(h, 1)) if key in boxes: bad.append(img_id) boxes.append(key) print('有问题的图片数量:', len(bad))

这段脚本把越界框和重复框都标为bad。标完以后,把有问题的框直接画到原图上回显,肉眼确认是漏标还是误标。这一步不要省,因为polygon转bbox、cvat导出以及多轮修改标注时最容易产生精度损失。

回显代码就是读取图片后用cv2.rectangle把bbox画在原图上,再叠加类别名保存。自己做一次就能发现很多自动化体检发现不了的问题,比如两张牌叠在一起时只标注了上面一张,下面的牌被当成背景,训练出来的模型在这个角度附近会随机丢牌。这类漏标问题是扑克牌识别特有的黑匣子,靠统计脚本看不出来,必须可视化。

我的固定习惯是:正式训练前一天,把越界、重复框、类别分布和可视化结果一起过一遍。前面几章讨论的大多数训练问题,其实都能在这一轮检查里提前预判。真正值得投入的并不是训练命令上的微调,而是把数据质量检查养成流程的一部分。等你在生产环境里把识别率推到98%以后回头再看,会确认这套基本功才是让模型站住脚的原因。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询