Android图片压缩原理分析(三)—— 哈夫曼压缩讲解

news2024/11/25 7:03:59

前言

 

前面几篇文章,我们了解了一些关于图片压缩的基础知识以及Android的Bitmap相关的知识,然后也提到的SkiaAndroid的重要组成部分。在鲁班压缩算法解析中初次提到了哈夫曼压缩,那么他们之间到底是存在什么关系呢?今天我们就来探究探究。

认识Android Skia 图像引擎

什么是skia图像引擎了,详细介绍如下四点:

  • Skia 是一个2D向量图形处理函数库, 2005年被Google收购后并自己维护的 c++ 实现的图像引擎,实现了各种图像处理功能,并且广泛地应用于谷歌自己和其它公司的产品中(如:Chrome、Firefox、 Android等),基于它可以很方便为操作系统、浏览器等开发图像处理功能。

  • SkiaAndroid 中提供了基本的画图和简单的编解码功能,可以挂接其他的第三方编码解码库或者硬件编解码库,例如 libpng 和 libjpeg ,libgif 等等。因此,这个函数调用bitmap.compress(Bitmap.CompressFormat.JPEG...),实际会调用 libjpeg.so动态库进行编码压缩。

  • 最终 Android 编码保存图片的逻辑是 Java 层函数→Native 函数→Skia函数→对应第三库函数(例如 libjpeg)。所以skia就像一个 胶水层,用来链接各种第三方编解码库,不过 Android 也会对这些库做一些修改,比如修改内存管理的方式等等。

  • Android 在之前从某种程度来说使用的算是 libjpeg 的功能阉割版,压缩图片默认使用的是 standard huffman,而不是 optimized huffman,也就是说使用的是默认的哈夫曼表,并没有根据实际图片去计算相对应的哈夫曼表,Google 在初期考虑到手机的性能瓶颈,计算图片权重这个阶段非常占用 CPU 资源的同时也非常耗时,因为此时需要计算图片所有像素 argb 的权重,这也是 Android 的图片压缩率对比 iOS 来说差了一些的原因之一。

Huffman算法

哈夫曼算法是在多媒体处理里常用的算法之一。这里通过举一个简单的列子来讲解哈夫曼算法。比如一个文件中存在五个值,分别是 a,b,c,d,e,它们用二进制表达是:

a. 1010 

b. 1011

c. 1100

d. 1101

e. 1110

分析上面的二进制,你会发现,其实他们开头都有1,那么,其实这样是浪费掉了,而在定长算法下,最优的表达式如下所示:

a. 010 

b. 011

c. 100

d. 101

e. 110

这样你就会发现,这样做,我们就能做到节省一位的损耗,那哈夫曼算法比起定长算法改进的地方在哪里呢?在哈夫曼算法中我们可以给信息赋予权重,即为信息加权重,假设 a 占据了 60%,b 占据了 20%, c 占据了 20%,d,e 都是 0%

a:010 (60%)

b:011 (20%)

c:100 (20%)

d:101 (0%)

e:110 (0%)

在这种情况下,我们可以使用哈夫曼树算法再次优化为:

a:1 b:01 c:00

其优化的原理其实就是频率高的字母使用短码,对出现频率低的使用长码,不出现的直接就去掉。

所以最后我们就会看到 abcde 优化过后的值如上面所示。所以abcde的哈夫曼编码就对应:1 01 00

 定长编码下的abcde010 011 100 101 110, 使用哈夫曼树加权重后的编码则为:1 01 00,这就是哈夫曼算法的整体思路。听了这里你如果对哈夫曼的优化原理,或者说哈夫曼树加权重后的编码还是不够理解,可以参考这篇《哈夫曼树及编码讲解的详细介绍》。

所以这个算法一个很重要的思路是必须知道每一个元素出现的权重,如果我们能够知道每一个元素的权重,那么就能够根据权重动态生成一个最优的哈夫曼表。

但是怎么去获取每一个元素,对于图片就是每一个像素中 argb 的权重呢,只能去循环整个图片的像素信息,这无疑是非常消耗性能的,所以早期 android 就使用了默认的哈夫曼表进行图片压缩。

libjpeg 与 optimize_coding的前世今生:

libjpeg在压缩图像时,有一个参数叫 optimize_coding,关于这个参数,libjpeg.doc 有如下解释:

TRUE causes the compressor to compute optimal Huffman coding tables for the image. This requires an extra pass over the data and therefore costs a good deal of space and time. The default is FALSE, which tells the compressor to use the supplied or default Huffman tables. In most cases optimal tables save only a few percent of file size compared to the default tables. Note that when this is TRUE, you need not supply Huffman tables at all, and any you do supply will be overwritten.

我们翻译这段话,意思大致如下:

TRUE使压缩器计算图像的最佳霍夫曼编码表。这需要对数据进行额外的传递,因此需要大量的空间和时间。默认值为FALSE,这告诉压缩器使用提供的或默认的霍夫曼表。在大多数情况下,与默认表相比,最佳表只节省了文件大小的百分之几。请注意,当此值为TRUE时,您根本不需要提供Huffman表,并且您提供的任何表都将被覆盖。

由上可知,如果设置 optimize_coding 为TRUE,将会使得压缩图像过程中,会先基于图像数据计算哈弗曼表。由于这个计算会显著消耗空间和时间,默认值被设置为FALSE

那么 optimize_coding 参数的影响究竟会有多大呢?Skia 的官方人员经过实际测试,分别设置 optimize_coding=TRUEFALSE 进行压缩,发现 FALSE 时的图片大小大约是 TRUE 时的 2倍+。换言之就是相同文件体积的图片,不使用哈夫曼编码图片质量会比使用哈夫曼低 2倍+

 从 Android 7.0 版本开始,optimize_code 标示已经设置为了 TRUE,也就是默认使用图像生成哈夫曼表,而不是使用默认哈夫曼表。

在这里要非常感谢QQ音乐技术团队,本文也是借鉴他们文章(Android 中图片压缩分析(上) ),从而获得启发,如有冒犯,请立即联系删除。

分析Bitmap.cpp图像处理引擎的调用关系

我们都知道bitmap是在native层被创建的,在Bitmap.cpp文件中,创建的bitmap其实是创建了一个SKBitmap的对象,交给了skia引擎去处理。我们一起先看看Bitmap.cpp文件都干了什么。

在这之前我们先看一下Android的代码。在Android中,对图片进行质量压缩,通常我们的实现方式如下所示:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);

在上述代码中,我们选择的压缩格式是CompressFormat.JPEG,除此之外还有两个选择:

CompressFormat.PNG: PNG 格式是无损的,它无法再进行质量压缩,quality 这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;

CompressFormat.WEBP: 这个格式是 google 推出的图片格式,它会比 JPEG 更加省空间,经过实测大概可以优化 30% 左右。

由于项目原因和兼容性选择了JPEG,因此接下来的分析也将是围绕 JPEG 展开。

将 PNG 图片转成 JPEG 格式之后不会降低这个图片的尺寸,但是会降低视觉质量,从而降低存储体积。同时,由于尺寸不变,所以将这个图片解码成相同色彩模式的 bitmap 之后,占用的内存大小和压缩前是一样的。

bitmap函数 compress 经过一连串的 java 层调用之后,最后来到了一个 native 函数,如下:

//Bitmap.cpp
static jboolean Bitmap_compress(JNIEnv* env, jobject clazz, jlong bitmapHandle,
                                jint format, jint quality,
                                jobject jstream, jbyteArray jstorage) {

    LocalScopedBitmap bitmap(bitmapHandle);
    SkImageEncoder::Type fm;

    switch (format) {
    case kJPEG_JavaEncodeFormat:
        fm = SkImageEncoder::kJPEG_Type;
        break;
    case kPNG_JavaEncodeFormat:
        fm = SkImageEncoder::kPNG_Type;
        break;
    case kWEBP_JavaEncodeFormat:
        fm = SkImageEncoder::kWEBP_Type;
        break;
    default:
        return JNI_FALSE;
    }

    if (!bitmap.valid()) {
        return JNI_FALSE;
    }

    bool success = false;

    std::unique_ptr<SkWStream> strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage));
    if (!strm.get()) {
        return JNI_FALSE;
    }

    std::unique_ptr<SkImageEncoder> encoder(SkImageEncoder::Create(fm));
    if (encoder.get()) {
        SkBitmap skbitmap;
        bitmap->getSkBitmap(&skbitmap);
        success = encoder->encodeStream(strm.get(), skbitmap, quality);
    }
    return success ? JNI_TRUE : JNI_FALSE;
}

可以看到最后调用了函数 encoder->encodeStream(....) 编码保存本地。该函数是调用 skia 引擎来对图片进行编码压缩。

实现JPEG图像处理引擎

上面分析我们已经知道,最后是调用了函数 encoder->encodeStream(....) 编码保存本地。该函数是调用 skia 引擎来对图片进行编码压缩。下面我们自己动手实现一个jpeg的图像处理引擎。

首先我们打开jpeglib的官网,找到源码并下载下来,然后导入jpeglib.h的头文件会需要其他的.h头文件,如下图所示会包括了源码,FAQ:

 也可以下载libjpeg-turbo,后面基本都是使用libjpeg-turbo。libjpeg-turbo的源码见GitHub。

 导入头文件后,我们按照着安卓源码中 libjpeg-turbo库里的example.c文件(系统提供的例子),开始编写native-lib.cpp文件:

#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <android/log.h>
#include <malloc.h>
// 因为头文件都是c文件,咱们写的是.cpp 是C++文件,这时候就需要混编,所以加入下面关键字
extern "C"
{
#include "jpeglib.h"
}
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define LOG_TAG "louis"
#define true 1
typedef uint8_t BYTE;
// 写入图片函数
void writeImg(BYTE *data, const char *path, int w, int h) {

//  信使: java与C沟通的桥梁,jpeg的结构体,保存的比如宽、高、位深、图片格式等信息
    struct jpeg_compress_struct jpeg_struct;
//  设置错误处理信息 当读完整个文件的时候就会回调my_error_exit,例如内置卡出错、没权限等
    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;
//  初始化  初始化
//  改成FALSE   ---》 开启hufuman算法
    jpeg_struct.arith_code = FALSE;
//  是否采用哈弗曼表数据计算 品质相差2倍多,官方实测, 吹5-10倍的都是扯淡
    jpeg_struct.optimize_coding = TRUE;
//  设置结构体的颜色空间为RGB
    jpeg_struct.in_color_space = JCS_RGB;
//  颜色通道数量
    jpeg_struct.input_components = 3;
//  其他的设置默认
    jpeg_set_defaults(&jpeg_struct);
//  设置质量
    jpeg_set_quality(&jpeg_struct, 60, true);
//  开始压缩,(是否写入全部像素)
    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];
//		此方法会将jcs.next_scanline加1
        jpeg_write_scanlines(&jpeg_struct, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
    }
    jpeg_finish_compress(&jpeg_struct);
    jpeg_destroy_compress(&jpeg_struct);
    fclose(file);
}

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

    const char *path = env->GetStringUTFChars(path_, 0);
//  获取Bitmap信息
    AndroidBitmapInfo bitmapInfo;
    AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
//  存储ARGB所有像素点
    BYTE *pixels;
//  1、读取Bitmap所有像素信息
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
//  获取bitmap的 宽,高,format
    int h = bitmapInfo.height;
    int w = bitmapInfo.width;
//  存储RGB所有像素点
    BYTE *data,*tmpData;
//  2、解析每个像素,去除A通量,取出RGB通量,
//  假如图片的像素是1920*1080,只有RGB三个颜色通道的话,计算公式为 w*h*3
    data= (BYTE *) malloc(w * h * 3);
//  存储RGB首地址
    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 G B 
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));
            // 赋值
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            // 指针后移
            data += 3;
            pixels += 4;
        }
    }
//  3、读取像素点完毕 解锁,
    AndroidBitmap_unlockPixels(env, bitmap);
//  直接用data写数据
    writeImg(tmpData, path, w, h);
    env->ReleaseStringUTFChars(path_, path);
}

小结总结

查阅源码后发现:

在Android系统在7.0版本之前内部使用的是libjpeg非turbo版,并且为了性能关闭了Huffman编码计算,使用默认的哈夫曼表,而不是算数编码。

从Android 7.0版本开始,系统内部使用了libjpeg-turbo库并且启用Huffman编码,标示就是optimize_code已经设置为了TRUE,也就是默认使用Huffman压缩计算生成新的哈夫曼表。libjpeg-turbo是一个C语音编写的高效JPEG图像处理库,相当于是一个libjpeg的增强版。

这也就是Luban压缩为什么会给出一个turbo分支,其实是为了兼容Android 7.0版本之前。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/922708.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring 事务(事务、声明式事务@Transactional、事务隔离级别、事务传播机制)

目录 1.事务的定义 2.Spring中事务的实现 2.1 MySQL中的事务使用 2.2 Spring中编程事务的实现 2.3 Spring中声明式事务 2.3.1 声明式事务的实现 Transactional 2.3.2 Transactional 作用域 2.3.3 Transactional 参数说明 2.3.4 注意事项 &#xff08;1&#xff09;解…

html学习第2篇---标签(1)

html学习第2篇---标签 1、标题标签h1---h62、段落标签p3、换行标签br4、文本格式化标签5、div标签和span标签6、图像标签img6.1、图像属性6.2、相对路径、绝对路径 7、超链接标签a7.1、属性7.2、分类 8、注释标签和特殊字符8.1、注释8.2、特殊字符 1、标题标签h1—h6 为了使网…

【java安全】FastJson反序列化漏洞浅析

文章目录 【java安全】FastJson反序列化漏洞浅析0x00.前言0x01.FastJson概述0x02.FastJson使用序列化与反序列化 0x03.反序列化漏洞0x04.漏洞触发条件0x05.漏洞攻击方式JdbcRowSetImpl利用链TemplatesImpl利用链**漏洞版本**POC漏洞分析 【java安全】FastJson反序列化漏洞浅析 …

网络丢包故障如何定位?如何解决?

引言 本期分享一个比较常见的网络问题--丢包。例如我们去ping一个网站&#xff0c;如果能ping通&#xff0c;且网站返回信息全面&#xff0c;则说明与网站服务器的通信是畅通的&#xff0c;如果ping不通&#xff0c;或者网站返回的信息不全等&#xff0c;则很可能是数据被丢包了…

java8:HashMap的实现原理

一概述 这个哈希表是基于 Map 接口的实现的&#xff0c;它允许 null 值和null 键&#xff0c;它不是线程同步的&#xff0c;同时也不保证有序。 Map 的这种实现方式为 get&#xff08;取&#xff09;和 put&#xff08;存&#xff09;带来了比较好的性能。但是如果涉及到大量的…

C++入门:内联函数,auto,范围for循环,nullptr

目录 1.内联函数 1.1 概念 1.2 特性 1.3 内联函数与宏的区别 2.auto关键字(C11) 2.1 auto简介 2.2 auto的使用细则 2.3 auto不能推导的场景 3.基于范围的for循环(C11) 3.1 范围for的语法 3.2 范围for的使用方法 4.指针空值nullptr(C11) 4.1 C98中的指针空值 1.内联…

开悟Optimization guide for intermediate tracks

目录 认识模型 参考方案&#xff08;按模块拆解&#xff09; 认识模型 模型控制1名英雄进行镜像1 v 1对战 Actor集群资源为64核CPU 问题特点&#xff1a;单一公平对抗场景&#xff08;同英雄镜像对赛&#xff09;&#xff0c;单位时间样本产能低&#xff0c;累计训练资源相…

macOS - 安装 Python 及地址

文章目录 Python 官方安装包Pip3Applications - PythonMiniconda多个python环境有多种方式安装 python,比如 Python 官方包、anaconda、miniconda、brew 等 这里记录使用 Python 官方包进行安装,和 miniconda 安装方式,以及安装后 各执行文件、安装包的地址。 明确这些地址后…

Arduino开发Seeed Studio XIAO RP2040

前言 准备一些硬件设备 Seeed Studio XIAO RP2040 一块电脑——window 或 Mac 一台Type-C数据线 某些USB线只支持充电&#xff0c;无传输数据功能。 连接电脑 按住boot按钮&#xff0c;然后将 Seeed Studio XIAO RP2040 连接到 PC。 2. 如果电脑文件管理器上显示了“RPI-RP2…

一生一芯9——ubuntu22.04安装valgrind

这里安装的valgrind版本是3.19.0 下载安装包 在选定的目录下打开终端&#xff0c;输入以下指令 wget https://sourceware.org/pub/valgrind/valgrind-3.19.0.tar.bz2直至下载完成 解压安装包 输入下面指令解压安装包 tar -xvf valgrind-3.19.0.tar.bz2.tar.bz2注&#xf…

大转盘抽奖活动设计完全指南,轻松打造火爆营销

在如今竞争激烈的商业环境中&#xff0c;如何吸引顾客、提升销售额成为了每个商家都必须面对的问题。而大转盘抽奖活动作为一种互动性强、刺激性高的推广方式&#xff0c;成为了越来越多商家的首选。本文将详细介绍如何通过乔拓云后台制作大转盘抽奖活动&#xff0c;助力商家的…

高压功率放大器在管道损伤检测中的应用有哪些

高压功率放大器管道损伤检测中有着广泛的应用。管道是现代社会中重要的基础设施之一&#xff0c;用于输送各种液体或气体。然而&#xff0c;由于外部因素或长时间使用引起的磨损、腐蚀或撞击等问题&#xff0c;管道可能出现损伤&#xff0c;这可能对环境和人员安全产生严重影响…

【ag-grid-vue】基本使用

ag-grid是一款功能和性能强大外观漂亮的表格插件&#xff0c;ag-grid几乎能满足你对数据表格所有需求。固定列、拖动列大小和位置、多表头、自定义排序等等各种常用又必不可少功能。关于收费的问题&#xff0c;绝大部分应用用免费的社区版就够了&#xff0c;ag-grid-community社…

axios 进阶

axios 进阶 接口传参方式 使用 xhr 原生技术或者是 axios 时&#xff0c;它的 post 传参方式是键值对的形式 keyvalue。但是在实际开发中一般是使用对象的形式定义数据&#xff0c;方便读取和赋值。所以当我们需要发起请求时可以通过 qs 这一款插件将对象转成键值对形式&…

221. 最大正方形 Python

文章目录 一、题目描述示例 1示例 2示例 3 二、代码三、解题思路 一、题目描述 在一个由 0 和 1 组成的二维矩阵内&#xff0c;找到只包含 1 的最大正方形&#xff0c;并返回其面积。 示例 1 输入&#xff1a;matrix [["1","0","1","0&q…

视频集中存储/云存储平台EasyCVR国标GB28181协议接入的报文交互数据包分析

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。视频汇聚融合管理…

loss.sum.backward()为什么要sum()?

在动手学深度学习中&#xff0c;这样解释的&#xff1a; 当y不是标量时&#xff0c;向量y关于向量x的导数的最自然解释是一个矩阵。 对于高阶和高维的y和x&#xff0c;求导的结果可以是一个高阶张量。 然而&#xff0c;虽然这些更奇特的对象确实出现在高级机器学习中&#xff…

TypeScript初体验

1.安装编译TS工具包 npm i -g typescript 2. 查看版本号 tsc -v 3.创建ts文件 说明&#xff1a;创建一个index.ts文件 4.TS编译为JS tsc index.ts 5.执行JS代码 node index.js 6.简化TS的步骤 6.1安装 npm i -g ts-node 6.2执行 ts-node index.ts

PL端案例开发手册

目 录 前 言 1 工程编译、程序加载方法 1.1 工程编译 1.2 程序加载 2 led-flash 2.1 案例说明 2.2 操作说明 2.3 关键代码 更多帮助 前 言 本文主要介绍PL端案例的使用说明&#xff0c;适用开发环境&#xff1a;Windows 7/10 64bit、Xilinx Unified 20…

SpringDataRedis 使用

1. SpringDataRedis 特点2. 使用 SpringDataRedis 步骤3. 自定义 RedisTemplate 序列化4. SpringDataRedis 操作对象 1. SpringDataRedis 特点 提供了对不同 Redis 客户端的整合&#xff08;Lettuce 和 Jedis&#xff09;提供了 RedisTemplate 统一 API 来操作 Redis支持 Redi…