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
写的真的好累,休息去了,以为对你有用的请给我买杯啤酒或者咖啡吧。