
简介这是一个面向计算机视觉目标检测任务的扑克牌标注数据集适用于初学者练习YOLO、SSD等检测模型的训练与评估。数据集涵盖queen、ten、nine、king、jack、ace共6种扑克牌类别共363张真实场景图片每张均配套对应的XML标注文件标注工具为LabelImg格式为Pascal VOC可直接用于主流检测框架的数据预处理。RAR压缩包内文件总数为726个包含363个jpg图像与363个xml标注文件整体大小36.62MB目录结构简洁便于快速解压并划分训练集与验证集。目前已有395人学习下载适合目标识别入门者、课程设计或小型项目实践。通过这份数据集读者可直观了解目标检测数据集的标注规范、类别平衡情况并直接用于模型训练、精度对比或标注格式转换等环节。1. 扑克牌目标识别数据集最容易被低估的标注工程做目标检测的人迟早会遇到一个尴尬场景模型在通用数据集上跑得挺好一换到自己的窄业务场景就失灵。扑克牌识别就是典型——牌面图案复杂、花色和点数组合多、拍摄角度随意、还有反光和遮挡连人眼都有时看错。所谓“标注扑克牌目标识别数据集”不是简单拿 labelimg 框几个框而是要规划一个能被检测模型稳定吃进去的数据体系牌面要不要拆成点数和花色两个维度、要不要上旋转框、正负样本怎么配比。这篇文章会把从采集、类别体系设计到标注工具选型、格式转换、训练前验证的全过程拆开适合正在做棋牌识别、卡牌类业务或想自己造一个小型目标检测数据集的工程师照着复现也适合第一次接触数据标注的新手按步骤落地。2. 先定标注方案54 类单标签还是点数花色双分支2.1 任务边界决定类别体系54 类直出 vs 孪生标签扑克牌目标识别在业务上通常有两种需求。第一种是整牌识别模型直接输出“这张是红桃A”或“这张是黑桃10”这是一张牌一个标签总共 54 个类大小王算两个类。第二种是只关心牌面数值比如做二十一点或德州扑克辅助分析这类需求只需要识别点数而不在乎花色那类别就压缩到 13 类有时甚至只要A、10、J、Q、K这5类。我见过不少人在这一步踩坑一上来就按 54 类全量标注结果发现模型在牌面相似的两个类之间频繁混淆比如红桃K和方块K因为颜色和图案高度接近标注数据又不充分训练出来精度上不去。更合理的长尾做法是设计孪生标签一个分支识别点数rank一个分支识别花色suit最后通过后处理把两个结果拼起来。这样做的好处是类别数从 54 降到 134每个类的样本量直接翻了好几倍小样本下的训练难度大大降低。这个决策要写在数据集的 readme 里而不要只放在脑子里。因为后续所有标注工具的类别列表、格式转换脚本、模型输出解析都要跟着这个体系走。如果中途改方案前面标注的几千张图都要返工这是数据标注最贵的一笔血泪成本。2.2 采集策略真实拍摄和程序渲染各解决一半问题扑克牌数据集和通用目标检测数据集有个显著区别纯靠人工拍照采集一张图里可能只有 3 到 5 张牌要凑够每个类别上千个正样本工作量非常大。所以我一般会把数据集分成两部分一部分用程序渲染合成保证每个类别的样本都足够且标注绝对准确另一部分用真实拍摄负责把光照、畸变、反光这些真实噪声带进来。程序渲染的做法是用 OpenCV 或 PIL 把牌面图像贴到随机背景上加上随机旋转、缩放、透视变换和亮度扰动同时直接记录每张牌的框坐标。这种方式生成的标注不会有人为错漏而且样本量可以无限扩充。下面的代码是一段最小可用的渲染脚本用 PIL 将一张牌面素材贴到白底背景上并输出一张 YOLO 格式的标注文件。# generate_synthetic_cards.py # 用 PIL 把单张牌面渲染到随机位置同时输出 YOLO 格式标注 import random from PIL import Image CARD_IMG_PATH card_ace_of_spades.png # 一张抠好背景的牌面透明素材 NUM_IMAGES 10 # 生成 10 张合成图 for i in range(NUM_IMAGES): bg Image.new(RGB, (640, 640), (random.randint(200, 255),) * 3) card Image.open(CARD_IMG_PATH).convert(RGBA) # 随机缩放牌面大小约为原图的 0.15 到 0.35 倍 scale random.uniform(0.15, 0.35) new_w int(card.width * scale) new_h int(card.height * scale) card card.resize((new_w, new_h), Image.LANCZOS) # 随机旋转expandFalse 保证尺寸不变丢掉的边角不算在框内 angle random.uniform(-45, 45) card card.rotate(angle, expandFalse) # 随机位置保证整个牌面不超出背景 x random.randint(0, 640 - card.width) y random.randint(0, 640 - card.height) bg.paste(card, (x, y), card) # YOLO 格式类别id 中心x 中心y 宽度 高度坐标都归一化到 0-1 # 旋转只影响像素内容不改变外接框的标注形式 cx (x card.width / 2) / 640 cy (y card.height / 2) / 640 w card.width / 640 h card.height / 640 with open(fsynthetic_ann_{i}.txt, w) as f: f.write(f0 {cx:.4f} {cy:.4f} {w:.4f} {h:.4f}\n) bg.save(fsynthetic_img_{i}.jpg, quality95)这里的关键参数是scale和angle。scale决定牌面在画面里的占比如果业务里牌是放在桌上的远景摄像头那 0.15 到 0.35 合理如果是近景特写要调大到 0.4 以上。angle控制在正负 45 度以内是因为超过这个角度牌面图案本身会大面积翻转模型学到的是“倒着的牌”与真实场景分布不一致。expandFalse也是刻意的旋转后裁掉的边角会让牌面内容缺失反而更接近真实遮挡情况。2.3 真实拍摄时的负样本与场景多样性合成数据有个天然短板背景太干净。真实场景里牌会放在桌上、手上、牌堆里周围还有筹码、纸杯、香烟盒这些干扰物。所以真实拍摄的数据里至少要留出 10% 到 20% 的负样本——就是画面里没有牌或者牌被手挡住超过一半的图片。这些图不参与标注但会作为背景图混入训练集告诉模型“没牌的时候不要乱框”。拍摄时尽量模拟真实使用场景的光线条件。扑克牌的反光很烦人特别是深色牌面在灯光下会出现高光区域导致花色完全看不清。不要只拍光线均匀的正面照要刻意加入侧光、逆光和冷暖光。另外一个容易被忽略的点是手机自动对焦和 EXIF 旋转很多手机竖屏拍摄的 JPG 会在 EXIF 里记录旋转角度标注软件读出来的像素尺寸和实际显示方向不一致。这个坑会在后面避坑章节专门展开。3. 选择标注工具与标注规范labelimg、labelme 还是更重的 CVAT3.1 工具选型矩形框能解决 80% 的扑克牌任务扑克牌在真实场景中通常是矩形但如果拍摄角度倾斜牌的平行四边形效果明显。这时用轴对齐矩形框会框进大量背景特别是牌和牌挨得近时两个矩形框互相重叠模型就很难区分边界。所以工具选型要看你的牌面姿态平视俯拍为主用 labelimg 就够了有大量倾斜角或需要精细化分割建议用 labelme 打多边形或旋转框。labelimg 的优势是轻量、启动快、快捷键顺手适合大批量手工标注。它的默认输出是 Pascal VOC 的 XML 格式后续需要转成 YOLO 的 txt 或 COCO 的 json。labelme 输出的是单个 JSON 文件每张图一个文件支持多边形和旋转矩形适合复杂姿态。CVAT 是 Web 版的标注平台支持多人协作和自动化预标注适合团队作业但部署和维护成本高一个人做小数据集有点重。我自己的习惯是合成数据全自动标注人工只修边界真实数据用 labelimg 矩形框如果有超过 30% 的样本倾斜明显就换成 labelme 打四点多边形。这里的“四点”指的是牌面四个角能精确贴合倾斜矩形而不是八点、十点那样精细的轮廓——扑克牌是刚体不需要曲线拟合。3.2 标注规范先定规则再动手避免返工数据标注最忌讳边标边定规则。有一类错误是“框的范围不一致”有人框牌面白色边框内沿有人框外沿还有人把整张牌包括阴影全框进去。这个差异在训练时会被模型当成同类目标的不同尺寸直接拉低 mAP。所以我通常会在开始标注前排一张规范图写明矩形框的边界具体到哪个像素线比如“框的范围是牌面四角的圆角切点连线”。扑克牌还有两个独有的标注细节。第一牌面是双面的如果业务需要区分正反面要单独设两个类比如“card_front”和“card_back”如果不需要就统一按牌面内容标。第二牌堆里露出三分之一张牌时要不要标我建议设一个人工判定规则露出面积小于整牌的 40% 就不标因为这种样本框出来都是残缺特征模型会学到奇怪的响应。规范化还意味着类别名要统一。不要出现“heart_A”“红桃A”“Ha”这种混写。类名列表建议单独存一个 classes.txt在标注开始前导入工具并且全程不允许手打新类名。这一条如果管住了后面格式转换脚本就能写得极其简单。3.3 标注质量校验用脚本扫描标注文件的低级错误标注完成不等于数据可用。标注文件里最常见的问题是坐标越界box 的 xmax 或 ymax 超出了图片宽高通常是工具边界处理或人工操作失误导致。这种错误会让模型在训练时遇到负坐标或超宽框轻则 Loss 异常重则训练直接崩掉。我会用一段短脚本批量检查所有标注文件把越界和空标注的样本挑出来。# check_annotations.py # 扫描 labelimg 输出的 VOC 格式标注找出坐标越界、类别为空、宽高为 0 的文件 import os import xml.etree.ElementTree as ET ANNOT_DIR annotations IMG_W, IMG_H 640, 640 # 与图片实际分辨率保持一致 for xml_file in sorted(os.listdir(ANNOT_DIR)): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(ANNOT_DIR, xml_file)) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) has_error False for obj in root.findall(object): name obj.find(name).text if not name: print(f{xml_file}: 空类别名) has_error True 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)) if xmin 0 or ymin 0 or xmax img_w or ymax img_h: print(f{xml_file}: 坐标越界 ({xmin},{ymin},{xmax},{ymax})) has_error True if xmax - xmin 0 or ymax - ymin 0: print(f{xml_file}: 宽高为 0) has_error True if not has_error: print(f{xml_file}: OK)这段脚本的核心是读取每个 XML 里的 image size 字段再逐个 object 检查边界。注意size里的宽高必须和 JPG 实际像素一致所以脚本里没有用硬编码而是直接解析 XML 里记录的尺寸。如果发现一批文件全部越界优先检查是不是图片被批量压缩过但 XML 没更新这个情况我在处理手机拍摄素材时遇到过不止一次重采样后忘记重新导出标注所有框全部偏了一截。4. 坐标换算与格式转换从 VOC 到 YOLO 再到旋转框4.1 三种常见格式间的换算关系目标检测数据集的标注格式大体分三类。第一类是 VOC 的 XML框坐标是绝对像素值形式是xmin, ymin, xmax, ymax左上角为原点。第二类是 YOLO 的 txt坐标是归一化后的相对值形式是class_id, center_x, center_y, width, height所有值都在 0 到 1 之间。第三类是 COCO 的 json框坐标是绝对像素值的[x, y, width, height]注意这里的 x, y 是框左上角不是中心。换算公式不复杂最容易翻车的点是“归一化”。YOLO 格式里所有坐标都要除以图片的宽和高宽度方向的坐标除宽度高度方向的坐标除高度不能交叉。很多转换脚本写错就是因为这里用混了。另外 VOC 的xmax - xmin算出来的宽度是像素值转 YOLO 时也要除以图片宽度这一步的整数除法在 Python 3 里是安全的但如果你从别的语言迁移过来要小心整除丢精度。4.2 VOC 到 YOLO 的转换脚本与类别映射训练 YOLOv8 系列模型时最常见的是拿 labelimg 标注好的 VOC 数据集来喂。这时必须写一个转换脚本把每个 XML 转成 txt并把类别转换成数字 id。这里的类别 id 不是随便排的它必须和后续配置文件的names列表一一对应。如果第 0 类是“spade_A”配置里第 0 个名字也得是“spade_A”错一位所有标注全部废掉。# voc_to_yolo.py # 把 labelimg 导出的 VOC XML 转为 YOLO 格式的 txt import os import xml.etree.ElementTree as ET # 类别映射表类名 - id顺序必须与训练配置的 names 完全一致 CLASS_MAP { spade_A: 0, spade_2: 1, spade_3: 2, heart_A: 3, heart_2: 4, heart_3: 5, # 实际使用时把 54 个类全部列全 } def convert(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: print(f{xml_path}: 未知类 {name}跳过) continue class_id CLASS_MAP[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) # 转成 YOLO 的中心点加宽高描述 cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 越界保护把坐标钳制到 0-1 之间 cx max(0, min(1, cx)) cy max(0, min(1, cy)) w max(0, min(1, w)) h max(0, min(1, h)) lines.append(f{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_name os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines)) # 批处理 os.makedirs(yolo_labels, exist_okTrue) for f in os.listdir(annotations): if f.endswith(.xml): convert(os.path.join(annotations, f), yolo_labels)这段脚本里的越界保护不是多余操作。半遮挡的牌会有标注员故意把框往图外拉的情况如果不在转换时钳制训练时 YOLO 的损失函数计算会遇到奇怪的宽高比Loss 曲线出现锯齿。钳制到 0-1 后框虽然贴着图像边缘但至少训练过程不会炸。要知道这个保护是“补救”而不是“根治”根治的办法是回到标注阶段把越界框重新拉回图内。4.3 旋转框标注角度定义是最大的黑匣子如果你的扑克牌场景大量是斜着摆的矩形框的拟合效果会很差这时就要上旋转框。旋转框标注的格式有两大流派一种是四点坐标polygon 四点另一种是中心点加宽高加角度。用 labelme 打四点是最直观的但转到 mmrotate 这类训练框架时需要把四点坐标换算成旋转角度而不同框架对角度定义不一致有的用弧度有的用角度有的定义是 x 轴正方向逆时针有的用负数表示顺时针。我踩过最深的坑是角度定义搞反导致模型训练时把横着的牌和竖着的牌当成完全不同的目标。解决方法是先拿一两张图做可视化校验把旋转框画出来叠加到原图上确认转出来的框和原标注重合再批量转换。不要相信“这个库默认和那个库一致”这种说法每个库的算子实现都可能有差异可视化永远是最便宜的保险。4.4 数据集切分先按场景切再按类别比例切切分数据集看起来简单实际坑很深。如果直接对整批图片随机划分很容易出现同一张桌上的牌在不同角度下出现在训练集和验证集里验证集 mAP 虚高。正确做法是先按“拍摄场景”或“拍摄批次”分组把同一组的图片全部放进同一个集合再在组级别随机切分。真实场景的扑克牌数据尤其要注意这一点同一个桌子拍 500 张图它的光线和背景是一致的模型很可能学会了识别背景而不是牌本身。切分的比例一般用 8:1:1即训练 80%、验证 10%、测试 10%。扑克牌目标识别这类类别多、单类样本少的任务可以先按类别做一次统计保证每个类别在三个集合里的占比接近总体占比避免某种牌只出现在训练集不出现在测试集造成测试指标虚高或虚低。5. 避坑实录扑克牌数据集的 5 个常见翻车现场5.1 花色误标模型精度天花板的主要来源现象训练结束后模型把“方块”和“红桃”混在一起Accuracy 卡在 85% 上不去。检查训练集的 Ground Truth发现一部分“方块”类图片标注成了“红桃”。原因方块和红桃的颜色在低光照下几乎没有区别标注员在快速赶工时凭第一眼颜色判断导致标签污染。解决用程序自动对比每张图的牌面中心区域主色相把色相接近的样本单独筛出来人工复核。扑克牌数据集的标注速度要控制在每天 500 张以内超过这个量误标率会指数上升。5.2 类别严重不平衡54 类里 4 种牌占了 70% 的样本现象模型对样本量大的类别 mAP 到 0.95对样本少的类别只有 0.4。原因人工拍照和合成渲染时没有控制每个类别的生成数量常见牌出现次数多。解决先跑一个统计脚本对每个类别的标注数量画直方图然后针对数量少的类别额外渲染合成数据或重新拍摄。合成数据在这里有天然优势可以精准地按需生成每类目标补齐到至少 800 个实例。5.3 旋转框角度和方向完全错乱现象用 mmrotate 训练旋转框模型Loss 再怎么调参都不收敛可视化发现预测框旋转了 180 度。原因四点标注转角度时起点没有固定为“左上角顺时针第一个点”导致不同样本的同一张牌角度值偏差很大。解决写转换脚本时先对四点坐标做排序固定起始点顺序再在转换后画图可视化逐张比对旋转框和人眼判断的倾斜方向是否一致。这条属于典型的“看着是参数问题实际是数据顺序问题”。5.4 手机照片 EXIF 旋转导致所有标注错位现象标注好的图片在本地预览正常但送入训练后模型全学偏验证集表现极差。原因手机竖拍的照片在 EXIF 里记录了一个旋转标记labelimg 显示时自动旋转了画面但保存的 XML 坐标却没有跟着旋转。解决批量预处理所有图片直接用 PIL 读取 EXIF 并执行旋转然后把图片保存为无 EXIF 的新文件再重新标注。这一步必须在标注前完成标注后补救的成本翻倍。5.5 小目标识别牌在远处只占画面 3% 时的漏检现象摄像头在桌上方的 2 米处牌面平均像素宽只有 40 像素模型对这类小目标的 Recall 极低。原因YOLO 模型下采样 32 倍后40 像素的目标在特征图上只剩 1 到 2 个像素几乎无法提取到有效特征。解决把输入分辨率从 640 提升到 1280代价是训练速度下降约一半或者在标注时把这类小目标单独设为一个子集用 SAHI 这类切图推理工具处理推理阶段的输入。处理小目标时一个非常实用的技巧是把小目标的标注框面积直接写入数据集的 metadata在训练时按面积采样确保每个 batch 里都混有若干张含小目标的图。6. 数据集验收先让模型把错误标出来再让人去复核6.1 用 YOLOv8 跑一个短训练把 Loss 曲线当质检报告拿到一批标注好的数据不要急着做正式训练先用 YOLOv8 跑一个 30 轮左右的短训练。把训练集 Loss 和验证集 Loss 画在同一个坐标系里看两件事一是训练 Loss 是否平滑下降如果出现阶梯状跳变大概率是标注文件里有脏数据二是验证 Loss 是否在降到某个值后急剧反弹如果反弹多半是验证集里有没见过的强反射样本。6.2 用混淆矩阵找出最容易标混的类别YOLOv8 训练结束后会输出混淆矩阵。扑克牌数据集的混淆矩阵比通用数据集直观得多如果“方块3”和“红桃3”互相混淆说明标注层面的颜色区分度不够如果“黑桃4”和“黑桃A”互相混淆那就是牌面图案在特定角度下被误判。把混淆矩阵里每一对高频错分组合拉出来去原始图片里逐一查看通常能看到标注员当时的误标原因而不是模型问题。6.3 用自动增强把合成数据活用到极致合成数据有一个常见争议模型在合成图上训练效果好一到真实图片就掉点。问题的根源往往是增强参数设置过激把牌面细节渲染成了看不清的模糊色块。我建议把增强限制在亮度扰动正负 20%、对比度扰动正负 15%、仿射变换旋转正负 15 度不要用 Cutout 这种会遮挡牌面关键信息的增强。扑克牌的纹理细节集中在中心图案上遮盖掉了模型就学了个寂寞。最后多留一句我自己的教训每次做数据集时我都会在项目里放一个 runbook 文件把标注规则、类名表、转换脚本的输入输出路径都写清楚。这个文件最初只花 20 分钟写却在后来每次返工时帮我省下一整天。标注好的扑克牌数据集就像是轮胎看起来谁都会做但只有把每一步都做扎实了最终跑起来的模型才敢被业务用。希望帮到你。本文还有配套的精品资源点击获取