图像处理:simd快速灰度化

一、引言

一直想着找个机会学习记录一下使用simd指令集来进行图像处理,这样可以高效的利用cpu,实现图像处理的加速。

二、图像灰度化

图像灰度化其实就是将三通道或者多通道转为单通道的过程。

拿三通道图像为例,三通道为B、G、R通道,分别代表不同的颜色,也就是我们说的彩色图像,通过对每个通道的像素值进行加权融合为一个数值,最后存储起来,就成为了一个单通道图像,也就是灰度图像。

有的人习惯使用平均权重,也就是

但是研究方向,反而是不同权重呈现的灰度效果最佳

三、代码实现

cpp 复制代码
#if defined(_WIN32) || defined(_M_X64) || defined(__x86_64__)
    #define USE_SSE
    #include <immintrin.h>
#elif defined(__aarch64__) || defined(_M_ARM64)
    #define USE_NEON
    #include <arm_neon.h>
#endif

void convertToGrayscaleSIMD(const cv::Mat& input, cv::Mat& output) {
    if (input.empty()) {
        return;
    }

    // 确保输入是3通道的BGR图像
    CV_Assert(input.type() == CV_8UC3);

    // 创建输出灰度图像
    output.create(input.size(), CV_8UC1);

    const int width = input.cols;
    const int height = input.rows;
    const int channels = input.channels();

#ifdef USE_SSE
    // SSE版本 - 每次处理16个像素
    const int blockSize = 16;

    for (int y = 0; y < height; ++y) {
        const uchar* src = input.ptr<uchar>(y);
        uchar* dst = output.ptr<uchar>(y);

        int x = 0;

        // 使用SSE处理16个像素的块
        for (; x <= width - blockSize; x += blockSize) {
            // 加载48个字节(16个像素的BGR数据)
            __m128i bgr0 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src + x * channels));
            __m128i bgr1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src + x * channels + 16));
            __m128i bgr2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src + x * channels + 32));

            // 解交织B、G、R分量
            __m128i b0 = _mm_shuffle_epi8(bgr0, _mm_setr_epi8(0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
            __m128i b1 = _mm_shuffle_epi8(bgr1, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14, -1, -1, -1, -1, -1));
            __m128i b2 = _mm_shuffle_epi8(bgr2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 4, 7, 10, 13));
            __m128i b = _mm_or_si128(_mm_or_si128(b0, b1), b2);

            __m128i g0 = _mm_shuffle_epi8(bgr0, _mm_setr_epi8(1, 4, 7, 10, 13, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
            __m128i g1 = _mm_shuffle_epi8(bgr1, _mm_setr_epi8(-1, -1, -1, -1, -1, 0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1));
            __m128i g2 = _mm_shuffle_epi8(bgr2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14));
            __m128i g = _mm_or_si128(_mm_or_si128(g0, g1), g2);

            __m128i r0 = _mm_shuffle_epi8(bgr0, _mm_setr_epi8(2, 5, 8, 11, 14, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
            __m128i r1 = _mm_shuffle_epi8(bgr1, _mm_setr_epi8(-1, -1, -1, -1, -1, 1, 4, 7, 10, 13, -1, -1, -1, -1, -1, -1));
            __m128i r2 = _mm_shuffle_epi8(bgr2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 3, 6, 9, 12, 15));
            __m128i r = _mm_or_si128(_mm_or_si128(r0, r1), r2);

            // 转换为16位整数
            __m128i b_lo = _mm_cvtepu8_epi16(b);
            __m128i g_lo = _mm_cvtepu8_epi16(g);
            __m128i r_lo = _mm_cvtepu8_epi16(r);
            __m128i b_hi = _mm_cvtepu8_epi16(_mm_unpackhi_epi64(b, b));
            __m128i g_hi = _mm_cvtepu8_epi16(_mm_unpackhi_epi64(g, g));
            __m128i r_hi = _mm_cvtepu8_epi16(_mm_unpackhi_epi64(r, r));

            // 计算灰度值:Gray = (77 * R + 150 * G + 29 * B) >> 8
            __m128i gray_lo = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(
                _mm_mullo_epi16(r_lo, _mm_set1_epi16(77)),
                _mm_mullo_epi16(g_lo, _mm_set1_epi16(150))),
                _mm_mullo_epi16(b_lo, _mm_set1_epi16(29))), 8);

            __m128i gray_hi = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(
                _mm_mullo_epi16(r_hi, _mm_set1_epi16(77)),
                _mm_mullo_epi16(g_hi, _mm_set1_epi16(150))),
                _mm_mullo_epi16(b_hi, _mm_set1_epi16(29))), 8);

            // 打包回8位整数
            __m128i gray = _mm_packus_epi16(gray_lo, gray_hi);

            // 存储结果
            _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + x), gray);
        }

        // 处理剩余像素
        for (; x < width; ++x) {
            const uchar* p = src + x * channels;
            dst[x] = (77 * p[2] + 150 * p[1] + 29 * p[0]) >> 8;
        }
    }

#elif defined(USE_NEON)
    // NEON版本 - 每次处理8个像素
    const int blockSize = 8;

    for (int y = 0; y < height; ++y) {
        const uint8_t* src = input.ptr<uint8_t>(y);
        uint8_t* dst = output.ptr<uint8_t>(y);

        int x = 0;

        // 使用NEON处理8个像素的块
        for (; x <= width - blockSize; x += blockSize) {
            // 加载24个字节(8个像素的BGR数据)
            uint8x8x3_t bgr = vld3_u8(src + x * channels);

            // 转换为16位整数
            uint16x8_t r16 = vmovl_u8(bgr.val[2]); // R分量
            uint16x8_t g16 = vmovl_u8(bgr.val[1]); // G分量
            uint16x8_t b16 = vmovl_u8(bgr.val[0]); // B分量

            // 计算灰度值:Gray = (77 * R + 150 * G + 29 * B) >> 8
            uint16x8_t gray16 = vaddq_u16(
                vaddq_u16(
                    vmull_u8(vget_low_u8(bgr.val[2]), vdup_n_u8(77)),
                    vmull_u8(vget_low_u8(bgr.val[1]), vdup_n_u8(150))
                ),
                vmull_u8(vget_low_u8(bgr.val[0]), vdup_n_u8(29))
            );
            
            // 右移8位并转换为8位
            uint8x8_t gray = vshrn_n_u16(gray16, 8);

            // 存储结果
            vst1_u8(dst + x, gray);
        }

        // 处理剩余像素
        for (; x < width; ++x) {
            const uchar* p = src + x * channels;
            dst[x] = (77 * p[2] + 150 * p[1] + 29 * p[0]) >> 8;
        }
    }

#else
    // 通用版本(无SIMD)
    for (int y = 0; y < height; ++y) {
        const uchar* src = input.ptr<uchar>(y);
        uchar* dst = output.ptr<uchar>(y);
        
        for (int x = 0; x < width; ++x) {
            const uchar* p = src + x * channels;
            dst[x] = (77 * p[2] + 150 * p[1] + 29 * p[0]) >> 8;
        }
    }
#endif
}

四、指令集解析

cpp 复制代码
// 平台检测宏
#if defined(_WIN32) || defined(_M_X64) || defined(__x86_64__)
    #define USE_SSE
    #include <immintrin.h>
#elif defined(__aarch64__) || defined(_M_ARM64)
    #define USE_NEON
    #include <arm_neon.h>
#endif

1.这部分代码通过预处理器宏检测目标平台:

  • 如果是Windows或x86/x64架构,定义USE_SSE并包含SSE指令集头文件

  • 如果是ARM64架构,定义USE_NEON并包含NEON指令集头文件

2.const int blockSize = 16;

SSE寄存器是128位,可存储16个像素,一个uchar类型像素的8bit。

3.

cpp 复制代码
__m128i bgr0 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src + x * channels));
__m128i bgr1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src + x * channels + 16));
__m128i bgr2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(src + x * channels + 32));

_mm_loadu_si128(),用于将128位数据从内存加载到SSE寄存器。

除此之外,还有一个类似的函数_mm_load_si128()。这两个函数的区别在于load和loadu。

_mm_load_si128()函数要求内存地址必须16字节对齐(即地址是16的倍数)。若地址未对齐,调用此函数会导致未定义行为(程序崩溃或数据错误)。

_mm_loadu_si128()允许内存地址未对齐(无需是16的倍数)。

牺牲少量性能换取灵活性,适用于无法保证对齐的场景。


reinterpret_cast<const __m128i*>(),强制转换功能。

  • static_cast<类型说明符>(表达式)
  • dynamic_cast<类型说明符>(表达式)
  • const_cast<类型说明符>(表达式)
  • reinterpret_cast<类型说明符>(表达式)

与这些类似

4.

cpp 复制代码
// 解交织B、G、R分量
__m128i b0 = _mm_shuffle_epi8(bgr0, _mm_setr_epi8(0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
__m128i b1 = _mm_shuffle_epi8(bgr1, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14, -1, -1, -1, -1, -1));
__m128i b2 = _mm_shuffle_epi8(bgr2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 4, 7, 10, 13));
__m128i b = _mm_or_si128(_mm_or_si128(b0, b1), b2);

_mm_shuffle_epi8()函数,这个函数是根据掩码mask(_mm_setr_epi8()),获取对应的bgr0中的元素。

bgr0中存储的为(b0,g0,r0,b1,g1,r1,b2,g2,r2,b3,g3,r3,b4,g4,r4,b5)

经过这个shuffle函数后,依据mask(0,3,6,9,12,15,-1......),分别取第0,3,6...位置的bgr0元素,即为(b0,b1,b2,b3,b4,b5,0,0,0,.......)

剩余的也是类似处理流程。

_mm_or_si128(b0,b1),可以理解为将两个数值合并异或操作。最终获取全部的B通道元素。

5.

cpp 复制代码
// 转换为16位整数
            __m128i b_lo = _mm_cvtepu8_epi16(b);
            __m128i g_lo = _mm_cvtepu8_epi16(g);
            __m128i r_lo = _mm_cvtepu8_epi16(r);
            __m128i b_hi = _mm_cvtepu8_epi16(_mm_unpackhi_epi64(b, b));
            __m128i g_hi = _mm_cvtepu8_epi16(_mm_unpackhi_epi64(g, g));
            __m128i r_hi = _mm_cvtepu8_epi16(_mm_unpackhi_epi64(r, r));

这里是为了转到16位,因为8位元素范围是0-255,后面要经过乘法运算,这个范围是会越界的。所以转为16位,128的SSE只能存储8个16位元素,所以分别将原数据的低8位与高8位分别存储下来。

_mm_unpackhi_epi64(b, b),这个函数就是获取高8位的函数。然后将低、高8位分别存储。

6.

cpp 复制代码
   // 计算灰度值:Gray = (77 * R + 150 * G + 29 * B) >> 8
            __m128i gray_lo = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(
                _mm_mullo_epi16(r_lo, _mm_set1_epi16(77)),
                _mm_mullo_epi16(g_lo, _mm_set1_epi16(150))),
                _mm_mullo_epi16(b_lo, _mm_set1_epi16(29))), 8);

这里就是计算灰度值了,需要先将权重0.144等分别×256。最后再进行移位运算,移8位与÷256有着一样的结果。

_mm_add_epi16(),相加运算

_mm_mullo_epi16(),相乘运算

_mm_srli_epi16(),可以理解为移位运算

7.

cpp 复制代码
     // 打包回8位整数
            __m128i gray = _mm_packus_epi16(gray_lo, gray_hi);

            // 存储结果
            _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + x), gray);

_mm_packus_spi16(),将两个 128 位寄存器中的 16 位有符号整数压缩为 8 位无符号整数,并使用饱和运算来处理溢出。

_mm_storeu_si128(),用于将128位SIMD寄存器(__m128i类型)中的数据非对齐存储到内存地址.

操作类型 内在函数 速度

对齐存储 _mm_store_si128 更快

非对齐存储 _mm_storeu_si128 稍慢

五、结果对比

采用opencv自带的函数耗时4ms左右,而采用自己实现的simd加速方法,速度将近提升了100倍。

相关推荐
Yao.Li8 分钟前
PVN3D ORT CUDA Custom Ops 实现与联调记录
人工智能·3d·具身智能
诺伦11 分钟前
LocalClaw 在智能制造的新机会:6部门AI+电商政策下的工厂AI升级方案
人工智能·制造
小陈工2 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
墨染天姬6 小时前
【AI】端侧AIBOX可以部署哪些智能体
人工智能
AI成长日志6 小时前
【Agentic RL】1.1 什么是Agentic RL:从传统RL到智能体学习
人工智能·学习·算法
2501_948114247 小时前
2026年大模型API聚合平台技术评测:企业级接入层的治理演进与星链4SAPI架构观察
大数据·人工智能·gpt·架构·claude
小小工匠7 小时前
LLM - awesome-design-md 从 DESIGN.md 到“可对话的设计系统”:用纯文本驱动 AI 生成一致 UI 的新范式
人工智能·ui
黎阳之光7 小时前
黎阳之光:视频孪生领跑者,铸就中国数字科技全球竞争力
大数据·人工智能·算法·安全·数字孪生
小超同学你好7 小时前
面向 LLM 的程序设计 6:Tool Calling 的完整生命周期——从定义、决策、执行到观测回注
人工智能·语言模型
智星云算力7 小时前
本地GPU与租用GPU混合部署:混合算力架构搭建指南
人工智能·架构·gpu算力·智星云·gpu租用