jpeg 格式与 libjpeg 库编译移植

字节流动

共 11861字,需浏览 24分钟

 · 2021-12-12

原文链接: https://juejin.cn/post/7032217239839309861

1. libjpeg介绍

libJPEG 库是一款功能强大的JPEG图像处理开源库,它支持将图像数据压缩编码为JPEG格式和对原有的JPEG图像解压缩,Android系统底层处理图片压缩就是用得libJPEG库。

但有一点需要注意的是,为了适配低版本的Android手机,Android系统在内部的压缩算法并没有采用普通的哈夫曼(Huffman)算法,因为哈夫曼算法比较占CPU,从而导致Android在压缩的同时保持较高的图像质量和色彩丰富时,压缩的性能不是很好。

基于此,本文将使用AS的Cmake工具编译libJPEG-turbo源码,然后通过JNI/NDK技术编写采样哈夫曼算法压缩接口,以提高在Android中图片压缩质量。


注:libjpeg-turbo是针对libjpeg库的一个优化版本。

1.1 哈夫曼编码

Haffman编码是Huffman于1952年提出的一种高效的无损压缩编码方法(注:压缩20%~90%),该方法的编码的码长是可变的,它完全依据字符出现概率(频率)来构造异字头的平均长度最短的码字,即对于出现概率(频率)高的信息,编码的码长较短;对于出现概率(频率)高的信息,编码的码长较长。

在图像压缩的应用场景中,Haffman编码的基本方法是先对图像数据扫描一遍,计算出各种像素出现的概率,按概率的大小指定不同长度的唯一码字,由此得到一张该图像的Haffman码表。

编码后的图像数据记录的是每个像素的码字,而码字与实际像素值的对应关系记录在码表中。

  • Haffman树

假设有n个权值{w1,w2,...,wn},构造一棵有n个叶子结点的二叉树,每个叶子结点带权为wk,每个叶子的路径长度为lk,则其中树的带权路径长度WPL=∑(wk*lk)最小的二叉树称为赫夫曼树,也称最优二叉树。举个栗子:

 该树的带权路径长度:WPL=∑(wklk) = 110+270+315+3*5=210

  • Haffman算法原理

假设需要编码的字符集为{d1,d2,...dn},各个字符在电文中出现的次数或频率集合为{w1,w2,...,wn},以d1,d2,...,dn作为叶子结点,以w1,w2,...,wn作为相应叶子结点的权值来构造一棵赫夫曼树。

规定:赫夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是Haffman编码。

举个栗子:对字符串“BADCADFEED”进行Haffman编码?

首先,计算每个字母出现的概率A 27%,B 8%,C 15%,D 15%,E 30%,F 5%;其次,构造一颗哈夫曼树(左小,右大),并将每个叶子节点的左右权值分别改为0,1; 第三,将每个字母从根节点到叶子结点所经过的路径0或1来编码; 最后,得到字符串的Haffman编码。即“BADCADFEED”的Haffman编码为“1001010010101001000111100”。

1.2 libjpeg编码与解码

1.2.1 压缩JPEG

(1) 分配和初始化JPEG压缩对象jpeg_compress_struct,并设置错误处理模块。

// JPEG压缩编码的核心结构体,位于源码jpeglib.h
// 它包含了各种压缩参数和指向工作空间的指针(JPEG库所需内存)等
struct jpeg_compress_struct cinfo;
// JPEG错误处理结构体,位于源码jpeglib.h
struct jpeg_error_mgr jerr;
// 设置错误处理对象,以防初始化失败,比如内存溢出
cinfo.err = jpeg_std_error(&jerr);
// 初始化JPEG压缩对象(结构体对象)
jpeg_create_compress(&cinfo);

(2) 指定压缩数据输出。这里假设指定一个文件路径,然后将压缩后的JPEG数据存储到该文件中。

// 打开指定路径的文件
if ((outfile = fopen(filename, "wb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
exit(1);
}
// 指定JPEG压缩数据保存位置
jpeg_stdio_dest(&cinfo, outfile);

(3) 设置压缩参数。当然,首先我们需要填充输入图像的相关信息,比如宽高、颜色空间等。

// 获取输入图像信息
cinfo.image_width = image_width; // 宽度
cinfo.image_height = image_height; // 高度
cinfo.input_components = 3; // 每个像素占的颜色分量数量
cinfo.in_color_space = JCS_RGB; // 颜色空间,RGB
// 设置压缩参数
cinfo.optimize_coding = true; // 压缩优化
cinfo.arith_code = false; // 使用哈夫曼编码
jpeg_set_defaults(&cinfo);
// 设置压缩质量,0~100
jpeg_set_quality(&cinfo, quality, true);

(4) 启动压缩引擎,并按行处理数据。由于图像数据在内存中是以字节为单位按顺序存储的,对于一张尺寸为wxh图像来说,它在内存中是按行存储的,共h行,至于每行占多少个字节由w和每个像素大小决定。

假如这里有张分辨率为640x480且颜色空间为RGB的图像(每个像素占三个分量,即R分量、G分量、B分量,每个分量占1个字节),那么,在内存中每行占640x3=1920字节,共480行,因此这张图片在内存中总共占[wx像素)xh]=[(640x3)x480]=921600字节。

// 开启压缩引擎
jpeg_start_compress(&cinfo, TRUE);

extern JSAMPLE *image_buffer; // 存储要压缩的图像数据,按R、G、B分量顺序
extern int image_height; // 图像行数
extern int image_width; // 图像列数
// 存储行起始地址
JSAMPROW row_pointer[1];
// 图像缓冲区中的物理行宽度,其中,对于RGB来说,每个像素占3个颜色分量
// 每个分量占一个字节,那么图像中一行的宽度为:(width * 3)
// 即cinfo.image_width * cinfo.input_components
int row_stride = cinfo.image_width * cinfo.input_components;
// 按行读取图像数据
// 然后进行压缩,并存储到目的地址中
while (cinfo.next_scanline < cinfo.image_height) {
// 从数据源缓存区image_buffer读取一行数据
// 并将起始地址赋值给row_pointer[0]
row_pointer[0] = &image_buffer[cinfo.next_scanline * row_stride];
// 将image_buffer中的数据写到JPEG编码器中
(void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
}

(5) 结束压缩,释放资源。

// 停止压缩
jpeg_finish_compress(&cinfo);
// 关闭文件描述符
fclose(outfile);
// 释放引擎所占资源
jpeg_destroy_compress(&cinfo);

1.2.2 解码JPEG

(1) 分配、初始化JPEG解压对象

// JPEG解压结构体
struct jpeg_decompress_struct cinfo;

// 1. 设置程序错误处理
// 这里对错误处理做了优化,对标准的error_exit方法做了处理

//
// 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);
// longjmp(myerr->setjmp_buffer, 1);
// }
struct my_error_mgr {
struct jpeg_error_mgr pub; // 错误处理结构体
jmp_buf setjmp_buffer; // 异常信息,回调给调用者
};
struct my_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr.pub); // 设置错误处理标准程序
jerr.pub.error_exit = my_error_exit; // 使用自定义的error_exit函数
if (setjmp(jerr.setjmp_buffer)) {
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return 0;
}
// 2. 初始化JPEG解压对象
jpeg_create_decompress(&cinfo);

(2) 指定数据源(待解压JPEG图像)

// 打开待解压的JPEG文件
if ((infile = fopen(filename, "rb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
return 0;
}
// 将JPEG文件指定为数据源
jpeg_stdio_src(&cinfo, infile);

(3) 读取JPEG图像文件头部参数

(void)jpeg_read_header(&cinfo, TRUE);

(4) 设置解压参数,开始解压。这里我们无需改变JPEG图像文件的头部信息,因此,不设置解压参数。

// 开始解压
(void)jpeg_start_decompress(&cinfo);

(5) 读取图像数据存储到缓存区buffer中。

// 计算图像存储在物理内存中每一行占的大小(字节)
// 即图像的宽*每个像素所占分量数
int row_stride = cinfo.output_width * cinfo.output_components;
// 分配存储解压数据的缓存区
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)
((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
// 循环读取output_height行字节数据,存储到buffer缓存区中
while (cinfo.output_scanline < cinfo.output_height) {
(void)jpeg_read_scanlines(&cinfo, buffer, 1);
// 将得到的解压数据作进一步处理
// 这里如要自己实现,如定义一个函数
// put_scanline_someplace(buffer[0], row_stride);
}

(6) 结束解压,释放资源

// 停止解压
(void)jpeg_finish_decompress(&cinfo);
// 释放内存资源
jpeg_destroy_decompress(&cinfo);
fclose(infile);

2. libjpeg编译与移植

2.1 使用Cmake编译 libJPEG-turbo 源码

(1) 新建Android工程libjpeg,并将libjpeg-turbo源码全部拷贝到src/main/cpp目录下;

(3) 修改Android工程的build.gradle,配置libjpeg-turbo的CmakeLists.txt;

android {
compileSdkVersion 28



defaultConfig {
applicationId "com.jiangdg.libjpeg"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

externalNativeBuild {
cmake {
cppFlags ""
// 配置编译的平台版本
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
}
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}


externalNativeBuild
{
cmake {
path "src\\main\\cpp\\CMakeLists.txt"//路径改为cpp文件夹下CMakeList的路径
}
// cmake {
// path file('CMakeLists.txt')
// }
}

}

(3) 编译Android项目,得到对应平台架构的libjpeg.so文件,以及jconfig.h、jconfigint.h头文件。

Github项目地址:

https://github.com/jiangdongguo/AndroidFastDevelop/tree/master/libjpeg

3.2 使用libjpeg压缩编码JPEG图像

(1) 新建Android项目HandleJpeg,拷贝头文件jconfig.h、jconfigint.h、jpeglib.h和jmorecfg.h到src/main/cpp目录,同时拷贝动态库libjpeg.so到src/main/jniLibs目录下(如果没有创建一个)。

(2) 配置CmakeList.txt,导入libjpeg.so

cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_VERBOSE_MAKEFILE on)

# 设置so输出路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/libs)

# 指定libjpeg动态库路径
set(jpeglibs "${CMAKE_SOURCE_DIR}/src/main/jniLibs")

# 导入第三方库:libjpeg.so
add_library(libjpeg SHARED IMPORTED)
set_target_properties(libjpeg PROPERTIES
IMPORTED_LOCATION "${jpeglibs}/${ANDROID_ABI}/libjpeg.so")

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -fexceptions -frtti")

# 配置、链接动态库
add_library(
jpegutil

SHARED

src/main/cpp/NativeJPEG.cpp)

# 查找NDK原生库log,android
find_library(log-lib log)
find_library(android-lib android)

# 链接所有库到jpegutil
# AndroidBitmapInfo需要库jnigraphics
target_link_libraries(jpegutil
libjpeg
jnigraphics
${log-lib}
${android-lib})

(3) 编写Java层native方法

/**
* 使用libjpeg实现JPEG编码压缩、解压
*
* @author Jiangdg
* @since 2019-08-12 09:54:00
* */

public class JPEGUtils {
static {
System.loadLibrary("jpegutil");
}

public native static int nativeCompressJPEG(Bitmap bitmap, int quality, String outPath);
}

(4) 编写native层实现

// JPEG图形编码压缩、解压
// 采用libjpeg库(libjpeg_turbo版本)实现
//
// Created by Jiangdg on 2019/8/12.
//
#include
#include
#include
#include
#include "jpeglib.h"
#include
#include
#include
#include

#define TAG "NativeJPEG"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
#define UNSUPPORT_BITMAP_FORMAT -99
#define FAILED_OPEN_OUTPATH -100
#define SUCCESS_COMPRESS 1

typedef uint8_t BYTE;

// 自定义error结构体
struct my_error_mgr {
struct jpeg_error_mgr pub;
jmp_buf setjmp_buffer;
};

int compressJPEG(BYTE *data, int width, int height, jint quality, const char *path) {
int nComponent = 3;
FILE *f = fopen(path, "wb");
if(f == NULL) {
return FAILED_OPEN_OUTPATH;
}

// 初始化JPEG对象,为其分配空间
struct my_error_mgr my_err;
struct jpeg_compress_struct jcs;
jcs.err = jpeg_std_error(&my_err.pub);
if(setjmp(my_err.setjmp_buffer)) {
return 0;
}
jpeg_create_compress(&jcs);

// 指定压缩数据源,设定压缩参数
// 使用哈夫曼算法压缩编码
jpeg_stdio_dest(&jcs, f);
jcs.image_width = width;
jcs.image_height = height;
jcs.arith_code = false; // false->哈夫曼编码
jcs.input_components = nComponent;
jcs.in_color_space = JCS_RGB;
jpeg_set_defaults(&jcs);
jcs.optimize_coding = quality; // 压缩质量 0~100
jpeg_set_quality(&jcs, quality, true);
// 开始压缩,一行一行处理
jpeg_start_compress(&jcs, true);
JSAMPROW row_point[1];
int row_stride;
row_stride = jcs.image_width * nComponent;
while(jcs.next_scanline < jcs.image_height) {
row_point[0] = &data[jcs.next_scanline * row_stride];
jpeg_write_scanlines(&jcs, row_point, 1);
}
// 结束压缩,释放资源
if(jcs.optimize_coding != 0) {
LOGI("使用哈夫曼压缩编码完成");
}
jpeg_finish_compress(&jcs);
jpeg_destroy_compress(&jcs);
fclose(f);
return SUCCESS_COMPRESS;
}

const char *jstringToString(JNIEnv *env, jstring jstr) {
char *ret;
const char * c_str = env->GetStringUTFChars(jstr, NULL);
jsize len = env->GetStringLength(jstr);
if(c_str != NULL) {
ret = (char *)malloc(len+1);
memcpy(ret, c_str, len);
ret[len] = 0;
}
env->ReleaseStringUTFChars(jstr, c_str);
return ret;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_jiangdg_natives_JPEGUtils_nativeCompressJPEG(JNIEnv *env, jclass type, jobject bitmap,
jint quality, jstring outPath_)
{
// 获取bitmap的属性信息
int ret;
int width, height, format;
int color;
BYTE r, g, b;
BYTE *pixelsColor;
BYTE *data, *tmpData;
AndroidBitmapInfo androidBitmapInfo;
const char *outPath = jstringToString(env, outPath_);
LOGI("outPath=%s", outPath);
if((ret = AndroidBitmap_getInfo(env, bitmap, &androidBitmapInfo)) < 0) {
LOGI("AndroidBitmap_getInfo failed, error=%d", ret);
return ret;
}
if((ret = AndroidBitmap_lockPixels(env, bitmap, reinterpret_cast<void **>(&pixelsColor))) < 0) {
LOGI("AndroidBitmap_lockPixels failed, error=%d", ret);
return ret;
}
width = androidBitmapInfo.width;
height = androidBitmapInfo.height;
format = androidBitmapInfo.format;
LOGI("open image:w=%d, h=%d, format=%d", width, height, format);
// 将bitmap转换为rgb数据,只处理RGBA_8888格式
// 一行一行的处理,每个像素占4个字节,包括a、r、g、b三个分量,每个分量占8位
data = (BYTE *)malloc(width * height * 3);
tmpData = data;
for(int i=0; i for(int j=0; j if(format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
color = *((int *)pixelsColor);
b = (color >> 16) & 0xFF;
g = (color >> 8) & 0xFF;
r = (color >> 0) & 0xFF;
*data = r;
*(data + 1) = g;
*(data + 2) = b;
data += 3;
// 处理下一个像素,在内存中即占4个字节
pixelsColor += 4;
} else {
return UNSUPPORT_BITMAP_FORMAT;
}
}
}
if((ret = AndroidBitmap_unlockPixels(env, bitmap)) < 0) {
LOGI("AndroidBitmap_unlockPixels failed,error=%d", ret);
return ret;
}
// 编码压缩图片
ret = compressJPEG(tmpData, width, height, quality, outPath);
free((void *)tmpData);
return ret;
}

当然,如果你需要使用本工程生成的so运用到其他项目,需要编译本项目,AS会自动在.externalNativeBuild/.../libs目录下生成libjpegtil.so文件,然后,将libjpegtil.so和libjpeg.so同时拷贝到目标工程中即可。


Github项目地址:

https://github.com/jiangdongguo/AndroidFastDevelop/tree/master/HandleJpeg


推荐:

Android FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

一文掌握 YUV 图像的基本处理

抖音传送带特效是怎么实现的?

所有你想要的图片转场效果,都在这了

面试官:如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?

我用 OpenGL ES 给小姐姐做了几个抖音滤镜

浏览 135
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报