700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Android-JNI开发系列《十》实践利用libjpeg-turbo完美压缩图片不失真

Android-JNI开发系列《十》实践利用libjpeg-turbo完美压缩图片不失真

时间:2021-09-18 13:30:19

相关推荐

Android-JNI开发系列《十》实践利用libjpeg-turbo完美压缩图片不失真

人间观察

步入社会后,你会发现,老人说的话都是对的。

前面讲了些Android的jni知识和bitmap的实践,接下来几篇应该都是Android中jni的一些实践。这篇我们对Android中图片在jni层利用libjpeg-turbo进行大小压缩,并且压缩后不失真,清晰度和原图基本无差别。

背景

libjpeg开源的JPEG图像库,它使用非常广泛,Android也依赖libjpeg来压缩图片,但是Android不是直接使用libjpeg,而是基于一个叫Skia的开源项目来作为的图像处理引擎,Skialibjpeg进行了良好的封装。libjpeg在压缩图像时,有一个参数叫optimize_coding,这个参数的设置直接影响图片的质量和大小。

如果设置optimize_coding为true,将会使得压缩图像过程中基于图像数据计算哈弗曼表(关于图片压缩中的哈弗曼表,可以百度下查阅相关资料),由于这个计算会显著消耗空间和时间,默认值被设置为false。采用默认哈夫曼表进行计算。optimize_codingSkia中默认值也是false。

随着时间的推移现在 Android 手机性能越来越好,Google 在Android 7.0后已经设置为true了。如下代码可以看到。

源码地址:

/androidossearch?query=SkImageDecoder_libjpeg.cpp

>=android 7.0 后的源码// ...省略其它代码// Tells libjpeg-turbo to compute optimal Huffman coding tables// for the image. This improves compression at the cost of// slower encode performance.cinfo.optimize_coding = TRUE;jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);// ...省略其它代码

也就是说在Android 7.0前中无论你怎么压缩(尺寸压缩,质量压缩,Matrix 矩阵变换)它都会导致图片质量变差,而且在app应用层是无法修改的。但是我们可以自己编译libjpeg来设置这个参数为true,既然Android 7.0 后已经是optimize_coding = TRUE;还有必要自己编译libjpeg来设置这个参数为true 不过没关系,我们就拿这个来学习jni也是挺好的。这个库也支持解压缩(有兴趣的可以研究下),接下来我们看下如何实现压缩。

在有ugc功能的app中,拍照上传图片的时候基本都会进行压缩。

但是我看了下快手,微信,抖音的apk里。 快手里有用这个压缩库,微信抖音好像并使用libjpeg,也可能是改了so的名字,也可能出于7.0后已经是true了,也可能是用的Android 系统提供的API,也可能直接上传的原图云端进行的压缩。

压缩效果对比

下图是一张2.4MB的原始图片采用30%的压缩后是632KB,4倍还是可以的。清晰度对比如下,几乎看不出来差别,但是如果压缩到10%,图片有稍微的清晰度降低,真实项目可以权衡下。说明效果远比Android 内置的好。

(图片拍摄于-11-16号北京西北旺下班回家的公交站~,留下纪念,说不定哪天就不在北京了。)

libjpeg-turbo在Android环境下的编译

开源libjpeg-turbo源码

这个确实不好编译,有点坑。。。我编译的时候在网上也百度了下,大部分的文章都是几年前的,大部分在linux环境下编译的,提供了一些脚本,但都不是Android平台下的,也不是基于libjpeg-turbo最新的代码进行,最后尝试都编不过。哭唧唧,只能自己看文档了最后折腾ok。

编译步骤大概如下

下载libjpeg-turbo源码编写脚本,结合libjpeg-turbo目录下BUILDING.md文件,使用Android ndk提供的cmake自带交叉编译工具链编译跑脚本,最后生成Android下个平台的so和需要的头文件

备注,我用的是最新的ndk 21.1.6352462(最新)编译的,它已经不支持生成armeabi平台的so了,有点奇怪。

下面是完整的编译build.sh脚本,如果你要编译需要把build.sh放在与下载后的源码命令同级下执行sh build.sh

同时把脚本的CMAKE_PATHNDK_PATH改为自己电脑的路径即可。

build.sh也放到了这个压缩demo的工程里了。

#编译参考了/p/20902ca448ae?utm_source=oschina-app# lib-nameMY_LIBS_NAME=libjpeg-turboMY_SOURCE_DIR=$(pwd)/libjpeg-turboMY_BUILD_DIR=binaryCMAKE_PATH=/Users/guxiuzhong/Library/Android/sdk/cmake/3.10.2.4988404export PATH=${CMAKE_PATH}/bin:$PATHNDK_PATH=/Users/guxiuzhong/Library/Android/sdk/ndk/21.1.6352462BUILD_PLATFORM=linux-x86_64TOOLCHAIN_VERSION=4.9ANDROID_VERSION=24ANDROID_ARMV5_CFLAGS="-march=armv5te"ANDROID_ARMV7_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon" # -mfpu=vfpv3-d16 -fexceptions -frttiANDROID_ARMV8_CFLAGS="-march=armv8-a " # -mfloat-abi=softfp -mfpu=neon -fexceptions -frttiANDROID_X86_CFLAGS="-march=i386 -mtune=intel -mssse3 -mfpmath=sse -m32"ANDROID_X86_64_CFLAGS="-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel"# params($1:arch,$2:arch_abi,$3:host,$4:compiler,$5:cflags,$6:processor)build_bin() {echo "-------------------start build $1-------------------------"ANDROID_ARCH_ABI=$1 # armeabi armeabi-v7a x86 mipsCFALGS="$2"PREFIX=$(pwd)/dist/${MY_LIBS_NAME}/${ANDROID_ARCH_ABI}/# build 中间件BUILD_DIR=./${MY_BUILD_DIR}/${MY_LIBS_NAME}/${ANDROID_ARCH_ABI}echo "path==>$PATH"echo "build_dir==>$BUILD_DIR"echo "ANDROID_ARCH_ABI==>$ANDROID_ARCH_ABI"echo "CFALGS==>$CFALGS"mkdir -p ${BUILD_DIR}cd ${BUILD_DIR}# -DCMAKE_MAKE_PROGRAM=${NDK_PATH}/prebuilt/${BUILD_PLATFORM}/bin/make \# -DCMAKE_ASM_COMPILER=${NDK_PATH}/prebuilt/${BUILD_PLATFORM}/bin/yasm \cmake -G"Unix Makefiles" \-DANDROID_ABI=${ANDROID_ARCH_ABI} \-DANDROID_PLATFORM=android-${ANDROID_VERSION} \-DCMAKE_BUILD_TYPE=Release \-DANDROID_NDK=${NDK_PATH} \-DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \-DCMAKE_POSITION_INDEPENDENT_CODE=1 \-DCMAKE_INSTALL_PREFIX=${PREFIX} \-DANDROID_ARM_NEON=TRUE \-DANDROID_TOOLCHAIN=clang \-DANDROID_STL=c++_static \-DCMAKE_C_FLAGS="${CFALGS} -Os -Wall -pipe -fPIC" \-DCMAKE_CXX_FLAGS="${CFALGS} -Os -Wall -pipe -fPIC" \-DANDROID_CPP_FEATURES=rtti exceptions \-DWITH_JPEG8=1 \${MY_SOURCE_DIR}make cleanmakemake installcd ../../../echo "-------------------$1 build end-------------------------"}# build armeabibuild_bin armeabi "$ANDROID_ARMV5_CFLAGS"#build armeabi-v7abuild_bin armeabi-v7a "$ANDROID_ARMV7_CFLAGS"#build arm64-v8abuild_bin arm64-v8a "$ANDROID_ARMV8_CFLAGS"#build x86build_bin x86 "$ANDROID_X86_CFLAGS"#build x86_64build_bin x86_64 "$ANDROID_X86_64_CFLAGS"

编译结构&编译成功后会生成Android下个平台的so和需要的头文件 如下:

压缩

编译后,把生成的so和头文件拷贝到Android工程中,同时修改CMakeLists.txt文件,指定头文件,查找so的路径,以及该jni工程生成的so需要链接的so:libjpeg.solibturbojpeg.so

怎么压缩呢? 其实很简单的,整体分如下几步。

获取图片Bitmap的像素。取出每个像素的argb通道,alpha通道丢弃,把bitmap的rgb像素转为一维数组进行保存(格式是R,G,B,R,G,B,R,G,B,…)。用libjpeg进行压缩。释放资源

获取Bitmap的像素

这个在上一篇文章有介绍,这里就不多介绍了。

Android-JNI开发系列《九》实战-Bitmap处理实现底片灰度化黑白化暖冷色调等效果

获取每个像素取出ARGB通道

只要拿到了这个就可以对图片进行任何处理了(包含上篇文章对图片的特效处理)。libjpeg-turbo这个开源库也不例外。

因为我们把它压缩为jepg格式的图片,alpha通道是可以丢弃的。有一个特别注意的点就是:在jni层中,Bitmap像素点的值是ABGR,而不是ARGB,也就是说,高端到低端:A,B,G,R。这个我们在上一篇文章也验证过。 因为libjpeg-turbo压缩的时候需要的格式是R,G,B,R,G,B,R,G,B,...也就是一维数组。 也就是把图片的二位像素转为一维数组,很简单,赋值然后指针++处理就行了。部分代码:

int i = 0, j = 0;BYTE r, g, b;//存储RGB所有像素点BYTE *data = (BYTE *) malloc(w * h * 3);// 临时保存指向像素内存的首地址BYTE *tempData = data;uint32_t color;for (i = 0; i < h; i++) {for (j = 0; j < w; j++) {// 取出一个像素 去调了alpha,然后保存到data中,对应指针++color = *((uint32_t *) pixelsColor);// 在jni层中,Bitmap像素点的值是ABGR,而不是ARGB,也就是说,高端到低端:A,B,G,Rb = ((color & 0x00FF0000) >> 16);g = ((color & 0x0000FF00) >> 8);r = ((color & 0x000000FF));// jpeg压缩需要的是rgb// for example, R,G,B,R,G,B,R,G,B,... for 24-bit RGB color.*data = r;*(data + 1) = g;*(data + 2) = b;data += 3;pixelsColor += 4;}}

简单吧, BYTE就类似java的byte,typedef uint8_t BYTE;别名,无符号8位。

用libjpeg进行压缩

这一步最关键,我们拿到了图片bitmap的原始的像素就可以做处理。怎么使用libjpeg-turbo这个开源库提供的压缩方法呢,其实你下载后在源码的目录下有一个example.txt文件,这里有很清晰的使用方法,还有详细的注释。

大概分为7步。

初始化压缩对象。jpeg_create_compress设置压缩后的数据的输出形式jpeg_stdio_dest,比如输出到文件设置压缩的参数jpeg_set_defaults。 这里最重要,也就是我们需要把optimize_coding设置为true。 因为默认是false。开始压缩jpeg_start_compress按行循环写入。jpeg_write_scanlines结束压缩。jpeg_finish_compress释放压缩对象。jpeg_destroy_compress

代码中有比较详细的注释。按照它提供的example.txt文件中的示例写就行,压缩方法如下:

int write_JPEG_file(BYTE *data, int w, int h, int quality,const char *outFilename, jboolean optimize) {//jpeg的结构体,保存的比如宽、高、位深、图片格式等信息struct jpeg_compress_struct cinfo;/* Step 1: allocate and initialize JPEG compression object *//* We set up the normal JPEG error routines, then override error_exit. */struct my_error_mgr jem;cinfo.err = jpeg_std_error(&jem.pub);jem.pub.error_exit = my_error_exit;/* Establish the setjmp return context for my_error_exit to use. */if (setjmp(jem.setjmp_buffer)) {/* If we get here, the JPEG code has signaled an error.and return.*/return -1;}jpeg_create_compress(&cinfo);/* Step 2: specify data destination (eg, a file) */FILE *outfile = fopen(outFilename, "wb");if (outfile == nullptr) {LOGE("can't open %s", outFilename);return -1;}jpeg_stdio_dest(&cinfo, outfile);/* Step 3: set parameters for compression */cinfo.image_width = w;/* image width and height, in pixels */cinfo.image_height = h;cinfo.input_components = 3; /* # of color components per pixel */cinfo.in_color_space = JCS_RGB; /* colorspace of input image */cinfo.optimize_coding = optimize;//哈夫曼编码和算术编码,TRUE=arithmetic coding, FALSE=Huffmanif (optimize) {cinfo.arith_code = false;} else {cinfo.arith_code = true;}// 其它参数 全部设置默认参数jpeg_set_defaults(&cinfo);//设置质量jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);/* Step 4: Start compressor */jpeg_start_compress(&cinfo, TRUE);/* Step 5: while (scan lines remain to be written) *//* jpeg_write_scanlines(...); */JSAMPROW row_pointer[1];int row_stride;//一行的RGB数量row_stride = cinfo.image_width * 3; /* JSAMPLEs per row in image_buffer *///一行一行遍历while (cinfo.next_scanline < cinfo.image_height) {//得到一行的首地址row_pointer[0] = &data[cinfo.next_scanline * row_stride];//此方法会将jcs.next_scanline加1jpeg_write_scanlines(&cinfo, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数}/* Step 6: Finish compression */jpeg_finish_compress(&cinfo);/* After finish_compress, we can close the output file. */fclose(outfile);outfile = nullptr;/* Step 7: release JPEG compression object *//* This is an important step since it will release a good deal of memory. */jpeg_destroy_compress(&cinfo);/* And we're done! */return 0;}

我这里是直接同步压缩的,压缩是个耗时的操作(上面的效果图测试大概360毫秒左右),你可以在jni层中开启线程,然后压缩成功/失败通过回调到java层中,当然也可以在java层开启线程,都差不多。这里demo就直接int返回了,成功0,失败-1.

压缩失败的处理,在压缩的步骤1中进行设置,在jpeg_compress_structerr字段,err字段是一个jpeg_error_mgr的结构体,该结构体描述压缩失败的信息,比如错误信息,错误码,有几个函数指针,比如error_exitemit_messageoutput_message等。如果赋值的话当压缩失败的时候会回调你的方法。

释放资源

最后记得文件该closefclose,内存该freefree即可,jni中的也该释放的释放,jpeg的用完调用jpeg_destroy_compress。避免内存泄漏问题。

最后上源码:

demo是读取sd卡的图片压缩后写到了sd卡里,记得添加读写sd卡的权限。

/ta893115871/JNIBitmapCompress

备注

本文也是为实践jni,学习jpeg压缩。其中编译libjpeg参考了网上的libjpeg-turbo的编译,有一篇不错。

/p/20902ca448ae?utm_source=oschina-app

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。