如何用SSE将__m128i寄存器乘浮点因子实现图像灰度化?
SSE实现RGBA转灰度的修改方案
嘿,第一次上手SSE就能搞定通道拆分已经很厉害了!咱们直接把现有代码改成灰度化处理,核心是实现fY(R, G, B)=R×0.29891 + G×0.58661 + B×0.11448这个加权公式,同时保留Alpha通道(如果需要的话)。下面一步步来调整你的代码:
核心思路梳理
你的现有代码已经完成了通道拆分,但现在需要把「通道直接相加」换成「加权乘法→累加→饱和转8位」,还要注意定点数运算的精度问题(这是你当前代码的一个关键小问题)。
修改后的完整代码
__m128i vecSrc = _mm_loadu_si128((__m128i*) &src[srcIndex]); // 1. 定义定点数权重:把浮点系数转成16.16定点格式(乘以0x10000=65536) const __m128i factorR = _mm_set1_epi16((short)(0.29891 * 65536.0f)); const __m128i factorG = _mm_set1_epi16((short)(0.58661 * 65536.0f)); const __m128i factorB = _mm_set1_epi16((short)(0.11448 * 65536.0f)); __m128i zero = _mm_setzero_si128(); // 2. 把8位通道扩展成16位整数(方便定点乘法) // vectSrcLow: 前8个字节(4个RGBA像素的R/G/B/A)转成8个epi16 __m128i vectSrcLow = _mm_cvtepu8_epi16(vecSrc); // vectSrcHigh: 后8个字节(另外4个RGBA像素的R/G/B/A)转成8个epi16 __m128i vectSrcHigh = _mm_unpackhi_epi8(vecSrc, zero); // 3. 提取每个通道的16位值(优化原来的mask乘法方式,用shuffle更高效) // 掩码说明:每个epi16位置对应要提取的通道,比如R通道是每个RGBA的第0个字节 __m128i shuffleR = _mm_setr_epi16(0, 4, 8, 12, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF); // 前4个像素的R __m128i shuffleG = _mm_setr_epi16(1, 5, 9, 13, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF); // 前4个像素的G __m128i shuffleB = _mm_setr_epi16(2, 6, 10, 14, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF); // 前4个像素的B __m128i shuffleA = _mm_setr_epi16(3, 7, 11, 15, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF); // 前4个像素的A // 提取前4个像素的通道 __m128i vecR_L = _mm_shuffle_epi8(vectSrcLow, shuffleR); __m128i vecG_L = _mm_shuffle_epi8(vectSrcLow, shuffleG); __m128i vecB_L = _mm_shuffle_epi8(vectSrcLow, shuffleB); __m128i vecA_L = _mm_shuffle_epi8(vectSrcLow, shuffleA); // 提取后4个像素的通道(掩码对应后4个RGBA的位置) __m128i shuffleR_H = _mm_setr_epi16(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0, 4, 8, 12); __m128i shuffleG_H = _mm_setr_epi16(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 1, 5, 9, 13); __m128i shuffleB_H = _mm_setr_epi16(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 2, 6, 10, 14); __m128i shuffleA_H = _mm_setr_epi16(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 3, 7, 11, 15); __m128i vecR_H = _mm_shuffle_epi8(vectSrcHigh, shuffleR_H); __m128i vecG_H = _mm_shuffle_epi8(vectSrcHigh, shuffleG_H); __m128i vecB_H = _mm_shuffle_epi8(vectSrcHigh, shuffleB_H); __m128i vecA_H = _mm_shuffle_epi8(vectSrcHigh, shuffleA_H); // 4. 加权计算:用_mm_mulhi_epi16取定点乘法的高16位(这是关键!原来的_mullo_epi16会丢精度) __m128i grayR_L = _mm_mulhi_epi16(vecR_L, factorR); __m128i grayG_L = _mm_mulhi_epi16(vecG_L, factorG); __m128i grayB_L = _mm_mulhi_epi16(vecB_L, factorB); __m128i grayR_H = _mm_mulhi_epi16(vecR_H, factorR); __m128i grayG_H = _mm_mulhi_epi16(vecG_H, factorG); __m128i grayB_H = _mm_mulhi_epi16(vecB_H, factorB); // 5. 累加三个通道的加权值,得到灰度值(16位) __m128i gray_L = _mm_add_epi16(_mm_add_epi16(grayR_L, grayG_L), grayB_L); __m128i gray_H = _mm_add_epi16(_mm_add_epi16(grayR_H, grayG_H), grayB_H); // 6. 把16位灰度值饱和转成8位,同时保留Alpha通道 // 先把灰度值和Alpha值合并:每个像素的R=G=B=灰度,A=原Alpha __m128i grayPacked_L = _mm_packus_epi16(gray_L, gray_L); // 把16位灰度转成8位,重复两次(给R和G) grayPacked_L = _mm_packus_epi16(grayPacked_L, gray_L); // 再重复一次给B,然后和Alpha合并 // 合并Alpha:把原Alpha放到每个像素的第3个字节 __m128i alphaPacked_L = _mm_packus_epi16(vecA_L, vecA_L); alphaPacked_L = _mm_packus_epi16(alphaPacked_L, vecA_L); // 用shuffle把灰度值放到R/G/B位置,Alpha放到A位置 __m128i maskRGBA_L = _mm_setr_epi8(0,0,0,1, 4,4,4,5, 8,8,8,9, 12,12,12,13); __m128i rgba_L = _mm_shuffle_epi8(_mm_or_si128(grayPacked_L, alphaPacked_L), maskRGBA_L); // 对后4个像素做同样的处理 __m128i grayPacked_H = _mm_packus_epi16(gray_H, gray_H); grayPacked_H = _mm_packus_epi16(grayPacked_H, gray_H); __m128i alphaPacked_H = _mm_packus_epi16(vecA_H, vecA_H); alphaPacked_H = _mm_packus_epi16(alphaPacked_H, vecA_H); __m128i rgba_H = _mm_shuffle_epi8(_mm_or_si128(grayPacked_H, alphaPacked_H), maskRGBA_L); // 7. 把前后8个字节的结果合并成128位寄存器 __m128i resultVect = _mm_or_si128(_mm_unpacklo_epi8(rgba_L, rgba_H), _mm_unpackhi_epi8(rgba_L, rgba_H)); // 8. 存回原数组 _mm_storeu_si128((__m128i*)&src[srcIndex], resultVect);
关键修改点说明
- 定点乘法的正确姿势:原来的
_mm_mullo_epi16取的是乘法结果的低16位,而我们用的是16.16定点数,相乘后需要取高16位(相当于除以65536),所以必须用_mm_mulhi_epi16,否则结果会完全错误。 - 通道提取优化:用
_mm_shuffle_epi8直接提取通道比原来的掩码乘法更高效,减少了不必要的运算。 - 饱和转换:用
_mm_packus_epi16把16位的累加结果转成8位时,会自动处理超过255的情况(饱和到255),避免溢出。 - 保留Alpha通道:如果不需要Alpha,可以直接把灰度值填充到所有三个颜色通道,忽略Alpha即可。
额外小提示
如果你的CPU支持SSE4.1,可以用_mm_shuffle_epi32或者_mm_blend_epi16进一步优化通道提取步骤,不过上面的代码兼容SSE2,适配性更广。
内容的提问来源于stack exchange,提问作者CoffeRight
相关产品推荐
相关产品推荐

