文章目录[隐藏]
1. 简介
本文以目标检测模型 FCOS 为例,详细介绍从使用 PaddleDetection 训练模型,到最终部署到移动端安卓设备的全流程。
2. PaddleDetection
项目地址:https://github.com/PaddlePaddle/PaddleDetection,PaddleDetection 项目结构非常简单(几个关键的目录):
PaddleDetection
├─ configs
├─ dataset
├─ ppdet
└─ tools
2.1 安装
不使用源码安装 ppdet 的话,在 PaddleDetection 根目录下直接安装依赖即可:
pip install -r requirements
编译安装 ppdet(可选):
python setup.py install
2.2 configs
configs 文件夹的内容以模型为基本单位,以 yml 作为文件格式,采用递归的方式配置模型训练的全部参数。如本文使用的目标检测模型 FCOS 相关的配置文件:
FCOS
├─ _base_
│ ├─ fcos_r50_fpn.yml
│ ├─ fcos_reader.yml
│ └─ optimizer_1x.yml
├─ fcos_dcn_r50_fpn_1x_coco.yml
├─ fcos_r50_fpn_1x_coco.yml
└─ fcos_r50_fpn_multiscale_2x_coco.yml
训练模型时,指定 FCOS 根目录下的 yml 即可。如 fcos_r50_fpn_1x_coco.yml 表示主干网络使用 ResNet50、颈部使用 FPN、训练周期为 1x、数据集使用 coco,后续训练模型时指定改配置文件即可。
2.3 dataset
dataset 文件夹的内容以数据集为基本单位,可以本地数据手动将放到对应文件夹或通过自带的脚本下载。注意,可以将自己的数据集转换为标准数据集的格式,然后放到对应目录即可。如果有将 voc 格式格式的数据集转换为 coco 数据集格式的需求,欢迎评论区交流。
2.4 ppdet
ppdet 是 PaddleDetection 的核心目录,里面完成串联和解析各配置文件、数据处理、模型训练等核心功能。以下是几个重要的目录:
ppdet
├─ data
├─ engine
├─ modeling
│ ├─ architectures
│ ├─ backbones
│ ├─ heads
│ ├─ losses
│ └─ necks
└─ utils
data 目录主要完成数据的处理,engine 目录主要完成模型的调度,modeling 目录主要完成模型的搭建,utils 目录完成一些通用功能。其中,如果想更改网络结构或添加新模块等,操作 modeling 文件夹内的对应目录即可。
2.5 tools
tools 目录主要完成模型训练、验证、测试的启动。主要选项的用法:
# -c 指定配置文件即可训练对应的模型,-r 选项用于恢复训练,--eval 用于开启训练时的验证
python tools/train.py -c configs/fcos/fcos_r50_fpn_1x_coco.yml
# -c 指定配置文件即可验证对应的模型
python tools/eval.py -c configs/fcos/fcos_r50_fpn_1x_coco.yml
# -c 指定配置文件即可测试对应的模型,--infer_img 指定测试图像
python tools/infer.py -c configs/fcos/fcos_r50_fpn_1x_coco.yml --infer_img=demo.jpg
在模型的验证和测试时,模型路径由配置文件的 weights 关键字指定,也可以使用 -o weights=xxx 手动指定。PaddleDetection 的其他详细内容请见,PaddleDetection 中文文档。
3. FCOS 后处理裁剪
在导出模型前,由于 ncnn 不支持 FCOS 的后处理,所以在导出前将其删除。① 删除配置文件 configs/fcos/_base_/fcos_r50_fpn.yml 的后处理部分:
FCOSPostProcess:
decode:
name: FCOSBox
num_classes: 18
nms:
name: MultiClassNMS
nms_top_k: 1000
keep_top_k: 100
score_threshold: 0.025
nms_threshold: 0.6
② 修改后处理 ppdet/modeling/post_process.py 的返回结果:
def __call__(self, fcos_head_outs, scale_factor):
locations, cls_logits, bboxes_reg = fcos_head_outs
bboxes, score = self.decode(locations, cls_logits, bboxes_reg, scale_factor)
bbox_pred, bbox_num, _ = self.nms(bboxes, score)
return bbox_pred, bbox_num
# 修改为===>
def __call__(self, fcos_head_outs, scale_factor):
locations, cls_logits, bboxes_reg = fcos_head_outs
return cls_logits, bboxes_reg, centerness
③ 修改网络颈部 ppdet/modeling/architectures/fcos.py 的返回结果:
def get_pred(self):
bbox_pred, bbox_num = self._forward()
output = {'bbox': bbox_pred, 'bbox_num': bbox_num}
return output
# 修改为===>
def get_pred(self):
cls_logits, bboxes_reg, centerness = self._forward()
output = {'cls': cls_logits, 'bbox': bboxes_reg, 'centerness': centerness}
return output
4. Paddle 模型转 onnx
PaddleDetection 完成后得到一个 pdparams 和 pdopt 文件,前者存放了模型权重、后者存放用于断点训练的参数等。Paddle 提供了与其框架衔接的部署套件,Paddle-Lite 仓库,但本人还没有尝试过,所以最终以 onnx 为中间件。PaddleDetection 中提供了模型导出脚本:
python tools/export_model.py -c configs/fcos/fcos_r50_fpn_1x_coco.yml
-o weights =<path-to-weights>
TestReader.inputs_def.image_shape=[n,c,h,w]
--output_dir <out-dir>
执行命令后得到后续使用 pdparams 文件和 pdiparams 文件,然后使用 paddle2onnx 转换,首先使用 pip 安装即可:
pip install paddle2onnx
然后执行转换:
paddle2onnx --model_dir <dir> --model_filename model.pdmodel --params_filename model.pdiparams
--opset_version 11 --save_file fcos.onnx
Paddle 模型转换为 onnx 格式可参考,Paddle 模型导出教程。
5. onnx 转 ncnn
5.1 protobuf
git clone https://github.com/protocolbuffers/protobuf.git
首先编译 protobuf,为避免编译子模块时出错,首先修改 .gitmodules 下的地址,然后执行:
git submodule update --init --recursive
生成配置文件 configure:
./autogen.sh
编译时设置产生的文件的存放地址:
./configure --prefix=<install-path>
依次执行
make
make check
make install
5.2 ncnn
git clone https://github.com/Tencent/ncnn.git
首先编译 ncnn 得到转换工具。为避免编译子模块时出错,首先修改 .submodule 和 .git 目录下 config 文件中的地址,然后执行:
git submodule update --init
cmake ..
make
期间如果找不到 protobuf,则在 tools/onnx/CMakeLists.txt 中添加:
set(Protobuf_LIBRARIES <protobuf-dir>/lib/libprotobuf.so)
set(Protobuf_INCLUDE_DIR <protobuf-dir>/include)
编译完成后,得到转换工具 build/tools/onnx2ncnn。在转换前,首先使用 onnxsim 精简模型,使用 pip 安装即可:
pip install onnxsim
然后执行:
python -m onnxsim fcos.onnx. fcossim.onnx --input-shape n,c,h,w
最后执行转换:
./onnx2ncnn fcossim.onnx fcos.param fcos.bin
即得到转换结果,下一步的 int8 量化步骤为可选。
6. ncnn 的 int8 量化(可选)
首先使用优化器优化模型,在目录 build/tools 下:
./ncnnoptimize fcos.param fcos.bin fcos_opt.param fcos_opt.bin 0
首先使用 ImageNet 的部分数据集 calibration 生成校准列表:
find calibration/ -type f > imagelist.txt
然后使用 build/tools/quantize 下的工具生成量化表:
./ncnn2table fcos_opt.param fcos_opt.bin imagelist.txt fcos.table mean=[x,x,x] norm=[x,x,x]
shape=[h,w,c] pixel=BGR thread=2 method=kl
最后,得到量化模型:
./ncnn2int8 fcos_opt.param fcos_opt.bin fcos_int8.param fcos_int8.bin fcos.table
7. 基于 x86 平台的推理
在进行最后的部署前,由于本人不熟悉安卓项目的构建,所以首先在 Linux 平台模拟一遍模型的推理,仓库地址。项目结构为:
demo_ncnn
├─ include
├─ lib
└─ nets
└─ fcos
├─ CMakeLists.txt
├─ fcos.cpp
├─ fcos.hpp
└─ main.cpp
在 fcos.hpp 中定义了 FCOS 类,在 fcos.cpp 中实现类的成员函数,最后在 main.cpp 测试。首先在类的构造函数中完成相关初始化,包括模型加载、图像加载、计算缩放系数等:
FCOS::FCOS(const char* param, const char* bin, const char* filename, bool use_gpu)
{
// 初始化网络
this->net = new ncnn::Net();
// 是否使用GPU
this->net->opt.use_vulkan_compute = use_gpu;
// 是否使用FP16半精度
this->net->opt.use_fp16_arithmetic = false;
// 加载网络结构和权重
int ret = this->net->load_param(param);
if (ret != 0)
{
printf("[ERROR] Load Param File Failed!\n");
return;
}
ret = this->net->load_model(bin);
if (ret != 0)
{
printf("[ERROR] Load Bin File Failed!\n");
return;
}
// 加载图像
this->image = cv::imread(filename, 1);
// 图像的宽和高
this->w = image.cols;
this->h = image.rows;
// 计算缩放系数
this->scale_factor = std::min(this->target_w / float(this->w), this->target_h / float(this->h));
}
模型推理的流程为:
① 推理前数据预处理函数:
void FCOS::preprocess(cv::Mat& image, ncnn::Mat& processed_image)
{
// 缩放后的宽和高
int resized_w = int(this->w * this->scale_factor);
int resized_h = int(this->h * this->scale_factor);
// 将OpenCV的Mat转换为ncnn格式,并resize
ncnn::Mat in = ncnn::Mat::from_pixels_resize(
image.data, ncnn::Mat::PIXEL_BGR, this->w, this->h, resized_w, resized_h
);
// 宽和高方向上的填充
int padded_w = (resized_w + this->padding - 1) / this->padding * this->padding - resized_w;
int padded_h = (resized_h + this->padding - 1) / this->padding * this->padding - resized_h;
// pad,以左上角为起点开始填充,上面和左边填充0、下面填充padded_h、右边填充padded_w
ncnn::copy_make_border(in, processed_image, 0, padded_h, 0, padded_w, ncnn::BORDER_CONSTANT, 0.f);
// 减均值除方差
processed_image.substract_mean_normalize(this->mean_vals, this->norm_vals);
// 网络输入宽高
this->input_w = processed_image.w;
this->input_h = processed_image.h;
}
② 推理函数:
std::vector<Object> FCOS::detect(ncnn::Mat& input)
{
// 创建执行器
ncnn::Extractor ex = this->net->create_extractor();
// 设置输入
ex.input("image", input);
// 存放处理后的框集合
std::vector<Object> objects;
// 产生置信度大于指定阈值的框
for (int i = 0; i < 15; i += 3)
{
// 接收输出
ncnn::Mat cls_pred;
ncnn::Mat dis_pred;
ncnn::Mat cnt_pred;
// 提取三个分支的输出
ex.extract(this->out_blobs[i].c_str(), cls_pred);
ex.extract(this->out_blobs[i + 1].c_str(), dis_pred);
ex.extract(this->out_blobs[i + 2].c_str(), cnt_pred);
// 解码输出
std::vector<Object> objs;
decode(
cls_pred, dis_pred, cnt_pred, this->stride[i / 3], objs,
this->cls_threshold, this->input_w, this->input_h
);
// 添加
objects.insert(objects.end(), objs.begin(), objs.end());
}
// 返回
return objects;
}
③ 推理结果的后处理:
std::vector<Object> FCOS::postprocess(std::vector<Object>& proposals)
{
// 按照置信度降序排序所有框
qsort_descent_inplace(proposals);
// nms
std::vector<int> picked;
nms_sorted_bboxes(proposals, picked, this->nms_threshold);
// 处理保留的框
int count = picked.size();
// 存放最终返回结果
std::vector<Object> objects;
objects.resize(count);
// 遍历
for (int i = 0; i < count; i++)
{
// 取对应框
objects[i] = proposals[picked[i]];
// 将坐标还原为相对于原图
float x0 = (objects[i].rect.x) / this->scale_factor;
float y0 = (objects[i].rect.y) / this->scale_factor;
float x1 = (objects[i].rect.x + objects[i].rect.width) / this->scale_factor;
float y1 = (objects[i].rect.y + objects[i].rect.height) / this->scale_factor;
// 越界处理
x0 = std::max(std::min(x0, (float)(this->w - 1)), 0.f);
y0 = std::max(std::min(y0, (float)(this->h - 1)), 0.f);
x1 = std::max(std::min(x1, (float)(this->w - 1)), 0.f);
y1 = std::max(std::min(y1, (float)(this->h - 1)), 0.f);
// 重新放入objects[i]中
objects[i].rect.x = x0;
objects[i].rect.y = y0;
objects[i].rect.width = x1 - x0;
objects[i].rect.height = y1 - y0;
}
// 返回
return objects;
}
④ 可视化:
void FCOS::visualization(const cv::Mat& img, const std::vector<Object>& objects, const char*)
{
// 拷贝
cv::Mat image = img.clone();
// 遍历所有检测结果
for (size_t i = 0; i < objects.size(); i++)
{
// 取检测结果,并log相关信息
const Object& obj = objects[i];
fprintf(stderr, "%d = %.5f at %.2f %.2f %.2f x %.2f\n", obj.label, obj.prob,
obj.rect.x, obj.rect.y, obj.rect.width, obj.rect.height);
// 绘制矩形框
cv::rectangle(image, obj.rect, cv::Scalar(255, 0, 0));
// log信息
char text[256];
// 屏蔽类型不匹配的警告
#pragma GCC diagnostic ignored "-Wformat="
sprintf(text, "%s %.1f%%", this->labels[obj.label].c_str(), obj.prob * 100);
int baseLine = 0;
cv::Size label_size = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
// 绘制文本信息
int x = obj.rect.x;
int y = std::max(int(obj.rect.y - label_size.height) - baseLine, 0);
if (x + label_size.width > image.cols)
x = image.cols - label_size.width;
cv::rectangle(image, cv::Rect(cv::Point(x, y), cv::Size(label_size.width, label_size.height + baseLine)),
cv::Scalar(255, 255, 255), -1);
cv::putText(image, text, cv::Point(x, y + label_size.height),
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
}
// 保存图片
cv::imwrite("detect.jpg", image);
}
8. 基于安卓平台的推理
在完成 x86 平台的推理后,整体模型推理部分的代码变化不大,Gitee仓库地址。在 AndroidStudio 中新建 native C++ 项目,项目结构为:
app
└─ src
├─ AndroidManifest.xml
├─ assets
├─ cpp
│ ├─ CMakeLists.txt
│ ├─ fcos.cpp
│ ├─ fcos.h
│ ├─ fcos_jni.cpp
│ ├─ fcos_jni.h
│ ├─ include.ncnn
│ └─ lib
├─ java.com.example.ncnn_android_fcos
│ ├─ Box.java
│ ├─ FCOS.java
│ └─ MainActivity.java
└─ res
└─ layout
8.1 编译 ncnn
这里以构建 armeabi-v7a 为例,Android ABI 相关内容。
cmake -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI="armeabi-v7a" -DANDROID_ARM_NEON=ON \
-DANDROID_PLATFORM=android-24 -DNCNN_VULKAN=ON ..
make
make install
更多内容,ncnn wiki。
8.2 构建安卓工程
首先,整个安卓程序默认在 MainActivity.java 中启动,而 Java 代码无法直接调用 C++ 代码,这里使用 JNI 使二者交互。具体流程为,① 在 FCOS.java 中通过 native 关键字定义待调用的函数,这里将整个推理过程浓缩为 init 和 detect 两个函数:
public static native boolean init(AssetManager manager);
public static native Box[] detect(Bitmap bitmap);
② 在命令行中通过 javac 生成相应的 JNI 头文件 fcos_jni.h(手动改名):
javac -h . -classpath android.jar FCOS.java
③ 新建 fcos_jni.cpp 并完成相应的函数。在 init 函数中完成 FCOS 类的构造函数:
JNIEXPORT jboolean
JNICALL Java_com_example_ncnn_1android_1fcos_FCOS_init
(JNIEnv * env, jclass, jobject assetManager) {
if (FCOS::detector == nullptr) {
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
FCOS::detector = new FCOS(mgr, "fcos_int8.param", "fcos_int8.bin");
}
return JNI_TRUE;
}
在 detect 函数中完成推理:
JNIEXPORT jobjectArray
JNICALL Java_com_example_ncnn_1android_1fcos_FCOS_detect
(JNIEnv * env, jclass, jobject image) {
// 推理预处理
ncnn::Mat processed_image;
FCOS::detector->preprocess(env, image, processed_image);
// 推理
std::vector<Object> outs = FCOS::detector->detect(processed_image);
// 推理后处理
std::vector<Object> after_outs = FCOS::detector->postprocess(outs);
// 获取Box类
jclass box_class = env->FindClass("com/example/ncnn_android_fcos/Box");
// 获取init函数ID
jmethodID method_id = env->GetMethodID(box_class, "<init>", "(FFFFIF)V");
jobjectArray results = env->NewObjectArray(after_outs.size(), box_class, nullptr);
// 遍历
int i = 0;
for (Object& box : after_outs) {
env->PushLocalFrame(1);
jobject obj = env->NewObject(box_class, method_id, box.x0, box.y0, box.x1, box.y1,
box.label, box.score);
obj = env->PopLocalFrame(obj);
env->SetObjectArrayElement(results, i++, obj);
}
return results;
}
④ 将 x86 的推理代码放到 cpp 目录下,即 fcos.h 和 fcos.cpp 文件。由于 Android 提供了图像处理类 Bitmap,所以这里摈弃了 OpenCV 的使用,代码改动也主要这一点。这样,即完成了在 Java 中调用 C++ 实现的代码。
⑤ 最后,在 MainActivity.java 中通过 FCOS.java 这一中间件即可完成在安卓平台的推理。
8.3 MainActivity.java
在主程序中,首先绑定图片检测和视频检测按钮:
Button detect_image = findViewById(R.id.button_image);
detect_image.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_IMAGE);
});
Button detect_video = findViewById(R.id.button_video);
detect_video.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("video/*");
startActivityForResult(intent, REQUEST_VIDEO);
});
然后执行 onActivityResult 函数,根据 startActivityForResult 的第二个参数分别调用 detectByImage 和 detectByVideo 分别检测图片和视频。如 detectByImage:
// 获取图像
Bitmap image = getPicture(data.getData());
Thread thread = new Thread(() -> {
// 拷贝图像
mutableBitmap = image.copy(Bitmap.Config.ARGB_8888, true);
// 开始时间
long start = System.currentTimeMillis();
// 检测并绘制结果
mutableBitmap = detectAndDrawResults(mutableBitmap);
// 计算持续时间
final long during = System.currentTimeMillis() - start;
runOnUiThread(() -> {
// 将绘制好的图像重新贴上去
imageView.setImageBitmap(mutableBitmap);
// 图像宽高
int w = mutableBitmap.getWidth();
int h = mutableBitmap.getHeight();
// 展示检测信息
fpsView.setText(String.format(Locale.CHINESE,
"Image: %d,%d\nTime: %.3f\nFPS: %.3f",
h, w, during / 1000.0, 1000.0/during));
});
});
thread.start();
视频检测思路是逐帧抽取视频的图像分别检测,其他流程同图片检测。这里通过 FFmpegMediaMetadataRetriever 类处理视频,① 计算视频的持续时间,单位是毫秒:
String duration_str = retriever.extractMetadata(
FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION);
int duration = Integer.parseInt(duration_str);
② 计算视频的 FPS:
String fps_str = retriever.extractMetadata(
FFmpegMediaMetadataRetriever.METADATA_KEY_FRAMERATE);
float fps = Float.parseFloat(fps_str);
③ FPS 表示每秒的帧数,则 1.0f / FPS 表示每帧的时长,单位为秒,转换成微秒:
float durr_per_frame = 1.0f / fps * 1000 * 1000 * 1.0f;
④ getFrameAtTime 根据时间节点取对应帧的图像,单位为微秒:
Bitmap image = retriever.getFrameAtTime(video_curr_frame_loc,
MediaMetadataRetriever.OPTION_CLOSEST);
⑤ 取到 Bitmap 格式的图像后,后续流程同图片检测。
9. 效果展示
测试模式:真机测试,机型:红米 K40、Android 11
由于视频采取逐帧抽取的方式检测,只有模型达到实时检测才能流畅播放。
10. Q&A
【1】编译 ncnn 时出现错误 *** target pattern contains no ‘%’. Stop.
- 定位到文件,发现是 Protobuf 的错误。将 protoc 添加到环境变量。
参考
【1】https://github.com/PaddlePaddle/PaddleDetection
【2】https://github.com/Tencent/ncnn
【3】https://github.com/RangiLyu/nanodet/tree/main/demo_android_ncnn
版权声明:本文为CSDN博主「zhangts20」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Skies_/article/details/122501300
暂无评论