大家都知道,使用哈夫曼壓縮能達到無損壓縮,也就是說。保證了原圖質量的同時,能夠降低圖片的大小。這是什麼原理呢?首先我們需要了解的是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構建。