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

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

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

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

blob.png

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

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

blob.png

这里我不会讨论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上获得。

1-ZeULXAdCeiS12CC4OBBvYw.png

 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。让我介绍一下上面代码中使用到的三个重要的对象:

  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目录。

#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并写关于它的文章。