YOLOv8 基于NCNN的安卓部署

news2024/11/26 0:48:13

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  # BCHW
    for i in range(self.nl):
        x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
    if self.training:
        return x
    elif 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

    pred = 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  # BCHW
    for i in range(self.nl):
        x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
    if self.training:
        return x
    elif 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  # BCHW
    for i in range(self.nl):
        x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
    if self.training:
        return x
    elif 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

    pred = 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.coloradmin.cn/o/2190440.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【STM32 Blue Pill编程实例】-OLED显示HC-SR04超声波测距结果

OLED显示HC-SR04超声波测距结果 文章目录 OLED显示HC-SR04超声波测距结果1、HC-SR04超声波传感器介绍2、硬件准备及接线模块配置3.1 定时器配置3.2 OLED I2C接口配置3.3 HC-SR04引脚配置4、代码实现在本文中,我们将 HC-SR04 超声波传感器与 STM32 Blue Pill 开发板结合使用,并…

Python-函数与数据容器超详解

1.函数的定义 函数是:组织好的、可重复使用的、用来实现特定功能的代码段。它的优点:将功能封装在函数内,可供随时随地重复利用,提高代码的复用性,减少重复代码,提高开发效率 Python函数的定义方式&#…

Perforce演讲回顾(上):从UE项目Project Titan,看Helix Core在大型游戏开发中的版本控制与集成使用策略

日前,Perforce携手合作伙伴龙智一同亮相Unreal Fest 2024上海站,分享Helix Core版本控制系统及其协作套件的强大功能与最新动态,助力游戏创意产业加速前行。 Perforce解决方案工程师Kory Luo在活动主会场,带来《Perforce Helix C…

论文理解【LLM-CV】—— 【MAE】Masked Autoencoders Are Scalable Vision Learners

文章链接:Masked Autoencoders Are Scalable Vision Learners代码:GitHub - facebookresearch/mae发表:CVPR 2022领域:LLM CV一句话总结:本文提出的 MAE 是一种将 Transformer 模型用作 CV backbone 的方法&#xff0c…

新闻推荐系统:Spring Boot的可扩展性

6系统测试 6.1概念和意义 测试的定义:程序测试是为了发现错误而执行程序的过程。测试(Testing)的任务与目的可以描述为: 目的:发现程序的错误; 任务:通过在计算机上执行程序,暴露程序中潜在的错误。 另一个…

csp-j模拟五补题报告

前言 今天第二题文件名把 r 写成 t 了 又跌出前10名了 白丢了好多分 &#xff08;“关于二进制中一的个数的研究与规律”这篇文章正在写&#xff09; 第一题 牛奶(milk) 我的代码&#xff08;AC&#xff09; #include<bits/stdc.h> #define ll long long #define fi …

Acwing 线性DP

状态转移方程呈现出一种线性的递推形式的DP&#xff0c;我们将其称为线性DP。 Acwing 898.数字三角形 实现思路&#xff1a; 对这个三角形的数字进行编号&#xff0c;状态表示依然可以用二维表示&#xff0c;即f(i,j),i表示横坐标&#xff08;横线&#xff09;&#xff0c;j表…

pygame--超级马里奥(万字详细版)

超级马里奥点我下载https://github.com/marblexu/PythonSuperMario 1.游戏介绍 小时候的经典游戏&#xff0c;代码参考了github上的项目Mario-Level-1&#xff0c;使用pygame来实现&#xff0c;从中学习到了横版过关游戏实现中的一些处理方法。原项目实现了超级玛丽的第一个小…

Windows应急响应-Auto病毒

文章目录 应急背景分析样本开启监控感染病毒查看监控分析病毒行为1.autorun.inf分析2.异常连接3.进程排查4.启动项排查 查杀1.先删掉autorun.inf文件2.使用xuetr杀掉进程3.启动项删除重启排查入侵排查正常流程 应急背景 运维人员准备通过windows共享文档方式为公司员工下发软件…

【Java】Java面试题笔试

[赠送]面试视频指导简历面试题 Java面试笔试题题库华为 java笔试面试题2014.files 就业相关java 面试题 面试题库 视频笔记 java笔试题大集合及答案 java面试书籍源码 java算法大全源码包8张图解 java.docx25个经典的Spring面试问答.docx 25个经典的Spring面试问答.docx 100家大…

知识图谱入门——10:使用 spaCy 进行命名实体识别(NER)的进阶应用:基于词袋的实体识别与知识抽取

在构建知识图谱的过程中&#xff0c;如何准确地识别和提取实体是关键。spaCy 提供了强大的命名实体识别&#xff08;NER&#xff09;功能&#xff0c;我们可以结合自定义规则和工具来实现更精准的实体抽取。本文将详细探讨如何在 spaCy 中实现自定义实体抽取&#xff0c;包括使…

OpenAI 推出 Canvas 工具,助力用户与 ChatGPT 协作写作和编程

OpenAI 近日推出了一款名为 Canvas 的新工具&#xff0c;旨在帮助用户更高效地与 ChatGPT 协作进行写作与编程。 Canvas 允许用户在一个独立窗口中与 ChatGPT 实时协作修改内容。无论是改进文本、调整语言风格、审查代码&#xff0c;还是在不同编程语言间转换&#xff0c;Canv…

Js逆向分析+Python爬虫结合

JS逆向分析Python爬虫结合 特别声明&#x1f4e2;&#xff1a;本教程只用于教学&#xff0c;大家在使用爬虫过程中需要遵守相关法律法规&#xff0c;否则后果自负&#xff01;&#xff01;&#xff01; 完整代码地址Github&#xff1a;https://github.com/ziyifast/ziyifast-co…

自闭症干预寄宿学校:为孩子搭建沟通与社交的桥梁

在探索自闭症儿童教育的广阔天地里&#xff0c;一所优秀的寄宿学校不仅是知识的殿堂&#xff0c;更是孩子们学习沟通与社交技能的桥梁。位于广州的星贝育园自闭症儿童寄宿制学校&#xff0c;正是这样一所专注于为自闭症儿童提供全面、个性化教育服务的机构&#xff0c;它以其独…

Linux-du命令使用方法

Linux-du&#xff08;disk useage&#xff09;命令 du 命令用于查看文件和目录占用的磁盘空间。 du [选项] [文件或目录]-h (human-readable)&#xff1a; 将输出格式转为人类可读的形式&#xff0c;使用 KB、MB 等单位。 du -h /path/to/directory1.5M /path/to/directory…

Pikachu-SSRF(curl / file_get_content)

SSRF SSRF是Server-side Request Forge的缩写&#xff0c;中文翻译为服务端请求伪造。产生的原因是由于服务端提供了从其他服务器应用获取数据的功能且没有对地址和协议等做过滤和限制。常见的一个场景就是&#xff0c;通过用户输入的URL来获取图片。这个功能如果被恶意使用&am…

Linux 之 安装软件、GCC编译器、Linux 操作系统基础

安装软件、GCC编译器、Linux 操作系统基础 学习任务&#xff1a; 安装 Vmware虚拟机、掌握Ubuntu 系统的使用认识 Ubuntu 操作系统的终端和 Shell掌握软件安装、文件系统、掌握磁盘管理与解压缩掌握 VIM 编辑器、Makefile 基本语法熟悉 Linux 常见指令操作 安装好开发软件&…

力扣189.轮转数组

给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4…

数据库(MySQL):使用命令从零开始在Navicat创建一个数据库及其数据表(一).创建基础表

一. 使用工具和命令 1.1 使用的工具 Navicat Premium 17 &#xff1a;“Navicat”是一套可创建多个连接的数据库管理工具。 MySQL版本8.0.39 。 1.2 使用的命令 Navicat中使用的命令 命令命令解释SHOW DATABASES&#xff1b;展示所有的数据库CREATE DATABASE 数据库名称; 创…

10以内数的分解

// 10以内数的分解.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 //#include <iostream> using namespace std; int main(int argc, char* argv[]){for (int i 2; i < 10; i){for (int j 1; j < i; j){printf("%d%d%d ",j…