SSE图像算法优化系列十五:YUV/XYZ和RGB空间相互转化的极速实现(此后老板不用再担心算法转到其他空间通道的耗时了)。
在颜色空间系列1: RGB和CIEXYZ颜色空间的转换及相关优化和颜色空间系列3: RGB和YUV颜色空间的转换及优化算法两篇文章中我们给出了两种不同的颜色空间的相互转换之间的快速算法的实现代码,但是那个是C#版本的,为了比较方便,我们这里提供C版本的代码,以RGB转到YUV空间的代码为例:
void RGBToYUV(unsigned char *RGB, unsigned char *Y, unsigned char *U, unsigned char *V, int Width, int Height, int Stride)
{
const int Shift = ;
const int HalfV = << (Shift - );
const int Y_B_WT = 0.114f * ( << Shift), Y_G_WT = 0.587f * ( << Shift), Y_R_WT = ( << Shift) - Y_B_WT - Y_G_WT;
const int U_B_WT = 0.436f * ( << Shift), U_G_WT = -0.28886f * ( << Shift), U_R_WT = -(U_B_WT + U_G_WT);
const int V_B_WT = -0.10001 * ( << Shift), V_G_WT = -0.51499f * ( << Shift), V_R_WT = -(V_B_WT + V_G_WT);
for (int YY = ; YY < Height; YY++)
{
unsigned char *LinePS = RGB + YY * Stride;
unsigned char *LinePY = Y + YY * Width;
unsigned char *LinePU = U + YY * Width;
unsigned char *LinePV = V + YY * Width;
for (int XX = ; XX < Width; XX++, LinePS += )
{
int Blue = LinePS[], Green = LinePS[], Red = LinePS[];
LinePY[XX] = (Y_B_WT * Blue + Y_G_WT * Green + Y_R_WT * Red + HalfV) >> Shift;
LinePU[XX] = ((U_B_WT * Blue + U_G_WT * Green + U_R_WT * Red + HalfV) >> Shift) + ;
LinePV[XX] = ((V_B_WT * Blue + V_G_WT * Green + V_R_WT * Red + HalfV) >> Shift) + ;
}
}
}
上述代码和颜色空间系列3: RGB和YUV颜色空间的转换及优化算法中的有所不同,但是应该说更加合理,注意Y_R_WT/U_R_WT/V_R_WT 的书写方式有所不同,这主要是为了保证定点化后的系数不会放大误差,同时注意计算时每一项还增加了HalfV值,这主要是为了保证对计算结果的四舍五入。
现代的CPU都很强劲,找了一台普通的I5电脑测试,1080P的图平均转换时间为10ms,大概能得到100fps的转换速率,但是从实际应用来讲,在很多场合这个耗时还是多了点,如果需要处理1080P这样的高清视频,考虑到综合因素,单帧的总处理时间不宜超过30ms,所以还有必要进一步提高这个算法的速度。
凭直觉,上述代码用GPU实现应该有很高的并行性,不知道最后速度会如何。
俺不会GPU,只会用SSE进行优化,先试一试把,一种最直接最朴实的写法如下:
void RGBToYUVSSE(unsigned char *RGB, unsigned char *Y, unsigned char *U, unsigned char *V, int Width, int Height, int Stride)
{
const int Shift = ;
const int HalfV = << (Shift - );
const int Y_B_WT = 0.114f * ( << Shift), Y_G_WT = 0.587f * ( << Shift), Y_R_WT = ( << Shift) - Y_B_WT - Y_G_WT;
const int U_B_WT = 0.436f * ( << Shift), U_G_WT = -0.28886f * ( << Shift), U_R_WT = -(U_B_WT + U_G_WT);
const int V_B_WT = -0.10001 * ( << Shift), V_G_WT = -0.51499f * ( << Shift), V_R_WT = -(V_B_WT + V_G_WT); __m128i Weight_YB = _mm_set1_epi32(Y_B_WT), Weight_YG = _mm_set1_epi32(Y_G_WT), Weight_YR = _mm_set1_epi32(Y_R_WT);
__m128i Weight_UB = _mm_set1_epi32(U_B_WT), Weight_UG = _mm_set1_epi32(U_G_WT), Weight_UR = _mm_set1_epi32(U_R_WT);
__m128i Weight_VB = _mm_set1_epi32(V_B_WT), Weight_VG = _mm_set1_epi32(V_G_WT), Weight_VR = _mm_set1_epi32(V_R_WT);
__m128i C128 = _mm_set1_epi32();
__m128i Half = _mm_set1_epi32(HalfV);
__m128i Zero = _mm_setzero_si128(); const int BlockSize = , Block = Width / BlockSize;
for (int YY = ; YY < Height; YY++)
{
unsigned char *LinePS = RGB + YY * Stride;
unsigned char *LinePY = Y + YY * Width;
unsigned char *LinePU = U + YY * Width;
unsigned char *LinePV = V + YY * Width;
for (int XX = ; XX < Block * BlockSize; XX += BlockSize, LinePS += BlockSize * )
{
__m128i Src1, Src2, Src3, Blue, Green, Red; Src1 = _mm_loadu_si128((__m128i *)(LinePS + ));
Src2 = _mm_loadu_si128((__m128i *)(LinePS + ));
Src3 = _mm_loadu_si128((__m128i *)(LinePS + )); Blue = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , , -, -, -, -, -, -, -, -, -, -));
Blue = _mm_or_si128(Blue, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, -, , , , , , -, -, -, -, -)));
Blue = _mm_or_si128(Blue, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, -, , , , , ))); Green = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , -, -, -, -, -, -, -, -, -, -, -));
Green = _mm_or_si128(Green, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, , , , , , , -, -, -, -, -)));
Green = _mm_or_si128(Green, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, -, , , , , ))); Red = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , -, -, -, -, -, -, -, -, -, -, -));
Red = _mm_or_si128(Red, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, , , , , , -, -, -, -, -, -)));
Red = _mm_or_si128(Red, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, , , , , , ))); // 以上操作把16个连续像素的像素顺序由 B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R B G R
// 更改为适合于SIMD指令处理的连续序列 B B B B B B B B B B B B B B B B G G G G G G G G G G G G G G G G R R R R R R R R R R R R R R R R
// 并分别保存于三个SSE变量中,并且没任何的冗余数据。 __m128i Blue16L = _mm_unpacklo_epi8(Blue, Zero);
__m128i Blue16H = _mm_unpackhi_epi8(Blue, Zero);
__m128i Blue32LL = _mm_unpacklo_epi16(Blue16L, Zero);
__m128i Blue32LH = _mm_unpackhi_epi16(Blue16L, Zero);
__m128i Blue32HL = _mm_unpacklo_epi16(Blue16H, Zero);
__m128i Blue32HH = _mm_unpackhi_epi16(Blue16H, Zero); __m128i Green16L = _mm_unpacklo_epi8(Green, Zero);
__m128i Green16H = _mm_unpackhi_epi8(Green, Zero);
__m128i Green32LL = _mm_unpacklo_epi16(Green16L, Zero);
__m128i Green32LH = _mm_unpackhi_epi16(Green16L, Zero);
__m128i Green32HL = _mm_unpacklo_epi16(Green16H, Zero);
__m128i Green32HH = _mm_unpackhi_epi16(Green16H, Zero); __m128i Red16L = _mm_unpacklo_epi8(Red, Zero);
__m128i Red16H = _mm_unpackhi_epi8(Red, Zero);
__m128i Red32LL = _mm_unpacklo_epi16(Red16L, Zero);
__m128i Red32LH = _mm_unpackhi_epi16(Red16L, Zero);
__m128i Red32HL = _mm_unpacklo_epi16(Red16H, Zero);
__m128i Red32HH = _mm_unpackhi_epi16(Red16H, Zero); // 以上操作把三个SSE变量里的字节数据分别提取到12个包含4个int类型的数据的SSE变量里,以便后续的乘积操作不溢出 __m128i LL_Y = _mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32LL, Weight_YB), _mm_add_epi32(_mm_mullo_epi32(Green32LL, Weight_YG), _mm_mullo_epi32(Red32LL, Weight_YR))),Half), Shift);
__m128i LH_Y = _mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32LH, Weight_YB), _mm_add_epi32(_mm_mullo_epi32(Green32LH, Weight_YG), _mm_mullo_epi32(Red32LH, Weight_YR))), Half), Shift);
__m128i HL_Y = _mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32HL, Weight_YB), _mm_add_epi32(_mm_mullo_epi32(Green32HL, Weight_YG), _mm_mullo_epi32(Red32HL, Weight_YR))), Half), Shift);
__m128i HH_Y = _mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32HH, Weight_YB), _mm_add_epi32(_mm_mullo_epi32(Green32HH, Weight_YG), _mm_mullo_epi32(Red32HH, Weight_YR))), Half), Shift);
_mm_storeu_si128((__m128i*)(LinePY + XX), _mm_packus_epi16(_mm_packus_epi32(LL_Y, LH_Y), _mm_packus_epi32(HL_Y, HH_Y))); // 4个包含4个int类型的SSE变量重新打包为1个包含16个字节数据的SSE变量
// 以上操作完成:Y[0 - 15] = (Y_B_WT * Blue[0 - 15]+ Y_G_WT * Green[0 - 15] + Y_R_WT * Red[0 - 15] + HalfV) >> Shift; __m128i LL_U = _mm_add_epi32(_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32LL, Weight_UB), _mm_add_epi32(_mm_mullo_epi32(Green32LL, Weight_UG), _mm_mullo_epi32(Red32LL, Weight_UR))), Half), Shift), C128);
__m128i LH_U = _mm_add_epi32(_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32LH, Weight_UB), _mm_add_epi32(_mm_mullo_epi32(Green32LH, Weight_UG), _mm_mullo_epi32(Red32LH, Weight_UR))), Half), Shift), C128);
__m128i HL_U = _mm_add_epi32(_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32HL, Weight_UB), _mm_add_epi32(_mm_mullo_epi32(Green32HL, Weight_UG), _mm_mullo_epi32(Red32HL, Weight_UR))), Half), Shift), C128);
__m128i HH_U = _mm_add_epi32(_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32HH, Weight_UB), _mm_add_epi32(_mm_mullo_epi32(Green32HH, Weight_UG), _mm_mullo_epi32(Red32HH, Weight_UR))), Half), Shift), C128);
_mm_storeu_si128((__m128i*)(LinePU + XX), _mm_packus_epi16(_mm_packus_epi32(LL_U, LH_U), _mm_packus_epi32(HL_U, HH_U)));
// 以上操作完成:U[0 - 15] = ((U_B_WT * Blue[0 - 15]+ U_G_WT * Green[0 - 15] + U_R_WT * Red[0 - 15] + HalfV) >> Shift) + 128; __m128i LL_V = _mm_add_epi32(_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32LL, Weight_VB), _mm_add_epi32(_mm_mullo_epi32(Green32LL, Weight_VG), _mm_mullo_epi32(Red32LL, Weight_VR))), Half), Shift), C128);
__m128i LH_V = _mm_add_epi32(_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32LH, Weight_VB), _mm_add_epi32(_mm_mullo_epi32(Green32LH, Weight_VG), _mm_mullo_epi32(Red32LH, Weight_VR))), Half), Shift), C128);
__m128i HL_V = _mm_add_epi32(_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32HL, Weight_VB), _mm_add_epi32(_mm_mullo_epi32(Green32HL, Weight_VG), _mm_mullo_epi32(Red32HL, Weight_VR))), Half), Shift), C128);
__m128i HH_V = _mm_add_epi32(_mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(_mm_mullo_epi32(Blue32HH, Weight_VB), _mm_add_epi32(_mm_mullo_epi32(Green32HH, Weight_VG), _mm_mullo_epi32(Red32HH, Weight_VR))), Half), Shift), C128);
_mm_storeu_si128((__m128i*)(LinePV + XX), _mm_packus_epi16(_mm_packus_epi32(LL_V, LH_V), _mm_packus_epi32(HL_V, HH_V)));
// 以上操作完成:V[0 - 15] = ((V_B_WT * Blue[0 - 15]+ V_G_WT * Green[0 - 15] + V_R_WT * Red[0 - 15] + HalfV) >> Shift) + 128;
}
for (int XX = Block * BlockSize; XX < Width; XX++, LinePS += ) // 每行剩余的像素按照正常的方式处理
{
int Blue = LinePS[], Green = LinePS[], Red = LinePS[];
LinePY[XX] = (Y_B_WT * Blue + Y_G_WT * Green + Y_R_WT * Red + HalfV) >> Shift;
LinePU[XX] = ((U_B_WT * Blue + U_G_WT * Green + U_R_WT * Red + HalfV) >> Shift) + ;
LinePV[XX] = ((V_B_WT * Blue + V_G_WT * Green + V_R_WT * Red + HalfV) >> Shift) + ;
}
}
}
(显示器不是宽屏的朋友请下载代码在VS里浏览,不然会看晕的)。
基本上就是按照普通的C的代码翻译到SSE版本,按F5编译执行,测试速度1080P的平均每帧约4.5ms,大概是普通C语言的2.2倍,为什么没到四倍,可能是我写的不到位,但是我认为主要的原因还是这里面有蛮多的数据拆解和数据类型的转换占用了一定的CPU周期。但总的来说还是有相当大的进步的。
稍微分析下程序以帮助不太懂SSE的朋友。
前面的几句加载和shffule语句主要是把BGR格式的图像数据拆解为单独的通道数据,其详细的原理见(真心描述的清楚):
SSE图像算法优化系列八:自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。
中间一部分unpack代码主要的主要作用是把字节数据扩展到32位的int类型数,因为后面的乘法结果是只能用32位才能表达完整的。
那么后面的_mm_srai_epi32、_mm_add_epi32、_mm_mullo_epi32等就是直接的C代码的翻译了,只是把普通的晕算法用函数名代替了而已。
从理论上说,上述代码中的 const int Shift = 15;这个常量最大可以取到23,这个与最后的计算精度有一定的关系,但是也是影响不大。
2.2倍的提速,就这样放弃了吗,不,这不科学,也不是我Imageshop的风格,继续加油。
每当这个时候,我总是习惯性的再翻一遍Intel的SSE帮助文档,也许那个里面还藏着其他的葵花宝典。
当我们定位到_mm_madd_epi16这个函数时,虽然他只比普通的add函数多了一个m,但是当看到他下面对该函数功能的解释时,那真的眼前一亮,看下MSDN的说明:
Multiplies the 8 signed 16-bit integers from a by the 8 signed 16-bit integers from b.
__m128i _mm_madd_epi16 (__m128i a, __m128i b);
r0 := (a0 * b0) + (a1 * b1)
r1 := (a2 * b2) + (a3 * b3)
r2 := (a4 * b4) + (a5 * b5)
r3 := (a6 * b6) + (a7 * b7)
注意到这个函数内部实际执行了8次乘法和4次加法,唯一区别的就是参数要求是signed short类型,那能应用到我们的例子中吗?
首先我们来看Y变量的计算式:
LinePY[XX] = (Y_B_WT * Blue + Y_G_WT * Green + Y_R_WT * Red + HalfV) >> Shift;
LinePU[XX] = ((U_B_WT * Blue + U_G_WT * Green + U_R_WT * Red + HalfV) >> Shift) + 128;
LinePV[XX] = ((V_B_WT * Blue + V_G_WT * Green + V_R_WT * Red + HalfV) >> Shift) + 128;
注意到其中的HalfV = 1 << (Shift - 1),稍微改写一下,我们得到:
LinePY[XX] = (Y_B_WT * Blue + Y_G_WT * Green + Y_R_WT * Red + 1 * HalfV) >> Shift;
LinePU[XX] = (U_B_WT * Blue + U_G_WT * Green + U_R_WT * Red + 257 * HalfV) >> Shift;
LinePV[XX] = (V_B_WT * Blue + V_G_WT * Green + V_R_WT * Red + 257 * HalfV) >> Shift;
至于算式中的257是怎么得到的,如果你不明白,回家问下你正在上小学的儿子吧。
这样改写目的很明显,我们正好把括号内的计算配对成了2组可利用_mm_madd_epi16函数进行计算的形式,那么他在数据类型方面能符合_mm_madd_epi16的需求吗,或者说我们能改造他使得我们的数据满足要求不。
_mm_madd_epi16的文档中有提到其作用是:Multiplies the 8 signed 16-bit integers from a by the 8 signed 16-bit integers from b,即参与计算的必须是带符号的16位数据,首先图像数范围[0,255],肯定满足要求,同时注意到上述系数绝对值都小于1,因此只要(1 << Shift)不大于32768,即Shift最大取值15时,时能够满足需求的,而Shift=15时的精度也足以满足实际的运用需求。
注意我们改写的最后一项,比如257 * HalfV,这里我们可以认为HalfV是该项的权重,257是系数。
还有一点,也是最重要的一点,主要到没有r0 := (a0 * b0) + (a1 * b1)这里的系数是交叉的,比如我们认为变量b里面保存的是交叉的B和G分分量的权重,那么a变量里就应该交叉保存了Blue和Green的值,这时有两个途径:
1、我们上述代码里已经获得了Blue和Green分量的连续排列变量,这个时候只需要使用unpacklo和unpackhi就能分别获取低8位和高8位的交叉结果。
2、注意到获取Blue和Green分量的连续排列变量时是用的shuffle指令,我们也可以采用不同的shuffle系数直接获取交叉后的结果啊。
毫无疑问,第二种方式要快多了。
好,我们贴出这样改进后的结果(似乎不用宽屏也能显示全了):
void RGBToYUVSSE(unsigned char *RGB, unsigned char *Y, unsigned char *U, unsigned char *V, int Width, int Height, int Stride)
{const int Shift = ; // 这里没有绝对值大于1的系数,最大可取2^15次方的放大倍数。
const int HalfV = << (Shift - ); const int Y_B_WT = 0.114f * ( << Shift), Y_G_WT = 0.587f * ( << Shift), Y_R_WT = ( << Shift) - Y_B_WT - Y_G_WT, Y_C_WT = ;
const int U_B_WT = 0.436f * ( << Shift), U_G_WT = -0.28886f * ( << Shift), U_R_WT = -(U_B_WT + U_G_WT), U_C_WT = ;
const int V_B_WT = -0.10001 * ( << Shift), V_G_WT = -0.51499f * ( << Shift), V_R_WT = -(V_B_WT + V_G_WT), V_C_WT = ; __m128i Weight_YBG = _mm_setr_epi16(Y_B_WT, Y_G_WT, Y_B_WT, Y_G_WT, Y_B_WT, Y_G_WT, Y_B_WT, Y_G_WT);
__m128i Weight_YRC = _mm_setr_epi16(Y_R_WT, Y_C_WT, Y_R_WT, Y_C_WT, Y_R_WT, Y_C_WT, Y_R_WT, Y_C_WT);
__m128i Weight_UBG = _mm_setr_epi16(U_B_WT, U_G_WT, U_B_WT, U_G_WT, U_B_WT, U_G_WT, U_B_WT, U_G_WT);
__m128i Weight_URC = _mm_setr_epi16(U_R_WT, U_C_WT, U_R_WT, U_C_WT, U_R_WT, U_C_WT, U_R_WT, U_C_WT);
__m128i Weight_VBG = _mm_setr_epi16(V_B_WT, V_G_WT, V_B_WT, V_G_WT, V_B_WT, V_G_WT, V_B_WT, V_G_WT);
__m128i Weight_VRC = _mm_setr_epi16(V_R_WT, V_C_WT, V_R_WT, V_C_WT, V_R_WT, V_C_WT, V_R_WT, V_C_WT);
__m128i Half = _mm_setr_epi16(, HalfV, , HalfV, , HalfV, , HalfV);
__m128i Zero = _mm_setzero_si128(); int BlockSize = , Block = Width / BlockSize;
for (int YY = ; YY < Height; YY++)
{
unsigned char *LinePS = RGB + YY * Stride;
unsigned char *LinePY = Y + YY * Width;
unsigned char *LinePU = U + YY * Width;
unsigned char *LinePV = V + YY * Width;
for (int XX = ; XX < Block * BlockSize; XX += BlockSize, LinePS += BlockSize * )
{
__m128i Src1 = _mm_loadu_si128((__m128i *)(LinePS + ));
__m128i Src2 = _mm_loadu_si128((__m128i *)(LinePS + ));
__m128i Src3 = _mm_loadu_si128((__m128i *)(LinePS + )); __m128i BGL = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , , , , , , , -, -, -, -, -));
BGL = _mm_or_si128(BGL, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, -, , , , , ))); __m128i BGH = _mm_shuffle_epi8(Src2, _mm_setr_epi8(, , , , , , -, -, -, -, -, -, -, -, -, -));
BGH = _mm_or_si128(BGH, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, , , , , , , , , , ))); __m128i RCL = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, -, , -, , -, , -, , -, -, -, -, -, -, -));
RCL = _mm_or_si128(RCL, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, , -, , -, , -))); __m128i RCH = _mm_shuffle_epi8(Src2, _mm_setr_epi8(, -, , -, -, -, -, -, -, -, -, -, -, -, -, -));
RCH = _mm_or_si128(RCH, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, , -, , -, , -, , -, , -, , -))); __m128i BGLL = _mm_unpacklo_epi8(BGL, Zero);
__m128i BGLH = _mm_unpackhi_epi8(BGL, Zero);
__m128i RCLL = _mm_or_si128(_mm_unpacklo_epi8(RCL, Zero), Half); // HalfV是大于255的,只能在此处进行合并
__m128i RCLH = _mm_or_si128(_mm_unpackhi_epi8(RCL, Zero), Half);
__m128i BGHL = _mm_unpacklo_epi8(BGH, Zero);
__m128i BGHH = _mm_unpackhi_epi8(BGH, Zero);
__m128i RCHL = _mm_or_si128(_mm_unpacklo_epi8(RCH, Zero), Half);
__m128i RCHH = _mm_or_si128(_mm_unpackhi_epi8(RCH, Zero), Half); __m128i Y_LL = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGLL, Weight_YBG), _mm_madd_epi16(RCLL, Weight_YRC)), Shift);
__m128i Y_LH = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGLH, Weight_YBG), _mm_madd_epi16(RCLH, Weight_YRC)), Shift);
__m128i Y_HL = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGHL, Weight_YBG), _mm_madd_epi16(RCHL, Weight_YRC)), Shift);
__m128i Y_HH = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGHH, Weight_YBG), _mm_madd_epi16(RCHH, Weight_YRC)), Shift);
_mm_storeu_si128((__m128i*)(LinePY + XX), _mm_packus_epi16(_mm_packus_epi32(Y_LL, Y_LH), _mm_packus_epi32(Y_HL, Y_HH))); __m128i U_LL = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGLL, Weight_UBG), _mm_madd_epi16(RCLL, Weight_URC)), Shift);
__m128i U_LH = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGLH, Weight_UBG), _mm_madd_epi16(RCLH, Weight_URC)), Shift);
__m128i U_HL = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGHL, Weight_UBG), _mm_madd_epi16(RCHL, Weight_URC)), Shift);
__m128i U_HH = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGHH, Weight_UBG), _mm_madd_epi16(RCHH, Weight_URC)), Shift);
_mm_storeu_si128((__m128i*)(LinePU + XX), _mm_packus_epi16(_mm_packus_epi32(U_LL, U_LH), _mm_packus_epi32(U_HL, U_HH))); __m128i V_LL = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGLL, Weight_VBG), _mm_madd_epi16(RCLL, Weight_VRC)), Shift);
__m128i V_LH = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGLH, Weight_VBG), _mm_madd_epi16(RCLH, Weight_VRC)), Shift);
__m128i V_HL = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGHL, Weight_VBG), _mm_madd_epi16(RCHL, Weight_VRC)), Shift);
__m128i V_HH = _mm_srai_epi32(_mm_add_epi32(_mm_madd_epi16(BGHH, Weight_VBG), _mm_madd_epi16(RCHH, Weight_VRC)), Shift);
_mm_storeu_si128((__m128i*)(LinePV + XX), _mm_packus_epi16(_mm_packus_epi32(V_LL, V_LH), _mm_packus_epi32(V_HL, V_HH))); }
for (int XX = Block * BlockSize; XX < Width; XX++, LinePS += ) // 每行剩余的像素按照正常的方式处理
{
int Blue = LinePS[], Green = LinePS[], Red = LinePS[];
LinePY[XX] = (Y_B_WT * Blue + Y_G_WT * Green + Y_R_WT * Red + Y_C_WT * HalfV) >> Shift;
LinePU[XX] = (U_B_WT * Blue + U_G_WT * Green + U_R_WT * Red + U_C_WT * HalfV) >> Shift;
LinePV[XX] = (V_B_WT * Blue + V_G_WT * Green + V_R_WT * Red + V_C_WT * HalfV) >> Shift;
}
}
}
速度如何呢,结果是2.2ms,相比纯C语言约有4.5倍的提升。
接下来我们简单的谈下YUV到RGB空间的优化,普通的C语言如下:
void YUVToRGB(unsigned char *Y, unsigned char *U, unsigned char *V, unsigned char *RGB, int Width, int Height, int Stride)
{
const int Shift = ;
const int HalfV = << (Shift - );
const int B_Y_WT = << Shift, B_U_WT = 2.03211f * ( << Shift), B_V_WT = ;
const int G_Y_WT = << Shift, G_U_WT = -0.39465f * ( << Shift), G_V_WT = -0.58060f * ( << Shift);
const int R_Y_WT = << Shift, R_U_WT = , R_V_WT = 1.13983 * ( << Shift);
for (int YY = ; YY < Height; YY++)
{
unsigned char *LinePD = RGB + YY * Stride;
unsigned char *LinePY = Y + YY * Width;
unsigned char *LinePU = U + YY * Width;
unsigned char *LinePV = V + YY * Width;
for (int XX = ; XX < Width; XX++, LinePD += )
{
int YV = LinePY[XX], UV = LinePU[XX] - , VV = LinePV[XX] - ;
LinePD[] = ClampToByte(YV + ((B_U_WT * UV + HalfV) >> Shift));
LinePD[] = ClampToByte(YV + ((G_U_WT * UV + G_V_WT * VV + HalfV) >> Shift));
LinePD[] = ClampToByte(YV + ((R_V_WT * VV + HalfV) >> Shift));
}
}
}
虽然内部的加减乘除运算减少了,但是由于需要进行范围的Clamp,因此纯C版本的实际耗时要比RGB转到YUV多,大约在11.5ms左右。
同样的道理,我们还准备使用_mm_madd_epi16来优化,但是这里的情况就稍微复杂了一点。
第一,上面的C代码为了速度考虑,对YV分量没有乘以系数,因为他乘以系数和后面的移位抵消了,但是我们还是考虑把他们展开,以LinePD[0]为例:
LinePD[0] = ClampToByte(YV + ((B_U_WT * UV + HalfV) >> Shift));
展开:
LinePD[0] = ClampToByte(YV * (1 << Shift) + B_U_WT * UV + (1 << (Shift - 1))) >> Shift))
= ClampToByte((YV + 0.5) * (1 << Shift) + B_U_WT * UV) >> Shift))
= ClampToByte((YV * 2 + 1) * ((1 << Shift) >> 1) + B_U_WT * UV) >> Shift))
为什么这样做呢,其目的就是想只用一次_mm_madd_epi16就可以达到结果,此时相当于我们把B_Y_WT = 1 << Shift改为了B_Y_WT = (1 << Shift) >> 1,同时像素值进行了乘2和加1,同样的道理LinePD[1]也可以进行同样的处理。
LinePD[1]展开后你们觉得还要不要说,是RGB到YUV里是相同的道理。
至于ClampToByte,SSE自带了抗饱和处理,packs或者packus直接实现了这个功能,这也是个额外提速的地方。
但是注意,由于YUV到RGB的转换系数里有个数据2.03211f ,大于2了,因此考虑_mm_madd_epi16的应用范围这里的Shift最大也只能取为13了,但是似乎也足够了。编码实测SSE优化后的代码速度也在2.2ms左右,提速比达到了5.2倍,没有比RGB到SSE快,是因为中间增加了一些加法计算。
这样计算下来,两个颜色空间相互转换的时间针对1080P的图,大约需要4.4ms,通常情况下这种转换是为了让算法只在Y通道做处理了,处理完后新的Y通道值和原有图的UV通道组合起来,在转换回RGB图像,这样就把本来需要在RGB三通道都处理的算法简化到一个通道处理了,在对Y通道做的变化不太大的时候,这种处理方式的处理结果和直接在RGB空间做处理时效果差不多,但是如果不考虑空间转换的耗时,那么速度确能提高三倍,这里的空间转化前后耗时4.4ms,对整体帧率的影响就非常小了,比图我是用的基于均方差的磨皮就可以采用这种方式优化从而达到实时的效果。
在如上的实际应用中,考虑到内存占用,我们也没有必要把UV通道的数据计算出来,而只计算Y通道的数据,这样把上述计算Y通道的代码单独提取出来,然后再写个void ModifyYComponent(unsigned char *Src, unsigned char *NewY, unsigned char *Dest, int Width, int Height, int Stride)这样的函数把Y通道替换掉,这样一个连续的写法其中的-128和+128这项又可以免去处理,当然具体怎么写内部还是有很多学问值得研究的,只是感觉上没有太多人去使用SSE了,这里就不浪费自己的宝贵时间了。
至于XYZ和RGB空间的互换,由于他们的3个系数都没有为0的,只要注意相关shift值得取值合理,优化方式都例同RGB到XYZ的过程,这里不予以赘述。
其实还有很多细节的东西再最初写的时候也经过了各种尝试,只有通过自己编写才能体会中这中间的快乐和痛苦,看别人的代码你看到的只是最后的成品。
回顾下今年一年的文章,基本上就是在学习SIMD优化,确实SIMD的优化确实在某些场合有着很大的提速价值,在手机端也有NEON指令集,基本上和SSE2都有对应的关系,不过我一心只关注PC的优化,这个市场现在是越来越小了,虽然在航空、医疗等工业场景还是有一定的应用场景。手机编程太痛苦了,都一个老人家了,懒得再去折腾了。
快过年了,考虑下2018年,作为一个编程自由人,我初步考虑下2018可能研究下16位图像的增强、HDR技术等等,至于深度学习,还是算了吧,知道有那么个东西就OK了。
提前祝大家2018年新年快乐。
本文相关代码下载:https://files.cnblogs.com/files/Imageshop/YUV_RGB.rar
基于本文的优化应用于磨皮的效果见:彩色图工程:https://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar
到年底了,没钱回家过年,望各位路过的大神施舍点。
SSE图像算法优化系列十五:YUV/XYZ和RGB空间相互转化的极速实现(此后老板不用再担心算法转到其他空间通道的耗时了)。的更多相关文章
- SSE图像算法优化系列十:简单的一个肤色检测算法的SSE优化。
在很多场合需要高效率的肤色检测代码,本人常用的一个C++版本的代码如下所示: void IM_GetRoughSkinRegion(unsigned char *Src, unsigned char ...
- SSE图像算法优化系列十二:多尺度的图像细节提升。
无意中浏览一篇文章,中间提到了基于多尺度的图像的细节提升算法,尝试了一下,还是有一定的效果的,结合最近一直研究的SSE优化,把算法的步骤和优化过程分享给大家. 论文的全名是DARK IMAGE ENH ...
- SSE图像算法优化系列十四:局部均方差及局部平方差算法的优化。
关于局部均方差有着较为广泛的应用,在我博客的基于局部均方差相关信息的图像去噪及其在实时磨皮美容算法中的应用及使用局部标准差实现图像的局部对比度增强算法中都有谈及,即可以用于去噪也可以用来增强图像,但是 ...
- SSE图像算法优化系列十八:三次卷积插值的进一步SSE优化。
本文是在学习https://blog.csdn.net/housisong/article/details/1452249一文的基础上对算法的理解和重新整理,再次非常感谢原文作者的深入分析以及分享. ...
- SSE图像算法优化系列十九:一种局部Gamma校正对比度增强算法及其SSE优化。
这是一篇2010年比较古老的文章了,是在QQ群里一位群友提到的,无聊下载看了下,其实也没有啥高深的理论,抽空实现了下,虽然不高大上,还是花了点时间和心思优化了代码,既然这样,就顺便分享下优化的思路和经 ...
- SSE图像算法优化系列十六:经典USM锐化中的分支判断语句SSE实现的几种方法尝试。
分支判断的语句一般来说是不太适合进行SSE优化的,因为他会破坏代码的并行性,但是也不是所有的都是这样的,在合适的场景中运用SSE还是能对分支预测进行一定的优化的,我们这里以某一个算法的部分代码为例进行 ...
- SSE图像算法优化系列二十:一种快速简单而又有效的低照度图像恢复算法。
又有很久没有动笔了,主要是最近没研究什么东西,而且现在主流的趋势都是研究深度学习去了,但自己没这方面的需求,同时也就很少有动力再去看传统算法,今天一个人在家,还是抽空分享一个简单的算法吧. 前段日子在 ...
- SSE图像算法优化系列二十三: 基于value-and-criterion structure 系列滤波器(如Kuwahara,MLV,MCV滤波器)的优化。
基于value-and-criterion structure方式的实现的滤波器在原理上其实比较简单,感觉下面论文中得一段话已经描述的比较清晰了,直接贴英文吧,感觉翻译过来反而失去了原始的韵味了. T ...
- SSE图像算法优化系列二十五:二值图像的Euclidean distance map(EDM)特征图计算及其优化。
Euclidean distance map(EDM)这个概念可能听过的人也很少,其主要是用在二值图像中,作为一个很有效的中间处理手段存在.一般的处理都是将灰度图处理成二值图或者一个二值图处理成另外一 ...
随机推荐
- 如何迭代输出某文件夹下的所有文件的路径?(os.walk用法)
查看目录结构: tree 查看文件结构: os.walk 查看os.walk用法: help(os.walk) For each directory in the directory tree roo ...
- 牛客网linux试题-错误整理-20170914
Linux操作系统包括三种不同类型的进程,每种进程都有自己的特点和属性. 1.交互进程--由一个shell启动的进程.交互进程既可以在前台运行,也可以在后台运行. 2.批处理进程--这种进程和终端没有 ...
- 了解adb的基本原理
ADB就是Android调试桥的意思,很形象.但不知道是windows本身的原因还是windows乱七八糟软件太多,用USB调试手机时,经常连不上ADB,或者就出现offline,导致费时费力,焦头烂 ...
- Django----->Ajax
一,前情回顾(Json) Json的定义: JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式.它基于 ECMAScript (w3c制定的 ...
- hdu 6191--Query on A Tree(持久化字典树)
题目链接 Problem Description Monkey A lives on a tree, he always plays on this tree. One day, monkey A l ...
- DFS中的奇偶剪枝学习笔记
奇偶剪枝学习笔记 描述 编辑 现假设起点为(sx,sy),终点为(ex,ey),给定t步恰好走到终点, s | | | + — — — e 如图所示(“|”竖走,“—”横走,“+”转弯),易证abs( ...
- Codeforces Round #331 (Div. 2) _A. Wilbur and Swimming Pool
A. Wilbur and Swimming Pool time limit per test 1 second memory limit per test 256 megabytes input s ...
- python列表三
>>> list1 = [123]>>> list2 =[234]>>> list1 > list2False>>> li ...
- HDU 2063 过山车(模板—— 二分图最大匹配问题)
题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=2063 解题思路: 二分图最大匹配模板题. AC代码: #include<stdio.h> ...
- 学而精计算机公共基础学习之路TEST1
算法 一:算法基本概念 算法是个什么概念学了这么久的程序尽然没有听说过,其实算法就是为了解决问题那么怎么准确完整的解决这个问题就是算法.所以我们所写的程序就可以说为对算法的描述,但是程序编制是不能有于 ...