Android-JNI開發(fā)系列《九》實戰(zhàn)-Bitmap處理實現(xiàn)底片灰度化黑白化暖冷色調(diào)等效果

人間觀察

當你喜歡一個人的時候子姜,總是小心翼翼的酝润,笨笨的燎竖,傻傻的,生怕做錯了什么要销,又怕不做什么~

到此暴氏,Android中基本的JNI基礎知識以及常見的基本操作差不多就基本講完了茂蚓。我們來實踐一下畦徘,本文實現(xiàn)的是對Android Bitmap的處理: 對一張圖片進行處理,照片底片效果拨扶,黑白化,灰度化茁肠,左右翻轉患民,暖色,冷色垦梆,高斯模糊等等匹颤,市場上有很多這種處理圖片的app,就看誰的算法足夠厲害強大托猩。效果圖如下

效果圖

我們先貼一下我們對一張圖片處理后的各種效果圖印蓖。


原圖
底片效果
冷色調(diào)
暖色調(diào)
黑白化
灰度化
左右翻轉

在Android中JNI層操作bitmap的需要鏈接系統(tǒng)的動態(tài)庫nigraphics 圖像庫,怎么動態(tài)鏈接呢京腥? 就是通過上一篇文章的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的函數(shù)都定義在bitmap.h 的頭文件里,主要就三個函數(shù)公浪。AndroidBitmap_getInfo他宛,AndroidBitmap_lockPixelsAndroidBitmap_unlockPixels

返回值

3個方法的返回值都是如下情況欠气。成功是0厅各,失敗返回一個負數(shù)。

/** 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);

參數(shù)env JNI 接口指針

參數(shù)jbitmap Bitmap 對象的引用

參數(shù)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 就是每一行的字節(jié)數(shù)淋袖,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涝桅。

調(diào)用該方法后拜姿,會鎖定像素確保像素的內(nèi)存不會被移動,只有再次調(diào)用unlockPixels會再次釋放冯遂。 傳入addrPtr蕊肥,它會指向的圖片的那塊內(nèi)存。addrPtr的類型是void**,給了我們足夠的操作像素方式壁却,你可以隨意操作這塊內(nèi)存批狱。AndroidBitmap_lockPixels 同樣 執(zhí)行成功的話返回 0 ,否則返回一個負數(shù)展东,錯誤碼列表就是上面提到的赔硫。

特別注意
如果直接操作addrPtr指針所指向的內(nèi)容,相當于它會直接更改對應java層的bitmap對象盐肃。你如果不想這樣爪膊,可以直接在jni層中構造一個新的java層的bitmap對象然后返回,不影響原來的砸王。

解鎖像素緩存

Bitmap 調(diào)用完 AndroidBitmap_lockPixels 之后都應該對應調(diào)用一次 AndroidBitmap_unlockPixels 用來解鎖/釋放原生像素緩存推盛。

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

每個像素的ARGB的獲取,JAVA&JNI的轉換注意點

講這個前我們順帶提一下谦铃,Android 中 Bitmap 的占用內(nèi)存大小耘成,跟設備dpi和該圖片所放的資源目錄有關,與ImageView無關驹闰。比如一張像素為 300 * 300 的圖片放在xxdpi(480dpi)目錄中瘪菌,設備屏幕密度為 440 dpi,每個讀取參數(shù)為ARGB_8888(4個字節(jié))疮方。則內(nèi)存占用為:

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

如果是放在assets 目錄下的圖片則不壓縮計算控嗜。

在Android 中以 ARGB_8888 為例,A/R/G/B 各占 8 位骡显,各由兩個十六進制數(shù)表示疆栏,依次排列,比如常見的色值 #FF534F33惫谤,即各通道值為:透明度 alpha 0xFF壁顶,紅色 red 0x53,綠色 green 0x4F溜歪,藍色 blue 0x33若专。

如何才能從一個 int 值中獲取各個通道(RGB)的顏色呢?只有獲取了才能對RGB進行算法處理蝴猪。

還以 #FF234567 為例调衰,轉換為二進制為
1111 1111 | 0101 0011 | 0100 1111 | 0011 0011 (| 符號是方便劃分)

通過位運算,舍棄位數(shù)自阱,只有自己關心的即可嚎莉。比如將二進制右移 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檬输。這個很重要照瘾,網(wǎng)上的文章大部分都是錯誤的,在下面的代碼中我們也會驗證一下這個結論丧慈。

圖片底片效果

我們以實現(xiàn)圖片的底片效果為例析命,其它效果都一樣,都是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;
}

上面對像素的處理我們是按照二維數(shù)組的方式進行處理的,拿到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作比較吠冤,這是一個經(jīng)驗值可以根據(jù)效果來調(diào)整浑彰。

    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層中創(chuàng)建一個java層的bitmap對象newbitmap涯保,將處理好的數(shù)據(jù)保存到一個數(shù)組中诉濒。通過AndroidBitmap_lockPixels獲取一個指向像素內(nèi)存的指針,然后把處理完后的數(shù)據(jù)memcpy到該內(nèi)存即可夕春。部分代碼為:

// 省略對原始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層創(chuàng)建bitmap的代碼如下未荒,這個就是之前文章所講的,如何在jni中創(chuàng)建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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市速侈,隨后出現(xiàn)的幾起案子划纽,更是在濱河造成了極大的恐慌,老刑警劉巖锌畸,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勇劣,死亡現(xiàn)場離奇詭異,居然都是意外死亡潭枣,警方通過查閱死者的電腦和手機比默,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盆犁,“玉大人命咐,你說我怎么就攤上這事⌒乘辏” “怎么了醋奠?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵榛臼,是天一觀的道長。 經(jīng)常有香客問我窜司,道長沛善,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任塞祈,我火速辦了婚禮金刁,結果婚禮上,老公的妹妹穿的比我還像新娘议薪。我一直安慰自己尤蛮,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布斯议。 她就那樣靜靜地躺著产捞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哼御。 梳的紋絲不亂的頭發(fā)上轧葛,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音艇搀,去河邊找鬼尿扯。 笑死,一個胖子當著我的面吹牛焰雕,可吹牛的內(nèi)容都是我干的衷笋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼矩屁,長吁一口氣:“原來是場噩夢啊……” “哼辟宗!你這毒婦竟也來了?” 一聲冷哼從身側響起吝秕,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤泊脐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后烁峭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體容客,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年约郁,在試婚紗的時候發(fā)現(xiàn)自己被綠了缩挑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡鬓梅,死狀恐怖供置,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绽快,我是刑警寧澤芥丧,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布紧阔,位于F島的核電站,受9級特大地震影響续担,放射性物質(zhì)發(fā)生泄漏擅耽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一赤拒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诱鞠,春花似錦挎挖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至阳掐,卻和暖如春始衅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缭保。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工汛闸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人艺骂。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓诸老,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钳恕。 傳聞我的和親對象是個殘疾皇子别伏,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354