libjpeg-turbo 是一个 JPEG 图像编解码器,它使用 SIMD 指令加速 x86、x86-64、Arm、PowerPC 和 MIPS 系统上的基本 JPEG 压缩和解压缩,以及 x86、x86-64 和 Arm 系统上的渐进式 JPEG 压缩。在这些系统上其他条件相同的情况下,libjpeg-turbo 通常比 libjpeg 快 2-6 倍。在其他类型的系统上,由于其高度优化的 Huffman 编码算法,libjpeg-turbo 仍能比 libjpeg 有显著的性能提升。在许多情况下,libjpeg-turbo 的性能可与专有的高速 JPEG 编解码器相媲美。
libjpeg-turbo 实现了传统的 libjpeg API 以及功能较弱但更直观的 TurboJPEG API。libjpeg-turbo 最初基于 libjpeg/SIMD,这是 Miyasaka Masaru 开发的一款基于 MMX 加速的 libjpeg v6b 衍生版本。TigerVNC 和 VirtualGL 项目在 2009 年对编解码器进行了多项改进,2010 年初,libjpeg-turbo 成为一个独立的项目,旨在将高速 JPEG 压缩/解压缩技术提供给更广泛的用户和开发人员。libjpeg-turbo 是 JPEG 标准的 ISO/IEC 和 ITU-T 参考实现。
一、编译
项目 Github 地址:github.com/libjpeg-tur…
当前的 release 最新版是 3.0.4,我们基于此版本进行编译。
libjpeg-turbo/BUILDING.md 给出了各类系统的编译方法,比如我们接下来以安卓系统为例编译 armv7&arm64 这两种 so 文件。
Building libjpeg-turbo for Android
Building libjpeg-turbo for Android platforms requires v13b or later of the Android NDK.
Armv7 (32-bit)
NDK r19 or later with Clang recommended
The following is a general recipe script that can be modified for your specific needs.
# Set these variables to suit your needs
NDK_PATH={full path to the NDK directory-- for example,
/opt/android/android-ndk-r16b}
TOOLCHAIN={"gcc" or "clang"-- "gcc" must be used with NDK r16b and earlier,
and "clang" must be used with NDK r17c and later}
ANDROID_VERSION={the minimum version of Android to support-- for example,
"16", "19", etc.}
cd {build_directory}
cmake -G"Unix Makefiles" \
-DANDROID_ABI=armeabi-v7a \
-DANDROID_ARM_MODE=arm \
-DANDROID_PLATFORM=android-${ANDROID_VERSION} \
-DANDROID_TOOLCHAIN=${TOOLCHAIN} \
-DCMAKE_ASM_FLAGS="--target=arm-linux-androideabi${ANDROID_VERSION}" \
-DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
[additional CMake flags] {source_directory}
make
Armv8 (64-bit)
Clang recommended
The following is a general recipe script that can be modified for your specific needs.
# Set these variables to suit your needs
NDK_PATH={full path to the NDK directory-- for example,
/opt/android/android-ndk-r16b}
TOOLCHAIN={"gcc" or "clang"-- "gcc" must be used with NDK r14b and earlier,
and "clang" must be used with NDK r17c and later}
ANDROID_VERSION={the minimum version of Android to support. "21" or later
is required for a 64-bit build.}
cd {build_directory}
cmake -G"Unix Makefiles" \
-DANDROID_ABI=arm64-v8a \
-DANDROID_ARM_MODE=arm \
-DANDROID_PLATFORM=android-${ANDROID_VERSION} \
-DANDROID_TOOLCHAIN=${TOOLCHAIN} \
-DCMAKE_ASM_FLAGS="--target=aarch64-linux-android${ANDROID_VERSION}" \
-DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
[additional CMake flags] {source_directory}
make
参考官方给出的编译说明,我们写一个 build_libjpeg_trubo.sh 在 ubuntu 18.04 上编译,重点需要配置 cmake 编译链,由于配置了 CMAKE_INSTALL_PREFIX 为 out 目录,因此编译生成文件位于 out 目录下。
build_libjpeg_trubo.sh
NDK_PATH=/home/snake/Android/android-ndk-r21e
TOOLCHAIN=clang
ANDROID_VERSION=19
cd libjpeg-turbo-3.0.4
mkdir build
cd build
mkdir armeabi-v7a
cd armeabi-v7a
echo "Build armeabi-v7a arch..."
cmake -G"Unix Makefiles" \
-DANDROID_ABI=armeabi-v7a \
-DANDROID_ARM_MODE=arm \
-DCMAKE_INSTALL_PREFIX=../../../out/armeabi-v7a \
-DENABLE_STATIC=TRUE \
-DANDROID_PLATFORM=android-${ANDROID_VERSION} \
-DANDROID_TOOLCHAIN=${TOOLCHAIN} \
-DCMAKE_ASM_FLAGS="--target=arm-linux-androideabi${ANDROID_VERSION}" \
-DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
../../
make -j4
make install
cd ../
mkdir arm64-v8a
cd arm64-v8a
echo "Build arm64-v8a arch..."
cmake -G"Unix Makefiles" \
-DANDROID_ABI=arm64-v8a \
-DANDROID_ARM_MODE=arm \
-DCMAKE_INSTALL_PREFIX=../../../out/arm64-v8a \
-DENABLE_STATIC=TRUE \
-DANDROID_PLATFORM=android-${ANDROID_VERSION} \
-DANDROID_TOOLCHAIN=${TOOLCHAIN} \
-DCMAKE_ASM_FLAGS="--target=aarch64-linux-android${ANDROID_VERSION}" \
-DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
../../
make -j4
make install
二、使用
以下代码使用了更直观的 TurboJPEG API,网上这块资料非常少,另外官方 demo 也比较精简,经过摸索 I420 格式的 YUV 是可以正常编码为 JPEG 的,同时将 JPG 转化为了 RGB 数组。
JpegHandler.h
#ifndef JPEGHANDLER_H
#define JPEGHANDLER_H
#include <turbojpeg.h>
static const int HANDLE_JPEG_SUCC = 0;
static const int HANDLE_JPEG_FAIL = -1;
static const int COMPRESS_JPEG_ERR_NONSUPPORT_YUV_TYPE = -100;
static const int COMPRESS_JPEG_ERR_YUV_BUF_SIZE = -101;
static const int COMPRESS_JPEG_ERR_COMPRESS = -101;
static const int DECOMPRESS_JPEG_ERR_HEADER = -200;
static const int DECOMPRESS_JPEG_ERR_DECOMPRESS = -201;
enum YUV_T {
I420 = 1,
NV12 = 2,
NV21 = 3,
};
class JpegHandler {
public:
JpegHandler();
~JpegHandler();
int compressYUV2Jpeg(const unsigned char *srcYuv, int srcYuvSize, int width, int height,
YUV_T yuvT, unsigned char **jpegBuf, unsigned long *jpegSize);
int decompressJpeg2RGB(const unsigned char *jpegBuf, size_t jpegSize,
unsigned char *dstBuf, int *dstWidth, int *dstHeight);
private:
tjhandle mCompressTjInstance;
tjhandle mDecompressTjInstance;
};
#endif //JPEGHANDLER_H
以下是 cpp 实现文件,实现了头文件中定义的 compressYUV2Jpeg 压缩 YUV 为 JPEG 函数,和 decompressJpeg2RGB 解压 JPEG 为 RGB 的函数。同时构造函数中调用 tj3Init 初始化了 tjhandle 类型的压缩和解压缩实例,析构函数则调用销毁函数 tj3Destroy 进行“善后”处理。
JpegHandler.cpp
#include <cstring>
#include "JpegHandler.h"
#include "../logger.h"
static const int YUV_PLANES = 3;
int
JpegHandler::compressYUV2Jpeg(const unsigned char *srcYuv, int srcYuvSize, int width, int height,
YUV_T yuvT, unsigned char **jpegBuf, unsigned long *jpegSize) {
int subSamp = TJSAMP_420;
switch (yuvT) {
case I420:
case NV12:
case NV21:
subSamp = TJSAMP_420;
break;
default:
return COMPRESS_JPEG_ERR_NONSUPPORT_YUV_TYPE;
}
int align = 1;
int needSize = tj3YUVBufSize(width, align, height, subSamp);
if (needSize != srcYuvSize) {
LOGE("we detect yuv size: %d, but you give: %d, check again.", needSize, srcYuvSize);
return COMPRESS_JPEG_ERR_YUV_BUF_SIZE;
}
int jpegQuality = 90;
if (tj3Set(mCompressTjInstance, TJPARAM_QUALITY, jpegQuality) == HANDLE_JPEG_FAIL) {
LOGE("set TJPARAM_QUALITY err");
}
if (tj3Set(mCompressTjInstance, TJPARAM_SUBSAMP, subSamp) == HANDLE_JPEG_FAIL) {
LOGE("set TJPARAM_SUBSAMP err");
}
int yLen = width * height;
int uLen = yLen / 4;
const unsigned char *const srcPlanes[YUV_PLANES] = {srcYuv,
srcYuv + yLen,
srcYuv + yLen + uLen};
int halfWidth = width / 2;
const int strides[YUV_PLANES] = {width, halfWidth, halfWidth};
if (tj3CompressFromYUVPlanes8(mCompressTjInstance,
reinterpret_cast<const unsigned char *const *>(&srcPlanes),
width,
reinterpret_cast<const int *>(&strides),
height,
jpegBuf,
reinterpret_cast<size_t *>(jpegSize)) == HANDLE_JPEG_FAIL) {
int errCode = tj3GetErrorCode(mCompressTjInstance);
char *errStr = tj3GetErrorStr(mCompressTjInstance);
LOGE("Failed to compress YUV. %s(errCode=%d)", errStr, errCode);
return COMPRESS_JPEG_ERR_COMPRESS;
}
return HANDLE_JPEG_SUCC;
}
JpegHandler::JpegHandler() {
if ((mCompressTjInstance = tj3Init(TJINIT_COMPRESS)) == nullptr) {
LOGE("Failed to create compress turbojpeg instance.");
}
if ((mDecompressTjInstance = tj3Init(TJINIT_DECOMPRESS)) == nullptr) {
LOGE("Failed to create decompress turbojpeg instance.");
}
}
JpegHandler::~JpegHandler() {
if (!mCompressTjInstance) {
tj3Destroy(mCompressTjInstance);
mCompressTjInstance = nullptr;
}
if (!mDecompressTjInstance) {
tj3Destroy(mDecompressTjInstance);
mDecompressTjInstance = nullptr;
}
}
int JpegHandler::decompressJpeg2RGB(const unsigned char *jpegBuf, size_t jpegSize,
unsigned char *dstBuf, int *dstWidth, int *dstHeight) {
if (tj3DecompressHeader(mDecompressTjInstance, jpegBuf, jpegSize) == HANDLE_JPEG_FAIL) {
LOGE("decompress JPEG Header err.");
return DECOMPRESS_JPEG_ERR_HEADER;
}
int jpegWidth = tj3Get(mDecompressTjInstance, TJPARAM_JPEGWIDTH);
if (jpegWidth == HANDLE_JPEG_FAIL) {
LOGE("get TJPARAM_JPEGWIDTH err");
}
*dstWidth = jpegWidth;
int jpegHeight = tj3Get(mDecompressTjInstance, TJPARAM_JPEGHEIGHT);
if (jpegHeight == HANDLE_JPEG_FAIL) {
LOGE("get TJPARAM_JPEGHEIGHT err");
}
*dstHeight = jpegHeight;
if (tj3Decompress8(mDecompressTjInstance, jpegBuf, jpegSize,
dstBuf, 0, TJCS_RGB) ==
HANDLE_JPEG_FAIL) {
int errCode = tj3GetErrorCode(mDecompressTjInstance);
char *errStr = tj3GetErrorStr(mDecompressTjInstance);
LOGE("Failed to decompress JPEG. %s(errCode=%d)", errStr, errCode);
return DECOMPRESS_JPEG_ERR_DECOMPRESS;
}
return HANDLE_JPEG_SUCC;
}
其中 compressYUV2Jpeg 函数内,首先调用 tj3YUVBufSize 计算出 YUV 需要的 buffer 大小,接着调用 tj3Set 设置 jpeg Quality(程序中固定为 90),继续调用 tj3Set 设置 YUV 的采样格式,这些参数是调用压缩函数必须配置的。经过前面的两组参数配置,现在就可以调用 tj3CompressFromYUVPlanes8 真正做压缩了,压缩出现异常时可调用 tj3GetErrorCode 和 tj3GetErrorStr 获取相应的错误码和描述。
小技巧:通过错误码和错误描述,一步步得知如何使用此压缩函数正常工作,也是一种调试、摸索使用的手段。
decompressJpeg2RGB 也是类似的,首先调用 tj3DecompressHeader 获取 JPEG 必要的一些参数,比如图像的宽、高等。调用 tj3DecompressHeader 成功后就可以调用 tj3Get 获取各种 jpeg 的参数了,当然包括宽高,此处获取了宽高。最后调用 tj3Decompress8 将 JPEG 转化为了 RGB,当然入参最后一个参数配置为了 TJCS_RGB。
三、实测性能
本人使用华为 Mate60 作为测试设备,进行了几十次测试(压缩和解压基本上都是 10ms 左右可完成),图片分辨率为 1920*1080,将程序打包成 release,压缩和解压缩的速度都是非常快的, debug 版本要慢上不少。
2024-11-22 08:17:59.987 19472-19472/xxx D/MainActivity: compressHandlerJni compress begin
2024-11-22 08:17:59.996 19472-19472/xxx D/MainActivity: compressHandlerJni compress end, jpegLen=291778
2024-11-22 08:17:59.996 19472-19472/xxx D/MainActivity: compressHandlerJni decompress begin
2024-11-22 08:18:00.007 19472-19472/xxx D/MainActivity: compressHandlerJni decompress end, image width=1920, height=1080
2024-11-22 08:18:01.541 19472-19472/xxx D/MainActivity: compressHandlerJni compress begin
2024-11-22 08:18:01.554 19472-19472/xxx D/MainActivity: compressHandlerJni compress end, jpegLen=278227
2024-11-22 08:18:01.554 19472-19472/xxx D/MainActivity: compressHandlerJni decompress begin
2024-11-22 08:18:01.564 19472-19472/xxx D/MainActivity: compressHandlerJni decompress end, image width=1920, height=1080
2024-11-22 08:18:02.407 19472-19472/xxx D/MainActivity: compressHandlerJni compress begin
2024-11-22 08:18:02.419 19472-19472/xxx D/MainActivity: compressHandlerJni compress end, jpegLen=278319
2024-11-22 08:18:02.419 19472-19472/xxx D/MainActivity: compressHandlerJni decompress begin
2024-11-22 08:18:02.429 19472-19472/xxx D/MainActivity: compressHandlerJni decompress end, image width=1920, height=1080
2024-11-22 08:18:32.356 19472-19472/xxx D/MainActivity: compressHandlerJni compress begin
2024-11-22 08:18:32.370 19472-19472/xxx D/MainActivity: compressHandlerJni compress end, jpegLen=337098
2024-11-22 08:18:32.370 19472-19472/xxx D/MainActivity: compressHandlerJni decompress begin
2024-11-22 08:18:32.381 19472-19472/xxx D/MainActivity: compressHandlerJni decompress end, image width=1920, height=1080
2024-11-22 08:18:34.704 19472-19472/xxx D/MainActivity: compressHandlerJni compress begin
2024-11-22 08:18:34.717 19472-19472/xxx D/MainActivity: compressHandlerJni compress end, jpegLen=338297
2024-11-22 08:18:34.717 19472-19472/xxx D/MainActivity: compressHandlerJni decompress begin
2024-11-22 08:18:34.727 19472-19472/xxx D/MainActivity: compressHandlerJni decompress end, image width=1920, height=1080