将yolov8部署到安卓——安装

​ 上一篇文章我们介绍了模型部署部署的概念,接下来我们尝试安装ncnn并且让它跑起来。由于我们目前只是安装看看效果,所以我们选择编译安装Window的版本,等到开发时再编译Android版本,至于为什么需要编译,下面是GPT给出的回答:

NCNN的源代码需要编译,主要是因为它是一个跨平台的C++框架,而编译是将源代码转换为可执行代码或库文件的过程。下面是几个原因:

  1. 平台适配:NCNN需要在不同的操作系统和硬件平台上运行,例如Android、iOS、Windows、Linux等。编译过程中,编译器会根据目标平台的不同进行适配和优化,以确保NCNN能够在目标平台上正确地运行。
  2. 代码优化:编译过程中,编译器会对源代码进行优化,以提高程序的性能和效率。优化后的代码通常比未优化的代码更快、更稳定。
  3. 依赖解析:NCNN可能依赖于其他库或工具,例如OpenMP、Eigen等。编译过程中,编译器会解析这些依赖关系,并确保所有必要的依赖项都被正确地链接和包含在最终的二进制文件中。
  4. 生成可执行文件或库文件:编译过程将源代码转换为可执行文件或库文件,这样用户就可以直接使用NCNN提供的功能,并将它集成到自己的项目中。

综上所述,编译NCNN的源代码是为了生成能够在目标平台上运行的可执行文件或库文件,以便用户能够使用和集成NCNN框架。

​ 但是官方是有预编译好的版本的,就在https://github.com/Tencent/ncnn/releases,但是我搜了一圈也没找到预编译库的使用方法,无奈只能自己编译(大学四年各种安装环境的PTSD……

编译Protobuf

​ 根据官方的文档,在编译ncnn之前似乎需要先编译protobuf。但是protobuf是什么东西?Google!!!启动!!!以下是Wiki的解释:

Protocol Buffers(简称:ProtoBuf)是一种开源跨平台的序列号数据结构的协议。其对于存储资料或在网络上进行通信的程序是很有用的。这个方法包含一个接口描述语言,描述一些数据结构,并提供程序工具根据这些描述产生代码,这些代码将用来生成或解析代表这些数据结构的字节流。

​ 完全看不懂,找到另一段解释:

protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。

你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。

作者:401
链接:https://www.jianshu.com/p/a24c88c0526a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

​ 看来Protobuf也是一种和XML和JSON类似的结构数据序列号方法(只是我的非常粗浅的理解,欢迎指正,但是不要喷我),并且是跨平台,似乎就能够解释为什么Protobuf也需要编译了。我下载的是protobuf-3.4.0版本。但是官网上这玩意都到V26了,好奇这些新版本到底都是谁在用。下载下来后,打开VS2019自带的命令行(注意有x64也有x86版本的,我还不知道有什么区别,只是看自己的电脑是64的就进了64的)进入到下载解压后的Protobuf目录。

一些进入退出目录的命令行指令:

1
2
3
4
5
6
7
>进入D盘: D:

>进入指定文件夹: cd xxx/xxx/xxx

>返回上一级: cd ..

>创建文件夹:mkdir xxx

​ 然后输入下面的指令,等一下,我忘了先说安装CMake的事情了。

1
2
3
4
mkdir build-vs2019
cd build-vs2019
cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%cd%/install -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_MSVC_STATIC_RUNTIME=OFF ../cmake
nmake

安装CMake

​ 老样子,先查查CMake是什么,移步这里查看。在这里下载,我下载的是3.16.5版本。

​ 下载解压后添加环境变量即可。

​ 安装好CMake之后就可以继续编译Protobuf了,编译成功后build-vs2019文件夹下是这个样子:

编译ncnn

​ 先到ncnn官网把源码搞下来,我是采用的直接Clone的方法。

1
2
3
git https://github.com/Tencent/ncnn.git
cd ncnn
git submodule update --init

​ 注意最后一句不要漏了,否则编译的时候会报错说submodule缺失。同编译Protobuf时一样,进入到ncnn目录下,执行下面的指令。

1
2
3
4
5
mkdir -p build-vs2019
cd build-vs2019
cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%cd%/install -DProtobuf_INCLUDE_DIR=<替换成你的protobuf根目录,“>"也要去掉>/build-vs2019/install/include -DProtobuf_LIBRARIES=<替换成你的protobuf根目录,“>"也要去掉>/build-vs2019/install/lib/libprotobuf.lib -DProtobuf_PROTOC_EXECUTABLE=<替换成你的protobuf根目录,“>"也要去掉>/build-vs2019/install/bin/protoc.exe -DNCNN_VULKAN=OFF ..
nmake
nmake install
 注意这里的第三条指令结尾设置了一个参数,DNCNN_VULKAN=ON,这个参数可以简单的理解成是否调用GPU,我一开始选择的是ON,但是由于克隆ncnn源码时漏掉了submodule那一句,导致编译失败,当我重新运行完这句后,编译成功了,但是在代码调用ncnn时出现错误,于是我删掉编译完成后的目录,重新编译并且将VULKAN设置为OFF,这一次编译完成后的调用没有报错,所以我并不确定第一次的错误是因为中途编译失败导致的,还是由于选择了启用VULKAN但是没有安装VULKAN导致的。总之编译成功后的build-vs2019文件夹长这个样子:

安装OpenCV-Mobile

​ 之所以要用OpenCV是因为我们要用它来帮助看看效果,毕竟ncnn只是实现了网络,读入显示图片都是要OpenCV才行。我们这里选择OpenCV-Mobile,是一个针对移动端平台的精简版OpenCV,而且和ncnn是同一个作者,并且似乎ncnn有些实现也要用到OpenCV?OpenCV-Mobile编译好的版本可以在这里下载,我用的是opencv-mobile-4.6.0-windows-vs2019这个版本。下载完以后解压好就可以了。

在VS2019中配置ncnn、OpenCV依赖

​ 打开VS2019,新建一个最简单的Hello World项目,然后右键项目名,选择属性

​ 注意这两个地方都要选择Release和x64,如果只改一个地方是不生效的……

​ 接着依次把VC++目录->包含目录,链接器->常规,链接器->输入分别设置成下面的样子,话说OpenCV的预编译库都可以这样用,那ncnn的是不是类似,回头可以试试。

image-20240416161700263

image-20240416161852592

image-20240416161935740

​ 注意有些别的教程还会设置C/C++下的内容,也会添加Protobuf的内容,但是我没有加。

写代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include "cpu.h"
#include "net.h"
#include "gpu.h"
#include "benchmark.h"
#include "datareader.h"

#include<vector>
#include<stdio.h>
#include<algorithm>
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>

int demo(cv::Mat& image, ncnn::Net& detector, int detector_size_with, int detector_size_height) {
// 检测类别
static const char* class_names[] = {
"background","aeroplane","bicycle","bird","boat",
"bottle","bus","car","cat","chair","cow","diningtable",
"dog","horse","motorbike","person","pottedplant","sheep",
"sofa","train","tvmonitor"
};

cv::Mat bgr = image.clone();
int image_width = bgr.cols;
int image_height = bgr.rows;

ncnn::Mat input = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR2RGB,
bgr.cols, bgr.rows, detector_size_with, detector_size_height);
// 数据预处理
const float mean_vals[3] = { 0.f, 0.f, 0.f };
const float norm_vals[3] = { 1 / 255.f,1 / 255.f,1 / 255.f };
input.substract_mean_normalize(mean_vals, norm_vals);

ncnn::Extractor ex = detector.create_extractor();
ex.set_num_threads(8);
ex.input("data", input);
ncnn::Mat out;
ex.extract("output", out);

for (int i = 0; i < out.h; ++i) {
int label;
float x1, y1, x2, y2, score;
const float* values = out.row(i);

label = values[0];
score = values[1];
x1 = values[2] * image_width;
y1 = values[3] * image_height;
x2 = values[4] * image_width;
y2 = values[5] * image_height;
// 处理越界坐标
if (x1 < 0)x1 = 0;
if (y1 < 0)y1 = 0;
if (x2 < 0)x2 = 0;
if (y2 < 0)y2 = 0;
if (x1 > image_width)x1 = image_width;
if (y1 > image_height)y1 = image_height;
if (x2 > image_width)x2 = image_width;
if (y2 > image_height)y2 = image_height;

cv::rectangle(image, cv::Point(x1, y1), cv::Point(x2, y2), cv::Scalar(255, 255, 0), 1, 1, 0);

char text[256];
sprintf_s(text, "%s %.1f%%", class_names[label], score * 100);
int baseLine = 0;
cv::Size label_size = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
cv::putText(image, text, cv::Point(x1, y1 + label_size.height),
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
}
return 0;
}

int test() {
// 定义yolo-fatest检测器
ncnn::Net detector;
//模型和参数可以在https://github.com/dog-qiuqiu/Yolo-Fastest找到
detector.load_param("D:/yolo/test/yolo-fastest-1.1.param");
detector.load_model("D:/yolo/test/yolo-fastest-1.1.bin");
// 定义输入图像尺寸
int detector_size_width = 300;
int detector_size_height = 300;
// 测试图像
cv::Mat image = cv::imread("D:/yolo/test/20240416111210.png");
// 调用函数开始检测
demo(image, detector, detector_size_width, detector_size_height);
// 显示检测结果
cv::imshow("demo", image);
cv::waitKey(0);
return 0;
}

int main() {
test();
return 0;
}

​ 运行结果如下

​ 能够运行,并且检测框应该是对的,但是可以看到标签是错误的,不过应该是细节问题,起码说明ncnn的调用是没问题的。