如何使用 RenderScript实现抖音的黑金效果

原创
2021/09/19 00:54
阅读数 176

最近,有人问我一个问题,如何使用彩图转为黑白,又如何将黑白图片转换为彩图?对于这个问题,我能想到的最直接的方法是:调用Android的系统Api获取图片生成bitmap文件,然后再使用Android中的二值化技术即可实现;除此之外,还可以使用FFpeg等库的方式实现。不过,我们今天要讲的是另外一种方案,即使用RenderScript方式。

一、RenderScript简介

RenderScript 是用于在 Android 上以高性能运行计算密集型任务的框架。RenderScript 专为数据并行计算而设计,不过串行工作负载也可以从中受益。RenderScript 运行时可以并行安排设备上可用的多个处理器(如多核 CPU 和 GPU)上的工作负载,使开发者能够专注于表达算法而不是调度工作。RenderScript 对于专注于图像处理、计算摄影或计算机视觉的应用来说尤其有用。

RenderScript使用的是一种类似于C/C++的rs 脚本语法,且是在运行时编译、跨平台的。性能比 Java 好,比 Native 略差。下图是RenderScript在Android 8.0 及更高版本的设备上的一个框架示意图。
在这里插入图片描述

与 Android 7.x 及更低版本中的 RenderScript 之间的区别如下:

  • 一个进程中有两组 RenderScript 内部库的实例。一组用于 CPU 备用路径,直接来源于 /system/lib;另一组用于 GPU 路径,来源于 /system/lib/vndk-sp。

  • /system/lib 中的 RS 内部库是作为平台的一部分构建的,会随着 system.img 的升级而更新。不过,/system/lib/vndk-sp 中的库是面向供应商构建的,不会随着 system.img 的升级而更新(虽然可以针对安全修复程序进行更新,但其 ABI 仍然保持不变)。

  • 供应商代码(RS HAL、RS 驱动程序和 bcc plugin)与位于 /system/lib/vndk-sp 的 RenderScript 内部库相关联。它们无法与 /system/lib 中的库相关联,因为该目录中的库是面向平台构建的,可能与供应商代码不兼容(即,符号可能会被移除)。如此一来可能会导致仅针对框架的 OTA 无法实现。

关于RenderScript的说明,可以参考RenderScript架构组成

二、RenderScript使用

RenderScript的使用分为两个步骤:

  1. 编写 .rs 内核脚本文件;
  2. 使用编写的文件进行渲染方面的处理;

2.1 编写内核脚本文件

RenderScript 内核通常位于 <project_root>/src/ 目录下,由类C语言的.rs语法编写,每个.rs 文件就是一个脚本,每个脚本由一组内核、函数和变量构成。首先,创建一个rs脚本文件代码。
在这里插入图片描述

在这里插入图片描述
然后,打开 app 的 build.gradle 文件,在 android 的 defaultConfig 结点添加两句:

renderscriptTargetApi 18
renderscriptSupportModeEnabled true

接下来,下面以【将图片置灰】为例来说明如何编写内核脚本文件,新建一个 Gray.rs 文件,如下所示。

#pragma version(1)
#pragma rs java_package_name(com.avatar.rs)

void root(const uchar4 *in, uchar4 *out, uint32_t x, uint32_t y) {
    // a 是透明度,这里不修改透明度。
    out->a = in->a;

    // 快,但并不是真正意义的去色
    out->r = out->g = out->b = (in->r + in->g + in->b) / 3;

    // 慢,但是是真正的去色
    // out->r = out->g = out->b = (in->r * 299 + in->g * 587 + in->b * 114 + 500) / 1000;
}

void init() {
}

其中,第1行声明 RenderScript 的版本;第2行是申明该脚本所在的Java包的包名;root 函数是脚本文件的入口函数,对于图片来说,root函数负责对每一个像素做处理。参数 in 是输入像素点的指针; out 是输出像素点的指针。并且,init 函数是可选的,主要用于做一些初始化的工作。

2.2 调用rs脚步文件

使用前,需要先引入RenderScript脚本文件,如下所示。

import com.avatar.rs.ScriptC_greyscale;

这里的类名是 ScriptC_ 加上 .rs 的文件名,包名就是在创建 rs 文件时声明的包名。

import com.avatar.rs.ScriptC_greyscale;

public class MainActivity extends AppCompatActivity {

    private ImageView mImageView;
    private ImageView mRSImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView =  findViewById(R.id.image_view);
        mRSImageView =  findViewById(R.id.rs_image_view);

        Bitmap mInBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
        Bitmap  mOutBitmap = Bitmap.createBitmap(mInBitmap.getWidth(), mInBitmap.getHeight(), mInBitmap.getConfig());
        mImageView.setImageBitmap(mInBitmap);

        Bitmap bitmap=transGray(this,mInBitmap);
        mRSImageView.setImageBitmap(bitmap);
    }


    public static Bitmap transGray(@NonNull Context context, @NonNull Bitmap bitmap) {
        // 创建输出 bitmap
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Bitmap outBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        // 创建 RenderScript 对象
        RenderScript rs = RenderScript.create(context);
        // 创建输入、输出 Allocation
        Allocation allIn  = Allocation.createFromBitmap(rs, bitmap);
        Allocation allOut = Allocation.createFromBitmap(rs, outBitmap);
        // 创建我们在上面定义的 script
        ScriptC_greyscale script = new ScriptC_greyscale(rs);
        // 对每一个像素执行 root 方法
        script.forEach_root(allIn, allOut);
        // 将执行结果复制到输出 bitmap 上
        // 释放资源
        rs.destroy();
        return outBitmap;
    }
}

然后,我们运行下代码,看看前后的对比效果。
在这里插入图片描述

2.3 多函数调用

除了 root 函数,我们还可以在 .rs 中定义其他的 kernal 函数,比如:

/** 
 * 黑金色转换
 */
uchar4 __attribute__((kernel)) blackGold(uchar4 in, uint32_t x, uint32_t y) {
    uchar4 out = in;

    if ((in.r < in.b) && (in.g < in.b)) {
        out.r = out.g = out.b = (in.r*299 + in.g*587 + in.b*114 + 500) / 1000;
    }

    return out;
}


uchar4 __attribute__((kernel)) root(uchar4 v_in) {
    float4 f4 = rsUnpackColor8888(v_in);

    float3 mono = dot(f4.rgb, gMonoMult);
    return rsPackColorTo8888(mono);
}

uchar __attribute__((kernel)) toU8(uchar4 v_in) {
    float4 f4 = convert_float4(v_in);
    return (uchar)dot(f4.rgb, gMonoMult);
}

uchar4 __attribute__((kernel)) toU8_4(uchar v_in) {
    return (uchar4)v_in;
}

在定义kernal 函数时,函数返回值必须是 uchar4, 并且用 __attribute__((kernel)) 标记该函数是个 kernal 函数。然后,我们在Java代码中就可以使用下面的方式进行调用了。

script.forEach_blackGold(allIn, allOut);

本文同步分享在 博客“xiangzhihong8”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部
返回顶部
顶部