哈夫曼编码优化图片实战

前面说过了哈夫曼的原理实际上就是根据加权来重新对可能出现的RGB范围进行定义,将定长的二进制数据编程变长的二进制数据,今天主要介绍哈夫曼算法的实现。直接上代码哈:

下载libjpeg引擎使用的库—基于该引擎来做一定的开发,自己实现编码。

3.1 添加权限

为了让保存好的图片存放到SD卡,这里要提供对SD卡使用权限支持。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

3.2 添加lib依赖

  1. 在app下创建jni目录.

  2. 添加jpeg文件夹libjpegbither.so文件。其中jpeg文件夹存放libjpeg引擎主要的代码类。

  3. 创建具体的cpp代码实现文件seemygojpgjni.cpp

  4. 创建自动化编译脚本makefile文件 Android.mk

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    LOCAL_MODULE    :=jpegbither
    LOCAL_SRC_FILES :=libjpegbither.so
    include $(PREBUILT_SHARED_LIBRARY)
    include $(CLEAR_VARS)
    LOCAL_MODULE    :=seemygojpgjni
    LOCAL_SRC_FILES :=seemygojpgjni.cpp
    LOCAL_SHARED_LIBRARIES :=jpegbither
    LOCAL_LDLIBS := -ljnigraphics -llog  
    include $(BUILD_SHARED_LIBRARY)
  5. Application.mk指定生成 arm 平台的二进制处理库。

    APP_ABI := armeabi-v7a armeabi   #表示 编译目标 ABI(应用二进制接口)
  6. 指定生成后的文件存放位置:

    android {
    ...
       sourceSets {
           main {
               jniLibs.srcDirs = ['libs']
           }
       }
    }

3.3 添加Java代码实现

public class MainActivity extends AppCompatActivity {
    public static final int REQUEST_PICK_IMAGE = 10011;
    public static final int REQUEST_KITKAT_PICK_IMAGE = 10012;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void pickFromGallery(View v) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"),
                    REQUEST_PICK_IMAGE);
        } else {
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("image/*");
            startActivityForResult(intent, REQUEST_KITKAT_PICK_IMAGE);
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == Activity.RESULT_OK) {
            switch (requestCode) {
                case REQUEST_PICK_IMAGE:
                    if (data != null) {
                        Uri uri = data.getData();
                        compressImage(uri);
                    } else {
                        Log.e("520it", "========图片为空======");
                    }
                    break;
                case REQUEST_KITKAT_PICK_IMAGE:
                    if (data != null) {
                        Uri uri = ensureUriPermission(this, data);
                        compressImage(uri);
                    } else {
                        Log.e("520it", "====-----==图片为空======");
                    }
                    break;
            }
        }
    }

    @SuppressWarnings("ResourceType")
    @TargetApi(Build.VERSION_CODES.KITKAT)
    public static Uri ensureUriPermission(Context context, Intent intent) {
        Uri uri = intent.getData();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            final int takeFlags = intent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION;
            context.getContentResolver().takePersistableUriPermission(uri, takeFlags);
        }
        return uri;
    }

    public void compressImage(final Uri uri) {
        //Log.e("520it", "====开始====uri==" + uri.getPath());
        new Thread(){
            @Override
            public void run() {
                try {
                    Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
                    String saveDirPath= Environment.getExternalStorageDirectory()+File.separator+"hfm_img";
                    File saveDir=new File(saveDirPath);
                    if (!saveDir.exists())
                        saveDir.mkdirs();
                    File saveFile = new File(saveDir, "终极压缩.jpg");
                    NativeUtil.compressBmpByCpp(bitmap, saveFile.getAbsolutePath());
                    Log.i("520it","path:"+saveFile.getAbsolutePath());
                    File saveFile1 = new File(saveDir, "质量压缩.jpg");
                    NativeUtil.saveBmpByQualityZip(bitmap,saveFile1);

                    File saveFile2 = new File(saveDir, "尺寸压缩.jpg");
                    NativeUtil.saveBmpByZipSize(bitmap,saveFile2);

                    File saveFile3 = new File(saveDir, "采样率压缩.jpg");
                    NativeUtil.saveBmpBySamplingRate(bitmap,saveFile3);

                    File saveFile4 = new File(saveDir, "原图.jpg");
                    NativeUtil.saveBmp(bitmap,saveFile4);

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    public void deleteCacheFile(View v){
        String saveDirPath= Environment.getExternalStorageDirectory()+File.separator+"hfm_img";
        File saveDir=new File(saveDirPath);
        if (saveDir.exists()){
            try {
                File[] files = saveDir.listFiles();
                for (File file:files){
                    file.delete();
                }
            }catch (Exception e){
                Log.e("520it", e.getLocalizedMessage());
            }
        }
    }

}

对本地JNI代码实现,封装NativeUtil来处理:

public class NativeUtil {
    private static int DEFAULT_QUALITY = 95;

    /**
     * @param bit      bitmap对象
     * @param fileName 指定保存目录名
     * @Description: JNI基本压缩
     */
    public static void compressBmpByCpp(Bitmap bit, String fileName) {
        compressBitmap(bit, bit.getWidth(), bit.getHeight(), 100, fileName.getBytes(), true);
    }

    /**
     * 调用底层 bitherlibjni.c中的方法
     * @param bit           需要处理的图片
     * @param w
     * @param h
     * @param quality       保存的质量
     * @param fileNameBytes 文件保存路径
     * @param optimize      是否要优化
     * @return
     * @Description:函数描述
     */
    public static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,
                                               boolean optimize);

    /**
     * 计算缩放比
     * @param bitWidth  当前图片宽度
     * @param bitHeight 当前图片高度
     * @return
     * @Description:函数描述
     */
    public static int getRatioSize(int bitWidth, int bitHeight) {
        // 图片最大分辨率
        int imageHeight = 1920;
        int imageWidth = 1080;
        // 缩放比
        int ratio = 1;
        // 缩放比,由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
        if (bitWidth > bitHeight && bitWidth > imageWidth) {
            // 如果图片宽度比高度大,以宽度为基准
            ratio = bitWidth / imageHeight;
        } else if (bitWidth < bitHeight && bitHeight > imageHeight) {
            // 如果图片高度比宽度大,以高度为基准
            ratio = bitHeight / imageHeight;
        }
        // 最小比率为1
        if (ratio <= 0)
            ratio = 1;
        return ratio;
    }

    /**
     * 加载lib下两个so文件
     */
    static {
        System.loadLibrary("jpegbither");
        System.loadLibrary("seemygojpgjni");
    }


    /**
     * 1. 质量压缩
     * 设置bitmap options属性,降低图片的质量,像素不会减少
     * 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
     * 设置options 属性0-100,来实现压缩
     *
     * @param bmp
     * @param file
     */
    public static void saveBmpByQualityZip(Bitmap bmp, File file) {
        // 0-100 100为不压缩
        int options = 20;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        bmp.compress(Bitmap.CompressFormat.JPEG, options, baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 2. 尺寸压缩
     * 通过缩放图片像素来减少图片占用内存大小
     * @param bmp
     * @param file
     */
    public static void saveBmpByZipSize(Bitmap bmp, File file) {
        // 尺寸压缩倍数,值越大,图片尺寸越小
        int ratio = 8;
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
        canvas.drawBitmap(bmp, null, rect, null);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 3.设置图片的采样率,降低图片像素
     */
    public static void saveBmpBySamplingRate(Bitmap bitmap, File file) {
        // 数值越高,图片像素越低
        int inSampleSize = 7;
        BitmapFactory.Options options = new BitmapFactory.Options();
        //采样率
        options.inSampleSize = inSampleSize;

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        try {
            if (file.exists()) {
                file.delete();
            } else {
                file.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 4.保存原图片
     * @param bitmap
     * @param file
     */
    public static void saveBmp(Bitmap bitmap, File file) {
        try {
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, new FileOutputStream(file));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

3.4 对C++层代码的实现准备

上节代码中,对哈夫曼底层的实现提供了本地实现,代码如下:

public static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,boolean optimize);

因为这里的使用的是C++代码的实现,所以要先生成头文件:

  1. 打开dos命令行,也可以是as自带的Terminal模块

  2. 跳转到包名的最顶层,一般我们是到 项目/app/src/main/java文件夹

  3. 调用javah -jni 包名.类名

    javah -classpath /Users/lean/Library/Android/sdk/platforms/android-26/android.jar:. com.a520it.hfmdemo.NativeUtil
  4. 将文件名改为seemygojpgjni.h

  5. seemygojpgjni.cpp 添加导包

    
    #include "seemygojpgjni.h"
    
    
    #include <jni.h>
    
    
    //统一编译方式
    extern "C" {
    
    #include "jpeg/jpeglib.h"
    
    
    #include "jpeg/cdjpeg.h"     /* Common decls for cjpeg/djpeg applications */
    
    
    #include "jpeg/jversion.h"       /* for version message */
    
    
    #include "jpeg/android/config.h"
    
    }
  6. 创建cpp层哈夫曼算法的实现方法:

    JNIEXPORT jstring JNICALL Java_com_a520it_hfmdemo_NativeUtil_compressBitmap
           (JNIEnv *env, jclass thiz, jobject bitmap, jint w, jint h, jint quality,
            jbyteArray fileNameBytes, jboolean optimize) {
    
    }
  7. 如果你对C++不是很熟悉,需要查看源码,但是你会发现Ctrl+单击无效。这里需要添加该解决方案的配置:

    找到gradle.properties 添加如下代码并重新编译:

    android.useDeprecatedNdk=true

3.5 C++层哈夫曼算法实现

  1. 导入依赖文件:

    
    #include <string.h>
    
    
    #include <android/bitmap.h>
    
    
    #include <android/log.h>
    
    
    #include <stdio.h>
    
    
    #include <setjmp.h>
    
    
    #include <math.h>
    
    
    #include <stdint.h>
    
    
    #include <time.h>
    
  2. 实现日志打印:

    
    #define LOG_TAG "a520it"
    
    
    #define LOGW(...)  __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
    
    
    #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
    
    
    #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
    
  3. 具体哈夫曼实现代码:

    
    #define true 1
    
    
    #define false 0
    
    
    typedef uint8_t BYTE;
    char *error;
    
    struct my_error_mgr {
       struct jpeg_error_mgr pub;
       jmp_buf setjmp_buffer;
    };
    
    typedef struct my_error_mgr *my_error_ptr;
    
    METHODDEF(void)
    my_error_exit(j_common_ptr cinfo) {
       my_error_ptr myerr = (my_error_ptr) cinfo->err;
       (*cinfo->err->output_message)(cinfo);
       error = (char *) myerr->pub.jpeg_message_table[myerr->pub.msg_code];
       LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code,
            myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
       // LOGE("addon_message_table:%s", myerr->pub.addon_message_table);
    //  LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]);
    //  LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]);
       longjmp(myerr->setjmp_buffer, 1);
    }
    
    /**
    * byte数组转C的字符串
    */
    char *jstrinToCstring(JNIEnv *env, jbyteArray barr) {
       char *rtn = NULL;
       jsize alen = env->GetArrayLength(barr);
       jbyte *ba = env->GetByteArrayElements(barr, 0);
       if (alen > 0) {
           rtn = (char *) malloc(alen + 1);
           memcpy(rtn, ba, alen);
           rtn[alen] = 0;
       }
       env->ReleaseByteArrayElements(barr, ba, 0);
       return rtn;
    }
    
    int generateJPEG(BYTE *data, int w, int h, int quality,
                    const char *outfilename, jboolean optimize) {
    
       //jpeg的结构体,保存的比如宽、高、位深、图片格式等信息,相当于java的类
       struct jpeg_compress_struct jcs;
    
       //当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调。
       struct my_error_mgr jem;
       jcs.err = jpeg_std_error(&jem.pub);
       jem.pub.error_exit = my_error_exit;
       if (setjmp(jem.setjmp_buffer)) {
           return 0;
       }
    
       //初始化jsc结构体
       jpeg_create_compress(&jcs);
       //打开输出文件 wb:可写byte
       FILE *f = fopen(outfilename, "wb");
       if (f == NULL) {
           return 0;
       }
       //设置结构体的文件路径
       jpeg_stdio_dest(&jcs, f);
       jcs.image_width = w;//设置宽高
       jcs.image_height = h;
    //   if (optimize) {
    //       LOGI("optimize==ture");
    //   } else {
    //       LOGI("optimize==false");
    //   }
    
       //看源码注释,设置哈夫曼编码:/* TRUE=arithmetic coding自适应压缩算法, FALSE=Huffman */
       jcs.arith_code = false;
       int nComponent = 3;
       /* 颜色的组成 rgb,三个 # of color components in input image */
       jcs.input_components = nComponent;
       //设置结构体的颜色空间为rgb
       jcs.in_color_space = JCS_RGB;
    //   if (nComponent == 1)
    //       jcs.in_color_space = JCS_GRAYSCALE;
    //   else
    //       jcs.in_color_space = JCS_RGB;
    
       //全部设置默认参数/* Default parameter setup for compression */
       jpeg_set_defaults(&jcs);
       //是否采用哈弗曼表数据计算 品质相差5-10倍
       jcs.optimize_coding = optimize;
       //设置质量
       jpeg_set_quality(&jcs, quality, true);
       //开始压缩,(是否写入全部像素)
       jpeg_start_compress(&jcs, TRUE);
    
       JSAMPROW row_pointer[1];
       int row_stride;
       //一行的rgb数量
       row_stride = jcs.image_width * nComponent;
       //一行一行遍历
       while (jcs.next_scanline < jcs.image_height) {
           //得到一行的首地址
           row_pointer[0] = &data[jcs.next_scanline * row_stride];
    
           //此方法会将jcs.next_scanline加1
           jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
       }
       jpeg_finish_compress(&jcs);//结束
       jpeg_destroy_compress(&jcs);//销毁 回收内存
       fclose(f);//关闭文件
    
       return 1;
    }
    
    JNIEXPORT jstring JNICALL Java_com_a520it_hfmdemo_NativeUtil_compressBitmap
           (JNIEnv *env, jclass thiz, jobject bitmap, jint w, jint h, jint quality,
            jbyteArray fileNameBytes, jboolean optimize) {
       //1.将安卓中的bitmap解码,并转化成RGB.
       //2.为JPEG对象分配内存空间并初始化。
       //3.指定压缩数据源
       //4.获取文件信息
       //5.为压缩设置参数,比如图像的大小 颜色 类型空间
       //6.开始压缩 jpeg_start_compress()
       //7.结束压缩 jpeg_finish_compress()
       //8.释放资源
    
       //用来保存图片的像素值的二维码数组
       BYTE *pixelscolor;
       //锁定画布
       AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor);
       //解析每一个像素的RGB值到一维数组
       BYTE *data = (BYTE *) malloc(w * h * 3);
       BYTE *tempData = data;
       BYTE r, g, b;
       int singlePixelColor;
       //遍历二维数组每一个像素值的rgb
       for (int j = 0; j < h; ++j) {
           for (int i = 0; i < w; ++i) {
               singlePixelColor = *((int *) pixelscolor);
               //计算出红绿蓝值
               r = (singlePixelColor & 0xFF0000) >> 16;
               g = (singlePixelColor & 0xFF0000) >> 8;
               r = singlePixelColor & 0xFF0000;
               //将取出来的RGB值传递给以为数组
               *data = b;
               *(data + 1) = g;
               *(data + 2) = r;
               //一维数组移动三个地址
               data += 3;
               //二维数组移动四个数组
               pixelscolor += 4;
           }
       }
       //解锁画布
       AndroidBitmap_unlockPixels(env, bitmap);
       //对一维数组进行哈夫曼算法编码
       char *outfileName = jstrinToCstring(env, fileNameBytes);
       int result = generateJPEG(data, w, h, quality, outfileName, optimize);
    
       const char *jresultStr = result == 0 ? "failure" : "success";
       return env->NewStringUTF(jresultStr);
    }

3.5 编译实现

命令行进入jni文件夹:

这里写图片描述

执行效果如下图:

这里写图片描述

项目链接: https://pan.baidu.com/s/1Yjn74-H1HgReBTlrx5qGCA 密码: m78n

猜你喜欢

转载自blog.csdn.net/qq285016127/article/details/80146984