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,同时本地打开全局代理。
经过上面的编译基本就可以正常运行了,具体的效果暂时就不展示了,项目还在整理当中。。。
相关文章:

YOLOv8 基于NCNN的安卓部署
YOLOv8 NCNN安卓部署 前两节我们依次介绍了基于YOLOv8的剪枝和蒸馏 本节将上一节得到的蒸馏模型导出NCNN,并部署到安卓。 NCNN 导出 YOLOv8项目中提供了NCNN导出的接口,但是这个模型放到ncnn-android-yolov8项目中你会发现更换模型后app会闪退。原因…...

【Python|接口自动化测试】使用requests发送http请求时添加headers
文章目录 1.前言2.HTTP请求头的作用3.在不添加headers时4.反爬虫是什么?5.在请求时添加headers 1.前言 本篇文章主要讲解如何使用requests请求时添加headers,为什么要加headers呢?是因为有些接口不添加headers时,请求会失败。 2…...

需求管理工具Jama Connect:与Jira/Slack/GitHub无缝集成,一站式解决复杂产品开发中的协作难题
在产品和软件开发的动态世界中,有效协作是成功的关键。然而,团队往往面临着阻碍进步和创新的重大挑战。了解这些挑战并找到强有力的解决方案,对于实现无缝、高效的团队协作至关重要。Jama Connect就是这样一种解决方案,它是一个功…...

CSP-J/S 复赛算法 背包DP
文章目录 前言背包DP的简介问题描述目标解决方法1. **定义状态**2. **状态转移方程**3. **初始化**4. **目标**举个例子动态规划解决背包问题的核心 DP背包问题示例代码问题描述代码实现核心代码讲解:举例:总结: 总结 前言 背包问题是算法竞…...

如何评估和部署 IT 运维系统?
如何才能将如此新兴、流行的技术转化为企业中实用的系统环境呢? 为此,我们采访了一家已经成功部署IT运维体系的大型企业的IT总监龙先生,请他给我们讲一下企业应该如何真正评估和部署自己的IT运维体系。 真理就是价值。 1.评估选择…...

正态分布的极大似然估计一个示例,详细展开的方程求解步骤
此示例是 什么是极大似然估计 中的一个例子,本文的目的是给出更加详细的方程求解步骤,便于数学基础不好的同学理解。 目标 假设我们有一组样本数据 x 1 , x 2 , … , x n x_1, x_2, \dots, x_n x1,x2,…,xn,它们来自一个正态分布 N…...

s7-200SMART编程软件下载
1、官网: STEP 7 Micro/WIN SMART V2.2 完整版http://w2.siemens.com.cn/download/smart/STEP%207%20MicroWIN%20SMART%20V2.2.zip STEP 7 Micro/WIN SMART V2.3 完整版http://w2.siemens.com.cn/download/smart/STEP%207%20MicroWIN%20SMART%20V2.3.iso STEP 7 Mi…...

Linux驱动开发常用调试方法汇总
引言:在 Linux 驱动开发中,调试是一个至关重要的环节。开发者需要了解多种调试方法,以便能够快速定位和解决问题。 1.利用printk 描述: printk 是 Linux 内核中的一个调试输出函数,类似于用户空间中的 printf。它用于…...

将列表中的各字符串sn连接成为一个字符串s使用;将各sn间隔开os.pathsep.join()
【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 将列表中的各字符串sn 连接成为一个字符串s 使用;将各sn间隔开 os.pathsep.join() [太阳]选择题 下列说法中正确的是? import os paths ["/a", "/b/c", "/d&q…...

算法题总结(八)——字符串
531、反转字符串二 给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。 如果剩余字符少于 k 个,则将剩余字符全部反转。如果剩余字符小于 2k 但大于或等于 k 个,…...

大数据开发--1.2 Linux介绍及虚拟机网络配置
目录 一. 计算机入门知识介绍 软件和硬件的概述 硬件 软件 操作系统概述 简单介绍 常见的系统操作 学习Linux系统 二. Linux系统介绍 简单介绍 发行版介绍 常用的发行版 三. Linux系统的安装和体验 Linux系统的安装 介绍 虚拟机原理 常见的虚拟机软件 体验Li…...

2024CSP-J复赛易错点
低级错误 不开long long见祖宗写代码要有输入,别没写输入就交写完代码要在本地测试,多想写极端测试数据,或对拍注意考官说文件夹怎么建,别文件夹建错,爆0别忘写freopen或忘给freopen去注释记着把.exe文件删掉考试时不…...

pytorch 与 pytorch lightning, pytorch geometric 各个版本之间的关系
主要参考 官方的给出的意见; 1. pytorch 与 pytorch lightning 各个版本之间的关系 lightning 主要可以 适配多个版本的 torch; https://lightning.ai/docs/pytorch/latest/versioning.html#compatibility-matrix; 2. pytorch 与 pytorch geometric 各…...

Spring Boot项目的创建与使用
1.通过IDE创建Spring Boot项目 2.目录结构 3.新建TestController控制器 Controller public class TestController {RequestMapping("/test")public ModelAndView test(RequestParam(name "name", defaultValue "刘德华") String name){ModelA…...

pytorch常用函数view、sum、sequeeze、cat和chunk
文章目录 view()函数sequeeze和unsequeezecat和chunk函数sum函数view()函数 view()相当于reshape、resize,重新调整Tensor的形状。 指定调整形状之后的维度import torch re = torch.tensor([1, 2, 3, 4, 5...

【STM32开发之寄存器版】(四)-独立看门狗IWDG
一 、前言 独立看门狗简介: STM32F103ZET6内置两个看门狗,提供了更高的安全性、时间的精确性和使用的灵活性。两个看门狗设备(独立看门狗和窗口看门狗)可用来检测和解决由软件错误引起的故障。 独立看门狗主要性能: 自由运行的递减计数器时钟…...

【S32K3 RTD MCAL 篇1】 K344 KEY 控制 EMIOS PWM
【S32K3 RTD MCAL 篇1】 K344 KEY 控制 EMIOS PWM 一,文档简介二, 功能实现2.1 软硬件平台2.2 软件控制流程2.3 资源分配概览2.4 EB 配置2.4.1 Dio module2.4.2 Icu module2.4.4 Mcu module2.4.5 Platform module2.4.6 Port module2.4.7 Pwm module 2.5 …...

华为OD机试真题---绘图机器(计算面积)
题目描述 绘图机器的绘图笔初始位置在原点(0,0),机器启动后按照以下规则绘制直线: 尝试沿着横线坐标正向绘制直线直到给定的终点E。期间可以通过指令在纵坐标轴方向进行偏移,offsetY为正数表示正向偏移,为负数表示负向偏移。 给…...
HarmonyOs 查看官方文档使用弹窗
1. 学会查看官方文档 HarmonyOS跟上网上的视频学习一段时间后,基本也就入门了,但是有一些操作网上没有找到合适教学的视频,这时,大家就需要养成参考官方文档的习惯了,因为官方的开发文档是我们学习深度任何一门语言或…...

uniapp+Android智慧居家养老服务平台 0fjae微信小程序
目录 项目介绍支持以下技术栈:具体实现截图HBuilderXuniappmysql数据库与主流编程语言java类核心代码部分展示登录的业务流程的顺序是:数据库设计性能分析操作可行性技术可行性系统安全性数据完整性软件测试详细视频演示源码获取方式 项目介绍 老年人 登…...

在一台电脑上实现网页与exe程序使用udp通信
要在同一台电脑上实现网页(前端)与 EXE 程序(后端)通过 UDP 通信,可以使用以下步骤。前端可以使用 JavaScript 通过 WebSocket 与自定义服务器进行通信,该服务器通过 UDP 发送和接收数据,再与 E…...

基于Java的GeoTools对Shapefile文件属性信息深度解析
目录 前言 一、Shapefile的属性列表信息 1、属性表格信息 2、属性表格包含的要素 二、GeoTools对属性表格的解析 1、常规解析方法 2、基于dbf文件的属性信息读取 三、总结 前言 ESRI Shapefile(shp),或简称shapefile,是美…...

付费计量系统实体和接口(1)
13.System entities and interfaces 系统实体和接口 See also Clause 4 for a discussion on general concepts and Clause 5 for generic entity model. 参见条目 4 讨论总体概念、条目 5 通用实体模型。 An entity specification should specify the embodied functions and …...

网易博客旧文----bacnet学习系列之四----VTS的初步使用
bacnet学习系列之四----VTS的初步使用 2014-02-07 13:32:28| 分类: BACnet | 标签: |举报 |字号大中小 订阅 这是一个测试用 的工具,而且是开放源码的,下载地址为:VTS下载官网 也可以从我的网盘下载 VTS下载 我用的是…...

SpringIoC容器的初识
一、SpringIoC容器的介绍 Spring IoC 容器,负责实例化、配置和组装 bean(组件)。容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。配置元数据以 XML、Java 注解或 Java 代码形式表现。它允许表达组成应用程序的组件以及这…...

队列的实现与讲解
一.概念与结构 1.概念 只允许在⼀端进行插⼊数据操作,在另⼀端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进⾏插⼊操作的⼀端称为队尾 出队列:进⾏删除操作的⼀端称为队头 注意&…...

hbuilderx+uniapp+Android健身房管理系统 微信小程序z488g
目录 项目介绍支持以下技术栈:具体实现截图HBuilderXuniappmysql数据库与主流编程语言java类核心代码部分展示登录的业务流程的顺序是:数据库设计性能分析操作可行性技术可行性系统安全性数据完整性软件测试详细视频演示源码获取方式 项目介绍 用户功能…...

自动驾驶-参考线生成
为什么要进行参考线生成? apollo在routing模块,已经得到的全局路径,但是其不能直接作为局部路径规划的参考线,这是因为: routing给出的全局路径过长;routing是基于高精地图给出的路径,高精地图…...

厂商资源分享网站
新华三(H3C)是一家中国知名的网络设备供应商,提供网络设备、网络解决方案和云计算服务。公司成立于2003年,是华为公司和惠普公司合资的企业,总部位于中国深圳。 华为(Huawei)是一家全球知名的电…...

【ONE·Web || HTML】
总言 主要内容:HTML基本知识入门,主要介绍了常见的一些标签使用,以及简单案例演示。 文章目录 总言0、前置说明1、认识HTML1.1、是什么1.2、初识 HTML 标签、HTML 文件基本结构1.2.1、相关说明1.2.2、vscode如何快速生成代码 2、HT…...