ARTICLE DETAIL

资讯详情

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

YOLOv5斗地主牌面识别与安卓端NCNN部署实战

YOLOv5斗地主牌面识别与安卓端NCNN部署实战 简介面向斗地主牌面识别与安卓端AI部署需求的完整解决方案围绕YOLOv5训练的高精度检测模型best.pt支持从截图中定位并识别扑克牌花色与数字。资源包共39个文件约14.38MB主要包含Python训练推理脚本infer.py、datasets.py等、评估与预处理工具metrics.py、object_crop_img以及Android NCNN工程源码app/、gradle等既覆盖桌面端快速验证也可在移动端完成轻量级部署替代传统OpenCV模板匹配提升鲁棒性。已有49人学习。随包附有示例图像与README说明文档目录结构包括models、utils等模块适合目标检测入门者或扑克AI开发者直接下载研究从训练、测试到安卓落地一站式参考。 牌面识别这种事听起来像是个玩具项目但真做起来的时候你会发现它把目标检测、模型转换、端侧推理这条链路全串起来了。我当时是帮一个朋友做斗地主牌局自动复盘工具他要求不光是电脑上能跑最好手机上也直接能实时识别于是就有了这个“YOLOv5斗地主牌面识别模型及安卓端部署方案”。这篇就把整个项目从数据准备、模型训练到安卓部署的全过程拆开讲适合正在做YOLOv5目标检测落地、或者想把检测模型塞进手机的朋友参考。1. 项目概述与整体设计思路1.1 为什么选YOLOv5做牌面检测第一批纠结的就是选型。斗地主牌面检测是个典型的小目标、密集场景目标检测任务一局游戏桌面上可能同时出现十几张牌牌面之间还会互相遮挡、倾斜甚至有些牌只露一个小角。这类场景我和团队之前用传统图像处理方案试过比如颜色阈值分割加轮廓筛选光线一变就崩盘全靠调参硬撑完全不通用。换深度学习方案之后摆在面前的无非就是YOLOv5、YOLOv8、以及一些轻量检测网络。选YOLOv5不是因为它精度最高而是因为它的生态最成熟。我的理由很简单第一YOLOv5在端侧部署的参考资料最多NCNN、TFLite、OpenVINO都有现成的转换工具链踩坑时能找到前人经验第二模型体积和推理速度的平衡点好控制从n/s/m/l/x几个版本能灵活切安卓端用yolov5n或yolov5s就能跑得很流畅第三YOLOv5官方仓库已经把训练、验证、导出、推理整套流程封装好了对于要快速验证落地的项目来说不用自己造轮子。YOLOv8虽然更新但它的检测头结构和部署工具链在某些边缘设备上还不太成熟我试过转NCNN时遇到一些算子兼容问题排查成本比yolov5高不少。所以这次项目果断锁定YOLOv5。1.2 牌面识别任务的核心需求拆解很多人一上来就把问题想复杂了以为要识别出每一张牌是“红桃A”“黑桃K”之类的完整信息。其实斗地主这个游戏有个特点它不需要区分同花顺也不存在花色连牌真正决定牌型和大小的只有牌面点数也就是A、2到10、J、Q、K这13个等级再加上小王、大王一共15个类别。所以我做类别设计时直接忽略花色只识别点数。这样模型从54类输出直接缩减到15类训练难度和推理计算量都小很多。如果后续产品确实需要知道花色可以再加一个花色分类头或者在检测到点数区域后裁剪小图再跑一个轻量分类模型而不是一上来就检测“红桃A”这种组合类别。整个系统的流程设计成三段式手机摄像头取帧、YOLOv5检测牌面、结果绘制叠加。安卓端的核心是集成NCNN推理框架把训练好的PyTorch模型转成NCNN格式通过JNI调用C层的检测逻辑。这个架构的优点是推理管线完全放在Native层性能和内存都可控Java层只负责UI和摄像头数据传递。2. 数据集构建从零攒一批能用的斗地主牌面数据2.1 数据采集的几个主要来源模型效果的上限是数据决定的这句话在牌面识别这个项目里体现得特别明显。我一开始想偷懒从网上找现成的扑克牌数据集结果发现大部分是单张牌的正面特写图跟实际牌桌上的场景完全不是一个分布。真实牌桌上牌是平铺或半叠放的、有反光、有阴影、有手指遮挡角度也不固定。所以最终方案是自制数据集。我的采集来源分三类第一类是拿扑克牌自己摆拍覆盖不同桌面背景木纹、毛毡、塑料桌布、不同光线自然光、暖色灯、屏幕光每张牌变换角度和距离第二类是上网找斗地主游戏直播截图这类数据最接近真实使用场景牌面有遮挡、有动态模糊专门用来提升模型的鲁棒性第三类是录制斗地主牌局的视频然后用脚本抽帧一天能攒上千张有效样本。三类数据合起来最终标注了大概6000张图每张图平均有6到15张可标注牌面总计标注框超过50000个。这个量对于15类目标检测来说不算多但因为场景相对单一配合数据增强已经够用。2.2 标注规范类别怎么定最省事标注工具我用的是LabelImg直接在Windows上就能装。标注时有一个关键细节要提醒不要用矩形框把整张牌框进去而是只框牌面左上角的点数区域如果是J、Q、K还要包含右下角的人像区域也行。因为在实际识别中牌可能被遮挡只露出一部分如果把整张牌作为检测目标一旦牌被叠住一半框的完整性就没了。只检测牌面核心区域反而更稳定。类别ID我按这个顺序来0到12分别对应3、4、5、6、7、8、9、10、J、Q、K、A、213是“小王”14是“大王”。注意这里没有用自然顺序A到K排而是按斗地主的牌力大小排主要是为了后续逻辑处理方便训练时类别顺序对模型没有影响但后处理里要排序時有个天然顺序。标注完成后YOLO格式的txt文件里每行是“class x_center y_center width height”坐标都是归一化到0到1之间的。LabelImg导出时直接选YOLO格式就行会自动生成对应的txt。2.3 数据增强让模型扛住真实光线和遮挡YOLOv5自带的训练增强里Mosaic是对牌面检测最有用的它把4张图随机拼接成一张变相增加了一个batch里的小目标数量而且能模拟牌与牌之间的复杂空间关系。我训练时Mosaic增强全程开启没有像某些大目标检测任务那样在后半段关掉。HSV增强也很关键H、S、V三个通道我分别设了0.015、0.7、0.4这能让模型对灯光色温变化不敏感。实测下来如果把HSV增强关掉同一个模型在暖黄色灯光下识别率掉了接近8个百分点。另外注意不要开上下翻转增强扑克牌上下翻转后点数就会变成另一张牌比如6翻成9方向信息不能被随机翻转破坏。左右翻转我也关掉了因为有些牌的图案左右不对称但实际影响很小主要看你的数据量够不够。随机旋转和透视增强我设了比较小的范围旋转不超过10度、透视不超过0.3因为真实牌桌虽然会斜着放但不会出现90度旋转的牌。3. 模型训练环境、超参数、实测过程3.1 训练环境搭建与显卡驱动检查训练环境这块坑主要集中在显卡驱动和CUDA版本匹配上。YOLOv5不同版本要求的PyTorch版本不一样直接决定了装哪个CUDA。我用的是YOLOv5 v6.0分支配PyTorch 1.12CUDA 11.6。第一步先确认显卡驱动够不够新。命令行执行nvidia-smi看右上角显示的CUDA Version是多少。这块建议驱动升到比较新的版本至少支持CUDA 11.x以上不然即使你装了对应版本的PyTorch它也没法调用GPU。驱动检测完再装PyTorchconda create -n yolo python3.8 conda activate yolo pip install torch1.12.0 torchvision0.13.0 --extra-index-url https://download.pytorch.org/whl/cu116 pip install -r requirements.txt这里有个细节值得说requirements.txt里会装opencv-python、matplotlib、seaborn这些依赖国内网络环境下容易卡住建议配置pip镜像源。另外YOLOv5对Python版本有点挑剔3.7到3.9之间比较稳千万别用3.11去试第三方库兼容性会出问题。3.2 超参数与训练策略模型选择上我直接用了YOLOv5s作为base因为数据集规模和任务复杂度用不上v5m或v5lv5n又有点太激进精度会掉。如果只追求安卓端速度训练完后可以换v5n再跑一轮对比但作为第一版v5s是精度和速度的平衡点。训练命令python train.py --data cards.yaml --weights yolov5s.pt --img 640 --batch 32 --epochs 200 --hyp hyp.scratch-low.yamlcards.yaml里要配置nc为15并指定训练集和验证集的路径。这里有个容易错的地方路径一定要写绝对路径或者正确相对路径否则训练直接报FileNotFoundError排查时要浪费不少时间。超参数方面我主要改了hyp.scratch-low.yaml里的几个值。因为数据集不大初始学习率lr0建议设小一点0.01比默认的0.01稍保守配合warmup能稳一些。还有一个重点是anchorYOLOv5会在训练开始时用K-Means自动重算anchor牌面的宽高比和COCO数据集差异很大v6.0版本默认会自动计算不用手动干预但要注意如果看到日志里anchors和默认值差不多说明你的标注框比例分布和预设接近否则模型收敛会很慢。训练过程中最关键的观察点是loss曲线。正常情况下box_loss、obj_loss、cls_loss三个曲线整体是下降的但会有波动。如果发现某一轮的val mAP突然大幅下降先怀疑是不是学习率设置过大其次检查是否过拟合训练loss下降而验证loss上升。3.3 训练结果评估我训练的模型在验证集上mAP50做到了0.972mAP50-95也有0.881。对于牌面这种相对简单、背景不算复杂的检测任务这个指标已经很能打了。单类精度最低的是小王的“大王”类别主要原因是王牌的样本在数据集中比例偏低占比不到5%。后面我单独给大小王补了一些样本把比例提到10%左右精度才追上其他类别。训练结束后用python detect.py --source test.jpg --weights best.pt快速看一眼可视化效果重点看遮挡严重的牌能不能被检测到。如果发现漏检优先考虑加数据其次是调整置信度阈值到0.25左右不要轻易加大NMS的IOU阈值否则密集牌面容易把相邻两个框合并成一个。4. 从PyTorch到安卓模型转换与后处理4.1 ONNX导出与NCNN转换踩坑PyTorch模型不能直接给安卓用常规路线是PyTorch转ONNX再转NCNN或者TFLite。我选NCNN是因为它在CPU上的推理效率特别高而且腾讯开源的这套工具链里专门有YOLOv5的安卓示例参考价值很大。先用YOLOv5官方仓库的export.py导出ONNXpython export.py --weights best.pt --include onnx --opset 11 --simplify--opset 11这步很关键opset太高会导致NCNN转换时出现不支持的算子。--simplify会自动调用onnx-simplifier对计算图做简化省去一些冗余节点。拿到ONNX后用NCNN的工具转成.param和.binonnx2ncnn best.onnx best.param best.bin这个步骤最容易出问题的就是提示“Unsupported operator”常见的是Split、Resize这些在特定opset下的变体。我的经验是优先降opset到11如果还报错就手动把YOLOv5里的Detect层拆掉再导出后处理全部在C里手写。实际部署中手写后处理反而更灵活。之后还可以跑一下ncnnoptimizencnnoptimize best.param best.bin best_opt.param best_opt.bin 0最后一个数字0代表fp32存储想压体积就设1转fp16体积缩小接近一半精度几乎不受影响。安卓端我推荐用fp16版本模型体积从42MB降到21MB左右加载速度和显存占用都明显改善。4.2 NCNN后处理代码详解YOLOv5v6.0模型在640x640输入下输出张量形状是(1, 25200, 20)其中25200 3个预测尺度 × (80x80 40x40 20x20)的anchor点数量20 4个框坐标 1个目标置信度 15个类别概率。后处理的C代码逻辑主要分三步。第一步做letterbox预处理把原始图像等比缩放到640x640多余部分用灰边填充。这一步的坐标映射关系必须记录下来后处理还原时要减掉padding再除以缩放比例。第二步通过Extractor跑推理ncnn::Net net; net.load_param(best_opt.param); net.load_model(best_opt.bin); ncnn::Extractor ex net.create_extractor(); ex.input(images, in); ncnn::Mat out; ex.extract(output, out);这里的输入名称“images”和输出名称“output”在导出ONNX时就已经定好了不同版本YOLOv5的名称可能不同比如v5.0是“input”“output”。你可以在onnx文件里查一下节点名或者直接在代码里打印net.input_names()确认。这个坑我踩过加载明明成功了但推理结果全乱就是因为名字对不上。第三步是遍历25200个候选框先过滤置信度低于阈值的框再做NMS合并抑制。NMS的IOU阈值我设0.45置信度阈值设0.25。贴一段核心代码框架std::vectorBoxInfo generateProposals(const ncnn::Mat out, float prob_threshold) { std::vectorBoxInfo boxes; const int num_anchors out.h; const int num_classes out.w - 5; for (int i 0; i num_anchors; i) { const float* values out.row(i); float obj_score values[4]; if (obj_score prob_threshold) continue; // 找出最大类别概率 int class_id 0; float max_prob 0.f; for (int j 5; j 5 num_classes; j) { if (values[j] max_prob) { max_prob values[j]; class_id j - 5; } } float score obj_score * max_prob; if (score prob_threshold) continue; // 解析坐标并映射回原图 float cx values[0] * 640.0f; float cy values[1] * 640.0f; float w values[2] * 640.0f; float h values[3] * 640.0f; // 还原letterbox float x (cx - w / 2 - pad_w) / scale; float y (cy - h / 2 - pad_h) / scale; boxes.push_back({x, y, w / scale, h / scale, class_id, score}); } return boxes; }NMS实现就不贴了网上现成的一大堆注意按score降序排列然后逐个比较IOU大于阈值就直接丢弃。4.3 边缘端部署的思路扩展这套模型转换方案不只适用于安卓我还在树莓派5和NXP i.MX8MP这类边缘设备上做过验证。树莓派5的CPU跑YOLOv5s单帧推理大概在180ms左右把模型换成YOLOv5n可以压到90ms做非实时分析完全够用。树莓派上直接用NCNN的Linux版编译就行代码和安卓端几乎一样只是摄像头采集的接口不同。NXP i.MX8MP这种带NPU的板子NCNN在NPU上的支持并不好需要走它自家的eIQ工具链或者ONNX Runtime。NPU上的后处理是个容易忽视的点建议把数据从NPU回传CPU后用自己写的那套后处理代码解析不要依赖框架自带的后处理函数这样性能和稳定性上都更可控。5. 安卓端集成JNI、推理、性能调优5.1 安卓端工程结构怎么搭安卓端的代码结构我参考了nihui开源的ncnn_android_yolov5项目这个项目基本就是为YOLOv5NCNN定制的。它已经帮你把ncnn预编译库、JNI层、相机预览都串好了我主要做的是把模型文件换成自己的修改类别数量和后处理逻辑。关键的依赖有两个ncnn的Android库和OpenCV的Java或者Native库。OpenCV主要用于Bitmap和Mat之间的转换ncnn本身不直接支持Bitmap格式必须先把图片转成RGB数据。我建议OpenCV也用编译好的aar包省得自己编译浪费时间。工程目录大致是app/src/main/ ├── jni/ │ ├── CMakeLists.txt │ ├── ncnn_jni.cpp │ └── yolo_detect.cpp ├── libs/ │ └── arm64-v8a/ │ ├── libncnn.so │ └── libopencv_java4.so └── assets/ ├── best_opt.param └── best_opt.bin必须要在CMakeLists.txt里链接libncnn.so并且加入-fopenmp编译选项开启多线程否则推理性能会差很多。5.2 JNI层实现细节Java端需要预留一个native方法比如public static native void detect(Bitmap bitmap, float[][] result)在JNI层用cv::Mat接收Bitmap数据。CameraX预览拿到的帧要先做图像旋转因为手机竖屏时摄像头输出的传感器方向不是自然方向需要根据CameraCharacteristics的信息做旋转不然检测框的位置会和画面错位90度。JNI层核心处理逻辑我放在detect函数里先把Bitmap转成RGBA字节数组再转成cv::Mat然后做letterbox缩放再把Mat的data直接喂给NCNN的Mat作为输入。推理完成后把检测结果写回float数组最后在Java层做绘制。特别提醒一点JNI里一定不要让Java层频繁new大数组或者频繁调用Bitmap的getPixels方法内存碎片和GC压力会直接拖垮帧率。我的做法是在初始化时申请一块固定的Buffer每帧复用。5.3 性能实测与优化同一套模型我在几台不同手机上实测了推理耗时设备处理器模型推理耗时CPU推理耗时Vulkan GPU小米12骁龙8 Gen1yolov5n-fp1638ms18ms小米12骁龙8 Gen1yolov5s-fp1682ms45msRedmi Note 11骁龙680yolov5n-fp1695ms65msPixel 4a骁龙730Gyolov5s-fp16210ms120ms实测下来中端机型跑yolov5s的CPU已经有些吃力了所以正式版本我默认用yolov5n模型。如果你希望保留更高的精度建议开启Vulkan加速同时把resize尺寸从640降到448速度能再升一倍牌面检测这种场景下448分辨率完全够用。Vulkan的坑是部分老旧机型不支持加载GPU实例时会fail。我在初始化逻辑里做了fallback如果net.opt.use_vulkan_compute true时create_extractor失败就自动切回CPU模式并在日志里打出警告避免直接崩溃。6. 常见问题与避坑速查表6.1 训练和转换阶段的典型问题CUDA out of memory是新手最常遇到。YOLOv5的训练其实对显存很敏感如果你的显卡只有8G显存batch设32会直接爆显存。建议先设batch为16或8然后观察GPU利用率再决定要不要往上加。另外YOLOv5默认开启AMP混合精度一般能省三分之一显存别轻易关。nvidia-smi显示的CUDA版本很高但PyTorch还是用不了GPU。这个问题的根源是PyTorch编译时绑定的CUDA runtime和驱动版本是两回事驱动版本只要不低于PyTorch要求的cu版本就行不用完全匹配。如果还不行优先确认你装的是不是CPU版PyTorch用torch.cuda.is_available()一测就知道了。模型转换到NCNN时算子不支持。解决思路有几个层级先换opset调到11或10再试onnx-simplifier官方export.py里的--simplify能解决大部分Reshape和Transpose的问题如果还不行就用onnx2ncnn的-o参数手动指定输出节点去掉Detect层后处理自己在C里实现。最后这一招看着麻烦但实际用起来反而最灵活因为官方NCNN的YOLOv5后处理实现不一定和你模型的输出格式对得上。6.2 安卓运行时的典型问题识别框的位置和实际牌对不上或者整体偏移。八成是letterbox的padding坐标没有正确还原。输入到模型的是640x640带灰边的图输出坐标也是在这个640x640坐标系里的你要把它映射回原始预览帧的坐标必须把减掉的padding按缩放比例加回去。很多现成示例代码用的pad值是(640 - 原宽缩放后宽) / 2但实际左右两边的pad可能不一样尤其是相机预览分辨率不是方形的时候需要精确记录每一边的pad值。应用启动后直接闪退logcat里报UnsatisfiedLinkError。这说明JNI库没找到检查libncnn.so是否真的编译进了app的jniLibs目录注意Android系统的ABI匹配arm64-v8a和armeabi-v7a的库不能混用。另外模型文件放assets目录时别在JNI层直接按路径打开要用AAssetManager读取。同一张牌的检测框抖动厉害。这通常不是模型的问题而是置信度阈值设低了。把置信度阈值从0.25提到0.4抖动会明显减少。如果产品要求稳还可以做轻量级的时序平滑就是记录最近3帧的检测结果用投票或者加权平均的方式输出最终框位置。手机发热严重。摄像头连续预览加CPU推理本来功耗就高建议推理分辨率降到448x448并限制帧率在15FPS左右。如果开启Vulkan还可以把GPU频率锁在比较保守的档位NCNN在接口层没有直接暴露这个配置但在厂商的高性能模式下会有额外功耗这点需要自己在系统层面权衡。最后再分享一个小技巧斗地主牌面识别这个项目如果你不想自己攒数据先跑通整个链路再去优化数据分布。很多人在数据采集阶段就花了两三周结果模型部署流程还没跑通效率很低。我建议先用一小批数据1000张把端到端流程打通确认安卓端识别效果达到了基本可用再回头扩充数据、调参。这套方法对任何目标检测落地项目都通用模型训练和端侧部署是两条线尽量并行推进而不是一条道走到黑再考虑部署。本文还有配套的精品资源点击获取
返回列表