高级UI<第三十五篇>:图像处理之高斯模糊
有时候为了美观,背景图片会加上模糊处理。一张图片其实是由若干个像素组成,每个像素都可以用一个矩阵表示,然而,所谓的模糊,就是将矩阵的值缩小一定的倍数,让像素值变小。
百度一下,发现有很多介绍图像高斯模糊的,我想说的是,这些资料都很棒,所以本人就随便提一下两种高斯模糊写法: Java版本的高斯模糊实现
和RenderScript的高斯模糊实现
。
效果如下:
图片.png(1)Java版本的高斯模糊的简单实现
这种写法的时间复杂度为O(m2 * n2)。
代码实现如下:
BlurTransform.java
public class BlurTransform extends BitmapTransformation {
//模糊程度
private int radius;
public BlurTransform(){
}
public BlurTransform(int radius){
this.radius = radius;
}
@Override
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap source, int outWidth, int outHeight) {
return doBlur(source, radius, true);
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
}
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) {
radius = 1;
}
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);
}
}
Glide.with(MainActivity.this)
.load(R.mipmap.pic4)
.transform(new BlurTransform(100))
.into(iv_image);
构造方法BlurTransform的参数radius表示模糊程度,数值越大越模糊,它的区间是[1,正无穷)。
(2)RenderScript的高斯模糊实现(比第一种快)
这种方法实现只支持API17以上的版本,API17以下的版本不支持,当然,自从Android9.0问世之后,也就移位了API17之前的手机已经完全淘汰了,所以这种方案可以放心使用。
代码实现如下:
BlurTransform.java
public class BlurTransform extends BitmapTransformation {
//模糊程度
private int radius;
private Context mContext;
public BlurTransform(Context mContext){
this.mContext = mContext;
}
public BlurTransform(Context mContext, int radius){
this.mContext = mContext;
if(radius < 1){
this.radius = 1;
}else if(radius >25){
this.radius = 25;
}else{
this.radius = radius;
}
}
@Override
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap source, int outWidth, int outHeight) {
return getBlurBitmap(mContext, pool, source, radius);
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
}
/**
* 得到模糊后的bitmap
*
* @param context
* @param inputBitmap
* @param radius
* @return
*/
private Bitmap getBlurBitmap(Context context, BitmapPool pool, Bitmap inputBitmap, int radius) {
if(inputBitmap == null){
return null;
}
// 创建一张渲染后的输出图片。
Bitmap outputBitmap = pool.get(inputBitmap.getWidth(), inputBitmap.getHeight(), inputBitmap.getConfig());
// 创建RenderScript内核对象
RenderScript rs = RenderScript.create(context);
// 创建一个模糊效果的RenderScript的工具对象
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
// 由于RenderScript并没有使用VM来分配内存,所以需要使用Allocation类来创建和分配内存空间。
// 创建Allocation对象的时候其实内存是空的,需要使用copyTo()将数据填充进去。
Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
// 设置渲染的模糊程度
blurScript.setRadius(radius);
// 设置blurScript对象的输入内存
blurScript.setInput(tmpIn);
// 将输出数据保存到输出内存中
blurScript.forEach(tmpOut);
// 将数据填充到Allocation中
tmpOut.copyTo(outputBitmap);
return outputBitmap;
}
}
Glide.with(MainActivity.this)
.load(R.mipmap.pic4)
.transform(new BlurTransform(MainActivity.this, 10))
.into(iv_image);
但是呢?这种方法有个很大的缺点,它的模糊程度区间是[1,25],25就是最大值了,即使设置了最大值25,那么图片的效果看起来就像毛玻璃
效果,但是如果需要更加模糊呢?我想可以从透明度着手。
现在,我们加上修改透明度的代码,新增一个number参数,这个number就是透明度程度,取值范围值[0,100],具体代码如下:
public class BlurTransform extends BitmapTransformation {
private Context mContext;
//模糊程度,区间是[1,25]
private int blur_radius;
//number表示透明度程度,区间是[0, 100],0表示完全透明,1表示不透明
private int number;
public BlurTransform(Context mContext, int blur_radius, int number){
this.mContext = mContext;
if(blur_radius < 1){
this.blur_radius = 1;
}else if(blur_radius >25){
this.blur_radius = 25;
}else{
this.blur_radius = blur_radius;
}
if(number < 0){
this.number = 0;
}else if(number >100){
this.number = 100;
}else{
this.number = number;
}
}
@Override
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap source, int outWidth, int outHeight) {
return getBlurBitmap(mContext, pool, source);
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
}
/**
* 得到模糊后的bitmap
*
* @param context
* @param bitmap
* @return
*/
private Bitmap getBlurBitmap(Context context, BitmapPool pool, Bitmap bitmap) {
if(bitmap == null){
return null;
}
int width = bitmap.getWidth();
int height = bitmap.getHeight();
//像素数组
int[] pixels = new int[width * height];
//int[] pixels, int offset, int stride, int x, int y, int width, int height
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
number = number * 255 / 100;
for(int i=0;i<pixels.length;i++){
pixels[i] = (number << 24) | (pixels[i] & 0x00FFFFFF);
}
//
Bitmap inputBitmap = Bitmap.createBitmap(pixels, width, height, bitmap.getConfig());
// 创建一张渲染后的输出图片。
Bitmap outputBitmap = pool.get(inputBitmap.getWidth(), inputBitmap.getHeight(), inputBitmap.getConfig());
RenderScript rs = RenderScript.create(context);
//Create an Intrinsic Blur Script using the Renderscript
ScriptIntrinsicBlur theIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
//native层分配内存空间
Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
//设置blur的半径然后进行blur
theIntrinsic.setRadius(blur_radius);
theIntrinsic.setInput(tmpIn);
theIntrinsic.forEach(tmpOut);
//拷贝blur后的数据到java缓冲区中
tmpOut.copyTo(outputBitmap);
//销毁Renderscript
rs.destroy();
return outputBitmap;
}
}
Glide.with(MainActivity.this)
.load(R.mipmap.pic4)
.transform(new BlurTransform(MainActivity.this, 25, 80))
.into(iv_image);
[从性能上分析]
ScriptIntrinsicBlur
的执行效率比java实现快很多,ScriptIntrinsicBlur
属于Android实现,和C++实现相比差不多,总之,如果从执行效率(时间复杂度)选择的话,必然选择ScriptIntrinsicBlur
实现高斯模糊。
但是使用ScriptIntrinsicBlur
实现高斯模糊有个很明显的弊端,就是它的模糊程度最大值为25,最多可实现毛玻璃效果,但是更模糊的却实现不了,以上给出的解决方案是修改bitmap透明度使bitmap更加模糊,但是如果是大图的情况呢?大图生成的bitmap的大小是非常恐怖的,很有可能会产生OOM问题,所以,这里要需要将bitmap的分辨率缩小。
在很多时候,很多APP将一张模糊图片作为全屏背景,那么如果我们将每英寸的像素个数减少是不是也可以使图片模糊的效果呢?这里使用的方法是按比例缩小bitmap的分辨率。
所以,我们最终代码是:
public class BlurTransform extends BitmapTransformation {
private Context mContext;
//模糊程度,区间是[1,25]
private int blur_radius;
//number表示透明度程度,区间是[0, 100],0表示完全透明,1表示不透明
private int number;
//blur_scale表示缩放比例,区间是(0,1]的浮点数
private float blur_scale;
public BlurTransform(Context mContext, int blur_radius, float blur_scale, int number){
this.mContext = mContext;
if(blur_radius < 1){
this.blur_radius = 1;
}else if(blur_radius >25){
this.blur_radius = 25;
}else{
this.blur_radius = blur_radius;
}
if(blur_scale < 0){
this.blur_scale = 0;
}else if(blur_scale >1){
this.blur_scale = 1;
}else{
this.blur_scale = blur_scale;
}
if(number < 0){
this.number = 0;
}else if(number >100){
this.number = 100;
}else{
this.number = number;
}
}
@Override
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap source, int outWidth, int outHeight) {
return getBlurBitmap(mContext, pool, source);
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
}
/**
* 得到模糊后的bitmap
*
* @param context
* @param bitmap
* @return
*/
private Bitmap getBlurBitmap(Context context, BitmapPool pool, Bitmap bitmap) {
if(bitmap == null){
return null;
}
Bitmap inputBitmap = null;
if(blur_scale != 1){
int width = Math.round(bitmap.getWidth() * blur_scale);
int height = Math.round(bitmap.getHeight() * blur_scale);
inputBitmap = Bitmap.createScaledBitmap(bitmap, width, height, false);
}else{
inputBitmap = bitmap;
}
if(number != 100){
//像素数组
int[] pixels = new int[inputBitmap.getWidth() * inputBitmap.getHeight()];
//int[] pixels, int offset, int stride, int x, int y, int width, int height
inputBitmap.getPixels(pixels, 0, inputBitmap.getWidth(), 0, 0, inputBitmap.getWidth(), inputBitmap.getHeight());
number = number * 255 / 100;
for(int i=0;i<pixels.length;i++){
pixels[i] = (number << 24) | (pixels[i] & 0x00FFFFFF);
}
inputBitmap = Bitmap.createBitmap(pixels, inputBitmap.getWidth(), inputBitmap.getHeight(), inputBitmap.getConfig());
}
// 创建一张渲染后的输出图片。
Bitmap outputBitmap = pool.get(inputBitmap.getWidth(), inputBitmap.getHeight(), inputBitmap.getConfig());
RenderScript rs = RenderScript.create(context);
//Create an Intrinsic Blur Script using the Renderscript
ScriptIntrinsicBlur theIntrinsic = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
//native层分配内存空间
Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
//设置blur的半径然后进行blur
theIntrinsic.setRadius(blur_radius);
theIntrinsic.setInput(tmpIn);
theIntrinsic.forEach(tmpOut);
//拷贝blur后的数据到java缓冲区中
tmpOut.copyTo(outputBitmap);
//销毁Renderscript
rs.destroy();
inputBitmap.recycle();
return outputBitmap;
}
}
Glide.with(MainActivity.this)
.load(R.mipmap.pic4)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transform(new BlurTransform(MainActivity.this, 25, 80f, 50))
.into(iv_image);
参考:
本篇文字描述比较少,因为网上好博客很多很多,所以感觉没必要做重复的描述了。
Android图像处理 - 高斯模糊的原理及实现
Android背景模糊话模糊、高斯模糊(FastBlur)
Android下实现高效的模糊效果
教你一分钟实现动态模糊效果
还有一种通过JNI实现高斯模糊的资料:
https://github.com/kikoso/android-stackblur
[本章完...]