1、前言
在上个博客中对NEON有了基础的了解,本文将针对一个图像下采样的例子对NEON进行学习。
学习链接:CPU优化技术 - NEON 开发进阶
上文链接:https://blog.csdn.net/weixin_42108183/article/details/136412104
2、第一个例子
现在有一张图片,需要对UV通道的数据进行下采样,对于同种类型的数据,相邻的4个元素求和并求均值。示意图如下图所示:
假定图像数据的宽为16的整数倍,如果使用c++代码,可以写出下面的代码:
void DownscaleUv(uint8_t *src, uint8_t *dst, int32_t src_stride, int32_t dst_width, int32_t dst_height, int32_t dst_stride)
{
//遍历每一行的数据
for (int32_t j = 0; j < dst_height; j++)
{
// 偶数行起始位置,
uint8_t *src_ptr0 = src + src_stride * j * 2;
// 奇数行起始位置
uint8_t *src_ptr1 = src_ptr0 + src_stride;
// 存储起始位置
uint8_t *dst_ptr = dst + dst_stride * j;
// 没一次循环计算没
for (int32_t i = 0; i < dst_width; i += 2)
{
// U通道 (u1 + u2 + u3 + u4) / 4
dst_ptr[i] = (src_ptr0[i * 2] + src_ptr0[i * 2 + 2] +
src_ptr1[i * 2] + src_ptr1[i * 2 + 2]) / 4;
// V通道 (v1 + v2 + v3 + v4) / 4
dst_ptr[i + 1] = (src_ptr0[i * 2 + 1] + src_ptr0[i * 2 + 3] +
src_ptr1[i * 2 + 1] + src_ptr1[i * 2 + 3]) / 4;
}
}
}
通过学习向量化编程,我们可知,数据的计算可以利用单指令多数据的方式进行加速,例如上面的例子中的内层循环,下面就使用NEON来试试吧。
3、第2个例子
为了进行向量化加速,首先需要将UV数据分离,将UV数据分离的操作在NEON中很容易进行, 使用vld2交织加载或者储存即可。对于每一行的数据,交织加载的示意图如下。
交织加载的基本原理是按照间隔挑选数据。交织加载的例子如下所示:
void DownscaleUvNeon()
{
vector<uint8_t> data; // UVUVUVUVUV...
for(int i=0;i<32;i++){
data.push_back(i);
}
//
uint8_t *src_ptr0 = (uint8_t *)data.data();
// load 第一行的数据
uint8x16x2_t src;
src = vld2q_u8(src_ptr0); // 交织读取 16 * 2 的数据,需要两个q寄存器。
auto a = src_odd.val[0]; // 一行的U数据
vector<uint8_t> show_data(16);
vst1q_u8 (show_data.data(),a); // 将U数据顺序储存到内存中
// 打印
for(auto n : show_data){
cout << static_cast<int>(n) << endl; // 0,2,4,6,...
}
}
4、第3个例子
对于下UV数据采样来说,在偶数行进行上面的交织加载,再在奇数行上进行同样的操作。奇数行和偶数行相应的数据进行相加再求平均,即可得到最后的结果。代码实现如下:
#include <arm_neon.h>
void DownscaleUvNeon(uint8_t *src, uint8_t *dst, int32_t src_width, int32_t src_stride, int32_t dst_width, int32_t dst_height, int32_t dst_stride)
{
//用于加载偶数行的源数据,2组每组16个u8类型数据,(16 * 8) * 2 = 128 * 128, 因此需要两个q寄存器。
uint8x16x2_t v8_src0;
//用于加载奇数行的源数据
uint8x16x2_t v8_src1;
//目的数据变量,需要一个Q寄存器
uint8x8x2_t v8_dst;
//目前只处理16整数倍部分的结果
int32_t dst_width_align = dst_width & (-16); // dst_width & (-16),最大能够整除16的数。
//向量化剩余的部分需要单独处理
int32_t remain = dst_width & 15;
int32_t i = 0;
//外层高度循环,逐行处理
for (int32_t j = 0; j < dst_height; j++)
{
//偶数行源数据地址
uint8_t *src_ptr0 = src + src_stride * j * 2;
//奇数行源数据地址
uint8_t *src_ptr1 = src_ptr0 + src_stride;
//目的数据指针
uint8_t *dst_ptr = dst + dst_stride * j;
//内层循环,一次16个u8结果输出
for (i = 0; i < dst_width_align; i += 16)
{
//提取数据,进行UV分离
v8_src0 = vld2q_u8(src_ptr0);
src_ptr0 += 32; // 偶数行进入下一个stride
v8_src1 = vld2q_u8(src_ptr1);
src_ptr1 += 32; // 奇数行行进入下一个stride
//水平两个数据相加
uint16x8_t v16_u_sum0 = vpaddlq_u8(v8_src0.val[0]);
uint16x8_t v16_v_sum0 = vpaddlq_u8(v8_src0.val[1]);
uint16x8_t v16_u_sum1 = vpaddlq_u8(v8_src1.val[0]);
uint16x8_t v16_v_sum1 = vpaddlq_u8(v8_src1.val[1]);
//上下两个数据相加,之后求均值
v8_dst.val[0] = vshrn_n_u16(vaddq_u16(v16_u_sum0, v16_u_sum1), 2);
v8_dst.val[1] = vshrn_n_u16(vaddq_u16(v16_v_sum0, v16_v_sum1), 2);
//UV通道结果交织存储
vst2_u8(dst_ptr, v8_dst);
dst_ptr += 16;
}
//process leftovers......
}
}
5、第4个例子
当图像的宽度不是16的整数倍,需要考虑结尾数据处理,按照链接里面的例子,可以分为以下几种。
1、 padding
也就是将数据补齐到想要的长度,如下图所示,比如我这里需要操作 uint8x8_t的数据,但是我的数据长度只有5,可以将数据的长度填充至8。
2、Overlap
也就是重复利用其中的某些数据,在不填充其他数据的情况下进行,如下图所示,当需要利用uint8x4_t来对下面的数据进行计算时,可以先将04加载到寄存器上,再将36加载到寄存器上操作。
常用第二种方法对结尾数据进行处理,那么图像下采样的数据代码可以写成:
#include <arm_neon.h>
void DownscaleUvNeon(uint8_t *src, uint8_t *dst, int32_t src_width, int32_t src_stride, int32_t dst_width, int32_t dst_height, int32_t dst_stride)
{
uint8x16x2_t v8_src0;
uint8x16x2_t v8_src1;
uint8x8x2_t v8_dst;
int32_t dst_width_align = dst_width & (-16); // 最大能够整除16的数。
int32_t remain = dst_width & 15; // 需要剩余处理的数据长度
int32_t i = 0;
for (int32_t j = 0; j < dst_height; j++)
{
uint8_t *src_ptr0 = src + src_stride * j * 2;
uint8_t *src_ptr1 = src_ptr0 + src_stride;
uint8_t *dst_ptr = dst + dst_stride * j;
// 处理完宽度为16的整数倍数据了
for (i = 0; i < dst_width_align; i += 16)
{
v8_src0 = vld2q_u8(src_ptr0);
src_ptr0 += 32;
v8_src1 = vld2q_u8(src_ptr1);
src_ptr1 += 32;
uint16x8_t v16_u_sum0 = vpaddlq_u8(v8_src0.val[0]);
uint16x8_t v16_v_sum0 = vpaddlq_u8(v8_src0.val[1]);
uint16x8_t v16_u_sum1 = vpaddlq_u8(v8_src1.val[0]);
uint16x8_t v16_v_sum1 = vpaddlq_u8(v8_src1.val[1]);
v8_dst.val[0] = vshrn_n_u16(vaddq_u16(v16_u_sum0, v16_u_sum1), 2);
v8_dst.val[1] = vshrn_n_u16(vaddq_u16(v16_v_sum0, v16_v_sum1), 2);
vst2_u8(dst_ptr, v8_dst);
dst_ptr += 16;
}
// process leftover
// remain 剩余需要处理的数据长度
if (remain > 0)
{
// 从后往前回退一次向量计算需要的数据长度
// 有部分数据是之前处理过的,这部分的数据在这里重复计算一次
src_ptr0 = src + src_stride * (j * 2) + src_width - 32;
src_ptr1 = src_ptr0 + src_stride;
dst_ptr = dst + dst_stride * j + dst_width - 16;
v8_src0 = vld2q_u8(src_ptr0);
v8_src1 = vld2q_u8(src_ptr1);
uint16x8_t v16_u_sum0 = vpaddlq_u8(v8_src0.val[0]);
uint16x8_t v16_v_sum0 = vpaddlq_u8(v8_src0.val[1]);
uint16x8_t v16_u_sum1 = vpaddlq_u8(v8_src1.val[0]);
uint16x8_t v16_v_sum1 = vpaddlq_u8(v8_src1.val[1]);
v8_dst.val[0] = vshrn_n_u16(vaddq_u16(v16_u_sum0, v16_u_sum1), 2);
v8_dst.val[1] = vshrn_n_u16(vaddq_u16(v16_v_sum0, v16_v_sum1), 2);
vst2_u8(dst_ptr, v8_dst);
}
}
}
3、 single
将剩余的元素单独处理,就是将剩余的元素利用NEON的只加载一个元素的功能,不推荐使用,因为这里又可能for循环多次。
4、将剩余的元素当作标量处理
也就是将剩下的元素直接使用c语言编程的方式进行计算。
void DownscaleUvNeonScalar(uint8_t *src, uint8_t *dst, int32_t src_width, int32_t src_stride, int32_t dst_width, int32_t dst_height, int32_t dst_stride)
{
uint8x16x2_t v8_src0;
uint8x16x2_t v8_src1;
uint8x8x2_t v8_dst;
int32_t dst_width_align = dst_width & (-16);
int32_t remain = dst_width & 15;
int32_t i = 0;
for (int32_t j = 0; j < dst_height; j++)
{
uint8_t *src_ptr0 = src + src_stride * j * 2;
uint8_t *src_ptr1 = src_ptr0 + src_stride;
uint8_t *dst_ptr = dst + dst_stride * j;
for (i = 0; i < dst_width_align; i += 16) // 16 items output at one time
{
v8_src0 = vld2q_u8(src_ptr0);
src_ptr0 += 32;
v8_src1 = vld2q_u8(src_ptr1);
src_ptr1 += 32;
uint16x8_t v16_u_sum0 = vpaddlq_u8(v8_src0.val[0]);
uint16x8_t v16_v_sum0 = vpaddlq_u8(v8_src0.val[1]);
uint16x8_t v16_u_sum1 = vpaddlq_u8(v8_src1.val[0]);
uint16x8_t v16_v_sum1 = vpaddlq_u8(v8_src1.val[1]);
v8_dst.val[0] = vshrn_n_u16(vaddq_u16(v16_u_sum0, v16_u_sum1), 2);
v8_dst.val[1] = vshrn_n_u16(vaddq_u16(v16_v_sum0, v16_v_sum1), 2);
vst2_u8(dst_ptr, v8_dst);
dst_ptr += 16;
}
//process leftover
src_ptr0 = src + src_stride * j * 2;
src_ptr1 = src_ptr0 + src_stride;
dst_ptr = dst + dst_stride * j;
for (int32_t i = dst_width_align; i < dst_width; i += 2)
{
dst_ptr[i] = (src_ptr0[i * 2] + src_ptr0[i * 2 + 2] +
src_ptr1[i * 2] + src_ptr1[i * 2 + 2]) / 4;
dst_ptr[i + 1] = (src_ptr0[i * 2 + 1] + src_ptr0[i * 2 + 3] +
src_ptr1[i * 2 + 1] + src_ptr1[i * 2 + 3]) / 4;
}
}
}
6、总结
本次学习中通过一个下采样的例子学习的NEON编程过程中的优势以及将要面临的问题,主要是剩余数据处理的方式,后面将继续深入学习。
/ 4;
dst_ptr[i + 1] = (src_ptr0[i * 2 + 1] + src_ptr0[i * 2 + 3] +
src_ptr1[i * 2 + 1] + src_ptr1[i * 2 + 3]) / 4;
}
}
}
#### 6、总结
本次学习中通过一个下采样的例子学习的NEON编程过程中的优势以及将要面临的问题,主要是剩余数据处理的方式,后面将继续深入学习。