Android高效计算-RenderScript介绍及简单使用(附Sobel轮廓图提取实现)

关于RenderScriptRenderScript是安卓平台上很受谷歌推荐的一个高效计算平台,它能够自动把计算任务分配到各个可用的计算核心上,包括CPU,GPU以及DSP等,能大大提高在图片处理、数学模型等领域提供高效的计算能力;在使用的时候只需要关心算法的实现,不需要关心核心、线程、资源的调度。语言本身是用于编写高性能计算代码的 C99 衍生语言。

官方文档:https://developer.android.com/guide/topics/renderscript/compute (查参数类型、API会用到,网上资料很少,这个非常有用)

官方demo:https://github.com/android/renderscript-samples
优点

  1. 可移植性:对于不同架构,不同的处理器都不需要考虑代码的差异化,因为都是运行时在设备上进行编译的;
  2. 高性能:提供充分利用所有核心的无缝的并行化计算
  3. 易用性:简化编码,不需要像JNI一样写胶水代码

缺点

  1. 开发复杂,需要学习新的api,代码纯手撸
  2. 调试困难,出问题难排查

使用(图片处理相关,数学运算方面暂时没研究)

在app的build.gradle的defaultConfig中添加:

renderscriptTargetApi 18
renderscriptSupportModeEnabled true

src/main文件夹下创建rs文件夹,用于存放rs脚本文件

创建一个rs文件

//编译声明
//RenderScript版本,目前只有1
#pragma version(1)
//声明编译后Java文件所在的包
#pragma rs java_package_name(tech.lihang.rs)
//精度控制,主要有三个等级
//rs_fp_full 默认等级 表示的完全遵守IEEE 754-2008 standard的精度要求
//rs_fp_felaxed 不严格的IEEE 754-2008 standard的精度要求 正常用这个就可以了
//rs_fp_imprecise 比relaxed更低的精度要求
#pragma rs_fp_relaxed
//转灰度rgb最佳比例,常量
const static float3 gMonoMult = {0.299f, 0.587f, 0.114f};
//这里定义的值算是变量,可以通过外部的java对象来进行赋值 uint32_t width;
//RenderScript入口函数,in代表的是当前像素(还有其他入口函数,详见文档)
//uchar4是bitmap像素在rs中的类型,是一个4位的无符号float数组{r,g,b,a}(每个元素值的范围是0~255)
//x y 是当前元素在bitmap中的位置(坐上角为原点)
//函数名就是最终生成java方法的名字(forEach_calculate),java类名是ScriptC_{文件名}
uchar4 __attribute__((kernel)) calculate(uchar4 in, uint32_t x, uint32_t y){
    //图片转灰度示例,
    //实现算法时最好查下文档,文档中有很多封装的计算函数,能最大化的提高运算效率
    float gray = (in.r+in.g+in.b)/3.0f;
    in.r = gray;
    in.g = gray;
    in.b = gray;
    //返回处理后的像素值
    return in;
}

调用(Kotlin)

val rs = RenderScript(context) val script = ScriptC_sobel(rs) val inBitmap = 输入的bitmap对象 val outBitmap = 输出的bitmap对象 val ain = Allocation.createFromBitmap(rs,inBitmap) val aout = Allocation.createFromBitmap(rs,outBitmap) script.forEach_calculate(ain,aout) script._width = 100//这就是刚才在rs文件中声明的变量,在使用时通过script对象进行赋值 aout.copyTo(outBitmap) //结束,outBitmap就是处理后的图片了 官方还内置了几个处理脚本,如高斯模糊val rs = RenderScript(context) val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)) val inBitmap = 输入的bitmap对象 val outBitmap = 输出的bitmap对象 val ain = Allocation.createFromBitmap(rs,inBitmap) val aout = Allocation.createFromBitmap(rs,outBitmap) script.setInput(ain) script.setRadius(3f)//模糊半径,值越大图片越模糊,最大25 script.forEach(aout) aout.copyTo(outBitmap) //结束,outBitmap就是处理后的图片了
Sobel轮廓提取脚本

参照上面的调用示例即可返回轮廓图,生成的轮廓图是轮廓点的地方是非透明像素,非轮廓点是透明像素//编译声明 //RenderScript版本,目前只有1 #pragma version(1) //声明编译后Java文件所在的包 #pragma rs java_package_name(dev.lihang.cannyedge) //精度控制,主要有三个等级 //rs_fp_full 默认等级 表示的完全遵守IEEE 754-2008 standard的精度要求 //rs_fp_felaxed 不严格的IEEE 754-2008 standard的精度要求 正常用这个就可以了 //rs_fp_imprecise 比relaxed更低的精度要求 #pragma rs_fp_relaxed //转灰度rgb最佳比例 const static float3 gMonoMult = {0.299f, 0.587f, 0.114f}; //透明像素 rgba const static uchar4 alphaPixel = {0.0f,0.0f,0.0f,0.0f}; //非透明像素 rgba const static uchar4 edgePixel = {0.0f,0.0f,0.0f,255.0f}; //rs_matrix3x3 3*3矩阵,在rs中是一个长度为9的float数组,sobel算子 const static rs_matrix3x3 sobelX = {-1.0f , 0.0f , 1.0f, -2.0f , 0.0f , 2.0f, -1.0f , 0.0f , 1.0f}; const static rs_matrix3x3 sobelY = {-1.0f , -2.0f , -1.0f, 0.0f , 0.0f , 0.0f, 1.0f , 2.0f , 1.0f}; //这里定义的值算是变量,可以通过外部的java对象来进行赋值 //图片的宽度,使用时需先赋值 uint32_t width; //图片的高度,使用时需先赋值 uint32_t height; //梯度阈值,大于这个值就算作边缘点,高斯模糊后图像更平滑,边缘点梯度差异可能变小,需适当降低阈值 //最终感觉比较合适的数值是,高斯模糊半径3,边缘梯度阈值25 float threshold; //源图片对象 rs_allocation originBitmap; //RenderScript入口函数,in代表的是当前像素(还有其他入口函数,详见文档) //uchar4是bitmap像素在rs中的类型,是一个4位的无符号float数组{r,g,b,a}(每个元素值的范围是0~255) //x y 是当前元素在bitmap中的位置(坐上角为原点) //在c++实现中是嵌套的for循环来遍历每个像素,在这只需要关心当前像素即可(系统会自动并行调用遍历每个像素) uchar4 __attribute__((kernel)) calculateSobel(uchar4 in, uint32_t x, uint32_t y){ if(x<1 || x>=width || y<1|| y>=height){ return alphaPixel; } //rsDebug(“RSLog x = “,x); //rsDebug(“RSLog y = “,y); //rsDebug(“RSLog width = “,width); //rsDebug(“RSLog height = “,height); //从原图中拿到当前像素周围的像素点 uchar4 lt = rsGetElementAt_uchar4(originBitmap,x-1,y-1); uchar4 t = rsGetElementAt_uchar4(originBitmap,x,y-1); uchar4 rt = rsGetElementAt_uchar4(originBitmap,x+1,y-1); uchar4 l = rsGetElementAt_uchar4(originBitmap,x-1,y); uchar4 r = rsGetElementAt_uchar4(originBitmap,x+1,y); uchar4 lb = rsGetElementAt_uchar4(originBitmap,x-1,y+1); uchar4 b = rsGetElementAt_uchar4(originBitmap,x,y+1); uchar4 rb = rsGetElementAt_uchar4(originBitmap,x+1,y+1); //求出每一个像素的灰度值,这里用的是r+g+b除以3 float ltG = (lt.r+lt.g+lt.b)/3.0f; float tG = (t.r+t.g+t.b)/3.0f; float rtG = (rt.r+rt.g+rt.b)/3.0f; float lG = (l.r+l.g+l.b)/3.0f; float cG = (in.r+in.g+in.b)/3.0f; float rG = (r.r+r.g+r.b)/3.0f; float lbG = (lb.r+lb.g+lb.b)/3.0f; float bG = (b.r+b.g+b.b)/3.0f; float rbG = (rb.r+rb.g+rb.b)/3.0f; //对灰度值进行卷积,对应矩阵中因数为0的就不进行计算了 //x方向的卷积 float pixel_x = (rsMatrixGet(&sobelX,0,0) * ltG) + (rsMatrixGet(&sobelX,0,2) * rtG) + (rsMatrixGet(&sobelX,1,0) * lG) + (rsMatrixGet(&sobelX,1,2) * rG) + (rsMatrixGet(&sobelX,2,0) * lbG) + (rsMatrixGet(&sobelX,2,2) * rbG); //y方向的卷积 float pixel_y = (rsMatrixGet(&sobelY,0,0) * ltG) + (rsMatrixGet(&sobelY,0,1) * tG) + (rsMatrixGet(&sobelY,0,2) * rtG) + (rsMatrixGet(&sobelY,2,0) * lbG) + (rsMatrixGet(&sobelY,2,1) * bG) + (rsMatrixGet(&sobelY,2,2) * rbG); //计算梯度值,梯度值体现的当前像素,是左右/上下两个像素在灰度上的差异(颜色有没有较大的变化) float boundary_gray = sqrt(pixel_x * pixel_x + pixel_y * pixel_y); if(boundary_gray>threshold){ //梯度大于阈值,设置为边缘像素颜色,返回非透明色 return edgePixel; }else{ //梯度小于阈值,返回透明色 return alphaPixel; } } 原图

轮廓图(梯度阈值25)

这时由于原图噪点较多轮廓图有点模糊不清,噪点较多解决方法有两个,提高梯度阈值或者先对图片使用高斯模糊,使图片更平滑后再进行轮廓图生成比较好的方法是先高斯模糊再生成轮廓高斯模糊后的图片

高斯模糊后的轮廓图

噪点少了很多,轮廓边缘更平滑圆润(还有优化空间,使用完整的Canny轮廓提取算法,这里只使用了前两步,高斯滤波+Sobel算法)这是从最初的Java实现开始,感觉较为完美的实现,也是运行速度最快的实现
运行时间1000*1000的图,Java 10+s,NDK 1000毫秒左右,手机性能差的话可能会更长RS 高斯滤波+轮廓生成100毫秒左右(实际计算时间十几毫秒,创建脚本对象较为耗时,中低端机也差不多是这个时间😏,巴适),且轮廓图更为精细
在附加一个四级中使用的一个优化点:由于在使用的时候图片的展示方式是fitCenter,如果图片较小生成的轮廓图也会较小,轮廓图展示出来等比放大后会有锯齿解决方案是:原图片加载后,对图片加载的view进行截图(只截图片显示的部分),然后生成轮廓图,这样轮廓图的像素就完全是对应图片真实显示的像素生成,无论原图片是过大还是过小,整体的效果是一致的