SSE图像算法优化系列八:自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。...
Vibrance这个单词搜索翻译一般振动,抖动或者是响亮、活力,但是官方的词汇里还从来未出现过自然饱和度这个词,也不知道当时的Adobe中文翻译人员怎么会这样处理。但是我们看看PS对这个功能的解释:
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.
确实是和饱和度有关的,这样理解中文的翻译反而倒是合理,那么只能怪Adobe的开发者为什么给这个功能起个名字叫Vibrance了。
闲话不多说了,其实自然饱和度也是最近几个版本的PS才出现的功能,在调节有些图片的时候会有不错的效果,也可以作为简单的肤色调整的一个算法,比如下面这位小姑娘,用自然饱和度即可以让她失血过多,也可以让他肤色红晕。
原图 面色苍白 肤色红晕一点
那么这个算法的内在是如何实现的呢,我没有仔细的去研究他,但是在开源软件PhotoDemon-master(开源地址:https://github.com/tannerhelland/PhotoDemon,visual basic 6.0的作品,我的最爱)提供了一个有点相似的功能,我们贴出他对改效果的部分注释:
'***************************************************************************
'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官方文档的描述有类似之处。
我们在贴出他的核心代码:
For x = initX To finalXquickVal = x * qvDepthFor y = initY To finalY'Get the source pixel color valuesr = ImageData(quickVal + 2, y)g = ImageData(quickVal + 1, y)b = ImageData(quickVal, y)'Calculate the gray value using the look-up tableavgVal = grayLookUp(r + g + b)maxVal = Max3Int(r, g, b)'Get adjusted averageamtVal = ((Abs(maxVal - avgVal) / 127) * vibranceAdjustment)If r <> maxVal Thenr = r + (maxVal - r) * amtValEnd IfIf g <> maxVal Theng = g + (maxVal - g) * amtValEnd IfIf b <> maxVal Thenb = b + (maxVal - b) * amtValEnd If'Clamp values to [0,255] rangeIf r < 0 Then r = 0If r > 255 Then r = 255If g < 0 Then g = 0If g > 255 Then g = 255If b < 0 Then b = 0If b > 255 Then b = 255ImageData(quickVal + 2, y) = rImageData(quickVal + 1, y) = gImageData(quickVal, y) = bNextNext
很简单的算法,先求出每个像素RGB分量的最大值和平均值,然后求两者之差,之后根据输入调节量求出调整量。
VB的语法有些人可能不熟悉,我稍微做点更改翻译成C的代码如下:
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 averageif (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;elseMax = Green;if (Red > Max)Max = Red;int AmtVal = (Max - Avg) * Adjustment; // Get adjusted averageif (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 averageif (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
写的真的好累,休息去了,觉得对你有用的请给我买杯啤酒或者咖啡吧。
转载于:https://www.cnblogs.com/Imageshop/p/7234463.html
SSE图像算法优化系列八:自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。...相关推荐
- (附源码)计算机毕业设计SSM基于推荐算法的鞋服代购平台
(附源码)计算机毕业设计SSM基于推荐算法的鞋服代购平台 项目运行 环境配置: Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe( ...
- (附源码)springboot+mysql+采用协同过滤算法的视频推荐系统的设计与实现 毕业设计261620
摘 要 现阶段,社会的发展和科技的进步,以及大数据时代下纷繁数据信息的融合,使得人们在生产及生活过程中,都将会接收到各种类型的数据信息,而通过计算机技术与网络技术,则能够将众多人们所不了解或不常用的信 ...
- 【Python自然语言处理】隐马尔可夫模型中维特比(Viterbi)算法解决商务选择问题实战(附源码 超详细必看)
需要源码请点赞关注收藏后评论区留言私信~~~ 一.统计分词 统计分词基本逻辑是把每个词语看做由单字组成,利用统计学原理计算连接字在不同文本中出现的次数,以此判断相连字属于特定词语的概率. 二.隐马尔可 ...
- ASP.NET页面进行GZIP压缩优化的几款压缩模块的使用简介及应用测试!(附源码)
在介绍之前,先简单说一说ASP.NET服务端GZIP压缩模块的作用及工作原理,很多人编写网页的时候页面因为使用了大量的JS特效又或者放置很多大型动态广告导致了页面或脚本体积庞大,通常都会使用一些压缩工 ...
- 针对ASP.NET页面实时进行GZIP压缩优化的几款压缩模块的使用简介及应用测试!(附源码)...
在介绍之前,先简单说一说ASP.NET服务端GZIP压缩模块的作用及工作原理,很多人编写网页的时候页面因为使用了大量的JS特效又或者放置很多大型动态广告导致了页面或脚本体积庞大,通常都会使用一些压缩工 ...
- 附源码|复杂网络社区发现——标签传播算法(LPA)
本文通过Python 3.7实现了标签传播算法的两个代码(1.自己写的,2.调包实现),并通过空手道俱乐部的例子进行可视化显示. 标签传播是一种半监督机器学习算法,它将标签分配给以前未标记的数据点.在 ...
- 2023最新SSM计算机毕业设计选题大全(附源码+LW)之java基于推荐算法的餐厅点餐系统7czh9
现在毕设刚开始.时间还有很多,可以从头开始学也可以.毕设其实不难,难的是我们懒散到这种时候再去静下心学.能自己独立完成尽量自己独立完成.相信你看过很多上面回答的,都不建议去某宝.毕竟这一行参差不齐哈. ...
- 计算机图形学实习教程之基本图形的生成(直线DDA算法,直线中点算法,Bresenham画圆算法),利用C#实现,附源码
环境:Win10+Visual Studio 2022 Community 在本次实验中需要用到上一篇文章实验内容的代码及环境,详情请见:传送门 目录 一.实验目的 二.实验过程 1.生成直线的DDA ...
- 【附源码】布雷森汉姆直线算法(bresenham‘s line algorithm)
1.布雷森汉姆直线算法简介 我们在纸上画直线时,只需要定一个起点和终点,然后把两点连接起来就是一条直线,你将会得到一条笔直的直线. 但是,这个简单的过程,在计算机上却并不容易.首先计算机的屏幕是一个一 ...
最新文章
- Android selector 使用
- php100例代码教程,php实例代码_php用户登录实例教程代码
- linux命令date
- httpd-2.4编译安装基本步骤
- AltiumDesigner17快捷键
- 路由器在多个OSPF进程下的路由学习问题
- V 神呼吁宽大处理,以太坊开发者 Virgil Griffith 被判入狱 63 个月
- Process when we start our testing work
- php服务端搜索,功能改进
- CLion开发,运行报错后问题排查
- Codesys + BeagleBone PLC控制达到小儿科水平
- 关于无线传感器网络(WSN)的学术会议和期刊
- 到底是影像杀死了建筑,还是建筑变成了屏幕? | 浅空间专栏
- 计算机1M代表什么,1M网速是多少 Mbps是什么意思
- 在Ubuntu里配置slim
- 先决条件检查失败错误
- Facial landmarks: 使用 dlib, OpenCV, and Python提取面部标志点
- RT_Thread_空闲线程
- java jni ocx_Java调用ocx控件以及dll
- 9月15日云栖精选夜读:BCG与阿里研究院等联合揭秘中国互联网经济:成功的关键是什么?