Android 图片高斯模糊解决方案

SaraMighell 7年前
   <p>近年来,图片高斯模糊备受设计师的青睐,在各大知名APP中,如微信、手机QQ、网易云音乐等等都有对背景高斯图模糊的设计,在Adnroid 中,现在常用的图片高斯模糊技术有三种: <strong>RenderScript</strong> 、 <strong>fastBlur</strong> 、 <strong>对RenderScript和fastBlur的优化</strong> ,接下来分别分析各自的优缺点和在项目中该使用哪个解决方案。先上一张效果图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7a6ee537f8c4f595cf0b421679740a32.png"></p>    <p style="text-align:center">高斯模糊效果图.png</p>    <h3>1,RenderScript</h3>    <p>RenderScript是在Android上的高性能运行密集型运算的框架,RenderScript主要用于数据并行计算,尤其对图像处理、摄影分析和计算机视觉特别有用。RenderScript是在Android3.0(API 11)引入的。而Android图片高斯模糊处理,通常也是用这个库来完成。它提供了我们Java层调用的API,实际上是在c/c++ 层来处理的,所以它的效率和性能通常是最高的。要使用RenderScript完成图片高斯模糊只需要以下几步:</p>    <p>(1) <strong>初始化一个RenderScript Context</strong> :RenderScript 上下文环境通过 create(Context) 方法来创建,它保证RenderScript的使用并且提供一个控制后续所有RenderScript对象(如:ScriptIntrinsicBlur、Allocation等)生命周期的对象。</p>    <p>(2) <strong>通过Script至少创建一个Allocation</strong> :一个Allocation是提供存储大量可变数据的RenderScript 对象。在内核中,Allocation作为输入和输出,在内核中通过 rsGetElementAt_type () 和 rsSetElementAt_type() 方法来访问Allocation当script全局绑定的时候。使用 createFromBitmap 和 createTyped 来创建Allocation。</p>    <p>(3) <strong>创建ScriptIntrinsic</strong> :它内置了RenderScript 的一些通用操作,如高斯模糊、扭曲变换、图像混合等等,更多的操作请看 ScriptIntrinsic 的子类,本文要用的高斯模糊处理就是用的它的子类 ScriptIntrinsicBlur 。</p>    <p>(4) <strong>填充数据到Allocations</strong> :除了使用方法 createFromBitmap 创建的Allocation外,其它的第一次创建时都是填充的空数据。</p>    <p>(5) <strong>设置模糊半径</strong> :设置一个模糊的半径,其值为 0-25。</p>    <p>(6) <strong>启动内核,调用方法处理</strong> :调用forEach 方法模糊处理。</p>    <p>(7) <strong>从Allocation 中拷贝数据</strong> :为了能在Java层访问Allocation的数据,用Allocation其中一个 copy 方法来拷贝数据。</p>    <p>(8) <strong>销毁RenderScript对象</strong> :可以用 destroy 方法来销毁RenderScript对象或者让它可以被垃圾回收,destroy 之后,就能在用它控制的RenderScript对象了(比如在销毁了之后,再调用ScriptIntrinsic或者Allocation的方法是要抛异常的)。</p>    <p>以上几个步骤就可以完成的图片的高斯模糊,看一下对应的代码:</p>    <pre>  <code class="language-java">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;      }</code></pre>    <p>上面对应的步骤已经用序号标出,代码就十行左右,很简单。这就十Android提供给我们的可以处理图片高斯模糊的库。性能比较好,因为是在c/c++层做的处理。 <strong>但是它只能在API 17或者更高的版本使用</strong> , 看一下文档的说明:</p>    <p><img src="https://simg.open-open.com/show/61a265840f302d1c9f4900a2c8270d49.png"></p>    <p style="text-align:center">RC——API.png</p>    <p>如上图,红框中标记的ScriptIntrinsicBlur 是在API 17加入的,因此低版本的手机是用不了,为了能兼容低版本的手机,我们还得探索其他方案。</p>    <p>RenderScript 兼容包:</p>    <p>所幸的是,Google 为了兼容低版本也可以用RenderScript,加了一个兼容包, <strong> android.support.v8.renderscript </strong> ,使用 support.v8.renderscript 就能兼容到Android 2.3版本(API 9),现在市面上估计没有比2.3版本还低的手机了(4.x 的手机都不多了)。使用兼容包和使用原生的RenderScript完全一样,代码还是上面的代码。只是需要在app 的build.gradle添加如下的代码</p>    <pre>  <code class="language-java">android {      compileSdkVersion 23      buildToolsVersion "23.0.3"        defaultConfig {          minSdkVersion 9          targetSdkVersion 19            // 使用support.v8.renderscript          renderscriptTargetApi 18          renderscriptSupportModeEnabled true        }  }</code></pre>    <p>只要添加上面的2行代码就行了。但是有2点需要注意:</p>    <p>注意:</p>    <p>1,Android SDK Tools revision 22.2 or higher(Tools 需要22.2或者更高的版本)</p>    <p>2,Android SDK Build-tools revision 18.1.0 or higher( Build-tools 需要18.1.0或者更高的版本)</p>    <p>如果没有达到的话,通过Anroid SDK Manager 更新安装。</p>    <p>有了兼容包,那么RenderScript就是一个完美的解决方案了吗?答案是NO,还有2个缺点:</p>    <ul>     <li>虽然RenderScript效率不错,但是处理尺寸大一点的图片还是达不到16ms每一帧,需要优化</li>     <li>虽然兼容包能解决API17以下不能使用的问题,但是引入兼容包又带来了新的问题,APK 的包大小增大了,support.v8.renderscript有160k,现在各家的APP都在要求APK瘦身,对于那种本来就很大的APK来说还是不能接受的。</li>    </ul>    <p>因此我们还要找一下其他方案,接下来看一下 <strong>fastBlur算法</strong> 。</p>    <h3>2,fastBlur</h3>    <p>fastBlur 是除了RenderScript 之外的另一种方法,它直接在Java层做图片的模糊处理。对每个像素点应用高斯模糊计算、最后在合成Bitmap。请看源码:</p>    <pre>  <code class="language-java">/**       * 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);      }</code></pre>    <p>如上所示,就一个方法,使用这种方式不会有兼容性问题,也不会引入jar包导致APK变大。但是这种方法的效率是非常低的,想想也知道,因为是在Java 层处理,速度当然慢。测试了一张800 x 450 的图片,RenderScript平均25 ms 左右,fastBlur平均310ms 左右,相当于差了10倍。还有就是使用这种方式是把图片全部加载到内存,如果图片较大,容易导致OOM。</p>    <h3>3,对RenderScript 和fastBlur 的优化</h3>    <p>上面对RenderScript 和fastBlur做了分析,虽然RenderScript的效率要比fastBlur 好很多,但是还是有可能达不到16ms每一帧的要求而导致卡顿。所以需要进行优化。</p>    <p>思路:在stackOverFlow上有提供优化思路</p>    <p>,原理是这样的:通过缩小图片,使其丢失一些像素点,接着进行模糊化处理,然后再放大到原来尺寸。由于图片缩小后再进行模糊处理,需要处理的像素点和半径都变小,从而使得模糊处理速度加快。</p>    <p>因此我们只需要将原来的图片缩小,然后在用RenderScript 或者fastBlur 处理,就可以加快速度了,添加如下代码:</p>    <pre>  <code class="language-java">int width = Math.round(source.getWidth() * scale);          int height = Math.round(source.getHeight() * scale);            Bitmap inputBmp = Bitmap.createScaledBitmap(source,width,height,false);</code></pre>    <p>renderScript 高斯模糊的完整方法如下:</p>    <pre>  <code class="language-java">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;      }</code></pre>    <p>先对Bitmap 缩小,然后再模糊处理。</p>    <p>Note:缩小的系数应该为2的整数次幂 ,即上面代码中的scale应该为1/2、1/4、1/8 ... 参考BitmapFactory.Options 对图片缩放 的inSample系数。据前辈们经验,一般scale = 1/8 为佳。</p>    <p>看一下使用RenderScript和fastBlur 以及优化后,高斯模糊一张图片所花时间的对比表,测试机型为魅族metal,系统为Android 5.1,如下:</p>    <p><img src="https://simg.open-open.com/show/f764644aa9a14c3b40e6722e48c7328f.png"></p>    <p style="text-align:center">模糊时间对比表.png</p>    <p>如上图:以1080 x 1349 的图片为例(每一个半径取5次的均值),使用原尺寸用两种方法进行高斯模糊,RenderScript的效率比fastBlur高,大约快10倍,但是都超过了16ms,而使用优化方法后,使其先缩小8倍,再模糊,2种方法效率都有质的提高,RenderScript模糊时间不足5ms,fastBlur 也接近16ms,半径为15以下小与16ms。</p>    <p>因此不管使用哪种方法模糊图片,都应该先优化,再模糊。</p>    <h3>4,优缺点比较及图片高斯模糊方案</h3>    <p>RenderScript 优点:</p>    <ul>     <li>使用简单,原生的API,十行左右的代码就能完成高斯模糊</li>     <li>效率较高,是在c/c++层做处理</li>    </ul>    <p>RenderScript 缺点:</p>    <ul>     <li>API 17以上才能使用</li>     <li>用兼容包的话,会导致APK 体积增大,support包约160k</li>    </ul>    <p>fastBlur的优点:</p>    <ul>     <li>没有兼容版本问题</li>     <li>不用引入三方包,不会增加APK大小</li>    </ul>    <p>fastBlur的缺点:</p>    <ul>     <li>效率很低,在Java层做处理</li>     <li>将Bitmap全部加载到内存,较大图片容易OOM</li>    </ul>    <p>以上对比了2种方法的优缺点,各有优劣,那么我们到底选择哪一种呢?这个需要看情况而定,给出下面2种方案:</p>    <p>高斯模糊方案一:如果APK本身较小,可以接受增大的160k体积,那么直接使用兼容包的RenderScript (注意需要先优化,用上面的先缩小再模糊)。</p>    <p>高斯模糊方案二:如果不想APK体积增大,那么 在模糊的时候做判断, API版本大于17 ,直接使用原生的RenderScript模糊,API版本小于17,则用fastBlur方法。(同样需要先优化,后模糊) 。</p>    <h3>6,轮子</h3>    <p>由于高斯模糊在项目中用得比较多,而每一个项目都去拷贝代码,这样很麻烦,并且不优雅,因此,对这两种方法优化后,封装成了一个Lib,要使用时直接添加依赖就行。</p>    <p>添加依赖:</p>    <p>1, 最外层build.gradle 添加一下代码:</p>    <pre>  <code class="language-java">allprojects {      repositories {          jcenter()            maven {url "https://jitpack.io"}      }  }</code></pre>    <p>2,app 的build.gradle添加:</p>    <pre>  <code class="language-java">dependencies {    compile 'com.github.pinguo-zhouwei:EasyBlur:v1.0.0'  }</code></pre>    <p>3,app 的build.gradle添加:</p>    <pre>  <code class="language-java">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      }</code></pre>    <p>使用方法:</p>    <p>1,简单使用,指定Bitmap和半径</p>    <pre>  <code class="language-java">Bitmap finalBitmap = EasyBlur.with(MainActivity.this)                          .bitmap(overlay) //要模糊的图片                          .radius(10)//模糊半径                          .blur();</code></pre>    <p>2,可以指定缩小的倍数,默认缩小倍数为8</p>    <pre>  <code class="language-java">Bitmap finalBitmap = EasyBlur.with(MainActivity.this)                          .bitmap(overlay) //要模糊的图片                          .radius(10)//模糊半径                          .scale(4)//指定模糊前缩小的倍数                          .blur();</code></pre>    <p>3, 指定使用哪一种方法,默认是使用兼容的RenderScript 高斯模糊</p>    <pre>  <code class="language-java">Bitmap finalBitmap = EasyBlur.with(MainActivity.this)                          .bitmap(overlay) //要模糊的图片                          .radius(10)//模糊半径                          .scale(4)//指定模糊前缩小的倍数                          .policy(EasyBlur.BlurPolicy.FAST_BLUR)//使用fastBlur                          .blur();</code></pre>    <p> </p>    <h3>参考资料</h3>    <p><a href="/misc/goto?guid=4959740946256122861" rel="nofollow,noindex">RenderScript API 指南</a></p>    <p><a href="/misc/goto?guid=4959740946350161947" rel="nofollow,noindex">android图片处理之图像模糊</a></p>    <p><a href="/misc/goto?guid=4959740946438600101" rel="nofollow,noindex">高斯模糊实现方案探究</a></p>    <p><a href="/misc/goto?guid=4959740946511957274" rel="nofollow,noindex">Fast Bitmap Blur For Android SDK</a></p>    <p> </p>    <p>来自:http://www.jianshu.com/p/02da487a2f43</p>    <p> </p>