同步简书:Android 图片高斯模糊解决方案 html
近年来,图片高斯模糊备受设计师的青睐,在各大知名APP中,如微信、手机QQ、网易云音乐等等都有对背景高斯图模糊的设计,在Adnroid 中,如今经常使用的图片高斯模糊技术有三种:RenderScript 、fastBlur、对RenderScript和fastBlur的优化,接下来分别分析各自的优缺点和在项目中该使用哪一个解决方案。先上一张效果图:java
RenderScript是在Android上的高性能运行密集型运算的框架,RenderScript主要用于数据并行计算,尤为对图像处理、摄影分析和计算机视觉特别有用。RenderScript是在Android3.0(API 11)引入的。而Android图片高斯模糊处理,一般也是用这个库来完成。它提供了咱们Java层调用的API,其实是在c/c++ 层来处理的,因此它的效率和性能一般是最高的。要使用RenderScript完成图片高斯模糊只须要如下几步:
(1) 初始化一个RenderScript Context:RenderScript 上下文环境经过create(Context)
方法来建立,它保证RenderScript的使用而且提供一个控制后续全部RenderScript对象(如:ScriptIntrinsicBlur、Allocation等)生命周期的对象。android
(2)经过Script至少建立一个Allocation:一个Allocation是提供存储大量可变数据的RenderScript 对象。在内核中,Allocation做为输入和输出,在内核中经过rsGetElementAt_type ()
和rsSetElementAt_type()
方法来访问Allocation当script全局绑定的时候。使用createFromBitmap
和createTyped
来建立Allocation。c++
(3)建立ScriptIntrinsic:它内置了RenderScript 的一些通用操做,如高斯模糊、扭曲变换、图像混合等等,更多的操做请看ScriptIntrinsic
的子类,本文要用的高斯模糊处理就是用的它的子类ScriptIntrinsicBlur
。
(4)填充数据到Allocations:除了使用方法createFromBitmap
建立的Allocation外,其它的第一次建立时都是填充的空数据。git
(5) 设置模糊半径:设置一个模糊的半径,其值为 0-25。github
(6) 启动内核,调用方法处理:调用forEach 方法模糊处理。算法
(7) 从Allocation 中拷贝数据:为了能在Java层访问Allocation的数据,用Allocation其中一个copy
方法来拷贝数据。
(8) 销毁RenderScript对象:能够用destroy
方法来销毁RenderScript对象或者让它能够被垃圾回收,destroy 以后,就能在用它控制的RenderScript对象了(好比在销毁了以后,再调用ScriptIntrinsic或者Allocation的方法是要抛异常的)。promise
以上几个步骤就能够完成的图片的高斯模糊,看一下对应的代码:微信
private static Bitmap rsBlur(Context context,Bitmap source,int radius){
Bitmap inputBmp = source;
//(1)
RenderScript renderScript = RenderScript.create(context);
Log.i(TAG,"scale size:"+inputBmp.getWidth()+"*"+inputBmp.getHeight());
// Allocate memory for Renderscript to work with
//(2)
final Allocation input = Allocation.createFromBitmap(renderScript,inputBmp);
final Allocation output = Allocation.createTyped(renderScript,input.getType());
//(3)
// Load up an instance of the specific script that we want to use.
ScriptIntrinsicBlur scriptIntrinsicBlur = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
//(4)
scriptIntrinsicBlur.setInput(input);
//(5)
// Set the blur radius
scriptIntrinsicBlur.setRadius(radius);
//(6)
// Start the ScriptIntrinisicBlur
scriptIntrinsicBlur.forEach(output);
//(7)
// Copy the output to the blurred bitmap
output.copyTo(inputBmp);
//(8)
renderScript.destroy();
return inputBmp;
}复制代码
上面对应的步骤已经用序号标出,代码就十行左右,很简单。这就十Android提供给咱们的能够处理图片高斯模糊的库。性能比较好,由于是在c/c++层作的处理。可是它只能在API 17或者更高的版本使用, 看一下文档的说明:app
如上图,红框中标记的ScriptIntrinsicBlur 是在API 17加入的,所以低版本的手机是用不了,为了能兼容低版本的手机,咱们还得探索其余方案。
RenderScript 兼容包:
所幸的是,Google 为了兼容低版本也能够用RenderScript,加了一个兼容包,android.support.v8.renderscript ,使用support.v8.renderscript
就能兼容到Android 2.3版本(API 9),如今市面上估计没有比2.3版本还低的手机了(4.x 的手机都很少了)。使用兼容包和使用原生的RenderScript彻底同样,代码仍是上面的代码。只是须要在app 的build.gradle添加以下的代码
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 9
targetSdkVersion 19
// 使用support.v8.renderscript
renderscriptTargetApi 18
renderscriptSupportModeEnabled true
}
}复制代码
只要添加上面的2行代码就好了。可是有2点须要注意:
注意:
1,Android SDK Tools revision 22.2 or higher(Tools 须要22.2或者更高的版本)
2,Android SDK Build-tools revision 18.1.0 or higher( Build-tools 须要18.1.0或者更高的版本)
若是没有达到的话,经过Anroid SDK Manager 更新安装。
有了兼容包,那么RenderScript就是一个完美的解决方案了吗?答案是NO,还有2个缺点:
所以咱们还要找一下其余方案,接下来看一下fastBlur算法。
fastBlur 是除了RenderScript 以外的另外一种方法,它直接在Java层作图片的模糊处理。对每一个像素点应用高斯模糊计算、最后在合成Bitmap。请看源码:
/** * Stack Blur v1.0 from * http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html * Java Author: Mario Klingemann <mario at quasimondo.com> * http://incubator.quasimondo.com * * created Feburary 29, 2004 * Android port : Yahel Bouaziz <yahel at kayenko.com> * http://www.kayenko.com * ported april 5th, 2012 * * This is a compromise between Gaussian Blur and Box blur * It creates much better looking blurs than Box Blur, but is * 7x faster than my Gaussian Blur implementation. * * I called it Stack Blur because this describes best how this * filter works internally: it creates a kind of moving stack * of colors whilst scanning through the image. Thereby it * just has to add one new block of color to the right side * of the stack and remove the leftmost color. The remaining * colors on the topmost layer of the stack are either added on * or reduced by one, depending on if they are on the right or * on the left side of the stack. * * If you are using this algorithm in your code please add * the following line: * Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com> */
private static Bitmap fastBlur(Bitmap sentBitmap, float scale, int radius) {
int width = Math.round(sentBitmap.getWidth() * scale);
int height = Math.round(sentBitmap.getHeight() * scale);
sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false);
Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
if (radius < 1) {
return (null);
}
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] pix = new int[w * h];
Log.e("pix", w + " " + h + " " + pix.length);
bitmap.getPixels(pix, 0, w, 0, 0, w, h);
int wm = w - 1;
int hm = h - 1;
int wh = w * h;
int div = radius + radius + 1;
int r[] = new int[wh];
int g[] = new int[wh];
int b[] = new int[wh];
int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
int vmin[] = new int[Math.max(w, h)];
int divsum = (div + 1) >> 1;
divsum *= divsum;
int dv[] = new int[256 * divsum];
for (i = 0; i < 256 * divsum; i++) {
dv[i] = (i / divsum);
}
yw = yi = 0;
int[][] stack = new int[div][3];
int stackpointer;
int stackstart;
int[] sir;
int rbs;
int r1 = radius + 1;
int routsum, goutsum, boutsum;
int rinsum, ginsum, binsum;
for (y = 0; y < h; y++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
for (i = -radius; i <= radius; i++) {
p = pix[yi + Math.min(wm, Math.max(i, 0))];
sir = stack[i + radius];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rbs = r1 - Math.abs(i);
rsum += sir[0] * rbs;
gsum += sir[1] * rbs;
bsum += sir[2] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
}
stackpointer = radius;
for (x = 0; x < w; x++) {
r[yi] = dv[rsum];
g[yi] = dv[gsum];
b[yi] = dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if (y == 0) {
vmin[x] = Math.min(x + radius + 1, wm);
}
p = pix[yw + vmin[x]];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer = (stackpointer + 1) % div;
sir = stack[(stackpointer) % div];
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];
yi++;
}
yw += w;
}
for (x = 0; x < w; x++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
yp = -radius * w;
for (i = -radius; i <= radius; i++) {
yi = Math.max(0, yp) + x;
sir = stack[i + radius];
sir[0] = r[yi];
sir[1] = g[yi];
sir[2] = b[yi];
rbs = r1 - Math.abs(i);
rsum += r[yi] * rbs;
gsum += g[yi] * rbs;
bsum += b[yi] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
if (i < hm) {
yp += w;
}
}
yi = x;
stackpointer = radius;
for (y = 0; y < h; y++) {
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
pix[yi] = ( 0xff000000 & pix[yi] ) | ( dv[rsum] << 16 ) | ( dv[gsum] << 8 ) | dv[bsum];
rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;
stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];
routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];
if (x == 0) {
vmin[y] = Math.min(y + r1, hm) * w;
}
p = x + vmin[y];
sir[0] = r[p];
sir[1] = g[p];
sir[2] = b[p];
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
rsum += rinsum;
gsum += ginsum;
bsum += binsum;
stackpointer = (stackpointer + 1) % div;
sir = stack[stackpointer];
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];
yi += w;
}
}
Log.e("pix", w + " " + h + " " + pix.length);
bitmap.setPixels(pix, 0, w, 0, 0, w, h);
return (bitmap);
}复制代码
如上所示,就一个方法,使用这种方式不会有兼容性问题,也不会引入jar包致使APK变大。可是这种方法的效率是很是低的,想一想也知道,由于是在Java 层处理,速度固然慢。测试了一张800 x 450 的图片,RenderScript平均25 ms 左右,fastBlur平均310ms 左右,至关于差了10倍。还有就是使用这种方式是把图片所有加载到内存,若是图片较大,容易致使OOM。
上面对RenderScript 和fastBlur作了分析,虽然RenderScript的效率要比fastBlur 好不少,可是仍是有可能达不到16ms每一帧的要求而致使卡顿。因此须要进行优化。
思路: 在stackOverFlow上有提供优化思路(地址:stackoverflow.com/questions/2…
,原理是这样的:经过缩小图片,使其丢失一些像素点,接着进行模糊化处理,而后再放大到原来尺寸。因为图片缩小后再进行模糊处理,须要处理的像素点和半径都变小,从而使得模糊处理速度加快。
所以咱们只须要将原来的图片缩小,而后在用RenderScript 或者fastBlur 处理,就能够加快速度了,添加以下代码:
int width = Math.round(source.getWidth() * scale);
int height = Math.round(source.getHeight() * scale);
Bitmap inputBmp = Bitmap.createScaledBitmap(source,width,height,false);复制代码
renderScript 高斯模糊的完整方法以下:
private static Bitmap rsBlur(Context context,Bitmap source,int radius,float scale){
Log.i(TAG,"origin size:"+source.getWidth()+"*"+source.getHeight());
int width = Math.round(source.getWidth() * scale);
int height = Math.round(source.getHeight() * scale);
Bitmap inputBmp = Bitmap.createScaledBitmap(source,width,height,false);
RenderScript renderScript = RenderScript.create(context);
Log.i(TAG,"scale size:"+inputBmp.getWidth()+"*"+inputBmp.getHeight());
// Allocate memory for Renderscript to work with
final Allocation input = Allocation.createFromBitmap(renderScript,inputBmp);
final Allocation output = Allocation.createTyped(renderScript,input.getType());
// Load up an instance of the specific script that we want to use.
ScriptIntrinsicBlur scriptIntrinsicBlur = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
scriptIntrinsicBlur.setInput(input);
// Set the blur radius
scriptIntrinsicBlur.setRadius(radius);
// Start the ScriptIntrinisicBlur
scriptIntrinsicBlur.forEach(output);
// Copy the output to the blurred bitmap
output.copyTo(inputBmp);
renderScript.destroy();
return inputBmp;
}复制代码
先对Bitmap 缩小,而后再模糊处理。
Note:缩小的系数应该为2的整数次幂 ,即上面代码中的scale应该为1/二、1/四、1/8 ... 参考BitmapFactory.Options 对图片缩放 的inSample系数。据前辈们经验,通常scale = 1/8 为佳。
看一下使用RenderScript和fastBlur 以及优化后,高斯模糊一张图片所花时间的对比表,测试机型为魅族metal,系统为Android 5.1,以下:
如上图:以1080 x 1349 的图片为例(每个半径取5次的均值),使用原尺寸用两种方法进行高斯模糊,RenderScript的效率比fastBlur高,大约快10倍,可是都超过了16ms,而使用优化方法后,使其先缩小8倍,再模糊,2种方法效率都有质的提升,RenderScript模糊时间不足5ms,fastBlur 也接近16ms,半径为15如下小与16ms。
所以无论使用哪一种方法模糊图片,都应该先优化,再模糊。
RenderScript 优势:
RenderScript 缺点:
fastBlur的优势:
fastBlur的缺点:
以上对比了2种方法的优缺点,各有优劣,那么咱们到底选择哪种呢?这个须要看状况而定,给出下面2种方案:
高斯模糊方案一: 若是APK自己较小,能够接受增大的160k体积,那么直接使用兼容包的RenderScript (注意须要先优化,用上面的先缩小再模糊)。
高斯模糊方案二:若是不想APK体积增大,那么在模糊的时候作判断, API版本大于17 ,直接使用原生的RenderScript模糊,API版本小于17,则用fastBlur方法。(一样须要先优化,后模糊)。
因为高斯模糊在项目中用得比较多,而每个项目都去拷贝代码,这样很麻烦,而且不优雅,所以,对这两种方法优化后,封装成了一个Lib,要使用时直接添加依赖就行。
添加依赖:
1, 最外层build.gradle 添加一下代码:
allprojects {
repositories {
jcenter()
maven {url "https://jitpack.io"}
}
}复制代码
2,app 的build.gradle添加:
dependencies {
compile 'com.github.pinguo-zhouwei:EasyBlur:v1.0.0'
}复制代码
3,app 的build.gradle添加:
defaultConfig {
applicationId "com.zhouwei.easyblur"
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//使用renderscript 兼容包
renderscriptTargetApi 25
renderscriptSupportModeEnabled true
}复制代码
使用方法:
1,简单使用,指定Bitmap和半径
Bitmap finalBitmap = EasyBlur.with(MainActivity.this)
.bitmap(overlay) //要模糊的图片
.radius(10)//模糊半径
.blur();复制代码
2,能够指定缩小的倍数,默认缩小倍数为8
Bitmap finalBitmap = EasyBlur.with(MainActivity.this)
.bitmap(overlay) //要模糊的图片
.radius(10)//模糊半径
.scale(4)//指定模糊前缩小的倍数
.blur();复制代码
3, 指定使用哪种方法,默认是使用兼容的RenderScript 高斯模糊
Bitmap finalBitmap = EasyBlur.with(MainActivity.this)
.bitmap(overlay) //要模糊的图片
.radius(10)//模糊半径
.scale(4)//指定模糊前缩小的倍数
.policy(EasyBlur.BlurPolicy.FAST_BLUR)//使用fastBlur
.blur();复制代码
代码已经上传Github:EasyBlur
RenderScript API 指南
android图片处理之图像模糊
高斯模糊实现方案探究
Fast Bitmap Blur For Android SDK