Vibrance這個單詞搜索翻譯通常振動,抖動或者是響亮、活力,可是官方的詞彙裏還歷來未出現過天然飽和度這個詞,也不知道當時的Adobe中文翻譯人員怎麼會這樣處理。可是咱們看看PS對這個功能的解釋:html
Vibrance: Adjusts the saturation so that clipping is minimized as colors approach full saturation. This setting changes the saturation of all lower-saturated colors with less effect on the higher-saturated colors. Vibrance also prevents skin tones from becoming oversaturated.git
確實是和飽和度有關的,這樣理解中文的翻譯反而卻是合理,那麼只能怪Adobe的開發者爲何給這個功能起個名字叫Vibrance了。github
閒話很少說了,其實天然飽和度也是最近幾個版本的PS纔出現的功能,在調節有些圖片的時候會有不錯的效果,也能夠做爲簡單的膚色調整的一個算法,好比下面這位小姑娘,用天然飽和度便可以讓她失血過多,也可讓他膚色紅暈。算法
原圖 面色蒼白 膚色紅暈一點app
那麼這個算法的內在是如何實現的呢,我沒有仔細的去研究他,可是在開源軟件PhotoDemon-master(開源地址:https://github.com/tannerhelland/PhotoDemon,visual basic 6.0的做品,個人最愛)提供了一個有點類似的功能,咱們貼出他對改效果的部分註釋:less
'***************************************************************************
'Vibrance Adjustment Tool
'Copyright 2013-2017 by Audioglider
'Created: 26/June/13
'Last updated: 24/August/13
'Last update: added command bar
'
'Many thanks to talented contributer Audioglider for creating this tool.
'
'Vibrance is similar to saturation, but slightly smarter, more subtle. The algorithm attempts to provide a greater boost
' to colors that are less saturated, while performing a smaller adjustment to already saturated colors.
'
'Positive values indicate "more vibrance", while negative values indicate "less vibrance"
'
'All source code in this file is licensed under a modified BSD license. This means you may use the code in your own
' projects IF you provide attribution. For more information, please visit http://photodemon.org/about/license/
'
'***************************************************************************
其中的描述和PS官方文檔的描述有相似之處。ide
咱們在貼出他的核心代碼:函數
For x = initX To finalX quickVal = x * qvDepth For y = initY To finalY 'Get the source pixel color values
r = ImageData(quickVal + 2, y) g = ImageData(quickVal + 1, y) b = ImageData(quickVal, y) 'Calculate the gray value using the look-up table
avgVal = grayLookUp(r + g + b) maxVal = Max3Int(r, g, b) 'Get adjusted average
amtVal = ((Abs(maxVal - avgVal) / 127) * vibranceAdjustment) If r <> maxVal Then r = r + (maxVal - r) * amtVal End If
If g <> maxVal Then g = g + (maxVal - g) * amtVal End If
If b <> maxVal Then b = b + (maxVal - b) * amtVal End If
'Clamp values to [0,255] range
If r < 0 Then r = 0
If r > 255 Then r = 255
If g < 0 Then g = 0
If g > 255 Then g = 255
If b < 0 Then b = 0
If b > 255 Then b = 255 ImageData(quickVal + 2, y) = r ImageData(quickVal + 1, y) = g ImageData(quickVal, y) = b Next
Next
很簡單的算法,先求出每一個像素RGB份量的最大值和平均值,而後求二者之差,以後根據輸入調節量求出調整量。佈局
VB的語法有些人可能不熟悉,我稍微作點更改翻譯成C的代碼以下:post
float VibranceAdjustment = -0.01 * Adjustment; // Reverse the vibrance input; this way, positive values make the image more vibrant. Negative values make it less vibrant.
for (int Y = 0; Y < Height; Y++) { unsigned char * LinePS = Src + Y * Stride; unsigned char * LinePD = Dest + Y * Stride; for (int X = 0; X < Width; X++) { int Blue = LinePS[0], Green = LinePS[1], Red = LinePS[2]; int Avg = (Blue + Green + Green + Red) >> 2; int Max = IM_Max(Blue, IM_Max(Green, Red)); float AmtVal = (abs(Max - Avg) / 127.0f) * VibranceAdjustment; // Get adjusted average
if (Blue != Max) Blue += (Max - Blue) * AmtVal; if (Green != Max) Green += (Max - Green) * AmtVal; if (Red != Max) Red += (Max - Red) * AmtVal; LinePD[0] = IM_ClampToByte(Blue); LinePD[1] = IM_ClampToByte(Green); LinePD[2] = IM_ClampToByte(Red); LinePS += 3; LinePD += 3; } }
這個的結果和PS的是比較接近的,最起碼趨勢是很是接近的,可是細節仍是不同,不過能夠判定的是方向是對的,若是你必定要複製PS的結果,我建議你花點時間改變其中的一些常數或者計算方式看看。應該能有收穫,國內已經有人摸索出來了。
咱們重點講下這個算法的優化及其SSE實現,特別是SSE版本代碼是本文的重中之重。
第一步優化,去除掉沒必要要計算和除法,很明顯,這一句是本段代碼中耗時較爲顯著的部分
float AmtVal = (abs(Max - Avg) / 127.0f) * VibranceAdjustment;
/127.0f能夠優化爲乘法,同時注意VibranceAdjustment在內部不變,能夠把他們整合到循環的最外層,即改成:
float VibranceAdjustment = -0.01 * Adjustment / 127.0f;
再注意abs裏的參數, Max - Avg,這有必要取絕對值嗎,最大值難道會比平均值小,浪費時間,最後改成:
float AmtVal = (Max - Avg) * VibranceAdjustment;
這是浮點版本的簡單優化,若是不勾選編譯器的SSE優化,直接使用FPU,對於一副3000*2000的24位圖像耗時在I5的一臺機器上運行用時大概70毫秒,但這不是重點。
咱們來考慮某些近似和定點優化。
第一咱們把/127改成/128,這基本不影響效果,同時Adjustment默認的範圍爲[-100,100],把它也線性擴大一點,好比擴大1.28倍,擴大到[-128,128],這樣在最後咱們一次性移位,減小中間的損失,大概的代碼以下:
int IM_VibranceI(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int Adjustment) { int Channel = Stride / Width; if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE; if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER; if (Channel != 3) return IM_STATUS_INVALIDPARAMETER; Adjustment = -IM_ClampI(Adjustment, -100, 100) * 1.28; // Reverse the vibrance input; this way, positive values make the image more vibrant. Negative values make it less vibrant. for (int Y = 0; Y < Height; Y++) { unsigned char *LinePS = Src + Y * Stride; unsigned char *LinePD = Dest + Y * Stride; for (int X = 0; X < Width; X++) { int Blue, Green, Red, Max; Blue = LinePS[0]; Green = LinePS[1]; Red = LinePS[2]; int Avg = (Blue + Green + Green + Red) >> 2; if (Blue > Green) Max = Blue; else Max = Green; if (Red > Max) Max = Red; int AmtVal = (Max - Avg) * Adjustment; // Get adjusted average if (Blue != Max) Blue += (((Max - Blue) * AmtVal) >> 14); if (Green != Max) Green += (((Max - Green) * AmtVal) >> 14); if (Red != Max) Red += (((Max - Red) * AmtVal) >> 14); LinePD[0] = IM_ClampToByte(Blue); LinePD[1] = IM_ClampToByte(Green); LinePD[2] = IM_ClampToByte(Red); LinePS += 3; LinePD += 3; } } return IM_STATUS_OK; }
這樣優化後,一樣大小的圖像算法用時35毫秒,效果和浮點版本的基本沒啥區別。
最後咱們重點來說講SSE版本的優化。
對於這種單像素點、和領域無關的圖像算法,爲了能利用SSE提升程序速度,一個核心的步驟就是把各顏色份量分離爲單獨的連續的變量,對於24位圖像,咱們知道圖像在內存中的佈局爲:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
B1 | G1 | R1 | B2 | G2 | R2 | B3 | G3 | R3 | B4 | G4 | R4 | B5 | G5 | R5 | B6 | G6 | R6 | B7 | G7 | R7 | B8 | G8 | R8 | B9 | G9 | R9 | B10 | G10 | R10 | B11 | G11 | R11 | B12 | G12 | R12 | B13 | G13 | R13 | B14 | G14 | R14 | B15 | G15 | R15 | B16 | G16 | R16 |
咱們須要把它們變爲:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
B1 | B2 | B3 | B4 | B4 | B5 | B7 | B8 | B9 | B10 | B11 | B12 | B13 | B14 | B15 | B16 | G1 | G2 | G3 | G4 | G5 | G6 | G7 | G8 | G9 | G10 | G11 | G12 | G13 | G14 | G15 | G16 | R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | R10 | R11 | R12 | R13 | R14 | R15 | R16 |
處理完後咱們又要把他們恢復到原始的BGR佈局。
爲了實現這個功能,我參考了採石工大俠的有關代碼,分享以下:
咱們先貼下代碼:
Src1 = _mm_loadu_si128((__m128i *)(LinePS + 0)); Src2 = _mm_loadu_si128((__m128i *)(LinePS + 16)); Src3 = _mm_loadu_si128((__m128i *)(LinePS + 32)); Blue8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)); Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14, -1, -1, -1, -1, -1))); Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 4, 7, 10, 13))); Green8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(1, 4, 7, 10, 13, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)); Green8 = _mm_or_si128(Green8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, 0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1))); Green8 = _mm_or_si128(Green8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14))); Red8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(2, 5, 8, 11, 14, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)); Red8 = _mm_or_si128(Red8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, 1, 4, 7, 10, 13, -1, -1, -1, -1, -1, -1))); Red8 = _mm_or_si128(Red8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 3, 6, 9, 12, 15)));
首先,一次性加載48個圖像數據到內存,正好放置在三個__m128i變量中,同時另一個很好的事情就是48正好能被3整除,也就是說咱們完整的加載了16個24位像素,這樣就不會出現斷層,只意味着下面48個像素能夠和如今的48個像素使用一樣的方法進行處理。
如上代碼,則Src1中保存着:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
B1 | G1 | R1 | B2 | G2 | R2 | B3 | G3 | R3 | B4 | G4 | R4 | B5 | G5 | R5 | B6 |
Src2中保存着:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
G6 | R6 | B7 | G7 | R7 | B8 | G8 | R8 | B9 | G9 | R9 | B10 | G10 | R10 | B11 | G11 |
Src3中的數據則爲:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
R11 | B12 | G12 | R12 | B13 | G13 | R13 | B14 | G14 | R14 | B15 | G15 | R15 | B16 | G16 | R16 |
爲了達到咱們的目的,咱們就要利用SSE中強大的shuffle指令了,若是可以把shuffle指令運用的出神入化,能夠獲取不少頗有意思的效果,有如鳩摩智的小無相功同樣,能夠催動拈花指發、袈裟服魔攻等等,成就世間能和我鳩摩智打成平成的沒有幾我的同樣的豐功偉績。哈哈,說遠了。
簡單的理解shuffle指令,就是將__m128i變量內的各個數據按照指定的順序進行從新佈置,固然這個佈置不必定要徹底利用原有的數據,也能夠重複某些數據,或者某些位置無數據,好比在執行下面這條指令
Blue8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(0, 3, 6, 9, 12, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1));
Blue8中的數據爲:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
B1 | B2 | B3 | B4 | B5 | B6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
_mm_setr_epi8指令的參數順序可能更適合於咱們經常使用的從左到右的理解習慣,其中的某個參數若是不在0和15之間時,則對應位置的數據就是被設置爲0。
能夠看到進行上述操做後Blue8的籤6個字節已經符合咱們的需求了。
在看代碼的下一句:
Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14, -1, -1, -1, -1, -1)));
這句的後半部分和前面的相似,只是裏面的常數不一樣,由_mm_shuffle_epi8(Src2, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, 2, 5, 8, 11, 14, -1, -1, -1, -1, -1))獲得的臨時數據爲:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
0 | 0 | 0 | 0 | 0 | 0 | B7 | B8 | B9 | B10 | B11 | 0 | 0 | 0 | 0 | 0 |
若是把這個臨時結果和以前的Blue8進行或操做甚至直接進行加操做,新的Blue8變量則爲:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 | B9 | B10 | B11 | 0 | 0 | 0 | 0 | 0 |
最後這一句和Blue8相關的代碼爲:
Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 4, 7, 10, 13)));
後面的shuffle臨時的獲得的變量爲:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | B12 | B13 | B14 | B15 | B16 |
再次和以前的Blue8結果進行或操做獲得最終的結果:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 | B9 | B10 | B11 | B12 | B13 | B14 | B15 | B16 |
對於Green和Red份量,處理的方法和步驟是同樣的,只是因爲位置不一樣,每次進行shuffle操做的常數有所不一樣,但原理徹底一致。
若是理解了由BGRBGRBGR ---》變爲了BBBGGGRRR這樣的模式的原理後,那麼由BBBGGGRRR-->變爲BGRBGRBGR的道理就很是淺顯了,這裏不贅述,直接貼出代碼:
Dest1 = _mm_shuffle_epi8(Blue8, _mm_setr_epi8(0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1, -1, 5)); Dest1 = _mm_or_si128(Dest1, _mm_shuffle_epi8(Green8, _mm_setr_epi8(-1, 0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1, -1))); Dest1 = _mm_or_si128(Dest1, _mm_shuffle_epi8(Red8, _mm_setr_epi8(-1, -1, 0, -1, -1, 1, -1, -1, 2, -1, -1, 3, -1, -1, 4, -1))); Dest2 = _mm_shuffle_epi8(Blue8, _mm_setr_epi8(-1, -1, 6, -1, -1, 7, -1, -1, 8, -1, -1, 9, -1, -1, 10, -1)); Dest2 = _mm_or_si128(Dest2, _mm_shuffle_epi8(Green8, _mm_setr_epi8(5, -1, -1, 6, -1, -1, 7, -1, -1, 8, -1, -1, 9, -1, -1, 10))); Dest2 = _mm_or_si128(Dest2, _mm_shuffle_epi8(Red8, _mm_setr_epi8(-1, 5, -1, -1, 6, -1, -1, 7, -1, -1, 8, -1, -1, 9, -1, -1))); Dest3 = _mm_shuffle_epi8(Blue8, _mm_setr_epi8(-1, 11, -1, -1, 12, -1, -1, 13, -1, -1, 14, -1, -1, 15, -1, -1)); Dest3 = _mm_or_si128(Dest3, _mm_shuffle_epi8(Green8, _mm_setr_epi8(-1, -1, 11, -1, -1, 12, -1, -1, 13, -1, -1, 14, -1, -1, 15, -1))); Dest3 = _mm_or_si128(Dest3, _mm_shuffle_epi8(Red8, _mm_setr_epi8(10, -1, -1, 11, -1, -1, 12, -1, -1, 13, -1, -1, 14, -1, -1, 15)));
核心仍是這些常數的選取。
以上是處理的第一步,看上去這個代碼不少,實際上他們的執行時很是快的,3000*2000的圖這個拆分和合並過程也就大概2ms。
固然因爲字節數據類型的表達範圍很是有限,除了少有的幾個有限的操做能針對字節類型直接處理外,好比本例的丘RGB的Max值,就能夠直接用下面的SIMD指令實現:
Max8 = _mm_max_epu8(_mm_max_epu8(Blue8, Green8), Red8);
很其餘多計算都是沒法直接在這樣的範圍內進行了,所以就有必要將數據類型擴展,好比擴展到short類型或者int/float類型。
在SSE裏進行這樣的操做也是很是簡單的,SSE提供了大量的數據類型轉換的函數和指令,好比有byte擴展到short,則能夠用_mm_unpacklo_epi8和_mm_unpackhi_epi8配合zero來實現:
BL16 = _mm_unpacklo_epi8(Blue8, Zero);
BH16 = _mm_unpackhi_epi8(Blue8, Zero);
其中
Zero = _mm_setzero_si128();
頗有意思的操做,好比_mm_unpacklo_epi8是將兩個__m128i的低8位交錯佈置造成一個新的128位數據,若是其中一個參數爲0,則就是把另一個參數的低8個字節無損的擴展爲16位了,以上述BL16爲例,其內部佈局爲:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
B1 | 0 | B2 | 0 | B3 | 0 | B3 | 0 | B4 | 0 | B5 | 0 | B6 | 0 | B7 | 0 |
若是咱們須要進行在int範圍內進行計算,則還需進一步擴展,此時可使用_mm_unpackhi_epi16/_mm_unpacklo_epi16配合zero繼續進行擴展,這樣一個Blue8變量須要4個__m128i int範圍的數據來表達。
好,說道這裏,咱們繼續看咱們C語言裏的這句:
int Avg = (Blue + Green + Green + Red) >> 2;
能夠看到,這裏的計算是沒法再byte範圍內完成的,中間的Blue + Green + Green + Red在大部分狀況下都會超出255而絕對小於255*4,,所以咱們須要擴展數據到16位,按上述辦法,對Blue8\Green8\Red8\Max8進行擴展,以下所示:
BL16 = _mm_unpacklo_epi8(Blue8, Zero); BH16 = _mm_unpackhi_epi8(Blue8, Zero); GL16 = _mm_unpacklo_epi8(Green8, Zero); GH16 = _mm_unpackhi_epi8(Green8, Zero); RL16 = _mm_unpacklo_epi8(Red8, Zero); RH16 = _mm_unpackhi_epi8(Red8, Zero); MaxL16 = _mm_unpacklo_epi8(Max8, Zero); MaxH16 = _mm_unpackhi_epi8(Max8, Zero);
此時計算Avg就水到渠成了:
AvgL16 = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(BL16, RL16), _mm_slli_epi16(GL16, 1)), 2); AvgH16 = _mm_srli_epi16(_mm_add_epi16(_mm_add_epi16(BH16, RH16), _mm_slli_epi16(GH16, 1)), 2);
中間兩個Green相加是用移位仍是直接相加對速度沒啥影響的。
接下來的優化則是本例的一個特點部分了。咱們來詳細分析。
咱們知道,SSE對於跳轉是很不友好的,他很是擅長序列化處理一個事情,雖然他提供了不少比較指令,可是不少狀況下複雜的跳轉SSE仍是不管爲力,對於本例,狀況比較特殊,若是要使用SSE的比較指令也是能夠直接實現的,實現的方式時,使用比較指令獲得一個Mask,Mask中符合比較結果的值會爲FFFFFFFF,不符合的爲0,而後把這個Mask和後面須要計算的某個值進行And操做,因爲和FFFFFFFF進行And操做不會改變操做數自己,和0進行And操做則變爲0,在不少狀況下,就是不管你符合條件與否,都進行後面的計算,只是不符合條件的計算不會影響結果,這種計算可能會低效SSE優化的部分提速效果,這個就要具體狀況具體分析了。
注意觀察本例的代碼,他的本意是若是最大值和某個份量的值不相同,則進行後面的調整操做,不然不進行調節。可後面的調整操做中有最大值減去該份量的操做,也就意味着若是最大值和該份量相同,二者相減則爲0,調整量此時也爲0,並不影響結果,也就至關於沒有調節,所以,把這個條件判斷去掉,並不會影響結果。同時考慮到實際狀況,最大值在不少狀況也只會和某一個份量相同,也就是說只有1/3的機率不執行跳轉後的語句,在本例中,跳轉後的代碼執行復雜度並不高,去掉這些條件判斷從而增長一路代碼所消耗的性能和減小3個判斷的時間已經在一個檔次上了,所以,徹底能夠刪除這些判斷語句,這樣就很是適合於SSE實現了。
接着分析,因爲代碼中有((Max - Blue) * AmtVal) >> 14,其中AmtVal = (Max - Avg) * Adjustment,展開即爲: ((Max - Blue) * (Max - Avg) * Adjustment)>>14;這三個數據相乘很大程度上會超出short所能表達的範圍,所以,咱們還須要對上面的16位數據進行擴展,擴展到32位,這樣就多了不少指令,那麼有沒有不須要擴展的方式呢。通過一番思索,我提出了下述解決方案:
在超高速指數模糊算法的實現和優化(10000*10000在100ms左右實現 一文中,我在文章最後提到了終極的一個指令:_mm_mulhi_epi16(a,b),他能一次性處理8個16位數據,其計算結果至關於對於(a*b)>>16,但這裏很明a和b必須是short類型所能表達的範圍。
注意咱們的這個表達式:
((Max - Blue) * (Max - Avg) * Adjustment)>>14
首先,咱們將他擴展爲移位16位的結果,變爲以下:
((Max - Blue) * 4 * (Max - Avg) * Adjustment)>>16
Adjustment咱們已經將他限定在了[-128,128]之間,而(Max - Avg)理論上的最大值爲255 - 85=170,(即RGB份量有一個是255,其餘的都爲0),最小值爲0,所以,二者在各自範圍內的成績不會超出short所能表達的範圍,而(Max-Blue)的最大值爲255,最小值爲0,在乘以4也在short類型所能表達的範圍內。因此,下一步大家懂了嗎?
通過上述分析,下面這四行C代碼可由下述SSE函數實現:
int AmtVal = (Max - Avg) * Adjustment; // Get adjusted average if (Blue != Max) Blue += (((Max - Blue) * AmtVal) >> 14); if (Green != Max) Green += (((Max - Green) * AmtVal) >> 14); if (Red != Max) Red += (((Max - Red) * AmtVal) >> 14);
對應的SSE代碼爲:
AmtVal = _mm_mullo_epi16(_mm_sub_epi16(MaxL16, AvgL16), Adjustment128); BL16 = _mm_adds_epi16(BL16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxL16, BL16), 2), AmtVal)); GL16 = _mm_adds_epi16(GL16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxL16, GL16), 2), AmtVal)); RL16 = _mm_adds_epi16(RL16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxL16, RL16), 2), AmtVal)); AmtVal = _mm_mullo_epi16(_mm_sub_epi16(MaxH16, AvgH16), Adjustment128); BH16 = _mm_adds_epi16(BH16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxH16, BH16), 2), AmtVal)); GH16 = _mm_adds_epi16(GH16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxH16, GH16), 2), AmtVal)); RH16 = _mm_adds_epi16(RH16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxH16, RH16), 2), AmtVal));
最後一步就是將這些16位的數據再次轉換爲8位的,注意原始代碼中有Clamp操做,這個操做實際上是個耗時的過程,而SSE自然的具備抗飽和的函數。
Blue8 = _mm_packus_epi16(BL16, BH16); Green8 = _mm_packus_epi16(GL16, GH16); Red8 = _mm_packus_epi16(RL16, RH16);
_mm_packus_epi16這個的用法和含義本身去MSDN搜索一下吧,實在是懶得解釋了。
最終優化速度:5ms。
來個速度比較:
版本 | VB6.0 | C++,float優化版本 | C++定點版 | C++/SSE版 |
速度 | 400ms | 70ms | 35ms | 5ms |
上面的VB6.0的耗時是原做者的代碼編譯後的執行速度,若是我本身去用VB6.0去優化他的話,有信心能作到70ms之內的。
但不管如何,SSE優化的速度提高是巨大的。
結論:
簡單的分析了天然飽和度算法的實現,分享了其SSE實現的過程,對於那些剛剛接觸SSE,想作圖像處理的朋友有必定的幫助。
源代碼下載地址:http://files.cnblogs.com/files/Imageshop/Vibrance.rar
寫的真的好累,休息去了,以爲對你有用的請給我買杯啤酒或者咖啡吧。