JNI方法实现图片压缩(压缩率极高)

前言

直接使用项目或直接复制libs中的so库到项目中即可(当前只构建了armeabi),需要其他ABI可检下项目另外使用CMake构建即可。

结果预览:


效果图.png

jni_278KB.png

quality_484KB.png

sample_199KB.png

size_238KB.png

原图大小5.99M~~ 我们把所有经过压缩的图片放到同等大小的情况后,很明显,采样压缩跟尺寸压缩都不是我们想要的结果,而质量压缩跟JNI压缩我设置的质量压缩值都是30,JNI压缩出来只有278KB,直接质量压缩出来的有484KB,综合之后,JNI才是综合最优的方式,当然,如果只是头像,我们设置可以把配置值设置得更小,图片就更小。

为什么iPhone手机图片的质量比Android的好?

首先了解两个图像处理库:libjpeg、Skia。

Skia:图像处理引擎,Google在Android系统上就是采用Skia,它是基于libjpeg的二次封装,Google在很多其它产品也使用了这个库,比如Chorme,Firefox等等。

libjpeg:早期的图像处理引擎,用于PC端。

官方文档可以看到libjpeg.doc这样一段话:

boolean optimize_coding
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.

boolean optimize_coding:

  • 参数为TRUE时,图片压缩算法使用最优的哈夫曼编码表,它需要额外传递数据,因此会耗费CPU运算时间,以及开辟很多临时内存空间。
  • 参数为FALSE时,使用默认的哈夫曼编码表。在大多数情况,使用最优哈夫曼编码表相比默认哈夫曼编码表,能节省图像文件很大比例的大小。

为什么使用最优哈夫曼编码表可以节省图像文件很大的比例大小呢?

哈夫曼树和哈夫曼编码

当树中的节点被赋予一个表示某种意义的数值,我们称之为该节点的权。从树的根节点到任意节点的路径长度(经过的边数)与该节点上权值的乘积称为该节点的带权路径长度。树中所有叶节点的带权路径长度之和称为该树的带权路径长度(WPL)。当带权路径长度最小的二叉树被称为哈夫曼树,也成为最优二叉树。

如下图所示,有三课二叉树,每个树都有四个叶子节点a,b,c,d,分别取带权7,5,2,4。他们的带权路径长度分别为

(a) WPL = 7x2+5x2+2x2+4x2=36

(b) WPL = 2X1+4X2+7X3+5X3 = 46

(c) WPL = 7x1+5x2+2x3+4x3 = 35

节点如果像c中的方式分布的话,WPL能取最小值(可证明),我们称为哈夫曼树。

哈夫曼树构造

哈夫曼树在构造时每次从备选节点中挑出两个权值最小的节点进行构造,每次构造完成后会生成新的节点,将构造的节点从备选节点中删除并将新产生的节点加入到备选节点中。新产生的节点权值为参与构造的两个节点权值之和。举例如下:

  • 备选节点为a,b,c,d,权值分别为7,5,2,4
  • 选出c和d进行构造(权值最小),生成新节点为e(权值为6),备选节点变为7,5,6
  • 选出b和e进行构造,生成新节点f(权值为11),备选节点为7,11
  • 将最后的7和11节点进行构造,最后生成如图所示的哈夫曼树

哈夫曼树应用

在处理字符串序列时,如果对每个字符串采用相同的二进制位来表示,则称这种编码方式为定长编码。若允许对不同的字符采用不等长的二进制位进行表示,那么这种方式称为可变长编码。可变长编码其特点是对使用频率高的字符采用短编码,而对使用频率低的字符则采用长编码的方式。这样我们就可以减少数据的存储空间,从而起到压缩数据的效果。而通过哈夫曼树形成的哈夫曼编码是一种的有效的数据压缩编码。

如果没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。如0,101和100是前缀编码。由前缀码形成的序列可以被唯一的组成一个字符串序列。如00101100可以被唯一的分析为0,0,101和100。

示例:

我们对一个字符串进行统计发现a-f出现的频率分别为a:45,b:13,c:12,d:16,e:9,f:5,我们对该字符串进行采用哈夫曼编码进行存储。

WPL = 1x45+3x(13+12+16)+4x(5+9)=224

这样算下来使用224二进制位就可以将该字符串存储起来,因为哈夫曼码是前缀码,所以可以唯一的还原出原来的字符序列。如果我们每个字符使用3位进行存储(至少3位),那么需要300bit才能将该字符串存储下。

其次了解下libjpeg使用哈夫曼编码是对图片上的每个像素(ARGB)进行编码,比如

ARGB(每个颜色通道取值范围0-255)的编码分别是:

A:001
R:010
G:011
B:100

如果采用定长,那一个图片下来一个人像素就是001010011100...

如果我们使用可变长编码方式,遍历、再嵌套遍历,再嵌套遍历每一个像素来获取前缀编码(没错,这个运算过程很大,而且临时变量内存也需要很大,但相对于今天的手机CPU来说,so easy),我们大致可以得到这样的编码:

A:01
R:10
G:11
B:100

那一个图片下来一个人像素就是011011100...

编码长度的优化后,接下来干的事就是计算权重以及每个颜色通道对应的编码的出现频次构建哈夫曼树了,这里就参考上面的图片了。

通过上面的介绍,可以知道最优哈夫曼编码其实是使用了可变长编码方式,而默认的哈夫曼编码使用了定长编码方式,因此需要更多的存储空间,呈现出来的手机图片自然会大很大。

libjpeg把optimize_coding参数默认设置为FALSE是因为10多年前的Android手机CPU跟内存都非常吃紧,所以当年没有设置为TRUE。如今的手机CPU跟内存都“起飞了”,Goolge的Skia图像处理引擎却还是使用optimize_coding的默认值FALSE。我们无法修改系统Skia的这个参数值,所以只能默默忍受size很大的图像文件。

经过大量图像压缩测试结果,得到两个结论:

1.图片压缩到相同的质量,FALSE所产出的图像文件大小是TRUE的5-10倍。

2.图片压缩到相同的质量,Android所产出的图像文件大小比iOS也是大5-10倍。

所以,通过使用libjpeg编译自己的native library修改optimize_coding参数的值,达图像质量相同,所产出的图像却能节省5-10倍空间大小的效果。

实现的步骤:

1.构建libjpeg的so库

到官方下载对应自己电脑系统类型的压缩包,创建Android项目导入压缩包里头的xx.h、xx.c文件构建so库。bither/bither-android-lib已经做了这个工作,因此我们只需直接拿他的libjpegbither.so即可。

2.导入libjpeg的声明头文件,因为步骤1的libjpegbither.so是对这些头文件的实现,因此需要导入这些头文件。

3.创建CMake脚本

cmake_minimum_required(VERSION 3.4.1)

add_library( effective-bitmap
SHARED
src/main/cpp/effective-bitmap.c )

include_directories( src/main/cpp/jpeg/
)

add_library(jpegbither SHARED IMPORTED)
set_target_properties(jpegbither
PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libjpegbither.so)

find_library( log-lib
log )

find_library( jnigraphics-lib jnigraphics )

target_link_libraries( effective-bitmap
jpegbither
${log-lib}
${jnigraphics-lib})

4.配置gradle关联CMakeLists.txt构建脚本,以及指定产出的ABI的类型

android {
// ...
defaultConfig {
// ...
externalNativeBuild {
cmake {
cppFlags ""
}
}

ndk {
abiFilters 'armeabi'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}

5.编写C/C++代码

int generateJPEG(BYTE* data, int w, int h, int quality,
const char* outfilename, jboolean optimize) {
int nComponent = 3;
// jpeg的结构体,保存的比如宽、高、位深、图片格式等信息
struct jpeg_compress_struct jcs;

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;
}
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;

// 设置哈夫曼编码
jcs.arith_code = false;
jcs.input_components = nComponent;
if (nComponent == 1)
jcs.in_color_space = JCS_GRAYSCALE;
else
jcs.in_color_space = JCS_RGB;

jpeg_set_defaults(&jcs);
jcs.optimize_coding = optimize;
jpeg_set_quality(&jcs, quality, true);
// 开始压缩,写入全部像素
jpeg_start_compress(&jcs, TRUE);

JSAMPROW row_pointer[1];
int row_stride;
row_stride = jcs.image_width * nComponent;
while (jcs.next_scanline < jcs.image_height) {
row_pointer[0] = &data[jcs.next_scanline * row_stride];
jpeg_write_scanlines(&jcs, row_pointer, 1);
}

jpeg_finish_compress(&jcs);
jpeg_destroy_compress(&jcs);
fclose(f);

return 1;
}

6.构建so库

源码:https://github.com/zengfw/EffectiveBitmap (本地下载)

参考链接:

Why the image quality of iPhone is much better than Android?

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • Java进阶:JNI使用技巧点滴

    文章来源:csdn 作者:normalnotebook 摘要 本文为在 32 位 Windows 平台上实现 Java 本地方法提供了实用的示例.步骤和准则.本文中的示例使用 Sun Microsystems 公司创建的 Java Development Kit (JDK) 版本 1.4.1.用 C 语言编写的本地代码是用 Microsoft Visual C++ 编译器编译生成. 简介 近日,由于项目需要,要在WEB页面实现图像转换功能,而VC在图像转换方面有着得天独厚的优势.我们首先用VC封

  • Android拍照得到全尺寸图片并进行压缩

    废话不多说了,直接给大家贴代码了,具体代码如下所示: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <

  • Android中3种图片压缩处理方法

    Android中图片的存在形式: 1:文件形式:二进制形式存在与硬盘中. 2:流的形式:二进制形式存在与内存中. 3:Bitmap的形式 三种形式的区别: 文件形式和流的形式:对图片体积大小并没有影响.也就是说,如果你手机SD卡上的图片通过流的形式读到内存中,在内存中的大小也是原图的大小. 注意:不是Bitmap的形式. Bitmap的形式:图片占用的内存会瞬间变大. 以下是代码的形式: /** * 图片压缩的方法总结 */ /* * 图片压缩的方法01:质量压缩方法 */ private Bi

  • Android图片压缩上传之基础篇

    在android程序开发中我们经常见到需要上传图片的场景,在这里有个技术点,需要把图片压缩处理,然后再进行上传.这样可以减少流量的消耗,提高图片的上传速度等问题. 关于android如何压缩,网上的资料也是很多,但大多数都是代码片段,讲解压缩步骤,而没有一个实用的工具类库.那么如何将压缩算法封装成一个实用工具库呢?其中会遇到些什么问题,比如: 1.需要压缩的图片有多少 2.压缩后的图片是覆盖还是保存到另外的目录 3.如果是另存目录需要将原始图片删除吗 4.如果改变压缩后的图片的尺寸大小是按照原图

  • 安卓应用开发通过java调用c++ jni的图文使用方法

    首先建议一个工程 HelloJni如下图: 按照默认的配置下一步,直到完成 . 如下图操作,点击windows菜单->Prefrence菜单: 弹出如下图:选择Andriod ->NDK: 配置完成以后,点击工程属性菜单: 做这一步的目的是,增加对c++代码的支持,他会自动生成一些东西,你会看到多一个jni的文件夹. 这个名字可以默认,就用工程的名字,实际上就是产生的c++代码生成.so文件的名称(windows上的dll文件). 完成以后.可以看代码,生成一个HelloJni的.cpp文件.

  • android图片压缩的3种方法实例

    android 图片压缩方法: 第一:质量压缩法: 复制代码 代码如下: private Bitmap compressImage(Bitmap image) { ByteArrayOutputStream baos = new ByteArrayOutputStream();        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中        int op

  • Android图片压缩以及优化实例

    前言 图片压缩在Android技术中已经属于烂大街,上周看了2个开源库然后对自己项目的压缩做了对比,发现一些新东西,记录与此. 为何要压缩 1.体积的原因 如果你的图片是要准备上传的,那动辄几M的大小肯定不行的,况且图片分辨率大于设备分辨率的话毫无意义. 2.内存原因 如果图片要显示下Android设备上,ImageView最终是要加载Bitmap对象的,就要考虑单个Bitmap对象的内存占用了,如何计算一张图片的加载到内存的占用呢?其实就是所有像素的内存占用总和: bitmap内存大小 = 图

  • Android图片压缩几种方式总结

    Android图片压缩几种方式总结 图片压缩在Android开发中很常见也很重要,防止图片的OOM也是压缩的重要原因. 首先看下Bitmap图片文件的大小的决定因素: Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数.3个参数,任意减少一个的值,就达到了压缩的效果. 接下来看下Bitmap图片的几种格式的特点: ALPHA_8  表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度  ARGB_4444 表示16位ARGB位图,即A=4

  • Java的JNI快速入门教程(推荐)

    1. JNI简介 JNI是Java Native Interface的英文缩写,意为Java本地接口. 问题来源:由于Java编写底层的应用较难实现,在一些实时性要求非常高的部分Java较难胜任(实时性要求高的地方目前还未涉及,实时性这类话题有待考究). 解决办法:Java使用JNI可以调用现有的本地库(C/C++开发任何和系统相关的程序和类库),极大地灵活Java的开发. 2. JNI快速学习教程 2.1 问题: 使用JNI写一段代码,实现string_Java_Test_helloworld

  • 基于jni调用时,jvm报错问题的深入分析

    执行如下的jni调用: 复制代码 代码如下: package jni;public class JNITransObject { public native TestJNI[] ObjectMethod(String text); static {  System.loadLibrary("JNITransObject"); } public static void main(String args[]) {  JNITransObject jniTransObject = new J

随机推荐