人间观察

当你喜欢一个人的时候,总是小心翼翼的,笨笨的,傻傻的,生怕做错了什么,又怕不做什么~

到此,Android中基本的JNI基础知识以及常见的基本操作差不多就基本讲完了。我们来实践一下,本文实现的是对Android Bitmap的处理: 对一张图片进行处理,照片底片效果,黑白化,灰度化,左右翻转,暖色,冷色,高斯模糊等等,市场上有很多这种处理图片的app,就看谁的算法足够厉害强大。效果图如下

在Android中JNI层操作bitmap的需要链接系统的动态库nigraphics 图像库,怎么动态链接呢? 就是通过上一篇文章的CMake中的方法target_link_libraries来链接,即:

target_link_libraries( # Specifies the target library.native-libjnigraphics #JNI层,添加bitmap支持# Links the target library to the log library# included in the NDK.${log-lib})

在JNI层操作bitmap的函数都定义在bitmap.h 的头文件里,主要就三个函数。AndroidBitmap_getInfoAndroidBitmap_lockPixelsAndroidBitmap_unlockPixels

####效果图
我们先贴一下我们对一张图片处理后的各种效果图。

返回值

3个方法的返回值都是如下情况。成功是0,失败返回一个负数。

/** AndroidBitmap functions result code. */
enum {/** Operation was successful. */ANDROID_BITMAP_RESULT_SUCCESS           = 0,/** Bad parameter. */ANDROID_BITMAP_RESULT_BAD_PARAMETER     = -1,/** JNI exception occured. */ANDROID_BITMAP_RESULT_JNI_EXCEPTION     = -2,/** Allocation failed. */ANDROID_BITMAP_RESULT_ALLOCATION_FAILED = -3,
};

获取bitmap的信息

通过AndroidBitmap_getInfo可以获取图片的基本信息,比如宽高,图像的格式

/*** Given a java bitmap object, fill out the AndroidBitmapInfo struct for it.* If the call fails, the info parameter will be ignored.*/
int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,AndroidBitmapInfo* info);

参数env JNI 接口指针

参数jbitmap Bitmap 对象的引用

参数info AndroidBitmapInfo 结构体的指针

返回值 0 成功

传入AndroidBitmapInfo结构体的指针,即可获取图片的信息,结构体针织如下

/** Bitmap info, see AndroidBitmap_getInfo(). */
typedef struct {/** The bitmap width in pixels. */uint32_t    width;/** The bitmap height in pixels. */uint32_t    height;/** The number of byte per row. */uint32_t    stride;/** The bitmap pixel format. See {@link AndroidBitmapFormat} */int32_t     format;/** Unused. */uint32_t    flags;      // 0 for now
} AndroidBitmapInfo;

width 就是图片的宽,height就是图片的高,stride 就是每一行的字节数,format是图像的格式。格式有如下:

/** Bitmap pixel format. */
enum AndroidBitmapFormat {/** No format. */ANDROID_BITMAP_FORMAT_NONE      = 0,/** Red: 8 bits, Green: 8 bits, Blue: 8 bits, Alpha: 8 bits. **/ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,/** Red: 5 bits, Green: 6 bits, Blue: 5 bits. **/ANDROID_BITMAP_FORMAT_RGB_565   = 4,/** Deprecated in API level 13. Because of the poor quality of this configuration, it is advised to use ARGB_8888 instead. **/ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,/** Alpha: 8 bits. */ANDROID_BITMAP_FORMAT_A_8       = 8,
};

这个格式熟悉吧和Android中bitmap一样。

获取bitmap的每个像素信息

/*** Given a java bitmap object, attempt to lock the pixel address.* Locking will ensure that the memory for the pixels will not move* until the unlockPixels call, and ensure that, if the pixels had been* previously purged, they will have been restored.** If this call succeeds, it must be balanced by a call to* AndroidBitmap_unlockPixels, after which time the address of the pixels should* no longer be used.** If this succeeds, *addrPtr will be set to the pixel address. If the call* fails, addrPtr will be ignored.*/
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);

这个方法是我们最重要的一个方法,拿到图片的每个像素之后就可以对每个像素值进行操作,从而更改 Bitmap。

调用该方法后,会锁定像素确保像素的内存不会被移动,只有再次调用unlockPixels会再次释放。 传入addrPtr,它会指向的图片的那块内存。addrPtr的类型是void**,给了我们足够的操作像素方式,你可以随意操作这块内存。AndroidBitmap_lockPixels 同样 执行成功的话返回 0 ,否则返回一个负数,错误码列表就是上面提到的。

特别注意
如果直接操作addrPtr指针所指向的内容,相当于它会直接更改对应java层的bitmap对象。你如果不想这样,可以直接在jni层中构造一个新的java层的bitmap对象然后返回,不影响原来的。

解锁像素缓存

Bitmap 调用完 AndroidBitmap_lockPixels 之后都应该对应调用一次 AndroidBitmap_unlockPixels 用来解锁/释放原生像素缓存。

/*** Call this to balance a successful call to AndroidBitmap_lockPixels.*/
int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);

每个像素的ARGB的获取,JAVA&JNI的转换注意点

讲这个前我们顺带提一下,Android 中 Bitmap 的占用内存大小,跟设备dpi和该图片所放的资源目录有关,与ImageView无关。比如一张像素为 300 * 300 的图片放在xxdpi(480dpi)目录中,设备屏幕密度为 440 dpi,每个读取参数为ARGB_8888(4个字节)。则内存占用为:

(440 / 480 * 300 )  * (440 / 480 * 300 )*4=302500(byte)

如果是放在assets 目录下的图片则不压缩计算。

在Android 中以 ARGB_8888 为例,A/R/G/B 各占 8 位,各由两个十六进制数表示,依次排列,比如常见的色值 #FF534F33,即各通道值为:透明度 alpha 0xFF,红色 red 0x53,绿色 green 0x4F,蓝色 blue 0x33。

如何才能从一个 int 值中获取各个通道(RGB)的颜色呢?只有获取了才能对RGB进行算法处理。

还以 #FF234567 为例,转换为二进制为
1111 1111 | 0101 0011 | 0100 1111 | 0011 0011 (| 符号是方便划分)

通过位运算,舍弃位数,只有自己关心的即可。比如将二进制右移 24位得到1111 1111 ,然后 & 0xFF得到alpha。即int alpha = (color >> 24) & 0xFF。

再比如得到红色二进制右移 16位得0101 0011 然后 & 0xFF得到red,即 int red=(color >> 16) & 0xFF。

0xFF的二进制的低8位是1111 1111前面24为都是0.

但是在jni的C层中不是的,在C层中,Bitmap像素点的值是ABGR,而不是ARGB,也就是说,B和R交换了,高端到低端:A,B,G,R。这个很重要,网上的文章大部分都是错误的,在下面的代码中我们也会验证一下这个结论。

图片底片效果

我们以实现图片的底片效果为例,其它效果都一样,都是AndroidBitmap_lockPixels后操作每个像素,只是对像素操作的算法不一样。

底片的算法原理:将当前像素点的RGB值分别与255之差后的值作为当前点的RGB值,即 R = 255 – R;G = 255 – G;B = 255 – B;

int BitmapUtil::negative(JNIEnv *env, jobject bitmap) {AndroidBitmapInfo bitmapInfo;// 获取bitmap的属性信息int ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);if (ret != ANDROID_BITMAP_RESULT_SUCCESS) {LOG_D("AndroidBitmap_getInfo %d", ret);return JNI_FALSE;}void *bitmapPixels;int pixRet = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels);if (pixRet != ANDROID_BITMAP_RESULT_SUCCESS) {LOG_D("AndroidBitmap_lockPixels %d", pixRet);return JNI_FALSE;}int w = bitmapInfo.width;int h = bitmapInfo.height;uint32_t *srcPix = (uint32_t *) bitmapPixels;// 在C层中,Bitmap像素点的值是ABGR,而不是ARGB,也就是说,高端到低端:A,B,G,R// 底片效果算法原理:将当前像素点的RGB值分别与255之差后的值作为当前点的RGB值,即// R = 255 – R;G = 255 – G;B = 255 – B;for (int i = 0; i < h; ++i) {for (int j = 0; j < w; ++j) {uint32_t color = srcPix[w * i + j];uint32_t blue = (color >> 16) & 0xFF;uint32_t green = (color >> 8) & 0xFF;uint32_t red = color & 0xFF;uint32_t alpha = (color >> 24) & 0xFF;if (i == 0 && j == 0) {LOG_D("jni color %d=%x", color, color);LOG_D("jni red %d=%x", red, red);LOG_D("jni green %d=%x", green, green);LOG_D("jni blue %d=%x", blue, blue);LOG_D("jni alpha %d=%x", alpha, alpha);}red = 255 - red;green = 255 - green;blue = 255 - blue;uint32_t newColor =(alpha << 24) | ((blue << 16)) | ((green << 8)) | red;if (i == 0 & j == 0) {LOG_D("newColor %d=%x", newColor, newColor);}srcPix[w * i + j] = newColor;}}AndroidBitmap_unlockPixels(env, bitmap);return JNI_TRUE;
}

上面对像素的处理我们是按照二维数组的方式进行处理的,拿到abgr每个像素的值后进行处理后,然后再把每个abgr的值通过位移放到int的各自位上去即可。最后别忘了AndroidBitmap_unlockPixels来释放解锁缓存。

我们测试一下并验证刚才的结论,在C层中Bitmap像素点的值是ABGR,我们在java和jni中各取第一行第一列的像素值并打印观察。

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.normal);int color = bitmap.getPixel(0, 0);Log.e(TAG, "java getPixel[0][0] " + color + "=" + Integer.toHexString(color));JNIBitmap jniBitmap = new JNIBitmap();long start = System.currentTimeMillis();if (jniBitmap.negative(bitmap) == 1) {Log.e(TAG, "negative cost:" + (System.currentTimeMillis() - start));imageView.setImageBitmap(bitmap);int color2 = bitmap.getPixel(0, 0);Log.e(TAG, "java getPixel[0][0] " + color2 + "=" + Integer.toHexString(color2));}

日志打印:
可以看到java层的像素传到jni层确实是B和R交换了吧。

2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp E/JNI: java getPixel[0][0] -5000782=ffb3b1b2
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni color -5066317=ffb2b1b3
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni red 179=b3
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni green 177=b1
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni blue 178=b2
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni alpha 255=ff
2020-11-07 17:30:48.767 16236-16236/com.bj.gxz.jniapp D/JNI: jni newColor -11710900=ff4d4e4c
2020-11-07 17:30:48.853 16236-16236/com.bj.gxz.jniapp E/JNI: negative cost:86
2020-11-07 17:30:48.854 16236-16236/com.bj.gxz.jniapp E/JNI: java getPixel[0][0] -11776435=ff4c4e4d

其它底片效果

比如黑白色的算法原理:

求RGB平均值Avg = (R + G + B) / 3,如果Avg >= 100,则新的颜色值为R=G=B=255;如果Avg < 100,则新的颜色值为R=G=B=0;255就是白色,0就是黑色;至于为什么用100作比较,这是一个经验值可以根据效果来调整。

    for (int i = 0; i < h; ++i) {for (int j = 0; j < w; ++j) {uint32_t color = srcPix[w * i + j];uint32_t blue = (color >> 16) & 0xFF;uint32_t green = (color >> 8) & 0xFF;uint32_t red = color & 0xFF;uint32_t alpha = (color >> 24) & 0xFF;uint32_t gray = (int) (red * 0.3f + green * 0.59f + blue * 0.11f);gray = gray >= 100 ? 255 : 0;uint32_t newColor = (alpha << 24) | (gray << 16) | (gray << 8) | gray;srcPix[w * i + j] = newColor;}}

其它具体参考文章末尾的源代码。

返回新的bitmap不影响原始的bitmap

上面的操作都是基于原始的bitmap处理的,在jni侧改完后的bitmap随之对应的java侧的bitmap对象的像素也会改变,在有些情况下我们希望不想改变原来的。那这样就需要我们在jni层中创建一个java层的bitmap对象newbitmap,将处理好的数据保存到一个数组中。通过AndroidBitmap_lockPixels获取一个指向像素内存的指针,然后把处理完后的数据memcpy到该内存即可。部分代码为:

// 省略对原始bitmap处理的过程...
// resultBitmapPixels 为处理后的jobject newBitmap = createBitmap(env, w, h);
void *resultBitmapPixels;pixRet = AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels);if (pixRet != ANDROID_BITMAP_RESULT_SUCCESS) {LOG_D("AndroidBitmap_lockPixels %d", pixRet);return nullptr;}memcpy(resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * w * h);delete[]  newBitmapPixels;AndroidBitmap_unlockPixels(env, newBitmap);

jni层创建bitmap的代码如下,这个就是之前文章所讲的,如何在jni中创建java对象。

jobject createBitmap(JNIEnv *env, uint32_t w, uint32_t h) {jclass clsBp = env->FindClass("android/graphics/Bitmap");jmethodID createBitmapMid = env->GetStaticMethodID(clsBp, "createBitmap","(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");if (createBitmapMid == nullptr) {LOG_E("createBitmapMid nullptr");return nullptr;}jclass clsConfig = env->FindClass("android/graphics/Bitmap$Config");if (clsConfig == nullptr) {LOG_E("clsConfig nullptr");return nullptr;}jmethodID valueOfMid = env->GetStaticMethodID(clsConfig, "valueOf","(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");if (valueOfMid == nullptr) {LOG_E("valueOfMid nullptr");return nullptr;}jstring configName = env->NewStringUTF("ARGB_8888");jobject bitmapConfig = env->CallStaticObjectMethod(clsConfig, valueOfMid, configName);jobject newBitmap = env->CallStaticObjectMethod(clsBp, createBitmapMid, w, h, bitmapConfig);return newBitmap;
}

到此结束。

最后源代码:https://github.com/ta893115871/JNIAPP

Android-JNI开发系列《九》实战-Bitmap处理实现底片灰度化黑白化暖冷色调等效果相关推荐

  1. Android JNI开发入门与实战

    简介: 涉及到一些算法或者底层驱动的时候,往往需要使用jni来开发.现在官方推荐使用CMake工具来开发jni. 使用CMake开发Jni其实挺简单的,要求不高的话只需要简单配置一下就可以使用了. 配 ...

  2. android jni 调用java_Android JNI开发系列(九)JNI调用Java的静态方法实例方法

    JNI调用Java的静态方法&实例方法 package org.professor.jni.bean; import android.util.Log; /** * Created by pe ...

  3. android 字符串函数,Android JNI开发系列(六)字符串操作

    JNI字符串操作 字符串是引用数据类型,不属于基本数据类型 Java 使用unicode编码,C使用UTF-8,所以在操作中 C语言的字符串操作在头文件中 示例代码 public native Str ...

  4. Android JNI开发系列(二)HelloWorld

    2019独角兽企业重金招聘Python工程师标准>>> 入门HelloWorld 新建项目 Configure your new project部分选中 Include C++ Su ...

  5. Android商城开发系列

    Android商城开发系列(一)--开篇 Android商城开发系列(二)--App启动欢迎页面制作 Android商城开发系列(三)--使用Fragment+RadioButton实现商城底部导航栏 ...

  6. Android Camera开发系列(下)——自定义Camera实现拍照查看图片等功能

    Android Camera开发系列(下)--自定义Camera实现拍照查看图片等功能 Android Camera开发系列(上)--Camera的基本调用与实现拍照功能以及获取拍照图片加载大图片 上 ...

  7. Android 快速开发系列 打造万能的ListView GridView 适配器

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38902805 ,本文出自[张鸿洋的博客] 1.概述 相信做Android开发的写 ...

  8. Android JNI开发入门之二

    在上一篇文章<Android JNI开发入门之一>中,我介绍了Android应用程序(APK)怎样通过JNI调用Native C实现的共享库.本文将进一步介绍Android应用程序通过JN ...

  9. android开发入门与实践_我的新书《Android App开发入门与实战》已经出版

    前言 工作之余喜欢在CSDN平台上写一些技术文章,算下时间也有两三年了.写文章的目的一方面是自己对技术的总结,另一方面也是将平时遇到的问题和解决方案与大家分享,还有就是在这个平台上能和大家共同交流. ...

最新文章

  1. 如何提高阿里云上应用的可用性(一)
  2. Java JIT在运行JDK代码时是否作弊?
  3. Linux学习-漫游根目录和/usr目录
  4. php post请求跳转,学习猿地-php如何实现post跳转
  5. LeetCode MySQL 1501. 可以放心投资的国家
  6. 中科院遗传所钱文峰组发表新冠病毒源于自然界的科学证据
  7. CCF-CSP认证历年真题详解
  8. linux如何查看实时优先级,Linux进程优先级系统——设置实时进程优先级
  9. 如何优雅地进行接口管理?(大厂内部分享)
  10. 怎么用计算机拨号手机,教你如何用电脑连接手机自动打电话
  11. 利用java程序实现文件加密
  12. 04.数据分析之pillow与matplotlib
  13. CodeLite 16.0可以编译通过,但是在编辑器界面会显示找不到标准库头文件
  14. centos7 查看内存使用
  15. K60的FTM的PWM、输入捕获、正交解码
  16. 微信活码应用 淘宝购物好评返现卡
  17. NLP教程笔记:BERT 双向语言模型
  18. 高德地图第三方工具网站
  19. PPT文件无法打开处理
  20. C语言自学路之将小写数字完美转换成大写数字

热门文章

  1. intelj maven 指定编译器版本
  2. mybatis中的#{}与${}在原理上的区别
  3. 关于Win32 DialogBox的一些收获
  4. 「docker实战篇」python的docker爬虫技术-在linux下mitmproxy介绍和安装(四)
  5. 好程序员分享Web前端知识之HTML
  6. React组件: 提取图片颜色
  7. 存数字,储未来——新华三2018存储瞄准闪存、海量、AI与超融合
  8. (转)WebApi发送HTML表单数据:文件上传与多部分MIME
  9. 基于FPGA的跨时钟域信号处理——专用握手信号
  10. MathType几个常用字体的名称