做这个扑克牌识别检测系统最初的想法其实特别具体——当时帮某位朋友整理一批棋牌游戏的数据分析需求需要快速从一堆牌桌照片里识别出每张牌的牌面和位置。试过传统图像处理颜色分割加轮廓提取在固定光照下勉强能用但换个环境、换个牌背图案就崩了。后来切到YOLOv11深度学习方案直接拿数据和模型说话把扑克牌识别检测系统做成了包含数据集训练、检测推理、UI界面和登录注册的完整Python桌面应用。这篇就完整记录我从零搭建这套系统时踩过的坑、验证过的做法以及YOLOv11在扑克牌识别这类小目标、细分类任务上的真实表现。整个项目交付的形态是YOLOv11训练好的检测模型 Python项目源码 PyQt5界面 登录注册模块。目标使用者不需要懂深度学习原理打开软件登录账号拖一张扑克牌照片进去界面直接标出每张牌的类别和置信度也可以打开摄像头做实时牌面识别。对想练手YOLO目标检测落地流程、或者做棋牌类小工具的朋友来说这个项目的完整链路很有参考价值。1. 扑克牌识别的技术切入点为什么非YOLOv11不可1.1 先看这个任务的真实难点扑克牌识别听起来简单好像只是“看图片输出牌名”但实际做下来难点很集中。第一是类目多一副标准扑克去掉大小王是52张按“红桃A”“黑桃K”这种组合算就是52个类别即使拆成花色4类加点数13类也有17个输出维度。类别一多模型容量和数据集规模的要求就上去了。第二是牌面之间相似度极高红方片和红桃的形状几乎一样只是颜色有差异黑色牌里的黑桃和梅花在一些低分辨率截图里也很容易混。第三是实际场景里的干扰反光、手部遮挡、牌与牌重叠、倾斜透视、小目标牌面这些问题在真实牌桌上几乎躲不掉。YOLOv11正好适合这个场景。它是目标检测模型天然能同时输出目标的类别和坐标框而扑克牌识别恰恰需要“在哪里 是什么”两个信息。如果只用分类模型还得先做目标裁剪流程长且误差会被放大如果只用传统CV方案上面说的光照和外观变化又扛不住。选择YOLOv11而不是更早的YOLOv8或更轻的YOLOv5核心原因是它在小目标检测和细粒度分类上的综合表现更稳而且ultralytics框架的训练、导出、部署链路非常顺做项目不用从零造轮子。1.2 YOLOv11相对前代的几个关键改进从我实际使用体验来看YOLOv11和YOLOv8相比最明显的感知是收敛速度和精度上限都有提升。这代模型结构里调整了骨干网络的特征提取方式使用C3k2模块代替部分C2f结构还在不同尺度上引入了C2PSA注意力机制。注意力机制对扑克牌识别这种任务的意义在于模型能更关注牌面的纹理和边缘细节而不是被背景颜色带偏。另一个改进是训练策略上的优化比如更合理的动态标签分配策略让模型在训练早期更容易学到稳定特征。当然选YOLOv11也不是没有代价。相比轻量级模型YOLOv5s或YOLOv8nYOLOv11的稍大版本在推理时占用的显存和计算量更多。扑克牌识别至少要做到30fps以上流畅我的实际测试结论是在普通独立显卡上用YOLOv11s做640x640推理单帧耗时大约25-35ms足够用了如果部署到没有GPU的机器建议导出为ONNX并切换到CPU模式或者改用YOLOv11n以损失一小部分精度为代价换取速度。2. 数据集构建52类还是混合标注这是第一个分岔口2.1 收集牌面图片的三种可靠途径做数据集是整个项目里最耗时也最决定上限的环节。模型最后准不准一半靠算法一半靠数据集。我收集扑克牌图片主要用了三条途径。第一条是自采集。用手机或普通摄像头把牌一张张摆在纯色背景上拍摄变换角度、远近和光照。这个过程相当枯燥但拍出来的图片最贴近真实使用场景。我拍的时候把牌分成几组红牌一组黑牌一组数字牌和人脸牌分开方便后续标注每张牌拍摄距离在30-80厘米之间。第二条是公开数据集。网上可以找到一些扑克牌目标检测数据集常见的是标好52类的格式YOLO标注格式下每行是“类别id 中心x 中心y 宽 高”下载后要检查标注框是否贴合牌面很多公开数据集的标注框是带牌边的识别需求是只框牌面时就需要重新过滤一遍。第三条是程序化合成。用透明PNG牌面素材叠加到随机背景、做随机旋转和透视变换再自动生成标注框。合成数据对提升模型对背景变化的鲁棒性很有帮助但我仅把它作为补充因为合成数据无法模拟真实镜头的反光和模糊。2.2 类别定义方式的对比试验这里要先做一个关键决定类别是按52张独立牌定义还是按“花色4类 点数13类”分两步检测我两组方案都试过。按52类定义模型直接输出“红桃A”这类完整标签推理环节最简单一步到位。但问题在于数据集规模必须足够大每类至少要有稳定的样本数量。如果某些牌比如方块7样本偏少模型很容易在这些类别上欠拟合mAP会掉得很难看。按混合方案定义即模型输出4个花色类 13个点数类类别总数只有17个每类样本更充足但也引入了新麻烦模型要在同一个画面里同时输出一副牌的多个框而且如果牌面被部分遮挡花色和点数检测结果可能不对应。我最后的结论是如果场景是桌面摄像头拍摄的完整牌面直接选52类最省事如果要做视频流中移动牌面的实时识别可以尝试混合方案配合后处理逻辑。这个项目最终采用52类所有训练、验证都围绕这个设定来。2.3 标注规范与数据增强的实操细节标注用的是开源工具拉框时统一只框牌面区域不包含牌背和白边。牌面左上角的花色和点数、右下角的反向标志都要尽量完整包含在框内因为模型很大程度上是依赖左上角区域来做细分类的。如果框得太松把背景也包进去训练时模型会被背景干扰框得太紧则可能丢失关键纹理。数据增强方面我除了用ultralytics自带的增强策略随机翻转、缩放、色彩抖动、马赛克增强还额外增加了两类自定义增强其一是对图片做随机透视变换模拟倾斜牌面其二是添加高斯噪声和模拟运动模糊。实际效果表明透视变换增强对提升真实牌桌上的识别鲁棒性帮助最大因为绝大多数俯拍牌面都带有一定透视形变。数据集最终规模在9000张左右其中自采集约3000张、公开数据集筛选后约4000张、合成补充约2000张。按7:2:1划分为训练集、验证集和测试集确保同一张牌的多种变体不会同时出现在训练和测试里避免测出来的指标虚高。3. 训练阶段的关键参数与收敛判断3.1 环境配置和一条命令启动训练训练环境建议用Python 3.9以上版本PyTorch需要匹配本机的CUDA版本。我使用的组合是Python 3.10 PyTorch 2.x CUDA 11.8显卡是消费级的8GB显存型号。安装ultralytics库之后核心训练命令一行就能完成yolo detect train datacards.yaml modelyolo11s.pt epochs150 imgsz640 batch16 device0其中cards.yaml是数据集配置文件里面写清楚训练集和验证集路径、类别数量nc52以及所有类别名称列表。第一次跑的时候我建议先把batch调小一点比如8如果显存不够就再降不用强行追求大batch。训练过程中记得开启WandB或者直接用ultralytics自带的训练日志一眼就能看到每个epoch的box_loss、cls_loss、dfl_loss曲线。3.2 超参数选择背后的逻辑有几个参数的设置值得说一下。imgsz我选了640而不是1280原因是显存容量有限而且扑克牌在桌面场景里通常占画面的比例不算特别小640分辨率下小目标问题在可接受范围内。如果识别的是距离很远、牌面占比很小的摄像头画面才需要考虑1280训练加高分辨率推理。epochs选了150实际在100个epoch左右loss就开始趋于平稳后面是精细调优阶段。从收敛曲线看前50个epoch下降最快50到100之间逐步变缓超过120之后基本只有小幅波动。batch size对收敛速度的影响比大多数人以为的要小它主要影响梯度稳定性。batch16在我的显存上限附近训练速度和效果比较均衡。学习率方面我用的是ultralytics默认的自动调度策略初始学习率0.01配合余弦退火衰减没有额外调参。如果数据量较小可以适当启用更大的学习率预热比例帮助模型在初期更快进入稳定状态。3.3 看懂mAP指标不是越高越好训练结束后评估主要看mAP50和mAP50-95。扑克牌检测任务上mAP50达到0.98以上其实不难因为牌面目标大、纹理清晰但mAP50-95更能反映定位精度我最终得到的稳定结果是mAP50为0.984mAP50-95为0.87。这里的经验是不要只盯着mAP50如果mAP50-95偏低说明边界框回归不够精细在密集牌局里多个框贴在一起时可能出现框位偏移。单独挑几个困难类别看指标更有意义。颜色相近的红方片和红桃、牌面纹理相近的黑桃与梅花这些类别的召回率如果低于0.9就要优先补充对应类别的训练数据。测试集上发现黑桃3和黑桃K偶尔混淆原因是反光导致点数区域模糊后来人为增加了低光照样本混淆率明显下降。训练完成后导出模型这一步不要跳过在推理脚本里直接加载训练得到的权重文件即可。如果想部署到没有YOLO环境的机器可以导出为ONNX格式再加OpenCV的DNN模块做推理这也是我后面在做UI系统时考虑过的高性能选项。4. 从PyQt5到登录注册把模型包成一个能用的系统4.1 系统整体架构与模块划分模型训练完只是第一步这套系统的价值还得靠界面串起来。我把系统分成三个相对独立的模块登录注册模块、检测推理模块、结果展示模块。三者之间通过简单的接口连接避免以后改界面时动到推理核心代码。登录注册模块负责用户验证使用SQLite数据库存储账号信息密码用哈希存储而不是明文。检测推理模块基于ultralytics的YOLO类封装核心是一个Detector类接收图像路径或摄像头帧返回检测框列表、类别名称和置信度。结果展示模块在PyQt5的主窗口中绘制检测框和标签同时显示图片文件名、检测耗时、检测数量等信息。这种层次的划分最大的好处是方便替换。比如UI用的PyQt5如果你想换成Web界面只需要把推理模块包装成HTTP接口前端用浏览器展示即可训练好的模型和检测逻辑完全不用动。4.2 登录注册模块SQLite加哈希加密登录注册用到的SQLite是Python自带的不需要额外安装数据库服务。用户表结构很简单用户id、用户名、密码哈希、注册时间。注册的时候做两次校验第一次检查用户名是否已存在第二次才写入新记录。密码的哈希存储宁可多写几行代码也不要偷懒存明文凡是涉及用户数据的项目密码安全都是底线。import hashlib import sqlite3 def hash_password(password: str, salt: str) - str: return hashlib.sha256((salt password).encode()).hexdigest() class AuthService: def __init__(self, db_path): self.conn sqlite3.connect(db_path) self._init_table() def _init_table(self): self.conn.execute( CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, salt TEXT NOT NULL, created_at TEXT NOT NULL) ) self.conn.commit() def register(self, username, password): if self._user_exists(username): return False, 用户名已存在 salt username[::-1] fixed_salt_suffix pw_hash hash_password(password, salt) self.conn.execute( INSERT INTO users (username, password_hash, salt, created_at) VALUES (?, ?, ?, datetime(now)), (username, pw_hash, salt), ) self.conn.commit() return True, 注册成功这里只说最简版本实际项目里我还在注册表单里加了密码确认框并在登录界面加了“记住我”的简单逻辑通过一个标志文件保存当前登录用户名方便下次默认填充。登录验证的逻辑就是查询用户名对应的哈希再和输入的密码重新计算哈希比对比对通过才允许进入主界面。如果连续失败三次界面会锁定30秒这个机制防止简单爆破代码量也不大。4.3 推理模块与UI解耦界面不卡顿的关键PyQt5界面最忌讳的是在UI主线程里做耗时操作。模型推理虽然单帧只要几十毫秒但如果处理的是视频流每秒运行30次加上图像缩放、绘制框、刷新标签UI线程很容易被阻塞出现窗口无响应的问题。我的做法是使用QThread把推理逻辑放到工作线程。主线程负责接收用户点击事件、显示帧画面工作线程循环读取摄像头或图片队列执行模型推理把检测结果通过信号发回主线程。使用信号槽机制连接两个线程数据交互安全又方便。一个具体的实现思路是定义DetectThread类继承QThread在run方法里不断读取队列中的图像帧调用detector.detect(frame)然后emit处理完成的信号。主线程在收到信号后直接用结果更新QLabel上的QPixmap和表格控件。这个模式看起来多了一个类实际上极大提升了流畅度。实测在我的机器上使用多线程后摄像头实时识别稳定在28fps到33fps之间UI完全跟手。4.4 主界面布局与使用路径主界面布局我设计成左右两栏。左侧是操作区放置“打开图片”“摄像头识别”“开始/停止”三个按钮以及一个参数设置区可以选择置信度阈值默认值是0.5低于这个值的检测结果会被过滤。右侧是显示区上方是图像画布下方是检测结果表格表格列分别是序号、牌面类别、置信度、框坐标。这样布局的好处是用户操作路径非常短登录、选图、识别、看结果全部在同一个窗口内完成。对于图片识别读取本地图片后除了画框显示还会把检测结果保存为标注过的图片副本。这个功能在做数据复盘时很有用可以拿着标注图去对比模型哪些牌漏检了。摄像头识别模式则适合做实时演示界面上会显示当前每秒处理帧数和累计检测到的牌数。5. 联调过程中的真实踩坑记录5.1 第一版检测不准的根因系统刚联调完时模型在测试集上的指标很好看但一到实际场景就出现各种漏检和错检。拍了一张牌桌照片中间的牌识别得很好角落里的牌经常漏掉而且有一张反光的红方片被识别成了红桃。排查下来的根因有三个。第一个是训练数据里牌的占比普遍偏大实际场景远距离拍摄时牌面占比小模型小目标检测能力没有充分被训练。第二个是反光样本太少真实牌桌的塑料牌面反光会直接把花色和点数区域洗白模型没见过这种输入自然就乱猜。第三个是置信度阈值0.5在真实场景下有点高角落小目标的得分普遍在0.4到0.55之间。做法也很直接补充一批模拟牌桌视角的小目标训练数据把原图缩放后粘贴到更大的背景中重新标注增加反光增强推理时的置信度阈值降到0.4低分框宁可信其有不可信其无再通过非极大值抑制去重。改完之后角落牌面的召回率提升了约8个百分点更重要的是错检没有因此增加。5.2 界面卡顿不是模型的锅项目过程中有段时间摄像头识别特别卡帧率掉到10fps以下。一开始怀疑是模型太大但用单独脚本测推理速度发现单帧只要28ms根本不该卡。最后定位到问题出在视频帧缩放的效率上。PyQt5的QLabel显示图片时如果原图是从720P摄像头来的每一帧都必须先缩放成QLabel的尺寸而我的缩放代码使用了逐帧转换QImage再调用scaled这个操作在主线程里做是严重的性能瓶颈。后来改成先把摄像头帧转为OpenCV的Mat用cv2.resize缩放到统一尺寸再一次性转成QImage显示帧率立刻回到30fps。这里想提醒的是做图像类桌面应用图像格式转换的开销可能比模型推理还大需要尽早把这条链路调优。5.3 资源打包与部署建议项目全部完成后会把界面、权重文件、icons资源打包成可执行文件。打包工具我用的是PyInstaller需要注意两点一是要把训练好的权重文件作为数据文件一并打包并在代码里用相对路径加资源路径的方式取权重避免打包后找不到文件二是ultralytics在运行时可能会动态加载一些子模块PyInstaller收集依赖时容易漏需要添加隐藏导入参数把必要的模块显式加进去。如果目标机器没有GPU建议导出ONNX模型并配合OpenCV DNN或ONNX Runtime做推理这样既不用装完整PyTorch环境也能在纯CPU机器上跑。实测CPU推理一帧约120ms在演示场景下完全够用。想兼容所有平台的话PyQt5的跨平台特性已经足够Windows、Linux和macOS都能跑只是打包命令略有差异。5.4 界面上容易被忽略的几个交互细节做这类工具型软件技术难点反而不是大头细节决定实际使用的舒适度。我后来给系统加了三个很小的交互改善体验上了一个台阶。第一个是检测结果的展示改用表格之后增加了按识别次数排序的功能用户可以直接看到52张牌里哪些出现了、哪些没出现这在做牌面计数统计时非常直观。第二个是图片拖拽直接识别用户不需要先点按钮再选择文件支持把图片文件直接拖到窗口里就开始检测。第三个是日志区域在界面右下角像一个控制台那样滚动输出检测耗时、帧率、每次识别的类别列表方便调试和抽查。这三个改动都很小但真实用户反馈里提到最多的反而是它们。模型和算法固然是系统的核心但界面顺手、信息清晰才能让非技术背景的用户愿意真正去用这套系统。从拿到第一个数据集到整个系统跑通大概花了两个多月其中标注和清洗数据的时间占了将近一半。回头看这个项目技术栈并不复杂难度集中在数据质量和工程细节上。如果你也想做类似的扑克牌识别检测系统我的建议是不要急着训练模型先花时间把自己的使用场景想清楚摄像头距离多远、牌面占画面多大、环境光照稳定与否这些决定了数据集应该怎么拍、类别怎么定义、模型选哪个尺寸。把这几个决策做对了后面的训练和界面开发都只是按部就班的活。