在上一篇文章Android自动手绘,圆你儿时画家梦!中结尾提到,我将介绍提升轮廓提取速度相关内容,今天一起学习Android中的RenderScript。看完本文,你将学会如何使用并行计算技术,提高你的app中计算模块速度,尤其是提升图像处理中的复杂计算。
RenderScript介绍
根据Android官方网站的介绍:RenderScript是Android平台上用于运行计算密集任务的框架。RenderScript主要是面向数据并行计算,当然了,RenderScript中使用串行计算效率也很好。RenderScript是充分利用GPU,CPU的计算能力,由于不同的硬件对应的并行执行不同,RenderScript会编译2次,首先是我们的PC编译器编译到apk中,然后在apk安装的时候,再编译一次。这样的好处是,可以充分利用不同的硬件,我们编写的代码无需关心具体的硬件的不同,都能写出高性能的代码。
RenderScript相关文档并不多,导致很难去学好RenderScript。但是其实用起来并不复杂,结合SDK中的两个例子和官方文档,基本可以入门了。
在使用RenderScript之前,请在eclipse的project.properties加上:
renderscript.target=18
renderscript.support.mode=true
Hello RenderScript
概念说太多没啥用,先来一段简单代码。需求很简单,我们需要将一张图片中的每个像素的颜色取反色,即分别将255减去当前像素点的R、G、B,得到的新的RGB作为当前像素点的新颜色。如果不用RenderScript,实现起来也非常简单,通过两个for循环,遍历每个像素点,然后替换像素就好,如下:
int width = mInBitmap.getWidth();
int height = mInBitmap.getHeight();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
int color = mInBitmap.getPixel(x, y);
int r= 255-(Color.red(color) ;
int g= 255-(Color.green(color) ;
int b= 255-(Color.blue(color) ;
int c = Color.rgb(gray, gray, gray);
mOutBitmap.setPixel(x, y, c);
}
}
这是使用普通java代码实现,如果对一张较大的图执行这段代码,其耗时可想而知!再去看看RenderScript是如何实现相同的功能的:
首先,在代码目录下(即包目录下)创建rs文件,取名可以任意,我们新建一个hello.rs文件:
#pragma version(1)
#pragma rs java_package_name(com.hc.renderscript)
uchar4 __attribute__((kernel)) invert(uchar4 in)
{
uchar4 out = in;
out.r =255- in.r;
out.g = 255-in.g;
out.b = 255-in.b;
return out;
}
看不懂?不要急!我们一行一行解释。仔细看会发现其实大部分跟C语言很像,首先#pragma是给编译器看的,#pragma version(1)是指版本号,目前只能选择1,没有更高的版本了。#pragma rs java_package_name(com.hc.renderscript)是告诉编译器,包的名称。因为每个rs文件都会自动生成对应的Java代码,比如,我们新建的hello.rs文件,会自动生成ScriptC_hello类,因此,我们需要在rs声明包的名称。接下来比较重要的关键字__attribute__((kernel)),它跟函数放在一起,用于声明这个函数是个RenderScript核心函数,而不是一个可调用的函数。什么意思呢?其实可以这样理解,就是这个函数不是个普通函数,是用于并行计算的函数。我们不能显式调用,它是RenderScript内部调用的函数。这时你可能会想,既然我们不能显式调用,那该怎么调用呢?别急,接下来为你揭晓。
我们继续看到invert函数,这个函数有个uchar4类型,不用想,肯定表示占用4个字节,每个字节表示的取值范围0~255。但是接下来的事情就很奇怪了,uchar4 in中直接可以用in.r、in.g、in.b分别取出rgb颜色。我猜想uchar4是个结构体类型,本来想去官网查看一下,找了很久没找到。找到的童鞋麻烦告诉我一下,我可以重新编辑这篇文章。但是就算没找到,我们也可以理解的通,其实,如果从本质上来说,它并不复杂,r表示第一个字节,g表示第二个字节,b表示第三个字节。甚至我们可以可以猜得到,还有in.a表示透明度,然后我测试了一下,发现真的编译通过。另外,从RenderScript Basics Tutorial这篇文章可以知道,还可以通过x、y、z、w分别取出对应的第1、2、3、4个字节。也就是说,in.x与in.r都是一个意思.好了这里不再继续纠结uchar4.
RenderScript的核心我们编写完成了,从上面rs文件的invert函数我们知道,这个函数只对具体一个像素点操作,可是我们的图片有width*height个像素点,我们需要这些像素点并行执行inver函数才能得到我们想要的结果。
我们再看看Java代码如何调用,使之并行计算。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSrcImageView = (ImageView) findViewById(R.id.src);
mDstImageView = (ImageView) findViewById(R.id.dst);
mInBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mOutBitmap = Bitmap.createBitmap(mInBitmap.getWidth(),mInBitmap.getHeight(), mInBitmap.getConfig());
mSrcImageView.setImageBitmap(mInBitmap);
RenderScript rs = RenderScript.create(this);
mScript = new ScriptC_hello(rs);
aIn = Allocation.createFromBitmap(rs, mInBitmap);
aOut = Allocation.createFromBitmap(rs, mInBitmap);
mScript.forEach_invert(aIn, aOut);
aOut.copyTo(mOutBitmap);
mDstImageView.setImageBitmap(mOutBitmap);
rs.destroy();
}
运行出来的结果:
反色效果
我们继续解释Java代码:先看到第13行,创建的是一个RenderScript对象。接下来是将我们编写的rs文件对应的自动生成的Java类(即ScriptC_hello类)初始化。到目前为止,这些都很好理解。紧接着是创建了两个Allocation对象,这个对象是干嘛用的呢?从名称可以看出,它是用于分配内存的,createFromBitmap根据Bitmap分配内存。为什么需要创建2个Allocation对象呢?这主要是在执行rs文件里面的并行函数时一个Allocation类型 aIn用于参数传入,一个Allocation类型 aOut用于计算结果输出。这两个Allocation的Element类型必须相同,在函数调用时RenderScript会检查,如果不想同会抛异常。这里提到了Element,Elemtent是指Allocation里的一项。比如我们要处理的是Bitmap,则Element表示的类型是像素。做并行计算时,aIn对应的一个元素(Element)的计算结果会放入aOut对应的位置上。定位到代码:mScript.forEach_invert(aIn, aOut);我们的rs文件里面并没有写forEach_invert函数,但是却在ScriptC_hello 类里面生成了这个函数。请注意,我们编写了invert函数,正因为我们的invert函数加了__attribute__((kernel))关键字,所以,会生成forEach_invert函数,这个函数传入的参数aIn和aOut我们都清楚了,RenderScript会自动将aIn里的每个元素(Element)并行的去执行invert函数.得到的结果放入aOut里。最后调用Allocation的copyTo函数把计算的结果转入到Bitmap中。
另外,值得注意的是,__attribute__((kernel))修饰的函数,其形参该怎么写,为啥我们这里是uchar4而不是uchar3或者是uint32之类的呢?我们该怎么确定好这个参数呢?其实,这主要是跟我们的需求有关,你可以根据需求改动。比如我们的aIn里的元素是像素,而一个像素有RGBA占4个字节,因此我们写成uchar4作为形参。还有就是,后面还可以加形参uint32 x,uint32 y,uint32 z。这些是可选项,可以加也可以不加,不影响函数的调用,但是必须是uint32类型。
还有个可选函数init(),在rs文件里的这个函数会指初始化时调用,并且只会调用一次。
有时候我们希望返回的结果不止一个对象该怎么办?我们可以选择使用全局变量,在rs文件中声明全局变量,在rs文件的函数中把数据写入到rs文件的全局变量中。再从Java代码中读取rs的全局变量即可!那么在Java代码中该怎么读取和设置rs中的全局变量呢?答案是,rs文件对应生成的Java类会自动生成全局变量的get和set方法。比如,在hello.rs文件中定义了全局变量int myVar.自动生成的ScriptC_hello类中会自动生成函数:set_myVar(int v)和get_myVar().这样就可以访问rs文件中的全局变量了。
最后
回到最开始说的,提升上一篇文章的轮廓提取速度。如果没有看过上一篇文章的请跳过,或者是前去: Android自动手绘,圆你儿时画家梦!查看。我们去看看Sobel算法,主要分为2步,首先将彩色图转为灰度图,在CommenUtils类的toGrayscale函数中。然后再是调用Sobelsuanf ,在SobelUtils类的Sobel函数。先看看toGrayscale函数:这个函数是直接调用系统的函数,我们不去管。在Sobel函数中,有两个地方使用了两个for循环,显然可以通过RenderScript进行并行计算,提升速度。篇幅原因,具体的实现这里就不提了。