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

                             

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章