O(1)效率的表面模糊算法优化。

  很久没有写文章了,主要是最近一段时间没有以前那么多空暇空间,内存和CPU占用率一致都很高,应前几日群里网友的要求,今天发个表面模糊的小程序来找回以前写博的热血吧。

国内我认为,破解表面模糊的原理的最早作者是我一直很崇拜的一位女士,她不会编程,英文也不怎么好,仅凭计算器和Excel两个工具破解了PS了很多算法,真是个巾帼英雄。

详见地址:http://www.missyuan.com/thread-428384-1-1.htm

网上的有关该算法的matlab实现参考:http://www.cnblogs.com/tiandsp/archive/2012/11/06/2756441.html

用C实现的参考:http://blog.csdn.net/maozefa/article/details/8270990

表面模糊是属于典型的EPF滤波器中的一种,在PS的框架下好像也只有这一种自带的EPF算法,其核心也是数卷积的范畴,只是卷积的核是随着内容而变的,也属于方形半径内的算法,借助于直方图是可以做到于参数无关的O(1)算法。关于直方图的相关框架参考我的博文:任意半径局部直方图类算法在PC中快速实现的框架。, 但本文代码对其做了稍许改动。

为了表述方便,我们以灰度图像为例进行说明。首先,表面模糊有两个参数,半径Radius和阈值Threshold。 如果我们知道了以某点为中心,半径为Radius范围内的直方图数据Hist,以及该点的像素值,那根据原始的算法,其计算公式为:

//  最原始的算法void Calc(unsigned short *Hist, unsigned char Value, int Threshold, unsigned char *&Pixel)
{
    int Weight, Sum = 0, Divisor = 0;
    for (int Y = 0; Y < 256; Y++)
    {
        Weight = Hist[Y] * (2500 - abs(Y - Value) * 1000 / Threshold);
        if (Weight < 0) Weight = 0;
        Sum += Weight * Y;
        Divisor += Weight;
    }
    if (Divisor > 0) *Pixel = (Sum + (Divisor >> 1)) / Divisor;
}

 注意这里我们为了减少浮点计算,将权重的计算公式放大了2500倍以便进行定点化,同时必须在最后增加一个Divisor > 0的判断,因为当Threshold很小时,可能会出现Divisor为0的现象。

上述代码针对1000*1000的灰度图的执行时间约为1250ms,其中直方图的更新时间只有约50ms,速度难以接受。

分析计算方法1,很明显权重计算的几个加减乘除以及下面的那句判断是比较耗时的,而其只是Y-Value的一个函数,因此,我们可以提前建立一个表,该表的索引范围从Min[Y - Value]到Max[Y - Value]之间,很明显,这个范围是[-255, 255],因此,建立如下的一个查找表:

for (int Y = -255; Y <= 255; Y++)
{
    int Factor = (2500 - abs(Y) * 1000 / Threshold);
    if (Factor < 0) Factor = 0;
    Intensity[Y + 255] = Factor;
}

  有了这个查找表,我们来实现第二个版本的算法如下:

//    改进后的算法
unsigned char Calc2(unsigned short *Hist, unsigned char Value, unsigned short *Intensity)
{
    int Weight = 0, Sum = 0, Divisor = 0;
    unsigned short *Offset = Intensity + 255 - Value;
    for (int Y = 0; Y < 256; Y++)
    {
        Weight = Hist[Y] * Offset[Y];
        Sum += Weight * Y;
        Divisor += Weight;
    }
    if (Divisor > 0)
        return (Sum + (Divisor >> 1)) / Divisor;        //    四舍五入
    else
        return Value;
}

  同样大小的图,执行时间为350ms,速度提高约为3倍。

我们接着来思考问题,上述有256个循环,如果我们将循环手动展开,会不会有提高呢, 我们先把代码更改如下:

//    优化后的算法
unsigned char Calc3(unsigned short *Hist, unsigned char Value, unsigned short *Intensity)
{
    int Weight = 0, Sum = 0, Divisor = 0;
    unsigned short *Offset = Intensity + 255 - Value;
    Weight = Hist[0] * Offset[0];
    Sum += Weight * 0;  Divisor += Weight;        //    能不能用使用指令集的并行,没有去测试了
    Weight = Hist[1] * Offset[1];
    Sum += Weight * 1;  Divisor += Weight;
    Weight = Hist[2] * Offset[2];
    Sum += Weight * 2;  Divisor += Weight;
    Weight = Hist[3] * Offset[3];
    Sum += Weight * 3;  Divisor += Weight;
   /////////////////////////// ............................................................................
    Weight = Hist[251] * Offset[251];
    Sum += Weight * 251;  Divisor += Weight;
    Weight = Hist[252] * Offset[252];
    Sum += Weight * 252;  Divisor += Weight;
    Weight = Hist[253] * Offset[253];
    Sum += Weight * 253;  Divisor += Weight;
    Weight = Hist[254] * Offset[254];
    Sum += Weight * 254;  Divisor += Weight;
    Weight = Hist[255] * Offset[255];
    Sum += Weight * 255;  Divisor += Weight;
    if (Divisor > 0)
        return (Sum + (Divisor >> 1)) / Divisor;        //    四舍五入
    else
        return Value;
}

  为表述方便,中间省略了一些代码。

测试结果为250ms,又快了一点点,为什么呢,我分析认为第一是减少了循环计数的时间,第二循环展开的 乘以 常数会被CPU优化为相关的移位或其他操作,而Calc2内部编译器是无法优化的。

这样的函数系统一般是不会内联的,即使你在函数前面加上inline标识符,但是你可以在前面加上__forceinline标识,强制他内联,但是如果你这样做,你会发现速度反而会严重下降,为什么,请大家自行分析。

我们在自己仔细看看,上面的循环很容易用SSE函数实现,既然我们的直方图的获取和更新利用了SSE,这里为什么不用呢,这样就诞生了我们的Calc4函数。

//    用SSE优化的算法
unsigned char Calc4(unsigned short *Hist, unsigned char Value, unsigned short *Intensity, unsigned short *Level)
{
    unsigned short *Offset = Intensity + 255 - Value;
    __m128i SumS = _mm_setzero_si128();
    __m128i WeightS = _mm_setzero_si128();
    for (int K = 0; K < 256; K += 8)
    {
        __m128i H = _mm_load_si128((__m128i const *)(Hist + K));
        __m128i L = _mm_load_si128((__m128i const *)(Level + K));                //    有能力可以使用256位的AVX寄存器
        __m128i I = _mm_loadu_si128((__m128i const *)(Offset + K));
        SumS = _mm_add_epi32(_mm_madd_epi16(_mm_mullo_epi16(L, I), H), SumS);
        WeightS = _mm_add_epi32(_mm_madd_epi16(H, I), WeightS);
    }
    const int *WW = (const int *)&WeightS;
    const int *SS = (const int *)&SumS;

    int Sum = SS[0] + SS[1] + SS[2] + SS[3];
    int Divisor = WW[0] + WW[1] + WW[2] + WW[3];
    if (Divisor > 0)
        return (Sum + (Divisor >> 1)) / Divisor;        //    四舍五入
    else
        return Value;
}

  关于上面几个SSE函数的使用,我不想多谈,也没啥难易理解的,注意其中的Level是我们为了方便,预定义的一个表,其形式如下:

for (int Y = 0; Y < 256; Y++)    Level[Y] = Y;            //    这个是为CalcSSE方便的使用的,其他两可以删除掉这里

不定义这个也应该可以由其他的SSE函数构造k/k+1/k+2/k+3/k+4/k+5/k+6/k+7这样的__m128i变量,我这里这样做只是为了方便,你也可以自己更改下。

我们直接把Calc4嵌入到程序中,运行,发现运行时间降低到了100ms,比Calc3有提高了2倍多,但是效果似乎不对,怎么回事呢。

这主要是因为上述的SSE函数是针对unsigned short类型,而我们构造的Intensity数据较大,进行乘法后会超出unsigned short所能表达的范围,因此我们需要改动Intensity的定义:

    //    为了SSE里不溢出,把这里的数据变小,当然这样算法的准确度降低了,但是为了速度.......
    for (int Y = -255; Y <= 255; Y++)
    {
        int Factor = (255 - abs(Y) * 100 / Threshold);
        if (Factor < 0) Factor = 0;
        Intensity[Y + 255] = Factor / 2;
    }

  最后一个除以2估计是因为SSE内部还是按照signed short处理的,这样做会导致算法的精度降低。

经过上述改动,效果就正确了。

对于彩色图像,一种做法就是直接扩展现在单通道的代码,让其支持三通道,另外一个办法就是把图像先拆分成3通道独立的数据,然后没通道独立处理,处理完成后再合成,这样做有两个好处,第一是代码复用;第二就是如果支持Openmp或者其他的并行库,可以让3通道并行起来执行。但是也有2个不足,第一是内存占用会增加很多,因为这种算法是不支持In-Place操作的,所以必须分配6份单通道的数据,而算法内部分配的内存由于并行的关系也要增加一些(不是三倍),及时考虑到可以把其中三个通道的数放置到Dest中,也会增加3份通道的数据,这对于某些设备可能是难以接受的(比如低端的安卓机)。具体如何使用就看应用场景了。

我看到很多人转载我的文章,我很感谢,但是很多人没有一点点的尊重别人的意识,转载请你在博文的最前面声明为转载,并不要更改本文下部赞助二维码。

写博不易,土豪请打赏,屌丝一分也是爱(非强制要求):     

      

本文的完整VS2013代码下载地址(解压密码本人博客名):http://files.cnblogs.com/files/Imageshop/SurfaceBlur.rar

****************************作者: laviewpbt   时间: 2015.10.24    联系QQ:  33184777 转载请保留本行信息**********************

时间: 2024-10-23 10:07:08

O(1)效率的表面模糊算法优化。的相关文章

性能优化——算法优化

背景 由于某种原因,我们系统需要记录另一个系统中一个表里的id.但是,当我们记录完了以后,别人系统可能会删除那个表里的一些数据,这样的话,我们这边就多了一些无效数据,所以,我们必须的找到这些无效的id,然后将其删除. 开始,我们的实现是这样:我们将记录下来的所有id放在一个list里,然后传到另一个系统,他将他们已经删除的id返回.具体处理代码如下: <pre name="code" class="java">public String findDele

剪枝算法(算法优化)

一:剪枝策略的寻找的方法 1)微观方法:从问题本身出发,发现剪枝条件 2)宏观方法:从整体出发,发现剪枝条件. 3)注意提高效率,这是关键,最重要的. 总之,剪枝策略,属于算法优化范畴:通常应用在DFS 和 BFS 搜索算法中:剪枝策略就是寻找过滤条件,提前减少不必要的搜索路径. 二:剪枝算法(算法优化) 1.简介 在搜索算法中优化中,剪枝,就是通过某种判断,避免一些不必要的遍历过程,形象的说,就是剪去了搜索树中的某些"枝条",故称剪枝.应用剪枝优化的核心问题是设计剪枝判断方法,即确定

算法优化:rgb向yuv的转化最优算法

朋友曾经给我推荐了一个有关代码优化的pdf文档<让你的软件飞起来>,看完之后,感受颇深.为了推广其,同时也为了自己加深印象,故将其总结为word文档.下面就是其的详细内容总结,希望能于己于人都有所帮助. 速度取决于算法 同样的事情,方法不一样,效果也不一样.比如,汽车引擎,可以让你的速度超越马车,却无法超越音速:涡轮引擎,可以轻松 超越音障,却无法飞出地球:如果有火箭发动机,就可以到达火星. 代码的运算速度取决于以下几个方面 1.  算法本身的复杂度,比如MPEG比JPEG复杂,JPEG比BM

内部元素一一相应的集合的算法优化,从list到hashmap

说是算法优化,基本上是在吹牛,仅仅只是算是记录下,我写代码时候的思路.毕竟还是小菜鸟. 我要开一个party,与会者都是情侣,可是情侣并非一起过来的,而是有先有后,可是每位与会者来的时候都拿着一束鲜花,第一件事情就是送给自己的伴侣. 设计一个算法,最高效率的解决这个事情. 最開始的时候,是这种. import java.util.ArrayList; import java.util.List; public class TestParty { List<Person> persons = n

算法优化分析---打印素数

1.针对 不同的代码,时间复杂度会有所不一样 时间复杂度:O(1),O(n),O(n^2) 2.for 循环一般最多使用2层,3层以上效率都很低 3.能使用for循环的地方尽量使用for循环,不要去使用while循环---while循环需要设计一个自增数count 算法优化实例比较:合数法求质数 原文地址:https://www.cnblogs.com/pengwa1226/p/10116915.html

python 简单算法优化

计算机界著名公式,由瑞士计算机科学家尼克劳斯.威茨(Niklaus Wirth)提出,也因此获得图灵奖程序 = 数据结构 + 算法没有看过数据结构和算法,有时面对问题可能会没有任何思路,不知如何下手去解决:大部分时间可能解决了问题,可是对程序运行的效率和开销没有意识,性能低下:有时会借助别人开发的利器暂时解决了问题,可是遇到性能瓶颈的时候,又不知该如何进行*针对性的优化* 模拟场景a,b,c三个数1. a+b+c = 1000(0--1000)2.a**2 +b**2 = c**2求a,b,c可

[算法] 优化算法 梯度下降

导数 导数是一个数,函数y(x)在x0点的导数y'(x0)反应了在x0处y随x的变化快慢 微分 微分指函数值的微小变化 在x0可微:在x0点y和x的微分成线性关系(只与该点函数值有关) 导数可看做函数的微分与自变量的微分之商,故导数又称微商 偏导数 函数在一点处沿坐标轴的变化率 方向导数 函数在一点处沿射线方向的变化率 偏导数是双侧的,方向导数是单侧的.函数f(x,y)在一点处对x偏导数等于沿x轴正向的方向导数 梯度 梯度是一个向量 方向导数沿梯度方向取最大值,最大值为梯度的模,即沿梯度方向函数

SQL Server 聚合函数算法优化技巧

Sql server聚合函数在实际工作中应对各种需求使用的还是很广泛的,对于聚合函数的优化自然也就成为了一个重点,一个程序优化的好不好直接决定了这个程序的声明周期.Sql server聚合函数对一组值执行计算并返回单一的值.聚合函数对一组值执行计算,并返回单个值.除了 COUNT 以外,聚合函数都会忽略空值. 聚合函数经常与 SELECT 语句的 GROUP BY 子句一起使用. v1.写在前面 如果有对Sql server聚合函数不熟或者忘记了的可以看我之前的一片博客.sql server 基

冒泡排序及其算法优化分析

1.基本冒泡排序 冒泡排序的基本思想:假设被排序的记录数组d[1...N]垂直竖立,将每个记录d[i]看作是一个气泡,那么重的气泡就会向下下沉,轻的气泡就会向上升.每次都是相邻的两个气泡d[i]和d[i+1]进行比较.如果d[i]>d[i+1],那么就交换两个气泡,然后在比较d[i+1]和d[i+2],以此类推,知道所有的气泡都有序排列.假设排序20,37,11,42,29. 第1次冒泡:20.37,11,42,29 d[0]和d[1]比较 第2次冒泡:20,11,37,42,29 d[1]和d