Android NDK之旅——圖片高斯模糊

李季_發表於2017-10-07
  • 閱讀本文可能花費的時間
    15分鐘
  • 本文可能瞭解到的知識
    1. CMake基本使用
    2. Android NDK開發/使用
    3. JNI層操作Java物件

  • 實現效果
    Android使用C/C++實現圖片的毛玻璃效果。

  • 注:
    1. 本文研究物件為Android JNI/NDK開發,非圖片演算法,故不對毛玻璃演算法做闡述。
    2. 本人能力有限,如有不妥請指出。

前言

十一假期幾天的思考,確立了自己的進階方向,打算了解下計算機視覺方面的技術,也就是opencv。在Android中整合opencv的話必然要掌握JNI/NDK的開發,所以寫了本文,一是向大家分享自己的學習經驗,二是鞏固自己的JNI/NDK開發和拋棄已久的C/C++方面的知識。

CMake

CMake是一款專案構建工具,通過編寫簡單明瞭的在CmakeLists.txt來生成makefile,簡單來說就是一個makefile生成器。

在Android Studio中安裝CMake非常簡單,開啟Tools->Android->SDK Manager,選擇SDK Tools標籤頁,勾選CMake、LLDB、NDK,OK自動安裝即可。其中LLDB可以使我們在Android Studio中除錯C/C++程式。NDK為原生開發工具包,必不可少。

為什麼要做JNI/NDK開發

眾所周知,Java/Android程式是執行在JVM/Dalvik VM中,所以Java程式遠沒有C/C++程式效能高,尤其是在CPU密集型運算時,所以Java平臺提供了JNI(Java Native Interface),可通過JNI呼叫C/C++等編寫的so動態連結庫。
注:Google在Android L以後用ART徹底代替了Dalvik VM,但ART本質上仍是一個虛擬機器,並支援所有Dalvik VM指令集。
Java API中幾乎所有與硬體相關的方法都是native的,比如I/O操作、網路訪問、手機感測器、串列埠讀寫等。
本文涉及的圖片處理是一種CPU密集型任務,在Android開發中使用native方法最為合適。

如何使用CMake做JNI/NDK開發

1 新建工程


選中Include C++ Support,意為引入C++支援。

2 配置C++支援


在Customize C++ Support介面預設即可,意為CMake/C++11環境

3 認識CMakeLists.txt

工程建立完畢之後Android Studio會在app目錄下生成CMakeLists.txt檔案。CMakeLists.txt是CMake的配置檔案,用於表明版本、依賴、等資訊,以下為Android Studio生成的CMakeLists(過濾註釋)

cmake_minimum_required(VERSION 3.4.1)

add_library(native-lib SHARED src/main/cpp/native-lib.cpp)

find_library(log-lib log)

target_link_libraries(native-lib ${log-lib})複製程式碼
  • cmake_minimum_required(VERSION 3.4.1)
    CMake最小版本使用的是3.4.1。
  • add_library()
    配置so庫資訊(為當前當前指令碼檔案新增庫)
    • native-lib
      這個是宣告引用so庫的名稱,在專案中,如果需要使用這個so檔案,引用的名稱就是這個。值得注意的是,實際上生成的so檔名稱是libnative-lib。
    • SHARED
      這個參數列示共享so庫檔案,也就是在Run專案或者build專案時會在目錄intermediates\transforms\mergeJniLibs\debug\folders\2000\1f\main下生成so檔案。
    • src/main/cpp/native-lib.cpp
      構建so庫的原始檔。
  • find_library()
    查詢一個庫檔案
    • log-lib
      這個指定的是在NDK庫中每個型別的庫會存放一個特定的位置,而log庫存放在log-lib中
    • log
      指定使用log庫
  • target_link_libraries()
    如果你本地的庫(native-lib)想要呼叫log庫的方法,那麼就需要配置這個屬性,意思是把NDK庫關聯到本地庫。
    • native-lib
      要被關聯的庫名稱
    • ${log-lib}
      要關聯的庫名稱,要用大括號包裹,前面還要有$符號去引用。

4 瞭解JNI的C/C++規範

資料型別

JNI的資料型別包含兩種,分別是基本型別和引用型別,它們和Java中的資料型別對應關係如下兩表所示。

基本資料型別
JNI型別 Java型別 描述
jboolean boolean 無符號8位整型
jbyte byte 無符號8位整型
jchar char 無符號16位整型
jshort short 有符號16位整型
jint int 32位整型
jlong long 64位整型
jfloat float 32位浮點型
jdouble double 64位浮點型
void void 無型別
引用資料型別
JNI型別 Java型別 描述
jobject Object Object型別
jclass Class Class型別
jstring String String型別
jobjectArray Object[] 物件陣列
jbooleanArray boolean[] boolean陣列
jbyteArray byte[] byte陣列
jcharArray char[] char陣列
jshortArray short[] short陣列
jintArray int[] int陣列
jlongArray long[] long陣列
jfloatArray float[] float陣列
jdoubleArray double[] double陣列
jthrowable Throwable Throwable

JNI的型別簽名

JNI的型別簽名標識了一個特定的Java型別,這個型別既可以是類也可以是方法,也可以是資料型別。

  • 類的簽名比較簡單,它採用 L+包名+型別+; 的形式,只需要將其中的.替換為/即可。例如java.lang.String, 它的簽名為Ljava/lang/String; ,注意末尾的;也是簽名一部分。
  • 基本資料型別的簽名採用一系列大寫字母來表示, 如下表所示
基本資料型別的簽名
Java型別 簽名 Java型別 簽名 Java型別 簽名
boolean Z byte B char C
short S int I long J
float F double D void V

JNI C/C++函式編寫

先來看看Android Studio為我們生成的示例

JNIEXPORT jstring JNICALL
Java_com_glee_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}複製程式碼
  • JNIEXPORT & JNICALL
    JNIEXPORT和JNICALL這兩個巨集(被定義在jni.h)確保這個函式在本地庫外可見,並且編譯器會進行正確的呼叫轉換。
  • 函式規範
    在JNI中C/C++的函式名是有規範要求的,由以下幾部分串接而成
    • Java_字首
    • 完全限定的類名,並用下劃線“_”作為分隔符
    • 第一引數JNIEnv* env
    • 第二個引數jobject或jclass
    • 其他引數按型別對映
    • 返回引數按型別對映

JNI層操作Bitmap物件

原理

Android中JNI層處理Bitmap通常有兩種方法

  • 獲取到Bitmap中的byte陣列並傳入native方法,JNI層處理得到的byte陣列後返回一個新的byte陣列,Java層重建Bitmap物件。(不推薦)
  • Java層直接向JNI層傳入Bitmap的引用,JNI層得到Bitmap物件的影像資料的地址,直接修改Bitmap的byte陣列。

閱讀了很多篇部落格,很多開發者都會採用第一種方法,本人是極不推薦的。這種方法會在記憶體中重建一個byte陣列,會造成記憶體的浪費,效能低下。
第二種方法是效能最優的,JNI層充分利用的C/C++指標的特性,直接獲取到Bitmap中byte陣列在記憶體中的地址,通過指標直接修改影像資料,所以用到了NDK中的android/bitmap.h。

android/bitmap.h

android/bitmap.h這個標頭檔案用於在JNI層操作Bitmap物件的,其包含於jnigraphics庫中,所以要在CMakeLists.txt中的target_link_libraries加入-ljnigraphics,如下

target_link_libraries(native-lib -ljnigraphics ${log-lib})複製程式碼

三個常用函式

  • AndroidBitmap_getInfo() 從點陣圖控制程式碼獲得資訊(寬度、高度、畫素格式)
  • AndroidBitmap_lockPixels() 對畫素快取上鎖,即獲得該快取的指標。
  • AndroidBitmap_unlockPixels() 解鎖

JNI介面函式

請看註釋

JNIEXPORT void JNICALL
Java_com_glee_ndkroad1006_MainActivity_gaussBlur(JNIEnv *env, jobject /* this */, jobject bmp) {
    AndroidBitmapInfo info = {0};//初始化BitmapInfo結構體
    int *data=NULL;//初始化Bitmap影像資料指標
    AndroidBitmap_getInfo(env, bmp, &info);
    AndroidBitmap_lockPixels(env, bmp, (void **) &data);//鎖定Bitmap,並且獲得指標
    /**********高斯模糊演算法作對int陣列進行處理***********/
    //呼叫gaussBlur函式,把影像資料指標、圖片長寬和模糊半徑傳入
    gaussBlur(data,info.width,info.height,80);
    /****************************************************/
    AndroidBitmap_unlockPixels(env,bmp);//解鎖
}複製程式碼

這裡用到的gaussBlur函式程式碼將在文章最後列出。
這裡用到的gaussBlur函式程式碼將在文章最後列出。
這裡用到的gaussBlur函式程式碼將在文章最後列出。

Java層程式碼

請看註釋

public class MainActivity extends AppCompatActivity {

    static {
        //通過靜態程式碼塊載入so庫
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化兩個ImageView
        ImageView iv1 = (ImageView) findViewById(R.id.img1);
        ImageView iv2 = (ImageView) findViewById(R.id.img2);
        //iv1設定圖片
        iv1.setImageResource(R.drawable.test);
        //生成bitmap物件
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
        //呼叫native方法,傳入Bitmap物件,對Bitmap進行高斯迷糊處理
        gaussBlur(bitmap);
        //把Bitmap物件設定給iv2
        iv2.setImageBitmap(bitmap);
    }
    //native方法宣告
    public native void gaussBlur(Bitmap bitmap);
}複製程式碼

執行效果

上方的ImageView是沒有進行高斯模糊處理的,下方的ImageView呼叫了JNI方法進行高斯模糊處理。

高斯模糊演算法

void gaussBlur1(int* pix, int w, int h, int radius)
{
    float sigma = (float) (1.0 * radius / 2.57);
    float deno  = (float) (1.0 / (sigma * sqrt(2.0 * PI)));
    float nume  = (float) (-1.0 / (2.0 * sigma * sigma));
    float* gaussMatrix = (float*)malloc(sizeof(float)* (radius + radius + 1));
    float gaussSum = 0.0;
    for (int i = 0, x = -radius; x <= radius; ++x, ++i)
    {
        float g = (float) (deno * exp(1.0 * nume * x * x));
        gaussMatrix[i] = g;
        gaussSum += g;
    }
    int len = radius + radius + 1;
    for (int i = 0; i < len; ++i)
        gaussMatrix[i] /= gaussSum;
    int* rowData  = (int*)malloc(w * sizeof(int));
    int* listData = (int*)malloc(h * sizeof(int));
    for (int y = 0; y < h; ++y)
    {
        memcpy(rowData, pix + y * w, sizeof(int) * w);
        for (int x = 0; x < w; ++x)
        {
            float r = 0, g = 0, b = 0;
            gaussSum = 0;
            for (int i = -radius; i <= radius; ++i)
            {
                int k = x + i;
                if (0 <= k && k <= w)
                {
                    //得到畫素點的rgb值
                    int color = rowData[k];
                    int cr = (color & 0x00ff0000) >> 16;
                    int cg = (color & 0x0000ff00) >> 8;
                    int cb = (color & 0x000000ff);
                    r += cr * gaussMatrix[i + radius];
                    g += cg * gaussMatrix[i + radius];
                    b += cb * gaussMatrix[i + radius];
                    gaussSum += gaussMatrix[i + radius];
                }
            }
            int cr = (int)(r / gaussSum);
            int cg = (int)(g / gaussSum);
            int cb = (int)(b / gaussSum);
            pix[y * w + x] = cr << 16 | cg << 8 | cb | 0xff000000;
        }
    }
    for (int x = 0; x < w; ++x)
    {
        for (int y = 0; y < h; ++y)
            listData[y] = pix[y * w + x];
        for (int y = 0; y < h; ++y)
        {
            float r = 0, g = 0, b = 0;
            gaussSum = 0;
            for (int j = -radius; j <= radius; ++j)
            {
                int k = y + j;
                if (0 <= k && k <= h)
                {
                    int color = listData[k];
                    int cr = (color & 0x00ff0000) >> 16;
                    int cg = (color & 0x0000ff00) >> 8;
                    int cb = (color & 0x000000ff);
                    r += cr * gaussMatrix[j + radius];
                    g += cg * gaussMatrix[j + radius];
                    b += cb * gaussMatrix[j + radius];
                    gaussSum += gaussMatrix[j + radius];
                }
            }
            int cr = (int)(r / gaussSum);
            int cg = (int)(g / gaussSum);
            int cb = (int)(b / gaussSum);
            pix[y * w + x] = cr << 16 | cg << 8 | cb | 0xff000000;
        }
    }
    free(gaussMatrix);
    free(rowData);
    free(listData);
}複製程式碼

相關文章