0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

移动端arm cpu优化学习笔记第2弹-常量阶时间复杂度中值滤波

电子设计 来源:电子设计 作者:电子设计 2020-12-10 20:02 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

在复现 Side window 中值滤波的时候就在思考中值滤波能怎么优化,直观上看中值滤波好像没什么可优化的点,因为中值滤波需要涉及到排序,而且半径越大,排序的耗时也越大。那么中值滤波能否进一步加速呢?或者像均值滤波一样,可以不受滤波半径的影响呢?
作者:梁德澎

最近在复现 Side window 中值滤波的时候就在思考中值滤波能怎么优化,直观上看中值滤波好像没什么可优化的点,因为中值滤波需要涉及到排序,而且半径越大,排序的耗时也越大。那么中值滤波能否进一步加速呢?或者像均值滤波一样,可以不受滤波半径的影响呢?

答案是能!这篇博客就是记录了我是怎么去优化中值滤波的实践过程。而前面的3小节都是介绍我自己尝试的优化思路,最后一节才是讲本文标题提到的常量阶时间复杂度中值滤波的实现思路,想直接看其实现思路的读者可以跳到最后一小节。

1、一般中值滤波的实现

一开始能想到的中值滤波最直观的实现就是,把每个滤波窗口的内的值放进一个数组里面,然后排序,排序结果的排中间的值就是滤波结果。下面给出中值滤波的一般实现的示例代码(下面展示的所有代码只是为了用于说明,不保证能运行,实际代码以github上的代码为准):

median_filter(const float  *input,
              const int     radius,
              const int     height,
              const int     width,
              float        *output) {

  int out_idx = 0;
  for (int h = 0; h < height; ++h) {
    const int h_lower_bound = std::max(0, h - radius);
    const int h_upper_bound = std::min(height - 1, h + radius);
    const int h_interval = h_upper_bound - h_lower_bound + 1;

    for (int w = 0; w < width; ++w) {
      const int w_left_bound = std::max(0, w - radius);
      const int w_right_bound = std::min(width - 1, w + radius);
      const int arr_len = h_interval * (w_right_bound - w_left_bound + 1);

      int idx = 0;
      for (int i = h_lower_bound; i <= h_upper_bound; ++i) {
        const int h_idx = i * width;
        for (int j = w_left_bound; j <= w_right_bound; ++j) {
          m_cache[idx ++] = input[h_idx + j];
        }
      }

      sortArr(m_cache.data(), arr_len);
      output[out_idx ++] = m_cache[arr_len / 2];
    }
  }
}

排序函数sortArr的实现函数,这是实现的是选择排序法:

static void sortArr(float *arr, int len) {
  const int middle = len / 2;
  for (int i = 0; i <= middle; ++i) {
    float min = arr[i];
    int min_idx = i;
    for (int j = i + 1; j < len; ++j) {
      if (min > arr[j]) {
        min_idx = j;
        min = arr[j];
      }
    }
    // swap idx i and min_idx
    float tmp = arr[min_idx];
    arr[min_idx] = arr[i];
    arr[i] = tmp;
  }
}

这里有个小技巧是,实现排序函数的时候因为我们只是为了求中值,所以只需计算出前一半的有序元素即可,比如数组:

132, 45, 8, 1, 9, 100, 34

一般是全部排完得到:

1, 8, 9, 34, 45, 100, 132

中值就是34,但其实外部循环迭代只需要迭代到原来的一半(7 / 2)= 3 就行了就可停止了,下面看下选择排序中间每一步结果:

第0步,1和132交换:

132, 45, 8, 1, 9, 100, 34 -> 1, 45, 8, 132, 9, 100, 34

第1步,8和45交换:

1, 45, 8, 132, 9, 100, 34 -> 1, 8, 45, 132, 9, 100, 34

第2步,9和45交换:

1, 8, 45, 132,9, 100, 34 -> 1, 8, 9, 132, 45, 100, 34

第3步,34和132交换:

1, 8, 9, 132, 45, 100, 34 -> 1, 8, 9, 34, 45, 100, 132

到这一步就可停止,因为中值已经得到了,不过刚好这个例子是排到这一步就全部排好了而已。

然后看下这个最普通的实现在手机上的耗时,测试机型是华为P30(麒麟980),下面所有实验设置输入分辨率都是512x512,滤波半径大小从1到5,耗时跑30次取平均:

可以看到性能很一般,而且随着半径增加耗时也急剧增加。下面来看下第一版的优化,首先可以优化的点就是计算的数据类型。

2、第一版优化,float数据类型改uint16_t

因为一般我们处理图像的数据像rgb类型的数据其起取值范围是[0 ~ 255],这时候其实完全不需要用float来存储,用uint16_t类型就足够了,中间计算也是全部用uint16_t替换,完整代码:
https://github.com/Ldpe2G/ArmNeonOptimization/blob/master/ConstantTimeMedianFilter/src/normal_median_filter_uint16.cpp

这样子简单改一下数据类型之后,我们来看下其耗时:

可以看到就是简单改下运算数据类型,其运行耗时就可以下降不少。

3,第二版优化,简单利用并行计算指令

这版优化其实非常的暴力,就是既然每个窗口单次排序这样子太慢,那么就利用并行计算一次同时计算8个窗口的排序结果,下面是示例代码:

#if defined(USE_NEON_INTRINSIC) && defined(__ARM_NEON)
    int neon_arr_len = h_interval * (w_end - w_start + 1) * 8;
    for (int w = w_second_loop_start; w < remain_start; w += 8) {
      const int w_left_bound = std::max(0, w + w_start);
      const int w_right_bound = std::min(width - 1, w + w_end);

      int idx = 0;
      for (int i = h_lower_bound; i <= h_upper_bound; ++i) {
        const int h_idx = i * width;
        for (int j = w_left_bound; j <= w_right_bound; ++j) {
          for (int q = 0; q < 8; ++q) {
            m_cache[idx ++] = input[h_idx + j + q];
          }
        }
      }

      sortC4ArrNeon(m_cache.data(), neon_arr_len);
      for (int i = 0; i < 8; ++i) {
        m_out_buffer[out_idx ++] = m_cache[(neon_arr_len / 8 / 2) * 8 + i];
      }
    }
#endif

完整代码见:
https://github.com/Ldpe2G/ArmNeonOptimization/blob/master/ConstantTimeMedianFilter/src/normal_median_filter_uint16.cpp#L102

从代码上可以看到,因为用的是uint16_t类型的数据,所以可以一次处理8个窗口,相当于把从左到右8个窗口内的数据打包成C8的结构,然后看下排序函数的改动:

#if defined(USE_NEON_INTRINSIC) && defined(__ARM_NEON)
static void sortC4ArrNeon(uint16_t *arr, int len) {
  const int actual_len = len / 8;
  const int middle = actual_len / 2;
  uint16_t *arr_ptr = arr;
  for (int i = 0; i <= middle; ++i) {
    uint16x8_t  min = vld1q_u16(arr_ptr);
    uint16x8_t   min_idx = vdupq_n_u16(i);

    uint16_t *inner_arr_ptr = arr_ptr + 8;
    for (int j = i + 1; j < actual_len; ++j) {
      uint16x8_t curr =  vld1q_u16(inner_arr_ptr);
      uint16x8_t   curr_idx = vdupq_n_u16(j);
      uint16x8_t  if_greater_than = vcgtq_u16(min, curr);
      min     = vbslq_u16(if_greater_than, curr, min);
      min_idx = vbslq_u16(if_greater_than, curr_idx, min_idx);
      inner_arr_ptr += 8;
    }
    // swap idx i and min_idx
    for (int q = 0; q < 8; ++q) {
      float tmp = arr[min_idx[q] * 8 + q];
      arr[min_idx[q] * 8 + q] = arr[i * 8 + q];
      arr[i * 8 + q] = tmp;
    }
    arr_ptr += 8;
  }
}
#endif // __ARM_NEON

其实代码上看主体框架改动不大,还是采用选择排序法,不过如何利用neon intrinsic并行计算指令,同时对8个窗口内的数据进行排序呢?借助 vcgtqvbslq 这两个指令就可以做到。

vcgtq 表示将第一个参数内的数组元素与第二个参数对应元素比较,如果第一个数组的元素,大于等于对应第二个数组的对应元素,则结果对应位置会置为1,否则为0。

vbslq 指令有三个输入,第一个输入可以看做是判断条件,如果第一个输入的元素位置是1则结果的对应的位置就取第二个输入的对应位置,否则从第三个输入对应位置取值。其实这和mxnet的where操作子很像。

然后一次循环迭代完了之后,min_idx 数组就包含了这8个窗口当前迭代的各自最小值的位置。

ok,我们接着来看下这版的耗时:

可以看到用了neon加速之后,耗时减少了很多,大概是3~4倍的提速。

4,第三版优化,算法上的改进

经过前面的铺垫,终于到了本文的重点部分。如何让中值滤波的耗时不受滤波半径的影响,其实本质来说就是改变一下计算滤波窗口内中值的思路,不再采用排序,而是采用统计直方图的方式,因为一般图像数据rgb取值范围就是[0~255],那么求一个窗口内的的中值完全可以采统计这个窗口内的长度是256的直方图,然后中值就是从左到右遍历直方图,累加直方图内每个bin内的值,当求和结果大于等于窗口内元素个数的一半,那么这个位置的索引值就是这个窗口的中值。

不过这也不能解决滤波半径增大的影响,那么如何去除半径的影响呢,本文开头提到的这篇“Median Filtering in Constant Time ”文章里面引入了列直方图的方法,也就是除了统计滤波窗口的直方图,还对于图像的每一列,都初始化一个长度是256的直方图,所以滤波图像太宽的话需要的内存消耗也会更多

然后不考虑边界部分,对于中间部分的滤波窗口,其直方图不需要重新统计,只需要减去移出窗口的列直方图,然后加上新进来的列直方图即可,然后再计算中值,这三步加起来时间复杂度不会超过O(256*3),不受滤波半径影响,所以在行方向上是常量阶时间复杂度。

然后列方向就是同样的,列直方图在往下一行移动的时候也是采用同样方法更新,减去上一行和加上下一行的值,然后这样子列方向上也不受滤波半径影响了。

论文里采用的计算方式,当从左到右滤波的时候,第一次用到列直方图的时候才去更新列直方图,而我在实现的时候是移动到新的一行从头开始滤波之前,首先更新所有的列直方图,然后再计算每个滤波窗口的中值。而且我在申请直方图缓存的时候是所有直方图都放在同一段缓存内。

之后来看下这一版的耗时:

可以看到耗时很稳,基本不受滤波半径影响,不过由于需要涉及到直方图的计算,在滤波窗口比较小的时候,这个算法相对于直接算是没有优势的,但是当滤波窗口大于等于3的时候,其优势就开始体现了,而且越大越有优势。

论文里还提到了其他的优化思路,比如下面这篇文章的对直方图分级,不过我目前还没看懂怎么做,这个先挖个坑吧,等以后有机会再深挖:)。

A coarse-to-fine algorithm for fast median filtering of image data with a huge number of levels


还有在计算中值的时候其实是不需要每次都从头开始遍历直方图来计算中值的,下面这篇论文介绍了一个计算技巧可以减少计算中值的时间,有兴趣的读者可以看下:

A fast two-dimensional median filtering algorithm

更多AI移动端优化的请关注专栏嵌入式AI以及知乎(@梁德澎)。

审核编辑 黄昊宇
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • ARM
    ARM
    +关注

    关注

    135

    文章

    9588

    浏览量

    393604
  • cpu
    cpu
    +关注

    关注

    68

    文章

    11327

    浏览量

    225878
  • 人工智能
    +关注

    关注

    1820

    文章

    50324

    浏览量

    266908
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    利用ExecuTorch和Arm SME2加速侧机器学习推理

    贴纸、分离主体以替换背景,或是对图像局部进行选择性增强。这些效果背后,是轻量级分割模型在运行,这些模型通过 ExecuTorch(PyTorch 的开源侧推理运行时)以及第二代 Arm 可伸缩矩阵扩展技术 (Arm SME
    的头像 发表于 03-03 10:27 720次阅读
    利用ExecuTorch和<b class='flag-5'>Arm</b> SME<b class='flag-5'>2</b>加速<b class='flag-5'>端</b>侧机器<b class='flag-5'>学习</b>推理

    MAX270/MAX271 数字可编程双二连续时间低通滤波器详解

    MAX270/MAX271 数字可编程双二连续时间低通滤波器详解 引言 在电子设计领域,滤波器的性能对于信号处理至关重要。Maxim Integrated公司的MAX270/MAX2
    的头像 发表于 01-19 16:30 307次阅读

    电能质量在线监测装置支持密码复杂度要求吗?

    现代电能质量在线监测装置(尤其是中高端型号,适配电网安全要求)普遍支持密码复杂度要求 ,且部分装置会强制启用该功能,核心目的是防范弱密码导致的非法访问、数据篡改或设备操控风险,符合电力行业信息安全
    的头像 发表于 12-12 11:07 701次阅读

    如何训练好自动驾驶模型?

    [首发于智驾最前沿微信公众号]最近有位小伙伴在后台留言提问:算法是怎样训练的?是模仿学习、强化学习和离线强化学习这三类吗?其实
    的头像 发表于 12-08 16:31 1602次阅读
    如何训练好自动驾驶<b class='flag-5'>端</b>到<b class='flag-5'>端</b>模型?

    cs1237怎么优化滤波

    用mcu+cs1237做了一个电子秤,但是要过EN 61000-4-3,在空间辐射抗扰测试时180M和800M左右不符合要求,电源的滤波已经加了10uf 1uf 0.1u1n 100p 能提供一下帮助吗,cs1237的电源和信号滤波
    发表于 11-27 15:44

    程序运行慢,是否需检查算法时间复杂度过高?

    程序运行慢,需检查算法时间复杂度是否过高?
    发表于 11-17 08:08

    程序运行速度很慢如何优化

    ;gt;外设,内存<->内存)交给DMA,释放CPU资源。 优化算法: 选择时间复杂度更低的算法。避免不必要的循环和重复计算。 减少函数调用开销: 对于频繁调用的小函数
    发表于 11-17 06:12

    Arm Unlocked 2025上海站精彩回顾

    、应用需求、智能体 / 侧 AI、设计复杂度与成本、能效及创新速度六大维度重新定义计算,并重塑计算技术的研发、部署与规模化应用模式。
    的头像 发表于 09-25 17:15 1323次阅读

    负载减少50%!Arm用AI重新定义移动图形渲染

    电子发烧友网报道(文 / 吴子鹏)在移动互联网与游戏产业深度融合的当下,用户对移动游戏体验的期待持续攀升 —— 更清晰的画质、更流畅的帧率、更长的续航能力。然而,要在移动
    发表于 08-20 08:00 4157次阅读
    负载减少50%!<b class='flag-5'>Arm</b>用AI重新定义<b class='flag-5'>移动</b><b class='flag-5'>端</b>图形渲染

    Arm神经超级采样 以ML进一步强化性能 实现卓越的移动图形性能

    受限的移动设备上平衡这些目标体验,往往需要权衡取舍。传统的优化升级方法不够灵活,而实时人工智能 (AI) 渲染则又依然存在复杂、耗电或依赖硬件性能等难题。 Arm 神经超级采样 (
    的头像 发表于 08-14 18:15 4967次阅读
    <b class='flag-5'>Arm</b>神经超级采样 以ML进一步强化性能 实现卓越的<b class='flag-5'>移动</b><b class='flag-5'>端</b>图形性能

    一文了解Arm神经超级采样 (Arm Neural Super Sampling, Arm NSS) 深入探索架构、训练和推理

    本文将从训练、网络架构到后处理和推理等方面,深入探讨 Arm 神经超级采样 (Arm Neural Super Sampling, Arm NSS) 的工作原理,希望为机器学习 (ML
    的头像 发表于 08-14 16:11 3262次阅读

    HDI盲埋孔PCB数区分方法解析

    “a+N+N+a”形式表示,其中: a(增层):代表外层的增层次数,增层1次为一,增层2次为二,以此类推。 N(核心层):指中间的芯板层数,不直接决定数,但影响整体结构
    的头像 发表于 08-05 10:34 4848次阅读
    HDI盲埋孔PCB<b class='flag-5'>阶</b>数区分方法解析

    基于Matlab与FPGA的双边滤波算法实现

    前面发过中值、均值、高斯滤波的文章,这些只考虑了位置,并没有考虑相似。那么双边滤波来了,既考虑了位置,有考虑了相似,对边缘的保持比前几个
    的头像 发表于 07-10 11:28 4818次阅读
    基于Matlab与FPGA的双边<b class='flag-5'>滤波</b>算法实现

    Arm 公司面向移动市场的 ​Arm Lumex​ 深度解读

    面向移动市场的 ​ Arm Lumex ​ 深度解读 ​ Arm Lumex ​ 是 Arm 公司面向
    的头像 发表于 05-29 09:54 4562次阅读

    Arm CPU适配通义千问Qwen3系列模型

    与阿里巴巴开源的轻量级深度学习框架 MNN 已深度集成。得益于此,Qwen3-0.6B、Qwen3-1.7B 及 Qwen3-4B 三款模型能够在搭载 Arm 架构 CPU移动
    的头像 发表于 05-12 16:37 1589次阅读