SSE图像算法优化系列三十一:RGB2HSL/RGB2HSV及HSL2RGB/HSV2RGB的指令集优化-上。
RGB和HSL/HSV颜色空间的相互转换在我们的图像处理中是有着非常广泛的应用的,无论是是图像调节,还是做一些肤色算法,HSL/HSV颜色空间都非常有用,他提供了RGB颜色空间不具有的一些独特的特性,但是由于HSL/HSV颜色空间的复杂性,他们之间的转换的效率一直不是很高的,有一些基于定点算法的尝试,对速度有一定的提升,但一个是提升不是特别的明显,另外就是对结果的精度有一定的影响。
对于这两个算法的指令集优化,网络上就根本没有任何资料,也没有任何人进行过尝试,我也曾经有想法去折腾他,但是初步判断觉得他里面有太多的分支了,应该用了指令集后也不会有多大的速度区别,所以一直没有动手。
但是最近的一个朋友的潜在需求,然后我又对这个算法有些期待,重新动手拾起这个转换过程,结果还是有所收获,速度获得了3到4倍的提升。、
我们先来谈谈RGB到HSL或者HSV颜色空间的转换优化
这个网络上一大堆,我也就不浪费时间去重新整理,我直接分享一段代码和网址吧:
参考网址: http://www.xbeat.net/vbspeed
这个文章给出的是VB6的代码,可以参考下。
我们约定:RGB数据源是unsigned char 类型, 有效范围就是[0,255],而HSL/HSV都是浮点型,其中H的有效范围时[0,6],S的有效范围是[0,1], L/V的有效范围也是[0,1]。
经过我个人的整理和稍微优化,一个简单的RGB2HSV代码如下所示:
void IM_RGB2HSV_PureC(unsigned char Blue, unsigned char Green, unsigned char Red, float &Hue, float &Sat, float &Val)
{
int Min = IM_Min(Red, IM_Min(Green, Blue));
int Max = IM_Max(Red, IM_Max(Green, Blue));
if (Max == Min)
{
Hue = 0;
Sat = 0;
}
else
{
int Delta = Max - Min;
if (Max == Red)
Hue = (float)(Green - Blue) / Delta;
else if (Max == Green)
Hue = 2.0f + (float)(Blue - Red) / Delta;
else
Hue = 4.0f + (float)(Red - Green) / Delta;
// 实际上只有Max==Red时,方有可能Hue < 0 (对应Green < Blue),
// 所以有的代码在Max == Red内再做判断,对于C代码来说,这样效率应该会高一点
if (Hue < 0) Hue += 6;
Sat = (float)Delta / Max;
}
Val = Max * IM_Inv255;
}
RGB2HSL的代码如下:
void IM_RGB2HSL_PureC(unsigned char Blue, unsigned char Green, unsigned char Red, float &Hue, float &Sat, float &Val)
{
int Min = IM_Min(Red, IM_Min(Green, Blue));
int Max = IM_Max(Red, IM_Max(Green, Blue));
int Sum = Max + Min;
if (Max == Min)
{
Hue = 0;
Sat = 0;
}
else
{
int Delta = Max - Min;
if (Max == Red)
Hue = (float)(Green - Blue) / Delta;
else if (Max == Green)
Hue = 2.0f + (float)(Blue - Red) / Delta;
else
Hue = 4.0f + (float)(Red - Green) / Delta;
if (Hue < 0) Hue += 6;
if (Sum <= 255)
Sat = (float)Delta / Sum;
else
Sat = (float)Delta / (510 - Sum);
}
Val = Sum * IM_Inv510;
}
比较两个不同的模型的代码可以发现,他们对于H分量的定义是相同的,对于V/L分量一个使用了最大值,一个使用了最大值和最小值的平均值,对于S分量,大家都考虑了最大值和最下值的差异,只是一个和最大值做比较,一个是和最大值和最小值之和做比较,整体来说,RGB2HSV模型相对来说简单一些,计算量也少一些。
可以看到,无论是RGB2HSL还是RGB2HSV,求H的过程都有非常多的判断和分支语句,而且整体考虑除零错误(Max == Min)还有一些其他的特殊判断, 正如我在博文中多次提到,指令集里没有分支跳转的东西,这些跳转是非常不利于指令集优化。指令集里要实现这样的东西,只有两个办法:
1、想办法把所有分支跳转用一些奇技淫巧合并到一起,用一个语句来表达他。
2、对所有分支语句的结果都计算出来,然后使用相关的Blend进行条件合并。
仔细的分析上面的C代码,我是没有想到什么特别好的技巧把色相部分的三个分支合并为一个语句。凭个人的感觉,只能使用第二种方式。
为了描述方便,我先贴出RGB2HSV算法一个比较简单的SIMD指令集优化的结果:
1 void IM_RGB2HSV_SSE_Old(__m128i Blue, __m128i Green, __m128i Red, __m128 &Hue, __m128 &Sat, __m128 &Val)
2 {
3 __m128i Max = _mm_max_epi32(Red, _mm_max_epi32(Green, Blue)); // R/G/B的最大值Max
4 __m128i Min = _mm_min_epi32(Red, _mm_min_epi32(Green, Blue)); // R/G/B的最小值Min
5 __m128i Delta = _mm_sub_epi32(Max, Min); // 最大值和最小值之间的差异Delta = Max - Min
6
7 __m128 MaxS = _mm_cvtepi32_ps(Max);
8 __m128 DeltaS = _mm_cvtepi32_ps(Delta);
9
10 Sat = _mm_divz_ps(DeltaS, MaxS); // S = Delta / Max, 注意有了除零的异常处理,同时如果Max == Min, Delta就为0, S也返回0,是正确的
11 Val = _mm_mul_ps(MaxS, _mm_set1_ps(IM_Inv255)); // V = Max / 255;
12
13 __m128 Inv = _mm_divz_ps(_mm_set1_ps(1), DeltaS);
14
15 //if (Max == Red)
16 // Hue = (float)(Green - Blue) / Delta;
17
18 __m128 HueR = _mm_mul_ps(_mm_cvtepi32_ps(_mm_sub_epi32(Green, Blue)), Inv);
19
20 //else if (Max == Green)
21 // Hue = 2.0f + (float)(Blue - Red) / Delta;
22
23 __m128i Mask = _mm_cmpeq_epi32(Max, Green);
24 __m128 HueG = _mm_add_ps(_mm_set1_ps(2.0f), _mm_mul_ps(_mm_cvtepi32_ps(_mm_sub_epi32(Blue, Red)), Inv));
25
26 Hue = _mm_castsi128_ps(_mm_blendv_epi8(_mm_castps_si128(HueR), _mm_castps_si128(HueG), Mask));
27
28 //else
29 // Hue = 4.0f + (float)(Red - Green) / Delta;
30 Mask = _mm_cmpeq_epi32(Max, Blue);
31 __m128 HueB = _mm_add_ps(_mm_set1_ps(4.0f), _mm_mul_ps(_mm_cvtepi32_ps(_mm_sub_epi32(Red, Green)), Inv));
32
33 Hue = _mm_castsi128_ps(_mm_blendv_epi8(_mm_castps_si128(Hue), _mm_castps_si128(HueB), Mask));
34
35 // if (H < 0) H += 6; 其实这个主要是针对Max == R的情况会出现
36 Hue = _mm_blendv_ps(Hue, _mm_add_ps(Hue, _mm_set1_ps(6)), _mm_cmplt_ps(Hue, _mm_setzero_ps()));
37
38 }
说明: IM_RGB2HSV_SSE函数中的Blue、Green、Red三个__m128i 变量中保存的是4个32位的颜色分量,而不是16个颜色。
第三、第四、第五行求Max\Min\Delta这些过程没有什么难以理解的。第七、第八行只是把整形转换为浮点型(注意SSE指令也是强类型的哦,必须自己手动转换类型)。
第十、第十一行直接就求出了Sat和Val分量, Val不难理解,Sat在对应的C代码中是分了Max == Min及Max != Min两种状况,当Max == Min时,为0,否则,要使用(Max - Min) / Max, 其实这里不用做判断直接统一使用 (Max - Min) / Max即可,因为Max == Min时, Max - Min也是0, 但是唯一需要注意的就是如果Max = Min = 0时, Max也为0, 0 / 0 在数学时不容许的,在计算上也会有溢出错误,所以这里使用了一个自定义的_mm_divz_ps函数,实现当除数为0时,返回0的结果。这样就可以剥离掉这个分支语句了。
复杂的是Hue分量的计算,从第十三行开始一直到最后都是关于他的优化。
第13行,我们先计算出1.0f / Delta,注意这里也是使用的_mm_divz_ps函数。
第16行我们先按照公式计算出当Max == Red时Hue的结果。
第23行我们比较Max和Green是否相等,注意这里也是使用的32位int类型的比较。
第24行按照公式计算出当Max == Green时,Hue分量的结果。
第25行则对这两个结果进行混合,这里的混合有很多编码上的技巧,因为我们两次计算的HueR和HueG都是__m128类型,而我们的比较是用的整形的比较,返回的是__m128i型的数据,而_mm_blendv_ps的混合需要的__m128的比较结果,但是如果直接将Mask强制转换为浮点类型,作为_mm_blendv_ps的参数,将会产生不正确的结果。那么解决方案有2个:
一、使用浮点类型的比较,即将Blue\Green分量先转换为__m128型,然后使用_mm_cmpeq_ps进行比较,这样增加几条类型转换函数。
二、就是使用本例的代码,使用_mm_blendv_epi8 + _mm_castps_si128进行混合,表面上看多了3次cast的过程,似乎更为耗时,但是实际上cast系列的语句只是个语法糖,编译后他不产生任何汇编指令。他只是让编译器认为他是另外一个类型的数据类型了,这样就可以编译了,实际上__m128、__m128i这些东西在硬件上都是保存在XMM寄存器上的,寄存器本身不分数据类型。
第30和31行也是类似的到里,对那些Max == Blue分量的结果进行混合。
第36行则是对Hue < 0的特殊情况进行处理。也没有什么特别复杂的。
我们对一副5000*5000大小的24位图像(填充的随机数据)进行测试,普通C语言的耗时约为114ms,上述SIMD优化的耗时约为 49ms,提速比接近2.2倍。
实际上上述SIMD指令优化的代码还有一定的优化空间,我们注意到为了计算HueR\HueG\HueB,我们进行了3次浮点版本的乘法和加法。但是如果我们把这个乘法和加法的部分单独提出来,每次都进行相应的混合,那么只需要最后进行一次乘法和加法即可以了,这样增加了混合的次数,但是减少了计算的次数,而混合指令其实都是通过位运算实现的,相对来说非常快,具体的代码如下所示:
1 void IM_RGB2HSV_SSE(__m128i Blue, __m128i Green, __m128i Red, __m128 &Hue, __m128 &Sat, __m128 &Val)
2 {
3 __m128i Max = _mm_max_epi32(Red, _mm_max_epi32(Green, Blue)); // R/G/B的最大值Max
4 __m128i Min = _mm_min_epi32(Red, _mm_min_epi32(Green, Blue)); // R/G/B的最小值Min
5 __m128i Delta = _mm_sub_epi32(Max, Min); // 最大值和最小值之间的差异Delta = Max - Min
6
7 __m128 MaxS = _mm_cvtepi32_ps(Max);
8 __m128 DeltaS = _mm_cvtepi32_ps(Delta);
9
10 Sat = _mm_divz_ps(DeltaS, MaxS); // S = Delta / Max, 注意有了除零的异常处理,同时如果Max == Min, Delta就为0, S也返回0,是正确的
11 Val = _mm_mul_ps(MaxS, _mm_set1_ps(IM_Inv255)); // V = Max / 255;
12
13 // SIMD没有跳转方面的指令,只能用Blend加条件判断来实现多条件语句。注意观察三种判断的情况可以看成是一个Base(0/120/240)加上不同的Diff乘以Inv。
14 // 以Max == B为基础,这样做的好处是:当Max == Min时,H是要返回0的,但是如果按照C语言的那个混合顺序,则最后判断Max == B时成立,则H返回的是4,那么为了返回正确的结果
15 // 就还要多一个_mm_blendv_epi8语句,注意这里隐藏的一个事实是Max == Min时,G - B, B - R, R - G其实都是为0的,那么类似这样的 (float)(G - B) / Delta * 60结果也必然是0。
16
17 // if (Max == bB)
18 // H = 4.0f + (float)(R - G) / Delta;
19
20 __m128i Base = _mm_set1_epi32(4);
21 __m128i Diff = _mm_sub_epi32(Red, Green);
22
23 //if (Max == G)
24 // H = 2.0f + (float)(B - R) / Delta;
25
26 __m128i Mask = _mm_cmpeq_epi32(Max, Green);
27 Base = _mm_blendv_epi8(Base, _mm_set1_epi32(2), Mask); // 当Mask为真时,_mm_blendv_epi8返回第二个参数的值,否则返回第一个参数的值
28 Diff = _mm_blendv_epi8(Diff, _mm_sub_epi32(Blue, Red), Mask);
29
30 // if (Max == R)
31 // H = (float)(G - B) / Delta;
32 Mask = _mm_cmpeq_epi32(Max, Red);
33 Base = _mm_blendv_epi8(Base, _mm_setzero_si128(), Mask);
34 Diff = _mm_blendv_epi8(Diff, _mm_sub_epi32(Green, Blue), Mask);
35
36 __m128 Inv = _mm_divz_ps(_mm_set1_ps(1), DeltaS); // 1 / Delta,注意有了除零的异常处理
37
38 // H = Base + Diff * Inv
39 Hue = _mm_add_ps(_mm_cvtepi32_ps(Base), _mm_mul_ps(_mm_cvtepi32_ps(Diff), Inv));
40
41 // if (H < 0) H += 6; 其实这个主要是针对Max == R的情况会出现
42 Hue = _mm_blendv_ps(Hue, _mm_add_ps(Hue, _mm_set1_ps(6)), _mm_cmplt_ps(Hue, _mm_setzero_ps()));
43
44 }
通过这种方式优化大概还能获取15-25%的性能提升。
当然,这里可能还有一部分空间可以考虑,即我们使用的是32位int类型的比较,一次只能比较4个数,另外诸如_mm_max_epi32这样的计算,对于原始的图像数据来说,都可以使用epi8来做的,这样一次性就是可以获取16个像素的信息,而不是8位,但是这样做面临的问题就是后面要做多次数据类型转换。这些转换的耗时和比较的耗时孰重孰轻暂时还没有结论,有兴趣的读者可以自行测试下。
如果您看懂了RGB2HSV的SSE代码,那么RGB2HSL你觉得还会有难度吗,希望读者可以自行编码实现。
下一篇将着重讲述HSL2RGB及HSV2RGB空间的优化,那个的优化难度以及优化的提速比相对来讲要比RGB2HSL和RGB2HSL更为复杂和有效。
本文的测试代码可从下述链接获取: https://files.cnblogs.com/files/Imageshop/RGB2HSV.rar?t=1689216617&download=true
如果想时刻关注本人的最新文章,也可关注公众号或者添加本人微信: laviewpbt