图像分割应用样例开发介绍(Python)
样例介绍
本文以MindX SDK来开发一个简单的图像分割应用,图像分割模型推理流程如图1 分割模型推理流程图所示。
本例中使用的是开源数据集训练的Unet++图片分割模型(参见ModelZoo-PyTorch-Unet++)。可以直接使用训练好的开源模型,也可以基于开源模型的源码进行修改、重新训练,还可以基于算法、框架构建适合的模型。
模型的输入数据与输出数据格式:
- 输入数据:RGB格式图片,分辨率为96*96,输入shape为(1,3,96,96),即(batchsize,channel,height,width),也即每个batch的图片数量、图片的RGB维度、图片高度、图片宽度。
- 输出数据:分割结果灰度图,分辨率为96*96,输入shape为(1,1,96,96),即(batchsize,channel,height,width),也即每个batch的图片数量、图片的RGB维度、图片高度、图片宽度。
前期准备
- 获取代码文件。
单击获取链接或使用wget命令获取代码(使用wget时需确保开发者套件能够连接外网),下载代码文件压缩包,以root用户登录开发者套件。
wget https://ascend-repo.obs.cn-east-2.myhuaweicloud.com/Atlas%20200I%20DK%20A2/DevKit/models/sdk_cal_samples/unetplusplus_sdk_python_sample.zip
- 将“unetplusplus_sdk_python_sample.zip”压缩包上传到开发者套件,解压并进入解压后的目录。
unzip unetplusplus_sdk_python_sample.zip cd unetplusplus_sdk_python_sample
代码目录结构如下所示,按照正常开发流程,需要将框架模型文件转换成昇腾AI处理器支持推理的om格式模型文件,鉴于当前是入门内容,用户可直接获取已转换好的om模型进行推理。|-- unetplusplus_sdk_python_sample |-- main.py # 主程序 |-- img.png # 测试图片 |-- model |-- unetplusplus.om # om模型
- 准备用于推理的图片数据。
如2所示文件结构,内置测试图片为“img.png”,用户也可从开源数据集中获取其它图片。
代码解析
开发代码过程中,在“unetplusplus_sdk_python_sample/main.py”文件中已包含读入数据、前处理、推理、后处理等功能,串联整个应用代码逻辑,此处仅对代码进行解析。
- 在“main.py”文件的开头有如下代码,用于导入需要的第三方库以及MindX SDK推理所需文件。
import numpy as np # 用于对多维数组进行计算 import cv2 # 图片处理三方库,用于对图片进行前后处理 from albumentations.augmentations import transforms # 数据增强库,此处用于对像素值进行值域变换 from mindx.sdk import Tensor # mxVision 中的 Tensor 数据结构 from mindx.sdk import base # mxVision 推理接口
- 初始化资源、定义模型相关变量,如图片路径、模型路径、类别数量、指定device。
# 初始化资源和变量 base.mx_init() # 初始化 mxVision 资源 pic_path = 'img.png' # 单张图片 model_path = "model/unetplusplus.om" # 模型路径 num_class = 1 # 类别数量, 需要根据模型结构、任务类别进行改变; 此处只分割出细胞, 即为一分类 device_id = 0 # 指定运算的Device
- 对输入数据进行前处理。先使用OpenCV读入图片,得到三维数组,再进行相应的图片大小缩放、像素值缩放等处理,并将其转化为MindX SDK推理所需要的数据集格式(Tensor类)。
# 前处理 img_bgr = cv2.imread(pic_path) # 读入图片 img = cv2.resize(img_bgr, (96, 96)) # 将原图缩放到 96*96 大小 img = transforms.Normalize().apply(img) # 将像素值标准化(减去均值除以方差) img = img.astype('float32') / 255 # 将像素值缩放到 0~1 范围内 img = img.transpose(2, 0, 1) # 将形状转换为 channel first (3, 96, 96) img = np.expand_dims(img, 0) # 将形状转换为 (1, 3, 96, 96),即扩展第一维为 batchsize img = np.ascontiguousarray(img) # 将内存连续排列 img = Tensor(img) # 将numpy转为转为Tensor类
- 使用MindX SDK接口进行模型推理,得到模型输出结果。
# 模型推理 model = base.model(modelPath=model_path, deviceId=device_id) # 初始化 base.model 类 outputs = model.infer([img]) # 执行推理。输入数据类型:List[base.Tensor], 返回模型推理输出的 List[base.Tensor]
- 对模型输出进行后处理。将base.tensor类并转换为利于处理的numpy数组,再进行后处理。后处理步骤即将像素值变换到0~1范围内后,再将其画在原图上,得到最终可以用于显示的的分割结果。
# 后处理 model_out_msk = outputs[0] # 取出模型推理结果, 推理结果形状为 (1, 1, 96, 96),即(batchsize, num_class, height, width) model_out_msk.to_host() # 移动tensor到内存中 model_out_msk = np.array(model_out_msk) # 将 base.Tensor 类转为numpy array model_out_msk = sigmoid(model_out_msk[0][0]) # 利用 sigmoid 将模型输出变换到 0~1 范围内 img_to_save = plot_mask(img_bgr, model_out_msk) # 将处理后的输出画在原图上, 并返回 # 保存图片到文件 cv2.imwrite('result.png', img_to_save) print('save infer result success')
以上步骤为模型推理的整体流程,代码中的函数调用定义在代码最后。
- 主体函数定义。
其中sigmoid函数用于将矩阵的每个元素变换到0~1范围内,plot_mask用于将得到的分割结果画在原图上。
以下代码定义了这两个函数功能。def sigmoid(x): y = 1.0 / (1 + np.exp(-x)) # 对矩阵的每个元素执行 1/(1+e^(-x)) return y def plot_mask(img, msk): """ 将推理得到的 mask 覆盖到原图上 """ msk = msk + 0.5 # 将像素值范围变换到 0.5~1.5, 有利于下面转为二值图 msk = cv2.resize(msk, (img.shape[1], img.shape[0])) # 将 mask 缩放到原图大小 msk = np.array(msk, np.uint8) # 转为二值图, 只包含 0 和 1 # 从 mask 中找到轮廓线, 其中第二个参数为轮廓检测的模式, 第三个参数为轮廓的近似方法 # cv2.RETR_EXTERNAL 表示只检测外轮廓, cv2.CHAIN_APPROX_SIMPLE 表示压缩水平方向、 # 垂直方向、对角线方向的元素, 只保留该方向的终点坐标, 例如一个矩形轮廓只需要4个点来保存轮廓信息 # contours 为返回的轮廓(list) contours, _ = cv2.findContours(msk, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 在原图上画出轮廓, 其中 img 为原图, contours 为检测到的轮廓列表 # 第三个参数表示绘制 contours 中的哪条轮廓, -1 表示绘制所有轮廓 # 第四个参数表示颜色, (0, 0, 255)表示红色, 第五个参数表示轮廓线的宽度 cv2.drawContours(img, contours, -1, (0, 0, 255), 1) # 将轮廓线以内(即分割区域)覆盖上一层红色 img[..., 2] = np.where(msk == 1, 255, img[..., 2]) return img
运行推理
- 配置环境变量。
- Ubuntu OS:
. /usr/local/Ascend/mxVision/set_env.sh
- openEuler OS:
. $HOME/Ascend/mxVision/set_env.sh
- Ubuntu OS:
- 运行主程序。
python main.py
系统回显如下,则表明运行成功。
save infer result success
推理完成后,在当前文件夹下生成“result.png”文件,如图2所示。
样例总结与扩展
以上代码包括以下几个步骤:
1. 前处理:对图片进行缩放到 96*96、标准化、像素值变换、维度转换、连续排列内存、转为Tensor类型等操作。
2. 推理:利用Model或者base.model 初始化模型,并用infer进行推理。
3. 后处理:将模型输出mask进行sigmoid变换,并画到原图上。
MindX SDK接口分类总结:
分类 |
接口函数 |
描述 |
---|---|---|
推理相关 |
base.model(model_path, device_id) |
初始化模型 |
model.infer([img]) |
通过输入Tensor列表进行模型推理 |
理解各个接口含义后,用户可进行灵活运用。除此外,此样例中只示范了图片推理,若需要对视频流数据进行推理,可用两种方式输入视频流数据:USB摄像头、手机摄像头。具体使用方式可参考《摄像头拉流》,用户只需将前处理、推理及后处理代码放入摄像头推理代码的循环中即可,注意有些细节地方需进行相应修改。
其中,加入了USB摄像头读写相关代码,并在将图片前处理、推理、后处理相关代码放入了try...except...结构中。其中sigmoid与plot_mask函数与原样例定义相同。运行代码后,可在主目录保存结果视频video_result.mp4。
import numpy as np # 用于对多维数组进行计算 import cv2 # 图片处理三方库,用于对图片进行前后处理 from albumentations.augmentations import transforms # 数据增强库,此处用于对像素值进行值域变换 from mindx.sdk import Tensor # mxVision 中的 Tensor 数据结构 from mindx.sdk import base # mxVision 推理接口 # 初始化变量 base.mx_init() # 初始化 mxVision 资源 pic_path = 'img.png' # 单张图片 model_path = "model/unetplusplus.om" # 模型路径 num_class = 1 # 类别数量, 需要根据模型结构、任务类别进行改变; 此处只分割出细胞, 即为一分类 device_id = 0 # 指定运算的Device # 打开摄像头 cap = cv2.VideoCapture(0) # 打开摄像头 # 获取保存视频相关变量 fps = cap.get(cv2.CAP_PROP_FPS) fourcc = cv2.VideoWriter_fourcc(*'mp4v') outfile = 'video_result.mp4' video_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) video_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter(outfile, fourcc, fps, (video_width, video_height)) try: while(cap.isOpened()): # 在摄像头打开的情况下循环执行 ret, frame = cap.read() # 此处 frame 为 bgr 格式图片 # 前处理 img = cv2.resize(frame, (96, 96)) # 将原图缩放到 96*96 大小 img = transforms.Normalize().apply(img) # 将像素值标准化(减去均值除以方差) img = img.astype('float32') / 255 # 将像素值缩放到 0~1 范围内 img = img.transpose(2, 0, 1) # 将形状转换为 channel first (3, 96, 96) img = np.expand_dims(img, 0) # 将形状转换为 (1, 3, 96, 96),即扩展第一维为 batchsize img = np.ascontiguousarray(img) # 将内存连续排列 img = Tensor(img) # 将numpy转为转为Tensor类 # 模型推理 model = base.model(modelPath=model_path, deviceId=device_id) # 初始化 base.model 类 outputs = model.infer([img]) # 执行推理。输入数据类型:List[base.Tensor], 返回模型推理输出的 List[base.Tensor] # 后处理 model_out_msk = outputs[0] # 取出模型推理结果, 推理结果形状为 (1, 1, 96, 96),即(batchsize, num_class, height, width) model_out_msk.to_host() # 移动tensor到host内存中 model_out_msk = np.array(model_out_msk) # 将 base.Tensor 类转为numpy array model_out_msk = sigmoid(model_out_msk[0][0]) # 利用 sigmoid 将模型输出变换到 0~1 范围内 img_to_save = plot_mask(frame, model_out_msk) # 将处理后的输出画在原图上, 并返回 print('infer current frame success') # 保存图片到文件 writer.write(img_to_save) # 将推理结果写入视频 except KeyboardInterrupt: cap.release() writer.release() print('save infer result success') finally: cap.release() writer.release() print('save infer result success')