Android|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 2int 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|Android RenderScript 实现 LowPoly 效果(三)
文章图片
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|Android RenderScript 实现 LowPoly 效果(三)
文章图片
log 根据一次处理的 log 信息,可以看到,处理一个 600 * 600 的图片,采样率为 1/20 ,总耗时为 479 ms,其中 GRAYED 步骤耗时 6ms ,SOBEL 步骤耗时 18ms, Canvas 绘图耗时 250 ms,其余时间用于数据传输与处理。
至此,关于使用 RenderScript 实现 LowPoly 的介绍就到这里了,有什么疑问或者文章有不对的地方请留言,感谢看到这里的同学。
【Android|Android RenderScript 实现 LowPoly 效果(三)】最后再附上 Github 地址:https://github.com/ReikyZ/LowPoly

    推荐阅读