151

Android NDK之旅——图片高斯模糊

 6 years ago
source link: https://juejin.im/post/59d894c06fb9a00a4843d5f2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
  • 阅读本文可能花费的时间
    15分钟
  • 本文可能了解到的知识
    1. CMake基本使用
    2. Android NDK开发/使用
    3. JNI层操作Java对象

  • 实现效果
    Android使用C/C++实现图片的毛玻璃效果。

  • 注:
    1. 本文研究对象为Android JNI/NDK开发,非图片算法,故不对毛玻璃算法做阐述。
    2. 本人能力有限,如有不妥请指出。

十一假期几天的思考,确立了自己的进阶方向,打算了解下计算机视觉方面的技术,也就是opencv。在Android中集成opencv的话必然要掌握JNI/NDK的开发,所以写了本文,一是向大家分享自己的学习经验,二是巩固自己的JNI/NDK开发和抛弃已久的C/C++方面的知识。

CMake

CMake是一款项目构建工具,通过编写简单明了的在CmakeLists.txt来生成makefile,简单来说就是一个makefile生成器。

在Android Studio中安装CMake非常简单,打开Tools->Android->SDK Manager,选择SDK Tools标签页,勾选CMake、LLDB、NDK,OK自动安装即可。其中LLDB可以使我们在Android Studio中调试C/C++程序。NDK为原生开发工具包,必不可少。

4e602acf78ad7afeaca4b5926f7656c5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp

为什么要做JNI/NDK开发

众所周知,Java/Android程序是运行在JVM/Dalvik VM中,所以Java程序远没有C/C++程序性能高,尤其是在CPU密集型运算时,所以Java平台提供了JNI(Java Native Interface),可通过JNI调用C/C++等编写的so动态链接库。
注:Google在Android L以后用ART彻底代替了Dalvik VM,但ART本质上仍是一个虚拟机,并支持所有Dalvik VM指令集。
Java API中几乎所有与硬件相关的方法都是native的,比如I/O操作、网络访问、手机传感器、串口读写等。
本文涉及的图片处理是一种CPU密集型任务,在Android开发中使用native方法最为合适。

如何使用CMake做JNI/NDK开发

1 新建工程

030ba32094de32b2c2dadb7a416c4535~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp


选中Include C++ Support,意为引入C++支持。

2 配置C++支持

ea878b247dc3b48b81f88f68bae6b8eb~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp


在Customize C++ Support界面默认即可,意为CMake/C++11环境

3 认识CMakeLists.txt

工程创建完毕之后Android Studio会在app目录下生成CMakeLists.txt文件。CMakeLists.txt是CMake的配置文件,用于表明版本、依赖、等信息,以下为Android Studio生成的CMakeLists(过滤注释)

cmake_minimum_required(VERSION 3.4.1)

add_library(native-lib SHARED src/main/cpp/native-lib.cpp)

find_library(log-lib log)

target_link_libraries(native-lib ${log-lib})复制代码
  • cmake_minimum_required(VERSION 3.4.1)
    CMake最小版本使用的是3.4.1。
  • add_library()
    配置so库信息(为当前当前脚本文件添加库)
    • native-lib
      这个是声明引用so库的名称,在项目中,如果需要使用这个so文件,引用的名称就是这个。值得注意的是,实际上生成的so文件名称是libnative-lib。
    • SHARED
      这个参数表示共享so库文件,也就是在Run项目或者build项目时会在目录intermediates\transforms\mergeJniLibs\debug\folders\2000\1f\main下生成so文件。
    • src/main/cpp/native-lib.cpp
      构建so库的源文件。
  • find_library()
    查找一个库文件
    • log-lib
      这个指定的是在NDK库中每个类型的库会存放一个特定的位置,而log库存放在log-lib中
    • log
      指定使用log库
  • target_link_libraries()
    如果你本地的库(native-lib)想要调用log库的方法,那么就需要配置这个属性,意思是把NDK库关联到本地库。
    • native-lib
      要被关联的库名称
    • ${log-lib}
      要关联的库名称,要用大括号包裹,前面还要有$符号去引用。

4 了解JNI的C/C++规范

JNI的数据类型包含两种,分别是基本类型和引用类型,它们和Java中的数据类型对应关系如下两表所示。

基本数据类型
JNI类型 Java类型 描述
jboolean boolean 无符号8位整型
jbyte byte 无符号8位整型
jchar char 无符号16位整型
jshort short 有符号16位整型
jint int 32位整型
jlong long 64位整型
jfloat float 32位浮点型
jdouble double 64位浮点型
void void 无类型
引用数据类型
JNI类型 Java类型 描述
jobject Object Object类型
jclass Class Class类型
jstring String String类型
jobjectArray Object[] 对象数组
jbooleanArray boolean[] boolean数组
jbyteArray byte[] byte数组
jcharArray char[] char数组
jshortArray short[] short数组
jintArray int[] int数组
jlongArray long[] long数组
jfloatArray float[] float数组
jdoubleArray double[] double数组
jthrowable Throwable Throwable

JNI的类型签名

JNI的类型签名标识了一个特定的Java类型,这个类型既可以是类也可以是方法,也可以是数据类型。

  • 类的签名比较简单,它采用 L+包名+类型+; 的形式,只需要将其中的.替换为/即可。例如java.lang.String, 它的签名为Ljava/lang/String; ,注意末尾的;也是签名一部分。
  • 基本数据类型的签名采用一系列大写字母来表示, 如下表所示
基本数据类型的签名
Java类型 签名 Java类型 签名 Java类型 签名
boolean Z byte B char C
short S int I long J
float F double D void V

JNI C/C++函数编写

先来看看Android Studio为我们生成的示例

JNIEXPORT jstring JNICALL
Java_com_glee_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}复制代码
  • JNIEXPORT & JNICALL
    JNIEXPORT和JNICALL这两个宏(被定义在jni.h)确保这个函数在本地库外可见,并且编译器会进行正确的调用转换。
  • 函数规范
    在JNI中C/C++的函数名是有规范要求的,由以下几部分串接而成
    • Java_前缀
    • 完全限定的类名,并用下划线“_”作为分隔符
    • 第一参数JNIEnv* env
    • 第二个参数jobject或jclass
    • 其他参数按类型映射
    • 返回参数按类型映射

JNI层操作Bitmap对象

Android中JNI层处理Bitmap通常有两种方法

  • 获取到Bitmap中的byte数组并传入native方法,JNI层处理得到的byte数组后返回一个新的byte数组,Java层重建Bitmap对象。(不推荐)
  • Java层直接向JNI层传入Bitmap的引用,JNI层得到Bitmap对象的图像数据的地址,直接修改Bitmap的byte数组。

阅读了很多篇博客,很多开发者都会采用第一种方法,本人是极不推荐的。这种方法会在内存中重建一个byte数组,会造成内存的浪费,性能低下。
第二种方法是性能最优的,JNI层充分利用的C/C++指针的特性,直接获取到Bitmap中byte数组在内存中的地址,通过指针直接修改图像数据,所以用到了NDK中的android/bitmap.h。

android/bitmap.h

android/bitmap.h这个头文件用于在JNI层操作Bitmap对象的,其包含于jnigraphics库中,所以要在CMakeLists.txt中的target_link_libraries加入-ljnigraphics,如下

target_link_libraries(native-lib -ljnigraphics ${log-lib})复制代码

三个常用函数

  • AndroidBitmap_getInfo() 从位图句柄获得信息(宽度、高度、像素格式)
  • AndroidBitmap_lockPixels() 对像素缓存上锁,即获得该缓存的指针。
  • AndroidBitmap_unlockPixels() 解锁

JNI接口函数

JNIEXPORT void JNICALL
Java_com_glee_ndkroad1006_MainActivity_gaussBlur(JNIEnv *env, jobject /* this */, jobject bmp) {
    AndroidBitmapInfo info = {0};//初始化BitmapInfo结构体
    int *data=NULL;//初始化Bitmap图像数据指针
    AndroidBitmap_getInfo(env, bmp, &info);
    AndroidBitmap_lockPixels(env, bmp, (void **) &data);//锁定Bitmap,并且获得指针
    /**********高斯模糊算法作对int数组进行处理***********/
    //调用gaussBlur函数,把图像数据指针、图片长宽和模糊半径传入
    gaussBlur(data,info.width,info.height,80);
    /****************************************************/
    AndroidBitmap_unlockPixels(env,bmp);//解锁
}复制代码

这里用到的gaussBlur函数代码将在文章最后列出。
这里用到的gaussBlur函数代码将在文章最后列出。
这里用到的gaussBlur函数代码将在文章最后列出。

Java层代码

public class MainActivity extends AppCompatActivity {

    static {
        //通过静态代码块加载so库
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化两个ImageView
        ImageView iv1 = (ImageView) findViewById(R.id.img1);
        ImageView iv2 = (ImageView) findViewById(R.id.img2);
        //iv1设置图片
        iv1.setImageResource(R.drawable.test);
        //生成bitmap对象
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
        //调用native方法,传入Bitmap对象,对Bitmap进行高斯迷糊处理
        gaussBlur(bitmap);
        //把Bitmap对象设置给iv2
        iv2.setImageBitmap(bitmap);
    }
    //native方法声明
    public native void gaussBlur(Bitmap bitmap);
}复制代码

上方的ImageView是没有进行高斯模糊处理的,下方的ImageView调用了JNI方法进行高斯模糊处理。

高斯模糊算法

void gaussBlur1(int* pix, int w, int h, int radius)
{
    float sigma = (float) (1.0 * radius / 2.57);
    float deno  = (float) (1.0 / (sigma * sqrt(2.0 * PI)));
    float nume  = (float) (-1.0 / (2.0 * sigma * sigma));
    float* gaussMatrix = (float*)malloc(sizeof(float)* (radius + radius + 1));
    float gaussSum = 0.0;
    for (int i = 0, x = -radius; x <= radius; ++x, ++i)
    {
        float g = (float) (deno * exp(1.0 * nume * x * x));
        gaussMatrix[i] = g;
        gaussSum += g;
    }
    int len = radius + radius + 1;
    for (int i = 0; i < len; ++i)
        gaussMatrix[i] /= gaussSum;
    int* rowData  = (int*)malloc(w * sizeof(int));
    int* listData = (int*)malloc(h * sizeof(int));
    for (int y = 0; y < h; ++y)
    {
        memcpy(rowData, pix + y * w, sizeof(int) * w);
        for (int x = 0; x < w; ++x)
        {
            float r = 0, g = 0, b = 0;
            gaussSum = 0;
            for (int i = -radius; i <= radius; ++i)
            {
                int k = x + i;
                if (0 <= k && k <= w)
                {
                    //得到像素点的rgb值
                    int color = rowData[k];
                    int cr = (color & 0x00ff0000) >> 16;
                    int cg = (color & 0x0000ff00) >> 8;
                    int cb = (color & 0x000000ff);
                    r += cr * gaussMatrix[i + radius];
                    g += cg * gaussMatrix[i + radius];
                    b += cb * gaussMatrix[i + radius];
                    gaussSum += gaussMatrix[i + radius];
                }
            }
            int cr = (int)(r / gaussSum);
            int cg = (int)(g / gaussSum);
            int cb = (int)(b / gaussSum);
            pix[y * w + x] = cr << 16 | cg << 8 | cb | 0xff000000;
        }
    }
    for (int x = 0; x < w; ++x)
    {
        for (int y = 0; y < h; ++y)
            listData[y] = pix[y * w + x];
        for (int y = 0; y < h; ++y)
        {
            float r = 0, g = 0, b = 0;
            gaussSum = 0;
            for (int j = -radius; j <= radius; ++j)
            {
                int k = y + j;
                if (0 <= k && k <= h)
                {
                    int color = listData[k];
                    int cr = (color & 0x00ff0000) >> 16;
                    int cg = (color & 0x0000ff00) >> 8;
                    int cb = (color & 0x000000ff);
                    r += cr * gaussMatrix[j + radius];
                    g += cg * gaussMatrix[j + radius];
                    b += cb * gaussMatrix[j + radius];
                    gaussSum += gaussMatrix[j + radius];
                }
            }
            int cr = (int)(r / gaussSum);
            int cg = (int)(g / gaussSum);
            int cb = (int)(b / gaussSum);
            pix[y * w + x] = cr << 16 | cg << 8 | cb | 0xff000000;
        }
    }
    free(gaussMatrix);
    free(rowData);
    free(listData);
}复制代码

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK