当前位置: 首页 > news >正文

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、2,13是“小王”,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.12,CUDA 11.6。

第一步先确认显卡驱动够不够新。命令行执行nvidia-smi,看右上角显示的CUDA Version是多少。这块建议驱动升到比较新的版本,至少支持CUDA 11.x以上,不然即使你装了对应版本的PyTorch,它也没法调用GPU。驱动检测完再装PyTorch:

conda create -n yolo python=3.8 conda activate yolo pip install torch==1.12.0 torchvision==0.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或v5l,v5n又有点太激进,精度会掉。如果只追求安卓端速度,训练完后可以换v5n再跑一轮对比,但作为第一版,v5s是精度和速度的平衡点。

训练命令:

python train.py --data cards.yaml --weights yolov5s.pt --img 640 --batch 32 --epochs 200 --hyp hyp.scratch-low.yaml

cards.yaml里要配置nc为15,并指定训练集和验证集的路径。这里有个容易错的地方:路径一定要写绝对路径或者正确相对路径,否则训练直接报FileNotFoundError,排查时要浪费不少时间。

超参数方面,我主要改了hyp.scratch-low.yaml里的几个值。因为数据集不大,初始学习率lr0建议设小一点,0.01比默认的0.01稍保守,配合warmup能稳一些。还有一个重点是anchor,YOLOv5会在训练开始时用K-Means自动重算anchor,牌面的宽高比和COCO数据集差异很大,v6.0版本默认会自动计算,不用手动干预,但要注意如果看到日志里anchors和默认值差不多,说明你的标注框比例分布和预设接近,否则模型收敛会很慢。

训练过程中最关键的观察点是loss曲线。正常情况下box_loss、obj_loss、cls_loss三个曲线整体是下降的,但会有波动。如果发现某一轮的val mAP突然大幅下降,先怀疑是不是学习率设置过大,其次检查是否过拟合(训练loss下降而验证loss上升)。

3.3 训练结果评估

我训练的模型在验证集上,mAP50做到了0.972,mAP50-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导出ONNX:

python export.py --weights best.pt --include onnx --opset 11 --simplify

--opset 11这步很关键,opset太高会导致NCNN转换时出现不支持的算子。--simplify会自动调用onnx-simplifier对计算图做简化,省去一些冗余节点。

拿到ONNX后,用NCNN的工具转成.param和.bin:

onnx2ncnn best.onnx best.param best.bin

这个步骤最容易出问题的就是提示“Unsupported operator”,常见的是Split、Resize这些在特定opset下的变体。我的经验是优先降opset到11,如果还报错,就手动把YOLOv5里的Detect层拆掉再导出,后处理全部在C++里手写。实际部署中手写后处理反而更灵活。

之后还可以跑一下ncnnoptimize:

ncnnoptimize 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::vector<BoxInfo> generateProposals(const ncnn::Mat& out, float prob_threshold) { std::vector<BoxInfo> 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项目,这个项目基本就是为YOLOv5+NCNN定制的。它已经帮你把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-fp1682ms45ms
Redmi Note 11骁龙680yolov5n-fp1695ms65ms
Pixel 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张)把端到端流程打通,确认安卓端识别效果达到了基本可用,再回头扩充数据、调参。这套方法对任何目标检测落地项目都通用,模型训练和端侧部署是两条线,尽量并行推进,而不是一条道走到黑再考虑部署。

本文还有配套的精品资源,点击获取

http://www.cnnetsun.cn/news/4346024.html

相关文章:

  • 安卓PS5模拟器SharpEmu深度解析:原理、性能与实测
  • 从原理到实战:构建与精调动态压枪系统的完整指南
  • 智慧物流调度架构设计:基于GPIO适配异构电梯的机器人梯控实现
  • Linux 之大文件拆分、合并与校验
  • ur_rtde:UR机器人RTDE实时控制与视觉引导实战解析
  • 从零搭建工业级多模态炼钢大模型:Qwen2.5-VL + LoRA 实战全流程
  • 基于SpringBoot的环保知识普及平台的设计与实现(源码+讲解视频+LW)
  • 蔚来数据分析岗笔试复盘:SQL窗口函数与业务案例实战解析
  • Palantir Study 02|Palantir 产品全景:Gotham、Foundry 等名词归位
  • OpenClaw Mac源码安装指南:开源AI代理框架部署实战
  • 2024秋招小米算法岗笔试全解析:考点题型与备考策略
  • VINS漂移别乱调参,imu-utils标定IMU噪声全流程
  • 15 年前的老笔记本也能用大模型写代码?| 实测 MiniCPM5-1B vs Qwen3.5-0.8B JavaScript 编程能力对比
  • Codex CLI 与中转 API 接入实战:本地部署与模型配置全解析
  • 2026小程序卖货平台搭建哪家好?长期稳定运营的选择方法
  • 大模型应用开发:小白程序员必备,抢占未来先机!
  • Graph Engineering:用图控制Agent执行SOP的工程实践
  • 瑞萨RH850F1L CAN通信驱动开发:从官方示例到实际项目调试指南
  • 管道漏水检测数据集与源码实战:从声学特征到深度学习模型
  • 基于PROSAIL查找表的LAI预测Python脚本实现与验证
  • Grok大模型驱动的机器人定制开发:从ROS2代码生成到API集成实践
  • 模拟智能体技术解析:从核心原理到实战应用指南
  • Unity开发自动化:用CLI工具整合AI辅助工作流
  • 科研绘图工具Skill-pubfig:一键生成符合期刊规范的图表
  • 智能体编程时代,软件工程基础技能图谱全解析
  • 浪潮NF5280M5固件升级全攻略:BIOS与BMC实操指南
  • 完全模型组智能车方案:从视觉识别到ROS控制的完整实践
  • MFC上位机实现DM码识别:自适应阈值与快速定位实战
  • 京东技术通用岗笔试全解析:高频考点与编程题思路
  • 苹果目标检测数据集制作:VOC标注与YOLO转换实战指南