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份通道的数据,这对于某些设备可能是难以接受的(比如低端的安卓机)。具体如何使用就看应用场景了。
针对实际的应用,一种可选的进一步加速的方式就是把图像的色阶范围进一步缩小,比如由256色阶变为128或者64色阶,这样理论上还可以在快2倍到4倍,不过效果会稍有下降,一般128位时还是可以接受的。
本文的完整VS2013代码下载地址(解压密码本人博客名):http://files.cnblogs.com/files/Imageshop/SurfaceBlur.rar
我看到很多人转载我的文章,我很感谢,但是很多人没有一点点的尊重别人的意识,转载请你在博文的最前面声明为转载,并不要更改本文下部打赏二维码。

****************************作者: laviewpbt 时间: 2015.10.24 联系QQ: 33184777 转载请保留本行信息**********************
O(1)效率的表面模糊算法优化。的更多相关文章
- Harris角点检测算法优化
Harris角点检测算法优化 一.综述 用 Harris 算法进行检测,有三点不足:(1 )该算法不具有尺度不变性:(2 )该算法提取的角点是像素级的:(3 )该算法检测时间不是很令人满意. 基于以上 ...
- 内部元素一一相应的集合的算法优化,从list到hashmap
说是算法优化,基本上是在吹牛,仅仅只是算是记录下,我写代码时候的思路.毕竟还是小菜鸟. 我要开一个party,与会者都是情侣,可是情侣并非一起过来的,而是有先有后,可是每位与会者来的时候都拿着一束鲜花 ...
- SQL Server 聚合函数算法优化技巧
Sql server聚合函数在实际工作中应对各种需求使用的还是很广泛的,对于聚合函数的优化自然也就成为了一个重点,一个程序优化的好不好直接决定了这个程序的声明周期.Sql server聚合函数对一组值 ...
- RSA算法优化
RSA算法优化 大数乘法 模乗优化 剩余定理(孙子定理) RSA加解密 python的RSA计算优化 #-*- coding: utf-8 -*- ''' /********************* ...
- Spark Mllib里的协调过滤的概念和实现步骤、LS、ALS的原理、ALS算法优化过程的推导、隐式反馈和ALS-WR算法
不多说,直接上干货! 常见的推荐算法 1.基于关系规则的推荐 2.基于内容的推荐 3.人口统计式的推荐 4.协调过滤式的推荐 (广泛采用) 协调过滤的概念 在现今的推荐技术和算法中,最被大家广泛认可和 ...
- LeetCode :2.两数相加 解题报告及算法优化思路
题目连接:2.两数相加 题意 题目难度标为 中等, 因为题意上有一部分理解难度,以及需要数据结构的链表基础. 还不知道到链表的童鞋可以粗略的看下百度百科或者是翻出数据结构的书看一看,通俗一点的语言来解 ...
- deeplearning算法优化原理
deeplearning算法优化原理目录· 量化原理介绍 · 剪裁原理介绍 · 蒸馏原理介绍 · 轻量级模型结构搜索原理介绍 1. Quantization Aware Training量化介绍1.1 ...
- 【51nod1674】区间的价值 V2(算法效率--位运算合并优化+链表实现)
题目链接: 51nod1674 题意:规定一个区间的价值为这个区间中所有数and起来的值与这个区间所有数or起来的值的乘积.现在l有一个 N 个数的序列,问所有n*(n+1)/2个区间的贡献的和对1 ...
- 算法优化:rgb向yuv的转化最优算法,快得让你吃惊!
朋友曾经给我推荐了一个有关代码优化的pdf文档<让你的软件飞起来>,看完之后,感受颇深.为了推广其,同时也为了自己加深印象,故将其总结为word文档.下面就是其的详细内容总结,希望能于己于 ...
随机推荐
- 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统(10)-系统菜单栏[附源码]
系列目录 似乎我们需要更多的模块了,我们有一个样例程序,可以帮助我们以后的系统开发做很多对照,我们稍后还有系统日志和系统异常的记录,这时浏览发生了困难,我们这一节来完成一个大家比较喜欢的东西吧,系统菜 ...
- Angular学习-指令入门
1.指令的定义 从用户的角度来看,指令就是在应用的模板中使用的自定义HTML标签.指令可以很简单,也可以很复杂.AngularJS的HTML编译器会解析指令,增强模板的功能.也是组件化未来的发展趋势, ...
- 设计模式(十二):通过ATM取款机来认识“状态模式”(State Pattern)
说到状态模式,如果你看过之前发布的重构系列的文章中的<代码重构(六):代码重构完整案例>这篇博客的话,那么你应该对“状态模式”并不陌生,因为我们之前使用到了状态模式进行重构.上一篇博客我们 ...
- MySQL笔记---视图,存储过程, 触发器的使用入门
大二学数据库的时候,只是隐约听到老师提起过视图啊,存储过程啊,触发器啊什么的,但只是淡淡的记住了名字,后来自己做些小项目,小程序,也没有用上过,都只是简单的建表,关联表之类的,导致我对这些东西的理解只 ...
- WCF之安全性
WCF 客户端代理生成 通过SvcUtil.exe http://www.cnblogs.com/woxpp/p/6232298.html WCF 安全性 之 None http://www.cnbl ...
- 利用Python进行数据分析(4) NumPy基础: ndarray简单介绍
一.NumPy 是什么 NumPy 是 Python 科学计算的基础包,它专为进行严格的数字处理而产生.在之前的随笔里已有更加详细的介绍,这里不再赘述. 利用 Python 进行数据分析(一)简单介绍 ...
- JQuery Sizzle引擎源代码分析
最近在拜读艾伦在慕课网上写的JQuery课程,感觉在国内对JQuery代码分析透彻的人没几个能比得过艾伦.有没有吹牛?是不是我说大话了? 什么是Sizzle引擎? 我们经常使用JQuery的选择器查询 ...
- DOM 事件深入浅出(二)
在DOM事件深入浅出(一)中,我主要给大家讲解了不同DOM级别下的事件处理程序,同时介绍了事件冒泡和捕获的触发原理和方法.本文将继续介绍DOM事件中的知识点,主要侧重于DOM事件中Event对象的属性 ...
- TeamCity : .NET Core 插件
笔者在<TeamCity : 配置 Build 过程>一文中提到 "TeamCity 内置支持几乎所有的 build 类型".在当今这个软件语言和各种框架飞速发展的时代 ...
- Xcode7.1环境下上架iOS App到AppStore 流程① (Part 一)
前言部分 之前App要上架遇到些问题到网上搜上架教程发现都是一些老的版本的教程 ,目前iTunesConnect 都已经迭代好几个版本了和之前的 界面风格还是有很大的差别的,后面自己折腾了好久才终于把 ...