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 脚本中,实现的方法主要为
root
、process
、 send_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 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 信息都是按顺序输出的,是可控的调用。文章图片
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
推荐阅读
- android|android kotlin Dimension
- Android开发笔记|【Android笔记28】Android中的数据存储技术之Shared Preferences
- Android安全|【原创工具】ADBGUI - GUI版ADB操作工具
- android|No version of NDK matched the requested version xxx 问题解决
- 投稿|爱买小样的年轻人,实现“大牌自由”了吗?
- iOS端如何实现微信分享链接与登陆
- SpringBoot使用自定义注解+AOP+Redis实现接口限流
- Android极简MVVM,从一个基类库谈起
- IIS|IIS 实现http重定向https(亲测有效(解决URL重写模块配置https重定向不生效的问题))
- 终于实现了一门属于自己的编程语言