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

原创作者: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;
}

看一下效果:

图片模糊前后对比

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

动态模糊

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

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

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

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

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

LayerDrawable类似photoShop图层的概念。

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

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

<SeekBar
    android:id="@+id/seekbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_marginBottom="20dp"
    android:layout_marginLeft="10dp"
    android:layout_marginRight="10dp"/>

先创建一个两个元素的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轴“倾向的时间更早”。

平方根函数与斜率为1的一次函数对比

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

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

运行程序:

动态模糊 2

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • 原创作者:AchillesL若转载文章,请在明显的位置标明文章出处 0 前言   网易云音乐是一款非常优秀的音乐播...
    AchillesL阅读 42,963评论 44 282
  • 人与人能够遇见都是一场缘分, 没有好坏,没有对错, 就像每个女孩都是最美的花儿, 就像每个少年在遇见自己心爱的女孩...
    椿零阅读 265评论 0 2
  • 莎姑娘是我从小玩到大的玩伴,我们俩从咿呀学语起便相识,直到20岁。20岁后,我便再也没有见过莎姑娘。 莎姑娘留给我...
    李素问阅读 568评论 4 8
  • 星期六,妈妈带我去公园玩。好久都没有去公园玩了,我特别高兴!我们等了好长时间终于来了一辆5路公交车,好不容易挤上车...
    小肥皂sy阅读 177评论 0 1