Unet.md 6.8 KB
Newer Older
lijian6's avatar
lijian6 committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
103
104
105
106
107
108
109
110
111
112
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
# 图像分割

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

![Unet_Image_1](../Images/Unet_Image_1.png)

## 参数设置

samples工程中的Resource/Configuration.xml文件的Unet节点表示图像分割模型Unet的参数,主要包括模型存放路径。

```xml
<!--Unet-->
<Unet>
	<ModelPath>"../Resource/Models/Segmentation/unet_13_256.onnx"</ModelPath>
</Unet>
```

## 模型初始化

首先,通过parse_onnx()函数加载图像分割Unet的onnx模型,并可以通过program的get_parameter_shapes()函数获取网络的输入属性。完成模型加载之后需要使用compile()方法编译模型,编译模式使用migraphx::gpu::target{}设为GPU模式,编译过程主要基于MIGraphX IR完成各种优化。

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

    // 加载模型
    net = migraphx::parse_onnx(modelPath);       // 根据提供的模型地址,去加载模型文件
    LOG_INFO(logFile,"succeed to load model: %s\n",GetFileName(modelPath).c_str());

    // 获取模型输入属性
    std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
    inputName=inputAttribute.first;
    inputShape=inputAttribute.second;
    inputSize=cv::Size(inputShape.lens()[3],inputShape.lens()[2]);

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

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0;                          // 设置GPU设备,默认为0号设备(>=1.2版本中支持)
    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)
{
    
    ...
        
    // 图像预处理,将图像resize到(256x256)大小,并转换为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作为输入数据执行推理。

## 推理

完成图像预处理后,就可以执行推理,得到推理结果。

```c++
ErrorCode Unet::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage)
{
    ...
        
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> results = net.eval(inputData);

    // 获取输出节点的属性
    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();                   // 输出节点数据指针

    // 计算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]的值
        }
    }
    // 将32S格式的数据转换为8U格式的数据
    outputImage.convertTo(maskImage, CV_8U, 255.0);
    ...
}
```

1.inputData表示MIGraphX的输入数据,这里表示为图像数据,inputName表示输入节点名,migraphx::argument{inputShape, (float*)inputBlob.data}表示该节点名对应的数据,这里是通过前面预处理的数据inputBlob来创建的,第一个参数表示数据的shape,第二个参数表示数据指针。

2.net.eval(inputData)返回模型的推理结果,由于这里只有一个输出节点,所以std::vector中只有一个数据,results[0]表示第一个输出节点,获取输出数据之后,就可以对输出数据执行相关后处理操作。

3.模型得到的推理结果并不能直接作为分割结果。首先,对推理结果计算sigmoid值,当计算值大于0.996时值为1,小于等于0.996时值为0,保存在数组value_mask中。其次,创建一个cv::Mat将value_mask数组中的值按行依次赋值到对应的位置。最后,将32S格式的数据转换为8U格式的数据,对应位置乘以255,得到最终的分割图像。

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

## 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```c++
./ MIGraphX_Samples 8
```

会在当前目录中生成分割结果图像Result.jpg

输出结果为:

![Unet_output_python](../Images/Unet_output_c++.jpg)