其实有关 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 ):

原图

均值模糊后

可以看到模糊的效果并不是很平滑,仔细看可以看到一个个小格子。显然取平均的方式并不是特别好,对于图像而言我们可以认为越靠近中心点与其关系越密切,而离中心点越远的像素相关程度也就越低,采用加权平均的方式似乎更合理一些。如果你是理科生的话,不知道你是否还记得高中数学书上提到过的正态分布(高斯分布)。下图是正态分布的函数曲线:

正态分布曲线

距离中心点越近,值就越大,完全符合我们的需求,可以作为计算平均时的权值,使用正态分布曲线来进行模糊计算的方式就叫做高斯模糊(gaussian blur)。u = 0 时的二维高斯曲线的函数如下:

其中 sigma 决定了数据的离散程度,sigma 越大曲线越扁,反之依然。

下面我们用代码简单实现一下,首先生成一个高斯模糊的概率矩阵:

private static float[][] makeGaussianBlurKernel(int radius, float sigma) {

//根据公式先计算一下2 * sigma ^ 2 便于之后计算

final float sigmaSquare2 = sigma * sigma * 2;

//半径是指中心点距离边界的距离,所以如果半径为1,则需要一个3 * 3的矩阵

final int size = radius * 2 + 1;

final float[][] matrix = new float[size][size];

float sum = 0;

int row = 0;

for (int i = -radius; i <= radius; i++) {

int column = 0;

for (int j = -radius; j <= radius; j++) {

//根据公式计算出值

matrix[row][column] = (float) (1 / (Math.PI * sigmaSquare2)

* Math.exp(-(i * i + j * j) / sigmaSquare2));

sum += matrix[row][column];

column++;

}

row++;

//算出均值,使总概率为1

for (int i = 0; i < size; i++) {

for (int j = 0; j < size; j++) {

matrix[i][j] /= sum;

}

}

return matrix;

}```

接下来就是根据上面算出的矩阵对图像进行加权平均了,在这里处理边界情况的时候偷了个懒,假设越界后的像素对于边界是镜面的。

```java

public static Bitmap gaussianBlur(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());

final float[][] blurMatrix = makeGaussianBlurKernel(radius, radius / 2.5f);

bitmap.getPixels(pixels, 0, width, 0, 0, width, height);

for (int i = 0; i < width; i++) {

for (int j = 0; j < height; j++) {

final int left = i - radius;

final int top = j - radius;

final int right = i + radius;

final int bottom = j + radius;

int row = 0;

float r = 0, g = 0, b = 0;

for (int n = top; n <= bottom; n++) {

int column = 0;

int y = n;

if (y >= height) y = height - 1 - (y - height);

if (y < 0) y = -y;

for (int m = left; m <= right; m++) {

int x = m;

if (x >= width) x = width - 1 - (x - width);

if (x < 0) x = -x;

final int pixel = pixels[y * width + x];

r = r + blurMatrix[row][column] * Color.red(pixel);

g = g + blurMatrix[row][column] * Color.green(pixel);

b = b + blurMatrix[row][column] * Color.blue(pixel);

column++;

}

row++;

}

outPixels[j * width + i] = Color.rgb((int) r, (int) g, (int) b);

}

}

outBitmap.setPixels(outPixels, 0, width, 0, 0, width, height);

bitmap.recycle();

return outBitmap;

}

效果如图(图像大小300 * 260,模糊半径 5,sigma = 2.5 ):

高斯模糊

可以看到,模糊效果相较之前过度更加平滑,不过上面这四个 for 循环真的有点蛋疼,不算生成模糊矩阵的时间,处理这张 300 * 260 的图片总共花了 387ms,我们看看能不能进行一下优化。

首先我们可以把二维的高斯模糊拆成横向与纵向两个一维高斯模糊的组合,这样时间复杂度就下降到了 O(n ^ 2 * 2 * m)。u = 0 时,一维高斯曲线的函数如下:

根据公式生成一维矩阵:

private static float[] makeGaussianBlur1DKernel(int radius, float sigma) {

final float sigmaSquare2 = sigma * sigma * 2;

final int size = radius * 2 + 1;

final float[] matrix = new float[size];

float sum = 0;

int index = 0;

for (int i = -radius; i <= radius; i++) {

matrix[index] = (float) (1 / (Math.sqrt(2 * Math.PI) * sigma) * Math.exp(-(i * i) / sigmaSquare2));

sum += matrix[index];

index++;

for (int i = 0; i < size; i++) {

matrix[i] /= sum;

}

return matrix;

}

调整下之前的算法:

public static Bitmap gaussianBlur2(Bitmap bitmap, int radius) {

final int width = bitmap.getWidth();

final int height = bitmap.getHeight();

final float[] blurMatrix = makeGaussianBlur1DKernel(radius, radius / 2f);

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++) {

final int left = j - radius;

final int right = j + radius;

int index = 0;

float r = 0, g = 0, b = 0;

for (int m = left; m <= right; m++) {

int x = m;

if (x >= width) x = width - 1 - (x - width);

if (x < 0) x = -x;

final int pixel = pixels[i * width + x];

r = r + blurMatrix[index] * Color.red(pixel);

g = g + blurMatrix[index] * Color.green(pixel);

b = b + blurMatrix[index] * Color.blue(pixel);

index++;

}

pixels[i * width + j] = Color.rgb((int) r, (int) g, (int) b);

}

}

//纵向高斯模糊

for (int i = 0; i < width; i++) {

for (int j = 0; j < height; j++) {

final int top = j - radius;

final int bottom = j + radius;

int index = 0;

float r = 0, g = 0, b = 0;

for (int m = top; m <= bottom; m++) {

int y = m;

if (y >= height) y = height - 1 - (y - height);

if (y < 0) y = -y;

final int pixel = pixels[y * width + i];

r = r + blurMatrix[index] * Color.red(pixel);

g = g + blurMatrix[index] * Color.green(pixel);

b = b + blurMatrix[index] * Color.blue(pixel);

index++;

}

pixels[j * width + i] = Color.rgb((int) r, (int) g, (int) b);

}

}

outBitmap.setPixels(pixels, 0, width, 0, 0, width, height);

bitmap.recycle();

return outBitmap;

}

运行一下,处理同一张图片的时间缩短到了 98ms。然而时间复杂度还可以降的更低,有一种作法是通过多次(通常是三次)均值模糊来模拟高斯模糊的效果。因为均值模糊的权值都为 1,所以可以记录一下移动过程中半径内像素的和,在 for 循环移动的过程中完成对所有像素点的计算,这样时间复杂度就降到了 O(n*n)。

public static Bitmap boxBlur(Bitmap bitmap, int radius) {

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 < width; i++) {

int r = 0, g = 0, b = 0;

int count = 0;

//处理边界情况先将模糊半径内像素导入并求和

for (int k = 0; k < radius; k++) {

final int pixel = pixels[k * width + i];

r += Color.red(pixel);

g += Color.green(pixel);

b += Color.blue(pixel);

count++;

}

int headIndex = 0;

//纵向逐个像素移动

for (int j = 0; j < height; j++) {

//预测尾部位置

int last = j + radius;

//超过宽度则从和中减去队列头部像素

if (last >= height) {

final int headPixel = pixels[headIndex * width + i];

r -= Color.red(headPixel);

g -= Color.green(headPixel);

b -= Color.blue(headPixel);

count--;

headIndex++;

} else if (count <= 2 * radius + 1) {

//队列长度不足2 * radius + 1时向队列尾部添加新的像素

final int pixel = pixels[j * width + i];

r += Color.red(pixel);

g += Color.green(pixel);

b += Color.blue(pixel);

count++;

} else {

//队列长度益处后加入新像素,移除头部像素

final int headPixel = pixels[headIndex * width + i];

final int pixel = pixels[j * width + i];

r = r - Color.red(headPixel) + Color.red(pixel);

g = g - Color.green(headPixel) + Color.green(pixel);

b = b - Color.blue(headPixel) + Color.blue(pixel);

headIndex++;

}

pixels[j * width + i] = Color.rgb(r / count, g / count, b / count);

}

}

//横向模糊

for (int i = 0; i < height; i++) {

int r = 0, g = 0, b = 0;

int count = 0;

for (int k = 0; k < radius; k++) {

final int pixel = pixels[i * width + k];

r += Color.red(pixel);

g += Color.green(pixel);

b += Color.blue(pixel);

count++;

}

int headIndex = 0;

for (int j = 0; j < width; j++) {

int last = j + radius;

if (last >= width) {

final int headPixel = pixels[i * width + headIndex];

r -= Color.red(headPixel);

g -= Color.green(headPixel);

b -= Color.blue(headPixel);

count--;

headIndex++;

} else if (count <= 2 * radius + 1) {

final int pixel = pixels[i * width + j];

r += Color.red(pixel);

g += Color.green(pixel);

b += Color.blue(pixel);

count++;

} else {

final int headPixel = pixels[i * width + headIndex];

final int pixel = pixels[i * width + j];

r = r - Color.red(headPixel) + Color.red(pixel);

g = g - Color.green(headPixel) + Color.green(pixel);

b = b - Color.blue(headPixel) + Color.blue(pixel);

headIndex++;

}

pixels[i * width + j] = Color.rgb(r / count, g / count, b / count);

}

}

outBitmap.setPixels(pixels, 0, width, 0, 0, width, height);

bitmap.recycle();

return outBitmap;

}

同一张图片,上面的代码花费的时间为 31ms,如果采用不同半径进行三次运算来模拟高斯模糊的话,花费的时间大约会在 100ms 左右,看来起跟上面第二种算法差别不大。我们把模糊半径设置为 20,上面的算法花费的时间为 26ms,而第二种算法的时间为 249ms。

准确的说这个算法的时间复杂度应该是 O(n * (n + m)),看起来已经快逼近极限了,那还有没有办法更快呢?聪明的你肯定会说,使用 JNI 把运算过程丢给执行效率更高的 C 语言,这几个 for 循环也完全可以并行计算。你说的很对,而且这一切都不用你自己去实现,android 为此已经提供了一套解决方案。下面就郑重有请今天的主角 RenderScript。

RenderScript 是 android 上的高性能计算密集型框架,在 native 层进行数据并行运算,并且可以充分利用多核CPU 以及 GPU 的运算能力。语言本身是基于 C99 标准的,会先用 LLVM 编译称字节码,然后会在设备运行时编译成相应的机器码,所以他是平台无关的。

android 提供了一些内置的 api,我们可以直接在 java 层 调用,如下:

名称

说明

ScriptIntrinsic3DLUT

把 RGB 转换成 RGBA

ScriptIntrinsicBLAS

ScriptIntrinsicBlend

混合两张图片,类似 imageView 的 tint

ScriptIntrinsicBlur

对图片进行高斯模糊运算

ScriptIntrinsicColorMatrix

将图片乘上一个色彩矩阵,可以实现诸如灰度化等色彩偏移效果

ScriptIntrinsicConvolve3x3

3 * 3 卷积运算 (事实上,均值模糊和高斯模糊都是对图像矩阵进行了卷积)

ScriptIntrinsicConvolve5x5

5 * 5 卷积运算

ScriptIntrinsicHistogram

ScriptIntrinsicLUT

ScriptIntrinsicResize

图像缩放

ScriptIntrinsicYuvToRGB

YUV 转 RGB

下面我就用 ScriptIntrinsicBlur 对图像进行高斯模糊,代码非常的简单:

public static Bitmap blur(Context context, Bitmap image, float radius) {

RenderScript rs = RenderScript.create(context);

Bitmap outputBitmap = Bitmap.createBitmap(image.getHeight(), image.getWidth(), Bitmap.Config.ARGB_8888);

Allocation in = Allocation.createFromBitmap(rs, image);

Allocation out = Allocation.createFromBitmap(rs, outputBitmap);

ScriptIntrinsicBlur intrinsicBlur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

intrinsicBlur.setRadius(radius);

intrinsicBlur.setInput(in);

intrinsicBlur.forEach(out);

out.copyTo(outputBitmap);

image.recycle();

rs.destroy();

return outputBitmap;

}

同一张图片,模糊半径设为 20,rs 处理的时间仅为 4ms。

可以看到在上面的代码中把 Bitmap 分配给了 Allocation。Allocation 用于和 RenderScript 共享内存。此外,还有 Element 和 Type。

名称

说明

描述了一个内存单元,可以表示一个 rs 中的基本单位如 float,或者是一个由一系列基本单位组成的结构体

描述了一段内存结构,是一个抽象的概念,并不实际分配内存。由一个 Element 和一个或多个维度组成(通常是一数组的 Element)

某一个 Type 所描述的类型实际所分配的内存

对 intrinsicBlur 设置完相应的参数后,调用 intrinsicBlur.forEach(out) 就能并行的将数据输出到 out 中了。

至此,关于图像模糊的部分就结束了,但 rs 的部分并没有结束,除了使用系统内置的 api 外,我们是可以编写自己的rs脚本的,并且在编译期间会自动生成相应的 java 类。

有时候我们会遇到这样的需求,在对图片进行模糊之后,为了使盖在上面的文本看起来更清楚,还需在图片上加一层灰色的蒙版。当然你可以再往上盖一层背景为灰色带透明度的 view,不过有可能你的父布局是一个 linearLayout。再套一层的话是不能忍受的,其实我们完全可以减少这些由 measure、layout 所带来的不必要的性能开销。一种方式是通过给 ImageView 设置 colorfilter 或者 tint,在 PorterDuff.Mode.SRC_OVER 模式下应该可以达到同一种效果,不过有时候我们可能并没有使用 ImageView 而是设置了一个 BackgroundDrawable。既然在做高斯模糊的时候用了 rs,为什么不把这个操作紧跟着模糊之后处理呢,而且 rs 是并行计算的。

其实上面也提了内置的 ScriptIntrinsicBlend 可以实现 tint 的效果,但为了演示如何写一个 renderScript 我们自己来实现一下这个效果,而且你难道不好奇颜色在计算机中是如何处理的吗?

计算机中的颜色只是对现实中颜色进行量化后进行的抽象,最后还是由显示器根据这些值来决定具体以何种颜色呈现在我们眼前的。这也是为什么同一种色号在不同的显示器上看到的颜色可能不一样。现在比较常用的 是256 色,rgb 都用 0-255 的整数表示,正好各占 1 个字节。不过有的图片格式像是 png 还有一个 alpha 通道,如果 alpha 不等于 255 的话,他的颜色又是如何显示的呢?

在 ps 中先填充满不透明的白色,然后再在上面新建一个图层并用 #999 填充满,然后把这个图层的透明度调至 40%。

0x99 = 153

0xff = 255

214 = 255 * 0.6 + 153 * 0.4

看来 c = c1 * (1 - a)+ c2 * a,最终 a = 255

那如果一张透明度为 0.6(为了直观下面全部换成小数展示)的图片,与一张透明度为 0.5 的图片进行叠加最后的透明度会是多少呢?

我们可以把这两张图片想象成现实生活的两块玻璃。当光穿过透明度为 0.6 的玻璃后,只剩下 40% 的强度,这 40% 的光再穿过透明度 0.5 的玻璃后剩下 20% 强度的光,所以我们可以认为这两块玻璃叠加之后,透明度变为了 0.8。所以透明度的计算公式为:

a = 1 - (1 - a1 ) (1 - a2) = a1 + a2 - a1a2

接下来就是计算叠加后的颜色了,我们可以假设叠加后的颜色为 x,然后将 x 与一张不透明的图片叠加,得到的结果应该等于不透明图片先叠加第一张图片,再叠加第二张图片。于是我们就得到一个方程,解得 x:

x = (c1a1 * (1-a2) + c2a2 ) / (a1 + a2 - a1a2)

有了理论基础,现在我们就可以动手去实现了,首先在项目的 main 文件夹下新建一个名为 rs 的文件夹,然后在这个文件夹下新建我们的 tint.rs 文件。在编译的过程中会从这个文件夹下寻找 rs 文件,生成后的字节码文件可在 app/build/generated/res/rs/ 目录下找到。要编译自己写的rs,我们还需在 gradle 中加入以下代码:

defaultConfig {

...

renderscriptTargetApi 18

renderscriptSupportModeEnabled true

}

下面就可以编写我们的 tint.rs 了

#pragma version(1)

#pragma rs java_package_name(com.test.helper)

uchar4 maskColor = {0, 0, 0, 0};

static uchar mixRGB (uchar src, uchar mask, float inAlpha, float maskAlpha, float outAlpha) {

return (uchar) (((src * (1 - maskAlpha) + mask * maskAlpha) / (inAlpha + maskAlpha - maskAlpha * inAlpha)) * outAlpha);

}

uchar4 __attribute__((kernel)) mask(uchar4 in) {

uchar4 out = in;

float inAlpha = (float)in.a / 255;

float maskAlpha = (float)maskColor.a / 255;

float outAlpha = 1 - (1 - maskAlpha) * (1 - inAlpha);

out.r = mixRGB(in.r, maskColor.r, inAlpha, maskAlpha, outAlpha);

out.g = mixRGB(in.g, maskColor.g, inAlpha, maskAlpha, outAlpha);

out.b = mixRGB(in.b, maskColor.b, inAlpha, maskAlpha, outAlpha);

out.a = (uchar) (outAlpha * 255);

return out;

}

第一行注释指定了 rs 的版本,目前只有 1。第二行的话指定了对应生成的 java 文件的包名。第三行定义了蒙版的颜色,可以看到是 uchar4 类型的。这其实是一个由4个uchar类型组成的结构体,而 uchar 的话之前说过,rs 是跨平台的,所以他抽象了一层数据类型来保持类型的统一:

| | 8bits | 16bits | 32bits | 64bits |

| ---- | -----| ---- | -----|

| integer: | char, int8_t | short, int16_t | int32_t | long, long long , int64_t |

| Unsigned integer: | uchar, uint8_t | ushort, uinit16_t | uint, uint32_t | ulong, uinit64_t |

| Floating point: | | half | float | double |

在 rs 里面声明的变量会在自动生成的java文件中自动生成 get、set 方法,生成的代码如下:

private Element __U8_4;

private FieldPacker __rs_fp_U8_4;

private final static int mExportVarIdx_maskColor = 0;

private Short4 mExportVar_maskColor;

public synchronized void set_maskColor(Short4 v) {

mExportVar_maskColor = v;

FieldPacker fp = new FieldPacker(4);

fp.addU8(v);

int []__dimArr = new int[1];

__dimArr[0] = 1;

setVar(mExportVarIdx_maskColor, fp, __U8_4, __dimArr);

}

public Short4 get_maskColor() {

return mExportVar_maskColor;

}

public Script.FieldID getFieldID_maskColor() {

return createFieldID(mExportVarIdx_maskColor, null);

}

//private final static int mExportForEachIdx_root = 0;

private final static int mExportForEachIdx_mask = 1;

public Script.KernelID getKernelID_mask() {

return createKernelID(mExportForEachIdx_mask, 35, null, null);

}

他具体做了些什么我们其实并不用关心,只要知道会生成 get 和 set 方法就可以了。

之后我们定义了一个 mixRGB 方法根据公式来混合像素点,将其声明为 static 方法,这样在编译的时候不会写进 java,而作为 rs 内部的方法使用。

接下来的 mask 方法是关键,__attribute__((kernel)) 可以简写为 RS_KERNEL,因为 rs 为了方便事先做了如下定义:

#define RS_KERNEL __attribute__((kernel))

这个声明告诉 rs,这个方法作为 mapping kernel 使用,他会在并行运算的时候被调用。类似函数式编程中的 map,对数据集依次执行 function,而这里被 RS_KERNEL 声明的方法就是这个 function。此外还有一个 reduction kernel,将数据集通过此方法合并成一个单一的值,类似于函数式编程中的 reduce 或者说 fold。 reduction kernel 是这样声明的:

#pragma rs reduce(addint) accumulator(addintAccum)

static void addintAccum(int *accum, int val) {

*accum += val;

}

编译时 mask 方法会在 java 文件中对应生成以下方法:

public void forEach_mask(Allocation ain, Allocation aout) {

forEach_mask(ain, aout, null);

}

public void forEach_mask(Allocation ain, Allocation aout, Script.LaunchOptions sc) {

// check ain

if (!ain.getType().getElement().isCompatible(__U8_4)) {

throw new RSRuntimeException("Type mismatch with U8_4!");

}

// check aout

if (!aout.getType().getElement().isCompatible(__U8_4)) {

throw new RSRuntimeException("Type mismatch with U8_4!");

}

Type t0, t1; // Verify dimensions

t0 = ain.getType();

t1 = aout.getType();

if ((t0.getCount() != t1.getCount()) ||

(t0.getX() != t1.getX()) ||

(t0.getY() != t1.getY()) ||

(t0.getZ() != t1.getZ()) ||

(t0.hasFaces() != t1.hasFaces()) ||

(t0.hasMipmaps() != t1.hasMipmaps())) {

throw new RSRuntimeException("Dimension mismatch between parameters ain and aout!");

}

forEach(mExportForEachIdx_mask, ain, aout, null, sc);

}

现在我们就可以在进行模糊处理之后加上一层蒙版了:

public static Bitmap blur(Context context, Bitmap image, float radius, String color) {

RenderScript rs = RenderScript.create(context);

Bitmap outputBitmap = Bitmap.createBitmap(image.getHeight(), image.getWidth(), Bitmap.Config.ARGB_8888);

Allocation tmp1 = Allocation.createFromBitmap(rs, image);

Allocation tmp2 = Allocation.createFromBitmap(rs, outputBitmap);

blur(rs, tmp1, tmp2, radius);

tint(rs, tmp2, tmp1, color);

tmp1.copyTo(outputBitmap);

image.recycle();

rs.destroy();

return outputBitmap;

}

private static void blur(RenderScript rs, Allocation in, Allocation out, float radius) {

ScriptIntrinsicBlur intrinsicBlur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

intrinsicBlur.setRadius(radius);

intrinsicBlur.setInput(in);

intrinsicBlur.forEach(out);

}

private static void tint(RenderScript rs, Allocation in, Allocation out, String color) {

ScriptC_tint mScript = new ScriptC_tint(rs);

mScript.set_maskColor(convertColor2Short4(color));

mScript.forEach_mask(in, out);

}

private static Short4 convertColor2Short4(String color) {

int c = Color.parseColor(color);

short b = (short) (c & 0xFF);

short g = (short) ((c >> 8) & 0xFF);

short r = (short) ((c >> 16) & 0xFF);

short a = (short) ((c >> 24) & 0xFF);

return new Short4(r, g, b, a);

}

生成的 java 类名由 ScriptC_ 加上 rs 文件名组成,我们先调用 set_maskColor 方法设置蒙版的颜色 Color.parseColor 会把 argb 按顺序存在一个 int 中,所以我们可以用位运算提取每个通道的值),最后调用 forEach_mask 并发执行我们写在 rs 中的 mask 方法。

验证一下,效果跟直接覆盖一层view是完全一样的,但是性能完胜。

现在基本上加载图片都是用一些三方库来完成的,所以最后我们可以稍微封装一下,比如你用 picasso 来加载图片可以自定义一个 Transformation,而且如果你的图片模糊的比较厉害的话为了性能我们可以把原图缩小一点,图片缩放也可以用 rs 来完成,还记得上面提到的 ScriptIntrinsicResize 吗?

public static Bitmap blur(Context context, Bitmap image, float scale, float radius, String color) {

RenderScript rs = RenderScript.create(context);

final int width = (int) (image.getWidth() * scale);

final int height = (int) (image.getHeight() * scale);

Bitmap outputBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

Allocation in = Allocation.createFromBitmap(rs, image);

Type t = Type.createXY(rs, in.getElement(), width, height);

Allocation tmp1 = Allocation.createTyped(rs, t);

Allocation tmp2 = Allocation.createTyped(rs, t);

//缩放

ScriptIntrinsicResize theIntrinsic = ScriptIntrinsicResize.create(rs);

theIntrinsic.setInput(in);

theIntrinsic.forEach_bicubic(tmp1);

//模糊

ScriptIntrinsicBlur intrinsicBlur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

intrinsicBlur.setRadius(radius);

intrinsicBlur.setInput(tmp1);

intrinsicBlur.forEach(tmp2);

//蒙版

ScriptC_tint mScript = new ScriptC_tint(rs);

mScript.set_maskColor(convertColor2Short4(color));

mScript.forEach_mask(tmp2, tmp1);

tmp1.copyTo(outputBitmap);

image.recycle();

rs.destroy();

return outputBitmap;

}

public class BlurTransformation implements Transformation {

private Context context;

private String color = "#00000000";

private float radius = 20f;

private int maxSize = 32;

private boolean shouldScale = false;

public BlurTransformation(Context context) {

this.context = context;

}

public BlurTransformation setMaskColor(String color) {

this.color = color;

return this;

}

public BlurTransformation setRadius(float radius) {

this.radius = radius;

return this;

}

public BlurTransformation setMaxSize(int maxSize) {

this.maxSize = maxSize;

return this;

}

public BlurTransformation shouldScale(boolean shouldScale) {

this.shouldScale = shouldScale;

return this;

}

@Override

public Bitmap transform(Bitmap source) {

final int width = source.getWidth();

final int height = source.getHeight();

float scale = 1;

if (shouldScale) {

scale = width > height ? (float) maxSize / width : (float) maxSize / height;

}

return BlurHelper.blur(context, source, scale, radius, color);

}

@Override

public String key() {

return "rounded_corners";

}

}

最后只需要这样调用一下就 ok 了

Picasso.with(this)

.load(R.drawable.b)

.transform(new BlurTransformation(this)

.setMaskColor("#661e1433")

.shouldScale(true)

.setMaxSize(32)

.setRadius(20))

.into(imageView);

参考资料

android图片模糊效果,Android下实现高效的模糊效果相关推荐

  1. Android 下实现高效的模糊效果

    Android 下实现高效的模糊效果 2017-07-29  Dajavu  code小生 作者 | Dajavu 地址 | http://www.jianshu.com/p/4abce9d7b347 ...

  2. android 图片分析,Android图片处理实例分析

    本文实例讲述了Android图片处理的方法.分享给大家供大家参考,具体如下: package cn.szbw.util; import Android.content.Context; import ...

  3. android 图片拍照,Android获取图片拍照时间

    为什么写这篇文章是因为今早有个需求需要获取图片拍照时的时间进行一些处理,有些方法参数名忘记了,所以谷歌百度了一下,Android 图片 时间,Android 图片 拍照 时间,这几个关键字居然无法搜索 ...

  4. android 图片列表,Android 列表使用(ListView GridView Gallery图片计时滚动)

    ListView 作用: 1.将数据填充到布局. 2.处理用户的选择点击等操作. 根据列表的适配器类型,列表分为三种,ArrayAdapter,SimpleAdapter和SimpleCursorAd ...

  5. android 图片变量,Android开发实现ImageView加载摄像头拍摄的大图功能

    本文实例讲述了Android开发实现ImageView加载摄像头拍摄的大图功能.分享给大家供大家参考,具体如下: 这个方法是从官方demo中摘录的,在此记录学习. 权限 android:name=&q ...

  6. android 图片气泡,android图片上显示气泡消息

    经常需要在一个图片上面显示一个气泡,在气泡上面显示消息的数量.这里提供一种方法,直接设置textview的背景色,并且设置textview的文本信息. xmlns:tools="http:/ ...

  7. Android图片褪色,Android – 使用Alpha褪色动画闪烁图片

    我一直在努力了几天,在这一切,终于刚决定问.它很简单我必须缺少一些非常基本的东西. 我有一个XML布局页面定义了一个图像.我有两个anim XML页面,一个将alpha从0更改为1,另一个从1更改为0 ...

  8. android 图片 切换,Android 应用开发笔记 - 切换图片(ImageSwitcher)

    在面板拖拽图标,然后更改相关属性(Properties),如下: android:id="@+id/imgSwit01" android:layout_width="wr ...

  9. android 图片淡入,Android使用ImageView淡入和淡出

    要实现这一点,你已经开始,你将需要添加一个 AnimationListener,以便您可以检测动画的开始和结束.当调用淡出的onAnimationEnd()时,您可以将ImageView对象的可见性设 ...

最新文章

  1. FPGA设计心得(11)关于FIFO IP核使用的一点注意事项
  2. CentOS进不了mysql
  3. Android使用C/C++来保存密钥
  4. php博客文章修改,wordpress博客如何快速修改文章阅读数
  5. BugkuCTF-MISC题MuMuMisc的简单题
  6. java演练 猜奇偶小游戏开发 DB游戏必输的设计
  7. 校验用户登录手机端还是PC端,是否微信浏览器打开
  8. 剑指offer:2.二维数组的查找(Java版)
  9. matplotlib.pyplot库解析
  10. android 开源病毒库,ClamAV+ClamTK,开源杀毒软件
  11. GD32VF103启动流程分析
  12. RangeError:Maximum call stack size exceeded
  13. SACD ISO提取DSF文件及添加封面
  14. 一款ModbusRTU/TCP485串口协议调试软件工具绿色版免安装
  15. Python之动态检测编码chardet
  16. 查找恶意的TOR中继节点
  17. linux mysql 超级用户_Linux下MySQL忘记超级用户口令的解决办法linux操作系统 -电脑资料...
  18. 北语计算机基础知识作业1,【图】- 北京语言大学20秋《计算机基础》作业1 - 昆山经济开发区其他教育培训 - 昆山百姓网...
  19. CentOS7/Debian 配置SOCKS5代理服务记录
  20. 网络连接正常但IP却Ping不通解答

热门文章

  1. Python-str2int
  2. 解决[Vue warn]: Unknown custom element: <ag> - did you register the component correctly? For recursive
  3. 最高月薪15K!当过老师、卖过保险的退伍小哥,用三个月开启技术人生!
  4. 用python写银行叫号系统(这个是学校的实训题目,真的没什么技术含量)
  5. Android:音乐播放器(3)—从播放列表到播放歌曲
  6. 用vmware安装redhat 9出现“光盘无法被挂载”错误的解决办法
  7. 「用ChatGPT搞钱年入百万!」各路博主发布生财之道,网友回呛:答辩的搬运工...
  8. Unity 径向模糊 简易解决方案
  9. CleanMyMac序列号密钥如何清理苹果电脑内存?
  10. OK6410 V4L2 分析