从端到端打通模型端侧部署流程(NCNN)

news2024/11/30 7:56:00

文章目录

  • 背景介绍:
  • 为什么要做端侧推理:
  • 端侧深度学习部署流程:
  • 一条主要技术路线:
  • ONNX:
  • NCNN框架:
    • NCNN的官方介绍:
    • NCNN问题解决:
    • NCNN使用样例
  • 快速在NCNN框架下验证自己的模型:
    • 一般流程:
    • YOLOv5的demo测试:
    • 全新部署:
      • 模型文件->ONNX文件:
      • ONNX格式模型->端侧推理框架模型(NCNN格式):
      • 在Android studio项目中使用模型文件:
      • 打日志测试:
  • 附录:NCNN如何操作bitmap(resize,RGB2BGR):

背景介绍:

深度学习已经在生活的方方面面被应用和重视。随着手机算力的不断提升,以及深度学习的快速发展,特别是小网络模型不断成熟,原本在云端执行的推理预测就可以转移到端上来做。端智能即在端侧部署运行 AI 算法,相比服务端智能,端智能具有低延时、兼顾数据隐私、节省云端资源等优势。目前端智能正逐渐变为趋势,从业界来看,它已经在 AI 摄像、视觉特效等场景发挥了巨大价值。

为什么要做端侧推理:

目前如何将神经网络部署到端侧,一般的模式是“云侧训练,端侧推理”。也就是说,把用户的数据传到云侧上面,云侧基于获得的多数据进行训练模型,训练好的模型直接下放到端侧,然后端侧利用训练好的模型直接进行推理。由于云端数据中心有更高的处理能力和更宽松的能耗限制,所以这种做法是更容易实践的。但这样的做法存在一个比较重要的问题:隐私问题。端设备收集的使用数据常常带有用户隐私:例如邮件App 得到的用户回复文本都可能含有用户私人信息。近几年的一系列事件唤起了用户对隐私的慎重态度:欧盟发布了GDPR 法案,限制科技公司从用户收集数据。除此之外,对于高访问量的应用需要调用大量的服务端部署资源来部署识别模型;另一方面,DL 在云端则意味着数据必须上传。即使不考虑计算压力,从网络延时、流量、隐私保护等角度,也给用户体验带来种种限制。因此,对相当多的应用来说,DL 模型前移到移动端部署可以看作是一种刚需。

端侧深度学习部署流程:

一般来说,端侧深度学习的应用可以分成如下几个阶段:

  • 模型训练阶段,主要解决模型训练,利用标注数据训练出对应的模型文件。面向端侧设计模型时,需要考虑模型大小和计算量;
  • 模型压缩阶段,主要优化模型大小,可以通过剪枝、量化等手段降低模型大小,以便在端上使用;
  • 模型部署阶段,主要实现模型部署,包括模型管理和部署、运维监控等;
  • 端侧推理阶段,主要完成模型推理,即加载模型,完成推理相关的所有计算;
    例子:MNN的部署流程
    在这里插入图片描述
    大致分为三个阶段,训练,转换,推理。

一条主要技术路线:

训练框架训练模型->模型转化为ONNX格式->ONNX格式转化为其他格式(NCNN,TNN,MNN的模型格式)->在对应的推理框架中部署

对于不同的推理框架,虽然细节部分略有不同,但是整体的流程则都是上面这样。

ONNX:

ONNX:(Open Neural Network Exchange):开放神经网络交换是一种针对机器学习算法所设计的开放式文件格式标准,用于存储训练好的算法模型。许多主流的深度学习框架(如 PyTorch、TensorFlow、MXNet)都支持将模型导出为 ONNX 模型。ONNX 使得不同的深度学习框架可以以一种统一的格式存储模型数据以及进行交互。
ONNX Runtime:ONNX运行时,支持Linux、Windows、Mac OS、Android、iOS等多种平台和多种硬件(CPU、GPU、NPU等)上进行模型部署和推理。
关于不同框架的分析可以参考如下文章:
深度学习框架大PK:TNN决战MNN,ncnn依旧经典

NCNN框架:

NCNN

NCNN的官方介绍:

ncnn 是一个为手机端极致优化的高性能神经网络前向计算框架。 ncnn 从设计之初深刻考虑手机端的部署和使用。 无第三方依赖,跨平台,手机端 cpu 的速度快于目前所有已知的开源框架。 基于 ncnn,开发者能够将深度学习算法轻松移植到手机端高效执行, 开发出人工智能 APP,将 AI 带到你的指尖。 ncnn目前已在腾讯多款应用中使用,如:QQ,Qzone,微信,天天P图等。

NCNN问题解决:

NCNN作为最早开源的一批深度学习推理框架,拥有着较为完善的社区和问题解决,基本上在开发过程中遇到的大多数问题都能够找到相应的解决方法。这个链接是NCNN的官方处理issue的地方,遇到了部署的问题可以首先在这里搜一下。

NCNN使用样例

• https://github.com/nihui/ncnn-android-squeezenet
• https://github.com/nihui/ncnn-android-styletransfer
• https://github.com/nihui/ncnn-android-mobilenetssd
• https://github.com/moli232777144/mtcnn_ncnn
• https://github.com/nihui/ncnn-android-yolov5
• https://github.com/xiang-wuu/ncnn-android-yolov7
• https://github.com/nihui/ncnn-android-scrfd 🤩
• https://github.com/shaoshengsong/qt_android_ncnn_lib_encrypt_example

快速在NCNN框架下验证自己的模型:

一般流程:

调用一般流程,涉及到数据的输入输出:

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "net.h"

int main()
{
	//外部传入图片
    cv::Mat img = cv::imread("image.ppm", CV_LOAD_IMAGE_GRAYSCALE);
    int w = img.cols;
    int h = img.rows;

    // subtract 128, norm to -1 ~ 1
    //重新resize大小到模型输入
    ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_GRAY, w, h, 60, 60);
    float mean[1] = { 128.f };
    float norm[1] = { 1/128.f };
    
    //图片归一化
    in.substract_mean_normalize(mean, norm);
	
	//定义ncnn网络
    ncnn::Net net;
    //加载模型结构
    net.load_param("model.param");
    //加载模型参数
    net.load_model("model.bin");
	
	//创建模型推理器
    ncnn::Extractor ex = net.create_extractor();
    ex.set_light_mode(true);
    ex.set_num_threads(4);
	
	//数据输入推理器
    ex.input("data", in);

	//创建输出矩阵
    ncnn::Mat feat;
    //模型推理
    ex.extract("output", feat);

    return 0;
}

YOLOv5的demo测试:

我们以YOLOV5的demo为例,来总结如何快速部署测试自己的模型。需要注意的是,YOLOv5是ncnn已经有框架和实现的,所以大多数的部署教程都是针对fine-tunning的方式来部署,例如下面这一篇。我们先分析这个。关于自己创建模型,后面再分析。

具体的训练和Android Studio的安装和环境搭建,可以参考下面这篇文章YOLOV5从训练到部署测试NCNN安卓端部署保姆级教程,这篇文章的步骤可以跑一下,安装Cmake和Sdk。当然如果从来都没有接触过这一块的话,即使把demo跑通了,应该也还是很蒙的,这里我把最重要的一个文件单摘出来,添加相关的注释帮助理解:

ncnn-android-yolov5/app/src/main/jni/yolov5ncnn_jni.cpp

选取和模型相关的函数分析


JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    __android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "JNI_OnLoad");

    ncnn::create_gpu_instance();

    return JNI_VERSION_1_4;
}

JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
{
    __android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "JNI_OnUnload");

    ncnn::destroy_gpu_instance();
}

// public native boolean Init(AssetManager mgr);
JNIEXPORT jboolean JNICALL Java_com_tencent_yolov5ncnn_YoloV5Ncnn_Init(JNIEnv* env, jobject thiz, jobject assetManager)
{
    ncnn::Option opt;
    opt.lightmode = true;
    opt.num_threads = 4;
    opt.blob_allocator = &g_blob_pool_allocator;
    opt.workspace_allocator = &g_workspace_pool_allocator;
    opt.use_packing_layout = true;

    // use vulkan compute
    if (ncnn::get_gpu_count() != 0)
        opt.use_vulkan_compute = true;

    AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);

    yolov5.opt = opt;

    yolov5.register_custom_layer("YoloV5Focus", YoloV5Focus_layer_creator);

    // init param
//    模型构建与导入源码平平无奇,就是detect_yolov5方法中的以下内容
//    ncnn的模型构建与权重导入其实做的工作就是:创建空白Net对象,导入模型结构文件(param)与权重文件(bin)
//    可能还有一些其他操作,比如自定义Layer,即register_custom_layer操作
    {
        int ret = yolov5.load_param(mgr, "yolov5s.param");
        if (ret != 0)
        {
            __android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "load_param failed");
            return JNI_FALSE;
        }
    }

    // init bin
    {
        int ret = yolov5.load_model(mgr, "yolov5s.bin");
        if (ret != 0)
        {
            __android_log_print(ANDROID_LOG_DEBUG, "YoloV5Ncnn", "load_model failed");
            return JNI_FALSE;
        }
    }

    // init jni glue
    // 初始化java需要的一些参数
    jclass localObjCls = env->FindClass("com/tencent/yolov5ncnn/YoloV5Ncnn$Obj");
    objCls = reinterpret_cast<jclass>(env->NewGlobalRef(localObjCls));

    constructortorId = env->GetMethodID(objCls, "<init>", "(Lcom/tencent/yolov5ncnn/YoloV5Ncnn;)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", "Ljava/lang/String;");
    probId = env->GetFieldID(objCls, "prob", "F");

    return JNI_TRUE;
}

// public native Obj[] Detect(Bitmap bitmap, boolean use_gpu);
JNIEXPORT jobjectArray JNICALL Java_com_tencent_yolov5ncnn_YoloV5Ncnn_Detect(JNIEnv* env, jobject thiz, jobject bitmap, jboolean use_gpu)
{
    if (use_gpu == JNI_TRUE && ncnn::get_gpu_count() == 0)
    {
        return NULL;
        //return env->NewStringUTF("no vulkan capable gpu");
    }
//    模型推理包括几个部分:
//    构建模型输入,并与模型类(即Net)绑定。
// 	  模型输入构建,主要工作包括
//    resize图像,令长边为target_size,短边按比例变化
//    执行pad操作,令边长能够被32整除
//    定义norm相关
//    将模型输入与模型绑定
    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;

    // ncnn from bitmap
    // 长边为 target_size,短边按比例变化
    const int target_size = 640;

    // letterbox pad to multiple of 32
    int w = width;
    int h = height;
    float scale = 1.f;
    if (w > h)
    {
        scale = (float)target_size / w;
        w = target_size;
        h = h * scale;
    }
    else
    {
        scale = (float)target_size / h;
        h = target_size;
        w = w * scale;
    }
    // resize 图片
    ncnn::Mat in = ncnn::Mat::from_android_bitmap_resize(env, bitmap, ncnn::Mat::PIXEL_RGB, w, h);

    // 执行pad操作,令图片尺寸可以被32整除
    // pad to target_size rectangle
    // yolov5/utils/datasets.py letterbox
    int wpad = (w + 31) / 32 * 32 - w;
    int hpad = (h + 31) / 32 * 32 - h;
    ncnn::Mat in_pad;
    // 实现pad操作
    ncnn::copy_make_border(in, in_pad, hpad / 2, hpad - hpad / 2, wpad / 2, wpad - wpad / 2, ncnn::BORDER_CONSTANT, 114.f);

    // yolov5
    std::vector<Object> objects;
    {
        const float prob_threshold = 0.25f;
        const float nms_threshold = 0.45f;
        // norm 操作参数
        const float norm_vals[3] = {1 / 255.f, 1 / 255.f, 1 / 255.f};
        in_pad.substract_mean_normalize(0, norm_vals);

        ncnn::Extractor ex = yolov5.create_extractor();

        ex.set_vulkan_compute(use_gpu);

        ex.input("images", in_pad);

        std::vector<Object> proposals;

        // anchor setting from yolov5/models/yolov5s.yaml
//        执行模型推理,主要功能包括
//        根据模型结果、anchors等参数构建bbox
//        实现NMS
//        将bbox尺寸转换为图像原始尺寸
        // yolov5 的输出由3个特征图生成,即下面的 stride 8/16/32
        // 每个部分都是根据 anchors 和输出结果构建最终预测结果(即bbox)
        // 预测结果都保存到 proposals 中,通过 Object 对象保存
        // stride 8
        {
            ncnn::Mat out;
            ex.extract("output", out);

            ncnn::Mat anchors(6);
            anchors[0] = 10.f;
            anchors[1] = 13.f;
            anchors[2] = 16.f;
            anchors[3] = 30.f;
            anchors[4] = 33.f;
            anchors[5] = 23.f;

            std::vector<Object> objects8;
            generate_proposals(anchors, 8, in_pad, out, prob_threshold, objects8);

            proposals.insert(proposals.end(), objects8.begin(), objects8.end());
        }

        // stride 16
        {
            ncnn::Mat out;
            ex.extract("781", out);

            ncnn::Mat anchors(6);
            anchors[0] = 30.f;
            anchors[1] = 61.f;
            anchors[2] = 62.f;
            anchors[3] = 45.f;
            anchors[4] = 59.f;
            anchors[5] = 119.f;

            std::vector<Object> objects16;
            generate_proposals(anchors, 16, in_pad, out, prob_threshold, objects16);

            proposals.insert(proposals.end(), objects16.begin(), objects16.end());
        }

        // stride 32
        {
            ncnn::Mat out;
            ex.extract("801", out);

            ncnn::Mat anchors(6);
            anchors[0] = 116.f;
            anchors[1] = 90.f;
            anchors[2] = 156.f;
            anchors[3] = 198.f;
            anchors[4] = 373.f;
            anchors[5] = 326.f;

            std::vector<Object> objects32;
            generate_proposals(anchors, 32, in_pad, out, prob_threshold, objects32);

            proposals.insert(proposals.end(), objects32.begin(), objects32.end());
        }
        // 根据score对所有proposals排序
        // sort all proposals by score from highest to lowest
        qsort_descent_inplace(proposals);

        // apply nms with nms_threshold
        std::vector<int> picked;
        nms_sorted_bboxes(proposals, picked, nms_threshold);

        int count = picked.size();
        // resize 所有bbox到原始尺寸
        objects.resize(count);
        for (int i = 0; i < count; i++)
        {
            objects[i] = proposals[picked[i]];

            // adjust offset to original unpadded
            float x0 = (objects[i].x - (wpad / 2)) / scale;
            float y0 = (objects[i].y - (hpad / 2)) / scale;
            float x1 = (objects[i].x + objects[i].w - (wpad / 2)) / scale;
            float y1 = (objects[i].y + objects[i].h - (hpad / 2)) / scale;

            // clip
            x0 = std::max(std::min(x0, (float)(width - 1)), 0.f);
            y0 = std::max(std::min(y0, (float)(height - 1)), 0.f);
            x1 = std::max(std::min(x1, (float)(width - 1)), 0.f);
            y1 = std::max(std::min(y1, (float)(height - 1)), 0.f);

            objects[i].x = x0;
            objects[i].y = y0;
            objects[i].w = x1 - x0;
            objects[i].h = y1 - y0;
        }
    }

    // objects to Obj[]
    static const char* class_names[] = {
        "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light",
        "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
        "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
        "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",
        "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
        "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
        "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone",
        "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear",
        "hair drier", "toothbrush"
    };

    jobjectArray jObjArray = env->NewObjectArray(objects.size(), objCls, NULL);

    for (size_t i=0; i<objects.size(); i++)
    {
        jobject jObj = env->NewObject(objCls, constructortorId, thiz);

        env->SetFloatField(jObj, xId, objects[i].x);
        env->SetFloatField(jObj, yId, objects[i].y);
        env->SetFloatField(jObj, wId, objects[i].w);
        env->SetFloatField(jObj, hId, objects[i].h);
        env->SetObjectField(jObj, labelId, env->NewStringUTF(class_names[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, "YoloV5Ncnn", "%.2fms   detect", elasped);

    return jObjArray;
}

从上面NCNN调用YOLOv5的例子,我们发现,实际上我们还是用了NCNN提供的模型和网络结构,具体的文件在这里:
ncnn-android-yolov5/app/src/main/assets/

接下来我们将从0到1部署一次模型

全新部署:

模型文件->ONNX文件:

Pytorch保存模型有两种方法:
第一种方法只保存模型参数。第二种方法保存完整模型。推荐使用第一种,第二种方法可能在切换设备和目录的时候出现各种问题。

  1. 保存模型参数方法:
print(model.state_dict().keys())                                # 输出模型参数名称
 
# 保存模型参数到路径"./data/model_parameter.pkl"
torch.save(model.state_dict(), "./data/model_parameter.pkl")
new_model = Model()                                                    # 调用模型Model
new_model.load_state_dict(torch.load("./data/model_parameter.pkl"))    # 加载模型参数     
new_model.forward(input)                                               # 进行使用
  1. 保存完整模型(不推荐)
torch.save(model, './data/model.pkl')        # 保存整个模型
new_model = torch.load('./data/model.pkl')   # 加载模型

目前大多数应用都会仅仅保存模型参数,用的时候创建模型结构,然后加载模型。

以pytorch为例,保存的模型为.pkl或者.model格式,可以用Netron打开查看。
Netron

接下来,是把模型参数文件和结构打包成ONNX的格式:

# onnx模型导入
import onnx
import torch
import torch.nn as nn
import torchvision.models as models


class Net(nn.Module):

    def __init__(self, prior_size):
         …………………

    def forward(self, x):
        …………………
        return x

model = Net(prior_size)
model.load_state_dict(torch.load('xxx.model', map_location='cpu'))
model.eval()

input_names = ['input']
output_names = ['output']
# 规定输入尺寸
x = torch.randn(1, 3, 240, 320, requires_grad=True)
torch.onnx.export(model, x, 'best.onnx', input_names=input_names, output_names=output_names, verbose='True')

验证转换是否成功:

# 验证onnx模型
import onnx

model_onnx = onnx.load("best.onnx")  # 加载onnx模型
onnx.checker.check_model(model_onnx)  # 验证onnx模型是否成功导出
# 如果没有报错,表示导出成功
#####################################################################################
# #测试onnx 模型
import onnxruntime
import numpy as np

# 创建会话
# session = onnxruntime.InferenceSession("best.onnx", providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
session = onnxruntime.InferenceSession("best.onnx", providers=['CPUExecutionProvider'])
x = np.random.randn(1, 3, 240, 320)
ort_input = {session.get_inputs()[0].name: x.astype(np.float32)}
ort_output = session.run(None, ort_input)
# # 如果没有报错,表示执行成功

经过上面的处理后,会得到一个名为best.onnx的文件。

ONNX格式模型->端侧推理框架模型(NCNN格式):

省去编译转换工具的时间,开箱即用,一键转换
利用模型转换网站,可以很方便的转化各种模型格式,而且网站是在本地处理,不必担心泄露。

输入ONNX,输出NCNN

得到两个文件一个.bin文件,用来存储具体的参数,一个.param文件,用来存储模型的框架

在Android studio项目中使用模型文件:

首先把文件模型放到路径:
app/src/main/assets
如果想从零开始构建一个自己的模型加载使用cpp文件,可以参考上面的官方案例,上面详细的写了输入输出部分该调用ncnn的哪个接口。

我们下面依然以一个demo的例子来说明问题,是风格迁移的例子:

在文件app/src/main/jni/styletransferncnn_jni.cpp中,
我们主要需要修改以下几个部分:

初始化部分:load我们的参数和模型作为其中一个可调用模型

// public native boolean Init(AssetManager mgr);
JNIEXPORT jboolean JNICALL Java_com_tencent_styletransferncnn_StyleTransferNcnn_Init(JNIEnv* env, jobject thiz, jobject assetManager)
{
    ncnn::Option opt;
    opt.lightmode = true;
    opt.num_threads = 4;
    opt.blob_allocator = &g_blob_pool_allocator;
    opt.workspace_allocator = &g_workspace_pool_allocator;

    // use vulkan compute
    if (ncnn::get_gpu_count() != 0)
        opt.use_vulkan_compute = true;

    AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
    const char* model_paths[5] = {"opt.bin", "mosaic.bin", "pointilism.bin", "rain_princess.bin", "udnie.bin"};
    for (int i=0; i<5; i++)
    {
        double start_time = ncnn::get_current_time();
        int ret0;
        int ret1;
        styletransfernet[i].opt = opt;
        if (i == 0){
        //我们自己的模型替代掉第一个模型
            ret0 = styletransfernet[i].load_param(mgr, "optfp16.param");
            ret1 = styletransfernet[i].load_model(mgr, "optfp16.bin");
        }else{
            ret0 = styletransfernet[i].load_param(styletransfer_param_bin);
            ret1 = styletransfernet[i].load_model(mgr, model_paths[i]);
        }

        __android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "load %d %d", ret0, ret1);
        double elasped = ncnn::get_current_time() - start_time;
        __android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "%.2fms   loadtime", elasped);
    }
    return JNI_TRUE;
}

调用部分:修改输入的格式,维度
输出部分:定义输出的格式

// public native Bitmap StyleTransfer(Bitmap bitmap, int style_type, boolean use_gpu);
JNIEXPORT jboolean JNICALL Java_com_tencent_styletransferncnn_StyleTransferNcnn_StyleTransfer(JNIEnv* env, jobject thiz, jobject bitmap, jint style_type, jboolean use_gpu)
{
   if (style_type < 0 || style_type >= 5)
       return JNI_FALSE;

   if (use_gpu == JNI_TRUE && ncnn::get_gpu_count() == 0)
       return JNI_FALSE;

   double start_time = ncnn::get_current_time();

   AndroidBitmapInfo info;
   AndroidBitmap_getInfo(env, bitmap, &info);
   if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
       return JNI_FALSE;

   int width = info.width;
   int height = info.height;

   const int downscale_ratio = 1;
   // 重新定义输入的维度,利用ncnn自带的插值
   ncnn::Mat in = ncnn::Mat::from_android_bitmap_resize(env, bitmap, ncnn::Mat::PIXEL_BGR, 320, 240);
   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 };
   // 图像归一化,把每个像素归一化到0-1
   in.substract_mean_normalize(mean_vals, norm_vals);
   // 定义输出矩阵
   ncnn::Mat out;
   {
       ncnn::Extractor ex = styletransfernet[style_type].create_extractor();

       ex.set_vulkan_compute(use_gpu);
       __android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "in %d %d %d", in.w, in.h, in.c);
//        ex.input(styletransfer_param_id::BLOB_input1, in);
//       注意这里的名字用的是转换onnx时起的名字
       ex.input("input", in);
       const float* ptr = in.channel(0);
       __android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "valueuse %f", ptr[0]);
//        ex.extract(styletransfer_param_id::BLOB_output1, out);
       //真正的调用是在这里发生的
//       注意这里的名字用的是转换onnx时起的名字
       ex.extract("output", out);
       const float mean_vals[3] = { 0.f, 0.f, 0.f };
       const float norm_vals[3] = { 255.f, 255.f,255.f };
       // 去归一化,让像素值回到0-255
       out.substract_mean_normalize(mean_vals, norm_vals);
       __android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "out %d %d", out.w, out.h);
       const float* pt = in.channel(0);
       __android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "valueout %f", pt[0]);
   }

   // ncnn to bitmap
   out.to_android_bitmap(env, bitmap, ncnn::Mat::PIXEL_GRAY);
   double elasped = ncnn::get_current_time() - start_time;
   __android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "%.2fms   styletransfer", elasped);

   return JNI_TRUE;
}

打日志测试:

经过上面的操作,原来的candy模型已经被替换成了我们的模型,可以直接使用测试了
也可以打log来测试耗时:

double start_time = ncnn::get_current_time();
double elasped = ncnn::get_current_time() - start_time;
__android_log_print(ANDROID_LOG_DEBUG, "StyleTransferNcnn", "%.2fms   loadtime", elasped);

然后在android studio的log里面filter选择StyleTransferNcnn即可看到模型的耗时。

附录:NCNN如何操作bitmap(resize,RGB2BGR):

输出Mat的内容
ncnn没有提供可以直接输出Mat数据的函数,所以想要输出Mat数据时,只能利用for循环进行遍历

void pretty_print(const ncnn::Mat& m)
{
    for (int q=0; q<m.c; q++)
    {
        const float* ptr = m.channel(q);
        for (int y=0; y<m.h; y++)
        {
            for (int x=0; x<m.w; x++)
            {
                printf("%f ", ptr[x]);
            }
            ptr += m.w;
            printf("\n");
        }
        printf("------------------------\n");
    }
}

可视化Mat

void visualize(const char* title, const ncnn::Mat& m)
{
    std::vector<cv::Mat> normed_feats(m.c);
 
    for (int i=0; i<m.c; i++)
    {
        cv::Mat tmp(m.h, m.w, CV_32FC1, (void*)(const float*)m.channel(i));
 
        cv::normalize(tmp, normed_feats[i], 0, 255, cv::NORM_MINMAX, CV_8U);
 
        cv::cvtColor(normed_feats[i], normed_feats[i], cv::COLOR_GRAY2BGR);
 
        // check NaN
        for (int y=0; y<m.h; y++)
        {
            const float* tp = tmp.ptr<float>(y);
            uchar* sp = normed_feats[i].ptr<uchar>(y);
            for (int x=0; x<m.w; x++)
            {
                float v = tp[x];
                if (v != v)
                {
                    sp[0] = 0;
                    sp[1] = 0;
                    sp[2] = 255;
                }
 
                sp += 3;
            }
        }
    }
 
    int tw = m.w < 10 ? 32 : m.w < 20 ? 16 : m.w < 40 ? 8 : m.w < 80 ? 4 : m.w < 160 ? 2 : 1;
    int th = (m.c - 1) / tw + 1;
 
    cv::Mat show_map(m.h * th, m.w * tw, CV_8UC3);
    show_map = cv::Scalar(127);
 
    // tile
    for (int i=0; i<m.c; i++)
    {
        int ty = i / tw;
        int tx = i % tw;
 
        normed_feats[i].copyTo(show_map(cv::Rect(tx * m.w, ty * m.h, m.w, m.h)));
    }
 
    cv::resize(show_map, show_map, cv::Size(0,0), 2, 2, cv::INTER_NEAREST);
    cv::imshow(title, show_map);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/344492.html

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

相关文章

数据分析思维(六)|循环/闭环思维

循环/闭环思维 1、概念 在很多的分析场景下&#xff0c;我们需要按照一套流程反复分析&#xff0c;而不是进行一次性的分析&#xff0c;也就是说这套流程的结果会成为该流程的新一次输入&#xff0c;从而形成一个闭环&#xff0c;此时的分析思维我们称之为循环/闭环思维。 常…

计算机断层扫描结肠镜和全自动骨密度仪在一次检查中的可行性

计算机断层扫描结肠镜和全自动骨密度仪在一次检查中的可行性 Feasibility of Simultaneous Computed Tomographic Colonography and Fully Automated Bone Mineral Densitometry in a Single Examination 简单总结&#xff1a; 数据&#xff1a;患者的结肠镜检查和腹部CT检查…

2022黑马Redis跟学笔记.实战篇(三)

2022黑马Redis跟学笔记.实战篇 三4.2.商家查询的缓存功能4.3.1.认识缓存4.3.1.1.什么是缓存4.3.1.2.缓存的作用1.为什么要使用缓存2.如何使用缓存3. 添加商户缓存4. 缓存模型和思路4.3.1.3.缓存的成本4.3.2.添加redis缓存4.3.3.缓存更新策略4.3.3.1.三种策略(1).内存淘汰:Redis…

NoSQL和Redis

NoSQL一、NoSqlNoSQL Not Only SQL(不仅仅是SQL)非关系型数据库二、为什么需要NoSQL1、web1.0在90年代&#xff0c;一个网站的访问量一般都不大&#xff0c;用单个数据库完全可以轻松应付。在那个时候&#xff0c;更多的都是静态网页&#xff0c;动态交互类型的网站不多。单机…

CS224W课程学习笔记(一):课程介绍与图深度学习概念

引言 我们从怎么利用图形或网络表示数据这一动机开始。网络成为了用于描述复杂系统中交互实体的通用语言。从图片上讲&#xff0c;与其认为我们的数据集由一组孤立的数据点组成&#xff0c;不如考虑这些点之间的相互作用和关系。 在不同种类的网络之间进行哲学上的区分是有启…

系统功能设计:教育缴费平台产品需求文档

教育缴费系统后台能够支撑前端业务&#xff0c;查询所需字段&#xff0c;为支撑前端业务提供服务&#xff0c;支持学校分校管理、班级分班管理、账单撤回及强制结束等功能。为了将教育缴费的需求清晰准确地描述清楚&#xff0c;本文作者编写了这个产品需求文档&#xff0c;一起…

Jmeter自带函数不够用?不如自己动手开发一个

在Jmeter的函数助手里&#xff0c;有很多内置的函数&#xff0c;比如Random、UUID、time等等。使用这些函数可以快速帮我们生成某些数据&#xff0c;进行一些逻辑处理。用起来非常的方便。 但是在实际接口测试过程中&#xff0c;有很多的需求&#xff0c;Jmeter内置的函数可能…

对抗生成网络GAN系列——Spectral Normalization原理详解及源码解析

&#x1f34a;作者简介&#xff1a;秃头小苏&#xff0c;致力于用最通俗的语言描述问题 &#x1f34a;专栏推荐&#xff1a;深度学习网络原理与实战 &#x1f34a;近期目标&#xff1a;写好专栏的每一篇文章 &#x1f34a;支持小苏&#xff1a;点赞&#x1f44d;&#x1f3fc;、…

JavaEE-HTTP协议(二)

目录HTTP请求的方法GET方法POST 方法其他方法“报头”User-AgentRefererCookieHTTP响应200 OK404 Not Found403 Forbidden405 Method Not Allowed500 Internal Server Error504 Gateway Timeout302 Move temporarily301 Moved PermanentlyHTTP请求的方法 GET方法 GET 是最常用…

Jmeter之直连数据库框架搭建简介

案例简介 通过直连数据库让程序代替接口访问数据库&#xff0c;如果二者预期结果不一致&#xff0c;就找到了程序的缺陷。 下面通过一个案例分析讲解如何实现&#xff1a;获取某个字段值&#xff0c;放在百度上搜索。 实现方式 1、Jmeter本身不具备直连数据库的功能&#xf…

机器学习笔记之生成模型综述(四)概率图模型 vs 神经网络

机器学习笔记之生成模型综述——概率图模型vs神经网络引言回顾&#xff1a;概率图模型与前馈神经网络贝叶斯网络 VS\text{VS}VS 神经网络表示层面观察两者区别推断、学习层面观察两者区别引言 本节将介绍概率图模型与神经网络之间的关联关系和各自特点。 回顾&#xff1a;概率…

Javaweb安全——Dubbo 反序列化(一)

Dubbo 反序列化&#xff08;一&#xff09; Dubbo 基础 Apache Dubbo 是一款 RPC 服务开发框架。提供三个核心功能&#xff1a;面向接口的远程方法调用、智能容错和负载均衡&#xff0c;以及服务自动注册和发现。 节点角色 节点角色说明Provider暴露服务的服务提供者Consume…

leaflet 加载KML数据显示图形(方法3)

第061个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中加载kml文件,并解析后在地图上显示图形,这里是第三种方法,前两种方法请参考目录查询。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共81行…

大数据培训课程分享:Python数据分析与挖掘实战课程介绍

《Python数据分析与挖掘实战》课程内容以Python数据分析与挖掘的常用技术与真实案例相结合的方式&#xff0c;深入浅出地介绍Python数据分析与挖掘的重要内容&#xff0c;共分为基础篇&#xff08;第1~5章&#xff09;和实战篇&#xff08;第6~11章&#xff09;。 基础篇内容包…

Git 安装和使用(非常详细教程)

Git 安装和使用Tips 目录&#xff1a;导读 1. git的安装 1)首先去下载 2)傻瓜式下一步再下一步地去安装 2. git的常见命令 提交代码 下载代码 分支提交代码 3. git的常见问题 1) 提示出错信息&#xff1a;fatal: remote origin already exists. 2) 发现日志等文件没…

通过异常处理错误

写在前面Java的基本理念是"结构不佳的代码不能运行"。发现错误的理想时机是在编译阶段, 也就是在你试图运行程序之前。然而, 编译期间并不能找出所有的错误, 余下的问题必须在运行期间解决。这就需要错误源能通过某种方式, 把适当的信息传递给某个接收者——该接收者…

情人节特刊 | “恋爱容易,相守难!” 犀思老兵谈破局之道!

付出甘之如饴&#xff0c;所得归于欢喜。 主动付出真心&#xff0c;问心无愧&#xff0c;未来无悔。老吴是我们公司十多年经验的售后服务主管&#xff0c;平时聊的不多&#xff0c;中午一起吃饭&#xff0c;偶然看到新闻说春节后多地都有排队办理离婚的现象。我不禁感叹一句&am…

三种查找Windows10环境变量的方法

文章目录一.在设置中查看二. 在我的电脑中查看三. 在资源管理器里查看一.在设置中查看 在系统中搜索设置 打开设置&#xff0c;在设置功能里&#xff0c;点击第一项 系统 在系统功能里&#xff0c;左侧菜单找到关于 在关于的相关设置里可以看到高级系统设置 点击高级系…

Java如何整合FFmpeg、FFprobe等音视频处理工具,零基础照样玩

前言&#xff1a;时隔一年多了&#xff0c;不知不觉博客停更那么久了&#xff0c;那不忘初心还记得吗&#xff1f; 最近在做音视频相关的开发&#xff0c;没什么资料并且之前也没有接触过这方面&#xff0c; 咨询了T届的好友&#xff0c;拿到了下面的这张表情包&#xff0c;问题…

从事架构师岗位快2年了,聊一聊我和ChatGPT对架构的一些感受和看法

从事架构师岗位快2年了&#xff0c;聊一聊我和ChatGPT对架构的一些感受和看法 职位不分高低&#xff0c;但求每天都能有新的进步&#xff0c;永远向着更高的目标前进。 文章目录踏上新的征程架构是什么&#xff1f;架构师到底是干什么的&#xff1f;你的终极目标又是什么&#…