摘要:本实践是基于Windows版MindStudio 5.0.RC3,远程连接ECS服务器使用,ECS是基于官方分享的CANN6.0.RC1_MindX_Vision3.0.RC3镜像创建的。
本文分享自华为云社区《【MindStudio训练营第一季】基于U-Net网络的图像分割的MindStudio实践》,作者:Tianyi_Li 。
1.U-Net网络介绍:
U-Net模型基于二维图像分割。在2015年ISBI细胞跟踪竞赛中,U-Net获得了许多最佳奖项。论文中提出了一种用于医学图像分割的网络模型和数据增强方法,有效利用标注数据来解决医学领域标注数据不足的问题。U型网络结构也用于提取上下文和位置信息。
[U-Net 论文]: Olaf Ronneberger, Philipp Fischer, Thomas Brox. “U-Net: Convolutional Networks for Biomedical Image Segmentation.” conditionally accepted at MICCAI 2015. 2015.
2.ECS运行说明
我们的操作基本都在root用户下执行。
首先,修改bash,具体命令和结果如下。
本项目支持MindStudio运行和终端运行。
(1)下载项目代码
下载链接:https://alexed.obs.cn-north-4.myhuaweicloud.com/unet_sdk.zip
将项目文件unet_sdk.zip上传至华为云ECS弹性云服务器/root/目录下,并解压;或者下载到本地电脑,用MindStudio打开。
将之前unet_hw960_bs1.air模型放到/unet_sdk/model/目录下。
项目文件结构
├── unet_sdk ├── README.md ├── data //数据集 │ ├── 1 │ │ ├──image.png //图片 │ │ ├──mask.png //标签 │ ... ├── model │ ├──air2om.sh // air模型转om脚本 │ ├──xxx.air //air模型 │ ├──xxx.om //om模型 │ ├──aipp_unet_simple_opencv.cfg // aipp文件 ├── pipeline │ ├──unet_simple_opencv.pipeline // pipeline文件 ├── main.py // 推理文件 ├── run.sh // 执行文件 ├── requirements.txt // 需要的三方库
(2) 模型转换
将unet_hw960_bs1.air模型转为昇腾AI处理器支持的.om格式离线模型,此处模型转换需要用到ATC工具。
昇腾张量编译器(Ascend Tensor Compiler,简称ATC)是昇腾CANN架构体系下的模型转换工具,它可以将开源框架的网络模型或Ascend IR定义的单算子描述文件(json格式)转换为昇腾AI处理器支持的.om格式离线模型。模型转换过程中可以实现算子调度的优化、权值数据重排、内存使用优化等,可以脱离设备完成模型的预处理。
ATC参数概览:
(3)运行脚本
运行脚本:
cd unet_sdk/model/ # 切换至模型存储目录 atc --framework=1 --model=unet_hw960_bs1.air --output=unet_hw960_bs1 --input_format=NCHW --soc_version=Ascend310 --log=error --insert_op_conf=aipp_unet_simple_opencv.cfg
- 注意air模型转om只支持静态batch,这里batchsize=1。
参数说明:
framework:原始框架类型。 model:原始模型文件路径与文件名。 output:转换后的离线模型的路径以及文件名。 input_format:输入数据格式。 soc_version:模型转换时指定芯片版本。 log:显示日志的级别。 insert_op_conf:插入算子的配置文件路径与文件名,这里使用AIPP预处理配置文件,用于图像数据预处理。
输出结果:
ATC run success,表示模型转换成功,得到unet_hw960_bs1.om模型。
模型转换成功之后,可以使用MindX SDK mxVision运行脚本,在Ascend 310上进行推理。
(4) MindX SDK mxVision 执行推理
MindX SDK文档请参考:https://support.huaweicloud.com/ug-vis-mindxsdk203/atlasmx_02_0051.html
MindX SDK执行推理的业务流程:
通过stream配置文件,Stream manager可识别需要构建的element以及element之间的连接关系,并启动业务流程。Stream manager对外提供接口,用于向stream发送数据和获取结果,帮助用户实现业务对接。
plugin表示业务流程中的基础模块,通过element的串接构建成一个stream。buffer用于内部挂载解码前后的视频、图像数据,是element之间传递的数据结构,同时也允许用户挂载元数据(Metadata),用于存放结构化数据(如目标检测结果)或过程数据(如缩放后的图像)。
MindX SDK基础概念介绍:
MindX SDK基础插件:
MindX SDK业务流程编排:
Stream配置文件以json格式编写,用户必须指定业务流名称、元件名称和插件名称,并根据需要,补充元件属性和下游元件名称信息。
以下表格为本实验pipeline/unet_simple_opencv.pipeline文件及其对应的名称及描述:
pipeline/unet_simple_opencv.pipeline文件内容如下,可根据实际开发情况进行修改。
{ "unet_mindspore": { "stream_config": { "deviceId": "0" }, "appsrc0": { "props": { "blocksize": "4096000" }, "factory": "appsrc", "next": "mxpi_imagedecoder0" }, "mxpi_imagedecoder0": { "props": { "cvProcessor": "opencv", "outputDataFormat": "BGR" }, "factory": "mxpi_imagedecoder", "next": "mxpi_imagecrop0" }, "mxpi_imagecrop0": { "props": { "cvProcessor": "opencv", "dataSource": "ExternalObjects" }, "factory": "mxpi_imagecrop", "next": "mxpi_imageresize0" }, "mxpi_imageresize0": { "props": { "handleMethod": "opencv", "resizeType": "Resizer_Stretch", "resizeHeight": "960", "resizeWidth": "960" }, "factory": "mxpi_imageresize", "next": "mxpi_tensorinfer0" }, "mxpi_tensorinfer0": { "props": { "dataSource": "mxpi_imageresize0", "modelPath": "model/unet_hw960_bs1_AIPP.om" }, "factory": "mxpi_tensorinfer", "next": "mxpi_dumpdata0" }, "mxpi_dumpdata0": { "props": { "requiredMetaDataKeys": "mxpi_tensorinfer0" }, "factory": "mxpi_dumpdata", "next": "appsink0" }, "appsink0": { "props": { "blocksize": "4096000" }, "factory": "appsink" } } }
(5) 修改modelPath
打开pipeline/unet_simple_opencv.pipeline文件,将"mxpi_tensorinfer0"元件的属性"modelPath"(模型导入路径)修改为模型转换后保存的om模型"model/unet_hw960_bs1.om"。
修改结果:
"modelPath": "model/unet_hw960_bs1.om"
modelPath修改完成之后,保存pipeline/unet_simple_opencv.pipeline文件。
StreamManagerApi:
StreamManagerApi文档请参考:
https://support.huaweicloud.com/ug-vis-mindxsdk203/atlasmx_02_0320.html
StreamManagerApi用于对Stream流程的基本管理:加载流程配置、创建流程、向流程发送数据、获得执行结果、销毁流程。
这里用到的StreamManagerApi有:
- InitManager:初始化一个StreamManagerApi。
- CreateMultipleStreams:根据指定的配置创建多个Stream。
- SendData:向指定Stream上的输入元件发送数据(appsrc)。
- GetResult:获得Stream上的输出元件的结果(appsink)
- DestroyAllStreams:销毁所有的流数据。
main.py文件内容如下,可根据实际开发情况进行修改。
import argparse import base64 import json import os import cv2 import numpy as np from StreamManagerApi import * import MxpiDataType_pb2 as MxpiDataType x0 = 2200 # w:2200~4000; h:1000~2800 y0 = 1000 x1 = 4000 y1 = 2800 ori_w = x1 - x0 ori_h = y1 - y0 def _parse_arg(): parser = argparse.ArgumentParser(description="SDK infer") parser.add_argument("-d", "--dataset", type=str, default="data/", help="Specify the directory of dataset") parser.add_argument("-p", "--pipeline", type=str, default="pipeline/unet_simple_opencv.pipeline", help="Specify the path of pipeline file") return parser.parse_args() def _get_dataset(dataset_dir): img_ids = sorted(next(os.walk(dataset_dir))[1]) for img_id in img_ids: img_path = os.path.join(dataset_dir, img_id) yield img_path def _process_mask(mask_path): # 手动裁剪 mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)[y0:y1, x0:x1] return mask def _get_stream_manager(pipeline_path): stream_mgr_api = StreamManagerApi() ret = stream_mgr_api.InitManager() #初始化stream if ret != 0: print(f"Failed to init Stream manager, ret={ret}") exit(1) with open(pipeline_path, 'rb') as f: pipeline_content = f.read() ret = stream_mgr_api.CreateMultipleStreams(pipeline_content) # 创建stream if ret != 0: print(f"Failed to create stream, ret={ret}") exit(1) return stream_mgr_api def _do_infer_image(stream_mgr_api, image_path): stream_name = b'unet_mindspore' # 与pipeline中stream name一致 data_input = MxDataInput() with open(image_path, 'rb') as f: data_input.data = f.read() # 插入抠图的功能,扣1800*1800大小 roiVector = RoiBoxVector() roi = RoiBox() roi.x0 = x0 roi.y0 = y0 roi.x1 = x1 roi.y1 = y1 roiVector.push_back(roi) data_input.roiBoxs = roiVector unique_id = stream_mgr_api.SendData(stream_name, 0, data_input) # 向指定Stream上的输入元件发送数据(appsrc) if unique_id < 0: print("Failed to send data to stream.") exit(1) infer_result = stream_mgr_api.GetResult(stream_name, unique_id) # 获得Stream上的输出元件的结果(appsink) if infer_result.errorCode != 0: print(f"GetResult error. errorCode={infer_result.errorCode}," f"errorMsg={infer_result.data.decode()}") exit(1) # 用dumpdata获取数据 infer_result_data = json.loads(infer_result.data.decode()) content = json.loads(infer_result_data['metaData'][0]['content']) tensor_vec = content['tensorPackageVec'][0]['tensorVec'][1] # 1是argmax结果 data_str = tensor_vec['dataStr'] tensor_shape = tensor_vec['tensorShape'] argmax_res = np.frombuffer(base64.b64decode(data_str), dtype=np.float32).reshape(tensor_shape) np.save("argmax_result.npy", argmax_res) tensor_vec = content['tensorPackageVec'][0]['tensorVec'][0] # 0是softmax结果 data_str = tensor_vec['dataStr'] tensor_shape = tensor_vec['tensorShape'] softmax_res = np.frombuffer(base64.b64decode(data_str), dtype=np.float32).reshape(tensor_shape) np.save("softmax_result.npy", softmax_res) return softmax_res # ndarray # 自定义dice系数和iou函数 def _calculate_accuracy(infer_image, mask_image): mask_image = cv2.resize(mask_image, infer_image.shape[1:3]) mask_image = mask_image / 255.0 mask_image = (mask_image > 0.5).astype(np.int) mask_image = (np.arange(2) == mask_image[..., None]).astype(np.int) infer_image = np.squeeze(infer_image, axis=0) inter = np.dot(infer_image.flatten(), mask_image.flatten()) union = np.dot(infer_image.flatten(), infer_image.flatten()) + np.dot(mask_image.flatten(), mask_image.flatten()) single_dice = 2 * float(inter) / float(union + 1e-6) single_iou = single_dice / (2 - single_dice) return single_dice, single_iou def main(_args): dice_sum = 0.0 iou_sum = 0.0 cnt = 0 stream_mgr_api = _get_stream_manager(_args.pipeline) for image_path in _get_dataset(_args.dataset): infer_image = _do_infer_image(stream_mgr_api, os.path.join(image_path, 'image.png')) # 抠图并且reshape后的shape,1hw mask_image = _process_mask(os.path.join(image_path, 'mask.png')) # 抠图后的shape, hw dice, iou = _calculate_accuracy(infer_image, mask_image) dice_sum += dice iou_sum += iou cnt += 1 print(f"image: {image_path}, dice: {dice}, iou: {iou}") print(f"========== Cross Valid dice coeff is: {dice_sum / cnt}") print(f"========== Cross Valid IOU is: {iou_sum / cnt}") stream_mgr_api.DestroyAllStreams() # 销毁stream if __name__ == "__main__": args = _parse_arg() main(args)
run.sh文件内容如下,可根据实际开发情况进行修改。参考SDK软件包sample脚本,需要按照实际路径修改各个环境变量路径。
set -e CUR_PATH=$(cd "$(dirname "$0")" || { warn "Failed to check path/to/run.sh" ; exit ; } ; pwd) # Simple log helper functions info() { echo -e "