# 图像分割 本示例采用了经典的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 "../Resource/Models/Segmentation/unet_13_256.onnx" ``` ## 模型初始化 首先,通过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 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 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(); // 输出节点数据指针 // 计算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]的值 } } // 将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)