# 图像分割
本示例主要通过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。
## 模型初始化
在模型初始化的过程中,首先采用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::pair inputAttribute=net.get_parameter_shapes();
inputName=inputAttribute.first;
inputShape=inputAttribute.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 results = net.eval(inputData);
// 获取输出节点的属性
migraphx::argument result = results[0]; // 获取第一个输出节点的数据
migraphx::shape outputShape=result.get_shape(); // 输出节点的shape
std::vector 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 0.996)
{
value_mask[i] = 1;
}
else
{
value_mask[i] = 0;
}
}
// 将对应的value_mask[]数组中的值依次赋值到outputImage对应位置处
cv::Mat outputImage = cv::Mat_(Size(outputShape.lens()[3], outputShape.lens()[2]), CV_32S);
for(int i=0;i(i,j)=value_mask[256*i+j]; // 其中,256代表了outputShape.lens()[3]的值
}
}
...
}
```
注:本次采用的模型权重onnx文件是通过使用具有普通背景的汽车图像来训练的。因此,“现实世界“图像的分割结果不完美是意料之中的。为了获得更好的结果,建议对现实世界示例数据集上的模型进行微调。