分享一种图片动态模糊思路

原创作者:AchillesL
若转载文章,请在明显的位置标明文章出处

最近在做仿写网易云音乐播放界面时,学习过图片的毛玻璃效果处理,毛玻璃效果其实就是图片模糊。

简单的模糊

对于模糊效果,我们一般使用StackBlur模糊算法以及它衍生的一些优化方案。在这里,我们会使用到StackBlur模糊算法的java实现类。

public class FastBlurUtil {

    public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) {

        Bitmap bitmap;
        if (canReuseInBitmap) {
            bitmap = sentBitmap;
        } else {
            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];
        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;
            }
        }
        bitmap.setPixels(pix, 0, w, 0, 0, w, h);
        return (bitmap);
    }
}

代码看不懂没关系,这是个工具类,我们先学会怎么去用也是可以的。先看下doBlur方法:

public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap)

第一个参数是需要模糊化的Bitmap对象,第二个参数是模糊半径,第三个参数表示是否复用。

这里需要注意一下

一般来讲,使用这个工具类时,传入的图片对象不能太大,否则容易OOM,同时性能上也会有问题。因此,我们一般都会先将图片缩小处理。

使用起来非常简单,传入图片,设置模糊半径就好了:

private Bitmap getBlurBitmap() {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;

    Bitmap originBitmap = null;
    try {
        originBitmap = BitmapFactory.decodeStream(getAssets().open(PIC_NAME), null,
                options);
    } catch (IOException e) {
        e.printStackTrace();
    }

    Bitmap thumbnailBitmap = Bitmap.createScaledBitmap(originBitmap, originBitmap.getWidth() / 10,
            originBitmap.getHeight() / 10, false);
    Bitmap blurBitmap = FastBlurUtil.doBlur(thumbnailBitmap, 8, false);

    return blurBitmap;
}

看一下效果:

分享一种图片动态模糊思路_第1张图片
图片模糊前后对比

可以看到,图片已经实现了模糊效果,质量还是挺不错的。

动态模糊

我们进一步探索,思考怎么实现动态模糊。可以直接动态修改上面的模糊半径吗?不行,解释如下:

如果使用上述代码进行实时渲染,会造成严重的掉帧

一般来讲,我们实现动态模糊的方法如下:

先得到模糊后的图片,然后把原图叠加在上面,通过不断改变原图的透明度(Alpha),实现动态模糊

这个方法很巧妙,但是需要注意这种实现方式,必须用到两张图片,有读者可能想到使用两个ImageView来加载两张图。
  其实,有更好的方法,那就是使用LayerDrawable。

LayerDrawable类似photoShop图层的概念。

它包含一个Drawable数组,并将这些Drawable对象的数组顺序来绘制它们,索引最大的Drawable对象将会被绘制在最上面。

我们先在布局中添加一个SeekBar控件,用于动态调节模糊程度。


先创建一个两个元素的Drawable数组,分别存放模糊后的图片以及原图。原图的索引为1,因此会显示在最上层。最后用Drawable数组来初始化LayerDrawable对象。

Drawable[] drawables = new Drawable[2];
    drawables[0] = new BitmapDrawable(blurBitmap);
    drawables[1] = new BitmapDrawable(originBitmap);

    LayerDrawable layerDrawable = new LayerDrawable(drawables);

当SeekBar滑动时,我们通过LayerDrawable的getDrawable方法取出原图,用SeekBar的值算出并改变原图的透明度,然后动态设置背景。

mSeekBar.setMax(1000);
    mSeekBar.setProgress(0);
    mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
            int alpha = (int) (255 - (i * 1.0 / 1000) * 255);
            mLayerDrawable.getDrawable(1).setAlpha(alpha);
            mRootView.setBackground(mLayerDrawable);
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {

        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {

        }
    });

我们可以看到效果,还是挺不错的:

动态模糊 1

但是我们发现一个细节,SeekBar从0滑动到一半这个阶段,模糊的效果并不显著。如果需要前半段范围的效果更明显一点,该怎么办呢?

这种方法的模糊效果,和我们计算透明度的算法有关。

明显的,我们计算透明度算法,是个斜率k= 1的一次函数,如果改成平方根函数呢?如下图所示,可以看到平方根函数比斜率k为1 的一次函数向Y轴“倾向的时间更早”。

分享一种图片动态模糊思路_第2张图片
平方根函数与斜率为1的一次函数对比

于是我们改一下计算透明度的算法:

int alpha = (int) (255 - Math.sqrt((i * 1.0 / 1000)) * 255);

运行程序:

动态模糊 2

看到效果,明显看到比第一种的算法要好一些。

你可能感兴趣的:(分享一种图片动态模糊思路)