自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。

时间:2021-12-20 05:42:33

  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才出现的功能,在调节有些图片的时候会有不错的效果,也可以作为简单的肤色调整的一个算法,比如下面这位小姑娘,用自然饱和度即可以让她失血过多,也可以让他肤色红晕。

自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。     自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。     自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。

原图                                                                                                  面色苍白                                                                                   肤色红晕一点

那么这个算法的内在是如何实现的呢,我没有仔细的去研究他,但是在开源软件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 finalX
quickVal = x * qvDepth
For y = initY To finalY
'Get the source pixel color values
r = ImageData(quickVal + , y)
g = ImageData(quickVal + , 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) / ) * 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 < Then r =
If r > Then r =
If g < Then g =
If g > Then g =
If b < Then b =
If b > Then b = ImageData(quickVal + , y) = r
ImageData(quickVal + , y) = g
ImageData(quickVal, y) = b
Next
Next

  很简单的算法,先求出每个像素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 = ; Y < Height; Y++)
{
unsigned char * LinePS = Src + Y * Stride;
unsigned char * LinePD = Dest + Y * Stride;
for (int X = ; X < Width; X++)
{
int Blue = LinePS[], Green = LinePS[], Red = LinePS[];
int Avg = (Blue + Green + Green + Red) >> ;
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[] = IM_ClampToByte(Blue);
LinePD[] = IM_ClampToByte(Green);
LinePD[] = IM_ClampToByte(Red);
LinePS += ;
LinePD += ;
}
}

  这个的结果和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 <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if (Channel != ) return IM_STATUS_INVALIDPARAMETER; Adjustment = -IM_ClampI(Adjustment, -, ) * 1.28; // Reverse the vibrance input; this way, positive values make the image more vibrant. Negative values make it less vibrant.
for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Stride;
for (int X = ; X < Width; X++)
{
int Blue, Green, Red, Max;
Blue = LinePS[]; Green = LinePS[]; Red = LinePS[];
int Avg = (Blue + Green + Green + Red) >> ;
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) >> );
if (Green != Max) Green += (((Max - Green) * AmtVal) >> );
if (Red != Max) Red += (((Max - Red) * AmtVal) >> );
LinePD[] = IM_ClampToByte(Blue);
LinePD[] = IM_ClampToByte(Green);
LinePD[] = IM_ClampToByte(Red);
LinePS += ;
LinePD += ;
}
}
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 + ));
Src2 = _mm_loadu_si128((__m128i *)(LinePS + ));
Src3 = _mm_loadu_si128((__m128i *)(LinePS + )); Blue8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , , -, -, -, -, -, -, -, -, -, -));
Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, -, , , , , , -, -, -, -, -)));
Blue8 = _mm_or_si128(Blue8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, -, , , , , ))); Green8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , -, -, -, -, -, -, -, -, -, -, -));
Green8 = _mm_or_si128(Green8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, , , , , , , -, -, -, -, -)));
Green8 = _mm_or_si128(Green8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, -, , , , , ))); Red8 = _mm_shuffle_epi8(Src1, _mm_setr_epi8(, , , , , -, -, -, -, -, -, -, -, -, -, -));
Red8 = _mm_or_si128(Red8, _mm_shuffle_epi8(Src2, _mm_setr_epi8(-, -, -, -, -, , , , , , -, -, -, -, -, -)));
Red8 = _mm_or_si128(Red8, _mm_shuffle_epi8(Src3, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, -, , , , , , )));

首先,一次性加载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

_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(, -, -, , -, -, , -, -, , -, -, , -, -, ));
Dest1 = _mm_or_si128(Dest1, _mm_shuffle_epi8(Green8, _mm_setr_epi8(-, , -, -, , -, -, , -, -, , -, -, , -, -)));
Dest1 = _mm_or_si128(Dest1, _mm_shuffle_epi8(Red8, _mm_setr_epi8(-, -, , -, -, , -, -, , -, -, , -, -, , -))); Dest2 = _mm_shuffle_epi8(Blue8, _mm_setr_epi8(-, -, , -, -, , -, -, , -, -, , -, -, , -));
Dest2 = _mm_or_si128(Dest2, _mm_shuffle_epi8(Green8, _mm_setr_epi8(, -, -, , -, -, , -, -, , -, -, , -, -, )));
Dest2 = _mm_or_si128(Dest2, _mm_shuffle_epi8(Red8, _mm_setr_epi8(-, , -, -, , -, -, , -, -, , -, -, , -, -))); Dest3 = _mm_shuffle_epi8(Blue8, _mm_setr_epi8(-, , -, -, , -, -, , -, -, , -, -, , -, -));
Dest3 = _mm_or_si128(Dest3, _mm_shuffle_epi8(Green8, _mm_setr_epi8(-, -, , -, -, , -, -, , -, -, , -, -, , -)));
Dest3 = _mm_or_si128(Dest3, _mm_shuffle_epi8(Red8, _mm_setr_epi8(, -, -, , -, -, , -, -, , -, -, , -, -, )));

  核心还是这些常数的选取。

以上是处理的第一步,看上去这个代码很多,实际上他们的执行时非常快的,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) >> );
if (Green != Max) Green += (((Max - Green) * AmtVal) >> );
if (Red != Max) Red += (((Max - Red) * AmtVal) >> );

  对应的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), ), AmtVal));
GL16 = _mm_adds_epi16(GL16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxL16, GL16), ), AmtVal));
RL16 = _mm_adds_epi16(RL16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxL16, RL16), ), 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), ), AmtVal));
GH16 = _mm_adds_epi16(GH16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxH16, GH16), ), AmtVal));
RH16 = _mm_adds_epi16(RH16, _mm_mulhi_epi16(_mm_slli_epi16(_mm_sub_epi16(MaxH16, RH16), ), 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

写的真的好累,休息去了,觉得对你有用的请给我买杯啤酒或者咖啡吧。

自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。

自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。的更多相关文章

  1. SSE图像算法优化系列八:自然饱和度&lpar;Vibrance&rpar;算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。

    Vibrance这个单词搜索翻译一般振动,抖动或者是响亮.活力,但是官方的词汇里还从来未出现过自然饱和度这个词,也不知道当时的Adobe中文翻译人员怎么会这样处理.但是我们看看PS对这个功能的解释: ...

  2. JAVA模拟Spring实现IoC过程&lpar;附源码&rpar;

    前言:本人大四学生,第一次写博客,如果有写得不好的地方,请大家多多指正 一.IoC(Inversion of Control)反转控制 传统开发都是需要对象就new,但这样做有几个问题: 效率低下,创 ...

  3. 三维网格细分算法(Catmull-Clark subdivision &amp&semi; Loop subdivision)附源码

    下图描述了细分的基本思想,每次细分都是在每条边上插入一个新的顶点,可以看到随着细分次数的增加,折线逐渐变成一条光滑的曲线.曲面细分需要有几何规则和拓扑规则,几何规则用于计算新顶点的位置,拓扑规则用于确 ...

  4. arcgis api 4&period;x for js 集成 Echarts4 实现模拟迁徙图效果(附源码下载)

    前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 4.x for js:esri 官网 api,里面详细的介绍 arcgis api 4.x 各个类 ...

  5. 三维网格细分算法(Catmull-Clark subdivision &amp&semi; Loop subdivision)附源码&lpar;转载&rpar;

    转载:  https://www.cnblogs.com/shushen/p/5251070.html 下图描述了细分的基本思想,每次细分都是在每条边上插入一个新的顶点,可以看到随着细分次数的增加,折 ...

  6. wpf 模拟3D效果&lpar;和手机浏览图片效果相似&rpar;(附源码)

    原文 wpf 模拟3D效果(和手机浏览图片效果相似)(附源码) pf的3D是一个很有意思的东西,类似于ps的效果,类似于电影动画的效果,因为动画的效果,(对于3D基础的摄像机,光源,之类不介绍,对于依 ...

  7. wpf 模拟抖音很火的罗盘时钟,附源码,下载就能跑

    wpf 模拟抖音很火的罗盘时钟,附源码 前端时间突然发现,抖音火了个壁纸,就是黑底蕾丝~~~  错错错,黑底白字的罗盘时钟! 作为程序员的我,也觉得很新颖,所以想空了研究下,这不,空下来了就用wpf, ...

  8. Vue源码终笔-VNode更新与diff算法初探

    写完这个就差不多了,准备干新项目了. 确实挺不擅长写东西,感觉都是罗列代码写点注释的感觉,这篇就简单阐述一下数据变动时DOM是如何更新的,主要讲解下其中的diff算法. 先来个正常的html模板: & ...

  9. SM4密码算法(附源码)

    SM4是我们自己国家的一个分组密码算法,是国家密码管理局于2012年发布的.网址戳→_→:http://www.cnnic.NET.cn/jscx/mixbz/sm4/ 具体的密码标准和算法官方有非常 ...

随机推荐

  1. C&plus;&plus;继承和多态

    继承 访问控制 基类的成员函数可以有public.protected.private三种访问属性. 类的继承方式有public.protected.private三种. 公有继承 当类的继承方式为pu ...

  2. 如何在html中做圆角矩形和 只有右边的&quot&semi;分隔线&quot&semi;

    这个网站满好的,可以常看看 css-matic中有几个很好的写css可视化的工具 其实做css 版式布局等都可以有工具的 推荐40个优秀的免费CSS工具 debugger正则表达式在线 其实是对(理论 ...

  3. Parallel&period;js初探

    今天闲着看了一下Parallel.js.这个库暂时貌似还没有什么中文的介绍(可能暂时用的人都不多吧).所以就只能上github找它得源码和介绍看看了.貌似它的代码也不多,以后可以深入研究一下. 先简单 ...

  4. 淘宝PHPSDK2&period;0 剔除 lotusphp框架---兄弟连教程

    淘宝PHPSDK2.0 剔除 lotusphp框架---兄弟连教程. lotusphp是一个国产开源的php框架 由于有个朋友公司是做淘宝客的,还由于不少朋友在开淘宝,于是有必要研究下.尽管个人认为微 ...

  5. 【待解决】编译V8引擎出错-snapshot&period;cc

    这几天学习nodejs,翻阅官网的API文档.看到nodejs插件时,想了解一下v8的实现机制,于是我便从GitHub社区克隆了一份v8源码库.哪知道,编译安装的时候就出了问题,这问题已经折磨我两天了 ...

  6. JUnit介绍

    8.1.1  JUnit简介 JUnit主要用来帮助开发人员进行Java的单元测试,其设计非常小巧,但功能却非常强 大. 下面是JUnit一些特性的总结: — 提供的API可以让开发人员写出测试结果明 ...

  7. 关于EL表达式的生效时间&lpar;猜想&rpar;

    通过ajax与服务端异步交互的时候,在服务端将某些变量或对象设置到request等域里,此时页面上的EL表达式是获取不到ajax异步交互时设置在request等域里的变量或对像的. 我猜测可能EL表达 ...

  8. linux普通帐号可以临时切换到root(添加用户到sudoers中)

    一般,进入terminal之后,默认是普通账户能操作的功能,能访问的目录有限,需要临时切换到root账户 那么此时就需要配置sudoers文件,可以让普通用户通过sudo命令临时切换到root账户 首 ...

  9. 想让安卓app不再卡顿?看这篇文章就够了

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由likunhuang发表于云+社区专栏 实现背景 应用的使用流畅度,是衡量用户体验的重要标准之一.Android 由于机型配置和系统的 ...

  10. Java中通过Class的反射来获取方法

    本文转自:http://z3sm2012.iteye.com/blog/1933227 今天在程序中用到java反射机制时,遇到的问题记录一下:我当时遇到的问题是,我用反射getMethod()调用类 ...