图片压缩--鲁班压缩

945 阅读3分钟

鲁班压缩的压缩质量和压缩大小比原生搞出一个数量级,微信也是使用该技术进行图片压缩。

原生压缩: Bitmap.compress():

主要的处理流程是在native层处理的,bitmap.cpp中的Bitmap_compress方法中通过SkBitmap处理(Sk开头的表示是skia里的相关类,skia是android里的图像渲染引擎),因此bitmap真正的加载图像是在native堆。
Java层 --> Bitmap对象-->skia引擎(native)
skia是基于jpeg的二次封装,而skia不支持哈夫曼压缩(鲁班压缩),jpeg支持

哈夫曼算法:

使用变长编码对元数据(不可再分,例如颜色中的argb)进行编码,其中变长编码是通过一种评估来源符号出现几率的方法得到的,出现几率较高的数据使用较短的编码,反之出现频率低的则使用较长点编码,使得编码之后的数据平均长度大大降低。

如何在android中使用哈夫曼算法压缩案图片:

因为skia是基于jpeg的二次封装,因此使用jni调用android系统源码里jpeg的相关方法来实现

1、先将java层传入点bitmap用像素数组表示,pixels即为获取点数组,里面的每个元素为所表示的颜色,因为是byte,所以4个元素表示一个颜色(颜色是int)

//图片解析成一组像素组 w*h

AndroidBitmapInfo bitmapInfo;
AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
BYTE *pixels;
AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);

2、将获得到点数组中点rgb提取出来,存放到一个长度为wh3的字节数组中(如果需要透明度的话就需要乘4)

int h = bitmapInfo.height;
int w = bitmapInfo.width;
BYTE *data,*tmpData;
data= (BYTE *) malloc(w * h * 3);
tmpData = data;

3、通过与运算提取出rgb对应点字节,如提取R对应点字节时,将像素数组与ox00FF0000进行与运算提取,还需将gb的字节排除,一个字节8位,所以需要右移16位。而且使用jpeg时,数组里的排列顺序需要是bgr。

BYTE r, g, b;
int color;
// 遍历每一个像素点 将rgb 提取出来
// jpeg opencv 里面颜色存储的顺序是 b g r
// 数组 w*h*3 【b g r b g r】
for (int i = 0; i < h; ++i) {
    for (int j = 0; j < w; ++j) {
        color = *((int *) pixels);
        r = ((color &amp; 0x00FF0000) >> 16);
        g = ((color &amp; 0x0000FF00) >> 8);
        b = ((color &amp; 0x000000FF));
        *data = b;
        *(data + 1) = g;
        *(data + 2) = r;
        data += 3;
        pixels += 4;
    }
}

4、引入jpeg的相关头文件之后,创建jpeg相关的bitmap

struct jpeg_compress_struct jpeg_struct;
// 设置错误处理信息
jpeg_error_mgr err;
jpeg_struct.err = jpeg_std_error(&err);
// 给结构体分配内存
jpeg_create_compress(&jpeg_struct);
FILE *file = fopen(path, "wb");
// 设置输出路径
jpeg_stdio_dest(&jpeg_struct, file);

5、初始化相关点属性,其中arith_code这个属性级表示是否需要开启哈夫曼编码,false表示开启

jpeg_struct.image_width = w;
jpeg_struct.image_height = h; 
//采取哈夫曼编码 在skia 源码中 jpeg_struct.arith_code=TRUE 
jpeg_struct.arith_code = FALSE; 
//优化编码 
jpeg_struct.optimize_coding = TRUE;
jpeg_struct.in_color_space = JCS_RGB; 
jpeg_struct.input_components = 3;
// 其他的设置默认 
jpeg_set_defaults(&jpeg_struct);
jpeg_set_quality(&jpeg_struct, 20, true); 
//------------------------jpeg的初始化

6.、开始压缩并写入文件,row_pointer表示每一行的第一个元素

//开始压缩 
jpeg_start_compress(&jpeg_struct, TRUE); 
JSAMPROW row_pointer[1]; 
// 一行的rgb int row_stride = w * 3; 
while (jpeg_struct.next_scanline < h) { 
    row_pointer[0] = &data[jpeg_struct.next_scanline * w * 3]; 
    // 这个函数一调用 jpeg_struct.next_scanline ++ 
    jpeg_write_scanlines(&jpeg_struct, row_pointer, 1); 
} 
jpeg_finish_compress(&jpeg_struct); 
jpeg_destroy_compress(&jpeg_struct); 
fclose(file);

完整代码:

#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <android/log.h>
#include <malloc.h>
extern "C"
{
#include "jpeglib.h"
}
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOG_TAG "david"
#define true 1
typedef uint8_t BYTE;
void writeImg(BYTE *data, const char *path, int w, int h) {
    struct jpeg_compress_struct jpeg_struct;
//    设置错误处理信息
    jpeg_error_mgr err;
    jpeg_struct.err = jpeg_std_error(&err);
//    给结构体分配内存
    jpeg_create_compress(&jpeg_struct);

    FILE *file = fopen(path, "wb");
//    设置输出路径
    jpeg_stdio_dest(&jpeg_struct, file);

    jpeg_struct.image_width = w;
    jpeg_struct.image_height = h;
    //采取哈夫曼编码   在skia 源码中    jpeg_struct.arith_code=TRUE
    jpeg_struct.arith_code = FALSE;
//优化编码
    jpeg_struct.optimize_coding = TRUE;
    jpeg_struct.in_color_space = JCS_RGB;

    jpeg_struct.input_components = 3;
//    其他的设置默认
    jpeg_set_defaults(&jpeg_struct);
    jpeg_set_quality(&jpeg_struct, 20, true);
    //------------------------jpeg的初始化

    jpeg_start_compress(&jpeg_struct, TRUE);

    JSAMPROW row_pointer[1];
//    一行的rgb
    int row_stride = w * 3;
    while (jpeg_struct.next_scanline < h) {
         row_pointer[0] = &data[jpeg_struct.next_scanline * w * 3];

//        这个函数一调用    jpeg_struct.next_scanline ++
        jpeg_write_scanlines(&jpeg_struct, row_pointer, 1);
    }
    jpeg_finish_compress(&jpeg_struct);

    jpeg_destroy_compress(&jpeg_struct);
    fclose(file);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hafumanimage_MainActivity_compress(JNIEnv *env, jobject instance,
                                                       jobject bitmap, jstring path_) {
    const char *path = env->GetStringUTFChars(path_, 0);
    AndroidBitmapInfo bitmapInfo;
    AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
    BYTE *pixels;
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
    int h = bitmapInfo.height;
    int w = bitmapInfo.width;
//
    BYTE *data,*tmpData;
    data= (BYTE *) malloc(w * h * 3);
    tmpData = data;
    BYTE r, g, b;
    int color;
    for (int i = 0; i < h; ++i) {
        for (int j = 0; j < w; ++j) {
            color = *((int *) pixels);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            data += 3;
            pixels += 4;
        }
    }
    AndroidBitmap_unlockPixels(env, bitmap);

    writeImg(tmpData, path, w, h);
    env->ReleaseStringUTFChars(path_, path);
}


谷歌不采用哈夫曼的原因:

使用定长编码可以直接一个像素一个像素加载,而使用变长编码,则需要先遍历,耗时耗内存,以前手机内存不大,所以没有采用