惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
A
About on SuperTechFans
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Blog — PlanetScale
Blog — PlanetScale
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
G
Google Developers Blog
J
Java Code Geeks
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
Cloudbric
Cloudbric
L
LINUX DO - 最新话题
MyScale Blog
MyScale Blog
H
Heimdal Security Blog
PCI Perspectives
PCI Perspectives
Attack and Defense Labs
Attack and Defense Labs
S
Security @ Cisco Blogs
Latest news
Latest news
I
Intezer
L
Lohrmann on Cybersecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
月光博客
月光博客
T
Threatpost
博客园 - 【当耐特】
S
Schneier on Security
P
Privacy International News Feed
G
GRAHAM CLULEY
T
Tenable Blog
AWS News Blog
AWS News Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
博客园 - Franky
Engineering at Meta
Engineering at Meta
美团技术团队
S
Secure Thoughts
T
Troy Hunt's Blog
Microsoft Security Blog
Microsoft Security Blog
SecWiki News
SecWiki News
V
Visual Studio Blog
人人都是产品经理
人人都是产品经理
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Martin Fowler
Martin Fowler
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
H
Hackread – Cybersecurity News, Data Breaches, AI and More

音视频开发进阶

音视频教程-第三节 音视频教程-第二节 真的,AI 可能就是新时代的信息差 充值 Cursor 之后,工作有了哪些变化?🤔 个人'蒸馏'大模型能做哪些有意思的事情 DeepSeek 大模型在 Mac 上的部署和运行 音视频教程-第一节 【WebRTC 专栏】-- Android 开发集成 WebRTC 库的几种方式 【WebRTC 专栏】-- 在 Mac M1 等系列芯片编译和开发 WebRTC-Android 库 Meta Llama3 大模型在 Mac 上的部署和运行 iOS VideoToolBox 解码 HEVC Open-GOP 视频的问题排查 Flutter 状态管理之 InheritedWidget 使用和分析 用 ChatGPT 回答技术问题怎么样 ? 音视频开发系统入门大致路线 UE 4.27 添加自定义 ShadingModel 用 UE4 虚幻引擎做个捏脸小功能~~ UE4 材质练习 之 凹凸贴图偏移的使用 UE4 材质练习系列基础 OpenGL上下文创建以及共享机制 007 | 播放器系列专栏-解析 MP4 文件读取信息 006 | 播放器系列专栏-在 Mac 上查看 MP4 格式信息 干货 | 快速抽取缩略图是怎么练成的? 关于直播、WebRTC、FFmpeg 的那些事 005 | 播放器系列专栏-在 Windows 上查看 MP4 格式信息 将音视频中的花屏、绿屏、黑屏问题一网打尽 关于音视频里面的解码帧率和渲染帧率 004 | 播放器系列专栏-认识MP4视频(下) 003 | 播放器系列专栏-认识MP4视频(上) 入门或者转行音视频,应该要怎么做? H264视频文件如何缩放分辨率 002 | 播放器系列专栏-FFmpeg依赖库的配置 001 | 播放器系列专栏-关于播放器项目的一个小实践 Seek策略以及在有B帧情况下的处理 目前流媒体开发工程师工作内容主要是什么? 一个音视频领域专业问答的小圈子 干货收藏 || Vulkan Game Engine 视频教程 音视频春节假期内卷指南(实操) Vulkan 在 FFmpeg 中的支持 Windows 下 FFmpeg 和 LibX264 的编译和配置 Metal 开发 | 使用 C++ 进行接口调用 音视频开发工作经验分享 || 视频版 FFmpeg 调用 MediaCodec 硬解码到 Surface 上 代码吸猫 | 用 OpenGL 图像渲染的养猫计划 百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!! 老生常谈-FFmpeg 的编译问题轻松搞定 FFmpeg 调用 Android MediaCodec 进行硬解码(附源码) 【WebRTC 专栏】--创建相机预览 Unity Shader 光照基础之 Half Lambert 光照模型 Unity Shader 光照基础之Lambert光照模型 Unity Shader 光照基础内容 Unity Shader 显示一张图片纹理 UnityShader 的基本概念 Unity 物体的基本操作 C++ 模板系列小结07-尾置返回类型 C++ 模板系列小结06-可变参数模板特性 C++ 中的多线程的使用和线程池建设 C++ 模板系列小结05-模板类型作为模板参数 C++ 模板系列小结04-类模板中的成员模板 C++ 模板系列小结03-在模板中指定变量类型 C++ 模板系列小结02-非类型模板参数 C++ 模板系列小结01-函数模板和类模板 从零打造渲染引擎系列01-什么是渲染引擎 iOS开发 - 在 Swift 中去调用 C/C++ 代码 2021 技术新番 - 从零打造渲染引擎系列 iOS 音视频开发的一些基础准备工作 音视频交流群又来啦~~~ 【WebRTC 专栏】WebRTC & Android 开发学习环境搭建~ 【喜大普奔】域名终于备案通过啦 Shader 优化 | OpenGL 绘制网格效果 【音视频连载-011】第二季 FFmpeg 一层一层获取文件信息 KodeLife | Shader 实时编辑预览的强大工具使用实践 推荐几个堪称教科书级别的 Android 音视频入门项目 【音视频连载-010】第二季 FFmpeg 日志打印 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下) 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上) 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示 【音视频连载-004】基础学习篇-SDL 加载图片并显示 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色 【音视频连载-001】基础学习篇- SDL 介绍以及工程配置 LearnOpenGL 源码在 MAC 上的编译与调试 2019 年终总结与回顾 Android NDK 开发的免费技术视频来啦~~ OpenGL 实现视频编辑中的转场效果 OpenGL 实践之贝塞尔曲线绘制 图像库 libpng 编译与实践 rust 开发编译 Android 动态库实践 Android NDK 开发 —— 从 Assets 文件夹加载图片并上传纹理 简单易用的图像解码库介绍 —— stb_image 博客图床迁移记 进击的 Vulkan 移动开发之 SwapChain 进击的 Vulkan 移动开发之 Command Buffer 进击的 Vulkan 移动开发之 Instance & Device & Queue 进击的 Vulkan 移动开发(一)之今生前世 Java 显式锁 Lock 与条件队列 C++ 标准容器库小结 MediaCodec 硬编码之相机内容编码成 H264 文件 一文读懂 YUV 的采样与格式 《OpenGL ES 3.x 游戏开发》碰撞检测之 AABB 包围盒
图像库 libjpeg-turbo 编译与实践
音视频开发进阶 · 2019-07-12 · via 音视频开发进阶

一个专注音视频领域的小圈子

避免图片丢失,建议阅读微信原文:

https://mp.weixin.qq.com/s/x-dTLowXcqysnLxZ0BGA2g

在之前的文章中已经陆续介绍了 stb_image、libpng 的使用,相关链接如下:

简单易用的图像解码库介绍 —— stb_image

https://glumes.com/post/android/stb-image-introduce/

图像库 libpng 编译与实践

https://glumes.com/post/opengl/libpng-compile-and-practice/

而今天的主题就是 libjpeg-turbo 。

它的官网地址如下:

https://libjpeg-turbo.org/

它的 github 地址如下:

https://github.com/libjpeg-turbo/libjpeg-turbo

编译

在 libjpeg-turbo 的源码中就已经有了讲述如何编译的 BUILDING.md 文件,还是使用 CMake 进行编译,大体方法和参数设置都大同小异了。

参考源码给出的编译代码:

# 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

由于 CMake 跨平台编译的特性,在进行交叉编译时要设置很多相关参数,比如编译的目标系统平台、交叉编译工具链、NDK 目录等。

详细的编译脚本可以参考项目中的:

https://github.com/glumes/InstantGLSL/tree/master/libjpeg_turbo_source/build_script

该目录下给出了编译 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64 等平台的编译脚本。修改一下相应文件路径,就能在 MAC 系统上编译 so 了。

另外如果是在 Android Studio 中用 CMake 编译 so,你会发现很少要设置那些参数,这是因为 Android Studio 中的 CMake 默认就设置好了那些参数。

因此还有一种更简单的方式进行编译,直接将 libjpeg-turbo 源码内容复制到 Android Studio 工程目录的 cpp 文件夹下,然后把 app 的 build.gradle 中 cmake path 改成 libjpeg-turbo 的 CMakeLists.txt 路径,如下所示:

    externalNativeBuild {
        cmake {
            path "src/main/cpp/libjpeg_turbo_source/CMakeLists.txt"
//            path "CMakeLists.txt"
        }
    }

然后直接编译,在 build/intermediates/cmake 路径下依旧可以找到编译好的 so 文件。

以上两种方式都可以实现 libjpeg-turbo 的编译,看个人喜好了。而且这种库一旦编译好了,以后也很少去更改,一劳永逸~~~

实践

在 libjpeg-turbo 的源码中有个 example.txt 文件,详细讲述了如何利用该库进行图片压缩和解压缩。

基本上照着文件内容看一遍就懂了,在这里还会大概讲述下,并且会用另一个实例来演示,也就是之前常用的,获取 jpeg 图像文件像素内容并且上传纹理。

压缩

在 Android 中通过 Java 方法也可以实现 Jpeg 的文件,因为底层就是基于 libjpeg 的。而 libjpeg-turbo 的压缩速度会比 Android 原生的速度更快了。

Android 中 Jpeg 文件压缩的方法如下:

compress(CompressFormat format, int quality, OutputStream stream)

其中重要的参数就是 quality ,代表要压缩的质量,而在 libjpeg-turbo 也会有这样的参数要设置。

libjpeg-turbo 的使用逻辑和 libpng 有点类似,首先都是要设置一个错误返回点,并且有一个结构体来存储信息。

在 libjpeg-turbo 进行压缩时,用到的结构体是 jpeg_compress_struct ,解压则是 jpeg_decompress_struct,两者名字上有着单词的不同。

而在 libpng 中,创建结构体的方法 png_create_write_structpng_create_read_struct 相配对,一个 write,一个 read 。

使用 libjpeg-turbo 的主要步骤如下:

  1. 设置压缩后的输出方式,可以的是文件的形式,也可以是内存数据格式
  2. 配置压缩的相关设置项,比如压缩后的图像宽高、压缩质量等
  3. 进行压缩,逐行读取数据源像素内容
  4. 压缩结束,得到压缩后的数据

对应到代码的逻辑如下:

    struct jpeg_compress_struct jpegCompressStruct;
    // 创建代表压缩的结构体
    jpeg_create_compress(&jpegCompressStruct);
    
    // 文件方式输出 还有一种是内存方式
    jpeg_stdio_dest(&jpegCompressStruct, fp);
    // 设置压缩的相关参数信息
    jpegCompressStruct.image_width = w;
    jpegCompressStruct.image_height = h;
    jpegCompressStruct.arith_code = false;
    jpegCompressStruct.input_components = nComponent;
    // 设置解压的颜色
    jpegCompressStruct.in_color_space = JCS_RGB;
    jpeg_set_defaults(&jpegCompressStruct);
    // 压缩的质量
    jpegCompressStruct.optimize_coding = quality;
    jpeg_set_quality(&jpegCompressStruct, quality, true);

首先是创建结构体 jpeg_compress_struct ,通过该结构体来完成压缩数据输出、配置压缩选项操作。

压缩数据输出有两种方式:

    // 以文件的方式
    jpeg_stdio_dest(j_compress_ptr cinfo, FILE *outfile);
    // 以内存的方式
    jpeg_mem_dest(j_compress_ptr cinfo, unsigned char **outbuffer,
                           unsigned long *outsize)

另外,压缩选项除了常见的宽高信息、颜色类型,还有最重要的图像质量参数,通过专门的方法进行设置。

    jpeg_set_quality(j_compress_ptr cinfo, int quality,
                                boolean force_baseline)

设置完要压缩的相关信息后,就可以开始压缩了。

    // 开始压缩
    jpeg_start_compress(&jpegCompressStruct, true);
    // JSAMPROW 代表每行的数据
    JSAMPROW row_point[1];
    int row_stride = jpegCompressStruct.image_width * nComponent;

    while (jpegCompressStruct.next_scanline < jpegCompressStruct.image_height) {
        // data 参数就是要压缩的数据源
        // 逐行读取像素内容
        row_point[0] = &data[jpegCompressStruct.next_scanline * row_stride];
        // 写入数据
        jpeg_write_scanlines(&jpegCompressStruct, row_point, 1);
    }
    
   // 完成压缩 
   jpeg_finish_compress(&jpegCompressStruct);
   // 释放相应的结构体
   jpeg_destroy_compress(&jpegCompressStruct);

主要的代码就在于 while 循环中。

next_scanline 类似于一个状态变量,需要逐行去扫描图像内容并写入,每次 jpeg_write_scanlines 方法之后,next_scanline 就会递增,直到退出循环。

解压缩

解压和压缩的代码结构大致相同了。

    jpeg_create_decompress(&cinfo);
    // 设置数据源数据方式 这里以文件的方式,也可以以内存数据的方式
    jpeg_stdio_src(&cinfo, fp);
    // 读取文件信息,比如宽高之类的
    jpeg_read_header(&cinfo, TRUE);

其中 jpeg_read_header 方法可以获取要解压的文件相关信息。

接下来设置解压的相关参数:

    jpeg_start_decompress(&cinfo);
    unsigned long width = cinfo.output_width;
    unsigned long height = cinfo.output_height;
    unsigned short depth = cinfo.output_components;
    row_stride = cinfo.output_width * cinfo.output_components;

定义相关的数据变量,来保存解压的数据。

    // 保存每行解压的数据内容    
    JSAMPARRAY buffer;
    // 初始化
    buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
    
    // 保存图像的所有数据
    unsigned char *src_buff;
    // 初始化并置空
    src_buff = static_cast<unsigned char *>(malloc(width * height * depth));
    memset(src_buff, 0, sizeof(unsigned char) * width * height * depth);

这里使用的是 JSAMPARRAY 来保存,在 libjpeg-turbo 中其实有多种结构来表示图像数据类型,压缩中用到的就是 JSAMPROW

/* Data structures for images (arrays of samples and of DCT coefficients).
 */
typedef unsigned char JSAMPLE;
typedef JSAMPLE *JSAMPROW;      /* ptr to one image row of pixel samples. */
typedef JSAMPROW *JSAMPARRAY;   /* ptr to some rows (a 2-D sample array) */
typedef JSAMPARRAY *JSAMPIMAGE; /* a 3-D sample array: top index is color */

通过上述代码不难看出,JSAMPROW 其实就是一维数组,而 JSAMPARRAY 就是二维数组。以下两行代码可以说是等价的:

    JSAMPARRAY buffer = nullptr;
    JSAMPROW *row_pointer = nullptr;

因为 JSAMPARRAY 的定义就是 JSAMPROW *

具体用哪个更好,要看调用方法需要的参数类型了,jpeg_write_scanlinesjpeg_read_scanlines 这两个方法需要的都是 JSAMPARRAY 类型。

另外,代码中还声明并初始化了 src_buff 变量,该变量就是用来表示解压后的图像数据。

具体解压的逻辑也比较清楚了,逐行扫描图像,用 buffer 变量去存储图像每行解压的数据,然后把这个数据给到 src_buff 变量,如下代码所示:

    unsigned char *point = src_buff;

    while (cinfo.output_scanline < height) {
        jpeg_read_scanlines(&cinfo, buffer, 1);
        memcpy(point, *buffer, width * depth);
        point += width * depth;
    }

    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);

这里用到了 point 这样的临时变量,实际上,这样的用法在 C++ 开发中应该算比较常见的。

因为把 buffer 的数据传到 src_buff 后,src_buff 指针要移动到下一个点去接收数据,这样一来,指针指向的位置就不是原始位置了,所以才需要临时变量去做移动操作,保证 src_buff 指向的位置为起始点。

最后,别忘了释放相应的变量,做一些收尾工作,解压就完成了。

jpeg 上传纹理渲染

说完了压缩和解压缩,最后以一个例子来实际应用,也是之前文章中常用的例子,通过 libjpeg-turbo 读取 jpeg 文件图像内容并上传纹理渲染。

    // 封装 jpeg 相关操作
    JpegHelper jpegHelper;
    // 读取的图像内容
    unsigned char *jpegData;
    int jpegSize;
    int jpegWidth;
    int jpegHeight;
    // 读取操作
    jpegHelper.read_jpeg_file(filePath, &jpegData, &jpegSize, &jpegWidth, &jpegHeight);

最终要获取的就是 jpegData 图像内容,并通过 glTexImage2D 等方法渲染到纹理上。

read_jpeg_file 方法的声明如下:

    int read_jpeg_file(const char *jpeg_file, unsigned char **rgb_buffer, 
                       int *size, int *width,int *height)

rgb_buffer 参数的数据类型,既可以声明成 unsigned char ** 这样指针的地址,也可以声明成引用 unsigned char *& ,大同小异。

至于具体的读取操作,和上面的解压缩过程大致相同,就不在阐述一遍了,可以查看我的项目代码实践:

https://github.com/glumes/InstantGLSL

总结

至此,总结了常用的三种图像库的编译和使用。

这三种图像库各有特点,要根据实际需要,选择最合适的。但实际我们用到的无非就是图像的读写操作。读取特定格式图像的像素内容,或者将像素内容写入特定格式文件。

平时写 demo ,追求简单方便的,就用 stb_image,对性能有要求的,针对特定格式的选择 libpng 或者 libjpeg-turbo

原创文章,转载请注明来源:    图像库 libjpeg-turbo 编译与实践