C++ 实现ONNX语义分割(TopFormer)

TopFormer是一个比较高效的语义分割模型,
TopFormer ONNX主页上可以找到python的实现。

用一个小猫的图片测试一下
在这里插入图片描述
瑕疵是有一些,不过它的特点是非常快,视频语义分割能达到实时(本地CPU)。
TopFormer模型的输出Size是64x64,这个是resize后的效果。

用cpp实现看看效果。
cpp版的onnxruntime安装这里就不说了,参考链接
还需要安装cpp版的opencv,我选的是4.5,安装参考

然后你的CmakeLists.txt里面要link这两样

target_link_libraries(${PROJECT_NAME}
        ${OpenCV_LIBS}
        ${ONNXRUNTIME_LIB}
        )

来一波include

#include <iostream>
#include <fstream>
#include <array>
#include <string>

#include <opencv4/opencv2/core/mat.hpp>
#include <opencv4/opencv2/imgcodecs.hpp>
#include <opencv4/opencv2/imgproc.hpp>
#include <core/session/experimental_onnxruntime_cxx_api.h>

using namespace std;
using namespace cv;

onnx模型直接用github下下来的就好,
定义几个onnxruntime变量

const string modelFile = "TopFormer-S_512x512_2x8_160k.onnx";
Ort::Env env;
Ort::SessionOptions sessionOptions;
Ort::Session session(nullptr);

try{
     session = Ort::Session(env, modelFile.data(), sessionOptions);
} catch(Ort::Exception& e) {
     cout << e.what() << endl;
     return 1;
}

TopFormer的输入Size是1x3x512x512,输出Size是1x150x64x64,
所以先定义下形状。

constexpr int64_t numChannels = 3;  //constexpr修饰,表示编译期就可以得到常量值去优化
constexpr int64_t width = 512;
constexpr int64_t height = 512;
constexpr int64_t out_width = 64;
constexpr int64_t out_height = 64;
constexpr int64_t numClasses = 150;  //类别数
constexpr int64_t numInputElements = numChannels * height * width;
constexpr int64_t numOutputElements = numClasses * out_height * out_width;

定义输入输出数组,输入输出Tensor,
有人会说,输入本身就是矩阵了,为什么要转成数组。
用array, vector的话会保存指针,就不需要反复create Tensor,只需要把数据copy到数组。

array<float, numInputElements> input;
array<float, numOutputElements> output;

//输入输出需要转成Tensor
//这个Tensor保存的是输入输出数组的pointer,所以不能删除输入输出数组
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
auto inputTensor = Ort::Value::CreateTensor<float>(memory_info, input.data(),
                                                   input.size(), inputShape.data(),
                                                   inputShape.size());
auto outputTensor = Ort::Value::CreateTensor<float>(memory_info, output.data(),output.size(),
                                                    outputShape.data(), outputShape.size());
//也可以不用转成数组,直接用Tensor,但是array,vector会便利一些
//因为图片如果很多,就需要反复create Tensor,用数组的话只需要copy到数组

读入image,预处理(比如-mean, /std),
注意要把image处理成CxHxW的形式

Mat img = imread(imageFile);
const vector<float> imageVec = loadImage(img); //CxHxW: [ch1:HXW][ch2:HxW][ch3:HxW]
//预处理(resize, normalize)放在loadImage函数里面

预处理中mean和std我们用TopFormer python版本中的数据

//-mean, /std
float img_mean[3] = {0.485, 0.456, 0.406};
float img_std[3] = {0.229, 0.224, 0.225};

注意要把image拉成一维数组,顺序是CxHxW

resize(image, image, Size(512, 512));
image = image.reshape(1, 1);  //图像拉成向量
//HWC -> CHW (transpose(2, 0, 1)
for(size_t ch = 0; ch < 3; ++ch) {
    for(size_t i = ch; i < vec.size(); i+= 3) {
        //保存成CxHxW的顺序
    }
}

copy图像数据到input数组

//把图像copy进input数组
copy(imageVec.begin(), imageVec.end(), input.begin());

定义输入输出的name,要和onnx模型的输入输出name一致,
可以直接定义,也可以从onnx模型中获取。

    //输入输出的name,可以自己定义,也可以从onnx模型中获取
    const array<const char*, 1> inputNames = {"input"};
    const array<const char*, 1> outputNames = {"output"};


//    //从onnx模型中获取输入输出名称
//    vector<const char*> inputNames;
//    vector<const char*> outputNames;
//
//    Ort::AllocatorWithDefaultOptions alloc;
//    for(size_t i = 0; i < session.GetInputCount(); i++) {
//        //push_back是创建元素,然后添加到vector末尾,
//        //emplace_back是直接在vector末尾创建元素,省去了copy或移动元素的过程
//        inputNames.emplace_back(session.GetInputName(i, alloc));
//    }
//    for(size_t i = 0; i < session.GetOutputCount(); i++) {
//        outputNames.emplace_back(session.GetOutputName(i, alloc));
//    }

推理过程,
这里的outputTensor是前面创建的,指向的是output数组,
所以最后的输出数据会在output数组里。

//inference
Ort::RunOptions runOptions;
try{
    session.Run(runOptions, inputNames.data(), &inputTensor, 1,
                outputNames.data(), &outputTensor, 1);
}catch (Ort::Exception& e) {
    cout << e.what() << endl;
    return 1;
}

现在我们得到的输出output是一条向量,并不是1x150x64x64的形状,
而我们想要的是1x150x64x64,然后在64x64的每个像素上,取所有channel的最大值index作为label,
取出label对应的颜色保存到像素上。
很复杂有木有。

先解决每个label对应什么颜色吧,
TopFormer共有150个类别,在ade20k_label_colors.txt(github下载)中有每个类别对应的颜色。
也可以自己定义颜色表。

const int classes = 150;
unsigned char colors[classes][3];
void color_map(int classes) {
   int r = 0, g = 0, b = 0;
   for(int i = 0; i < classes; i++) {  //150类
       int c = i;
       for(int j = 0; j < 8;j ++) {
           r = r | (bitget(c, 0) << 7-j);
           g = g | (bitget(c, 1) << 7-j);
           b = b | (bitget(c, 2) << 7-j);
           c = c >> 3;
       }
       for(int j = 0; j < 3; j++) {
           colors[i][0] = r;
           colors[i][1] = g;
           colors[i][2] = b;
       }
   }
}

把“在64x64的每个像素上,取所有channel的最大值index作为label,
取出label对应的颜色保存到像素上。“放在一起处理。
那么需要一个64x64x3的输出map

Mat output_map(out_height, out_width, CV_MAKETYPE(CV_8UC1, 3));

现在输出是1x150x64x64拉成一条的向量,
保存顺序是这样的,
[ch1: 64x64] [ch2: 64x64] … [ch150: 64x64]
只需要每隔64x64个点取各个channel数值,找到最大值所在的label即可。
注意opencv是以BGR的channel顺序储存的。

看看output_map的结果吧。
这里是64x64大小,没有resize.
可以看到跟python版本效果类似。
在这里插入图片描述
完整代码放在github的semantic

参考资料

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
ONNX(Open Neural Network Exchange)是今天最流行的深度学习模型格式之一,被广泛用于将训练好的模型在不同的平台间进行交换和部署。ONNX语义分割是指使用ONNX格式的模型进行图像语义分割任务。 语义分割计算机视觉领域中的重要任务,其目标是将图像中的每个像素分类为不同的预定义类别。传统的方法通常使用卷积神经网络(CNN)进行语义分割,而ONNX为这些CNN模型提供了一个通用的格式。 使用ONNX进行语义分割有以下几个步骤: 1. 训练模型:使用深度学习框架(如PyTorch、TensorFlow等)训练一个语义分割模型,确保模型能够准确地将图像中的像素分类为需要的类别。 2. 导出为ONNX格式:将训练好的模型导出为ONNX格式。不同的深度学习框架通常都提供了导出模型为ONNX格式的功能,可以通过相应的API或命令来完成。 3. 部署和推理:将导出的ONNX模型部署到目标平台上以进行推理。ONNX模型可以在支持ONNX的平台上进行部署和推理,如移动设备、物联网设备、嵌入式系统等。 4. 图像分割:使用部署的ONNX模型对新的图像进行语义分割。将待分割的图像输入到ONNX模型中,模型将根据其学习到的知识对每个像素进行分类。 ONNX语义分割提供了一种通用的、跨平台的解决方案,使得开发人员能够在不同的设备上部署和使用训练好的语义分割模型。它具有跨平台兼容性、高性能、低资源消耗等优势,因此在计算机视觉领域得到了广泛应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝羽飞鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值