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

Posted 顾修忠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android-JNI开发系列《九》实战-Bitmap处理实现底片灰度化黑白化暖冷色调等效果相关的知识,希望对你有一定的参考价值。

人间观察

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

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

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

target_link_libraries( # Specifies the target library.
        native-lib
        jnigraphics #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处理实现底片灰度化黑白化暖冷色调等效果的主要内容,如果未能解决你的问题,请参考以下文章

Android-JNI开发系列《十二》总结JNI知识体系

Android-JNI开发系列《十》实践利用libjpeg-turbo完美压缩图片不失真

Android-JNI开发系列《十一》实践-利用Android C源码实现GIF图片的播放

12.PGL图学习之项目实践(UniMP算法实现论文节点分类新冠疫苗项目实战,助力疫情)[系列九]

明晚九点|Flask 基础与 Web 开发实战

今晚九点|Flask 基础与 Web 开发实战