YOLOv8 基于NCNN的安卓部署

YOLOv8 NCNN安卓部署

前两节我们依次介绍了基于YOLOv8的剪枝和蒸馏

本节将上一节得到的蒸馏模型导出NCNN,并部署到安卓。

NCNN 导出

YOLOv8项目中提供了NCNN导出的接口,但是这个模型放到ncnn-android-yolov8项目中你会发现更换模型后app会闪退。原因我们后面说明,总之,在导出之前,我们需要做一些源代码修改。

第一步,修改ultralytics/nn/modules/block.py中的C2f类的forward函数

# def forward(self, x):
#     """Forward pass through C2f layer."""
#     y = list(self.cv1(x).chunk(2, 1))
#     y.extend(m(y[-1]) for m in self.m)
#     return self.cv2(torch.cat(y, 1))
def forward(self, x):x = self.cv1(x)x = [x, x[:, self.c:, ...]]x.extend(m(x[-1]) for m in self.m)x.pop(1)return self.cv2(torch.cat(x, 1))

做上述修改的原因,GPT的回答大致意思是原始代码中chunk和list都是动态操作,而NCNN、ONNX这些都是静态图,所以最好不要出现这些操作。在我看来有些强行解释了。但是修改后的代码肯定比修改前的代码好的地方在于,少进行了一次split和merge的操作。因为我们遍历m计算的时候只使用了x的后半部分,所以我们可以只切片出后半部分用于计算,然后merge之前第一个元素是前半部分+后半部分,第二个元素是后半部分,所以我们pop(1)去掉这个后半部分之后再merge即可实现跟修改前等价的操作,同时少了一次chunk split和merge。

修改ultralytics/nn/modules/head.py中的Detect类的forward函数

# def forward(self, x):
#     """Concatenates and returns predicted bounding boxes and class probabilities."""
#     if self.end2end:
#         return self.forward_end2end(x)
#
#     for i in range(self.nl):
#         x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
#     if self.training:  # Training path
#         return x
#     y = self._inference(x)
#     return y if self.export else (y, x)
def forward(self, x):shape = x[0].shape  # BCHWfor i in range(self.nl):x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)if self.training:return xelif self.dynamic or self.shape != shape:self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))self.shape = shapepred = torch.cat([xi.view(shape[0], self.no, -1).permute(0, 2, 1) for xi in x], 1)return pred#def _inference(self, x):
#     """Decode predicted bounding boxes and class probabilities based on multiple-level feature maps."""
#     # Inference path
#     shape = x[0].shape  # BCHW
#     x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
#     if self.dynamic or self.shape != shape:
#         self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
#         self.shape = shape
# 
#     if self.export and self.format in {"saved_model", "pb", "tflite", "edgetpu", "tfjs"}:  # avoid TF FlexSplitV ops
#         box = x_cat[:, : self.reg_max * 4]
#         cls = x_cat[:, self.reg_max * 4:]
#     else:
#         box, cls = x_cat.split((self.reg_max * 4, self.nc), 1)
# 
#     if self.export and self.format in {"tflite", "edgetpu"}:
#         # Precompute normalization factor to increase numerical stability
#         # See https://github.com/ultralytics/ultralytics/issues/7371
#         grid_h = shape[2]
#         grid_w = shape[3]
#         grid_size = torch.tensor([grid_w, grid_h, grid_w, grid_h], device=box.device).reshape(1, 4, 1)
#         norm = self.strides / (self.stride[0] * grid_size)
#         dbox = self.decode_bboxes(self.dfl(box) * norm, self.anchors.unsqueeze(0) * norm[:, :2])
#     else:
#         dbox = self.decode_bboxes(self.dfl(box), self.anchors.unsqueeze(0)) * self.strides
# 
#     return torch.cat((dbox, cls.sigmoid()), 1)

这部分的修改就有一个大坑,我在这里卡了很长时间。我们先看看这段代码做了什么修改。

首先,原始的代码需要调用_inference函数,关于export啥的在我们推理的时候都不需要,所以只需看else分支即可。end2end我们也不需要。所以下面这部分代码是一致的:

	shape = x[0].shape  # BCHWfor i in range(self.nl):x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)if self.training:return xelif self.dynamic or self.shape != shape:self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))self.shape = shape

这段代码之后呢,原始的代码有三步操作,即

box, cls = x_cat.split((self.reg_max * 4, self.nc), 1)
dbox = self.decode_bboxes(self.dfl(box), self.anchors.unsqueeze(0)) * self.strides
return torch.cat((dbox, cls.sigmoid()), 1)

前两行代码其实是在计算框的xywh四个值。而修改后的代码跳过了这个部分,直接cat,然后我们如果调用这个模型打印输出维度会发现他是一个1x8400x144的维度。而经过原始代码的输出是1x84x8400的维度。

84好理解,就是80个类别+4个bbox参数(x,y,w,h),由于YOLOv8去掉了置信度分支,所以一共就是84个值;

而144的含义其实是80+16x4,80依然表示类别数量,4表示bbox参数(lrtb),16来源于regmax的定义,表示区间分段大小。我们知道,YOLOv8的框参数的计算不是常规的回归方式,而是视作基于DFL的分类问题。也就是说我们可以认为边界框的参数一定在某个范围0-regmax之内,比如regmax为16的时候,在特征图上可以表示32的长度,还原到图像上为32x32 = 1024>640,所以对于640的输入,这个范围完全可以覆盖所有目标的尺度。然后YOLOv8会预测16个概率值,基于这个概率分布计算期望即为lrtb的长度,然后后续再转成xywh格式即可。

所以原始的代码就是多了上述这个边界框解码的过程。那么为什么要去掉呢?原因就是ncnn-android-yolov8这个项目中把这部分的后处理使用c++实现了。为什么用c++实现我的理解是这类后处理代码基本没法基于NCNN加速,使用c++实现会更加高效。

还有一个点我没有提到的是,为什么原本1x84x8400的维度,为什么修改后变成了1x8400x144,也就是发生了维度交换。这里就是一个大坑。首先我先说为什么要交换维度,因为ncnn-android-yolov8是基于交换维度之后的输出进行的,如果不加维度交换而是在c++里面基于ncnn编写维度交换的代码就比较麻烦了,首先ncnn::Mat类没有这一类的api实现,可能你要么基于ncnn创建一个Layer(“Permute”),要么就写for循环转移,总之,会很麻烦。所以我们尽可能在导出之前就完成这个维度的交换。

现在来看这段修改后的代码:

def forward(self, x):shape = x[0].shape  # BCHWfor i in range(self.nl):x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)if self.training:return xelif self.dynamic or self.shape != shape:self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))self.shape = shapepred = torch.cat([xi.view(shape[0], self.no, -1).permute(0, 2, 1) for xi in x], 1)return pred

注意到在cat之前,对每一个xi我都进行了一遍permute交换维度。你可能会想,为什么要这样呢?我先cat完然后再permute不就只需要一次permute了吗。恭喜你,我开始也是这么想的,网上的教程也是这么想的,并且ncnn-android-yolov8中给出的修改示例还是这么想的。

在这里插入图片描述

然后你就会运行yolo.export导出ncnn并且测试了一下输出shape之后发现,维度还是1x144x8400,根本没有交换过来。

这里我可能还有一点东西没有介绍,就是ncnn的导出是这样的。

我们改完代码之后直接yolo.export(format=“ncnn”)就行了。然后会看到路径下生成这样一个文件夹

其中,model.ncnn.bin和model.ncnn.param都是模型推理需要加载的文件,model_ncnn.py是一个加载ncnn模型推理的测试脚本。

回到我刚才说的,发现输出不对的话,我们可以打开model.ncnn.param查看一下,这里面可以看到导出的模型的层结构。

然后可以看到,导出的模型在Concat之后就完了,Permute层没有导出。开始以为可能是不支持Permute操作,然后换成了Transpose也不行。就卡在这里很长时间,一度让我开始基于c++和ncnn考虑怎么在android端实现这个维度转换,但是最终还是放弃了,重新回到这个问题。

最后我在issues里面看到这个回答:

在这里插入图片描述

基于pnnx转出的模型会自动删除末尾的reshape和permute。破案了破案了。。。

其实网上给出的教程很多会给我们一个网站https://convertmodel.com,但是这个网站已经挂了,现在来看,这个网站走的模型导出路线肯定是pytorch->onnx->ncnn,而yolov8默认的路线是pytorch->torchscript->ncnn,其中,torchscript到ncnn的转换是通过pnnx实现的。这也就是为什么这些教程都没有遇到我这个问题。

那么基于上述问题,我们应该怎么办呢?既然pnnx会删除最后的permute的操作,那么我们能不能把permute提前呢?还好,是可以的,也就是我给出的修改代码。

好了,到了这一步就结束了吗?还没有,如果你兴高采烈地拿着导出地模型放到android studio,重新编译运行app,然后就会。。。闪退。

因为YOLOv8的推理是基于letterbox操作的,真实推理时模型的输入不会是标准的640x640,但是我们导出的时候是默认640x640的,并且只能接受这个尺寸。我不想在c++端强行转成640x640,这会影响模型性能。那么剩余的方案就是修改输入shape或者希望导出的模型支持动态shape了。我做的是后者,因为我考虑到app上推理图片的尺寸可能总是大小不一的。

还好,要实现动态shape操作比较简单,我们只需要手动修改一下model.ncnn.param文件即可

在这个文件的最后,我们可以看到有三个Reshape操作,关键就在这个Reshape操作了,我们只需要把6400,1600,400都改成-1即可

我解释一下这些数字的含义,以180行和181行为例:

1 1:这是输入和输出的数量。第一个数字是输入的数量,第二个数字是输出的数量。

189 212:这是输入和输出的索引。第一个数字是输入的索引,第二个数字是输出的索引。索引是根据参数文件中的顺序来确定的。

0=6400 1=144:这是层的参数。0=6400表示将输入重塑为6400个元素,1=144表示每个元素有144个通道。

对于180行的Permute层,0=1表示将通道维度移到最前面。

至此,NCNN模型导出完毕。

NCNN Android部署

前面我们提到多次ncnn-android-yolov8,这是一个基于开源库,实现了c++版基于ncnn的yolov8模型推理和后处理等操作,并且提供了一个简易的demo。

在开始之前,我们需要预先准备一些东西:

  • ncnn-android-yolov8
  • ncnn-20240410-android-vulkan
  • opencv_mobile-2.4.13.7
  • Android Studio

ncnn-android-yolov8我们下载解压之后只需要ncnn-android-yolov8这个文件夹即可。

在这里插入图片描述

此外,下载的ncnn-20240410-android-vulkan和opencv_mobile-2.4.13.7解压后放到ncnn-android-yolov8/app/src/main/jni文件夹下。如下:(我多下了一些版本,调试过程产物,实际用啥都行)

然后修改CMakeLists.txt中opencv和ncnn的路径

project(yolov8ncnn)cmake_minimum_required(VERSION 3.10.1)set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/opencv-mobile-2.4.13.7-android/sdk/native/jni)
find_package(OpenCV REQUIRED core imgproc)set(ncnn_DIR ${CMAKE_SOURCE_DIR}/ncnn-20240410-android-vulkan/${ANDROID_ABI}/lib/cmake/ncnn)
find_package(ncnn REQUIRED)add_library(yolov8ncnn SHARED yolov8ncnn.cpp yolo.cpp ndkcamera.cpp)target_link_libraries(yolov8ncnn ncnn ${OpenCV_LIBS} camera2ndk mediandk)

随后,你就可以打开Android Studio加载这个项目了。

我们现在需要额外的东西:

  • cmake:3.10.2
  • JDK:corretto-11
  • SDK:30
  • NDK:27.1

比较关键的是上面几个,这些都可以在Android Studio中下载,而无需单独下载配置。我之前过程中有遇到过一些版本问题,如果你发现自己的编译报错的话,试试跟我的版本保持一致。

还有一些别的版本,总之所有的版本设置都在下面了,剩下就是对Android Studio的探索了。

另外,如果你发现编译下载文件失败的话,在Android Studio设置proxy,同时本地打开全局代理。

经过上面的编译基本就可以正常运行了,具体的效果暂时就不展示了,项目还在整理当中。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/439741.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[ComfyUI]Flux:太强了!任意扩图神器,小红书极致逼真风格出游打卡写实风

随着人工智能技术的不断发展,图像生成与反推技术已经成为了AI领域的一大热点。今天,我们就来为大家详细介绍一款由ComfyUI团队开发的超强图像反推工具——Flux,以及如何使用它实现任意扩图和极致逼真风格出游打卡写实风。 一、Flux&#xff…

【AI大模型】使用Embedding API

一、使用OpenAI API 目前GPT embedding mode有三种,性能如下所示: 模型每美元页数MTEB得分MIRACL得分text-embedding-3-large9,61554.964.6text-embedding-3-small62,50062.344.0text-embedding-ada-00212,50061.031.4 MTEB得分为embedding model分类…

centos7安装配置nginx

先安装依赖 安装依赖之前最好先执行下update yum update yum install gcc gcc-c pcre pcre-devel zlib zlib-devel openssl openssl-devel -y cd /usr/local/nginx wget http://nginx.org/download/nginx-1.18.0.tar.gz tar -zxvf nginx-1.18.0.tar.gz cd /usr/local/ngi…

双非本 985 硕,上岸快手大模型算法岗!

最近已有不少大厂都在秋招宣讲,也有一些已在 Offer 发放阶段了。 节前,我们邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对新手如何入门算法岗、该如何准备面试攻略、面试常考点、大模型技术趋势、算法项目落地经验分享等热门话题进行…

高校校园交友系统小程序的设计

管理员账户功能包括:系统首页,个人中心,管理员管理,用户管理,基础数据管理,论坛管理,公告信息管理,轮播图信息管理 微信端账号功能包括:系统首页,用户&#…

反调试—1

IsDebuggerPresent() CheckRemoteDebuggerPresent() 其内部实际调用NtQueryInformationProcess() bool _stdcall ThreadCall() {while (true){BOOL pbDebuggerPresent FALSE;CheckRemoteDebuggerPresent(GetCurrentProcess(), &pbDebuggerPresent);if (pbDebuggerPres…

fiddler抓包18-2_导出jmeter、postman脚本(带请求头)

课程大纲 1. Fiddler导出请求为curl脚本 选中请求,“文件” - “导出会话” - “选中的会话” - “cURL Script”。 2. 导入jmeter ① 复制curl脚本。 ② 打开jmeter,“工具” - “import from cURL”,粘贴脚本,勾选“Add cooki…

二分查找一>寻找峰值

1.题目&#xff1a; 2.解析&#xff1a; 暴力遍历代码&#xff1a;O(N),由于该题数据很少所以可以通过 暴力遍历&#xff1a;O(N),由于该题数据很少所以可以通过int index 0;for(int i 1; i < nums.length-1; i) {//某段区域内一直递增&#xff0c;更新就indexif(nums[i]…

红黑树学习

红黑树: k v 方式 用在哪里&#xff1a; 1.hash 强查找的过程&#xff1a; 1.rbtree 2.hash 3.b/b tree 4.链表 红黑树&#xff1a; 1.每个结点是红的或者是黑的 2.根结点是黑的 3.每个叶子结点是黑的 4.如果一个结点是红的&#xff0c;则它的两个儿子是黑的 5.对每个节点&…

性能学习5:性能测试的流程

一.需求分析 二.性能测试计划 1&#xff09;测什么&#xff1f; - 项目背景 - 测试目的 - 测试范围 - ... 2&#xff09;谁来测试 - 时间进度与分工 - 交付清单 - ... 3&#xff09;怎么测 - 测试策略 - ... 三.性能测试用例 四.性能测试执行 五.性能分析和调优 六…

ElasticSearch备考 -- Search across cluster

一、题目 配置两个集群&#xff0c;集群名称为my-application-01、my-application-02&#xff0c;导入es自带Sample flight data数据集&#xff0c;配置扩集群检索&#xff0c;查询数据 二、思考 准备工作有两个集群&#xff0c;并需要对集群配置角色中增加 remote_cluster_cl…

【优选算法】(第八篇)

目录 串联所有单词的⼦串&#xff08;hard&#xff09; 题目解析 讲解算法原理 编写代码 最⼩覆盖⼦串&#xff08;hard&#xff09; 题目解析 讲解算法原理 编写代码 串联所有单词的⼦串&#xff08;hard&#xff09; 题目解析 1.题目链接&#xff1a;. - 力扣&#…

光伏组件模型模板在SketchUp中如何完成成模数化设计?

选中模板组件&#xff0c;点击左侧工具栏中移动工具&#xff0c;按住Ctrl再依次点击组件起始点和终点&#xff0c;完成组件复制&#xff0c;输入需要复制的组件数量&#xff08;*n&#xff09;后回车&#xff0c;即可完成模数化设计。 选中模组的多块模型右键进行创建组件或群…

高考技术——pandas使用

百家讲坛&#xff0c;谈论古今&#xff0c;今天我们不聊别的&#xff0c;我们来聊一聊中国的国宝——大熊猫&#xff08;bushi&#xff09; 好好&#xff0c;言归正传&#xff0c;我们今天来讲pandas import pandas as pd 申明无需多言&#xff0c;高考主要考察Series和Data…

【Docker】docker的存储

介绍 docker存储主要是涉及到3个方面&#xff1a; 第一个是容器启动时需要的镜像 镜像文件都是基于图层存储驱动来实现的&#xff0c;镜像图层都是只读层&#xff0c; 第二个是&#xff1a; 容器读写层&#xff0c; 容器启动后&#xff0c;docker会基于容器镜像的读层&…

多文件并发多线程MD5工具(相对快速的MD5一批文件),适配自定义MD5 Hash I/O缓存。

自己写的多文件 MD5校验工具&#xff0c;一个文件开一个线程&#xff0c;有最大I/O 缓存设置&#xff0c;兼容读写MD5后缀文件。 共计91个文件&#xff0c;合计180G左右 12分钟左右&#xff0c;UI基本卡废&#xff0c;但程序没蹦&#xff0c;属于正常。 卡的原因是基本是用 I/O…

WSL2Linux 子系统(十二)

wsl 子系统安装 cuda 环境 《WSL2Linux 子系统(十一)》讲述 WSL 网络转为桥接模式的两种方法&#xff0c;WSL 网络桥接模式无论是静态 IP 还是动态分配 IP 均支持。本篇文章则是简单讲述 WSL 安装 cuda 环境。 作者&#xff1a;炭烤毛蛋 &#xff0c;点击博主了解更多。 提示…

RabbitMQ的各类工作模式介绍

简单模式 P: ⽣产者, 也就是要发送消息的程序 C: 消费者,消息的接收者 Queue: 消息队列, 图中⻩⾊背景部分. 类似⼀个邮箱, 可以缓存消息; ⽣产者向其中投递消息, 消费者从其中取出消息.特点: ⼀个⽣产者P&#xff0c;⼀个消费者C, 消息只能被消费⼀次. 也称为点对点(Point-to-…

从零开始构建大型语言模型——实现注意力机制

本章内容&#xff1a; 使用注意力机制的原因基本的自注意力框架&#xff0c;逐步深入到增强的自注意力机制允许LLMs逐个生成词元的因果注意力模块通过dropout随机屏蔽部分注意力权重以减少过拟合将多个因果注意力模块堆叠为多头注意力模块 到目前为止&#xff0c;你已经了解了…

参数标准+-db和-db

-db是因为比值是相近的&#xff0c;值越进行越好&#xff0c;正负db代表两个值差异不大&#xff0c;可以分子比分母大或者分母比分子大-db代表串扰&#xff0c;分子比分母小&#xff0c;所以负db的值越小越好