60行代码加速20倍: NEON实现深度学习OD任务后处理绘框

【前言】 本文版权属于GiantPandaCV,未经允许,请勿转载!
最近在学neon汇编加速,由于此前OD任务发现在检测后处理部分使用OpenCV较为占用资源且耗时,遂尝试使用NEON做后处理绘框,以达到加速并降低CPU资源消耗的目的。

一、实现思路

假设对一张Mat图像进行操作(其实也不仅仅是Mat对象,理论上只要知道图像通道的首指针即可),在ARM端使用NEON instrinc指令集里实现一个后处理绘框的功能,可以简单罗列成以下几步:
1. 定义参数: 首先确定图像的宽度和高度,图像的首地址指针,以及边界(边框)的厚度。
2. 向量寄存器加载: 使用NEON的加载指令从内存中加载像素数据到向量寄存器中。
3. 处理上下边框:

  • 对于顶部边界,遍历整个第一行的像素,并使用NEON的存储指令将特定颜色值写回到这些位置(比如想绘制的是绿框,那么需要将B通道的绘框元素数据更改为0,G通道为255,R通道为0)。
  • 同样地,对于底部边界,遍历最后一行的像素并执行相同的操作。

4.处理左右边框:
这个稍微复杂一些,因为需要处理每一行的开始和结束位置。一种方法是使用循环,每次处理一行,然后更新寄存器中的值以反映特定颜色。我们可以使用NEON的广播指令来创建一个包含特定颜色所有分量的向量,然后使用存储指令将其写入到图像的左侧和右侧边界。
5.边框优化:
由于很多检测框的宽度很难保证一定是SIMD WIDTH的倍数,这就造成了在绘图时一些不必要的麻烦,举个例子,假设检测框的width是97,SIMD WIDTH的长度是16(一次性处理16个元素的向量寄存器),那么97/16=6······1,刚好多出了1个pixel,此时需要某些处理措施规避这种情况。

二、实现过程

2.1 定义参数

首先确定图像的宽度和高度,本次测试所获得的检测框均由这篇博文中的end2end模型中获得【1】,也就是在绘框前,我们会得到一个vector数组,均为通过nms获得的检测框,这个数组数据排列格式如下:

一个box对应四个元素,其实box是按照obj的score排列,但为了方便讲解,我们假设他是按从左到右顺序排列,由于测试的图片均为COCO2017 Val中的数据,图片尺寸中值远大于320,为了美观,此篇博文默认绘框边界(边框)的厚度为2,也就是占满2个pixel。
函数定义如下:

void neon_rectangle_blod(uint8_t *img, uint16_t img_step, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t blue, uint8_t green, uint8_t red)

函数形参解释: ∗ i m g *img img为图像首指针, i m g s t e p img step imgstep指图像的width, x , y x,y x,y指检测框左上角, w , h w,h w,h指检测框的宽高, b l u e , g r e e n , r e d blue, green, red blue,green,red指三色通道需要填充的数值。

2.2 向量寄存器加载

这一步需要将图像BGR通道元素加载到寄存器,由于图像一般为uint8格式,这里可以使用最大的寄存器,把位宽拉满,也就是一次性操作16个元素,调用NEON instrinc中的vld3q_u8加载图像BGR数据到uint8x16x3_t寄存器中,再将单个通道的数据分发到到单个uint8x16_t寄存器中,伪代码如下:

// 假设img是指向图像BGR数据的指针
uint8x16x3_t bgr_data = vld3q_u8((uint8_t *) img);// 分别将BGR通道的数据分发到单独的uint8x16_t寄存器
uint8x16_t reg_b = bgr_data.val[0]; // 蓝色通道
uint8x16_t reg_g = bgr_data.val[1]; // 绿色通道
uint8x16_t reg_r = bgr_data.val[2]; // 红色通道// 后续对每个通道进行单独的操作
......
2.3 处理上下边框

我们需要定位到上下边框的起始位置,获取起始位置的地址,再将地址往后以16个pixel为一个SIMD_WIDTH塞入寄存器,将寄存器中的B,G,R通道进行向量赋值,表示一次性处理16个数据流位宽,代码如下:

// 绘制矩形的上下边界for (uint16_t i = 0; i < w; i += 16){// 计算当前行的起始地址uint8_t *top_row1 = img + (y * img_step + x + i) * 3;uint8_t *bottom_row1 = img + ((y + h) * img_step + x + i) * 3;// 使用NEON指令集并行加载和存储颜色uint8x16x3_t pixels_top1 = vld3q_u8(top_row1);uint8x16x3_t pixels_bottom1 = vld3q_u8(bottom_row1);// 绘制顶部和底部线条pixels_top1.val[0] = neon_color_b; // 蓝色通道pixels_top1.val[1] = neon_color_g; // 绿色通道pixels_top1.val[2] = neon_color_r; // 红色通道pixels_bottom1.val[0] = neon_color_b;pixels_bottom1.val[1] = neon_color_g;pixels_bottom1.val[2] = neon_color_r;vst3q_u8(top_row1, pixels_top1);vst3q_u8(bottom_row1, pixels_bottom1);// 计算当前行的起始地址uint8_t *top_row2 = img + ((y + 1) * img_step + x + i) * 3;uint8_t *bottom_row2 = img + ((y + h - 1) * img_step + x + i) * 3;// 使用NEON指令集并行加载和存储颜色uint8x16x3_t pixels_top2 = vld3q_u8(top_row2);uint8x16x3_t pixels_bottom2 = vld3q_u8(bottom_row2);// 绘制顶部和底部线条pixels_top2.val[0] = neon_color_b; // 蓝色通道pixels_top2.val[1] = neon_color_g; // 绿色通道pixels_top2.val[2] = neon_color_r; // 红色通道pixels_bottom2.val[0] = neon_color_b;pixels_bottom2.val[1] = neon_color_g;pixels_bottom2.val[2] = neon_color_r;vst3q_u8(top_row2, pixels_top2);vst3q_u8(bottom_row2, pixels_bottom2);}
2.4 处理左右边框

这里就有点难受了,因为是ARM架构通用的汇编,不像一些厂家有专门处理竖直方向的寄存器或者额外的硬件加速模块,所以这一步只能老老实实一个pixel一个pixel的去涂,因此和OpenCV的处理方式没有太大差异,代码如下:

// 绘制矩形的左右边界for (uint16_t j = 0; j < h; j++){// 计算当前列的起始地址uint8_t *left_col1 = img + ((y + j) * img_step + x) * 3;uint8_t *right_col1 = img + ((y + j) * img_step + (x + w)) * 3;// 设置左边和右边列的颜色left_col1[0] = right_col1[0] = blue;left_col1[1] = right_col1[1] = green;left_col1[2] = right_col1[2] = red;// 计算当前列的起始地址uint8_t *left_col2 = img + ((y + j) * img_step + x + 1) * 3;uint8_t *right_col2 = img + ((y + j) * img_step + (x + w) - 1) * 3;// 设置左边和右边列的颜色left_col2[0] = right_col2[0] = blue;left_col2[1] = right_col2[1] = green;left_col2[2] = right_col2[2] = red;}
2.5 优化边框

这里提供一种思路,既然没办法确保检测框的宽度刚好是SIMD_WIDTH的倍数,那我们就将宽度扩充或者减小到SIMD_WIDTH的倍数,但为了美观处理,不管是扩充还是减小宽度,我们都离不开一个操作,那就是中心对齐,以扩宽为例,如下图所示:

那么,就有很好的方式去应对这种情况,我们假设检测框的width对SIMD_WIDTH进行mod操作,如果余数小于
S I M D ‘ W I D T H / 2 SIMD_`WIDTH/2 SIMDWIDTH/2,对检测框width进行缩小操作,反之,则进行扩充操作,代码如下:

void check_point(int *x1, int *x2, int nstride)
{int mod, w, xc, nw;w = *x2 - *x1;xc = *x1 + (int)(w / 2);mod = w % nstride;if (mod > (nstride / 2)){*x1 = xc - (int)((w + nstride - mod) / 2);*x2 = xc + (int)((w + nstride - mod) / 2);}else{nw = w - mod;*x1 = xc - int(nw / 2);*x2 = xc + int(nw / 2);}
}

三、测试结果

测试机器为4+32内存的树莓派4B,共带有4颗A72核,我们分别使用NEON和OpenCV作为【1】中end2end模型出框后的后处理绘框函数,测试数据为COCO2017 Val数据集,将两个程序用taskset -c先绑定在编号为0的核上,得出两者在处理5000张图的处理速度差异,如下所示:

其中,cost time为推理完5000张图的所有耗时,单位为ms,average cost time为处理单张图片的耗时,单位为us,我们可以看到,在单个A72上,NEON实现的绘框函数要比OpenCV快了20倍左右。
此外,OpenCV的强大源于多核并行,为了能更加客观且全面的测试出两者的性能差异,我们在OpenCV版本的基础上,不断增加核进行测试,得出以下测试图例:

图中P/ms表示1ms能处理多少图,越高表示每毫秒处理图越多,单图绘框速度越快,从图可以看出,单核运行的NEON绘框的速度依旧稳稳碾压多核并行的OpenCV。
OpenCV绘框效果如下:

NEON汇编绘框效果如下:

四、完整代码

void check_point(int *x1, int *x2, int nstride)
{int mod, w, xc, nw;w = *x2 - *x1;xc = *x1 + (int)(w / 2);mod = w % nstride;if (mod > (nstride / 2)){*x1 = xc - (int)((w + nstride - mod) / 2);*x2 = xc + (int)((w + nstride - mod) / 2);}else{nw = w - mod;*x1 = xc - int(nw / 2);*x2 = xc + int(nw / 2);}
}void neon_rectangle_blod(uint8_t *img, uint16_t img_step, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t blue, uint8_t green, uint8_t red)
{// 创建一个全1的8位向量,用于绘制矩形的颜色uint8x16_t neon_color_b = vdupq_n_u8(blue);uint8x16_t neon_color_g = vdupq_n_u8(green);uint8x16_t neon_color_r = vdupq_n_u8(red);// 绘制矩形的上下边界for (uint16_t i = 0; i < w; i += 16){// 计算当前行的起始地址uint8_t *top_row1 = img + (y * img_step + x + i) * 3;uint8_t *bottom_row1 = img + ((y + h) * img_step + x + i) * 3;// 使用NEON指令集并行加载和存储颜色uint8x16x3_t pixels_top1 = vld3q_u8(top_row1);uint8x16x3_t pixels_bottom1 = vld3q_u8(bottom_row1);// 绘制顶部和底部线条pixels_top1.val[0] = neon_color_b; // 蓝色通道pixels_top1.val[1] = neon_color_g; // 绿色通道pixels_top1.val[2] = neon_color_r; // 红色通道pixels_bottom1.val[0] = neon_color_b;pixels_bottom1.val[1] = neon_color_g;pixels_bottom1.val[2] = neon_color_r;vst3q_u8(top_row1, pixels_top1);vst3q_u8(bottom_row1, pixels_bottom1);// 计算当前行的起始地址uint8_t *top_row2 = img + ((y + 1) * img_step + x + i) * 3;uint8_t *bottom_row2 = img + ((y + h - 1) * img_step + x + i) * 3;// 使用NEON指令集并行加载和存储颜色uint8x16x3_t pixels_top2 = vld3q_u8(top_row2);uint8x16x3_t pixels_bottom2 = vld3q_u8(bottom_row2);// 绘制顶部和底部线条pixels_top2.val[0] = neon_color_b; // 蓝色通道pixels_top2.val[1] = neon_color_g; // 绿色通道pixels_top2.val[2] = neon_color_r; // 红色通道pixels_bottom2.val[0] = neon_color_b;pixels_bottom2.val[1] = neon_color_g;pixels_bottom2.val[2] = neon_color_r;vst3q_u8(top_row2, pixels_top2);vst3q_u8(bottom_row2, pixels_bottom2);}// 绘制矩形的左右边界for (uint16_t j = 0; j < h; j++){// 计算当前列的起始地址uint8_t *left_col1 = img + ((y + j) * img_step + x) * 3;uint8_t *right_col1 = img + ((y + j) * img_step + (x + w)) * 3;// 设置左边和右边列的颜色left_col1[0] = right_col1[0] = blue;left_col1[1] = right_col1[1] = green;left_col1[2] = right_col1[2] = red;// 计算当前列的起始地址uint8_t *left_col2 = img + ((y + j) * img_step + x + 1) * 3;uint8_t *right_col2 = img + ((y + j) * img_step + (x + w) - 1) * 3;// 设置左边和右边列的颜色left_col2[0] = right_col2[0] = blue;left_col2[1] = right_col2[1] = green;left_col2[2] = right_col2[2] = red;}
}

五、总结

本篇博文主要讲述后处理绘框的汇编实现方式,在树莓派上的单核以及多核A72上都实现了加速,但时间关系未于其他开发板做比较,从去年开始,似乎4大+4小变成了业界主流,既4颗A76+4颗A57或者4颗A76+4颗A53,ARM端CPU算力要远远强过四颗A72,至于这种汇编实现方式,在这些开发板上能加速多少,确实不好说,有兴趣的朋友可以用这几十行代码去测试下~

六、参考

[1] https://zhuanlan.zhihu.com/p/672633849
[2] https://zhuanlan.zhihu.com/p/698551682
[3] https://developer.arm.com/documentation/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/26628.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 中 “ 磁盘、进程和内存 ” 的管理

在linux虚拟机中也有磁盘、进程、内存的存在。第一步了解一下磁盘 一、磁盘管理 &#xff08;1.1&#xff09;磁盘了解 track&#xff08; 磁道 &#xff09; &#xff1a;就是磁盘上的同心圆&#xff0c;从外向里&#xff0c;依次排序1号&#xff0c;2号磁盘........等等。…

802.11中的各种帧

在无线网络中&#xff0c;802.11协议定义了三种类型的帧&#xff1a;管理帧&#xff08;Management Frames&#xff09;、控制帧&#xff08;Control Frames&#xff09;和数据帧&#xff08;Data Frames&#xff09;。每种类型的帧都有其特定的功能&#xff0c;帮助维护和管理…

QNX简述

文章目录 前言1. QNX简介1.1 什么是QNX1.2 QNX的应用场景1.3 QNX的优点1.4 QNX的发展史1.5 QNX的商业模式 2. QNX的技术特点3. QNX和其它操作系统的比较3.1 QNX VS LINUX3.2 QNX VS FreeRTOS3.3 QNX VS 鸿蒙操作系统 4. 我的疑问4.1 微内核看起来又稳定又容易调试&#xff0c;为…

【讯为Linux驱动开发】6.自旋锁spinlock

【自旋锁】 线程A获取自旋锁后&#xff0c;B假如想获取自旋锁则只能原地等待&#xff0c;仍占用CPU&#xff0c;不会休眠&#xff0c;直到获取自旋锁为止。 【函数】 DEFINE SINLOCK(spinlock t lock) 定义并初始化一个变量int spin lock init(spinlock t*lock) 初始化自…

技术速递|Java on Azure Tooling 5月更新 - Java 对 Azure 容器应用程序的入门指南支持

作者&#xff1a;Jialuo Gan 排版&#xff1a;Alan Wang 大家好&#xff0c;欢迎阅读 Java on Azure 工具 5 月份更新。在本次更新中&#xff0c;我们将介绍 Java 在 Azure 上的容器应用程序的入门指南。希望您喜欢这些更新&#xff0c;并享受使用 Azure 工具包的流畅体验。请下…

Python中用pip命令用稳定的国内源安装第三方库

近期发现python中安装三方库的最稳定的方式还是用pip命令&#xff0c;带上国内源的地址。 比如清华源&#xff1a; pip install 包名 -i https://pypi.tuna.tsinghua.edu.cn/simple/ 用这个带国内源的格式&#xff0c;非常稳定&#xff01;

audio标签怎么使用

<audio> 标签在 HTML 中用于嵌入音频内容&#xff0c;如音乐、歌曲、音效等。这个标签允许你在网页上直接播放音频&#xff0c;而无需依赖任何外部播放器或插件&#xff08;如过去的 Flash 插件&#xff09;。 以下是如何使用 <audio> 标签的基本示例&#xff1a;…

oss一个桶中如何创建多个文件夹并在上传文件时上传到相应指定的桶中

在阿里云OSS&#xff08;Object Storage Service&#xff09;中&#xff0c;文件夹的概念实际上是一个逻辑上的概念&#xff0c;因为OSS是一个基于对象的存储服务&#xff0c;而不是基于文件系统的。但是&#xff0c;你可以通过为对象指定特定的key来模拟文件夹结构。以下是如何…

《pvz植物大战僵尸杂交版》V2.0.88整合包火爆全网,支持安卓、ios、电脑等!

今天来给大家安利一款让人欲罢不能的游戏——《植物大战僵尸杂交版》2.0.88版。这可不是普通的植物大战僵尸&#xff0c;它可是席卷了B站&#xff0c;火爆全网的存在&#xff01; 先说说这个版本&#xff0c;它可是网络上现存最全的植物大战僵尸杂交版整合包。里面不仅有修改工…

什么是 OSI 模型?

OSI 模型&#xff08;开放式系统互联模型&#xff09;是一个由国际标准化组织&#xff08;ISO&#xff09;提出的概念模型&#xff0c;旨在为计算机网络的互联互通提供标准框架&#xff08;定义于 ISO/IEC 7498-1&#xff09;。该模型将通信系统中的数据流划分为七个层&#xf…

uni-app中添加路由拦截

uni-app中添加路由鉴权和路由拦截 在main.js中添加如下代码 let list ["navigateTo", "redirectTo", "reLaunch", "switchTab"] let routesWhitelist [/pages/tabs/classify,/pages/tabs/study,/pages/tabs/mine] // 可以直接跳转的…

torch.squeeze() dim=1 dim=-1 dim=2

对数据的维度进行压缩 使用方式&#xff1a;torch.squeeze(input, dimNone, outNone) 将输入张量形状中的1 去除并返回。 如果输入是形如(A1B1C1D)&#xff0c;那么输出形状就为&#xff1a; (ABCD) import torch x torch.rand(2, 1, 1, 3, 1, 4) print(x) print(x.shape) …

wms海外仓系统什么价格?中小海外仓怎么选到高性价比wms系统

随着海外仓业务复杂度的逐渐提升&#xff0c;现在中小海外仓对wms海外仓系统的需求也越来越强烈。但是对于预算有限的中小海外仓企业来说&#xff0c;怎么才能选到性价比比较高的wms海外仓系统呢&#xff1f; 今天我们就来聊一下这个问题&#xff0c;希望对有类似需求的海外仓…

Git基础指令(图文详解)

目录 Git概述Git基础指令Linux系统操作指令 Git软件指令1.配置信息2.名称和邮箱3.初始化版本库4.向版本库中添加文件5.修改版本库文件6. 查看版本库文件历史 7.删除文件8.恢复历史文件 Git概述 Git基础指令 Linux系统操作指令 Git是一款免费、开源的分布式版本控制系统&…

ORACLE中ROWNUM的机制和注意细节(避坑

问题背景 mybatis对接oracle数据库中会用ROWNUM做分页处理。 形如如下sql SELECT * FROM ( SELECT TMP.*, ROWNUM ROW_ID FROM ( SELECT * FROM YOUR_TABLE ) TMP WHERE ROWNUM < ?) WHERE ROW_ID > ?简单说&#xff0c;ROWNUM就是一个对查找结果分配行号的伪列。 问…

github ssh key的SHA256是什么

github ssh key的SHA256是什么 怎么知道github上自己的公钥指纹和本地的公钥是否一致&#xff1f; 计算方法如下&#xff1a; cat .ssh/id_rsa.pub |awk { print $2 } | # Only the actual key data without prefix or commentsbase64 -d | # decode as base64s…

Guava常用方法

目录 一、数学和数值操作 二、并发库 三、缓存 四、集合 五、I/O 与文件操作 六、网络 七、时间处理 八、事件总线 九、反射 十、范围和集合操作 十一、随机数和测试 十二、注解处理 十三、比较器和排序 十四、哈希和散列 Guava 是 Google 开源的一个 Java 工具库&#xff…

【课程总结】Day8(下):计算机视觉基础入门

前言 数据结构 在人工智能领域&#xff0c;机器可以处理的数据类型如上图&#xff0c;大约可以分为以上类别。其中较为常用的数据类别有&#xff1a; 表格类数据 数据特点&#xff1a; 成行成列&#xff1a;一行一个样本&#xff0c;一列一个特征特征之间相互独立&#xff0…

RSS 解析:全球内容分发的利器及使用技巧

使用 RSS 可以将最新的网络内容从一个网站分发到全球数千个其他网站。 RSS 允许快速浏览新闻和更新。 RSS 文档示例 <?xml version"1.0" encoding"UTF-8" ?> <rss version"2.0"><channel><item></item><it…

kotlin 中的数字

以下均来自官方文档&#xff1a; 一、整数类型 1、kotlin中内置的整数类型&#xff0c;有四种不同大小的类型&#xff1a; 类型存储大小&#xff08;比特数&#xff09;最小值最大值Byte8-128127Short16-3276832767Int32-2,147,483,648 (-231)2,147,483,647 (231 - 1)Long64…