华为Atlas设备图像裁切
使用硬件加速的DVPP模块实现图像裁切功能。
在华为Ascend设备与传统X86设备上的图像应用开发存在明显模式差异。主要体现在:
- 设备初始化与去初始化
- 内存申请与释放
- 图像格式
- 各种操作限制
以下内容以图像裁切为例,展示了在Ascend设备上完整的图像处理过程。包括四个部分:
- 设备与环境初始化
- 读取图像文件
- 裁切图像
- 收尾,释放资源
希望通过这个最小代码例子,你能建立在Ascend设备上图像开发的直观感受。
让我们开始吧!
首先导入必要的开发包。
import acl
import cv2
import numpy as np
import atlas.common.atlas_utils.constants as constants
import atlas.common.atlas_utils.utils as utils
将重复代码抽象为一个辅助函数,方便复用。
def setup_desc(desc, width, height, w_align=16, h_align=2):
"""Setup the image description object.
The image should be in YUV420SP format.
Args:
desc: the image description.
width: the width of the image.
height: the height of the image.
w_align: the width align factor.
h_align: the height align factor.
Returns:
ptr_on_device: the pointer of the image on the device.
"""
# Calculate buffer size.
width_stride = ((width + w_align - 1) // w_align) * w_align
height_stride = ((height + h_align - 1) // h_align) * h_align
buf_size = (width_stride * height_stride * 3) // 2
# Allocate memory
ptr_on_device, ret = acl.media.dvpp_malloc(buf_size)
utils.check_ret("acl.media.dvpp_malloc", ret)
# Setup the image.
acl.media.dvpp_set_pic_desc_data(desc, ptr_on_device)
acl.media.dvpp_set_pic_desc_size(desc, buf_size)
acl.media.dvpp_set_pic_desc_format(
desc, constants.PIXEL_FORMAT_YUV_SEMIPLANAR_420)
acl.media.dvpp_set_pic_desc_width(desc, width)
acl.media.dvpp_set_pic_desc_height(desc, height)
acl.media.dvpp_set_pic_desc_width_stride(desc, width_stride)
acl.media.dvpp_set_pic_desc_height_stride(desc, height_stride)
return ptr_on_device
第一步:初始化
Ascend设备上运行至少需要初始化4个对象:ACL、Device、Context和Stream。这个过程对于Python开发者可能比较陌生。好在这个过程是一次性的。
ACL初始化。ACL是操作Ascend设备的Python库,有点像OpenCV。
ret = acl.init()
assert ret == 0, "初始化ACL失败。"
Device初始化。一张Atlas加速卡上可能有多个Ascend芯片可以使用。在这里指定你要选用的芯片序号。
device_id = 0
ret = acl.rt.set_device(device_id)
assert ret == 0, "初始化Device失败。"
Context初始化。Context可以看做是程序运行的小环境,它起着隔离作用。
context, ret = acl.rt.create_context(device_id)
assert ret == 0, "初始化Context失败。"
Stream初始化。Stream多用作程序执行的同步。
stream, ret = acl.rt.create_stream()
assert ret == 0, "初始化Stream失败。"
成功执行到这一步,初始化完成。
第二步:图像读入
ACL中专门为图像相关操作提供了硬件加速模块。这些加速有一个前提条件:图像需要为YUV格式。另外请注意,OpenCV暂时无法将BGR图像转换为ACL支持的YUV420SP格式。所以我们将直接读入一张YUV图像。
img_file = "wood_rabbit_1024_1068_nv12.yuv"
org_yuv = np.fromfile(img_file, dtype=np.byte)
为了验证读取结果,将读入的YUV图像转换为你熟悉的OpenCV图像:NumpyArray。并在窗口中查看图像内容。
org_yuv = np.reshape(
org_yuv, (int((1072)*3/2), int(1024))).astype(np.uint8)
org_bgr = cv2.cvtColor(org_yuv, cv2.COLOR_YUV2BGR_NV12)
第三步:图像裁切
如果你熟悉OpenCV的话,图像裁切可以通过数组slice的方式一行代码搞定。但是在ACL中,图像并不是以NumpyArray的形式存储。所以你无法通过slice的方式裁切图像,而只能依赖ACL提供的API。
完成一次裁切分为以下几步:
1. 准备待裁切图像
虽然你已经将YUV图像读入内存,但是硬件加速的裁切操作需要在Ascend芯片上进行,这意味着你需要把图像数据从主机(host)一侧拷贝到Ascend芯片(Device)一侧。另外,Device一侧的数据并非Numpy数组,在这里生存的图像有自己独特的“描述方式”。
构建Device一侧的图像描述。
input_desc = acl.media.dvpp_create_pic_desc()
assert input_desc is not None
图像描述对象构建之后,使用真实要处理的图像参数填充该描述,计算内存占用大小,并将具体的像数值拷贝到Device一侧。
h, w, _ = org_bgr.shape
intput_ptr = setup_desc(input_desc, w, h)
in_buffer_size = org_yuv.itemsize * org_yuv.size
np_yuv_ptr = acl.util.numpy_to_ptr(org_yuv)
ret = acl.rt.memcpy(intput_ptr, in_buffer_size,
np_yuv_ptr, in_buffer_size,
constants.ACL_MEMCPY_HOST_TO_DEVICE)
assert ret == 0, "图像内存拷贝失败。"
至此,待裁切图像准备完成。
2. 设定裁切区域。
由于输入图像不是Numpy数组,你只能使用ACL提供的API来设定裁切区域。另外,Ascend设备上的开发有诸多限制。例如裁切区域[x0, x1, y0, y1]必须满足[偶数、奇数、偶数、奇数]的约束。
crop_w = 320
crop_h = 420
x = 480
y = 360
crop_area = acl.media.dvpp_create_roi_config(
x, x + crop_w-1, y, y + crop_h-1)
3. 构建输出图像。
与输入图像类似,输出图像默认生存在Device一侧,你需要提前为它准备好内存空间。这个过程与输入图像的构建过程非常相似。
output_desc = acl.media.dvpp_create_pic_desc()
assert output_desc is not None
设置输出图片描述。输出图像的宽度与高度为截取后的大小。
setup_desc(output_desc, crop_w, crop_h)
4. 开始裁切
在正式执行裁切指令前,还需要为裁切操作创建“数据处理通道”,这也是ACL开发的不同之处。
dvpp_channel_desc = acl.media.dvpp_create_channel_desc()
ret = acl.media.dvpp_create_channel(dvpp_channel_desc)
一切就绪,执行裁切指令。
ret = acl.media.dvpp_vpc_crop_async(dvpp_channel_desc, input_desc,output_desc, crop_area, stream)
裁切指令为异步执行,所以需要等待它执行完毕才能获得裁切结果。
ret = acl.rt.synchronize_stream(stream)
至此裁切结束。
5. 获取结果。
别忘了裁切后的图像生存在Device一侧,并且是YUV420SP格式。我们需要将它拷贝到主机(host)一侧,然后转换成熟悉的Numpy数组。
获取裁切后的图像信息。
data = acl.media.dvpp_get_pic_desc_data(output_desc)
data_size = acl.media.dvpp_get_pic_desc_size(output_desc)
ret_code = acl.media.dvpp_get_pic_desc_ret_code(output_desc)
utils.check_ret("acl.media.dvpp_get_pic_desc_ret_code", ret_code)
拷贝数据到主机一侧并转换为Numpy数组。
np_pic = np.zeros(data_size, dtype=np.byte)
np_pic_ptr = acl.util.numpy_to_ptr(np_pic)
ret = acl.rt.memcpy(np_pic_ptr, data_size,
data, data_size,
constants.ACL_MEMCPY_DEVICE_TO_HOST)
utils.check_ret("acl.rt.memcpy", ret)
6. 查阅结果。
最后,在窗口中查阅裁切前后的对比。这一步需要将YUV420SP转换为BGR格式。
result_yuv = np.reshape(
np_pic, (int((crop_h)*3/2), int(crop_w))).astype(np.uint8)
result_bgr = cv2.cvtColor(result_yuv, cv2.COLOR_YUV2BGR_NV12)
cv2.imshow('Original', org_bgr)
cv2.imshow('Cropped', result_bgr)
cv2.waitKey()
第四步:收尾、释放资源
裁切结束后,释放资源,包括输入/输出图片的描述信息、输入/输出内存、通道描述信息、通道等。
释放图片描述。
acl.media.dvpp_destroy_pic_desc(input_desc)
acl.media.dvpp_destroy_pic_desc(output_desc)
释放Device侧内存。
ret = acl.media.dvpp_free(intput_ptr)
释放操作通道。
if dvpp_channel_desc:
ret = acl.media.dvpp_destroy_channel(dvpp_channel_desc)
assert ret == 0
ret = acl.media.dvpp_destroy_channel_desc(dvpp_channel_desc)
assert ret == 0
释放运行资源。
ret = acl.rt.destroy_stream(stream)
ret = acl.rt.destroy_context(context)
ret = acl.rt.reset_device(device_id)
ret = acl.finalize()
print('结束。')
至此DVPP加速的图像裁切完成。
总结
与Numpy下的图像裁切相比,基于DVPP的图像存在诸多限制、代码量也很可观。这直接导致开发工作量变大。但是使用DVPP裁切图像可以避免将图像传输到HOST一侧造成的性能损耗。在实际应用中,付出这些代价是值得的。
你可以在这里找到完整的代码:
Comments ()