基于昇腾CANN的推理应用开发——语义分割(Python)
前情提要
基于 Python 开发,通过网络模型加载、推理、结果输出的部署全流程展示,快速熟悉并掌握语义分割基本开发流程。
目录
1 内容及目标
1.1 内容
1.2 目标
1.3 先导知识
2 理解原始模型
2.1 网络结构
2.2 查看模型推理结果
3 编写推理应用
3.1 模型转换
3.2 初始化
3.3 加载模型
3.4 读取图像数据并进行预处理
3.5 模型推理
3.6 解析模型推理结果
4 结语
1. 内容及目标
1.1 内容
这里主要介绍基于昇腾 CANN 平台的语义分割的开发方法,是基于 DeepLab_v3+ 语义分割网络编写的示例代码,通过读取本地图像数据作为输入,对图像中的物体进行语义分割,最后使用 OpenCV 将推理结果写到本地文件中。旨在了解如何在昇腾平台上实现一个基于语义分割模型的推理应用。
1.2 目标
1.掌握一个基于昇腾CANN平台的推理应用的基本结构。
2.理解 deeplabv3_plus 模型的网络结构及其数据预处理/后处理方式。
1.3 先导知识
1.熟悉和掌握Python语言,具备一定的开发能力
2.深度学习基础知识,理解神经网络模型输入输出数据结构
2. 理解原始模型
2.1 网络结构
DeepLabv3+模型的整体架构如图所示,它的Encoder的主体是带有空洞卷积的DCNN,可以采用常用的分类网络如ResNet,然后是带有空洞卷积的空间金字塔池化模块(Atrous Spatial Pyramid Pooling, ASPP)),主要是为了引入多尺度信息;相比DeepLabv3,v3+引入了Decoder模块,其将底层特征与高层特征进一步融合,提升分割边界准确度。从某种意义上看,DeepLabv3+在DilatedFCN基础上引入了EcoderDecoder的思路。
对原始模型代码感兴趣的小伙伴可以参考链接。
2.2 查看模型推理的效果
通过下一张图可以很快了解到该模型是对图片进行与语义分割。
3. 编写推理应用
- 运行管理资源申请:用于初始化系统内部资源,固定的调用流程。
- 加载模型文件并构建输出内存:
- 从文件加载离线模型,模型加载进系统之后占用的内存需要由用户自行管理;
- 获取模型的基本信息,包含模型输入/输出的个数,每个输入/输出占用内存的大小;
- 上一步获取的信息申请模型输出内存,为接下来的模型推理做好准备。
- 获取本地图像并进行预处理:使用OpenCV从本地存放有图像数据的目录中循环读取图像数据,对读入的图像数据进行预处理,然后构建模型的输入数据。
- 模型推理:根据加载的模型、构建好的模型输入数据、申请好的模型输出内存进行模型推理。
- 解析推理结果:将推理结果作用于原始图片上,形成语义分割图像;随后使用OpenCV将转换后的结果图像数据保存为图片文件。
执行下方的代码,进行推理代码开发前的系统初始化。
3.1 模型转换
原始网络模型是Caffe预训练模型,而昇腾CANN软件栈推理所需要的的模型是.om离线模型。因此,需要将Caffe预训练模型通过ATC模型转换工具做一下转换。
ATC模型转换命令举例:
atc --model=./deeplabv3_plus.pb --framework=3 --output=./model/deeplabv3_plus --soc_version=Ascend310 -- --input_shape="data:1,513,513"
最终已转好的om模型,模型路径如下:
./model/deeplabv3_plus.om
3.2 初始化
在调用任何AscendCL接口进行操作之前,首先要对AscendCL的运行时环境进行初始化操作,以及申请运行时资源。
初始化及资源申请操作主要有以下几个步骤:
- 初始化系统内部资源,调用acl.init接口实现ACL初始化
- 调用acl.rt.set_device接口指定运算的Device
- 调用acl.rt.create_context接口显式创建一个Context
- 调用acl.rt.create_stream接口显式创建一个Stream
当所有数据处理都结束后,需要释放运行管理资源,释放资源时,需要按顺序释放,先释放Stream,再释放Context,最后再释放Device。释放运行管理资源时主要有以下几个步骤:
需按顺序依次释放:Stream、Context、Device。
- 调用acl.finalize接口实现ACL去初始化。
- 调用acl.rt.destroy_stream接口释放Stream
- 调用acl.rt.destroy_context接口释放Context
- 调用 acl.rt.reset_device接口释放Device上的资源。
我们针对上述操作封装了一套工具库,其中,初始化和去初始化,资源申请和资源释放部分,由一个“AclResource”类来实现
对初始化和去初始化的源码感兴趣的小伙伴可以从此链接查看:
运行下方代码,查看效果,并阅读“init()”方法,观察初始化和运行时资源申请的详细操作步骤。
import sys
import os
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
home_path = !echo ${HOME}
sys.path.append(os.path.join(home_path[0] , "jupyter-notebook/"))
print('System init success.')
# atlas_utils是昇腾团队基于pyACL封装好的一套工具库,如果您也想引用的话,请首先将
# https://gitee.com/ascend/samples/tree/master/python/common/atlas_utils
# 这个路径下的代码引入您的工程中
from atlas_utils.acl_resource import AclResource
from constants import *
from acl_model import Model
# 创建一个AclResource类的实例
acl_resource = AclResource()
#AscendCL资源初始化(封装版本)
acl_resource.init()
# 上方“init”方法具体实现(仅供参考),请阅读“init()”方法,观察初始化和运行时资源申请的详细操作步骤
def init(self):
"""
Init resource
"""
print("init resource stage:")
ret = acl.init()
utils.check_ret("acl.init", ret)
#指定用于运算的Device
ret = acl.rt.set_device(self.device_id)
utils.check_ret("acl.rt.set_device", ret)
print("Set device n success.")
#显式创建一个Context
self.context, ret = acl.rt.create_context(self.device_id)
utils.check_ret("acl.rt.create_context", ret)
#创建一个Stream
self.stream, ret = acl.rt.create_stream()
utils.check_ret("acl.rt.create_stream", ret)
#获取当前昇腾AI软件栈的运行模式
#0:ACL_DEVICE,表示运行在Device的Control CPU上或开发者版上
#1:ACL_HOST,表示运行在Host CPU上
self.run_mode, ret = acl.rt.get_run_mode()
utils.check_ret("acl.rt.get_run_mode", ret)
print("Init resource success")
# 请阅读下方代码,观察释放运行时资源的详细操作步骤
def __del__(self):
print("acl resource release all resource")
resource_list.destroy()
# 调用acl.rt.destroy_stream接口释放Stream
if self.stream:
acl.rt.destroy_stream(self.stream)
print("acl resource release stream")
# 调用acl.rt.destroy_context接口释放Context
if self.context:
acl.rt.destroy_context(self.context)
print("acl resource release context")
# 调用acl.rt.destroy_context接口释放Context
acl.rt.reset_device(self.device_id)
acl.finalize()
print("Release acl resource success")
3.3 加载模型
在调用模型进行推理之前,首先要将模型加载进来。对模型的操作,这里封装了一个类“Model”,源码路径在Gitee官仓。
加载模型的过程主要分以下几个步骤:
- 从磁盘读取om模型
- 分析该om模型,提取其描述信息,主要是模型有几个输入/输出,每个输入/输出占用多大内存
- 模型的输出尺寸通常是固定的,可以先为输出申请好内存
运行下方代码,观察效果。感兴趣的读者可以阅读一下Model类的源码。
# 从文件加载离线模型数据
model_path = './model/deeplabv3_plus.om'
model = Model(model_path)
3.4 读取图像数据并进行预处理
本应用的图像预处理部分主要做了以下几件事情::
- 使用opencv的imread接口读取图片,读取出来的是BGR格式;
- 得到原始图片的shape
- 模型输入为513×513,因此需要把读取到的图像resize到513×513
下面我们首先看一下原始图片的样貌:
import sys
import os
import numpy as np
import cv2 as cv
from PIL import Image
MODEL_WIDTH = 513
MODEL_HEIGHT = 513
#图片路径
image_dir = './deeplabv3_pascal_data/'
IMG_EXT = ['.jpg', '.JPG', '.png', '.PNG', '.bmp', '.BMP', '.jpeg', '.JPEG']
images_list = [os.path.join(image_dir,img)for img in os.listdir(image_dir)if os.path.splitext(img)[1] in IMG_EXT]
images_list
Image.open(images_list[0])
type(images_list[0])
接下来我们要对原始数据做上述3步预处理动作,具体操作说明参考注释:
# 1. 读取图片
bgr_img = cv.imread(images_list[0])
# 2. 得到图片shape
orig_shape = bgr_img.shape[:2]
print("Shape of original image:",bgr_img.shape)
# 3.对图片进行resize,缩放至模型要求的宽高:
img = cv.resize(bgr_img, (MODEL_WIDTH, MODEL_HEIGHT))
print("The size of the model to be entered:",img.shape)
# 保存并展示resize之后的图片
if not os.path.isdir('./outputs'):
os.mkdir('./outputs')
output_path = os.path.join("./outputs", "out_" + os.path.basename(images_list[0]))
cv.imwrite(output_path, img)
Image.open(output_path)
img = img.astype(np.int8)
if not img.flags['C_CONTIGUOUS']:
img = np.ascontiguousarray(img)
img.shape
img.nbytes
3.5 模型推理
准备好预处理后的数据以及模型之后,我们来到了核心的模型推理环节。
这一环节有一些细小的操作需要完成,主要有以下几步:
- 在Device侧申请内存,并把数据从Host侧发送到Device侧,这样昇腾 310 AI处理器才能获取到数据进行计算
- 为模型推理准备专用数据结构。原始图片数据是不能直接送进模型进行推理的,需要用两个特定的数据结构包装一下。
- 模型可能存在多个输入,例如第1个输入是若干张图片,第二个输入是每个图片的信息;此时,每个原始输入的数据要为其建立一个“dataBuffer”对象
- 所有的dataBuffer构成1个“dataSet”对象送进模型进行推理。
- 输出同理。1个模型只会有1个输入dataset,1个输出dataset,但是每个dataset可能包含多个databuffer
- 执行模型推理
上述这几步,在官方封装好的工具类中都做了通用实现,所以在使用过程中,只需要调用一下model.execute方法,将数据传进去就可以执行推理了:
# 根据构建好的模型输入数据进行模型推理
result_list = model.execute([img])
print(result_list)
3.6 解析模型推理结果
拿到模型推理结果之后,我们来到了最后一步——后处理。
解析模型推理结果的步骤:
- 获取模型推理输出数据并且将其 reshape 成 513 * 513;
- 对reshape之后的图片进行通道合并,重新合并成一个多通道的图像
- 将多通道图像resize到原始图片的大小;
- 保存为jpeg图片,得到分割后的图像。
下边是具体的操作步骤,执行一下观察结果:
# 1. 得到推理结果并且reshape成513*513,并且转换成uint8类型
result_img = result_list[0].reshape(513, 513)
result_img = result_img.astype('uint8')
result_img.shape
cv.imwrite(output_path, result_img)
Image.open(output_path)
# 2. 多通道图像融合
img = cv.merge((result_img, result_img, result_img))
print("Multi channel merged shape:",img.shape)
# 3. 将融合后的图像resize成原始图片的大小
bgr_img = cv.resize(img, (orig_shape[1], orig_shape[0]))
print("Shape of the original image size that is resized:",bgr_img.shape)
bgr_img = (bgr_img * 255)
# 保存图片
cv.imwrite(output_path, bgr_img)
Image.open(output_path)
结语
至此,语义分割应用代码编写完毕。让我们回顾一下在使用atlas_util库的场景下,我们都做了哪些主要的事情:
- 将Caffe训练出来的deeplabv3_plus模型转换为.om离线模型
- 初始化AscendCL运行时环境,申请运行资源
- 加载om模型
- 对原始图片做预处理
- 调用模型的推理接口进行推理
- 将推理结果作用于原始数据上进行语义分割,保存处理后的图片。
这里我们在atlas_util中屏蔽了很多对AscendCL接口的调用细节,感兴趣的朋友可以阅读一下atlas_util的源码。
如果您对上述代码有疑问请提交ISSUE到官仓。
更多昇腾有趣内容请移步至昇腾社区。
- 点赞
- 收藏
- 关注作者
评论(0)