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号磁盘........等等。…

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 工具包的流畅体验。请下…

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

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

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是一款免费、开源的分布式版本控制系统&…

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…

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

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

kotlin 中的数字

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

现货黄金交易多少克一手?国内外情况大不同

如果大家想参与国际市场上的现货黄金交易&#xff0c;就应该从它交易细则的入手&#xff0c;先彻底认识这个品种&#xff0c;因为它是来自欧美市场的投资方式&#xff0c;所以无论是从合约的计的单位&#xff0c;计价的货币&#xff0c;交易的具体时间&#xff0c;以及买卖过程…

【Python/Pytorch - 网络模型】-- 手把手搭建3D VGG感知损失模型

文章目录 文章目录 00 写在前面01 基于Pytorch版本的3D VGG代码02 论文下载 00 写在前面 感知损失&#xff1a;对于提升图片的肉眼可见细节&#xff0c;效果十分明显&#xff1b;对于一些指标如&#xff08;SSIM、PSNR&#xff09;这些&#xff0c;效果不明显。 在01中&…

springboot集成swagger、knife4j

1. 集成swagger2 1.1 引入依赖 <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</vers…

【Three.js】知识梳理十九:线性雾(Fog)、指数雾(FogExp2)和范围雾(RangeFog)

雾是3D图形中创建深度和氛围的重要工具。Three.js提供了多种类型的雾&#xff1a;线性雾&#xff08;THREE.Fog&#xff09;&#xff0c;指数雾&#xff08;THREE.FogExp2&#xff09;和范围雾&#xff08;RangeFog&#xff09;。本文将探讨这三种类型的雾&#xff0c;通过代码…

【大数据】Spark使用大全:下载安装、RDD操作、JAVA编程、SQL

目录 前言 1.下载安装 2.RDD操作 3.JAVA编程示例 4.Spark SQL 前言 本文是作者大数据系列中的一文&#xff0c;专栏地址&#xff1a; https://blog.csdn.net/joker_zjn/category_12631789.html?spm1001.2014.3001.5482 该系列会成体系的聊一聊整个大数据的技术栈&…

stable-diffusion 3 体验部署流程(ComfyUI)

环境准备 下载及简介 git clone https://huggingface.co/stabilityai/stable-diffusion-3-medium SD3 checkpoints&#xff1a; sd3_medium_incl_clips.safetensors (5.5GB)sd3_medium_incl_clips_t5xxlfp8.safetensors (10.1GB)sd3_medium.safetensors (4.3GB) 前两个可以…

SAP OB52 财务账期月结月底月初开关

公告&#xff1a;周一至周五每日一更&#xff0c;周六日存稿&#xff0c;请您点“关注”和“在看”&#xff0c;后续推送的时候不至于看不到每日更新内容&#xff0c;感谢。 这是一条刮刮乐&#xff0c;按住全部选中&#xff1a;点关注的人最帅最美&#xff0c;欢迎&#xff1…

vuInhub靶场实战系列--Kioptrix Level #4

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 arp-scan主机扫描 2.2 端口扫描2.3 指纹识别2.4 目…

nodejs——原型链污染

一、引用类型皆为对象 原型和原型链都是来源于对象而服务于对象的概念&#xff0c;所以我们要先明确一点&#xff1a; JavaScript中一切引用类型都是对象&#xff0c;对象就是属性的集合。 Array类型、Function类型、Object类型、Date类型、RegExp类型等都是引用类型。 也就…

Vue22-v-model收集表单数据

一、效果图 二、代码 2-1、HTML代码 2-2、vue代码 1、v-model单选框的收集信息 v-model&#xff1a;默认收集的就是元素中的value值。 单选框添加默认值&#xff1a; 2、v-model多选框的收集信息 ①、多个选择的多选 注意&#xff1a; 此处的hobby要是数组&#xff01;&…