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 文件实现。
这个方法的第一个步骤灰度化不做详细介绍了,代码比较直观。
第二个步骤处理了边缘查找和采样:

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<Int2> pointz = new ArrayList<Int2>();
    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<Integer> 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 信息都是按顺序输出的,是可控的调用。

log

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,602评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 一. Java基础部分.................................................
    wy_sure阅读 3,805评论 0 11
  • ***本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 ** 前言 之前在知乎看到一篇关于 Lo...
    阪有桑阅读 1,780评论 2 5
  • 一个彻底诚实的人是从不面对选择的,那条路永远会清楚地呈现在你面前,这和你的憧憬无关,就像你是一棵苹果树,你憧憬结出...
    田裡和樹阅读 418评论 0 1