前言 最近给自己挖了几个坑,准备填一下。现在来填一下第一个坑:图片模糊。关于图片模糊的方法有很多,比如:Open CV 的各种图片处理、Android 支持的高性能密集型任务执行框架 RenderScript、Java 或者 C/C++ 的算法实现图片模糊处理。本篇文章将包含以下内容:
RenderScript 简介与图片模糊的实现
Java / C++ 算法实现图片模糊处理
一个简单的动态模糊实现
总结
至于 Open CV 我以前的一些文有些简单的介绍,如果只是想模糊图片就引入整个的 Open CV 个人感觉还是有点“杀鸡用牛刀”的感觉。对了,关于算法实现什么的……我只是个代码收集者,并非我自己实现的。
开始之前先放个图,做成什么样心里有点x数
如果你看过我同学的那篇Android:简单靠谱的动态高斯模糊效果
你一定会发现我这个布局跟他的有那么一些相似,哇哈哈哈哈哈,你猜对了,我去他项目里复制的。当然了,实现方式不一样,他是用的 RecyclerView 实现的,我这里就自己复写了 Activity 的 onTouch 实现的动态模糊。
RenderScript 首先简单的介绍一下 RenderScript 这里是我读文档的翻译……又到了展现真正的辣鸡英语水平的时候了……
RenderScript 是 Android 上的高性能计算密集型任务的框架。虽然串行工作也能受益,但是RenderScript 主要面向并行数据计算。RenderScript 运行时可以跨越设备上可用的处理器如多核CPU和GPU进行并行工作。这让你可以专注于算法,而不是调度工作。RenderScript 对于应用进行图像处理,计算摄影或者计算机视觉等方面特别有用。 要开始使用RenderScript,有两个主要概念应该要理解:
恩,BB这么多,我们只需要有个大致概念就行了,因为也不是专门去学习这套框架,我们只是需要使用这套框架的一丁点图片处理相关的东西而已。千言万语,最后就一句话:
1 RenderScript 是 Android 上的高性能计算密集型任务的框架
行,对 RenderScript 有了大致的了解后,可以开始了,官方文档里其实有比较详细的流程,先创建什么 context 啦,然后分配内存巴拉巴拉的拉,不过我这又不是在学RenderScript,而是想实现一个功能,用完就可以把这框架扔一边了,如果你也是这样,不妨直接 copy 下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private static final float BITMAP_SCALE = 0.4f ;private static final float BLUR_RADIUS = 25f ;public static Bitmap blur (Context context, Bitmap image) { int width = Math.round(image.getWidth() * BITMAP_SCALE); int height = Math.round(image.getHeight() * BITMAP_SCALE); Bitmap inputBitmap = Bitmap.createScaledBitmap(image, width, height, false ); Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap); RenderScript rs = RenderScript.create(context); ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap); Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap); blur.setRadius(BLUR_RADIUS); blur.setInput(tmpIn); blur.forEach(tmpOut); tmpOut.copyTo(outputBitmap); return outputBitmap; }
运行效果:
Java & C++ 这两种都是采用同一种算法实现的,本质上都是对像素数组进行处理运算。本来我以为C++的方式会快一些,没想到在我的mix2上运行反而是Java实现的算法会快一些。唉,真是辣鸡C++还不如Java。 当然了……讲道理,代码我看了,就是一样的,可能是jni的开销吧。这里为什么要介绍Javah和C++的算法实现呢,因为RenderScript虽然文档上说可以运行在2.3及以上的平台,但是这个图片处理的api最低版本是17。所以说,如果你有需要兼容低版本,还是得采用下别的实现。
这里只放一下Java的实现代码,因为反而比较快的关系……至于JNI,我这里偷了个懒,因为以前用CMake项目编译生成了so,所以这里直接引用了so。当然了cpp源码也放在了项目里,感兴趣的可以自己去编译一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 public static Bitmap blurInJava (Bitmap bmp, int radius) { if (radius < 1 ) { return (null ); } Bitmap bitmap = ratio(bmp, bmp.getWidth() * BITMAP_SCALE, bmp.getHeight() * BITMAP_SCALE); if (radius == 1 ) { return bitmap; } bmp.recycle(); 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 ; short r[] = new short [wh]; short g[] = new short [wh]; short b[] = new short [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; short dv[] = new short [256 * divSum]; for (i = 0 ; i < 256 * divSum; i++) { dv[i] = (short ) (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++) { 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); }
动态模糊 这里实现的效果图就是开头的那张gif了,首先要明白一点,动态模糊,不可能每一帧都去调用方法生成一张模糊图,那样效率太低了。这里看了别人的思路,先生成一张模糊图片,之后在原来的布局上放上两个ImageView,一张原图,上面的一张是模糊图,动态改变上面模糊图的透明值就能实现动态透明效果。这想法阔以,只生成了一次模糊图片。
这里底部布局我本来是想放一个布局在屏幕外,后来发现这样无论怎么滑动都不能把布局滑入。可能是代码有问题,也可能是父容器的问题。于是之后就写了个全屏的布局,但是在界面启动后将之移动到屏幕外。设置了上下两块可点击将布局滑出的区域,在滑动的时候动态设置模糊图片控件的alpha值,这样就实现了动态模糊,话不多说,上关键代码: 布局代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:layout_width ="match_parent" android:layout_height ="match_parent" > <ImageView android:id ="@+id/iv_img" android:src ="@drawable/test" android:scaleType ="fitXY" android:layout_width ="match_parent" android:layout_height ="match_parent" /> <ImageView android:id ="@+id/iv_blur_img" android:scaleType ="fitXY" android:alpha ="0" android:layout_width ="match_parent" android:layout_height ="match_parent" /> <LinearLayout android:id ="@+id/rl_container" android:layout_width ="match_parent" android:layout_height ="match_parent" android:orientation ="vertical" > <TextView android:id ="@+id/tv_tem" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_margin ="10dp" android:fontFamily ="sans-serif-thin" android:gravity ="bottom" android:text ="37°" android:textColor ="@android:color/white" android:textSize ="90sp" /> <LinearLayout android:layout_width ="match_parent" android:layout_height ="120dp" android:layout_marginTop ="36dp" android:background ="#44000000" android:orientation ="vertical" android:paddingLeft ="20dp" > <TextView android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_marginTop ="10dp" android:text ="WeatherInfo" android:textColor ="@android:color/white" android:textSize ="24sp" /> <TextView android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_marginBottom ="10dp" android:layout_marginTop ="10dp" android:text ="Show more info" android:textColor ="@android:color/white" /> </LinearLayout > <LinearLayout android:layout_width ="match_parent" android:layout_height ="120dp" android:layout_marginTop ="36dp" android:background ="#44000000" android:orientation ="vertical" android:paddingLeft ="20dp" > <TextView android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_marginTop ="10dp" android:text ="WeatherInfo" android:textColor ="@android:color/white" android:textSize ="24sp" /> <TextView android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_marginBottom ="10dp" android:layout_marginTop ="10dp" android:text ="Show more info" android:textColor ="@android:color/white" /> </LinearLayout > <LinearLayout android:layout_width ="match_parent" android:layout_height ="120dp" android:layout_marginTop ="36dp" android:background ="#44000000" android:orientation ="vertical" android:paddingLeft ="20dp" > <TextView android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_marginTop ="10dp" android:text ="WeatherInfo" android:textColor ="@android:color/white" android:textSize ="24sp" /> <TextView android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_marginBottom ="10dp" android:layout_marginTop ="10dp" android:text ="Show more info" android:textColor ="@android:color/white" /> </LinearLayout > </LinearLayout > </RelativeLayout >
Activity代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 package com.xiasuhuei321.blur;import android.content.Context;import android.graphics.BitmapFactory;import android.os.Bundle;import android.os.Handler;import android.support.annotation.Nullable;import android.support.v7.app.AppCompatActivity;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.Window;import android.view.WindowManager;import android.widget.ImageView;import com.xiasuhuei321.gank_kotlin.ImageProcess;public class DynamicBlurActivity extends AppCompatActivity { private ImageView blurImg; private int height; private View container; @Override protected void onCreate (@Nullable Bundle savedInstanceState) { super .onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_NO_TITLE); int flag = WindowManager.LayoutParams.FLAG_FULLSCREEN; Window window = this .getWindow(); window.setFlags(flag, flag); setContentView(R.layout.activity_dynamic_blur); initView(); WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); height = wm.getDefaultDisplay().getHeight(); } private void initView () { blurImg = (ImageView) findViewById(R.id.iv_blur_img); blurImg.setImageBitmap(ImageProcess.blur(this , BitmapFactory.decodeResource(getResources(), R.drawable.test))); container = findViewById(R.id.rl_container); new Handler().postDelayed(new Runnable() { @Override public void run () { container.setTranslationY(height + 100 ); } }, 100 ); } float y; boolean scrollFlag = false ; float sumY = 0 ; boolean isShow = false ; @Override public boolean onTouchEvent (MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: y = event.getY(); if (y > height * 0.9 ) { scrollFlag = true ; } else if (y < height * 0.1 ) { if (!isShow) return false ; scrollFlag = true ; } break ; case MotionEvent.ACTION_MOVE: sumY = event.getY() - y; if (scrollFlag) { container.setTranslationY(event.getY()); if (!isShow) blur(sumY); else reverseBlur(sumY); } Log.e("DynamicBlurActivity" , "滚动sumY值:" + sumY + " scrollFlag:" + scrollFlag); break ; case MotionEvent.ACTION_UP: if (scrollFlag) { if (Math.abs(sumY) > height * 0.5 ) { if (isShow) hide(); else show(); } else { if (isShow) show(); else hide(); } sumY = 0 ; } scrollFlag = false ; break ; } return true ; } private void hide () { container.setTranslationY(height + 100 ); blur(0 ); isShow = false ; } private void show () { container.setTranslationY(0 ); blur(1000 ); isShow = true ; } private void blur (float sumY) { float absSum = Math.abs(sumY); float alpha = absSum / 1000 ; if (alpha > 1 ) alpha = 1 ; blurImg.setAlpha(alpha); } private void reverseBlur (float sumY) { float absSum = Math.abs(sumY); float alpha = absSum / 1000 ; if (alpha > 1 ) alpha = 1 ; blurImg.setAlpha(1 - alpha); } }
总结 在刚开始的时候我非常的作死,选用的图片是1080 * 1920 的,在处理的时候一看内存,我 * 飙到了200多M,而且处理这张图片花费了6.6秒左右的时间。我打印了一下这图片转化成Bitmap的width和height 分别是
这如果是 ARGB_8888 那么一张图就是61M,加上处理需要一个像素数组,得,另一个61M。剩下在处理像素的时候各种申请的内存飙到200M也不是不可以理解。本来我想是不是可以使用同一张 Bitmap,在最后setPixels的时候就不用再申请一次内存了,但是发现这样不行,直接报错了。因为从资源文件拿到的 Bitmap 的 isMutable属性是false,不可以直接在原来的 Bitmap 上 setPixels 。所以原来还需要拷贝一份Bitmap,不过拷贝之后可以将调用 bitmap.recycle() 方法,将之赶紧回收了。
当然,像我这样头铁硬怼并不好,飙到200M市面上很多手机都会OOM……更好的方式是对缩略图进行模糊处理。Android里有个缩略图工具,就几个方法,还挺好用的,我这里就用的缩略图工具获取缩略图:
1 2 3 public static Bitmap ratio (Bitmap bmp, float pixelW, float pixelH) { return ThumbnailUtils.extractThumbnail(bmp, (int ) pixelW, (int ) pixelH); }
在进行处理前先获取缩略图,然后再去处理效率无疑会高非常多。
最后,放上项目地址:https://github.com/ForgetAll/Blur