其实有关 android 下实现图片模糊的文章有很多,大多都是使用 renderscript 内置的 ScriptIntrinsicBlur 来实现的,这篇文章中的例子也不例外,但如果仅仅是调用一下 api 的话就没必要去写了。所以接下来会介绍均值模糊以及高斯模糊的原理、什么是 renderscript 以及如何编写 renderscript。最终的例子是将图片高斯模糊处理后再调用自己编写的 rs 对其增加一层蒙版效果(这里会提到计算机是如何处理透明度以及颜色叠加的)。
系好安全带,开车了!
上面这张图片是用于图像算法测试的国际标准图像,使用这张图片主要有两个原因:
图像包含了各种细节、平滑区域、阴影和纹理,这些对测试各种图像处理算法很有用。
图像里是一个很迷人的女子。而图像处理领域里的人大多为男性,可以吸引更多的人。
然而这张图片其实出自 1972 年的 《花花公子》,所以上面给出的图片并不完整,下面我们写一个小 demo 来展示一下完整的图片。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = (ImageView) findViewById(R.id.image);
drag = (ImageView) findViewById(R.id.drag);
SeekBar progressBar = (SeekBar) findViewById(R.id.seek);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.rina);
imageView.setImageBitmap(bitmap);
imageView.post(new Runnable() {
@Override
public void run() {
float scale = bitmap.getWidth() * 1f / imageView.getWidth();
mosaic = Bitmap.createBitmap(bitmap, (int) (drag.getX() * scale), (int) (drag.getY() * scale),
(int) (drag.getWidth() * scale), (int) (drag.getHeight() * scale));
drag.setImageBitmap(BlurHelper.mosaic(mosaic, currentRadius));
}
});
progressBar.setProgress(currentRadius * 5);
progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (progress % 5 == 0) {
currentRadius = progress / 5;
drag.setImageBitmap(BlurHelper.mosaic(mosaic, currentRadius));
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
drag.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xDown = event.getX();
yDown = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float targetX = event.getX() - xDown + v.getTranslationX();
float targetY = event.getY() - yDown + v.getTranslationY();
targetX = Math.min(Math.max(targetX, 0), imageView.getWidth() - drag.getWidth());
targetY = Math.min(Math.max(targetY, 0), imageView.getHeight() - drag.getHeight());
v.setTranslationX(targetX);
v.setTranslationY(targetY);
float scale = bitmap.getWidth() * 1f / imageView.getWidth();
b = Bitmap.createBitmap(bitmap, (int) (drag.getX() * scale), (int) (drag.getY() * scale),
(int) (drag.getWidth() * scale), (int) (drag.getHeight() * scale));
drag.setImageBitmap(BlurHelper.mosaic(mosaic, currentRadius));
break;
}
return true;
}
});
}
public static Bitmap mosaic(Bitmap bitmap, int radius) {
if (radius == 0) return bitmap;
final int width = bitmap.getWidth();
final int height = bitmap.getHeight();
final Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
final int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
int x = j % radius;
int y = i % radius;
pixels[i * width + j] = pixels[(i - y) * width + j - x];
}
}
outBitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return outBitmap;
}
因为原图有点少儿不宜,所以我这边手动给打了个码,在原图上盖了一层马赛克后的图片,每次拖动后都会重新计算。其实马赛克算法也是一种模糊算法,首先图片其实是由很多像素点组成的一个二维数组(或者矩阵)。上面的马赛克算法只是遍历了图片的每一个像素,然后在这个过程中对于给定的半径将所有的像素都设置成第一个像素的值。
我们由此抛砖引玉引出均值模糊(box blur),和马赛克算法差不多,他是每一个像素都取周围像素的平均值。算法也比较简单,如下:
public static Bitmap boxBlur(Bitmap bitmap, int radius) {
final int width = bitmap.getWidth();
final int height = bitmap.getHeight();
final int[] pixels = new int[width * height];
final int[] outPixels = new int[width * height];
final Bitmap outBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
//遍历bitmap每一个像素
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
//取半径为radius的矩形区域,并处理边界情况
final int left = i - radius < 0 ? 0 : i - radius;
final int top = j - radius < 0 ? 0 : j - radius;
final int right = i + radius > width ? width : i + radius;
final int bottom = j + radius > height ? height : j + radius;
//矩形区域总像素
final int count = (right - left) * (bottom - top);
//分别求出矩形区域内rgb的总值
int r = 0, g = 0, b = 0;
for (int m = left; m < right; m++) {
for (int n = top; n < bottom; n++) {
final int pixel = pixels[n * width + m];
r += Color.red(pixel);
g += Color.green(pixel);
b += Color.blue(pixel);
}
}
//设置新的像素为矩形区域内像素的均值
outPixels[j * width + i] = Color.rgb(r / count, g / count, b / count);
}
}
outBitmap.setPixels(outPixels, 0, width, 0, 0, width, height);
bitmap.recycle();
return outBitmap;
}
上面这么写是为了看起来更清楚,他的时间复杂度为 O(n^2 * m^2)效率是极低的。anyway 进行均值模糊之后的效果如下图(图像大小 300 * 260,模糊半径 5 )