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