RenderScript :简单而快速的图像处理
英文原文:Android : Simple and fast image processing with RenderScript
想用几行代码就能完成图片编辑吗?想不用复杂的OpenCL就能利用好你手机的GPU计算能力吗?那么,renderscript就是为你量身定做的。
仍然对这个强大工具的效果没有信心?那么我们用数据说话:
我比较了renderscript的模糊与基于java 的fastblur,fastblur可以在这里找到。山脉图片的分辨率是4806x3604像素。当在我的Nexus 6P上进行模糊处理时,renderscript花了738毫秒。而fastblur直接无法工作(out of memory)!所以我试了一个小点的图片(1944x1944),fastblur用了1,354毫秒,同时再次用renderscript尝试,花了160毫秒,快了8倍多。
下图是java与renderscript在高斯模糊性能上的比较:
这里我不会讨论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。
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。让我介绍一下上面代码中使用到的三个重要的对象:
-
Allocation: 内存分配是在java端完成的因此你不应该在每个像素上都要调用的函数中malloc。我创建的第一个allocation是用bitmap中的数据装填的。第二个没有初始化,它包含了一个与第一个allocation的大小和type都相同多2D数组。
-
Type: “一个Type描述了 一个Allocation或者并行操作的Element和dimensions ” (摘自 developer.android.com)
-
Element: “一个 Element代表一个Allocation内的一个item。一个 Element大致相当于RenderScript kernel里的一个c类型。Elements可以简单或者复杂” (摘自 developer.android.com)
例子2 : 直方图均衡化
现在你理解了基础,可以开始编写我们自己的脚本了。
Y的直方图均衡化算法很简单:
-
把RGB颜色空间转换成YUV颜色空间。
-
计算Y分量的直方图。
-
根据直方图重新映射Y分量。
-
重新把YUV转换回RGB颜色空间。
注:感谢Stephen Akridge 的帮助我重新编辑了代码(见评论)。现在快了20%,非常感谢他!
我们现在可以开始创建我们的rs文件了:histEq.rs,位于rs目录。
#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代码示例:
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消息和一个或者多个变量作为参数。
#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并写关于它的文章。