记录一下最近做的yolov8模型集成到android端的全过程。
1 yolo 模型概述
yolov8 模型是截止目前该系列最新的模型,可以完成检测、分类、分割任务,而且支持多种图像格式包括视频。它的分割任务已经不局限于矩形框的预测,可以完成多边形的物体分割。
2 模型训练
yolov8模型已经有了官方中文文档,模型训练的具体步骤均可参考主页 - Ultralytics YOLOv8 文档。
更加详细的步骤说明可以参考YOLOv8详解 【网络结构+代码+实操】-CSDN博客的实操部分。本章即基于此增加一些踩坑的细节。
2.1 环境配置
yolov8模型底层依赖Pytorch以及相关的视觉检测包,所以最好用conda配置python虚拟环境以免出现环境依赖不匹配的问题。
# 省略了配置conda的步骤
# 新建名为pytorch的虚拟环境,python=3.8
conda create -n pytorch python=3.8 -y
# 查看当前存在的虚拟环境
conda env list
# 进入名为pytorch的环境
conda activate pytorch
如果你认为你的模型仅用cpu可以完成训练,则可以跳过下面的步骤;如果你准备了Nvidia显卡,那就需要安装特殊版本的pytorch和touchVision。
Nvidia显卡的驱动平台cuda的版本也要与之对应,对应的torch版本在版本号后有类似cu102的序号说明是支持Nvidia显卡的cuda平台的。类似地,如果你期望使用amd显卡,你需要下载torch的cpu版本。
注意!不建议使用amd显卡去训练,坑非常的多!
你可以在https://download.pytorch.org/whl/中找到所需的文件。
pip install torch-1.9.0+cu102-cp38-cp38-linux_x86_64.whl
pip install torchvision-0.10.0+cu102-cp38-cp38-linux_x86_64.whl
最后,
# 克隆整个项目
git clone https://github.com/ultralytics/ultralytics
cd ultralytics
# 安装项目的其他依赖包
pip install -e .
2.2 数据集准备
yolov8支持使用命令行的模式完成整个训练,但是建议使用IDE如pycharm编辑python脚本的方式进行训练。
从本章之后的步骤都以检测(detect)任务为例,分类(classify)和分割(segment)任务大致类似。
新建自己的yaml文件
用IDE打开ultralytics项目,数据集相关配置的存放位置在ultralytics/ultralytics/cfg/datasets目录下。需要你模仿coco.yaml的格式配置你自己的数据集路径。
图中的names表示类别,比如这里只训练一种物体,那么只需一行,使用从0开始的标号方便后续步骤正常进行,图中的“tag”是你期望在预测边框上方显示的标签文本。
如上图所示,数据集目录名自定义(这里是shake),那么相应的你需要按照下面的结构准备数据。
shake
--images
--train #该目录放置训练集图片文件
--val #该目录放置测试集图片文件
--labels
--train #该目录放置训练集标签文本文件,文件名必须与对应的图片文件名一致
--val #该目录放置测试集标签文本文件
除此之外,还需要修改ultralytics/ultralytics/cfg/models/v8/yolov8.yaml文件里的配置,将“nc”属性改为你需要预测的类别数。
准备数据
准备你的训练集和测试集图片(测试集的目的是在训练是评估训练效果从而是模型自行优化),然后用Make Sense网站生成标签文件。
打标完成后点击左上角的Action,点击Export Annotations导出,选择导出格式为YOLO,下载文件后解压将得到与图片文件同名的txt文件,将其放在labels目录下。
标签文件的内容就是你打标的位置信息。
2.3 训练数据
检查cuda适配
在训练数据前,首先检查下cuda是否可用,否则请先解决环境配置问题。
import torch
print(torch.cuda.is_available()) # 返回为True表示可用
开始训练
使用下述代码进行模型训练:
from ultralytics import YOLO
# 加载模型
model = YOLO("yolov8n.yaml")
# model = YOLO("yolov8n.yaml").load("yolov8n.pt")
# 开始训练任务
model.train(data="你自己配置的yaml目录/myData.yaml",
epochs=500, patience=50, batch=-1, imgsz=640, lr0=0.005, device=0)
# 验证
model.val()
你可以在加载模型时载入预训练模型如yolov8n.pt,该文件可在https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt下载,也即官方帮你提前训练了80种常见的物体。
进行训练人物时的参数见下述说明:
- data: 你的数据集yaml配置文件的路径
- epoths: 训练轮数,默认为100轮,建议300轮。
- patience:早停的等待轮数,即等待一定轮数没有观察到模型性能提升,则停止训练。
- batch:每个批次放入的图片数量,默认为16,使用-1表示当前设备能容纳的最大数量。
- imgsz:输入图像的尺寸,并不是限定了数据集中图片的尺寸,在真正训练时会进行裁剪和拼接,这里设置越大,可以使得这些小对象从高分辨率中受益。
- device:指定训练设备,默认值为“cpu”,指定cpu则使用cpu训练,需要指定显卡训练则使用设备号,一般唯一安装的显卡的设备号为0。
其他参数的作用可以参考YOLOv8训练参数详解_yolov8n-CSDN博客。
完成训练
等待训练任务结束,在控制台可以看到训练结果保存的位置。
按上述步骤,可以得到下面的训练结果。
点击val_batch0_pred.jpg图片(如下图)查看验证集图片是否已经打上标签边框,如果没有或预测不全,则说明此次训练效果不佳,需要你调整训练参数或数据集进行优化。
2.4 预测数据
可以进一步建立预测任务检验模型的效果。
from ultralytics import YOLO
# 这里加载已经训练好的模型(.pt文件)
model = YOLO("你的训练结果目录/train/weights/best.pt")
# img图片路径可以是目录
img = ("你想要预测的图片路径")
# 保存文字结果,两个参数表示保存文字结果并带有置信度
# result = model.predict(source=img, save_txt=True, save_conf=True)
# 保存图片结果,默认在推理框上有标签文字和置信度
result = model.predict(source=img, save=True)
# 控制台直接打印结果:[类别序号, 左上角横坐标, 左上角纵坐标, 右上角横坐标, 右上角纵坐标, 可信度]
for r in result:
boxes = r.boxes
names = r.names
output = []
for d in reversed(boxes):
c, conf, id = int(d.cls), float(d.conf) if d.conf else None, None if d.id is None else int(d.id.item())
name = ('' if id is None else f'id:{id} ') + names[c]
label = (f'{name} {conf:.3f}' if conf else name)
xyxy = [float(f'{x:.3f}') for x in d.xyxy.tolist()[0]]
output.extend([c, xyxy[0], xyxy[1], xyxy[2], xyxy[3], float(f'{conf:.3f}')])
print(output)
3 部署到Android端
本章内容参考目标检测部署YOLOV8到安卓手机【Android Studio】 - 哔哩哔哩和将训练好的YOLOV8模型部署到【 Android Studio】 - 哔哩哔哩两篇文章。
第一篇文章中的demo试用的步骤已经很详细,这里只对第二篇文章进行补充。
需要下载的文件:
- demo项目:https://github.com/FeiGeChuanShu/ncnn-android-yolov8
- opencv-mobile: https://github.com/nihui/opencv-mobile
- ncnn-android-vulkan:https://github.com/Tencent/ncnn/releases
将上面下载好的两个压缩包解压后放入该位置:ncnn-android-yolov8\app\src\main\jni\ 下
有需要上述文件者还可以联系本人。
注意!这里省略了步骤,请按第一篇文章中的步骤将demo项目配置完全并测试是否能正常运行。
demo项目是一个调用摄像头,使用模型预测后实时在影像上绘制矩形框的android应用。
可能出现的问题:ndk版本太高,请尝试降低版本。
3.1 模型格式转换
经过第2章训练模型之后,默认得到的文件格式为pt后缀,通过导出 - Ultralytics YOLOv8 文档可以得知训练后可以导出多种个数的模型文件。
demo项目需要的是onnx文件再转换为ncnn文件,可以使用下面的代码将pt文件转换为onnx格式。
from ultralytics import YOLO
model = YOLO("你训练好的模型文件目录/train/weights/best.pt")
success = model.export(format="onnx", simplify= True, opset=12)
注意!使用demo项目在转换前需要对项目源码作出一些修改,这在推荐文章中没有说明。
待修改的内容在ncnn-android-yolov8-main/doc/中有显示,如使用检测任务则修改c2f.jpg和Detect.jpg两张图片上的内容。
也即把ultralytics项目中的下列函数修改为:
文件路径:ultralytics/ultralytics/nn/modules/block.py
class C2f(nn.Module):
# ...
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))
文件路径:ultralytics/ultralytics/nn/modules/head.py
class Detect(nn.Module):
# ...
def forward(self, x):
"""Concatenates and returns predicted bounding boxes and class probabilities."""
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
# 中间部分注释掉,return语句替换为
return torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2).permute(0, 2, 1)
注意!记得保留这两个函数原本的代码,这两处修改仅在格式转换时进行,如果想要重新训练,需要你再注释回去,使用原本的代码。
修改完成再执行模型格式转换的代码。
得到的文件类型为onnx格式,还需进一步转换为ncnn格式。使用一键转换 Caffe, ONNX, TensorFlow 到 NCNN, MNN, Tengine 即可。
转换后会得到两个文件,分别以bin和param做后缀。有一些关于yolov5的博客会提醒对param文件进行修改,这里使用yolov8模型已不再需要。两个文件即最终集成到android端的模型文件。
3.2 修改demo的配置
关于demo项目的修改请参考将训练好的YOLOV8模型部署到【 Android Studio】 - 哔哩哔哩。本文将补充直接使用so文件的部署方式。
demo项目解读:
- yolo.cpp和yolo.h:负责加载模型,执行预测任务,返回数据结果。
- ndkcamera.cpp和ndkcamera.h:负责摄像头相关以及实时绘制预测矩形框。
-
yolov8ncnn.cpp:JNI方法直接对应的C++文件,负责整合上述两部分。
推荐文章中修改的部分均在yolo.cpp和yolov8ncnn.cpp两个文件中。
如果你不希望以实时摄像的方式使用模型,只是想简单的拿到模型预测的数据结果或者想把数据结果之后的处理放在android端,可以参考下面的步骤。
3.3 获取预测的数据结果
新建JNI接口文件
由于项目原来的接口文件Yolov8Ncnn.java直接调用C++代码完成了绘制,而现在需要只拿到数据,因此新建一个接口文件YoloAPI.java
这里我希望直接传入可以代表图像的Bitmap数据。
public class YoloAPI {
public native boolean Init(AssetManager mgr, int cpugpu);
public class Obj
{
public float x;
public float y;
public float w;
public float h;
public int label;
public float prob;
}
public native Obj[] Detect(Bitmap bitmap, boolean use_gpu);
static {
System.loadLibrary("yolov8ncnn");
}
}
其中 Obj类接收返回数据,数据类型可以自行定义,需要在C++代码中做数据转换。
System.loadLibrary("yolov8ncnn");中的文件名与包含接口函数的C++文件名一致,这里我直接在原项目的yolov8ncnn.cpp文件中做的修改,因此直接使用这个名称。
实现接口方法
我们定义的Init方法和Detect方法需要在C++代码中实现,可以直接在yolov8ncnn.cpp中模仿Java_com_tencent_yolov8ncnn_Yolov8Ncnn_loadModel函数。
方法名必须与前面的定义一致,如YoloAPI所在包名为com.chyzh.yolo,那么应该这样定义Init函数。
static Yolo* g_yolo = 0;
static jclass objCls = NULL;
static jmethodID constructortorId;
static jfieldID xId;
static jfieldID yId;
static jfieldID wId;
static jfieldID hId;
static jfieldID labelId;
static jfieldID probId;
// 先定义返回的数据
extern "C"
JNIEXPORT jboolean JNICALL Java_com_chyzh_yolo_YoloAPI_Init(JNIEnv *env, jobject thiz, jobject assetManager, jint cpugpu){
// ...省略了项目原本的初始化代码,即设置一些参数,调用yolo.cpp的load方法进行初始化
// 注意需要YoloAPI.Obj是在java层定义的,为了使用它返回结果,需要将它转换为C++层的jclass格式
jclass localObjCls = env->FindClass("com/chyzh/yolo/YoloAPI$Obj"); // 注意名称要对应
objCls = reinterpret_cast<jclass>(env->NewGlobalRef(localObjCls));
constructortorId = env->GetMethodID(objCls, "<init>", "(Lcom/chyzh/yolo/YoloAPI;)V"); // 注意名称要对应
xId = env->GetFieldID(objCls, "x", "F");
yId = env->GetFieldID(objCls, "y", "F");
wId = env->GetFieldID(objCls, "w", "F");
hId = env->GetFieldID(objCls, "h", "F");
labelId = env->GetFieldID(objCls, "label", "I");
probId = env->GetFieldID(objCls, "prob", "F");
return JNI_TRUE;
}
下面是新建的Detect方法:
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_chyzh_yolo_YoloAPI_Detect(JNIEnv *env, jobject thiz, jobject bitmap,
jboolean use_gpu){
double start_time = ncnn::get_current_time();
AndroidBitmapInfo info;
AndroidBitmap_getInfo(env, bitmap, &info);
const int width = info.width;
const int height = info.height;
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
return NULL;
std::vector<Object> objects;
if (g_yolo)
{
// 这里调用了yolo.cpp的新函数,将在下面步骤中定义
g_yolo->detectPicture(env, bitmap, width, height, objects);
}
else
{
__android_log_print(ANDROID_LOG_DEBUG, "yolov8ncnn", "g_yolo is NULL!");
}
// 在detectPicture方法中将结果保存在了 objects 中,还需继续对他进行转换
jobjectArray jObjArray = env->NewObjectArray(objects.size(), objCls, NULL);
__android_log_print(ANDROID_LOG_DEBUG, "yolov8ncnn", "%d objects detected!", objects.size());
for (size_t i=0; i<objects.size(); i++)
{
jobject jObj = env->NewObject(objCls, constructortorId, thiz);
env->SetFloatField(jObj, xId, objects[i].rect.x);
env->SetFloatField(jObj, yId, objects[i].rect.y);
env->SetFloatField(jObj, wId, objects[i].rect.width);
env->SetFloatField(jObj, hId, objects[i].rect.height);
env->SetIntField(jObj, labelId, objects[i].label);
env->SetFloatField(jObj, probId, objects[i].prob);
env->SetObjectArrayElement(jObjArray, i, jObj);
}
double elasped = ncnn::get_current_time() - start_time;
__android_log_print(ANDROID_LOG_DEBUG, "yolov8ncnn", "the entire detection takes %.2fms", elasped);
return jObjArray;
}
注意!如果你的操作正确,IDE会自动检测到Java层调用这里的两个函数,因此他们的函数名为高亮,否则请检查包名、方法名是否完全对应。
与之类似,我们需要在yolo.cpp中模仿detect函数新建我们自己的detectPicture函数。
int Yolo::detectPicture(JNIEnv *env, jobject bitmap, int width, int height, std::vector<Object>& objects, float prob_threshold, float nms_threshold){
// 注意传入参数的不同点在于jobject bitmap, int width, int height,
int w = width;
int h = height;
// ... 省略和detect()方法完全一致的步骤
// 由于传入图像格式为Bitmap,这一步需要换成下面的方法
ncnn::Mat in = ncnn::Mat::from_android_bitmap_resize(env, bitmap, ncnn::Mat::PIXEL_RGB, w, h);
// ...其余部分也和detect()的剩余部分一致
}
测试运行
除上述步骤外,你还需要将图片文件转化为Bitmap格式传入JNI方法。
运行你的android项目,同时运行你的python脚本,预测同一张图片,如果结果一致,那么对demo项目的修改就算成功。
3.4 集成到其他项目
下面我们可以不再使用任何C++代码,而用so文件把整个模型检测过程集成到其他项目。
demo项目修改完测试运行成功后,点击build→Make Project,执行结束后可以在 项目目录/app/build/intermediates/cmake/debug/obj 目录下找到我们需要的so文件。CMake工具在初次打开项目时已经自动下载。
之后so文件集成到其他项目可以在网络上找到很多博客,比前面的步骤要简单得多。
在新的项目中,只需要新增以下内容:
- 模型原始文件:bin文件和param文件
- JNI接口:注意包名、类名、方法名全部和demo项目我们新增的内容一致
- so文件:新建jniLibs包,包生成的so文件复制到此
- build.gradle:配置jniLibs.srcDirs
集成完成,调用YoloAPI的方法即可实现demo项目的效果。