图像沿列方向降维的AVX2实现讨论

对于二维图像,取每一列像素序列的和或均值是一种基础算子。

本文主要介绍本人的优化思路,因此本文中要求图像高度不能超过256,这样就可以保证对于常见的8bit图像,每列求和的结果使用uint16_t保存即可保证不溢出,同时要求图像宽度是32或64的倍数等,省略对每行尾部无法并行的部分的计算。

普通指针实现

void ReduceColSum(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {for (size_t y = 0; y < height; ++y) {for (size_t x = 0; x < width; ++x) {dst[x] += (uint16_t)src[y * width + x];}}
}

当前OpenCV在4.6.0中的cv::Reduce代码中使用的便是使用通用寄存器执行for循环,不过对不同的算术运算封装成了模板,并使用了×4的循环展开。

AVX指令集实现

两种基本实现

第一种AVX指令集的实现如下函数ReduceColSum1所示。循环内部avx指令相关的操作主要包括load256->permute->unpacklo/unpackhi->adds->storeu。

void ReduceColSum1(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {assert(width % 32 == 0);assert(height <= 256);__m256i mSrc, mSum, mSum2;int nCount = (width / 32);const uint8_t* psrc = src;const __m256i m0 = _mm256_setzero_si256();for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSrc = _mm256_loadu_si256((__m256i*)(psrc));mSrc = _mm256_permute4x64_epi64(mSrc, 0xD8);mSum = _mm256_loadu_si256((__m256i*)(pdst));mSum2 = _mm256_loadu_si256((__m256i*)(pdst + 16));mSum = _mm256_adds_epu16(mSum, _mm256_unpacklo_epi8(mSrc, m0));_mm256_store_si256((__m256i*)(pdst), mSum);mSum2 = _mm256_adds_epu16(mSum2, _mm256_unpackhi_epi8(mSrc, m0));_mm256_storeu_si256((__m256i*)(pdst + 16), mSum2);pdst += 32;psrc += 32;}}
}

第二种实现如下ReduceColSum2函数所示,实现是将permute->unpacklo/unpackhi替换为AVX2指令集中的convert指令。可以代码更简洁易懂。但每个内层循环只能读取并处理16字节。经测试两种实现耗时大体相当。

void ReduceColSum2(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {assert(width % 32 == 0);assert(height <= 256);__m128i mSrc;__m256i mSum;const uint8_t* psrc = src;const int nCount = (width / 16);for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSum = _mm256_loadu_si256((__m256i*)(pdst));mSrc = _mm_loadu_si128((const __m128i*)(psrc));mSum = _mm256_adds_epu16(mSum, _mm256_cvtepu8_epi16(mSrc));_mm256_storeu_si256((__m256i*)(pdst), mSum);pdst += 16;psrc += 16;}}
}

测试_mm256_cvtepu8_epi32

ReduceColSum2_mm256_cvtepu8_epi16替换为_mm256_cvtepu8_epi32,取消图像高度不大于256的限制,经测试耗时增加了一倍左右。

void ReduceColSum3(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {assert(width % 32 == 0);assert(height <= 256);__m128i mSrc;__m256i mSum;const uint8_t* psrc = src;const int nCount = (width / 8);for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSum = _mm256_loadu_si256((__m256i*)(pdst));mSrc = _mm_loadu_si128((const __m128i*)(psrc));mSum = _mm256_add_epi32(mSum, _mm256_cvtepu8_epi32(mSrc));_mm256_storeu_si256((__m256i*)(pdst), mSum);pdst += 8;psrc += 8;}}
}

使用循环展开优化

可以看到列求和操作计算部分包括cvt和add两步骤,两条指令的Latency是比较低的,反之读写操作loadu和storeu的Latency相对较高,因此可以使用循环展开进行优化。ReduceColSum3ReduceColSum4分别是展开2倍和4倍的代码,经测试耗时约是不使用循环展开的70%和50%。

void ReduceColSum3(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {assert(width % 32 == 0);assert(height <= 256);__m128i mSrc, mSrc1;__m256i mSum, mSum1;const uint8_t* psrc = src;const int nCount = (width / 32);for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSum = _mm256_loadu_si256((__m256i*)(pdst));mSum1 = _mm256_loadu_si256((__m256i*)(pdst+16));mSrc = _mm_loadu_si128((const __m128i*)(psrc));mSrc1 = _mm_loadu_si128((const __m128i*)(psrc + 16));mSum = _mm256_adds_epu16(mSum, _mm256_cvtepu8_epi16(mSrc));_mm256_storeu_si256((__m256i*)(pdst), mSum);mSum1 = _mm256_adds_epu16(mSum1, _mm256_cvtepu8_epi16(mSrc1));_mm256_storeu_si256((__m256i*)(pdst + 16), mSum1);pdst += 32;psrc += 32;}}
}
void ReduceColSum4(const uint8_t* src, uint16_t* dst, uint16_t height,uint16_t width) {assert(width % 64 == 0);assert(height <= 256);__m128i mSrc, mSrc1, mSrc2, mSrc3;__m256i mSum, mSum1, mSum2, mSum3;const uint8_t* psrc = src;const int nCount = (width / 64);for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSum = _mm256_loadu_si256((__m256i*)(pdst));mSum1 = _mm256_loadu_si256((__m256i*)(pdst+16));mSum2 = _mm256_loadu_si256((__m256i*)(pdst+32));mSum3 = _mm256_loadu_si256((__m256i*)(pdst+48));mSrc = _mm_loadu_si128((const __m128i*)(pdst));mSrc1 = _mm_loadu_si128((const __m128i*)(pdst+16));mSrc2 = _mm_loadu_si128((const __m128i*)(pdst+32));mSrc3 = _mm_loadu_si128((const __m128i*)(pdst+48));mSum = _mm256_adds_epu16(mSum, _mm256_cvtepu8_epi16(mSrc));_mm256_storeu_si256((__m256i*)(pdst), mSum);mSum1 = _mm256_adds_epu16(mSum1, _mm256_cvtepu8_epi16(mSrc1));_mm256_storeu_si256((__m256i*)(pdst + 16), mSum1);mSum2 = _mm256_adds_epu16(mSum2, _mm256_cvtepu8_epi16(mSrc2));_mm256_storeu_si256((__m256i*)(pdst + 32), mSum2);mSum3 = _mm256_adds_epu16(mSum3, _mm256_cvtepu8_epi16(mSrc3));_mm256_storeu_si256((__m256i*)(pdst + 48), mSum3);pdst += 64;psrc += 64;}}
}

总结

除了本文之外,我实际进行了更多测试,总体由于不同指令的测试效果并不稳定,在本文并未体现。总体上可以看到使用AVX指令集并行只不过是第一步,尤其是对于简单的图像计算也是最简单的一步,全部依赖编译器优化也是不合理,比如使用的MSVC并不会使用循环展开。只有深入理解计算机系统,理解汇编代码、清楚计算机工作原理,才能更好的优化代码执行。

图像沿列方向降维的AVX2实现讨论相关推荐

  1. 图像算法九:【图像特征提取】特征降维、PCA人脸特征抽取、局部二进制

    PCA数学理论: 关于PCA的理论,资料很多,公式也一大把,本人功底有限,理论方面这里就不列出了.下面主要从应用的角度大概来讲讲具体怎么实现数据集的降维. 把原始数据中每个样本用一个向量表示,然后把所 ...

  2. ITK:计算图像在特定方向上的导数

    ITK:计算图像在特定方向上的导数 内容提要 C++实现代码 内容提要 计算图像在特定方向上的导数. C++实现代码 #include "itkImage.h" #include ...

  3. MIT线性代数笔记一 行图像和列图像

    文章目录 1. 曾经 2. 现在 3. 第一讲 行图像和列图像 3.1 行图像 3.2 列图像 1. 曾经   若干年前,有一个年轻的男老师(王清老师)给我们讲线性代数.他讲课的声音比较小,坐到后面接 ...

  4. opencv java水平投影_OpenCV实现图像在水平方向上投影

    开始没有将数组赋值为零,不能正常显示.代码如下: Mat srcImage=imread("test.png"); imshow("C",srcImage); ...

  5. 图像特征:方向梯度直方图 HOG

    文章目录 参考资料 简介 算法流程 灰度化和gamma校正 计算梯度 统计cell的梯度方向直方图 Block 块内归一化(重点) 组合为HOG特征 HOG特征与可视化 OpenCV 算法实现 参考资 ...

  6. PyQT:第一个Demo,画出鼠标单击位置出图像的列像素折线图

    场景:有一系列图像,需要查看图像每列的像素值的大小,可以把图像读出来然后指定列,再查看,但比较麻烦,每看一列都要修改一下.后面又用回调函数滑动条,这样不用每次都修改列了,但假如换张图像的话还是要修改图 ...

  7. python treeview insert_pythonttkinter Treeview添加图像作为列值

    您可以使用w.insert方法中的image参数显示图像.见下文.在from tkinter import PhotoImage. self._img = PhotoImage(file=" ...

  8. android 获取相机方向,android – 从相机捕捉图像,导致炸毁方向

    我试图从相机捕捉图像,它可以在横向模式下正常工作,当我拍摄肖像时,它会旋转.以下是我使用的代码: BitmapFactory.Options bounds = new BitmapFactory.Op ...

  9. numpy.hstack(a,b) 按列方向扩展 与 numpy.vstack(a,b) 按行方向扩展 其中n*1 数组可以写成 举例 np.array([[1],[2],[3]])

    https://www.jianshu.com/p/608140bec2f5 其中n*1 数组可以写成 举例 np.array([[1],[2],[3]])

最新文章

  1. 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
  2. 机器学习与气象数据_气象大数据与机器学习联合实验室 大数据和气象的“联姻”...
  3. Openldap命令详解
  4. 设计模式(二)设计模式的本质
  5. c调用其他类的方法_吊打面试官-类加载器
  6. oracle诊断日志,oracle日常诊断语句
  7. (66)FPGA面试题-为parallel encoder编写Verilog代码,实现MUX4_1
  8. 怎么看tomcat添加的项目名_Tomcat部署项目不加项目名访问,不加8080访问
  9. 名编辑电子杂志大师教程 | 添加搜索功能
  10. iris数据集——决策树
  11. 小甲鱼C语言单链表通讯录作业
  12. EC Final 2019 题解
  13. Lion Disk Maker让你一键制作Lion系统安装U盘
  14. 三种碎片化方法:RECAP, BRICS与eMolFrag
  15. C# 解决上传附件大小限制
  16. vue 动态生成下载二维码
  17. 三星手机html默认,关于三星手机恢复出厂设置的方法
  18. 【限流算法】java实现固定时间窗口算法
  19. java解惑之字符之谜(谜题17)
  20. 基恩士KV06N程序 基恩士KV06N,昆仑通态触摸屏 全自动LED划线点装机,PLC本体伺服轴控制

热门文章

  1. 三维可视化技术在超超临界锅炉防磨防爆中的应用
  2. 需求与商业模式创新-需求6-涉众分析与硬采样
  3. php一张纸对折,给你一张纸,你最多能对折几次?
  4. Nexus上传jar问题【史上最全,亲测可用】
  5. 计算机组成原理习题 第七章 外围设备
  6. 动态规划求解多段图问题
  7. Js 加载事件(onload) 可以作用的标签
  8. python脚本一键抓考试资料网答案
  9. Ubuntu(WSL)安装3b1b的manim
  10. java计算机毕业设计某山区环境保护监督管理平台源程序+mysql+系统+lw文档+远程调试