图像编辑器 Monica 之使用 ONNX Runtime 在端侧部署生成素描画的模型

一.  图像编辑器 Monica

Monica 是一款跨平台的桌面图像编辑软件,使用 Kotlin Compose Desktop 作为 UI 框架。应用层使用 Kotlin 编写,基于 mvvm 架构,使用 koin 作为依赖注入框架。

大部分算法也是用 Kotlin 编写,少部分通过 jni 调用 OpenCV C++ 实现的图像处理或调用深度学习的模型。

Monica 目前还处于开发阶段,当前版本的可以参见 github 地址:https://github.com/fengzhizi715/Monica

二. ONNX Runtime 部署模型

ONNX Runtime 是一个高性能的推理引擎,专门用于加速和优化基于 ONNX(Open Neural Network Exchange)模型格式的机器学习模型的推理。ONNX 是一个开源的中间表示格式,它可以使不同深度学习框架(如 PyTorch、TensorFlow 等)的模型互相转换并在不同的硬件平台上运行。

ONNX Runtime 常用于部署和推理阶段,能够快速高效地运行机器学习模型。

ONNX Runtime 在 mac 下可以通过 brew 或者直接下载源码的方式进行安装。我采用直接下载源码,这样可以安装我想要的版本。

在 cmake 做如下配置即可使用 ONNX Runtime

# 指定ONNX Runtime的路径
set(ONNXRUNTIME_ROOT "/Users/Tony/onnxruntime/onnxruntime-osx-x86_64-1.10.0")
# 包含ONNX Runtime头文件
include_directories(${ONNXRUNTIME_ROOT}/include/)
# 查找ONNX Runtime库文件
find_library(ONNXRUNTIME_LIBS onnx-runtime PATHS ${ONNXRUNTIME_ROOT}/lib)

target_link_libraries(MonicaImageProcess ${ONNXRUNTIME_ROOT}/lib/libonnxruntime.1.10.0.dylib)

安装完成后,先封装一个比较通用的 OnnxRuntimeBase 类,下面是 OnnxRuntimeBase.h

#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <vector>
#include <onnxruntime_cxx_api.h>
#include <onnxruntime_c_api.h>

class OnnxRuntimeBase
{

public:
    OnnxRuntimeBase(std::string modelPath, const char* logId, const char* provider);
    ~OnnxRuntimeBase();

    virtual std::vector<Ort::Value> forward(Ort::Value& inputTensors);

protected:
    Ort::Env env;
    Ort::Session ort_session{ nullptr };
    Ort::SessionOptions sessionOptions = Ort::SessionOptions();
    std::vector<char*> input_names;
    std::vector<char*> output_names;
    std::vector<std::vector<int64_t>> input_node_dims;  // >=1 outputs
    std::vector<std::vector<int64_t>> output_node_dims; // >=1 outputs

    Ort::MemoryInfo memory_info_handler = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
};

OnnxRuntimeBase 类的实现:

#include <iostream>
#include "../../include/common/OnnxRuntimeBase.h"
#include "../../include/Constants.h"
#include "../../include/utils/Utils.h"

using namespace cv;
using namespace std;
using namespace Ort;


OnnxRuntimeBase::OnnxRuntimeBase(string modelPath, const char* logId, const char* provider)
{
    sessionOptions.SetGraphOptimizationLevel(ORT_ENABLE_BASIC);

    env = Ort::Env(ORT_LOGGING_LEVEL_WARNING, logId);

    std::vector<std::string> availableProviders = Ort::GetAvailableProviders();
    auto cudaAvailable = std::find(availableProviders.begin(), availableProviders.end(), "CUDAExecutionProvider");
    OrtCUDAProviderOptions cudaOption;
    if (provider == OnnxProviders::CUDA.c_str()) {
        if (cudaAvailable == availableProviders.end()) {
            std::cout << "CUDA is not supported by your ONNXRuntime build. Fallback to CPU." << std::endl;
            std::cout << "Inference device: CPU" << std::endl;
        }
        else {
            std::cout << "Inference device: GPU" << std::endl;
            sessionOptions.AppendExecutionProvider_CUDA(cudaOption);
        }
    }
    else if (provider == OnnxProviders::CPU.c_str()) {
        // "cpu" by default
        std::cout << "Inference device: CPU" << std::endl;
    }
    else
    {
        throw std::runtime_error("NotImplemented provider=" + std::string(provider));
    }

    const char* model_path = "";
    #ifdef _WIN32
        auto modelPathW = get_win_path(modelPath);  // For Windows (wstring)
        model_path = modelPathW.c_str();
    #else
        model_path = modelPath.c_str();             // For Linux、MacOS (string)
    #endif

    ort_session = Ort::Session(env, model_path, sessionOptions);

    size_t numInputNodes = ort_session.GetInputCount();
    size_t numOutputNodes = ort_session.GetOutputCount();
    AllocatorWithDefaultOptions allocator;

    for (int i = 0; i < numInputNodes; i++)
    {
        input_names.push_back(ort_session.GetInputName(i, allocator));
        Ort::TypeInfo input_type_info = ort_session.GetInputTypeInfo(i);
        auto input_tensor_info = input_type_info.GetTensorTypeAndShapeInfo();
        auto input_dims = input_tensor_info.GetShape();
        input_node_dims.push_back(input_dims);
    }
    for (int i = 0; i < numOutputNodes; i++)
    {
        output_names.push_back(ort_session.GetOutputName(i, allocator)); 
        Ort::TypeInfo output_type_info = ort_session.GetOutputTypeInfo(i);
        auto output_tensor_info = output_type_info.GetTensorTypeAndShapeInfo();
        auto output_dims = output_tensor_info.GetShape();
        output_node_dims.push_back(output_dims);
    }
}

OnnxRuntimeBase::~OnnxRuntimeBase() {
    sessionOptions.release();
    ort_session.release();
}

std::vector<Ort::Value> OnnxRuntimeBase::forward(Ort::Value& inputTensors)
{
    return ort_session.Run(RunOptions{ nullptr }, input_names.data(), &inputTensors, 1, output_names.data(), output_names.size());
}

我使用的 ONNX Runtime 版本是 1.10.0,如果使用更高的版本 API 会略微有不同。

接下来是定义 SketchDrawing 类,继承自 OnnxRuntimeBase 类,用于部署模型和完成推理。

#include <iostream>
#include <fstream>
#include <string>
#include <math.h>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <onnxruntime_cxx_api.h>
#include "../common/OnnxRuntimeBase.h"

using namespace cv;
using namespace std;
using namespace Ort;

class SketchDrawing: public OnnxRuntimeBase {

public:
    SketchDrawing(std::string modelPath, const char* logId, const char* provider);
    Mat detect(Mat& image);

private:
    vector<float> input_image_;
    int inpWidth;
    int inpHeight;
    int outWidth;
    int outHeight;
};

SketchDrawing 类的实现可以参考项目的源码。

三. 应用层调用

定义和实现完 SketchDrawing 类后,需要通过 jni 暴露给应用层使用。下面是 jni 初始化相关的模型,生成素描画的方法。

SketchDrawing *sketchDrawing = nullptr;

JNIEXPORT void JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_initSketchDrawing
        (JNIEnv* env, jobject,jstring jModelPath) {

     const char* modelPath = env->GetStringUTFChars(jModelPath, JNI_FALSE);
     const std::string& onnx_logid = "Sketch Drawing";
     const std::string& onnx_provider = OnnxProviders::CPU;
     sketchDrawing = new SketchDrawing(modelPath, onnx_logid.c_str(), onnx_provider.c_str());

     env->ReleaseStringUTFChars(jModelPath, modelPath);
}

JNIEXPORT jintArray JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_sketchDrawing
        (JNIEnv* env, jobject,jbyteArray array) {

    Mat image = byteArrayToMat(env,array);
    Mat dst;

    try {
        dst = sketchDrawing->detect(image);
    } catch(...) {
    }

    jthrowable mException = NULL;
    mException = env->ExceptionOccurred();

    if (mException != NULL) {
       env->ExceptionClear();
       jclass exceptionClazz = env->FindClass("java/lang/Exception");
       env->ThrowNew(exceptionClazz, "jni exception");
       env->DeleteLocalRef(exceptionClazz);

       return env->NewIntArray(0);
    }

    if (dst.channels() == 1) {
        cvtColor(dst,dst,COLOR_GRAY2BGR);
    }

    return matToIntArray(env,dst);
}

对于应用层,需要编写好调用 jni 层的代码:

object ImageProcess {

    val loadPath = System.getProperty("compose.application.resources.dir") + File.separator

    init {
        // 需要先加载图像处理库,否则无法通过 jni 调用算法
        LoadManager.load()
    }

    ......

    /**
     * 初始化生成素描画模块
     */
    external fun initSketchDrawing(modelPath:String)

    /**
     * 生成素描画
     */
    external fun sketchDrawing(src: ByteArray):IntArray
}

在 Monica 启动时,先加载必须的模型

runInBackground { // 初始化生成素描画的模块
            DLManager.initSketchDrawingModule()
        }

最后,终于可以在应用层调用了

val (width,height,byteArray) = state.currentImage!!.getImageInfo()

                    try {
                        val outPixels = ImageProcess.sketchDrawing(byteArray)
                        state.addQueue(state.currentImage!!)
                        state.currentImage = BufferedImages.toBufferedImage(outPixels,width,height)
                    } catch (e:Exception) {
                        logger.error("faceDetect is failed", e)
                    }

我们来看看在 Monica 中使用的效果

9d68e4ee4c75385071843df51e739b7a.jpeg
image1.png
08847393c620d4d5cce497d8cd92b707.jpeg
image1的效果.png
6d97982810b57e9d92c19c485abfd5ed.jpeg
image2.png
528cf0fc33a09a8a0e8fd83522d3083a.jpeg
image2的效果.png

通过 ONNX Runtime 加速,推理的时间大概花费了0.5s

image: 4596x3064 538.9ms
speed: 5.8ms preprocess, 529.1ms inference, 4.0ms postprocess per image

四. 总结

Monica 快要到 1.0.0 版本,后续的重点依然是优化软件的架构,其次才会考虑引入一些比较有趣的深度学习的模型。如果模型过大的话,后续也可能会考虑放在服务端部署。

Monica github 地址:https://github.com/fengzhizi715/Monica

Java与Android技术栈】公众号

关注 Java/Kotlin 服务端、桌面端 、Android 、机器学习、端侧智能

更多精彩内容请关注:

宝塔面板是一款免费的服务器管理面板,可以帮助用户快速部署各种Web应用程序。在部署前后端分离项目时,需要分别部署前端和后端。 1. 部署后端 首先,需要在宝塔面板上安装需要的环境,比如PHP、MySQL等。然后,将后端代码上传到服务器上,可以使用FTP工具或者宝塔面板的文件管理器。 接着,需要创建一个数据库,并将后端代码中的数据库配置修改为正确的信息。最后,启动后端服务,可以使用宝塔面板的进程管理器或者命令行方式启动。 2. 部署前端 前端部署较为简单,只需要将前端代码上传到服务器上即可。可以使用FTP工具或者宝塔面板的文件管理器上传。 需要注意的是,前端代码通常是静态资源,可以使用Nginx等Web服务器进行部署。在宝塔面板上,可以通过添加网站来配置Nginx服务器,将前端代码部署到网站目录下即可。 3. 配置反向代理 为了让前端能够访问后端接口,需要在Nginx服务器中配置反向代理。具体操作为,在网站配置文件中添加如下代码: ``` location /api { proxy_pass http://localhost:8080; } ``` 其中,/api是前端访问后端接口的路径,http://localhost:8080是后端服务的地址。通过配置反向代理,前端就可以访问后端接口了。 4. 配置域名和SSL证书 为了让网站更加安全和方便访问,可以配置域名和SSL证书。在宝塔面板上,可以通过添加网站来配置域名和SSL证书。具体操作可以参考宝塔面板的文档。 以上就是在宝塔面板上部署前后端分离项目的步骤。需要注意的是,不同的项目可能会有一些差异,具体操作可以根据项目需求进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值