人間觀察
當你喜歡一個人的時候子姜,總是小心翼翼的酝润,笨笨的燎竖,傻傻的,生怕做錯了什么要销,又怕不做什么~
到此暴氏,Android中基本的JNI基礎知識以及常見的基本操作差不多就基本講完了茂蚓。我們來實踐一下畦徘,本文實現(xiàn)的是對Android Bitmap的處理: 對一張圖片進行處理,照片底片效果拨扶,黑白化,灰度化茁肠,左右翻轉患民,暖色,冷色垦梆,高斯模糊等等匹颤,市場上有很多這種處理圖片的app,就看誰的算法足夠厲害強大托猩。效果圖如下
效果圖
我們先貼一下我們對一張圖片處理后的各種效果圖印蓖。
在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_lockPixels
,AndroidBitmap_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;
}
到此結束片排。