GIMP源代码链接:https://gitlab.gnome.org/GNOME/gimp/-/archive/master/gimp-master.zip

  GEGL相关代码链接:https://gitlab.gnome.org/GNOME/gegl/-/archive/master/gegl-master.zip

  最近因为要研究下色温算法,顺便下载了最新的GIMP软件,色温算法倒是找到了(有空单独来讲下),也顺便看看GIMP都有些什么更新,嗯,更新还是蛮多的,界面UI上有很多改动,有些已经改的面目全非了。随便瞄了一下Enhance菜单,发现里面有一个Nosie Reduction算法,试了下,还有点效果。于是在github上下载了GIMP的源代码,可是在源代码里搜索相关的关键词确没有发现任何的相关代码,后来才发现很多东西都有个GEGL关键词,结果一百度,原来他是一个单独的软件包,于是有下载了GEGL的源代码,终于在gegl-master\operations\common\里面看到了noise-reduction.c文件。

  其核心的代码如下:

static void
noise_reduction (float *src_buf, /* source buffer, one pixel to the left
and up from the starting pixel */
int src_stride, /* stridewidth of buffer in pixels */
float *dst_buf, /* destination buffer */
int dst_width, /* width to render */
int dst_height, /* height to render */
int dst_stride) /* stride of target buffer */
{
int c;
int x,y;
int dst_offset; #define NEIGHBOURS 8
#define AXES (NEIGHBOURS/2) #define POW2(a) ((a)*(a))
/* core code/formulas to be tweaked for the tuning the implementation */
#define GEN_METRIC(before, center, after) \
POW2((center) * - (before) - (after)) /* Condition used to bail diffusion from a direction */
#define BAIL_CONDITION(new,original) ((new) > (original)) #define SYMMETRY(a) (NEIGHBOURS - (a) - 1) /* point-symmetric neighbour pixel */ #define O(u,v) (((u)+((v) * src_stride)) * 4)
int offsets[NEIGHBOURS] = { /* array of the relative distance i float
* pointers to each of neighbours
* in source buffer, allows quick referencing.
*/
O( -, -), O(, -), O(, -),
O( -, ), O(, ),
O( -, ), O(, ), O(, )};
#undef O dst_offset = ;
for (y=; y<dst_height; y++)
{
float *center_pix = src_buf + ((y+) * src_stride + ) * ;
dst_offset = dst_stride * y;
for (x=; x<dst_width; x++)
{
for (c=; c<; c++) /* do each color component individually */
{
float metric_reference[AXES];
int axis;
int direction;
float sum;
int count; for (axis = ; axis < AXES; axis++)
{ /* initialize original metrics for the horizontal, vertical
and 2 diagonal metrics */
float *before_pix = center_pix + offsets[axis];
float *after_pix = center_pix + offsets[SYMMETRY(axis)]; metric_reference[axis] =
GEN_METRIC (before_pix[c], center_pix[c], after_pix[c]);
} sum = center_pix[c];
count = ; /* try smearing in data from all neighbours */
for (direction = ; direction < NEIGHBOURS; direction++)
{
float *pix = center_pix + offsets[direction];
float value = pix[c] * 0.5 + center_pix[c] * 0.5;
int axis;
int valid; /* check if the non-smoothing operating check is true if
* smearing from this direction for any of the axes */
valid = ; /* assume it will be valid */
for (axis = ; axis < AXES; axis++)
{
float *before_pix = center_pix + offsets[axis];
float *after_pix = center_pix + offsets[SYMMETRY(axis)];
float metric_new =
GEN_METRIC (before_pix[c], value, after_pix[c]); if (BAIL_CONDITION(metric_new, metric_reference[axis]))
{
valid = ; /* mark as not a valid smoothing, and .. */
break; /* .. break out of loop */
}
}
if (valid) /* we were still smooth in all axes */
{ /* add up contribution to final result */
sum += value;
count ++;
}
}
dst_buf[dst_offset*+c] = sum / count;
}
dst_buf[dst_offset*+] = center_pix[]; /* copy alpha unmodified */
dst_offset++;
center_pix += ;
}
}
}

  这个代码看上去比较混乱,没办法,大型软件没有哪一个代码看上去能让人省心的,而且也不怎么讲究效率,我测试了一个3K*2K的彩色图,在GIMP里大概在4S左右处理完成,属于很慢的了,看这个代码,也知道大概有width * height * 3 * 8 * 4 * Iter个循环,计算量确实是相当的大。

  我试着尝试优化这个算法。

  优化的第一步是弄明白算法的原理,在GIMP的UI界面上可当鼠标停留在Noise Reduction菜单上时,会出现Anisotroic smoothing operation字样,所以初步分析他是属于各项异性扩散类的算法。稍微分析下代码,也确实是。明显这属于一个领域滤波器,对每一个像素,求取其3*3领域内的累加值,但是3*3领域的权重并不是平均分布或者是高斯分布,而是和领域的值有关的,如果领域的值是一个边缘点,他将不参与到累加中,权重为0,否则权重为1。

  具体一点,对于领域里的任何一点,我们先求取其和中心点的平均值,对应 float value = pix[c] * 0.5 + center_pix[c] * 0.5; 这条语句,然后计算这个值在2个45度对角线及水平和垂直方向的梯度(4个梯度值)是否比中心点在四个方向的梯度都小,如果都小,说明这个领域点不属于边缘点,可以往这个方向扩散,把他计入到统计值中,如果有任何一个方向小了,则不参与最终的计算。

  上面的过程可以看成是标准的各项异性扩散的特殊在特殊处理,他具有各项异性扩散的特性,也具有一些特殊性。

  下一步,稍微分析下最简单的优化方法。第一,我们知道,在大循环里一般不建议嵌套入小的循环,这样是很低效的。我们观察到上面代码里的

      for (c=0; c<3; c++) /* do each color component individually */

  这个语句主要是为了方便表达3通道的处理的方便,但是其实三通道之间的处理时没有任何联系的,对于这样的算法,很明显,我们可以一次性当然处理R G B R G B R G B ,而不需要像GIMP这个代码这样按照 RRR  GGG  BBB这样的顺序来写,GIMP这种写法浪费了很多CPU的CACHE,毕竟R和G和B在内存类分布本来就是连续的。这样就减少了一个小循环。

  第二个优化的点是,对于普通的图像数据,我们可以考虑不用浮点数来处理,毕竟上述计算里只有*0.5这样的浮点操作,我们考虑将原先的图像数据放大一定的倍数,然后用整形来玩,在处理完后,在缩小到原来的范围,比如使用short类型应该就足够了,我把数据放大16倍或者32倍,甚至8倍应该都能获得足够的精度。

  第三个优化点,程序中是使用的Pow来判断梯度的大小的,其实可以不用,直接使用绝对值的结果和Pow是完全一样的,而绝对值的计算量比pow要小很多,对于整数则更为如此(还可以不考虑pow数据类型的改变,比如short的绝对值还是short类型,但是其pow可能就需要用int来表示了,这在SIMD优化会产生不同的结果)。

  第四个优化点是 for (axis = 0; axis < AXES; axis++)这个小循环我们应该把它直接展开。

  第五点,我们还可以考虑我在其他文章里提到的支持Inplace操作的方式,这样noise_reduction这个函数的输入和输出就可以是同一个内存。

  第六点还有小点上的算法改进,比如一些中间计算没必要重复进行,有些可以提到外部来等。

  综合上面的描述,我整理除了一个优化的C语言版本的程序,如下所示:

void IM_AnisotropicDiffusion3X3(short *Src, short *Dest, int Width, int Height, int SplitPos, int Stride)
{
int Channel = Stride / Width; short *RowCopy = (short *)malloc((Width + ) * * Channel * sizeof(short));
short *First = RowCopy;
short *Second = RowCopy + (Width + ) * Channel;
short *Third = RowCopy + (Width + ) * * Channel;
memcpy(Second, Src, Channel * sizeof(short));
memcpy(Second + Channel, Src, Width * Channel * sizeof(short)); // 拷贝数据到中间位置
memcpy(Second + (Width + ) * Channel, Src + (Width - ) * Channel, Channel * sizeof(short)); memcpy(First, Second, (Width + ) * Channel * sizeof(short)); // 第一行和第二行一样 memcpy(Third, Src + Stride, Channel * sizeof(short)); // 拷贝第二行数据
memcpy(Third + Channel, Src + Stride, Width * Channel* sizeof(short));
memcpy(Third + (Width + ) * Channel, Src + Stride + (Width - ) * Channel, Channel* sizeof(short)); for (int Y = ; Y < Height; Y++)
{
short *LinePD = Dest + Y * Stride;
if (Y != )
{
short *Temp = First; First = Second; Second = Third; Third = Temp;
}
if (Y == Height - )
{
memcpy(Third, Second, (Width + ) * Channel * sizeof(short));
}
else
{
memcpy(Third, Src + (Y + ) * Stride, Channel * sizeof(short));
memcpy(Third + Channel, Src + (Y + ) * Stride, Width * Channel * sizeof(short)); // 由于备份了前面一行的数据,这里即使Src和Dest相同也是没有问题的
memcpy(Third + (Width + ) * Channel, Src + (Y + ) * Stride + (Width - ) * Channel, Channel * sizeof(short));
}
for (int X = ; X < SplitPos * Channel; X++)
{
short LT = First[X], T = First[X + Channel], RT = First[X + * Channel];
short L = Second[X], C = Second[X + Channel], R = Second[X + * Channel];
short LB = Third[X], B = Third[X + Channel], RB = Third[X + * Channel];
short LT_RB = LT + RB, RT_LB = RT + LB;
short T_B = T + B, L_R = L + R, C_C = C + C;
short Dist1 = IM_Abs(C_C - LT_RB), Dist2 = IM_Abs(C_C - T_B);
short Dist3 = IM_Abs(C_C - RT_LB), Dist4 = IM_Abs(C_C - L_R); int Sum = C_C, Amount = ; short LT_C = LT + C;
if ((IM_Abs(LT_C - LT_RB) < Dist1) && (IM_Abs(LT_C - T_B) < Dist2) && (IM_Abs(LT_C - RT_LB) < Dist3) && (IM_Abs(LT_C - L_R) < Dist4))
{
Sum += LT_C;
Amount += ;
}
short T_C = T + C;
if ((IM_Abs(T_C - LT_RB) < Dist1) && (IM_Abs(T_C - T_B) < Dist2) && (IM_Abs(T_C - RT_LB) < Dist3) && (IM_Abs(T_C - L_R) < Dist4))
{
Sum += T_C;
Amount += ;
}
short RT_C = RT + C;
if ((IM_Abs(RT_C - LT_RB) < Dist1) && (IM_Abs(RT_C - T_B) < Dist2) && (IM_Abs(RT_C - RT_LB) < Dist3) && (IM_Abs(RT_C - L_R) < Dist4))
{
Sum += RT_C;
Amount += ;
}
short L_C = L + C;
if ((IM_Abs(L_C - LT_RB) < Dist1) && (IM_Abs(L_C - T_B) < Dist2) && (IM_Abs(L_C - RT_LB) < Dist3) && (IM_Abs(L_C - L_R) < Dist4))
{
Sum += L_C;
Amount += ;
}
short R_C = R + C;
if ((IM_Abs(R_C - LT_RB) < Dist1) && (IM_Abs(R_C - T_B) < Dist2) && (IM_Abs(R_C - RT_LB) < Dist3) && (IM_Abs(R_C - L_R) < Dist4))
{
Sum += R_C;
Amount += ;
}
short LB_C = LB + C;
if ((IM_Abs(LB_C - LT_RB) < Dist1) && (IM_Abs(LB_C - T_B) < Dist2) && (IM_Abs(LB_C - RT_LB) < Dist3) && (IM_Abs(LB_C - L_R) < Dist4))
{
Sum += LB_C;
Amount += ;
}
short B_C = B + C;
if ((IM_Abs(B_C - LT_RB) < Dist1) && (IM_Abs(B_C - T_B) < Dist2) && (IM_Abs(B_C - RT_LB) < Dist3) && (IM_Abs(B_C - L_R) < Dist4))
{
Sum += B_C;
Amount += ;
}
short RB_C = RB + C;
if ((IM_Abs(RB_C - LT_RB) < Dist1) && (IM_Abs(RB_C - T_B) < Dist2) && (IM_Abs(RB_C - RT_LB) < Dist3) && (IM_Abs(RB_C - L_R) < Dist4))
{
Sum += RB_C;
Amount += ;
}
LinePD[X] = Sum / Amount;
}
}
free(RowCopy);
}

  调用函数

int IM_ReduceNoise(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, int SplitPos,  int Strength)
{
int Channel = Stride / Width;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= ) || (Height <= )) return IM_STATUS_INVALIDPARAMETER;
if ((Channel != ) && (Channel != )) return IM_STATUS_INVALIDPARAMETER; Strength = IM_ClampI(Strength, , );
SplitPos = IM_ClampI(SplitPos, , Width);
int Status = IM_STATUS_OK;
short *Temp = (short *)malloc(Height * Stride * sizeof(short));
if (Temp == NULL) return IM_STATUS_OUTOFMEMORY;
for (int Y = ; Y < Height * Stride; Y++)
{
Temp[Y] = Src[Y] << ;
}
for (int Y = ; Y < Strength; Y++)
{
IM_AnisotropicDiffusion3X3(Temp, Temp, Width, Height, SplitPos, Stride);
}
for (int Y = ; Y < Height * Stride; Y++)
{
Dest[Y] = Temp[Y] >> ;
}
free(Temp);
return IM_STATUS_OK;
}

  是不是看起来比上面的GIMP得要舒服些,而且中间也大概只要原始图像2倍的一个临时内存了。在速度和内存占用方面都前进了很多。

  我测试前面提到的那副3K*2K的图像,耗时要7S多,但是我测试表面GIMP用了多核的,如果论单核,我这里的速度要比他快2倍多。

  很明显,这个速度是不可以接受的,我们需要继续优化。

我还是老套路,使用SIMD指令做处理,看到上面的代码,其实真的觉得好容易改成SIMD的。

     short LT_RB = LT + RB,    RT_LB = RT + LB;
short T_B = T + B, L_R = L + R, C_C = C + C;
short Dist1 = IM_Abs(C_C - LT_RB), Dist2 = IM_Abs(C_C - T_B);
short Dist3 = IM_Abs(C_C - RT_LB), Dist4 = IM_Abs(C_C - L_R);
 
这些加减绝对值都有完全对应的SSE指令。 _mm_add_epi16、 _mm_sub_epi16、_mm_abs_epi16,基本上就是照着写。
  稍微复杂一点就是这里:
  if ((IM_Abs(LT_C - LT_RB) < Dist1) && (IM_Abs(LT_C - T_B) < Dist2) && (IM_Abs(LT_C - RT_LB) < Dist3) && (IM_Abs(LT_C - L_R) < Dist4))
{
  Sum += LT_C;
   Amount += 2;
}
  在C语言里,这里判断会进行短路计算,即如果前一个条件已经不满足了,后续的计算就不会进行。但是在SIMD指令里,是没有这样的机制的。我们只能全部计算,然后在通过某一种条件组合。
  在合理,要实现符合条件就进行累加,不符合条件就不做处理的需求,我们需要稍作修改,即不符合条件不是不做处理,而是加0,加0对结果没有影响的。主要借助下面的_mm_blendv_epi8来实现。
    __m128i LT_C = _mm_add_epi16(LT, C);
Flag1 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, LT_RB)), Dist1); // 只能全部都计算,但还是能提速
Flag2 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, T_B)), Dist2);
Flag3 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, RT_LB)), Dist3);
Flag4 = _mm_cmplt_epi16(_mm_abs_epi16(_mm_sub_epi16(LT_C, L_R)), Dist4);
Flag = _mm_and_si128(_mm_and_si128(Flag1, Flag2), _mm_and_si128(Flag3, Flag4));
Sum = _mm_adds_epu16(Sum, _mm_blendv_epi8(Zero, LT_C, Flag));
Amount = _mm_adds_epu16(Amount, _mm_blendv_epi8(Zero, Two, Flag));

  注意到我们这里用到了_mm_adds_epu16,无符号的16位加法,这是因为我需要尽量的提速,因此需要减少类型转换的次数。同时,我们看到在统计累加值时,我们并没有求平均值,而是直接用的累加值,这样理论上最大的累加值就是 255 * n * (8 + 1) * 2 < 65535, 这样n最大能取15,但是15不是个好数据,在放大和缩小都不能用移位来实现,所以我最后取得放大系数为8。

  另外,在最后还有个16位的整数的除法问题,这个没有办法,SSE指令没有提供整数的除法计算方法,还只能转换到浮点后,再次转换回来。

  这样用SSE处理后,还是同一幅测试图像,在同一台PC上速度能提升到400ms(4次迭代),比之前的普通的C语言提高了约17倍的速度。

  在现代CPU中,具有AVX2指令集已经是很普遍的了,单AVX2能同时处理256字节的数据,比SSE还要多一倍,我也常使用AVX2进行优化处理,速度能达到250ms,相当于普通C语言的28倍之多(但是AVX编程里有很多坑,这些坑都拜AVX不是完全的按照SSE的线性扩展导致的,这个后续有时间我单独提出)。

  经过测试,1080P的图像使用4次迭代大约需要80ms,3次迭代55ms,2次迭代月40ms,也就是说前面的一些方法和缩小所使用的时间几乎可以忽略。

  选了几幅有特点的图进行了去燥测试,其中分界线左侧的位处理的效果,右侧为未处理的。

  但是,这个算法也还是不是很好,他对于图像容易出现轻微的油画效果,对于一些细节特别丰富的图像非常明显,比如下图:

  这个应该是不太可以接受的,也许可以通过修改部分权重的规则来改变这个现象。这个属于后期研究的问题了。

    另外,在GIMP里也提供了这个算法的OPENCL实现,有兴趣的可以源代码里找一找,不晓得速度怎么样。

  本文Demo下载地址:  http://files.cnblogs.com/files/Imageshop/SSE_Optimization_Demo.rar,见其中的Denoise -> Anisotroic Diffusion 菜单。

  写博不易,欢迎土豪打赏赞助。

SSE图像算法优化系列三十:GIMP中的Noise Reduction算法原理及快速实现。的更多相关文章

  1. SSE图像算法优化系列三十二:Zhang\Guo图像细化算法的C语言以及SIMD指令优化

    二值图像的细化算法也有很多种,比较有名的比如Hilditch细化.Rosenfeld细化.基于索引表的细化.还有Opencv自带的THINNING_ZHANGSUEN.THINNING_GUOHALL ...

  2. SSE图像算法优化系列三十一:Base64编码和解码算法的指令集优化。

        一.基础原理 Base64是一种用64个Ascii字符来表示任意二进制数据的方法.主要用于将不可打印的字符转换成可打印字符,或者简单的说是将二进制数据编码成Ascii字符.Base64也是网络 ...

  3. SSE图像算法优化系列二十:一种快速简单而又有效的低照度图像恢复算法。

    又有很久没有动笔了,主要是最近没研究什么东西,而且现在主流的趋势都是研究深度学习去了,但自己没这方面的需求,同时也就很少有动力再去看传统算法,今天一个人在家,还是抽空分享一个简单的算法吧. 前段日子在 ...

  4. SSE图像算法优化系列二十五:二值图像的Euclidean distance map(EDM)特征图计算及其优化。

    Euclidean distance map(EDM)这个概念可能听过的人也很少,其主要是用在二值图像中,作为一个很有效的中间处理手段存在.一般的处理都是将灰度图处理成二值图或者一个二值图处理成另外一 ...

  5. SSE图像算法优化系列二十二:优化龚元浩博士的曲率滤波算法,达到约1000 MPixels/Sec的单次迭代速度

      2015年龚博士的曲率滤波算法刚出来的时候,在图像处理界也曾引起不小的轰动,特别是其所说的算法的简洁性,以及算法的效果.执行效率等方面较其他算法均有一定的优势,我在该算法刚出来时也曾经有关注,不过 ...

  6. SSE图像算法优化系列二十四: 基于形态学的图像后期抗锯齿算法--MLAA优化研究。

    偶尔看到这样的一个算法,觉得还是蛮有意思的,花了将近10天多的时间研究了下相关代码. 以下为百度的结果:MLAA全称Morphological Antialiasing,意为形态抗锯齿是AMD推出的完 ...

  7. SSE图像算法优化系列三:超高速导向滤波实现过程纪要(欢迎挑战)

    自从何凯明提出导向滤波后,因为其算法的简单性和有效性,该算法得到了广泛的应用,以至于新版的matlab都将其作为标准自带的函数之一了,利用他可以解决的所有的保边滤波器的能解决的问题,比如细节增强.HD ...

  8. SSE图像算法优化系列二十六:和时间赛跑之优化高斯金字塔建立的计算过程。

    图像金字塔技术在很多层面上都有着广泛的应用,很多开源的工具也都有对他们的建立写了专门的函数,比如IPP,比如OpenCV等等,这方面的理论文章特别多,我不需要赘述,但是我发现大部多分开源的代码的实现都 ...

  9. SSE图像算法优化系列二十九:基础的拉普拉斯金字塔融合用于改善图像增强中易出现的过增强问题(一)

    拉普拉斯金字塔融合是多图融合相关算法里最简单和最容易实现的一种,我们在看网络上大部分的文章都是在拿那个苹果和橙子融合在一起,变成一个果橙的效果作为例子说明.在这方面确实融合的比较好.但是本文我们主要讲 ...

随机推荐

  1. Python3程序设计指南:02 数据类型

    目录 1.标识符与关键字 1.1 规则 1.2 约定 2.Integral类型 2.1 整数 2.1.1 数值型操作符与函数 2.1.2 使用数据类型创建对象 2.1.3 整数位逻辑操作符 2.2 布 ...

  2. Eclipse 创建 Maven 项目

    本人也是新手小白,在创建 Maven 项目的时候几乎踩完了所有的坑.特此总结如下: 1.咱先选中 File -> New -> Maven Project 2.然后如下图    在这里说明 ...

  3. Oracle基于延时的盲注总结

    0x00 前言 oracle注入中可以通过页面响应的状态,这里指的是响应时间,通过这种方式判断SQL是否被执行的方式,便是时间盲注: oracle的时间盲注通常使用DBMS_PIPE.RECEIVE_ ...

  4. OCPC(Optimized Cost per Click)[Paper笔记]

    背景 在线广告中,广告按照CPM排序,排在前面的广告竞争有限广告位(截断).其中,CPM=bid*pctr.注GSP二价计费的,按照下一位bid计费.适当调整bid,可以提高竞价的排名,从而获得展现的 ...

  5. JavaScript中For循环以及For循环嵌套实例

    JavaScript中For循环实例 1.打印出所有的 "水仙花数 ",所谓 "水仙花数 "是指一个三位数,其各位数字立方和等于该数本身. 例如:153是一个 ...

  6. GDAL集成对KML文件的支持

    目录 1. 正文 1.1. 编译LibKML 1.1.1. 第三方库支持 1.1.2. 编译错误 1.2. 配置GDAL 1.3. 链接问题 2. 参考 1. 正文 GDAL可以支持将KML作为矢量文 ...

  7. 基于SignalR的服务端和客户端通讯处理

    SignalR是一个.NET Core/.NET Framework的实时通讯的框架,一般应用在ASP.NET上,当然也可以应用在Winform上实现服务端和客户端的消息通讯,本篇随笔主要基于Sign ...

  8. 百万年薪python之路 -- 并发编程之 多进程 一

    并发编程之 多进程 一. multiprocessing模块介绍 ​ python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在python中大 ...

  9. 百万年薪python之路 -- python2和python3的区别

    python2和python3的区别: python2获取的是整数 python3获取的是浮点数 print函数:(Python3中print为一个函数,必须用括号括起来:Python2中print为 ...

  10. Leetcode(9)回文数

    Leetcode(9)回文数 [题目表述]: 判断一个整数是否是回文数.回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数. 第一次:直接全部转 执行用时:148 ms: 内存消耗:13.4 ...