本章节介绍基于AscendCL接口如何开发一个基于Yolo模型的目标检测样例。
样例介绍
目标检测,即给定一张图片,识别出图片中的目标位置。
图1 目标检测应用
本例中使用的是pytorch框架的yolov5模型。可以直接使用训练好的开源模型,也可以基于开源模型的源码进行修改、重新训练,还可以基于算法、框架构建适合的模型。
模型的输入数据与输出数据格式:
- 输入数据:RGB格式图片,分辨率为 640×640,输入形状为(1,3,640,640),也即(batchsize,channel,height,width)。
- 输出数据:目标检测框的坐标值、置信度、类别。
说明
输出数组需要经过一定后处理,才能显示为正常的图片。
业务模块介绍
业务模块的操作流程如图2所示。
- 图片输入:收集待检测数据集做为输入数据。
- 输入预处理:将图片读入为数组,并缩放到模型所需大小,然后调整像素范围。
- 模型推理:经过模型推理后得到目标检测结果。
- 后处理:从输出中解析出检测框坐标,并在原始图像上画出检测框。
- 可视化或保存到文件:将图片显示在界面或保存到文件。
获取代码
- 获取代码文件。
单击获取链接或使用wget命令,下载代码文件压缩包,以root用户登录开发者套件。
wget https://ascend-devkit-tool.obs.cn-south-1.myhuaweicloud.com/models/yolo_acl_sample.zip
- 将“yolo_acl_sample.zip”压缩包上传到开发者套件,解压并进入解压后的目录。
unzip yolo_acl_sample.zip cd yolo_acl_sample
代码目录结构如下所示,按照正常开发流程,需要将框架模型文件转换成昇腾AI处理器支持推理的om格式模型文件,鉴于当前是入门内容,用户可直接获取已转换好的om模型进行推理。
|-- yolo_acl_sample |-- infer # 推理文件夹 |-- main.py # 主程序 |-- det_utils.py # 模型相关前后处理函数,函数为通用函数,和AscnedCL接口无关联 |-- coco_names.txt # coco数据集所有类别名 |-- world_cup.jpg # 测试图片 |-- yolov5s_bs1.om # om模型
代码解析
开发代码过程中,在“yolo_acl_sample/infer/main.py”文件中已包含读入数据、前处理、推理、后处理等功能,串联整个应用代码逻辑,此处仅对代码进行解析。
- 导入需要的第三方库以及调用AscendCL接口推理所需文件,定义模型相关变量,如设备ID,内存申请策略等。
# coding=utf-8 from abc import abstractmethod, ABC # 用于定义抽象类 import cv2 # 图片处理三方库,用于对图片进行前后处理 import numpy as np # 用于对多维数组进行计算 import torch # 深度学习运算框架,此处主要用来处理数据 import acl # AscnedCL推理相关接口 from det_utils import get_labels_from_txt, letterbox, scale_coords, nms, draw_bbox # 模型前后处理相关函数 DEVICE_ID = 0 # 设备id SUCCESS = 0 # 成功状态值 FAILED = 1 # 失败状态值 ACL_MEM_MALLOC_NORMAL_ONLY = 2 # 申请内存策略, 仅申请普通页 trained_model_path = '../yolov5s_bs1.om' # 模型路径 image_path = 'world_cup.jpg' # 测试图片路径
- 资源初始化。使用AscendCL接口开发应用时,必须先初始化AscnedCL,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。
# acl初始化 def init_acl(device_id): acl.init() ret = acl.rt.set_device(device_id) # 指定运算的Device if ret: # 若指定出错,则抛出异常 raise RuntimeError(ret) context, ret = acl.rt.create_context(device_id) # 显式创建一个Context if ret: # 若创建出错,则抛出异常 raise RuntimeError(ret) print('Init ACL Successfully') return context # acl 去初始化 def deinit_acl(context, device_id): ret = acl.rt.destroy_context(context) # 释放 Context if ret: # 若释放出错,则抛出异常 raise RuntimeError(ret) ret = acl.rt.reset_device(device_id) # 释放Device if ret: # 若释放出错,则抛出异常 raise RuntimeError(ret) ret = acl.finalize() # 去初始化 if ret: # 若去初始化出错,则抛出异常 raise RuntimeError(ret) print('Deinit ACL Successfully')
- 定义模型资源相关基类,承担初始化模型资源、创建输入输出数据集、执行推理、解析输出、释放模型资源等功能,之后的Yolo模型推理可继承此类。
class Model(ABC): def __init__(self, model_path): print(f"load model {model_path}") self.model_path = model_path # 模型路径 self.model_id = None # 模型 id self.input_dataset = None # 输入数据结构 self.output_dataset = None # 输出数据结构 self.model_desc = None # 模型描述信息 self._input_num = 0 # 输入数据个数 self._output_num = 0 # 输出数据个数 self._output_info = [] # 输出信息列表 self._is_released = False # 资源是否被释放 self._init_resource() def _init_resource(self): ''' 初始化模型、输出相关资源。相关数据类型: aclmdlDesc aclDataBuffer aclmdlDataset''' print("Init model resource") # 加载模型文件 self.model_id, ret = acl.mdl.load_from_file(self.model_path) # 加载模型 self.model_desc = acl.mdl.create_desc() # 初始化模型信息对象 ret = acl.mdl.get_desc(self.model_desc, self.model_id) # 根据模型获取描述信息 print("[Model] Model init resource stage success") # 创建模型输出 dataset 结构 self._gen_output_dataset() # 创建模型输出dataset结构 def _gen_output_dataset(self): ''' 组织输出数据的dataset结构 ''' ret = SUCCESS self._output_num = acl.mdl.get_num_outputs(self.model_desc) # 获取模型输出个数 self.output_dataset = acl.mdl.create_dataset() # 创建输出dataset结构 for i in range(self._output_num): temp_buffer_size = acl.mdl.get_output_size_by_index(self.model_desc, i) # 获取模型输出个数 temp_buffer, ret = acl.rt.malloc(temp_buffer_size, ACL_MEM_MALLOC_NORMAL_ONLY) # 为每个输出申请device内存 dataset_buffer = acl.create_data_buffer(temp_buffer, temp_buffer_size) # 创建输出的data buffer结构,将申请的内存填入data buffer _, ret = acl.mdl.add_dataset_buffer(self.output_dataset, dataset_buffer) # 将 data buffer 加入输出dataset if ret == FAILED: self._release_dataset(self.output_dataset) # 失败时释放dataset print("[Model] create model output dataset success") def _gen_input_dataset(self, input_list): ''' 组织输入数据的dataset结构 ''' ret = SUCCESS self._input_num = acl.mdl.get_num_inputs(self.model_desc) # 获取模型输入个数 self.input_dataset = acl.mdl.create_dataset() # 创建输入dataset结构 for i in range(self._input_num): item = input_list[i] # 获取第 i 个输入数据 data_ptr = acl.util.bytes_to_ptr(item.tobytes()) # 获取输入数据字节流 size = item.size * item.itemsize # 获取输入数据字节数 dataset_buffer = acl.create_data_buffer(data_ptr, size) # 创建输入dataset buffer结构, 填入输入数据 _, ret = acl.mdl.add_dataset_buffer(self.input_dataset, dataset_buffer) # 将dataset buffer加入dataset if ret == FAILED: self._release_dataset(self.input_dataset) # 失败时释放dataset print("[Model] create model input dataset success") def _unpack_bytes_array(self, byte_array, shape, datatype): ''' 将内存不同类型的数据解码为numpy数组 ''' np_type = None # 获取输出数据类型对应的numpy数组类型和解码标记 if datatype == 0: # ACL_FLOAT np_type = np.float32 elif datatype == 1: # ACL_FLOAT16 np_type = np.float16 elif datatype == 3: # ACL_INT32 np_type = np.int32 elif datatype == 8: # ACL_UINT32 np_type = np.uint32 else: print("unsurpport datatype ", datatype) return # 将解码后的数据组织为numpy数组,并设置shape和类型 return np.frombuffer(byte_array, dtype=np_type).reshape(shape) def _output_dataset_to_numpy(self): ''' 将模型输出解码为numpy数组 ''' dataset = [] # 遍历每个输出 for i in range(self._output_num): buffer = acl.mdl.get_dataset_buffer(self.output_dataset, i) # 从输出dataset中获取buffer data_ptr = acl.get_data_buffer_addr(buffer) # 获取输出数据内存地址 size = acl.get_data_buffer_size(buffer) # 获取输出数据字节数 narray = acl.util.ptr_to_bytes(data_ptr, size) # 将指针转为字节流数据 # 根据模型输出的shape和数据类型,将内存数据解码为numpy数组 dims = acl.mdl.get_output_dims(self.model_desc, i)[0]["dims"] # 获取每个输出的维度 datatype = acl.mdl.get_output_data_type(self.model_desc, i) # 获取每个输出的数据类型 output_nparray = self._unpack_bytes_array(narray, tuple(dims), datatype) # 解码为numpy数组 dataset.append(output_nparray) return dataset def execute(self, input_list): '''创建输入dataset对象, 推理完成后, 将输出数据转换为numpy格式''' self._gen_input_dataset(input_list) # 创建模型输入dataset结构 ret = acl.mdl.execute(self.model_id, self.input_dataset, self.output_dataset) # 调用离线模型的execute推理数据 out_numpy = self._output_dataset_to_numpy() # 将推理输出的二进制数据流解码为numpy数组, 数组的shape和类型与模型输出规格一致 return out_numpy def release(self): ''' 释放模型相关资源 ''' if self._is_released: return print("Model start release...") self._release_dataset(self.input_dataset) # 释放输入数据结构 self.input_dataset = None # 将输入数据置空 self._release_dataset(self.output_dataset) # 释放输出数据结构 self.output_dataset = None # 将输出数据置空 if self.model_id: ret = acl.mdl.unload(self.model_id) # 卸载模型 if self.model_desc: ret = acl.mdl.destroy_desc(self.model_desc) # 释放模型描述信息 self._is_released = True print("Model release source success") def _release_dataset(self, dataset): ''' 释放 aclmdlDataset 类型数据 ''' if not dataset: return num = acl.mdl.get_dataset_num_buffers(dataset) # 获取数据集包含的buffer个数 for i in range(num): data_buf = acl.mdl.get_dataset_buffer(dataset, i) # 获取buffer指针 if data_buf: ret = acl.destroy_data_buffer(data_buf) # 释放buffer ret = acl.mdl.destroy_dataset(dataset) # 销毁数据集 @abstractmethod def infer(self, inputs): # 保留接口, 子类必须重写 pass
- 定义yolo模型的具体推理功能,包含前处理、推理、后处理等功能。YoloV5继承自3中的Model,并重写其推理接口(infer函数),得到模型推理输出结果。
class YoloV5(Model): def __init__(self, model_path): super().__init__(model_path) self.neth = 640 # 缩放的目标高度, 也即模型的输入高度 self.netw = 640 # 缩放的目标宽度, 也即模型的输入宽度 self.conf_threshold = 0.1 # 置信度阈值 def infer(self, img_bgr): labels_dict = get_labels_from_txt('./coco_names.txt') # 得到类别信息,返回序号与类别对应的字典 # 数据前处理 img, scale_ratio, pad_size = letterbox(img_bgr, new_shape=[640, 640]) # 对图像进行缩放与填充 img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 # 转换为内存连续存储的数组 # 模型推理, 得到模型输出 output = self.execute([img, ])[0] # 后处理 boxout = nms(torch.tensor(output), conf_thres=0.4, iou_thres=0.5) # 利用非极大值抑制处理模型输出,conf_thres 为置信度阈值,iou_thres 为iou阈值 pred_all = boxout[0].numpy() # 转换为numpy数组 scale_coords([640, 640], pred_all[:, :4], img_bgr.shape, ratio_pad=(scale_ratio, pad_size)) # 将推理结果缩放到原始图片大小 img_dw = draw_bbox(pred_all, img_bgr, (0, 255, 0), 2, labels_dict) # 画出检测框、类别、概率 return img_dw
- 定义一个acl初始化“main”函数。
if __name__ == '__main__': context = init_acl(DEVICE_ID) # 初始化acl相关资源 det_model = YoloV5(model_path=trained_model_path) # 初始化模型 # 读入文件并推理 img = cv2.imread(image_path, cv2.IMREAD_COLOR) # 读入图片 img_res = det_model.infer(img) # 前处理、推理、后处理, 得到最终推理图片 cv2.imwrite('img_res.png', img_res) # 释放相关资源 det_model.release() # 释放 acl 模型相关资源, 包括输入数据、输出数据、模型等 deinit_acl(context, 0) # acl 去初始化
运行推理
进入“yolo_acl_sample/infer”目录,运行主程序“main.py” 。
cd infer python main.py
界面显示结果如下。
Init ACL Successfully load model yolov5s_bs1.om Init model resource [Model] Model init resource stage success [Model] create model output dataset success [Model] create model input dataset success start infer image: test.jpg Model start release... Model release source success Deinit ACL Successfully
在“yolo_acl_sample/infer”目录下,保存相应的推理结果:img_res.png。
图3 推理结果图
样例总结与扩展
以上代码包括以下几个步骤:
1. 初始化acl资源:在调用AscendCL相关资源时,必须先初始化AscendCL,否则可能会导致后续系统内部资源初始化出错。此样例中,包括指定计算设备、创建context等操作,再初始化了模型类,初始化时,进行了模型的加载以及输出数据集的创建。
2. 推理:读入图片,调用model.infer进行推理,其中包含数据的前处理、输入数据集结构的创建、推理、将推理结果转换为numpy、并进行后处理等操作,得到最终带有检测框的图片结果,最后将结果保存到图片。
3. 资源销毁:最后记得释放相关资源,包括卸载模型、销毁输入输出数据集、释放 Context、释放指定的计算设备、以及AscendCL去初始化等操作。
AscendCL接口分类总结:
分类 | 接口函数 | 描述 |
---|---|---|
AscendCL初始化相关 | acl.init() | pyACL初始化函数 |
acl.rt.set_device(device_id) | 指定当前进程或线程中用于运算的Device,同时隐式创建默认Context | |
acl.rt.create_context(device_id) | 在当前进程或线程中显式创建一个Context | |
模型描述信息相关 | acl.mdl.load_from_file(model_path) | 从文件加载离线模型数据(适配昇腾AI处理器的离线模型) |
acl.mdl.create_desc() | 创建aclmdlDesc类型的数据 | |
acl.mdl.get_desc(model_desc, model_id) | 根据模型ID获取该模型的aclmdlDesc类型数据 | |
acl.mdl.get_num_inputs(model_desc) | 根据aclmdlDesc类型的数据,获取模型的输入个数 | |
acl.mdl.get_num_outputs(model_desc) | 根据aclmdlDesc类型的数据,获取模型的输出个数 | |
acl.mdl.get_output_size_by_index(model_desc, i) | 根据aclmdlDesc类型的数据,获取指定输出的大小,单位为Byte | |
acl.mdl.get_output_dims(model_desc, i) | 根据模型描述信息获取指定的模型输出tensor的维度信息 | |
acl.mdl.get_output_data_type(model_desc, i) | 根据模型描述信息获取模型中指定输出的数据类型 | |
数据集结构相关 | acl.mdl.create_dataset() | 创建aclmdlDataset类型的数据 |
acl.create_data_buffer(data_addr, size) | 创建aclDataBuffer类型的数据,该数据类型用于描述内存地址、大小等内存信息 | |
acl.mdl.add_dataset_buffer(dataset, date_buffer) | 向aclmdlDataset中增加aclDataBuffer | |
acl.mdl.get_dataset_num_buffers(dataset) | 获取aclmdlDataset中aclDataBuffer的个数 | |
acl.mdl.get_dataset_buffer(dataset, i) | 获取aclmdlDataset中的第i个aclDataBuffer | |
acl.get_data_buffer_addr(buffer) | 获取aclDataBuffer类型中的数据的地址对象 | |
acl.get_data_buffer_size(buffer) | 获取aclDataBuffer类型中数据的内存大小,单位Byte | |
acl.util.bytes_to_ptr(bytes_data) | 将bytes对象转换成为void*数据,可以将转换好的数据传递给C函数直接使用 | |
acl.util.ptr_to_bytes(ptr, size) | 将void*数据转换为bytes对象,可以使python代码直接访问 | |
acl.rt.malloc(size, policy) | 申请Device上的内存 | |
推理相关 | acl.mdl.execute(model_id, input_dataset, output_dataset) | 执行模型推理,直到返回推理结果 |
销毁资源相关 | acl.destroy_data_buffer(data_buffer) | 销毁aclDataBuffer类型的数据 |
acl.mdl.destroy_dataset(dataset) | 销毁aclmdlDataset类型的数据 | |
acl.mdl.unload(model_id) | 系统完成模型推理后,可调用该接口卸载模型,释放资源 | |
acl.mdl.destroy_desc(model_desc) | 销毁aclmdlDesc类型的数据 | |
acl.rt.destroy_context(context) | 销毁一个Context,释放Context的资源 | |
acl.rt.reset_device(device_id) | 复位当前运算的Device,释放Device上的资源,包括默认Context、默认Stream以及默认Context下创建的所有Stream | |
acl.finalize() | pyACL去初始化函数,用于释放进程内的pyACL相关资源 |
理解各个接口含义后,用户可进行灵活运用。除此外,此样例中只示范了图片推理,若需要对视频流数据进行推理,可用三种方式输入视频流数据:USB摄像头和手机摄像头。具体使用方式可参考《摄像头拉流》,用户只需将前处理、推理及后处理代码放入摄像头推理代码的循环中即可,注意有些细节地方需进行相应修改,具体逻辑可参照图像分类应用中的样例总结与扩展。