Tutorial_Cpp.md 6.95 KB
Newer Older
1
2
3
4
5
6
# 图像分割

本示例主要通过Unet模型说明如何使用MIGraphX C++ API进行图像分割模型的推理,包括模型初始化、预处理、模型推理。

## 模型简介

liucong's avatar
liucong committed
7
本示例采用了经典的Unet模型进行图像分割,模型下载地址:https://www.dropbox.com/s/3ntkhyk30x05uuv/unet_13_256.onnx, 将unet_13_256.onnx文件保存在Resource/Models文件夹下。模型结构如下图所示,可以通过netron工具, 链接:https://netron.app/, 查看具体的模型结构,该模型的输入shape为[batch_size,3,256,256],输出shape为[batch_size,1,256,256],数据排布为NCHW。
8

liucong's avatar
liucong committed
9
<img src="./Images/Unet_01.png" style="zoom:80%;" align=middle>
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

## 模型初始化

在模型初始化的过程中,首先采用parse_onnx()函数根据提供的模型地址加载图像分割Unet的onnx模型,保存在net中。其次,通过net.get_parameter_shapes()获取Unet模型的输入属性,包含inputName和inputShape。最后,完成模型加载后使用migraphx::gpu::target{}设置编译模式为GPU模式,并使用compile()函数编译模型,完成模型的初始化过程。

其中,模型地址设置在/Resource/Configuration.xml文件中的Unet节点中。

```C++
ErrorCode Unet::Initialize(InitializationParameterOfSegmentation initParamOfSegmentationUnet)
{
    ...

    // 加载模型
    net = migraphx::parse_onnx(modelPath); 
    LOG_INFO(logFile,"succeed to load model: %s\n",GetFileName(modelPath).c_str());

liucong's avatar
liucong committed
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    // 获取模型输入/输出节点信息
    std::cout<<"inputs:"<<std::endl;
    std::unordered_map<std::string, migraphx::shape> inputs=net.get_inputs();
    for(auto i:inputs)
    {
        std::cout<<i.first<<":"<<i.second<<std::endl;
    }
    std::cout<<"outputs:"<<std::endl;
    std::unordered_map<std::string, migraphx::shape> outputs=net.get_outputs();
    for(auto i:outputs)
    {
        std::cout<<i.first<<":"<<i.second<<std::endl;
    }
    inputName=inputs.first;
    inputShape=inputs.second;
liucong's avatar
liucong committed
41
42
43
44
45
    int N=inputShape.lens()[0];
    int C=inputShape.lens()[1];
    int H=inputShape.lens()[2];
    int W=inputShape.lens()[3];
    inputSize=cv::Size(W,H);
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

    // 设置模型为GPU模式
    migraphx::target gpuTarget = migraphx::gpu::target{};

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0;                          // 设置GPU设备,默认为0号设备
    options.offload_copy=true;                    // 设置offload_copy
    net.compile(gpuTarget,options);               
    LOG_INFO(logFile,"succeed to compile model: %s\n",GetFileName(modelPath).c_str());

    ...
}
```

## 预处理

完成模型初始化后,需要将输入数据进行如下预处理:

​        1.尺度变换,将图像resize到256x256大小

​        2.归一化,将数据归一化到[0.0, 1.0]之间

​        3.数据排布,将数据从HWC转换为NCHW

本示例代码主要通过opencv实现预处理操作:

```C++
ErrorCode Unet::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage)
{
    ...
        
    // 图像预处理并转换为NCHW
    cv::Mat inputBlob;
    cv::dnn::blobFromImage(srcImage,    // 输入数据,支持多张图像
                    inputBlob,          // 输出数据
                    1 / 255.0,          // 缩放系数
                    inputSize,          // 模型输入大小,这里为256x256
                    Scalar(0, 0, 0),    // 均值,这里不需要减均值,所以设置为0
                    true,               // 通道转换,B通道与R通道互换,所以为true
                    false);
    
    ...
}
```

1.cv::dnn::blobFromImage()函数支持多个输入图像,首先将输入图像resize到inputSize大小,然后减去均值,其次乘以缩放系数1/255.0并转换为NCHW,最终将转换好的数据保存到inputBlob作为输入数据,执行后续的模型推理。

## 推理

完成图像预处理后,就可以执行模型推理。首先,定义inputData表示Unet模型的输入数据,inputName表示Unet模型的输入节点名,采用migraphx::argument{inputShape, (float*)inputBlob.data}保存前面预处理的数据inputBlob,第一个参数表示输入数据的shape,第二个参数表示输入数据指针。其次,执行net.eval(inputData)获得模型的推理结果,由于这里只有一个输出节点,仅使用results[0]获取输出节点的数据,就可以对输出数据执行相关后处理操作。

```c++
ErrorCode Unet::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage)
{
    ...
        
liucong's avatar
liucong committed
103
    // 创建输入数据
104
105
106
107
108
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> results = net.eval(inputData);
liucong's avatar
liucong committed
109
110
111
112
    // 如果想要指定输出节点,可以给eval()函数中提供outputNames参数来实现
    //std::vector<std::string> outputNames = {"outputs"};
    //std::vector<migraphx::argument> inferenceResults = net.eval(inputData, outputNames);

113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

    // 获取输出节点的属性
    migraphx::argument result = results[0];                 // 获取第一个输出节点的数据
    migraphx::shape outputShape=result.get_shape();         // 输出节点的shape
    std::vector<std::size_t> outputSize=outputShape.lens(); // 每一维大小,维度顺序为(N,C,H,W) 
    int numberOfOutput=outputShape.elements();              // 输出节点元素的个数
    float *data = (float *)result.data();                   // 输出节点数据指针

    ...
}
```

模型得到的推理结果并不能直接作为图像的分割结果,还需要做如下处理:

1.计算sigmoid值,当计算值大于0.996时值为1,小于等于0.996时值为0,保存在数组value_mask中。

2.保存结果,创建一个cv::Mat将value_mask数组中的值按行依次赋值到对应位置,得到最终的分割图像。

```c++
ErrorCode Unet::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage)
{
    ...

    // 计算sigmoid值,并且当大于0.996时,值为1,当小于0.996时,值为0,存储在value_mask[]数组中
    int value_mask[numberOfOutput];
    for(int i=0; i<numberOfOutput; ++i)
    {
        float num  = Sigmoid(data[i]);
        if (num > 0.996)
        {
            value_mask[i] = 1;
        }
        else
        {
            value_mask[i] = 0;
        }
    }

    // 将对应的value_mask[]数组中的值依次赋值到outputImage对应位置处
    cv::Mat outputImage = cv::Mat_<int>(Size(outputShape.lens()[3], outputShape.lens()[2]), CV_32S);
    for(int i=0;i<outputShape.lens()[2];++i)
    {
        for(int j=0;j<outputShape.lens()[3];++j)
        {
            outputImage.at<int>(i,j)=value_mask[256*i+j];  // 其中,256代表了outputShape.lens()[3]的值
        }
    }
    
    ...
}
```

注:本次采用的模型权重onnx文件是通过使用具有普通背景的汽车图像来训练的。因此,“现实世界“图像的分割结果不完美是意料之中的。为了获得更好的结果,建议对现实世界示例数据集上的模型进行微调。