# 图像分割

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

## 模型简介

本示例采用了经典的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。

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

## 模型初始化

在模型初始化的过程中，首先采用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());

    // 获取模型输入/输出节点信息
    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到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)
{
    ...
        
    // 创建输入数据
    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.计算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文件是通过使用具有普通背景的汽车图像来训练的。因此，“现实世界“图像的分割结果不完美是意料之中的。为了获得更好的结果，建议对现实世界示例数据集上的模型进行微调。


