LJ的Blog

学海无涯苦做舟

0%

记录三种实现图片模糊的方法

前言

最近给自己挖了几个坑,准备填一下。现在来填一下第一个坑:图片模糊。关于图片模糊的方法有很多,比如:Open CV 的各种图片处理、Android 支持的高性能密集型任务执行框架 RenderScript、Java 或者 C/C++ 的算法实现图片模糊处理。本篇文章将包含以下内容:

  • RenderScript 简介与图片模糊的实现
  • Java / C++ 算法实现图片模糊处理
  • 一个简单的动态模糊实现
  • 总结

至于 Open CV 我以前的一些文有些简单的介绍,如果只是想模糊图片就引入整个的 Open CV 个人感觉还是有点“杀鸡用牛刀”的感觉。对了,关于算法实现什么的……我只是个代码收集者,并非我自己实现的。

开始之前先放个图,做成什么样心里有点x数

动态模糊.gif

如果你看过我同学的那篇Android:简单靠谱的动态高斯模糊效果 你一定会发现我这个布局跟他的有那么一些相似,哇哈哈哈哈哈,你猜对了,我去他项目里复制的。当然了,实现方式不一样,他是用的 RecyclerView 实现的,我这里就自己复写了 Activity 的 onTouch 实现的动态模糊。

RenderScript

首先简单的介绍一下 RenderScript 这里是我读文档的翻译……又到了展现真正的辣鸡英语水平的时候了……

RenderScript 是 Android 上的高性能计算密集型任务的框架。虽然串行工作也能受益,但是RenderScript 主要面向并行数据计算。RenderScript 运行时可以跨越设备上可用的处理器如多核CPU和GPU进行并行工作。这让你可以专注于算法,而不是调度工作。RenderScript 对于应用进行图像处理,计算摄影或者计算机视觉等方面特别有用。
要开始使用RenderScript,有两个主要概念应该要理解:

  • 语言本身是为了编写高性能计算代码产生的 C99 衍生语言。这篇文章描述了如何使用它去编写一个计算内核。

  • 控制 API 是用来管理 RenderScript 资源的生命周期和控制内核运行的。这套API有三套语言实现:Java,Android NDK 的 C++ 和 C99 派生的内核语言本身。

恩,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;
/**
* 最大模糊度(在0.0到25.0之间)
*/
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 上下文
RenderScript rs = RenderScript.create(context);
// 创建一个模糊效果的 RenderScript 的工具对象
ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

// 由于RenderScript并没有使用VM来分配内存,所以需要使用Allocation类来创建和分配内存空间。
// 创建Allocation对象的时候其实内存是空的,需要使用copyTo()将数据填充进去。
Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
// 设置渲染的模糊程度, 25f是最大模糊度
blur.setRadius(BLUR_RADIUS);
// 设置blurScript对象的输入内存
blur.setInput(tmpIn);
// 将输出数据保存到输出内存中
blur.forEach(tmpOut);
// 将数据填充到Allocation中
tmpOut.copyTo(outputBitmap);
return outputBitmap;
}

运行效果:

renderscript.png

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
    /**
* StackBlur By Java Bitmap
*
* @param bmp bmp Image
* @param radius Blur radius
* @return Image Bitmap
*/
public static Bitmap blurInJava(Bitmap bmp, int radius) {
// 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>
if (radius < 1) {
return (null);
}

Bitmap bitmap = ratio(bmp, bmp.getWidth() * BITMAP_SCALE, bmp.getHeight() * BITMAP_SCALE);
// Bitmap bitmap = bmp.copy(Bitmap.Config.ARGB_8888, true);
// Return this none blur
if (radius == 1) {
return bitmap;
}
bmp.recycle();

int w = bitmap.getWidth();
int h = bitmap.getHeight();

int[] pix = new int[w * h];
// get array
bitmap.getPixels(pix, 0, w, 0, 0, w, h);

// run Blur
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++) {
// 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;
}
}

// set Bitmap
bitmap.setPixels(pix, 0, w, 0, 0, w, h);

return (bitmap);
}

image.png

动态模糊

这里实现的效果图就是开头的那张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;

/**
* Created by xiasuhuei321 on 2017/10/15.
* author:luo
* e-mail:xiasuhuei321@163.com
*/

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