RenderScript :简单而快速的图像处理

RenderScript :简单而快速的图像处理

泡在网上的日子 发表于 2016-05-04 10:16   1723  次阅读  RenderScript,图像处理
1

编辑推荐:稀土掘金,这是一个针对技术开发者的一个应用,你可以在掘金上获取最新最优质的技术干货,不仅仅是Android知识、前端、后端以至于产品和设计都有涉猎,想成为全栈工程师的朋友不要错过!

英文原文:Android : Simple and fast image processing with RenderScript 

想用几行代码就能完成图片编辑吗?想不用复杂的OpenCL就能利用好你手机的GPU计算能力吗?那么,renderscript就是为你量身定做的。

仍然对这个强大工具的效果没有信心?那么我们用数据说话:

RenderScript :简单而快速的图像处理_第1张图片

我比较了renderscript的模糊与基于java 的fastblur,fastblur可以在这里找到。山脉图片的分辨率是4806x3604像素。当在我的Nexus 6P上进行模糊处理时,renderscript花了738毫秒。而fastblur直接无法工作(out of memory)!所以我试了一个小点的图片(1944x1944),fastblur用了1,354毫秒,同时再次用renderscript尝试,花了160毫秒,快了8倍多。

下图是java与renderscript在高斯模糊性能上的比较:

RenderScript :简单而快速的图像处理_第2张图片

这里我不会讨论NDK因为我没有足够的相关知识,但是你可以在这里查看renderscript 和 NDK之间的比较。

Renderscript基于C99 (Ed. C 语言),因此你需要熟悉那门语言。如果你已经了解java那么也不难掌握其基础。

首先,你需要在build.gradle文件中添加那两行加粗的代码:

android {

    compileSdkVersion 23

    buildToolsVersion "23.0.3"

    defaultConfig {

        minSdkVersion 8

        targetSdkVersion 19

        renderscriptTargetApi 18

        renderscriptSupportModeEnabled true

    }

}

如果你的app的minSDK为16或者更低,你需要使用support模式,因为很多方法都是在API 17之后添加的。

renderscriptTargetApi最高到23,但是你应该把它设置到能保持脚本中使用到的功能完整的最低API。如果你想在support模式下target API 21+你必须使用gradle-plugin 2.1.0 和 buildToolsVersion “23.0.3” 或者以上。

Renderscript将使用C写的脚本并行计算图片的每一个像素。一个脚本就是一个扩展名为‘.rs’ 的文件,必须置于app/src/main/rs。Android Studio 并不会为你生成这个目录或者任何脚本。

为了举例说明本文,我做了一个sample app来计算YUV色彩空间Y分量上的直方图均衡化(见下图),以及用它来模糊图片。可以在github上获得。

 Y 分量上的Histogram Equalization (前后对比)


例子1 : 模糊图像

我们首先从一个简单的任务开始:模糊一张图像。这个例子不需要renderscript代码,因为api提供了一个类:ScriptIntrinsicBlur。

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
public static Bitmap blurBitmap(Bitmap bitmap, float radius, Context context) {
     //Create renderscript
     RenderScript rs = RenderScript.create(context);
 
     //Create allocation from Bitmap
     Allocation allocation = Allocation.createFromBitmap(rs, bitmap);
     
     Type t = allocation.getType();
 
     //Create allocation with the same type
     Allocation blurredAllocation = Allocation.createTyped(rs, t);
 
     //Create script
     ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
     //Set blur radius (maximum 25.0)
     blurScript.setRadius(radius);
     //Set input for script
     blurScript.setInput(allocation);
     //Call script for output allocation
     blurScript.forEach(blurredAllocation);
 
     //Copy script result into bitmap
     blurredAllocation.copyTo(bitmap);
 
     //Destroy everything to free memory
     allocation.destroy();
     blurredAllocation.destroy();
     blurScript.destroy();
     t.destroy();
     rs.destroy();
     return  bitmap;
}

可以看到这个方法返回一个模糊了的bitmap。让我介绍一下上面代码中使用到的三个重要的对象:

  1. Allocation: 内存分配是在java端完成的因此你不应该在每个像素上都要调用的函数中malloc。我创建的第一个allocation是用bitmap中的数据装填的。第二个没有初始化,它包含了一个与第一个allocation的大小和type都相同多2D数组。

  2. Type: “一个Type描述了 一个Allocation或者并行操作的Element和dimensions ” (摘自 developer.android.com)

  3. Element: “一个 Element代表一个Allocation内的一个item。一个 Element大致相当于RenderScript kernel里的一个c类型。Elements可以简单或者复杂” (摘自 developer.android.com)

例子2 : 直方图均衡化

现在你理解了基础,可以开始编写我们自己的脚本了。

Y的直方图均衡化算法很简单:

  1. 把RGB颜色空间转换成YUV颜色空间。

  2. 计算Y分量的直方图。

  3. 根据直方图重新映射Y分量。

  4. 重新把YUV转换回RGB颜色空间。

注:感谢Stephen Akridge 的帮助我重新编辑了代码(见评论)。现在快了20%,非常感谢他!

我们现在可以开始创建我们的rs文件了:histEq.rs,位于rs目录。

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
#pragma version(1)
#pragma rs_fp_relaxed
#pragma rs java_package_name(com.example.q.renderscriptexample)
 
#include "rs_debug.rsh"
 
int32_t histo[256];
float remapArray[256];
int size;
 
//Method to keep the result between 0 and 1
static float bound (float val) {
     float m = fmax(0.0f, val);
     return  fmin(1.0f, m);
}
 
uchar4 __attribute__((kernel)) root(uchar4  in , uint32_t x, uint32_t y) {
     //Convert input uchar4 to float4
     float4 f4 = rsUnpackColor8888( in );
 
     //Get YUV channels values
     float Y = 0.299f * f4.r + 0.587f * f4.g + 0.114f * f4.b;
     float U = ((0.492f * (f4.b - Y))+1)/2;
     float V = ((0.877f * (f4.r - Y))+1)/2;
 
     //Get Y value between 0 and 255 (included)
     int32_t val = Y * 255;
     //Increment histogram for that value
     rsAtomicInc(&histo[val]);
 
     //Put the values in the output uchar4, note that we keep the alpha value
     return  rsPackColorTo8888(Y, U, V, f4.a);
}
 
uchar4 __attribute__((kernel)) remaptoRGB(uchar4  in , uint32_t x, uint32_t y) {
     //Convert input uchar4 to float4
     float4 f4 = rsUnpackColor8888( in );
 
     //Get Y value
     float Y = f4.r;
     //Get Y value between 0 and 255 (included)
     int32_t val = Y * 255;
     //Get Y new value in the map array
     Y = remapArray[val];
 
     //Get value for U and V channel (back to their original values)
     float U = (2*f4.g)-1;
     float V = (2*f4.b)-1;
 
     //Compute values for red, green and blue channels
     float red = bound(Y + 1.14f * V);
     float green = bound(Y - 0.395f * U - 0.581f * V);
     float blue = bound(Y + 2.033f * U);
 
     //Put the values in the output uchar4
     return  rsPackColorTo8888(red, green, blue, f4.a);
}
 
void init() {
     //init the array with zeros
     for  (int i = 0; i < 256; i++) {
         histo[i] = 0;
         remapArray[i] = 0.0f;
     }
}
 
void createRemapArray() {
     //create map for y
     float sum = 0;
     for  (int i = 0; i < 256; i++) {
         sum += histo[i];
         remapArray[i] = sum / (size);
     }
}

这里有几个方法:

  • bound(float val): 这个方法用于让结果保持在0到1之间。

  • root(): 这个方法是为input Allocation的每一个像素而调用(被叫做一个kernel)。它把像素从RGBA转换成YUVA,它把结果放在 output allocation。它还增加了Y直方图的值。

  • remaptoRGB(): 这个方法也是一个kernel。它重新映射Y值然后从YUVA转换回RGBA。

  • init(): 当在java中创建脚本的时候,这个方法自动被调用。它用0初始化数组。

  • createRemapArray(): 它为Y分量创建remap数组。

你可以像以往在c中那样创建方法。但是这里如果你需要像我在bound()中那样返回什么东西,这个方法必须是静态的。

从Java代码中调用脚本

现在你的脚本已经准备好了,你需要在java代码中调用它。

当编译项目的时候将生成这些脚本的Java类(因此记住要在Java中使用你的脚本之前编译)。如果你有一个叫做foo.rs的脚本,将生成名为ScriptC_foo的类。你可以向构造器中传入RenderScript对象来实例化它。

你可以通过调用参数为input和output allocation的Each_root()方法 来调用kernel方法,它将计算每一个像素的root方法。

当你的rs脚本使用了一个全局变量,java代码中将生成 setter 和 getter,因此如果你使用一个叫做value的全局变量,你可以用script.set_value(yourValue) 来赋值,script.get_value()来获取值。

如果你使用一个array作为全局变量的,你可以定义一个Allocation type,并使用set 或 get 方法,或者定义一个type的array并用script.bind_variableName(yourAllocation)来绑定它。然后在script脚本中用rsGetElementAt_type(variableName, x, y) 获取值并用rsSetElementAt_type(variableName, element, x, y)赋值。

这里是Y 直方图均衡化的java代码示例:

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
public static Bitmap histogramEqualization(Bitmap image, Context context) {
     //Get image size
     int width = image.getWidth();
     int height = image.getHeight();
 
     //Create new bitmap
     Bitmap res = image.copy(image.getConfig(),  true );
 
     //Create renderscript
     RenderScript rs = RenderScript.create(context);
 
     //Create allocation from Bitmap
     Allocation allocationA = Allocation.createFromBitmap(rs, res);
 
     //Create allocation with same type
     Allocation allocationB = Allocation.createTyped(rs, allocationA.getType());
 
     //Create script from rs file.
     ScriptC_histEq histEqScript =  new  ScriptC_histEq(rs);
     
     //Set size in script
     histEqScript.set_size(width*height);
 
     //Call the first kernel.
     histEqScript.forEach_root(allocationA, allocationB);
 
     //Call the rs method to compute the remap array
     histEqScript.invoke_createRemapArray();
 
     //Call the second kernel
     histEqScript.forEach_remaptoRGB(allocationB, allocationA);
 
     //Copy script result into bitmap
     allocationA.copyTo(res);
 
     //Destroy everything to free memory
     allocationA.destroy();
     allocationB.destroy();
     histEqScript.destroy();
     rs.destroy();
 
     return  res;
}

Debugging RenderScript

现在你还不能使用debugger去分析你的renderScript(参见这里Stephen Hine在评论里的回复),但是你可以使用log。

要在rs文件里使用log你需要 include “rs_debug.sh”。然后你可以使用rsDebug方法,将log消息和一个或者多个变量作为参数。

1
2
3
4
5
6
7
8
9
10
#pragma version(1)
#pragma rs java_package_name(com.example.q.renderscriptexample)
#include "rs_debug.rsh"
 
void root(const uchar4 *v_in, uchar4 *v_out, const void *usrData, uint32_t x, uint32_t y) {
     float4 f4 = rsUnpackColor8888(*v_in);
 
     rsDebug( "Red" , f4.r);
     *v_out = rsPackColorTo8888(f4.r,f4.g,f4.b,f4.a);
}

我是在Pictarine工作的安卓图片处理工程师,我们每天要在100,000以上张图片上运算复杂的自动增强脚本。这要使用许多脚本,比如这里的直方图均衡化(两次),一般这需要花费不到1秒的时间去计算。我们非常喜欢这个强大的工具,如果你准备做图片处理,我强烈推荐你去使用它。

感谢Baptiste(我的团队领导),是他鼓励我学习renderscript并写关于它的文章。

只有会员才允许收藏操作!

你可能感兴趣的:(RenderScript :简单而快速的图像处理)