# 图像分割

本示例主要通过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()函数编译模型，完成模型的初始化过程。

其中，模型地址设置在/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());

    // 获取模型输入/输出节点信息
    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;
    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);

    // 设置模型为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到513x513大小

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

```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);
    // 如果想要指定输出节点，可以给eval()函数中提供outputNames参数来实现
    //std::vector<std::string> outputNames = {"outputs"};
    //std::vector<migraphx::argument> inferenceResults = net.eval(inputData, outputNames);


    // 获取输出节点的属性
    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.计算softmax值，计算不同通道同一[H,W]位置的softmax值，找出概率最高的通道。

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

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

	cv::Mat outputImage(cv::Size(W, H), CV_8UC3);
    // 创建颜色映射表
    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数据集来训练的。因此，“现实世界“图像的分割结果不完美是意料之中的。为了获得更好的结果，建议对现实世界示例数据集上的模型进行微调。

