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/

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PHP简约轻型聊天室留言源码

无名轻聊是一款phptxt的轻型聊天室。 无名轻聊特点&#xff1a; 自适应电脑/手机 数据使用txt存放&#xff0c;默认显示近50条聊天记录 采用jqueryajax轮询方式&#xff0c;适合小型聊天环境。 访问地址加?zhi进入管理模式&#xff0c;发送 clear 清空聊天记录。 修改在…

品质卓越为你打造App UI 风格

品质卓越为你打造App UI 风格

【ElasticSearch】ElasticSearch基本概念

ES 是一个开源的高扩展的分布式全文检索引擎&#xff0c;它是对开源库 Luence 的封装&#xff0c;提供 REST API 接口 MySQL 更适合数据的存储和关系管理&#xff0c;即 CRUD&#xff1b;而 ES 更适合做海量数据的检索和分析&#xff0c;它可以秒级地从数据库中检索出我们感兴…

【论文复现|智能算法改进】基于改进鲸鱼优化算法的移动机器人多目标点路径规划

目录 1.算法原理2.数学模型3.改进点4.结果展示5.参考文献6.代码获取 1.算法原理 SCI二区|鲸鱼优化算法&#xff08;WOA&#xff09;原理及实现【附完整Matlab代码】 2.数学模型 使用 A* 算法生成所有目标点之间的距离矩阵U: U [ d 1 − 1 d 1 − 2 d 1 − 3 ⋯ d 1 − i d…

关于python下安装selenium以及使用

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 目录 1、win10安装python环境 2、…

深入理解指针(二)

目录 1. 数组名的理解 2. 使用指针访问数组 3. ⼀维数组传参的本质 4. 冒泡排序 5. 二级指针 6. 指针数组 7. 指针数组模拟二维数组 1. 数组名的理解 有下面一段代码: #include <stdio.h> int main() {int arr[10] { 1,2,3,4,5,6,7,8,9,10 };int* p &arr[…

本地无法连接linux上的MariaDB数据库

使用mysql -u root -p 输入密码&#xff1a; 进去之后没有user表&#xff0c;无法改user、host等信息。

Blender帧动画

时间线窗口Timeline用于定义帧动画 -视图&#xff1a;方法&#xff0c;平移&#xff0c;框显全部 -帧范围&#xff1a;可以调整动画共多少帧 -当前帧&#xff1a;可以拖动或手工指定 默认每秒24帧 定义一个帧动画类似unity的Timeline&#xff0c;只需定义关键帧&#xff0c…

【太原理工大学】软件安全技术—书本重点梳理、带背

收回我上一篇的话&#xff0c;这科挂人还是挺狠的&#xff0c;去年好像挂了四十号人 ( 老师没有划重点&#xff0c;这篇是我自己根据之前的博主的和课本总结的一些重点&#xff0c;本篇为理解性带背。(&#xff61; _ &#xff61;) ### 第一章&#xff1a;软件安全基础 - **零…

Flask快速入门(路由、CBV、请求和响应、session)

Flask快速入门&#xff08;路由、CBV、请求和响应、session&#xff09; 目录 Flask快速入门&#xff08;路由、CBV、请求和响应、session&#xff09;安装创建页面Debug模式快速使用Werkzeug介绍watchdog介绍快速体验 路由系统源码分析手动配置路由动态路由-转换器 Flask的CBV…

青书学堂 看视频 耍课时

1. 获取课程节点id ( /nynzy/Student/Course/GetStudyRecordAndScore ) 接口地址 2. 把所有的nodeId 保存下来 保存到 old.txt 格式 课程id 与 nodeId 用 | 隔开 3. 然后创建 test.php 注意把 cookie 换成自己的 <?php$oldFilename ./old.txt; $newFilename ./new.…

使用MySQL全文索引实现高效搜索功能

MySQL全文索引是MySQL提供的一种高效的搜索功能&#xff0c;可以快速地搜索文本内容。全文索引可以用于搜索大量文本数据&#xff0c;通常应用在文章、博客、论坛等需要搜索的场景中。 什么是MySQL全文索引 MySQL全文索引是一种用于快速搜索文本内容的索引技术。它可以在存储和…

vue+elementplus模拟“山野愚人居”简单实现个人博客

目录 一、项目介绍 二、项目截图 1.项目结构图 2.项目首页 3.文章详情 4.留言 5.读者 三、源码实现 1.项目依赖package.json 2.项目启动 3.读者页面源码 四、总结 一、项目介绍 模仿原博客&#xff1a;山野愚人居 - 记录我的生活、所见、所闻、所想…… 本项目参考以…

联邦学习权重聚合,联邦学习权重更新

目录 联邦学习权重聚合 model.state_dict() 保存模型参数 加载模型参数 注意事项 联邦学习权重更新 联邦学习权重聚合 model.state_dict() 在PyTorch框架中,model.state_dict() 是一个非常重要的方法,它用于获取模型的参数(即权重和偏置)作为一个有序字典(Order…

最流行的后端框架:如何选择适合自己的框架

最流行的后端框架&#xff1a;如何选择适合自己的框架 在当今快节奏的数字环境中&#xff0c;软件开发需要高效、可扩展且可靠的解决方案。最流行的后端框架&#xff0c;这就是后端框架的用武之地。这些软件框架提供了构建 Web 应用程序的骨干&#xff0c;处理了从数据库交互到…

Spring系统学习 - Bean的作用域

bean作用域介绍 Spring框架提供了不同的作用域来管理Bean的生命周期和可见性&#xff0c;这对于控制不同类型的组件和处理并发请求尤其重要。 singleton&#xff08;默认&#xff09;&#xff1a; 每个Spring IoC容器只有一个bean实例。当容器创建bean后&#xff0c;它会被缓存…

CP AUTOSAR标准中文文档链接索引(更新中)

AUTOSAR标准的核心组件包括通信、诊断、安全等&#xff0c;这些组件通过模块化结构进行组织。系统被划分为多个模块&#xff0c;每个模块负责特定的功能。模块之间通过接口进行通信&#xff0c;接口定义了模块之间的交互规则。AUTOSAR标准支持模块的配置&#xff0c;可以根据不…

Vue25-内置指令02:v-text指令

一、v-html对比v-text v-html支持结构的解析&#xff0c;v-text不支持结构的解析。 二、v-html的安全性问题 2-1、cookie的原理&#xff08;node.js&#xff09; 7天免登录&#xff0c;cookie实现。 cookie的本质就是类似于json的字符串&#xff0c;格式是&#xff1a;key-va…

Web端在线/离线Stomp服务测试与WebSocket服务测试

Stomp服务测试 支持连接、发送、订阅、接收&#xff0c;可设置请求头、自动重连 低配置云服务器&#xff0c;首次加载速度较慢&#xff0c;请耐心等候 预览页面&#xff1a;http://www.daelui.com/#/tigerlair/saas/preview/lxbho9lkzvgc 演练页面&#xff1a;http://www.da…

「OC」UI练习(二)——照片墙

「OC」UI练习——照片墙 文章目录 「OC」UI练习——照片墙UITapGestureRecognizer介绍照片墙实现 UITapGestureRecognizer介绍 UITapGestureRecognizer是UIKit框架中的一个手势识别器类&#xff0c;用于检测用户在视图上的轻击手势。它是UIGestureRecognizer的一个子类&#x…