基于昇腾CANN的推理应用开发——语义分割(Python)

Tianyi_Li 发表于 2022/06/05 23:13:02 2022/06/05
【摘要】 基于 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的思路。

image.png

对原始模型代码感兴趣的小伙伴可以参考链接

2.2 查看模型推理的效果

通过下一张图可以很快了解到该模型是对图片进行与语义分割。

image.png

3. 编写推理应用

image.png

  1. 运行管理资源申请:用于初始化系统内部资源,固定的调用流程。
  2. 加载模型文件并构建输出内存:
  • 从文件加载离线模型,模型加载进系统之后占用的内存需要由用户自行管理;
  • 获取模型的基本信息,包含模型输入/输出的个数,每个输入/输出占用内存的大小;
  • 上一步获取的信息申请模型输出内存,为接下来的模型推理做好准备。
  1. 获取本地图像并进行预处理:使用OpenCV从本地存放有图像数据的目录中循环读取图像数据,对读入的图像数据进行预处理,然后构建模型的输入数据。
  2. 模型推理:根据加载的模型、构建好的模型输入数据、申请好的模型输出内存进行模型推理。
  3. 解析推理结果:将推理结果作用于原始图片上,形成语义分割图像;随后使用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的运行时环境进行初始化操作,以及申请运行时资源。
初始化及资源申请操作主要有以下几个步骤:

  1. 初始化系统内部资源,调用acl.init接口实现ACL初始化
  2. 调用acl.rt.set_device接口指定运算的Device
  3. 调用acl.rt.create_context接口显式创建一个Context
  4. 调用acl.rt.create_stream接口显式创建一个Stream

当所有数据处理都结束后,需要释放运行管理资源,释放资源时,需要按顺序释放,先释放Stream,再释放Context,最后再释放Device。释放运行管理资源时主要有以下几个步骤:

需按顺序依次释放:Stream、Context、Device。

  1. 调用acl.finalize接口实现ACL去初始化。
  2. 调用acl.rt.destroy_stream接口释放Stream
  3. 调用acl.rt.destroy_context接口释放Context
  4. 调用 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软件栈的运行模式
    #0ACL_DEVICE,表示运行在Device的Control CPU上或开发者版上
    #1ACL_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官仓

加载模型的过程主要分以下几个步骤:

  1. 从磁盘读取om模型
  2. 分析该om模型,提取其描述信息,主要是模型有几个输入/输出,每个输入/输出占用多大内存
  3. 模型的输出尺寸通常是固定的,可以先为输出申请好内存

运行下方代码,观察效果。感兴趣的读者可以阅读一下Model类的源码。

# 从文件加载离线模型数据
model_path = './model/deeplabv3_plus.om'
model = Model(model_path)

3.4 读取图像数据并进行预处理

本应用的图像预处理部分主要做了以下几件事情::

  1. 使用opencv的imread接口读取图片,读取出来的是BGR格式;
  2. 得到原始图片的shape
  3. 模型输入为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 模型推理

准备好预处理后的数据以及模型之后,我们来到了核心的模型推理环节。
这一环节有一些细小的操作需要完成,主要有以下几步:

  1. 在Device侧申请内存,并把数据从Host侧发送到Device侧,这样昇腾 310 AI处理器才能获取到数据进行计算
  2. 为模型推理准备专用数据结构。原始图片数据是不能直接送进模型进行推理的,需要用两个特定的数据结构包装一下。
  • 模型可能存在多个输入,例如第1个输入是若干张图片,第二个输入是每个图片的信息;此时,每个原始输入的数据要为其建立一个“dataBuffer”对象
  • 所有的dataBuffer构成1个“dataSet”对象送进模型进行推理。
  • 输出同理。1个模型只会有1个输入dataset,1个输出dataset,但是每个dataset可能包含多个databuffer
  1. 执行模型推理

上述这几步,在官方封装好的工具类中都做了通用实现,所以在使用过程中,只需要调用一下model.execute方法,将数据传进去就可以执行推理了:

# 根据构建好的模型输入数据进行模型推理
result_list = model.execute([img])
print(result_list)

3.6 解析模型推理结果

拿到模型推理结果之后,我们来到了最后一步——后处理。

解析模型推理结果的步骤:

  1. 获取模型推理输出数据并且将其 reshape 成 513 * 513;
  2. 对reshape之后的图片进行通道合并,重新合并成一个多通道的图像
  3. 将多通道图像resize到原始图片的大小;
  4. 保存为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库的场景下,我们都做了哪些主要的事情:

  1. 将Caffe训练出来的deeplabv3_plus模型转换为.om离线模型
  2. 初始化AscendCL运行时环境,申请运行资源
  3. 加载om模型
  4. 对原始图片做预处理
  5. 调用模型的推理接口进行推理
  6. 将推理结果作用于原始数据上进行语义分割,保存处理后的图片。

这里我们在atlas_util中屏蔽了很多对AscendCL接口的调用细节,感兴趣的朋友可以阅读一下atlas_util的源码

如果您对上述代码有疑问请提交ISSUE到官仓

更多昇腾有趣内容请移步至昇腾社区

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。