ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于YOLOv5和NCNN的斗地主牌面识别与安卓端部署实践

基于YOLOv5和NCNN的斗地主牌面识别与安卓端部署实践 简介一套基于YOLOv5的斗地主牌面识别与安卓端NCNN部署方案面向对扑克AI、移动端深度学习落地感兴趣的开发者与算法工程师。资源以训练好的best.pt模型为核心配合Python推理脚本可对截图中扑克牌数字与花色进行精准定位识别替代传统OpenCV模板匹配识别鲁棒性更强。资源包共39个文件涵盖Python源码、C/Java/XML等安卓工程代码、Gradle构建脚本、模型文件与说明文档约14.38MB结构上区分模型训练、数据预处理、评估工具和Android工程便于从训练到端侧部署一站式复现。已有49人学习下载适合需要快速搭建牌面识别Demo或参考NCNN移动端移植流程的实战型开发者。1. 项目背景与整体设计思路1.1 为什么做斗地主牌面识别第一次接触到“斗地主牌面识别”这个需求是帮一个做棋牌复盘工具的朋友做技术验证。他的想法很简单直播或录播的牌局视频里能不能自动把玩家手牌、地主牌、底牌全部识别出来生成一份完整的牌局数据再配合记牌逻辑做AI复盘。当时市面上现成的OCR方案只能识别印刷体文字遇到花色的红黑小图案、牌面旋转、光线变化就抓瞎。我把问题拆了一下发现核心其实就是目标检测——把54张牌当成54个类别或者把牌面特征区域当成检测目标然后用YOLO系列模型去做检测和分类。选YOLOv5而不是更新的YOLOv8或YOLOv11我的理由很务实第一v5的生态太成熟了训练脚本、预训练权重、转换工具链都打磨过无数轮遇到问题一搜就有答案第二v5s这个轻量版本在移动端的部署案例非常多ncnn、MNN、TFLite都有现成转换教程团队接手成本低第三牌面识别这个任务本身不算复杂不需要特别强的特征提取能力v5s的精度已经足够用。用更重的模型精度提升有限部署负担却实打实翻倍不划算。1.2 任务定义与技术选型分析牌面识别有两种常规做法。第一种是把整张牌当作一个检测目标检测出牌面后裁剪下来再做分类第二种是只检测牌面左上角和右下角的点数与花色区域。我最终选了第二种。原因很直接斗地主牌局截图或摄像头画面里牌通常有一定旋转角度整张牌的检测框很容易带上背景信息背面纹理、桌面反光都会干扰后续分类而点数区域在牌面的角落特征集中且稳定检测框小而准分类自然更可靠。当然这个方案也有代价——同一张牌会同时出现左上和右下两个检测框需要在后处理时合并去重这个逻辑我后面会细说。类别设计上我按“点数花色”解耦而不是直接定义54个类别。具体分成13个点数类A、2到10、J、Q、K和4个花色类黑桃、红桃、梅花、方块加起来17个类别。为什么不直接54类因为斗地主里大小王是特殊的单列两类就行而普通牌的点数和花色是正交组合如果直接54分类某些点数和花色组合样本不够均衡时模型很容易学偏。解耦之后哪怕某张牌的样本少一些只要点数样本够了、花色样本够了组合识别也能大概率正确。这个设计在实际训练中确实让mAP提升了两个多点。部署端我也做了一轮对比再决定用NCNN推理框架包体增量ARM CPU速度算子兼容性集成复杂度NCNN约2MB快YOLOv5友好中等MNN约3MB较快较好中等TFLite约2.5MB一般Focus/SiLU需预转换低OpenCV DNN约8MB慢算子覆盖不全低NCNN胜在两点一是针对ARM CPU做了相当精细的汇编级优化不用跑GPU也能维持流畅帧率二是对YOLOv5的Focus层、SiLU激活函数等算子的转换支持很完善pt转onnx再转ncnn基本是一条龙走通。TensorRT我也提一嘴它在安卓上主要优化的是GPU但Adreno和Mali的适配链路长、踩坑成本高不适合这个项目的时间预算。2. 数据集的构建与标注细节2.1 数据获取方案数据集质量直接决定模型效果这步偷懒后期要花十倍时间去补。我当时的采集渠道有三条。第一条是模拟器截图用安卓模拟器打开斗地主App切换不同房间、不同牌桌、不同视角用自动化脚本每隔几秒截一张图这个渠道能快速拿到大量相对干净的牌面图片缺点是背景单一、牌面光照一致泛化性有限。第二条是实拍牌桌拿手机俯拍实体扑克牌覆盖卧室台灯、客厅顶灯、户外阳光等不同光照条件旋转牌面角度甚至故意让牌叠放交错这个渠道的素材最接近真实使用场景。第三条是从牌局直播录屏里抽帧直播画面里有弹幕、头像框、UI遮挡正好用来训练模型的抗干扰能力。三类数据我按3:2:1的比例混合。模拟器截图占比虽然高但它只是让模型先学会基础特征实拍和直播帧才是真正提升鲁棒性的关键。数据量方面我最终保留约12000张图片其中有效标注框超过8万个。这里有个经验值目标检测任务每类样本不要少于500个实例我拆成17类后最少的花色类也有3000多个实例远高于这个阈值所以模型没有出现明显的类别偏置。2.2 标注规范与类别设计标注工具用的是labelImg输出YOLO格式的txt文件每行内容为类别id、归一化中心坐标x、归一化中心坐标y、归一化框宽w、归一化框高h。这个格式整个YOLO系列通用后面训练不用额外转换。标注上最需要注意的坑是不要把整个点数图案比如“10”全框进去也不要只框数字的一部分。我统一的规范是标注框紧贴点数图案的外轮廓稍微留1-2个像素的边距即可。为什么这样要求因为检测框的宽高比和位置如果忽大忽小模型拟合目标框回归头时会被迫学到一个模糊的分布推理时预测框会抖动。标注团队如果人多一定要先统一标准最好给每个人发一份示例图集避免同类目标的框大小差异过大。另一个容易忽略的点是硬负样本。我专门收集了几千张“没有牌的大桌面”图片不标注任何目标让它们在训练中作为纯背景参与。这些图片能明显降低误检率尤其是当识别画面切到牌桌外区域时模型不会乱框出一堆高置信度的“幽灵牌”。背景数据量占总数据量的20%左右是比较合理的比例。注意标注文件命名必须和图片完全一致labelImg默认会把标注文件生成在同目录但如果你把不同目录的图片混在一起先别急着合并先检查是否有重名文件否则训练时会出现严重的标签错位——你以为是黑桃A的标注实际对应的图片可能是一张梅花3。2.3 数据增强策略数据增强这块YOLOv5内置的策略已经很强了我实际只额外补了三项针对性增强。第一项是马赛克增强MosaicYOLOv5默认开启。它的原理是把4张图随机缩放后拼成一张大图相当于变相增大batch size让小目标更容易被学稳。第二项是旋转增强我把角度范围加大到了±45度因为牌面在手机摄像头画面里经常是歪的而默认的旋转角度只覆盖小角度偏移。第三项是亮度与对比度抖动用来模拟不同光照下的桌面环境增强模型对光线变化的免疫力。有一个坑需要提醒色彩空间上的HSV抖动不要调太狠。斗地主牌面的花色颜色是强语义特征——红桃和方块是红色黑桃和梅花是黑色如果把饱和度或色相扰动幅度设得过大模型可能会把颜色作为不可靠特征而忽略掉训练时loss降得快但一到真实场景就露馅。我的经验是把hsv_h设为0.01、hsv_s设为0.6、hsv_v设为0.4比默认值略降同时保证颜色语义不被破坏。3. 模型训练与环境踩坑3.1 训练环境搭建先说环境。训练机用的是单张RTX 309024GB显存系统Ubuntu 20.04显卡驱动470、CUDA 11.4、cuDNN 8.2.4。这里有两个容易出问题的环节。第一是Python环境我直接用了conda创建独立环境Python 3.9PyTorch 1.12.0。YOLOv5官方仓库对PyTorch版本不算严格但千万不要直接用新到离谱的PyTorch 2.x配老版本YOLOv5有概率出现算子不兼容导致训练时直接崩溃。保守做法是用requirements.txt里锁定的版本区间再配合torch官方安装命令把CUDA版本对应上。第二是显卡驱动与CUDA的匹配。实测最省事的方式不是自己装CUDA而是直接通过pip install torch1.12.0cu113这类带CUDA后缀的预编译包安装它会自动把CUDA runtime拉进来不需要你显式安装CUDA Toolkit。只要nvidia-smi显示的驱动版本足够新470以上就能直接跑起来。这一步很多人卡在“装了CUDA但torch检测不到GPU”上其实就是驱动太旧或者驱动和预编译包版本不匹配。3.2 超参数设置与训练策略超参数我直接基于yolov5s.yaml和hyp.scratch-low.yaml改。模型结构里nc改成17其他参数参考以下配置输入分辨率640×640。不要图快用320或416牌面点数区域属于小目标分辨率太低会直接丢掉细节特征。Batch size32。由于数据集不算大batch大一点有助于稳态收敛但如果你显存只有8G就老老实实降到16。Epochs前30轮冻结backbone训练再解冻训练到200轮。冻结阶段模型学的是检测头的回归能力解冻后整体微调收敛更稳。OptimizerSGD初始lr0.01配合Cosine LR调度。Adam在检测任务里不是最优选择SGDlr decay更不容易过拟合。预训练权重yolov5s.pt在COCO上训过的权重作为初始化。从零开始训的话收敛速度至少慢一倍而且最终mAP大概率低2-3个点。早停机制patience设20连续20个epoch在验证集上没有提升就自动停省时间。训练轮次不建议一次性拉到300以上因为牌面识别任务比较简单通常在150-200轮时已经过拟合。我训练完看log最佳模型出现在188轮。训练过程中我会重点盯三个指标train/box_loss、train/cls_loss、metrics/mAP_0.5。数据集准备没问题的情况下box_loss应该持续下降mAP_0.5在训练后期能到98%以上mAP_0.5:0.95在85%上下。如果mAP_0.5一直卡在80%以下甚至低于90%先别急着调参百分之九十的情况是标注出了问题比如类别标错、框偏移或者是训练集和验证集出现过大的分布不一致。4. 模型转换与安卓端部署4.1 转换流程与踩坑训练完成后第一步是把PyTorch权重pt转成ONNX再把ONNX转成NCNN的param和bin文件。整个链路是best.pt → best.onnx → best.param best.bin。pt转ONNX的命令直接用YOLOv5官方仓库的export.pypython export.py --weights best.pt --include onnx --opset 11。这里有一个关键参数默认导出的模型是带NMS后处理的但部署到NCNN时我强烈建议不要带NMS因为在NCNN里跑动态尺寸的NMS既慢又容易出错。正确做法是导出时设置--nms不启用让模型只输出原始的预测张量然后在Java/Kotlin层或JNI层自己写非极大值抑制。转NCNN用的是NCNN官方提供的onnx2ncnn工具通常没那么多问题。YOLOv5的Focus层、SiLU激活函数、SPP模块在onnx2ncnn里都支持一般能直接转换成功。如果遇到不支持的高版本算子可以尝试将opset降到11甚至10。第一次转换失败很正常不要慌去看NCNN的issue区搜索引擎一搜报错信息基本都有现成的解决方法。转换后一定要验证。最简单的验证方式是把同一个输入图片分别跑PyTorch模型和NCNN模型对比输出张量的差异。我通常用余弦相似度来评估如果高于0.98就说明转换没有明显损失。这个步骤千万不能跳我曾经因为opset版本问题导致输出张量完全错乱模型却“成功”转换了害得在安卓端白白排查了两天。4.2 安卓工程接入与JNI封装安卓端我用的技术栈是Android Studio CMake OpenCV NCNN。工程结构上C代码放在src/main/cpp目录通过JNI向上层提供Java接口。核心的Native逻辑其实并不多模型加载、前向推理、输出后处理总共也就几百行代码。接入的核心步骤在CMakeLists.txt里引入ncnn和opencv的预编译库。ncnn官方提供了android的预编译zip包下载后解压放到src/main/ncnn目录并在CMakeLists里设置ncnn_DIR指向它。把模型路径从Java层传下来用ncnn::Net加载param和bin文件。建议把模型文件放到Data/目录或assets目录运行时先复制到应用私有目录避免在assets里直接加载导致的内存问题。输入图像预处理将Bitmap转为RGBA格式的Mat再缩放到640×640归一化到0-1后按NCHW格式填入ncnn::Mat的输入通道。YOLOv5训练时使用了RGB通道顺序记得不要用BGR否则颜色通道错乱会导致识别率断崖式下降。推理完成后ncnn输出的张量shape是[1, 25200, 175]640×640输入下共有25200个锚框。其中前4个是框坐标第5个是置信度后面17个是类别概率。逐行解析过滤掉置信度低于0.3的检测框再做NMS。NMS实现我会直接复用NCNN自带的ncnn::nms函数但要注意它的输出是框的索引你需要自己维护一个数组来记录哪些框被保留再去拼接识别结果。数据层我设计了一个简单的识别结果类牌面点数、花色、置信度、检测框位置相对原图的坐标。因为同一张牌可能同时有左上和右下两个检测框我会在Java层做“重复框合并”逻辑如果两个检测框的中心距离小于宽度的三分之一且识别结果一致就合并为一个取置信度更高的作为最终结果。4.3 识别逻辑与性能优化最终的业务逻辑是每隔200ms从摄像头画面或视频帧中取一帧做识别然后将识别到的“底牌”“手牌”通过接口回调给上层UI。为了避免阻塞主线程整个推理流程放在后台线程池中执行。一个非常重要的性能优化点输入分辨率。推理一张640×640的图确实更准但如果要跑实时视频流我建议把输入分辨率降到480×480甚至416×416然后适当调高置信度阈值来补偿精度损失。实测在骁龙888上640×640单帧推理耗时约35ms480×480则能压到20ms以内对于实时识别场景来说流畅度比一点点精度差距更重要。另一个优化是线程数。NCNN的opt.num_threads设置为4通常最优。线程太少CPU利用率不足设置成8并不会更快反而会增加线程切换开销导致单帧延迟反而上升。开启opt.use_fp16_storagetrue后模型参数可以以FP16存储内存占用减半部分ARM芯片上还有额外加速。需要确认的是你的手机SoC是否支持FP16计算大多数中高端ARM CPU都支持低端机可能不支持这时候就需要回退到FP32。5. 常见问题与排查实录5.1 训练期突出问题Loss不降或震荡。如果box_loss和cls_loss在前10轮都不下降首先确认两点学习率是否设置合理以及数据标注是否正确。我用SGD初始lr0.01是经验值如果你换成了Adam初始学习率应该降到0.001级别。另一个很隐蔽的问题是类别id与class_names顺序不一致导致模型把黑桃标成了梅花loss也会很“迷茫”。验证集mAP与训练集差距过大。这往往是过拟合或者数据泄漏。如果实拍照片和模拟器截图混在一个目录下没做shuffle网络可能会“记住”特定背景而没学会真正的牌面特征。解决办法是保证split时按图片目录分组随机拆分不能出现同一局牌局画面的一部分在训练集、一部分在验证集的情况。5.2 转换阶段常见报错ONNX转换或NCNN转换报错九成以上是算子版本问题。最常见的报错是“Unsupported slice step”或“Unsupported onnx concat”之类的信息。我的排查方法是先用Netron可视化ONNX模型结构定位到报错的那个节点然后去NCNN仓库的docs/operators.md查支持列表。如果确实不支持就需要写自定义层或手写算子替代。好在YOLOv5的基础结构在NCNN都已经支持得很完善了我实际项目里没有遇到需要手写自定义层的情况。5.3 安卓端崩溃与性能问题JNI找不到本地方法导致崩溃这个问题几乎人人都会遇到。解决方法是在C层把函数命名为Java_包名_类名_方法名的规范格式并在Java层声明System.loadLibrary(native-lib)。如果名字写错一般启动时会直接报UnsatisfiedLinkError定位很快。模型加载慢或内存大我遇到过NCNN加载模型时内存突然冲到300MB的情况根因是图片转Mat的过程中每帧都调用了cv::Mat的深拷贝。优化方式是复用同一个Mat对象用cv::cvtColor直接原地转换避免每帧创建新对象。同时将输入的Bitmap统一转换为ARGB_8888格式能大幅减少类型转换开销。识别结果不稳定如果同一张牌在相邻几帧里识别结果跳变通常是因为置信度阈值设置得太临界。我建议把NMS的IoU阈值设为0.45置信度阈值设在0.3到0.5之间。如果要求更稳定可以在业务层做时间维度的“投票”机制——连续三帧同一位置识别结果一致才输出这种抖动过滤虽然简单但对用户体验的提升非常明显。写在最后的实操心得整套流程走下来我最深的体会是这个项目的难点不在模型训练而在数据与部署的两端。训练本身YOLOv5已经把门槛降得很低了真正需要花心思的是把数据标注规范定好、把转换验证做扎实、把安卓端的生命周期和后处理细节处理好。尤其是NCNN转换后的验证环节我建议每个人都不能跳——这一步省下的时间会在安卓端真机调试时十倍地还回来。如果你也想跑这个项目我建议按这个顺序推进先拿1000张模拟器截图跑通全流程确认模型转换、安卓部署、识别逻辑都正常再大规模扩充数据集和调优精度。这样做的好处是你不会在数据标注做了大半之后才发现某个环节存在根本性的设计问题。最后再分享一个小技巧安卓端调试时把模型放到侧边栏里实时显示检测框和置信度这一步能帮你快速发现哪些场景下模型会误检漏检定向补数据比盲目调参有效得多。本文还有配套的精品资源点击获取
返回列表