Android RenderScript 实现 LowPoly 效果(三)

***本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 **

前言

拖了好久,终于到了这系列的主要部分——在 Android 中使用 RenderScript 实现 LowPoly 的详细过程。示例下面 Github 中,有兴趣的同学可以参考,喜欢的可以 star 一下,谢谢。

示例

Github-LowPoly

demo_with_different_accuracy

*Gif 图片加载有点慢,加载完后显示的点击再次渲染的速度还是挺快的

使用

MainActivity.java

...
{
...
    LowPoly.createLowPoly(this, bitmapOriginal, accuracy, RENDERED_FLAG);

}

static Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case RENDERED_FLAG:
                    ivOut.setImageBitmap(LowPoly.bmpRendered);
                    Log.e(TAG, "Render FINISH in==" + (System.currentTimeMillis() - time) + " ms");
                    System.gc();
                    break;
            }
        }
};
...

由于 LowPoly 处理结果不在 UI 线程中,所以使用 Handler 设置渲染后的 Bitmap 到 ImageView 中。

lowpoly.rs

#include "pragma.rsh"

#define STATUS_GRAY 1
#define STATUS_SOBEL 2

int status = 0;

const static float3 gMonoMult = {0.299f, 0.587f, 0.114f};

rs_script gScript;
rs_allocation gOriginal;
rs_allocation gGrayed;
rs_allocation gSobel;

int width,height;
int rand;
int accuracy = 10;

int2 points[15000];
int count =0;

static void setColor(int px,int py){
   float4 l = {1.0f,1.0f,1.0f,1.0f};
    rsSetElementAt(gSobel,&l, px, py);
}

void root(const uchar4 *v_in, uchar4 *v_out, uint32_t x, uint32_t y){
    if(status == STATUS_GRAY){
        float4 f4 = rsUnpackColor8888(*(v_in));
        float3 mono = dot(f4.rgb, gMonoMult);
        *v_out = rsPackColorTo8888(mono);
    }else if(status == STATUS_SOBEL){
        float4 lt = rsUnpackColor8888(*(v_in-width-1));
        float4 ct = rsUnpackColor8888(*(v_in-width));
        float4 rt = rsUnpackColor8888(*(v_in-width+1));

        float4 l = rsUnpackColor8888(*(v_in-1));
        float4 c = rsUnpackColor8888(*(v_in));
        float4 r = rsUnpackColor8888(*(v_in+1));

        float4 lb = rsUnpackColor8888(*(v_in+width-1));
        float4 cb = rsUnpackColor8888(*(v_in+width));
        float4 rb = rsUnpackColor8888(*(v_in+width+1));

        float gx = lt.x*(-1)+l.x*(-2)+lb.x*(-1)+
           rt.x*(1)+r.x*(2)+rb.x*(1);

        float gy = lt.x*(-1)+ct.x*(-2)+rt.x*(-1)+
           lb.x*(1)+cb.x*(2)+rb.x*(1);

        float G = sqrt(gx*gx+gy*gy);

        rand = rsRand(1.0f) * 10 * accuracy;
        if(G > 0.1f && rand == 1){
            setColor(x,y);
            int2 i2 = {x,y};
            points[count] = i2;
            count++;
        }else{
           float3 black = { 0.0f,0.0f,0.0f};
            *v_out = rsPackColorTo8888(black);
        }
    }
}

void process(int stat){
    status = stat;
    rsDebug("process==",status);
    if(status == STATUS_GRAY){
            rsForEach(gScript,gOriginal,gGrayed);
            rsDebug("process GRAY finish==",stat);
            rsSendToClient(101,&count,101);
    }else if(status == STATUS_SOBEL){
            count=0;
            rsForEach(gScript,gGrayed,gSobel);
            rsDebug("process SOBEL finish==",stat);
            rsSendToClient(102,&count,102);
    }
}

void send_points(){
    // to client
    int group = (count-1)/625+1;
    rsDebug("points group==",group);
    rsDebug("points size==",count);
    rsSendToClient(0,&count,group);

    for(int i=1;i<=group;i++){
        int index = 625 *(i-1);
        rsSendToClient(i,&(points[index]),4999);
    }
}

在 rs 脚本中,实现的方法主要为 rootprocesssend_points 三个:

root 中,分两步分别处理 灰度化 和 查找边缘同时采样,第一篇提到的,灰度处理并不是必要步骤,所以可以省略。但是出于两点原因,依旧保留了这个步骤:1.灰度化处理在 rs 耗费的时间经过测试,一般只占 几ms ~ 10+ ms,在不是很苛刻的要求下还是可以接受的;2.图片的分步处理是比较普遍的场景,这样可以熟悉编写复杂 rs 脚本的过程,当前这些步骤也可以通过编写不同的 rs 文件实现。
这个方法的第一个步骤灰度化不做详细介绍了,代码比较直观。
第二个步骤处理了边缘查找和采样:

Android RenderScript 实现 LowPoly 效果(三)_第1张图片
sobel operator

通过使用 Sobel 算子, 计算 {x ,y}点的横向与纵向的亮度差异;同时,在 if(G > 0.1f && rand == 1) 一行,设置 0.1f 为进入总样本集的临界值,当然也可以根据需要在 java 层设置这个值,这个值越小,总样本集的元素越多; rand = rsRand(1.0f) * 10 * accuracy; 这一行,生成一个在 [0,10 * accuracy) 中随机整数, accuracy 值越低,采样精度越高,当 accuracy =1 时,采样率为 1/ (10 * 1) ,即期望上,边缘上每十个亮度值大于 0.1f 的点就会被选为构建三角形的一个元素点。因为采样方法是完全随机的,所以最后的效果有时出现一些不理想的三角形,因此这个步骤的调整对输出结果优化还有很大的提升空间,不过在时间上必然有一定的开销,这里就不作详细讨论。

process这个方法作为 java 层调用入口,传入处理的步骤,当前步骤处理结束后通过 rsSendToClient 方法给 java 层发送通知,类似 Handler 的 sendMessge() 方法,传入三个参数:mID、pointer、dataLength——消息的 ID,发送数组数据的指针地址,数据的长度。在这里调用,除了 mID 在 java 会被用到,另外两个参数并没有什么意义。

send_points 当 java 层收到 process 方法中 SOBEL 处理结束的消息后会被调用,这个方法将 root 第二个步骤选取的采样点发送到 java 层。有一点值得注意的是,rsSendToClient 这个方法第二参数的数组的长度上限为 1250,所以当采样点数量大于 625(一个点有两个数值组成)时,须要分批发送数据。rsSendToClient(0,&count,group); 以 0 为信息 ID,把采样点数,批数(包括当前信息批次),发送给 java 层,rsSendToClient(i,&(points[index]),4999); 分批次把采样点数据发给 java 层,第三个参数没有意义。

以上就是实现 LowPoly 效果 rs 脚本的所有代码,并不复杂,然后是 java 的调用。

LowPoly.java

final static String TAG = "==LowPoly==";

    private static Allocation allocationOriginal;
    private static Allocation allocationGrayed;
    private static Allocation allocationSobel;
    private static RenderScript mRs;
    private static ScriptC_lowpoly scriptLowPoly;
    private static int width, height;

    private static Bitmap mBitmapIn;
    public static Bitmap bmpRendered;

    private static int pointCount;
    private static int groupCount = 100;
    private static Int2[] points = new Int2[10000];
    private static List pointz = new ArrayList();
    private static int RENDERED_FLAG;

    public static void createLowPoly(Context context, Bitmap bitmapIn, int accuracy, int flag) {
        mBitmapIn = bitmapIn;
        RENDERED_FLAG = flag;

        Bitmap bitmapOut = Bitmap.createBitmap(bitmapIn.getWidth(), bitmapIn.getHeight(),
                bitmapIn.getConfig());
        width = bitmapIn.getWidth();
        height = bitmapIn.getHeight();
        Log.e(TAG, "Width==" + width + "==Height==" + height + "==accuracy==" + accuracy);
        createLowPolyScript(context, accuracy, bitmapIn, bitmapOut);

        Log.e(TAG, "Start GRAYED");
        scriptLowPoly.invoke_process(1);
    }

    private static void createLowPolyScript(Context context, int accuracy, final Bitmap bitmapIn, final Bitmap bitmapOut) {
        mRs = RenderScript.create(context);

        allocationOriginal = Allocation.createFromBitmap(mRs, bitmapIn,
                Allocation.MipmapControl.MIPMAP_NONE,
                Allocation.USAGE_SCRIPT);
        allocationGrayed = Allocation.createFromBitmap(mRs, bitmapOut,
                Allocation.MipmapControl.MIPMAP_NONE,
                Allocation.USAGE_SCRIPT);
        allocationSobel = Allocation.createFromBitmap(mRs, bitmapOut,
                Allocation.MipmapControl.MIPMAP_NONE,
                Allocation.USAGE_SCRIPT);

        scriptLowPoly = new ScriptC_lowpoly(mRs);

        scriptLowPoly.set_gScript(scriptLowPoly);
        scriptLowPoly.set_gOriginal(allocationOriginal);
        scriptLowPoly.set_gGrayed(allocationGrayed);
        scriptLowPoly.set_gSobel(allocationSobel);

        scriptLowPoly.set_accuracy(accuracy);
        scriptLowPoly.set_width(width);
        scriptLowPoly.set_height(height);


        mRs.setMessageHandler(new RenderScript.RSMessageHandler() {
            @Override
            public void run() {
                super.run();
                if (mID == 101) {
                    Log.e(TAG, "GRAYED finish");
//                    allocationGrayed.copyTo(bitmapOut);
                    Log.e(TAG, "Start SOBEL");
                    scriptLowPoly.invoke_process(2);
                    return;
                }
                if (mID == 102) {
                    Log.e(TAG, "SOBEL finish");
//                    allocationSobel.copyTo(bitmapOut);
                    scriptLowPoly.invoke_send_points();

                    points = new Int2[10000];
                    pointz.clear();
                    return;
                }

                if (mID == 0) {
                    pointCount = mData[0];
                    groupCount = mLength;
                    Log.e(TAG, "Receive points==" + pointCount + "==by group==" + groupCount);
                } else if (mID == groupCount) {
                    for (int i = 0; i < mData.length; i += 2) {
                        points[i / 2 + 625 * (mID - 1)] = new Int2(mData[i], mData[i + 1]);
                    }
                    for (int i = 0; i < pointCount; i++) {
                        Int2 int2 = points[i];
                        pointz.add(int2);
                    }

                    for (int i = 0; i < 200; i++) {
                        Int2 int2 = new Int2((int) (Math.random() * width), (int) (Math.random() * height));
                        pointz.add(int2);
                    }


                    pointz.add(new Int2(0, 0));
                    pointz.add(new Int2(0, height));
                    pointz.add(new Int2(width, 0));
                    pointz.add(new Int2(width, height));

                    Log.e(TAG, "Points size==" + pointz.size() + "");

                    List tris = Delaunay.triangulate(pointz);

                    Log.e(TAG, "Triangle size== " + tris.size() / 3 + "");

                    bmpRendered = Bitmap.createBitmap((int) (width), (int) (height), Bitmap.Config.ARGB_8888);

                    long t = System.currentTimeMillis();

                    Canvas canvas = new Canvas(bmpRendered);
                    Paint paint = new Paint();
                    paint.setAntiAlias(true);
                    paint.setStyle(Paint.Style.FILL);

                    float x1, x2, x3, y1, y2, y3, cx, cy;
                    for (int i = 0; i < tris.size(); i += 3) {
                        x1 = pointz.get(tris.get(i)).x;
                        x2 = pointz.get(tris.get(i + 1)).x;
                        x3 = pointz.get(tris.get(i + 2)).x;
                        y1 = pointz.get(tris.get(i)).y;
                        y2 = pointz.get(tris.get(i + 1)).y;
                        y3 = pointz.get(tris.get(i + 2)).y;

                        cx = (x1 + x2 + x3) / 3;
                        cy = (y1 + y2 + y3) / 3;

                        Path path = new Path();
                        path.moveTo(x1, y1);
                        path.lineTo(x2, y2);
                        path.lineTo(x3, y3);
                        path.close();

                        paint.setColor(mBitmapIn.getPixel((int) cx, (int) cy));

                        canvas.drawPath(path, paint);
                    }
                    Log.e(TAG, "Canvas cost === " + (System.currentTimeMillis() - t) + " ms");

                    MainActivity.mHandler.sendEmptyMessageAtTime(RENDERED_FLAG, 0);

                    System.gc();
                } else {
                    Log.e(TAG, "Receive group==" + mID);
                    for (int i = 0; i < mData.length; i += 2) {
                        points[i / 2 + 625 * (mID - 1)] = new Int2(mData[i], mData[i + 1]);
                    }
                }
            }
        });
    }

java 层与 rs 层的交互从 scriptLowPoly.invoke_process(1); 开始,在 RSMessageHandler 中处理 rs 发来的数据消息,交互步骤为:
java 层调用 rs 层 process(STATUS_GRAY) 处理灰度化;
rs 层 process(STATUS_GRAY) 处理结束通知 java 层;
java 接到通知后,调用 rs 层 process(STATUS_SOBEL) 处理查找边缘及采样;
rs 层 process(STATUS_SOBEL) 处理结束通知 java 层;
java 层接到采样结束通知后,调用 rs 层 send_points,把采样数据分批发到 java 层;
最后在 java 层完成 Delaunay 三角化,与绘图的过程,RSMessageHandler 不在 UI 线程中,所以使用 Handler 通知 UI 线程设置处理后的 Bitmap。

以上就是,实现 LowPoly 效果的完整过程。

有一点值得注意的是,为什么不在 java 层直接调用 forEach_root 方法处理各个步骤呢?

因为,在 java 层直接调用该方法结束的时间是不能确定的,但是在 rs 中 process 中,rsForEach执行前后的 rsDebug 与 java 的 log 信息都是按顺序输出的,是可控的调用。

Android RenderScript 实现 LowPoly 效果(三)_第2张图片
log

根据一次处理的 log 信息,可以看到,处理一个 600 * 600 的图片,采样率为 1/20 ,总耗时为 479 ms,其中 GRAYED 步骤耗时 6ms ,SOBEL 步骤耗时 18ms, Canvas 绘图耗时 250 ms,其余时间用于数据传输与处理。

至此,关于使用 RenderScript 实现 LowPoly 的介绍就到这里了,有什么疑问或者文章有不对的地方请留言,感谢看到这里的同学。

最后再附上 Github 地址:https://github.com/ReikyZ/LowPoly

你可能感兴趣的:(Android RenderScript 实现 LowPoly 效果(三))