Tutorial_Cpp.md 10.7 KB
Newer Older
shangxl's avatar
shangxl committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 图像分割

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

## 模型简介

本示例采用了经典的DeepLabv3模型进行图像分割, 模型deeplabv3_resnet101.onnx文件保存在Resource/Models文件夹下。模型结构如下图所示,可以通过netron工具, 链接:https://netron.app/, 查看具体的模型结构,该模型的输入shape为[batch_size,3,513,513],输出shape为[batch_size,513,513],数据排布为NCHW。

<img src="./Images/deeplabv3_01.jpg" style="zoom:80%;" align=middle>

## 模型初始化

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

15
其中,模型地址设置在/Resource/Configuration.xml文件中的DeepLabV3节点中。
shangxl's avatar
shangxl committed
16
17

```C++
18
ErrorCode DeepLabV3::Initialize(InitializationParameterOfSegmentation initParamOfSegmentationUnet){
shangxl's avatar
shangxl committed
19
20
21
    ...

    // 加载模型
22
    if(!Exists(modelPath))
shangxl's avatar
shangxl committed
23
    {
24
25
        LOG_ERROR(stdout, "%s not exist!\n", modelPath.c_str());
        return MODEL_NOT_EXIST;
shangxl's avatar
shangxl committed
26
    }
27
28
29
30
31
32

    migraphx::onnx_options onnx_options;
    if(initParamOfSegmentationUnet.loadMode){
        onnx_options.map_input_dims["input"] = {1, 3, 513, 513};
    }else{
        onnx_options.map_input_dims["input"] = {3, 3, 513, 513};
shangxl's avatar
shangxl committed
33
    }
34
35
    net = migraphx::parse_onnx(modelPath,onnx_options);
    LOG_INFO(stdout, "succeed to load model: %s\n", GetFileName(modelPath).c_str());
shangxl's avatar
shangxl committed
36

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    // 获取模型输入/输出节点信息
    std::unordered_map<std::string, migraphx::shape> inputs  = net.get_inputs();
    std::unordered_map<std::string, migraphx::shape> outputs = net.get_outputs();
    inputName = inputs.begin()->first;
    inputShape = inputs.begin()->second;
    outputName                                               = outputs.begin()->first;
    outputShape                                              = outputs.begin()->second;
    auto it = outputs.begin();
    ++it;
    outputName2                                              = it->first;
    outputShape2                                             = it->second;


    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);

 
shangxl's avatar
shangxl committed
57
58
59
    // 设置模型为GPU模式
    migraphx::target gpuTarget = migraphx::gpu::target{};

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    if(useInt8){
        std::vector<cv::Mat> calibrateImages;
        std::string folderPath = "../Resource/Images/calibrateImages/";
        std::string calibrateImageExt = "*.jpg";
        std::vector<cv::String> calibrateImagePaths;
        cv::glob(folderPath + calibrateImageExt, calibrateImagePaths, false);
        for(const auto& path : calibrateImagePaths){
            calibrateImages.push_back(cv::imread(path, 1));
        }
        cv::Mat inputcalibrateBlob;
        cv::dnn::blobFromImages(calibrateImages, inputcalibrateBlob, 1 / 255.0, inputSize, cv::Scalar(0, 0, 0), true, false);
        std::unordered_map<std::string, migraphx::argument> inputData;
        inputData[inputName] = migraphx::argument{inputShape, (float *)inputcalibrateBlob.data};
        std::vector<std::unordered_map<std::string, migraphx::argument>> calibrationData = {inputData};
         // INT8量化
        migraphx::quantize_int8(net, gpuTarget, calibrationData);
    }else{
        migraphx::quantize_fp16(net);
    }

shangxl's avatar
shangxl committed
80
81
    // 编译模型
    migraphx::compile_options options;
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
    options.device_id    = 0; // 设置GPU设备,默认为0号设备
    if(useOffloadCopy){
        options.offload_copy = true;
    }else{
        options.offload_copy = false;
    }

    net.compile(gpuTarget, options);
    LOG_INFO(stdout, "succeed to compile model: %s\n", GetFileName(modelPath).c_str());
    if(!useOffloadCopy){
        inputBufferDevice = nullptr;
        hipMalloc(&inputBufferDevice, inputShape.bytes());
        modalDataMap[inputName] = migraphx::argument{inputShape, inputBufferDevice};                   
    
        outputBufferDevice = nullptr;
        hipMalloc(&outputBufferDevice, outputShape.bytes());
        outputBufferDevice2 = nullptr;
        hipMalloc(&outputBufferDevice2, outputShape2.bytes());
        modalDataMap[outputName] = migraphx::argument{outputShape, outputBufferDevice};
        modalDataMap[outputName2] = migraphx::argument{outputShape2, outputBufferDevice2};
        outputBufferHost             = nullptr; // host内存
        outputBufferHost             = malloc(outputShape.bytes());
        outputBufferHost2            = nullptr; // host内存
        outputBufferHost2            = malloc(outputShape2.bytes());
    }
shangxl's avatar
shangxl committed
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

    ...
}
```

## 预处理

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

​        1.尺度变换,将图像resize到513x513大小

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

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

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

```C++
shangxl's avatar
shangxl committed
125
ErrorCode Unet::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage){
shangxl's avatar
shangxl committed
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
    ...
        
    // 图像预处理并转换为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作为输入数据,执行后续的模型推理。

## 推理

146
147
148
149
150
完成图像预处理后,就可以执行模型推理。

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

当useOffloadCopy==false时,首先,定义inputData表示deeplabv3模型的输入数据,并将数据拷贝到GPU中的输入内存。其次,执行net.eval(modalDataMap)执行推理,modalDataMap中保存着模型的GPU输出地址,推理的输出结果会保存在对应的输出内存中,将GPU输出数据拷贝到分配好的host输出内存后,即可获取输出节点的数据,就可以对输出数据执行相关后处理操作。
shangxl's avatar
shangxl committed
151
152

```c++
153
ErrorCode DeepLabV3::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage){
shangxl's avatar
shangxl committed
154
155
    ...
        
156
157
158
159
	if(useOffloadCopy){
        // 创建输入数据
        std::unordered_map<std::string, migraphx::argument> inputData;
        inputData[inputName] = migraphx::argument{inputShape, (float*)inputBatchBlob.data};
shangxl's avatar
shangxl committed
160

161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
        // 推理
        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();               // 输出节点数据指针

    }else{

        migraphx::argument inputData = migraphx::argument{inputShape, (float*)inputBatchBlob.data};
        // 拷贝到device输入内存
        hipMemcpy(inputBufferDevice, inputData.data(), inputShape.bytes(), hipMemcpyHostToDevice);
        // 推理
        std::vector<migraphx::argument> results = net.eval(modalDataMap);

        // 获取输出节点的属性
        migraphx::argument result    = results[0];                                      // 获取第一个输出节点的数据
        migraphx::shape outputShapes = result.get_shape();                              // 输出节点的shape
        std::vector<std::size_t> outputSize = outputShapes.lens();                      // 每一维大小,维度顺序为(N,C,H,W)
        int numberOfOutput = outputShapes.elements();                                   // 输出节点元素的个数
        // 将device输出数据拷贝到分配好的host输出内存
        hipMemcpy(outputBufferHost,outputBufferDevice, outputShapes.bytes(),hipMemcpyDeviceToHost); // 直接使用事先分配好的输出内存拷贝
        
    }    
shangxl's avatar
shangxl committed
189
190
191
192
193
194
195
196
197
198
199
200

    ...
}
```

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

1.计算softmax值,计算不同通道同一[H,W]位置的softmax值,找出概率最高的通道。

2.保存结果,创建一个cv::Mat,根据不同的通道索引在颜色映射表取值并按行依次赋值到Mat对应位置,得到最终的分割图像。

```c++
shangxl's avatar
shangxl committed
201
ErrorCode DeepLabV3::Segmentation(const cv::Mat &srcImage, cv::Mat &maskImage){
shangxl's avatar
shangxl committed
202
    ...
203
204
    
    cv::Mat outputImage(cv::Size(W, H), CV_8UC3);
shangxl's avatar
shangxl committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
    // 创建颜色映射表
    std::vector<cv::Scalar> color_map = create_color_map();

    for(int i = 0;i < H; i++){
        for(int j = 0;j < W;j++){
            std::vector<float> channel_value;
            for(int k = 0;k < C;k++){
                channel_value.push_back(data[k*(H*W)+i*W+j]);
            }
            std::vector<float> probs = softmax(channel_value);
            // 找到概率最高的类别索引
            int max_index = std::max_element(probs.begin(),probs.end())-probs.begin();
            cv::Scalar sc = color_map[max_index];
            outputImage.at<cv::Vec3b>(i, j)[0]= sc.val[0]; 
            outputImage.at<cv::Vec3b>(i, j)[1]= sc.val[1];
            outputImage.at<cv::Vec3b>(i, j)[2]= sc.val[2];
        }
    }
    maskImage = outputImage.clone();
    
    ...
}
```

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