RenderScript渲染利器

作者: 扎实基础cc | 来源:发表于2016-08-14 10:08 被阅读1267次

RenderScript

RenderScript是Android系统中能高效处理大量计算任务的框架,特别适用一些需要处理图片和加载图片以及计算机视觉的方面应用。

本文将先从如何使用出发,然后介绍关于 RenderScript的部分高级知识。

在阅读本文之前,需要知道的是Renderscript是用C99(C的一种标准)实现的。

First of all

在app的build.gradle文件加入下面两行:

 defaultConfig {
        applicationId "com.uniquestudio.renderscript"
        minSdkVersion 14
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        renderscriptTargetApi 18
        renderscriptSupportModeEnabled true
    }

考虑到兼容性,在使用RenderScript的类中,要引入:import android.support.v8.renderscript.*;

从实用性出发,下面介绍三种处理图片的方式。

Blur Image

现在很多APP都在使用 模糊 效果的图片,RenderScript提供了ScriptIntrinsicBlur帮助我们实现模糊效果。一个渲染的过程其实就是一个加工的过程,把送进来的原材料加工成想要的产品。

看一下这个加工过程:

 /**
     *
     * @param bitmap src
     * @param radius the radius of blur ,max is 25
     * @param context
     * @return a blur bitmap
     */
    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 blur 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;
    }

参数radius是模糊程度。虽然我们没有自己写脚本,但是抛开脚本,从上层java代码我们不难发现,使用RenderScript的几个关键步骤:

  • 将数据填充在Allocation对象中,一般来说需要两个Allocation,一个存放,原材料另一个是作为加工之后的数据存放地
  • 开启渲染
  • 将Allocation中的产品输出
  • 回收资源

代码很简单,不需要太多解释。在模糊效率上,是用java实现的fastblur速度的8倍,而且在ORM的问题上也有优化。
效果图:

原图
blur

Sketch Image

实现素描效果,处理的算法很简单:

求RGB平均值Avg = (R + G + B) / 3,如果Avg >= 100,则新的颜色值为R=G=B=255;如果Avg < 100,则新的颜色值为R=G=B=0;255就是白色,0就是黑色;至于为什么用100作比较,这是一个经验值吧,设置为128也可以,可以根据效果来调整。

STEP1:Writing a RenderScript Kernel

首先从渲染脚本文件写起,在main文件下新建一个包res,在res中新建sketch.rs注意文件的后缀是rs
我们先看一下官方文档的写法:

A simple kernel may look like the following:
uchar4 __attribute__((kernel)) invert(uchar4 in, uint32_t x, uint32_t y) {
  uchar4 out = in;
  out.r = 255 - in.r;
  out.g = 255 - in.g;
  out.b = 255 - in.b;
  return out;
}

文档对此的解释:

  • The first notable feature is the attribute((kernel)) applied to the function prototype. This denotes that the function is a RenderScript kernel instead of an invokable function.(attribute((kernel))用来区分kernel和invokable方法)
  • 函数参数的x,y,或者z是可选的,但是类型必须是uint32_t
  • pragma version(1)是指版本号,目前只能选择1,没有更高的版本了。
  • pragma rs java_package_name(com.hc.renderscript)是告诉编译器,包的名称。因为每个rs文件都会自动生成对应的Java代码。

我对in参数的理解是allocation中的Element

#pragma version(1)
#pragma rs_fp_relaxed
#pragma rs java_package_name(com.uniquestudio.renderscript)

// Debugging RenderScript
#include "rs_debug.rsh"

uchar4 __attribute__((kernel)) invert(uchar4 in, uint32_t x, uint32_t y) {
   //Convert input uchar4 to float4
   float4 f4 = rsUnpackColor8888(in);

   float r = f4.r;
   float g = f4.g;
   float b = f4.b;
   rsDebug("Red", r);

   if((r+g+b)*255/3>=100){
       return rsPackColorTo8888(1, 1, 1, f4.a);
   }else{
       return rsPackColorTo8888(0, 0, 0, f4.a);
   }
}

Tip:#include "rs_debug.rsh"可以打印log,但是在kernel method中打印log会造成io操作,使得整个处理过程的时间变长。

STEP2:Build project to generate .classes file

build之后会生成ScriptC_sketchjava文件,格式固定ScriptC_xxx,以此类推。

STEP3:Call scripts from Java code

RenderScript中用java代码控制脚本的生命周期。

public class SketchUtil {
    public static Bitmap sketchBitmap(Bitmap bitmap,Context context){
        RenderScript renderScript = RenderScript.create(context);
        ScriptC_sketch sketchScript = new ScriptC_sketch(renderScript);

        Allocation in = Allocation.createFromBitmap(renderScript,bitmap);
        Allocation out = Allocation.createTyped(renderScript,in.getType());

        // call kernel
        sketchScript.forEach_invert(in,out);

        out.copyTo(bitmap);

        renderScript.destroy();
        sketchScript.destroy();
        in.destroy();
        out.destroy();

        return bitmap;
    }
}

最后的效果图:


sketch

是不是很简单?之前我在kernel方法中打印过log,我发现,在java层中调用forEach_xx会遍历传入的Allocation参数,那么kernel方法中不要自己写循环了。

Magnifier Image

实现放大镜效果,类似这种:


example

假如我们定义放大镜的坐标为(atX,atY),半径为radius,而放大倍数为scale,那么其实就是将原图中的坐标为(atX,atY)、半径为radius/scale的区域的图像放大到放大镜覆盖的区域即可,算法其实很简单,对图片上的每一个点(X,Y),求其与(atX,atY)的距离Distance,若Distance < Radius,则取原图中坐标为(X/scale,Y/scale)的像素的颜色值作为新的颜色值。

这个问题的难点在于如何取回(X/scale,Y/scale)的像素,我们必然要存储所有的像素,以便我们取回。这里需要使用到脚本文件中与Allocation对应的rs_allocation以及rsGetElementAt_uchar4(allocation, X, Y)函数

具体的流程和上面的Sketch相同。源代码:

// Needed directive for RS to work
#pragma version(1)

// The java_package_name directive needs to use your Activity's package path
#pragma rs java_package_name(com.uniquestudio.renderscript)

// Store the input allocation
rs_allocation inputAllocation;

// Magnifying
// TODO: here, some checks should be performed to prevent atX and atY to be < 0, as well
//   as them to not be greater than width and height
int atX;
int atY;
float radius;
float scale; // The scale is >= 1

uchar4 __attribute__((kernel)) magnify(uchar4 in, int x, int y) {

    // Calculates the distance between the touched point and the current kernel
    // iteration pixel coordinated
    // Reference: http://math.stackexchange.com/a/198769
    float pointDistanceFromCircleCenter = sqrt(pow((float)(x - atX),2) + pow((float)(y - atY),2));


    // Is this pixel outside the magnify radius?
    if(radius < pointDistanceFromCircleCenter)
    {
        // In this case, just copy the original image
        return in;
    }


    // If the point is inside the magnifying inner radius, draw the magnified image

    // Calculates the current distance from the chosen magnifying center
    float diffX = x - atX;
    float diffY = y - atY;

    // Scales down the distance accordingly to scale and returns the original coordinates
    int originalX = atX + round(diffX / scale);
    int originalY = atY + round(diffY / scale);

    // Return the original image pixel at the calculated coordinates
    return rsGetElementAt_uchar4(inputAllocation, originalX, originalY);
}

Java Code:

 public static Bitmap magnifierBitmap(Bitmap bitmap, int x, int y, int radius,int scale, Context context){
        RenderScript rs = RenderScript.create(context);

        Allocation in = Allocation.createFromBitmap(rs, bitmap);
        Allocation out = Allocation.createTyped(rs,in.getType());


        ScriptC_magnifier magnifier = new ScriptC_magnifier(rs);

        magnifier.set_inputAllocation(in);
        magnifier.set_atX(x);
        magnifier.set_atY(y);
        magnifier.set_radius(radius);
        magnifier.set_scale(scale);


        magnifier.forEach_magnify(in,out);

        out.copyTo(bitmap);

        rs.destroy();
        magnifier.destroy();
        in.destroy();
        out.destroy();

        return bitmap;
}

效果图:


Magnifier

Advanced RenderScript

这一部分介绍两个layer(runtime和reflected),了解关于renderscript编译和执行。

RenderScript Runtime Layer

RenderScript的编译和执行都发生在这个层。

编译的过程:共进行两次编译。

  • 第一次.rs文件被llvm compiler编译为字节码,
  • 第二次将字节码由设备上的llvm compiler编译为机器码,而且机器码会在设备上存储起来,这样之后RenderScript的执行就不再需要编译字节码了。

这样就不难解释为什么RenderScript的移植性比较高了。

RenderScript运行的库中有几个关键的功能:

  • 内存分配功能
  • 大量的数学计算函数
  • 数据类型的转化
  • log函数

Reflected Layer

为了能从Android Framwork层访问RenderScript Runtime层,Android Build Tools生成了反射层。

.rs脚本文件被反射成位于project_root/gen/package/name/ScriptC_*renderscript_filename*的类,也就是.rs的java版本文件,然后我们就能从Android Framework层调用了。

.rs文件的反射中,无非是要把文件的变量和函数进行反射。

Variables

如果在RenderScript中有以下声明:

uint32_t unsignedInteger = 1;

那么反射之后会产生:

private long mExportVar_unsignedInteger;
public void set_unsignedInteger(long v){
    mExportVar_unsignedInteger = v;
    setVar(mExportVarIdx_unsignedInteger, v);
}

public long get_unsignedInteger(){
    return mExportVar_unsignedInteger;
}

所以我们在Java层就可以使用get或者set对RenderScript中的变量进行操作了。

但是如果在RenderScript中有const修饰变量时,就会不会产生set方法。

Functions

如果在RenderScript中定义了这样一个函数:

void touch(float x, float y, float pressure, int id) {
    if (id >= 10) {
        return;
    }

    touchPos[id].x = x;
    touchPos[id].y = y;
    touchPressure[id] = pressure;
}

反射之后产生:

public void invoke_touch(float x, float y, float pressure, int id) {
    FieldPacker touch_fp = new FieldPacker(16);
    touch_fp.addF32(x);
    touch_fp.addF32(y);
    touch_fp.addF32(pressure);
    touch_fp.addI32(id);
    invoke(mExportFuncIdx_touch, touch_fp);
}

RenderScript中函数最好不要带返回值,RenderScript本身就是异步的,如果函数有返回值,那么Android Framework调用时就会一直阻塞,直到有值返回,这样在处理大量计算任务的时候会直接影响效率。

上面的代码已经上传到github,传送门

相关文章

网友评论

  • 乘法表:这些rs******的方法都是从哪里找来的?
  • 好多个胖子:hi 博主您好,文中提到创建sketch.rs时,先在main文件夹下创建res包,但是在as中main文件夹下面已经有res资源包了,是否是新建别的包名呢?我尝试直接写在res资源包下面,并没有生成对应的java文件,还望指导一下,感谢
    62ac1da2e7da:lz:我对in参数的理解是allocation中的Element
    这么解释差不多,可以把allocation看作容器,Type看作链表(比如List),Element看作链表中的数据(比如int)。kernel函数就是并行的将链表中的数据一个一个的映射成in然后进行计算。再将结果out映射到输出容器进行返回。
    62ac1da2e7da:@好多个胖子 不是这么建立文件夹的,AS中方法是:右键java目录-》移动到New->选择Folder-》接着选择RenderScript Folder。然后会让你选择目录位置,直接选择默认。接着AS会在main目录下新建一个rs的文件夹。将RS代码放在里面就行了。
    res是资源文件夹

本文标题:RenderScript渲染利器

本文链接:https://www.haomeiwen.com/subject/zjgesttx.html