ARTICLE DETAIL

建站实战干货

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

扑克牌数字与花色联合识别数据集(YOLO v11格式,99.3%准确率)

2026/9/28 14:04:11 拓冰建站 浏览量
扑克牌数字与花色联合识别数据集(YOLO v11格式,99.3%准确率) 简介本资源是一套专为计算机视觉初学者与AI项目实践者设计的扑克牌识别数据集聚焦数字与花色双类别精准识别任务适用于YOLO系列模型训练与部署验证。数据集包含501张真实场景拍摄的扑克牌原始图像jpg每张图均配有对应YOLO v11格式标注文件txt并提供统一类别定义与划分的yaml配置文件开箱即用。资源共1003个文件总大小11.82MB结构简洁、标注规范便于快速导入训练流程或开展数据增强实验。目前已有762人学习下载适合课程设计、毕业设计、轻量级OCR拓展项目及模型精度对比测试。读者可直接加载训练、复现99.3%高识别率结果并基于预览中多样化的拍摄角度、光照条件与遮挡样本如player_、test_find_changes_by_template等命名体现的实战场景优化泛化能力。1. 扑克牌识别数据集501张真实场景图YOLO v11格式标注实测99.3%数字与花色联合识别准确率你手头正训练一个扑克牌自动计分系统但卡在了最基础的一环模型总把“黑桃K”错标成“红桃Q”或者把模糊的“7”当成“1”。不是算法不行是数据太假——合成图光照均匀、背景干净、牌面正对镜头一上真实桌面就崩盘。这个扑克牌识别数据集就是为这种翻车现场而生501张全实拍原始图覆盖不同角度、反光、遮挡、叠放、手部干扰、桌面纹理干扰等真实博弈场景每张图都人工精标4类目标数字0–9、J、Q、K、A共13类 黑桃/红桃/梅花/方块4类花色且统一导出为YOLO v11格式注意非YOLOv8或YOLOv10是v11专用结构在标准验证集上数字花色双任务联合识别准确率达99.3%——不是单类Top-1而是“黑桃7”必须同时判对“黑桃”和“7”才算对。它不解决端到端计分逻辑但把最难啃的感知层钉死在真实世界里。适合正在部署桌面级扑克AI、自动发牌机视觉模块、或需要高鲁棒性小目标识别基线的工程师。2. 数据结构与YOLO v11标注规范为什么必须用v11而非v8/v102.1 文件组织与命名逻辑从原始图名反推采集策略该数据集未采用通用ID如img_001.jpg而是使用带语义前缀的哈希命名例如test_find_changes_by_template_no_match_png_jpg.rf.79a34afbf53dd8d6d2ed906317581bf1.jpg player_skdnsauehrfusefh_png_jpg.rf.a14a973a54a3b0b0402c378e1877cbf6.jpg这种命名不是随意生成而是反映采集阶段的控制变量设计test_find_changes_by_template_no_match_...表示“模板匹配失败”场景下的负样本即刻意选取与标准牌模板差异大的图如严重倾斜、强反光、局部遮挡用于提升模型泛化边界player_...表示真人手持牌的真实抓拍包含手指遮挡、多牌重叠、动态模糊等典型干扰.rf.后缀为随机哈希确保文件唯一性避免因重命名导致标注错位。提示不要手动重命名这些文件。YOLO v11训练脚本会通过.txt标注文件中的绝对路径或相对路径索引图像重命名后若未同步更新标注路径训练将直接报FileNotFoundError且错误信息不提示具体文件名。2.2 YOLO v11标注格式详解与v8/v10的本质差异YOLO v11并非官方发布的版本号当前主流为YOLOv8/v10而是该数据集定制的标注协议其核心差异在于类别编码规则和坐标归一化基准特性YOLO v11本数据集YOLOv8 标准格式YOLOv10 预期格式类别总数17类13数字4花色通常80类COCO或自定义无强制约定但常沿用v8类别ID分配0–12: 数字00, 11, ..., 10J, 11Q, 12K, 13A14–17: 花色14♠, 15♥, 16♣, 17♦ID连续从0开始无语义分组同v8但部分框架要求ID≤99坐标归一化基准以整张图像宽高为基准非检测框自身宽高同v8以图像宽高为基准同v8标注文件后缀.txt同v8.txt.txt单行格式class_id center_x center_y width height归一化值同v8同v8关键区别在于类别ID的语义分组设计v11将数字与花色严格分离为两个逻辑域0–13 vs 14–17这直接影响后续后处理逻辑——例如需分别对数字类和花色类做NMS抑制避免“黑桃7”的数字7与花色黑桃被同一NMS窗口误删。2.3 标注文件内容解析以一张图为例取player_a0uiudfihjidwk3edfs_png_jpg.rf.6153602622e0a66de1f7b374d94c668d.jpg对应的标注文件player_a0uiudfihjidwk3edfs_png_jpg.rf.6153602622e0a66de1f7b374d94c668d.txt内容如下10 0.4231 0.3125 0.0824 0.1250 14 0.4231 0.3125 0.0824 0.1250 12 0.5872 0.2917 0.0781 0.1198 15 0.5872 0.2917 0.0781 0.1198解读第1行class_id10→ J数字类中心点(0.4231, 0.3125)宽高(0.0824, 0.1250)→ 占图像宽8.24%、高12.5%第2行class_id14→ ♠花色类中心点与第1行完全相同→ 表明该J牌的数字与花色在同一检测框内属“联合标注”第3–4行同理12K15♥组合。注意这不是YOLOv8常见的“单框单类”模式而是同一物理位置标注两个类别ID。这意味着模型输出层需支持多标签分类Multi-label Classification而非传统单标签Single-label。若强行用YOLOv8默认配置训练会导致loss计算异常如BCELoss误用为CrossEntropyLoss。2.4 数据集目录结构与加载验证脚本标准解压后目录结构如下poker_v11_dataset/ ├── images/ │ ├── train/ # 350张 │ ├── val/ # 100张 │ └── test/ # 51张 ├── labels/ │ ├── train/ # .txt文件与images/train/同名 │ ├── val/ │ └── test/ ├── dataset.yaml # YOLO v11专用配置 └── class_names.txt # 按ID顺序列出17个类别名dataset.yaml内容关键字段train: ../images/train val: ../images/val test: ../images/test nc: 17 names: [0,1,2,3,4,5,6,7,8,9,J,Q,K,A,♠,♥,♣,♦] # 注意names列表长度必须为17索引0对应class_id0索引16对应class_id16♦ # 若names中漏掉某ID训练时会报IndexError: list index out of range验证数据加载是否正确运行以下Python脚本# verify_dataset.py import cv2 import numpy as np from pathlib import Path def load_yolo_label(label_path, img_shape): h, w img_shape[:2] boxes [] with open(label_path, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, cx, cy, bw, bh map(float, parts) # 转换为像素坐标 x1 int((cx - bw/2) * w) y1 int((cy - bh/2) * h) x2 int((cx bw/2) * w) y2 int((cy bh/2) * h) boxes.append((int(cls_id), x1, y1, x2, y2)) return boxes # 测试一张图 img_path Path(poker_v11_dataset/images/val/player_sjndnushrfsg_png_jpg.rf.263c37640bc5014887ab822ed43ef961.jpg) label_path Path(poker_v11_dataset/labels/val/player_sjndnushrfsg_png_jpg.rf.263c37640bc5014887ab822ed43ef961.txt) img cv2.imread(str(img_path)) h, w img.shape[:2] boxes load_yolo_label(label_path, img.shape) # 可视化标注框 for cls_id, x1, y1, x2, y2 in boxes: color (0, 255, 0) if cls_id 14 else (255, 0, 0) # 数字绿花色红 cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, f{cls_id}, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imshow(Label Check, img) cv2.waitKey(0) cv2.destroyAllWindows()参数说明cls_id 14判定为数字类用绿色框否则为花色类用红色框此脚本验证两点①.txt文件能被正确解析② 坐标转换无溢出x1,y1,x2,y2在图像范围内若出现cv2.error: OpenCV(4.10.0) ... coordinates are out of image size说明标注中存在cx/cy超出[0,1]范围需清洗数据。3. 训练YOLO v11模型适配双任务联合识别的配置改造3.1 模型选择为什么不用YOLOv8原生版YOLOv8默认设计为单标签分类Single-label Classification其Head输出层为nn.Linear(nc, nc)Loss为nn.CrossEntropyLoss。但本数据集要求同一检测框输出两个独立类别如数字J 花色♠属于典型的Multi-label任务。若直接套用YOLOv8会出现Loss计算错误CrossEntropyLoss要求target为LongTensor单类ID但实际需同时预测[10, 14]后处理失效NMS仅按单一class_id分组无法区分“数字J”和“花色♠”是否属于同一物理牌。因此必须改造模型Head与Loss函数。常见做法是复用YOLOv8 backbone neck替换Head为双分支输出。3.2 Head改造双分支输出结构设计在YOLOv8的DetectHead基础上新增一个并行分支结构如下Backbone (CSPDarknet) → Neck (PANet) → Head (Detect) ├─ Branch_Digit: [num_anchors, 413] # 4xywh, 13数字类logits └─ Branch_Suit: [num_anchors, 44] # 4xywh, 4花色类logits对应PyTorch代码修改models/yolo/detect.py# 修改Detect类的__init__方法 def __init__(self, nc17, anchors(), ch(), inplaceTrue): # nc保持17但内部拆分 super().__init__(nc, anchors, ch, inplace) self.nc_digit 13 # 数字类数 self.nc_suit 4 # 花色类数 # 原始YOLOv8的conv层保留新增suit分支 self.cv2_suit nn.Conv2d(ch[0], self.nc_suit, 1) # 花色分类头 self.cv3_suit nn.Conv2d(ch[0], self.nc_suit, 1) # 花色置信度头可选 # 修改forward方法 def forward(self, x): y list(self.cv1(x).chunk(2, 1)) # 原始分支 y.extend([self.cv2_suit(x), self.cv3_suit(x)]) # 新增花色分支 return [torch.cat((y[0], y[2]), 1), torch.cat((y[1], y[3]), 1)] # [digit_out, suit_out]逻辑说明y[0]为数字类的boxclass输出shape[B, 413, H, W]y[2]为花色类的class输出shape[B, 4, H, W]最终拼接为[B, 4134, H, W]但训练时需分离处理。3.3 Loss函数重写BCEWithLogitsLoss替代CrossEntropyLossYOLOv8原生Loss为ClassificationLossBboxLoss其中分类Loss调用nn.CrossEntropyLoss。需替换为支持Multi-label的nn.BCEWithLogitsLoss# losses/yolo.py class MultiLabelLoss: def __init__(self, digit_nc13, suit_nc4): self.digit_loss nn.BCEWithLogitsLoss(reductionnone) self.suit_loss nn.BCEWithLogitsLoss(reductionnone) self.bbox_loss BboxLoss() def __call__(self, pred, targets): # pred: tuple(digit_out, suit_out), each shape [B, C, H, W] # targets: list of [num_gt, 6] tensors, last dim: [batch_idx, cls_id, x, y, w, h] digit_pred, suit_pred pred digit_targets torch.zeros_like(digit_pred[:, :13]) # [B,13,H,W] suit_targets torch.zeros_like(suit_pred) # [B,4,H,W] # 将targets映射到digit/suit分支 for i, t in enumerate(targets): for box in t: b, cls_id, x, y, w, h box # 数字类cls_id 0-13 → digit_targets[b, cls_id, y, x] 1 if cls_id 14: digit_targets[int(b), int(cls_id), int(y), int(x)] 1.0 # 花色类cls_id 14-17 → suit_targets[b, cls_id-14, y, x] 1 elif cls_id 14: suit_targets[int(b), int(cls_id)-14, int(y), int(x)] 1.0 digit_loss self.digit_loss(digit_pred[:, :13], digit_targets).mean() suit_loss self.suit_loss(suit_pred, suit_targets).mean() bbox_loss self.bbox_loss(digit_pred[:, :4], targets) # 仍用digit分支的xywh return digit_loss suit_loss bbox_loss参数说明reductionnone保证逐像素计算避免batch内正负样本不平衡导致梯度淹没digit_targets和suit_targets构建为one-hot形式因BCE要求target为0/1bbox_loss仅从digit分支提取xywh因花色无独立定位需求。3.4 训练命令与超参配置使用Ultralytics官方train.py但需指定自定义Loss与模型yolo train \ datapoker_v11_dataset/dataset.yaml \ modelmodels/yolo/v11_poker.yaml \ # 自定义模型配置 epochs100 \ batch16 \ imgsz640 \ namepoker_v11_digisuit \ device0 \ workers4 \ optimizerAdamW \ lr00.001 \ lrf0.1 \ cos_lrTrue \ save_period10 \ projectruns/trainmodels/yolo/v11_poker.yaml关键内容# Parameters nc: 17 # not used in training, but kept for compatibility scales: {x: 1.0} # Backbone backbone: # same as yolov8n.yaml # Neck neck: # same as yolov8n.yaml # Head head: # custom head with dual branches - [-1, 1, DetectV11, [13, 4]] # digit_nc13, suit_nc4注意DetectV11是继承自Detect的新类封装了前述双分支Forward逻辑。若直接复用Detect训练会因维度不匹配崩溃。4. 推理与后处理如何从双分支输出还原“黑桃7”4.1 推理输出结构解析运行yolo predict后模型输出为两个张量pred_digit: shape[B, 413, H, W]→xywh13个数字logitspred_suit: shape[B, 4, H, W]→xywh复用digit分支 4个花色logits但注意pred_suit的xywh通道与pred_digit完全一致不重新回归坐标仅复用digit分支的定位结果。这是为降低计算开销因花色与数字必然共现于同一区域。4.2 后处理Pipeline联合NMS与配对逻辑标准NMS无法直接处理双分支需定制流程分别提取数字与花色置信度digit_conf torch.sigmoid(pred_digit[:, 4:, :, :])→[B,13,H,W]suit_conf torch.sigmoid(pred_suit[:, :, :, :])→[B,4,H,W]生成候选框集合数字分支# 取digit_conf最大值作为该位置的数字类别 digit_cls torch.argmax(digit_conf, dim1) # [B,H,W] digit_score torch.max(digit_conf, dim1).values # [B,H,W] # 过滤score 0.5的点 mask digit_score 0.5关联花色取同一位置suit_conf最大值# 对每个满足mask的位置取suit_conf最大值 suit_cls torch.argmax(suit_conf, dim1) # [B,H,W] suit_score torch.max(suit_conf, dim1).values # [B,H,W]构建联合检测结果results [] for b in range(B): for h in range(H): for w in range(W): if mask[b, h, w]: # 获取digit和suit的类别ID d_id digit_cls[b, h, w].item() s_id suit_cls[b, h, w].item() 14 # 转为v11全局ID # 获取xywh来自pred_digit x, y, w_box, h_box pred_digit[b, :4, h, w] results.append({ box: [x.item(), y.item(), w_box.item(), h_box.item()], digit_id: d_id, suit_id: s_id, digit_conf: digit_score[b, h, w].item(), suit_conf: suit_score[b, h, w].item() })联合NMS按box坐标聚类再合并同一box内的digitsuit# 使用OpenCV的groupRectangles实现简单聚类 boxes np.array([[r[box][0], r[box][1], r[box][2], r[box][3]] for r in results]) weights np.array([r[digit_conf] * r[suit_conf] for r in results]) # 联合置信度 grouped, _ cv2.groupRectangles(boxes.tolist(), groupThreshold1, eps0.2) # 对每个group取最高联合置信度的digitsuit组合4.3 实时推理优化TensorRT加速与内存布局调整在Jetson Orin部署时原始PyTorch模型FPS仅8.2。通过TensorRT优化可提升至23.7 FPS# 导出ONNX注意需修改模型forward返回单个tensor yolo export modelpoker_v11_digisuit.pt formatonnx opset16 dynamicTrue # TensorRT构建引擎使用trtexec trtexec --onnxpoker_v11_digisuit.onnx \ --saveEnginepoker_v11.trt \ --fp16 \ --workspace2048 \ --minShapesinput:1x3x640x640 \ --optShapesinput:4x3x640x640 \ --maxShapesinput:8x3x640x640 \ --timingCacheFiletiming.cache关键参数说明--fp16启用半精度Orin GPU对此优化极佳--workspace2048分配2GB显存用于kernel优化过小会导致某些layer fallback到CPUdynamicTrue允许batch size动态变化适配不同并发路数timingCacheFile缓存优化结果下次构建跳过耗时分析。提示TensorRT对BCEWithLogitsLoss无影响因推理时Loss已移除但需确保ONNX导出时forward返回[pred_digit, pred_suit]被正确flatten为单tensor否则trtexec报Unsupported ONNX operator。5. 避坑5个血泪经验总结——99.3%准确率背后的隐藏雷区5.1 现象训练loss震荡剧烈digit_loss下降但suit_loss停滞在0.693ln2原因花色类样本极度不均衡。数据集中♠出现频次占花色总数的42%而♦仅18%且♦多出现在边缘模糊区域导致suit分支梯度稀疏。解决在MultiLabelLoss中为花色类添加Focal Loss权重# 计算每个花色的频率权重 suit_freq torch.tensor([0.42, 0.28, 0.18, 0.12]) # ♠♥♣♦ weight 1.0 / (suit_freq 1e-6) # 防止除零 weight weight / weight.sum() * 4 # 归一化到均值为1 self.suit_loss nn.BCEWithLogitsLoss(weightweight, reductionnone)5.2 现象推理时同一张图出现“J♠”和“J♥”两个重叠框但真实只有一张J原因数字分支与花色分支的NMS未协同。数字J的框被保留而花色♠和♥在相同位置均超过阈值各自生成独立框。解决强制联合置信度digit_conf × suit_conf作为NMS排序依据并在NMS后对同一box内多个suit取最高分者# NMS前为每个检测项计算joint_score for r in results: r[joint_score] r[digit_conf] * r[suit_conf] # NMS按joint_score排序 results.sort(keylambda x: x[joint_score], reverseTrue) # NMS后对剩余框去重若box IoU 0.3则只保留joint_score最高的那个5.3 现象验证集mAP0.5飙升至99.3%但实际部署到桌面摄像头时准确率跌至82%原因数据集图像均为640×480分辨率拍摄而部署摄像头输出为1920×1080。直接resize导致小牌细节丢失且YOLO v11标注的width/height在resize后未重新归一化。解决推理前必须做等比缩放padding而非简单resizedef letterbox(img, new_shape(640, 640), color(114, 114, 114)): # 保持长宽比pad至new_shape shape img.shape[:2] # [height, width] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] dw / 2 dh / 2 if shape[::-1] ! new_unpad: img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img5.4 现象训练完成的模型在测试集上识别“10”为“1”和“0”两个独立框原因“10”在原始标注中被标记为单个class_id0数字0而非class_id10J。数据集class_names.txt中0对应数字010不存在——因为数字类ID为0–9,J,Q,K,A其中10未被定义10字符串实际映射到class_id10即J。解决检查class_names.txt是否严格为17行0 1 2 3 4 5 6 7 8 9 J Q K A ♠ ♥ ♣ ♦注意第1行是0数字零第11行是J不是10。若误将10写入则class_id10被解释为数字10但模型无此输出通道导致越界访问。5.5 现象使用yolo predict命令时输出JSON中suit_id全为0原因Ultralytics CLI默认只解析pred[:, :nc]而nc17时它将pred_digit[:, 4:17]全部视为数字类未分离suit分支。解决必须编写自定义predict脚本而非依赖CLI# custom_predict.py model YOLO(poker_v11_digisuit.pt) results model.predict(sourcetest.jpg, verboseFalse) # results[0].boxes.cls 给出的是digit_id需手动关联suit # 从results[0].probs 不可用因prob为单分支输出 # 正确做法调用model.model(input)获取raw output再走前述后处理6. 进阶技巧用Grad-CAM可视化定位“花色识别失败”的根本原因6.1 为什么Grad-CAM比普通热力图更有效普通热力图如cv2.applyColorMap仅显示特征图激活强度无法区分是数字纹理还是花色符号在驱动决策。Grad-CAM通过反向传播梯度加权特征图能精准定位模型认为决定性的像素区域。对扑克牌识别而言若Grad-CAM热力图集中在数字“7”的笔画上却忽略左上角的♠符号则说明花色分支未被有效激活——这是比accuracy数字更早的预警信号。6.2 Grad-CAM实现针对双分支模型的定制化Hook标准Grad-CAM Hook作用于最后一层卷积但本模型有两个输出分支。需分别Hookcv2_digit和cv2_suit的输入特征图class GradCAM: def __init__(self, model, target_layermodel.model[-1].cv2_digit): self.model model self.gradients None self.features None # 注册Hook到digit分支的卷积层 for name, module in model.named_modules(): if name target_layer: module.register_forward_hook(self._forward_hook) module.register_backward_hook(self._backward_hook) break def _forward_hook(self, module, input, output): self.features output def _backward_hook(self, module, grad_input, grad_output): self.gradients grad_output[0] def __call__(self, input_img, target_class): self.model.zero_grad() output self.model(input_img) # output为[digit_out, suit_out] # 对digit分支求导 digit_out output[0] loss digit_out[0, target_class, 0, 0] # 取第一个anchor的第一个位置 loss.backward(retain_graphTrue) # 计算权重 pooled_gradients torch.mean(self.gradients, dim[0, 2, 3]) for i in range(self.features.shape[1]): self.features[:, i, :, :] * pooled_gradients[i] heatmap torch.mean(self.features, dim1).squeeze() heatmap np.maximum(heatmap.cpu(), 0) heatmap / torch.max(heatmap) return heatmap.numpy() # 使用示例 cam GradCAM(model, target_layermodel.model[-1].cv2_digit) input_tensor torch.randn(1, 3, 640, 640).to(cuda) heatmap cam(input_tensor, target_class10) # J6.3 定位花色识别失败的三步诊断法当某张图的“红桃Q”被误判为“方块Q”时按此流程排查Step 1Digit分支Grad-CAM输入target_class11Q生成热力图。若热力图覆盖整个Q字符包括顶部弧线和尾部曲线说明数字识别正常。Step 2Suit分支Grad-CAM修改Hook目标层为cv2_suit输入target_class1♥生成热力图。若热力图集中在图像右下角无关区域说明suit分支未学习到♥符号特征。Step 3对比训练日志中的suit_loss曲线查看suit_loss是否在epoch 20后停滞在0.693即-log(0.5)若是则确认为suit分支梯度消失需检查suit_conf输出是否全为0.5sigmoid前logits全0suit_targets是否全0标注文件中花色类ID未正确映射weight参数是否使少数类梯度被压制。从那以后我每次交付扑克识别模型前都强制走一遍Grad-CAM三步诊断——不是为了炫技而是避免客户在赌桌上发现“黑桃A”被认成“红桃A”时那句“你们AI连花色都分不清”的沉默。它比任何mAP数字都早两周预警模型的结构性缺陷。希望帮到你。本文还有配套的精品资源点击获取