在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上

这篇具有很好参考价值的文章主要介绍了在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

项目地址

项目:https://github.com/212534/Unity-Sentis-YOLOv8

Demo apk:链接:https://pan.baidu.com/s/1agTZRhnCzgT5P5HtuUvgWQ?pwd=ydj7
提取码:ydj7
–来自百度网盘超级会员V5的分享

效果展示

这是在电脑上的测试,用的摄像头拍屏幕
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎

安装Sentis

可以把Sentis看作Barracuda的升级版。
在Package里装com.unity.sentis
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎

使用Sentis推理yolov8的onnx

这一部分比较简单,教程非常多

using System.Collections;
using System.Collections.Generic;
using Unity.Sentis;
using UnityEngine;
using Unity.Sentis.Layers;

public class Test: MonoBehaviour
{
    public ModelAsset modelAsset;
    private Model model;
    private IWorker worker;
    Ops ops;

    void Start()
    {
        model = ModelLoader.Load(modelAsset);
        worker = WorkerFactory.CreateWorker(BackendType.GPUCompute, model);
        ops = WorkerFactory.CreateOps(BackendType.GPUCompute, null);
    }

    public void Predict(WebCamTexture camImage) //我这里使用的是摄像头图像,你也可以用普通图片。
    {
        using Tensor inputImage = TextureConverter.ToTensor(camImage,width:640,height:640, channels: 3); //对输入的图像做处理

        var m_Inputs = new Dictionary<string, Tensor> 
        {
            {"images", inputImage }
        };

        worker.Execute(m_Inputs);//执行推理

        var output0 = worker.PeekOutput("output0") as TensorFloat;  //获取输出结果
        output0.MakeReadable(); //从GPU中取出数据,经过这一步之后就可以读取output0中的数据了
    }
}

直接把onnx文件拖上去就行了。
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎

YOLOv8的输出格式

需要注意的是,yolov8的onnx的输出结果是一个1x84x8400的张量(当类别数不同时会有变化),具体含义可以参考这篇文章https://blog.csdn.net/yangkai6121/article/details/133843368
简单来讲,这里的8400是8400个预测框。而每个预测框包含以下信息:84 = 边界框预测4 + 数据集类别80
本文使用的yolov8是在coco数据集上训练的,包含80个类别,所以这里是4+80,具体情况与你的训练用的数据集类别有关。
1x84x8400这个输出很明显没法直接使用。所以需要加一个nms来后处理

NMS

yolo属于单阶段的目标检测算法,因此还要对预测框进行后处理。常见的做法是在Unity里用C#自己实现nms,但nms本身就是一个性能开销很大的后处理,自己用C#实现在性能上不划算。

如果是了解目标检测部署的同学,应该知道onnx在opset11及以上版本支持了内置nms层。那么理论上,只要在yolov8的检测头里把torch.ops.torchvision.nms加进去就可以导出自带nms层的ONNX模型。像下图一样
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎
实际上经过测试,Sentis确实可以识别出onnx的内置nms层,但是会报错。
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎
原因可能在于,Sentis内实现的nms层与torch.ops.torchvision.nms在input的shape上存在区别。
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎
举例来讲,在python中对yolov8使用torch.ops.torchvision.nms时,它要求的boxes输入shape为8400x4,scores为8400.
但是Sentis实现的nms层要求的shape分别为,1x8400x4与1x1x8400。

使用Sentis给模型加入NMS层

既然torch.ops.torchvision.nms不能用,那我们可以在Unity里用代码把Sentis实现的nms层加到模型里。
先看一下Sentis中NonMaxSuppression()的输入参数。详细的可以看这里
https://docs.unity3d.com/Packages/com.unity.sentis@1.2/api/Unity.Sentis.Layers.NonMaxSuppression.html
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎
大体来讲与torch.ops.torchvision.nms的输入参数比较一致。
我们需要解决的问题便是把yolov8输出的1x84x8400张量拆解为boxes与scores,也就是NonMaxSuppression()输入。而Sentis是支持在Unity中对模型结构进行修改的。

1.首先我们给模型加入三个新的输入,这也是NonMaxSuppression()所需要的输入。使用新增输入的形式设置这三个值,可以在程序运行时对它们动态修改。

		//Set input
        model.AddInput("maxOutputBoxesPerClass",DataType.Int, new SymbolicTensorShape(1)); //每个类别最多返回的边框数量。
        model.AddInput("iouThreshold",DataType.Float, new SymbolicTensorShape(1)); //iou阈值
        model.AddInput("scoreThreshold",DataType.Float, new SymbolicTensorShape(1)); //置信度阈值

2.对1x8400x4进行拆分。比较反人类的是,Sentis在添加层时,有些层的初始化只能输入字符串作为参数,这些字符串实际上是网络里的输出名,输入名,常量名等。所以这里要给模型加入几个常量。
我在注释里加入了每一层的shape变化,方便各位理解这个过程。如果你没搞过深度学习,不理解每一层的含义,可以去官方文档里看具体解释。
https://docs.unity3d.com/Packages/com.unity.sentis@1.2/api/Unity.Sentis.Layers.html

 		//Set constants   
        model.AddConstant(new Constant("0", new int[] { 0 })); 
        model.AddConstant(new Constant("1", new int[] { 1 }));
        model.AddConstant(new Constant("4", new int[] { 4 }));
        model.AddConstant(new Constant("84", new int[] { 84 })); //这个值根据你自己的类别数进行修改  4 + 类别数

        //Add layers    
        model.AddLayer(new Slice("boxCoords0", "output0", "0", "4", "1")); //1x84x8400 -> 1x4x8400
        model.AddLayer(new Transpose("boxCoords", "boxCoords0", new int[] { 0, 2, 1 })); // 1x4x8400 -> 1x8400x4  每个检测框的xywh

        model.AddLayer(new Slice("scores0", "output0", "4", "84", "1"));  //1x84x8400 -> 1x80x8400
        model.AddLayer(new ReduceMax("scores", new[] { "scores0", "1" }));  //1x80x8400 -> 1x1x8400   最有可能类别的置信度
        model.AddLayer(new ArgMax("classIDs", "scores0", 1)); //1x80x8400 -> 1x1x8400 最有可能类别的index

3.加入NMS层。其中,yolov8输出的边界框信息是xywh的格式,因此设置centerPointBox: CenterPointBox.Center

		model.AddLayer(new NonMaxSuppression("nmsOutput", "boxCoords", "scores",
            "maxOutputBoxesPerClass", "iouThreshold", "scoreThreshold",
            centerPointBox: CenterPointBox.Center
        ));

4.给网络加入新的输出

		model.AddOutput("boxCoords"); 
        model.AddOutput("classIDs");
        model.AddOutput("nmsOutput");  

最终变成这样:

	void Start()
    {
        model = ModelLoader.Load(modelAsset);
        worker = WorkerFactory.CreateWorker(BackendType.GPUCompute, model);
        ops = WorkerFactory.CreateOps(BackendType.GPUCompute, null);
        
        //Set input
        model.AddInput("maxOutputBoxesPerClass",DataType.Int, new SymbolicTensorShape(1));
        model.AddInput("iouThreshold",DataType.Float, new SymbolicTensorShape(1));
        model.AddInput("scoreThreshold",DataType.Float, new SymbolicTensorShape(1));

        //Set constants   
        model.AddConstant(new Constant("0", new int[] { 0 }));
        model.AddConstant(new Constant("1", new int[] { 1 }));
        model.AddConstant(new Constant("4", new int[] { 4 }));
        model.AddConstant(new Constant("84", new int[] { 84 }));

        //Add layers    
        model.AddLayer(new Slice("boxCoords0", "output0", "0", "4", "1")); //1x84x8400 -> 1x4x8400
        model.AddLayer(new Transpose("boxCoords", "boxCoords0", new int[] { 0, 2, 1 })); // 1x4x8400 -> 1x8400x4

        model.AddLayer(new Slice("scores0", "output0", "4", "84", "1"));  //1x84x8400 -> 1x80x8400
        model.AddLayer(new ReduceMax("scores", new[] { "scores0", "1" }));  //1x80x8400 -> 1x1x8400   最有可能类别的置信度
        model.AddLayer(new ArgMax("classIDs", "scores0", 1)); //1x80x8400 -> 1x1x8400 最有可能类别的index

        model.AddLayer(new NonMaxSuppression("nmsOutput", "boxCoords", "scores",
            "maxOutputBoxesPerClass", "iouThreshold", "scoreThreshold",
            centerPointBox: CenterPointBox.Center
        ));

        model.AddOutput("boxCoords");
        model.AddOutput("classIDs");
        model.AddOutput("nmsOutput");   
    }

使用NMS的输出结果

NonMaxSuppression()的输出是一个Nx3的数据。
N指的是最终保留了几个框,而3则代表[boxID,clsID,boxCoordID] ,有了clsID以及boxCoordID便可以从模型输出的boxCoords以及classIDs中提取出最终预测框的xywh和类别。
1.修改一下网络的输入,因为前文中给网络新加了三个输入,因此这里需要改变输入的数据。

		using Tensor inputImage = TextureConverter.ToTensor(camImage,width:640,height:640, channels: 3);
        var m_Inputs = new Dictionary<string, Tensor>
        {
            {"images", inputImage },
            {"maxOutputBoxesPerClass", new TensorInt(new TensorShape(1), new int[] { maxOutputBoxesPerClass })},
            {"iouThreshold", new TensorFloat(new TensorShape(1), new float[] { iouThreshold })},
            {"scoreThreshold",new TensorFloat(new TensorShape(1), new float[] { scoreThreshold })}
        };
        worker.Execute(m_Inputs);

2.获取网络输出

		var boxCoords = worker.PeekOutput("boxCoords") as TensorFloat;  //1x8400x4 所有的预测框的xywh  
        var nmsOutput = worker.PeekOutput("nmsOutput") as TensorInt;  //Nx3, N指的是最终保留了几个框. 返回结果为[boxID,clsID,boxCoordID]   
        var classIDs = worker.PeekOutput("classIDs") as TensorInt; //1x1x8400  每个预测框的类别id

3.处理网络的输出,方便后续使用。
模型的输出结果存在于GPU上,一般来讲,需要先从GPU上取出数据才能用CPU进行处理,这会导致性能的浪费。因此我们可以使用ops调用Sentis的算子,直接在GPU上对网络的结果进行处理,这样可以节省一些性能开销。

		if(nmsOutput.shape[0] == 0)//如果NMS之后一个框也未剩下 则终止
        {
            return;
        }

        var boxCoordIDs = ops.Slice(nmsOutput, new int[] { 2 }, new int[] { 3 }, new int[] { 1 }, new int[] { 1 }); //Nx3 -> Nx1  取出boxCoordID
        var boxCoordIDsFlat = boxCoordIDs.ShallowReshape(new TensorShape(boxCoordIDs.shape.length)) as TensorInt;  //Nx1 -> N  展平
        var output = ops.Gather(boxCoords, boxCoordIDsFlat, 1) as TensorFloat; //1x8400x4 -> 1xNx4
        var labelIDs = ops.Gather(classIDs, boxCoordIDsFlat, 2) as TensorInt; //1x1x8400 -> N

        output.MakeReadable(); //将处理好的结果从GPU取出
        labelIDs.MakeReadable();

4.经过前面处理过的输出,从GPU里取出来就可以直接使用。一个简单示例如下。

		for(int i = 0; i<output.shape[1]; i++)
        {
            float x=output[0,i,0];
            float y=output[0,i,1];
            float w=output[0,i,2];
            float h=output[0,i,3];
            DrawBoundingBoxes(x,y,w,h,labelIDs[i]);//绘制一个Bounding Box
        }

最终如下所示:

	public void Predict(WebCamTexture camImage)
    {
        using Tensor inputImage = TextureConverter.ToTensor(camImage,width:640,height:640, channels: 3);
        var m_Inputs = new Dictionary<string, Tensor>
        {
            {"images", inputImage },
            {"maxOutputBoxesPerClass", new TensorInt(new TensorShape(1), new int[] { maxOutputBoxesPerClass })},
            {"iouThreshold", new TensorFloat(new TensorShape(1), new float[] { iouThreshold })},
            {"scoreThreshold",new TensorFloat(new TensorShape(1), new float[] { scoreThreshold })}
        };
        worker.Execute(m_Inputs);
        var boxCoords = worker.PeekOutput("boxCoords") as TensorFloat;  //1x8400x4 所有的预测框的xywh
        var nmsOutput = worker.PeekOutput("nmsOutput") as TensorInt;  //Nx3, N指的是最终保留了几个框. 返回结果为[boxID,clsID,boxCoordID]   
        var classIDs = worker.PeekOutput("classIDs") as TensorInt; //1x1x8400  每个预测框的类别id

        if(nmsOutput.shape[0] == 0)//如果NMS之后一个框也未剩下 则终止
        {
            return;
        }

        var boxCoordIDs = ops.Slice(nmsOutput, new int[] { 2 }, new int[] { 3 }, new int[] { 1 }, new int[] { 1 }); //Nx3 -> Nx1  取出boxCoordID
        var boxCoordIDsFlat = boxCoordIDs.ShallowReshape(new TensorShape(boxCoordIDs.shape.length)) as TensorInt;  //Nx1 -> N  展平
        var output = ops.Gather(boxCoords, boxCoordIDsFlat, 1) as TensorFloat; //1x8400x4 -> 1xNx4
        var labelIDs = ops.Gather(classIDs, boxCoordIDsFlat, 2) as TensorInt; //1x1x8400 -> N

        output.MakeReadable();
        labelIDs.MakeReadable();

        for(int i = 0; i<output.shape[1]; i++)
        {
            float x=output[0,i,0];
            float y=output[0,i,1];
            float w=output[0,i,2];
            float h=output[0,i,3];
            DrawBoundingBoxes(x,y,w,h,labelIDs[i]);//绘制一个Bounding Box
        }
    }

导出到手机

如果你要打包到手机端的话,记得点一下onnx文件,然后点这里。
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎
下面是手机端的运行效果,我手机的CPU是骁龙850,可以发现帧数非常低。主要是我把推理放到了Update()里,所以性能开销很大,因此还有很多优化空间。
在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上,unity,YOLO,游戏引擎文章来源地址https://www.toymoban.com/news/detail-857300.html

到了这里,关于在Unity上使用Sentis部署yolov8与添加NMS,并编译到手机上的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [易语言][]使用易语言部署yolov8的onnx模型

    易语言部署yolo系列模型,现在网上有很多但是他们不够简洁也不够专业,有人专门把opencv封装成易语言支持库然后用opencv在易语言端写,其实这种效率没有在C++直接推理效率高,因为易语言往C++传递图像数据集是需要转换图像数据集格式才能传递给C++ opencv Mat对象,我们开发

    2024年02月09日
    浏览(87)
  • [C++]使用纯opencv部署yolov8旋转框目标检测

    【官方框架地址】 https://github.com/ultralytics/ultralytics 【算法介绍】 YOLOv8是一种先进的对象检测算法,它通过单个神经网络实现了快速的物体检测。其中,旋转框检测是YOLOv8的一项重要特性,它可以有效地检测出不同方向和角度的物体。 旋转框检测的原理是通过预测物体的边界

    2024年04月26日
    浏览(39)
  • 【YOLO】Windows 下 YOLOv8 使用 TensorRT 进行模型加速部署

    本文全文参考文章为 win10下 yolov8 tensorrt模型加速部署【实战】 本文使用的代码仓库为 TensorRT-Alpha 注:其他 Yolov8 TensorRT 部署项目:YOLOv8 Tensorrt Python/C++部署教程 安装Visual Studio 2019或者Visual Studio 2022、Nvidia驱动 安装cuda,cudnn、opencv、tensorrt并进行相应的环境配置,这里不做配

    2024年02月11日
    浏览(38)
  • Ubuntu环境下C++使用onnxruntime和Opencv进行YOLOv8模型部署

    目录 环境配置 系统环境 项目文件路径  文件环境  config.txt  CMakeLists.txt type.names  读取config.txt配置文件 修改图片尺寸格式 读取缺陷标志文件 生成缺陷随机颜色标识 模型推理 推理结果获取 缺陷信息还原并显示 总代码 Ubuntu18.04 onnxruntime-linux-x64 1.12.1:https://github.com/microsof

    2024年01月17日
    浏览(46)
  • YOLOV8代码本地编译

    下载pycharm 在 Linux 操作系统中安装 Pycharm 社区版_linux中安装pycharm社区版-CSDN博客 Pycharm中配置 Conda 虚拟环境 百度安全验证 https://baijiahao.baidu.com/s?id=1771914506705481878wfr=spiderfor=pc 源码编译 果您对参与开发感兴趣或希望尝试最新源代码,请克隆 ultralytics 仓库。克隆后,导航到目录

    2024年01月20日
    浏览(39)
  • yolov8-pose:在yolov8上添加人体关键点检测

        最近因为工作关系接触了yolo-pose,1月份yolov8也出来了,就想着能不能在yolov8上也加上pose分支,也算加深对网络的认识。     yolov8在数据处理上也考虑了keypoints的,所以数据处理部分不用太多的修改,主要修改了Detect类、Loss类。     Detect类:__init__方法中加入nkpt以及c

    2024年02月11日
    浏览(45)
  • 【AI】在NVIDIA Jetson Orin Nano上使用tensorrtx部署yolov8

    本人下载的yolov8n.pt yolov8n-cls.pt:用于分类 yolov8n-pose.pt:用于姿势识别 yolov8n-seg.pt:用于对象分割 yolov8n-v8loader.pt:专用于人员检测器??? yolov8n.pt:用于对象检测 1)测试图片

    2024年02月06日
    浏览(51)
  • YOLOv8如何添加注意力模块?

    分为两种:有参注意力和无参注意力。 eg: 有参: 无参: 1、在nn文件夹下新建attention.py文件,把上面俩代码放进去 2、在tasks.py文件里面导入俩函数 3、在解析函数里面添加解析代码 c1:上一层的输出通道数,也是这一层的输入通道数 C2:该层的输出通道数,即将成为下一层的输

    2024年02月07日
    浏览(43)
  • yolov7使用onnx推理(带&不带NMS)

    官方代码: GitHub - WongKinYiu/yolov7: Implementation of paper - YOLOv7: Trainable bag-of-freebies sets new state-of-the-art for real-time object detectors         上个月,官方放了个使用onnx推理的ipynb文件,过了几天上去看,官方又给删了,不知道是不是要更新波大的,还好手快保存了一份,这个可以

    2024年02月16日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包