性能優化--哈夫曼算法原理

大家都知道,使用哈夫曼壓縮能達到無損壓縮,也就是說。保證了原圖質量的同時,能夠降低圖片的大小。這是什麼原理呢?首先我們需要了解的是Android系統加載圖片使用的是Skia加載庫,當然這個庫的底層還是是用的jpeg對圖片進行壓縮處理,但是爲了能夠適配低端的手機(這裏的低端是指以前的硬件配置不高的手機,CPU和內存在手機上都非常喫緊 性能差),由於哈夫曼算法非常喫CPU,被迫用了其他的算法。所以Skia在進行圖片處理並沒有去使用到哈夫曼壓縮。但是解碼還是保留了哈夫曼算法,這就導致了圖片處理後文件變大了。

一、原理

看一張圖,如下:
在這裏插入圖片描述
這裏解釋一下,一張圖片都是有argb組成,這經過哈夫曼壓縮之前,可以將圖片的a通道拿掉,剩下的 rgb 進行壓縮。每一個通道的取值範圍都是0~255,也就是說每種顏色都是有256種表現形態,這裏以紅色 r 爲例。假如一張圖片紅色 r 共有6個等級,每個等級對應的像素點數如上圖。那麼根據哈夫曼算法會形成一個哈夫曼樹。如下圖:
在這裏插入圖片描述
哈夫曼樹形成的規則,首先從6個等級中找出兩個像素點最少的兩個(1和10),放在末端,然後將兩個像素點相加放在左端,然後再找出原像素點中較少的一個(100),放在右端,並用線將節點和分支連接起來。依次類推,直到找到頂點。其中左邊的分支用0標記,右邊的分支用1標記。

1個rgb對應3個字節,每一個字節都會形成一個像素表,像素表中存儲每個像素等級對應的像素點數。比如根據哈夫曼樹,我們如何找到紅色的等級1的像素,我們從頂點出發,只需要在表中查找1,就可以找到5000,那麼如何找到1000呢?查找01!如何查找500呢?查找001!如何查找100呢?0001?…等等。就是根據分支一直找下去。

優點:對於原始一個像素點佔8位,對於5000個像素點,所有那個內存爲8*5000 ,當使用哈夫曼壓縮後,只要一位(比如1)就可以表示這5000個像素點。並且沒有減少圖片的像素點數,所以叫做無損壓縮。另外,對於顏色值越單一的圖片,壓縮率越高。顏色值越多,樹會越來越長,找到末端的像素點佔用的位數會越來越多。所以它的壓縮效率一般在20%~90%。

二、實現

1、導庫

這裏我們需要libjpeg在android環境下的so庫,所以需要事先在linux環境下編譯好。將編譯好的so庫放到項目中,如下:
在這裏插入圖片描述
其中x86支持的模擬器,arm64真機。導入需要支持的頭文件在libjpeg目錄下:
在這裏插入圖片描述

2、鏈接庫

需要在CMakeList.txt文件中配置,如下;
在這裏插入圖片描述
紅色框框的內容,就是需要添加的。

3、調用
/**
     * @param bitmap 要壓縮的圖片
     * @param path 壓縮後存放的文件名稱
     * @param quality 壓縮質量
     */
    public native void compressImage(Bitmap bitmap, String path, int quality);

    public void compress(View view) {

        //sd卡目錄下的一張圖片
        File input = new File(Environment.getExternalStorageDirectory(), "timg750.png");
        inputBitmap = BitmapFactory.decodeFile(input.getAbsolutePath());

        //開始壓縮
        compressImage(inputBitmap,Environment.getExternalStorageDirectory() + "/timg751.png",50);
    }

我在SD卡目錄下有一張timg750.png的圖片,壓縮後調用jni的compressImage()方法,並且壓縮後的文件存爲SD卡的timg751.png,壓縮質量爲50。

4、壓縮代碼

(1)得到原始圖片信息,將原始圖片的argb的a通道信息移除,並存入到新的圖片信息data(包含r、g、b)中。

extern "C"
JNIEXPORT void JNICALL
Java_com_xinyartech_myhuffman_MainActivity_compressImage(JNIEnv *env, jobject instance,
                                                        jobject bitmap, jstring path_,
                                                        jint q) {

    const char *path = env->GetStringUTFChars(path_, 0);
    //從bitmap獲取argb數據
    AndroidBitmapInfo info;//info=new 對象();
    //獲取裏面的信息
    AndroidBitmap_getInfo(env, bitmap, &info);//  void method(list)
    //得到圖片中的像素信息
    uint8_t *pixels;//uint8_t char    java   byte     *pixels可以當byte[]
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
    //jpeg argb中去掉他的a ===>rgb
    int w = info.width;
    int h = info.height;
    int color;
    //開一塊內存用來存入rgb信息
    uint8_t* data = (uint8_t *) malloc(w * h * 3);//data中可以存放圖片的所有內容
    uint8_t* temp = data;
    uint8_t r, g, b;//byte
    //循環取圖片的每一個像素
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            color = *(int *) pixels;//0-3字節  color4 個字節  一個點
            //取出rgb
            r = (color >> 16) & 0xFF;//    #00rrggbb  16  0000rr   8  00rrgg
            g = (color >> 8) & 0xFF;
            b = color & 0xFF;
            //存放,以前的主流格式jpeg    bgr
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            data += 3;
            //指針跳過4個字節
            pixels += 4;
        }
    }
    //把得到的新的圖片的信息存入一個新文件 中
    write_JPEG_file(temp, w, h, q, path);
    //釋放內存
    free(temp);
    AndroidBitmap_unlockPixels(env, bitmap);
    env->ReleaseStringUTFChars(path_, path);
}

(2)將新的圖片信息存入到新的文件中(就是 timg751.png )

void write_JPEG_file(uint8_t *data, int w, int h, jint q, const char *path) {
//    3.1、創建jpeg壓縮對象
    jpeg_compress_struct jcs;
    //錯誤回調
    jpeg_error_mgr error;
    jcs.err = jpeg_std_error(&error);
    //創建壓縮對象
    jpeg_create_compress(&jcs);
//    3.2、指定存儲文件  write binary
    FILE *f = fopen(path, "wb");
    jpeg_stdio_dest(&jcs, f);
//    3.3、設置壓縮參數
    jcs.image_width = w;
    jcs.image_height = h;
    //bgr
    jcs.input_components = 3;
    jcs.in_color_space = JCS_RGB;
    jpeg_set_defaults(&jcs);
    //開啓哈夫曼功能
    jcs.optimize_coding = true;
    //設置壓縮質量
    jpeg_set_quality(&jcs, q, 1);
//    3.4、開始壓縮
    jpeg_start_compress(&jcs, 1);
//    3.5、循環寫入每一行數據
    int row_stride = w * 3;//一行的字節數
    JSAMPROW row[1];
    while (jcs.next_scanline < jcs.image_height) {
        //取一行數據
        uint8_t *pixels = data + jcs.next_scanline * row_stride;
        row[0]=pixels;
        jpeg_write_scanlines(&jcs,row,1);
    }
//    3.6、壓縮完成
    jpeg_finish_compress(&jcs);
//    3.7、釋放jpeg對象
    fclose(f);
    jpeg_destroy_compress(&jcs);
}

這裏面的關鍵代碼爲 jcs.optimize_coding = true;將開啓哈夫曼壓縮。

5、壓縮結果

在這裏插入圖片描述
壓縮前的大小爲521kb,壓縮後38kb。但是圖片完全看不出差異。

三、注意

  • 動態庫的環境配置地址一定要正確
  • app要配置好文件的讀寫權限
  • 我的環境是根據 AS 3.5構建。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章