[关闭]
@cxm-2016 2016-11-21T04:11:32.000000Z 字数 8776 阅读 4379

OpenGL-ES 3.0学习指南(三)——JNI操作Bitmap

OpenGL-ES

版本:1
作者:陈小默
声明:禁止商业,禁止转载

发布于作业部落简书


上一篇:OpenGL-ES 3.0学习指南(二)——Hello Java



五、使用JNI操作Bitmap

本章内容的安排为三个部分,第一部分主要介绍bitmap.h头文件中声明的含义。第二部分对基本操作进行面向对象的封装。第三部分通过一个将图像变为灰度的例子来比较Java与JNI在数据处理上的差别。

5.1 bitmap.h

bitmap.h头文件中的内容并不多,主要有这些部分组成:

5.1.1 响应结果定义

  1. #define ANDROID_BITMAP_RESULT_SUCCESS 0
  2. #define ANDROID_BITMAP_RESULT_BAD_PARAMETER -1
  3. #define ANDROID_BITMAP_RESULT_JNI_EXCEPTION -2
  4. #define ANDROID_BITMAP_RESULT_ALLOCATION_FAILED -3
  5. /* Backward compatibility: this macro used to be misspelled. */
  6. #define ANDROID_BITMAP_RESUT_SUCCESS ANDROID_BITMAP_RESULT_SUCCESS

这里定义了对Bitmap进行操作时的结果,分别对应成功错误的参数JNI异常内存分配错误,至于最后一个,这是个梗。Google工程师在定义NDK的时候写错一个单词,居然没有检查就发布了,然后就233333333了。看来IDE的拼写检查对自己人也有好处。

5.1.2 位图格式枚举

  1. enum AndroidBitmapFormat {
  2. ANDROID_BITMAP_FORMAT_NONE = 0,
  3. ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,
  4. ANDROID_BITMAP_FORMAT_RGB_565 = 4,
  5. ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,
  6. ANDROID_BITMAP_FORMAT_A_8 = 8,
  7. };

一般而言,常见的位图格式有RGB_565 、RGBA_8888、 ARGB_8888、 RGBA_4444、 ARGB_4444、 ALPHA_8 ,格式与每一位像素所占的长度和每一种颜色所占的位数相关,具体如下表所示

位图格式 像素长度(bit) 颜色分量(黑色代表透明度)
ALPHA_8 8 0000 0000
RGB_565 16 0000 0000 0000 0000
RGBA_4444 16 0000 0000 0000 0000
ARGB_4444 16 0000 0000 0000 0000
RGBA_8888 32 0000 0000 0000 0000
0000 0000 0000 0000
ARGB_8888 32 0000 0000 0000 0000
0000 0000 0000 0000

通过上表我们可以知道一个图片的鲜艳程度与其格式有关,选用ARGB_8888所能够表示的颜色要比RGB_565更多,但是却需要占用更多的内存空间。

5.1.3 位图信息结构体

  1. typedef struct {
  2. uint32_t width;
  3. uint32_t height;
  4. uint32_t stride;
  5. int32_t format;
  6. uint32_t flags; // 0 for now
  7. } AndroidBitmapInfo;

width表示图片的宽度(列数),height表示图片的高度(行数),stride为行跨度,具体含义后面会进行介绍。最后一个参数已经被弃用,其值始终为0。

5.1.4 位图操作函数声明

  1. int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap,
  2. AndroidBitmapInfo* info);
  3. int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
  4. int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);

5.2 封装Bitmap操作

通过上面的说明,我们已经基本了解了JNI中Bitmap的操作过程,现在我们需要将上述过程封装成类。在此之前,我们需要一个工具类其中包含输出LOG等基本功能,所以我们创建一个名为JniUtil.h的头文件:

  1. #ifndef NDK_JNIUTIL_H
  2. #define NDK_JNIUTIL_H
  3. #include <jni.h>
  4. #include <android/log.h>
  5. #include <cstdlib>
  6. using namespace std;
  7. #ifndef LOG_TAG
  8. #define LOG_TAG "NDK-LIB"
  9. #endif
  10. #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
  11. #define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
  12. #define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
  13. #define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
  14. #endif //NDK_JNIUTIL_H

现在我们来创建一个头文件Bitmap.h,并且在头文件中对这个类进行简单实现:

  1. #ifndef NDK_BITMAP_H
  2. #define NDK_BITMAP_H
  3. #include <android/bitmap.h>
  4. #include "JniUtil.h"
  5. typedef uint32_t ABsize;//Android Bitmap size
  6. typedef int32_t ABformat;//Android Bitmap format
  7. #ifdef ARGB_8888
  8. typedef uint32_t APixel;
  9. ABformat checkFormat = ANDROID_BITMAP_FORMAT_RGBA_8888;
  10. #elif defined(ARGB_4444)
  11. typedef uint16_t APixel;
  12. ABformat checkFormat = ANDROID_BITMAP_FORMAT_RGBA_4444;
  13. #elif defined(RGB_565)
  14. typedef uint16_t APixel;
  15. ABformat checkFormat = ANDROID_BITMAP_FORMAT_RGB_565;
  16. #elif defined(ALPHA_8)
  17. typedef uint8_t APixel;
  18. ABformat checkFormat = ANDROID_BITMAP_FORMAT_A_8;
  19. #else
  20. typedef uint32_t APixel;
  21. ABformat checkFormat = ANDROID_BITMAP_FORMAT_RGBA_8888;
  22. #endif
  23. class Bitmap {
  24. private:
  25. APixel *pixels;
  26. JNIEnv *jenv;
  27. _jobject *jbitmap;
  28. AndroidBitmapInfo info;
  29. int result;
  30. ABsize width;
  31. ABsize height;
  32. public:
  33. Bitmap(int width, int height) : jenv(NULL), jbitmap(NULL) {
  34. pixels = (APixel *) malloc(sizeof(APixel) * width * height);
  35. memset(pixels, 0, width * height);
  36. }
  37. Bitmap(JNIEnv *env, jobject bitmap) : pixels(NULL), jenv(env), jbitmap(bitmap) {
  38. if ((result = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {
  39. ALOGE("bitmap init failed ! error=%d", result);
  40. return;
  41. }
  42. if (info.format != checkFormat) {
  43. ALOGE("Bitmap format is not your selection !");
  44. return;
  45. }
  46. if ((result = AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels)) < 0) {
  47. ALOGE("bitmap get pixels failed ! error=%d", result);
  48. }
  49. }
  50. ~Bitmap() {
  51. if (jenv)
  52. AndroidBitmap_unlockPixels(jenv, jbitmap);
  53. else
  54. free(pixels);
  55. }
  56. ABsize getHeight() {
  57. return jenv ? info.height : height;
  58. }
  59. ABsize getWidth() {
  60. return jenv ? info.width : width;
  61. }
  62. ABformat getType() {
  63. return checkFormat;
  64. }
  65. int getErrorCode() {
  66. return result;
  67. }
  68. operator APixel *() {
  69. return pixels;
  70. }
  71. APixel *operator[](int y) {
  72. if (y >= getHeight()) return NULL;
  73. return pixels + y * getWidth();
  74. }
  75. };
  76. #endif //NDK_BITMAP_H

文件开始的一系列宏用来在编译时确定当前像素的类型。

  1. #ifdef ARGB_8888
  2. ...
  3. #endif

该类提供了两种构造器:

  1. Bitmap(int width, int height);
  2. Bitmap(JNIEnv *env, jobject bitmap);

第一种直接通过宽和高创建一个表示位图的数组,可以在本地使用,不能与JNI交互。第二种构造器使用JNIEnv和一个Java Bitmap位图对象来创建。

  1. APixel *operator[](int y) ;

这里重载了[]操作符,作用是可以将位图以二维方式操作。

5.3 将图片处理为灰度图片

将一张图片处理为灰度图片一般有三种形式,第一种是取RGB分量的最小值并分配给每一个颜色分量,但是这样会导致最终的效果图色调偏暗。第二种是第一种的反例,取RGB分量的最大值并分配给每一个颜色分量,这样又会导致最终效果图偏亮。综合以上两种方法,我们采取第三种方式,取RGB分量的平均值分配给各个分量。

5.3.1 使用Java实现灰度图

创建一个NBitmapLib的Java文件,在其中书写两个灰度变换的方法,第一个是native方法,我们使用JNI实现,第二个直接使用Java完成。

  1. public class NBitmapLib {
  2. public static native void renderGray(Bitmap bitmap);
  3. public static void javaRenderGray(Bitmap bitmap) {
  4. int MODEL = 0xFF;
  5. int height = bitmap.getHeight();
  6. int width = bitmap.getWidth();
  7. int pixelArray[] = new int[width * height];
  8. bitmap.getPixels(pixelArray, 0, width, 0, 0, width, height);
  9. int color;
  10. int av;
  11. for (int i = 0; i < pixelArray.length; i++) {
  12. color = pixelArray[i];
  13. av = 0;
  14. av += color & MODEL;
  15. av += (color >> 8) & MODEL;
  16. av += (color >> 16) & MODEL;
  17. av /= 3;
  18. color = 0xFF00 + av;
  19. color = (color << 8) + av;
  20. color = (color << 8) + av;
  21. pixelArray[i] = color;
  22. }
  23. bitmap.setPixels(pixelArray, 0, width, 0, 0, width, height);
  24. }
  25. }

在Java中一个像素采用Integer类型表示,类型为ARGB_8888。所以灰度变换的过程为取RGB分量的和,平均后分配到每一个分量上,这里采用位运算的方式实现。

public void getPixels(int[] pixels, int offset, int stride,int x, int y, int width, int height);

  • pixels:保存位图像素数据的数组。
  • offset:数组偏移量,也是行偏移量。
  • stride:幅度,实际每行保存数据数(不一定是显示数据)。
  • x:x轴起始位置。
  • y:y轴起始位置。
  • width:截取原图的宽度。
  • height:截取原图的高度。

举个例子:对于高为100,宽为100的一个位图bm,如果有一个数组其长度为[120列*30行]

  1. bm.getPixels(pixelArray, 0, 120, 0, 0, 30, 30);

这个例子表示,从原图中(0,0)位置,取30行,每行30列,存放到pixelArray的起始为0的位置,在pixelArray中每行的宽度为120(多出位图可显示的部分是保留段,用于存放一些数据)。

  1. bm.getPixels(pixelArray, 30, 120, 0, 0, 30, 30);

这个例子表示,从原图中(0,0)位置,取30行,每行数据保存到pixelArray中对应行偏移offset的位置(每行都有偏移),向后存放30个列数据。也就是pixelArray表示的二维矩阵的每一行的30-60之间。

  1. bm.getPixels(pixelArray, 120+30, 120, 0, 0, 30, 29);

这个例子跟上面的例子相比,区别就是offset是120+30,那么它与上面例子的差别就是会跳过第一行,然后从第二行开始,每行偏移30个位置来保存数据。

public void setPixels(int[] pixels, int offset, int stride,int x, int y, int width, int height);

参数含义与getPixels相同。

5.3.2 使用JNI实现灰度图

创建一个bitmap.cpp的文件,在其中完成如下:

  1. #define LOG_TAG "bitmap"
  2. #define ARGB_4444
  3. #include <android/bitmap.h>
  4. #include "Bitmap.h"
  5. extern "C"
  6. JNIEXPORT void JNICALL
  7. Java_com_github_cccxm_ndk_lib_NBitmapLib_renderGray(JNIEnv *env,
  8. jobject obj,
  9. jobject bitmap) {
  10. Bitmap bm(env, bitmap);
  11. ABsize height = bm.getHeight();
  12. ABsize width = bm.getWidth();
  13. const APixel MODEL = 0xF;
  14. APixel color;
  15. APixel av;
  16. APixel *pixelArray = bm;
  17. ABsize length = height * width;
  18. for (ABsize i = 0; i < length; i++) {
  19. av = 0;
  20. color = pixelArray[i];
  21. av += (color >>= 4) & MODEL;
  22. av += (color >>= 4) & MODEL;
  23. av += (color >> 4) & MODEL;
  24. av /= 3;
  25. color = av;
  26. color = (color << 4) + av;
  27. color = (color << 4) + av;
  28. pixelArray[i] = color << 4;
  29. }
  30. }
  31. #undef ARGB_4444

5.3.3 运行项目

在运行项目之前,我们要先创建一个NBitmapActivity,这个Activity的包含三个ImageView

  1. <ImageView
  2. android:layout_width="match_parent"
  3. android:layout_height="0dp"
  4. android:layout_weight="1"
  5. android:scaleType="centerCrop"
  6. android:src="@drawable/hy" />
  7. <ImageView
  8. android:id="@+id/native_to_gray"
  9. android:layout_width="match_parent"
  10. android:layout_height="0dp"
  11. android:layout_weight="1"
  12. android:scaleType="centerCrop" />
  13. <ImageView
  14. android:id="@+id/java_to_gray"
  15. android:layout_width="match_parent"
  16. android:layout_height="0dp"
  17. android:layout_weight="1"
  18. android:scaleType="centerCrop" />

第一个ImageView显示原图,后两个分别用来显示经JNI和Java处理过的灰度图,代码如下:

  1. class NBitmapActivity : AppCompatActivity() {
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. setContentView(R.layout.activity_nbitmap)
  5. val bitmap = BitmapFactory.decodeResource(resources, R.drawable.hy)
  6. val jni_bitmap = bitmap.copy(Bitmap.Config.ARGB_4444, true)
  7. var startTime = System.currentTimeMillis()
  8. NBitmapLib.renderGray(jni_bitmap)
  9. Log.e("---time----", "JNI Time:${System.currentTimeMillis() - startTime}")
  10. native_to_gray.setImageBitmap(jni_bitmap)
  11. val java_bitmap = bitmap.copy(Bitmap.Config.ARGB_4444, true)
  12. startTime = System.currentTimeMillis()
  13. NBitmapLib.javaRenderGray(java_bitmap)
  14. //kotlinRenderGray(java_bitmap)
  15. Log.e("---time----", "Java Time:${System.currentTimeMillis() - startTime}")
  16. java_to_gray.setImageBitmap(java_bitmap)
  17. }
  18. fun kotlinRenderGray(bitmap: Bitmap) {
  19. val MODEL = 0xFF
  20. val height = bitmap.height
  21. val width = bitmap.width
  22. val pixelArray = IntArray(width * height)
  23. bitmap.getPixels(pixelArray, 0, width, 0, 0, width, height)
  24. var color: Int
  25. var av: Int
  26. for (i in pixelArray.indices) {
  27. color = pixelArray[i]
  28. av = 0
  29. av += color and MODEL
  30. av += color shr 8 and MODEL
  31. av += color shr 16 and MODEL
  32. av /= 3
  33. color = 0xFF00 + av
  34. color = (color shl 8) + av
  35. color = (color shl 8) + av
  36. pixelArray[i] = color
  37. }
  38. bitmap.setPixels(pixelArray, 0, width, 0, 0, width, height)
  39. }
  40. }

5.3.3-最终效果图

在代码中我们对两种处理过程进行了计时,在红米2A上运行五次取平均值,使用JNI渲染的平均用时是 37.6 毫秒,使用Java渲染的平均用时是 101 毫秒,差距在2~3倍之间。在Activity中,使用了Kotlin语言实现了一遍,耗时与Java一致。


下一篇:OpenGL-ES 3.0学习指南(四)——NativeActivity


[1]CSDN博客:Bitmap 之 getPixels() 参数解析

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注