JPEG知识及tinyjpeg.h学习
文章目录
- JPEG相关知识及tinyjpeg.h库文件解读
- JPEG相关知识
- JPEG编码流程
- JPEG文件格式
- JPEG文件必须包含的段
- tinyjpeg.h文件解读
- 核心结构体
- 核心函数
- 实践
- 下载源码
- 跑通程序
- 自行修改源码学习
- 学习总结
JPEG相关知识及tinyjpeg.h库文件解读
JPEG相关知识
JPEG编码流程
图像压缩非常重要,jpeg或jpg是一种图像格式,也是一种图像压缩标准。jpeg2000是对jpeg的改进,但本文只是对图像压缩有一个简单的了解,只分析JPEG。
图像压缩的核心是减小像素间的空间相关性,从而获得压缩。JPEG压缩过程一般如下:
- JPEG处理的是YUV数据,因此需要先将其它格式的数据转为YUV,转换公式可在网上查阅;
- 然后对YUV图像分块,JPEG分为
8x8
的小块,并进行DCT
变换,得到DCT
系数; DCT
系数也是8x8
的矩阵。最左上角表示直流分量(DC系数),是图像的低频信息;其余部分代表交流分流(AC系数),是图像的中高频信息;- 压缩的核心:对高频信息压缩(量化),保留低频信息,如果采用的量化间隔越大,则压缩率越高,但图像丢失的信息越多,重建图像与原图像差距越大。JPEG是采用
zig-zag
顺序对DCT
系数矩阵进行压缩,这样能先对低频信息进行量化(TODO:有什么用呢?); - 对量化后的结果编码(熵编码),如采用Huffman或RLE。
JPEG文件格式
JPEG文件格式参考
整体过程如上,但JPEG类型文件的最开始还包括一系列文件头信息和其它标志,JPEG图片格式组成部分:SOI(文件头)+APP0(图像识别信息)+ DQT(定义量化表)+ SOF0(图像基本信息)+ DHT(定义Huffman表) + DRI(定义重新开始间隔)+ SOS(扫描行开始)+ EOI(文件尾)。
JPEG文件是一段一段进行存储的,JPEG文件的每个段都一定包含两部分:一个是段的标识,它由两个字节构成:第一个字节是十六进制0xFF,第二个字节对于不同的段,这个值是不同的。另一部分是:紧接着的两个字节存放的是这个段的长度(除了前面的两个字节0xFF和0xXX,X表示不确定。他们是不算到段的长度中的)。段的结构如下:
-----------------------------------------------------------------
名称 字节数 数据 说明
-----------------------------------------------------------------
段标识 1 FF 每个新段的开始标识
段类型 1 类型编码(称作“标记码”)
段长度 2 包括段内容和段长度本身,不包括段标识和段类型
段内容 ≤65533字节
特别注意:长度的表示方法是按照高位在前,低位在后的,即big-endiam模式。
JPEG文件必须包含的段
JPEG总共有30个段,有10个段必须提供,其余的可选。可参考此链接
tinyjpeg.h文件解读
tinyjpeg.h
是单文件的JPEG encoder,可读性高,适合学习JPEG编码,可在GitHub上获取。使用方法也很简单,在文件最开始部分也有说明:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h" // 另外一个开源库
#define TJE_IMPLEMENTATION
#include "tiny_jpeg.h"
int main()
{int width, height, num_components;unsigned char* data = stbi_load("in.bmp", &width, &height, &num_components, 0);if ( !data ) {puts("Could not find file");return EXIT_FAILURE;}if ( !tje_encode_to_file("out.jpg", width, height, num_components, data) ) {fprintf(stderr, "Could not write JPEG\n");return EXIT_FAILURE;}return EXIT_SUCCESS;
}
核心结构体
两个核心结构体,TJEState
包含TJEWriteContext
,图片数据流动过程:
段数据
8x8
图像块经过压缩后或者JPEG文件段信息数据通过函数tjei_write()
写入到TJEState.output_buffer
中去,然后再通过TJEState.wc.func()
函数写入到TJEState.wc.context
中去,TJEState.wc.context
可以是一个输出文件的文件指针。TJEState.wc.func()
是一个核心函数。
结构体具体成员:
typedef struct {void* context; // 数据目的地,如输出文件的文件指针tje_write_func* func;
} TJEWriteContext;typedef struct {// Huffman data.uint8_t ehuffsize[4][257];uint16_t ehuffcode[4][256];uint8_t const * ht_bits[4];uint8_t const * ht_vals[4];// Quantization tables.uint8_t qt_luma[64];uint8_t qt_chroma[64];// fwrite by default. User-defined when using tje_encode_with_func.TJEWriteContext write_context;// Buffered output. Big performance win when using the usual stdlib implementations.size_t output_buffer_count;uint8_t output_buffer[TJEI_BUFFER_SIZE];
} TJEState;
核心函数
公共接口:tje_encode_to_file(),tje_encode_to_file_at_quality()
,以及tje_encode_with_func()
。
tje_encode_to_file() // qulity=3|tje_encode_to_file_at_quality() // 使用默认tjei_stdlib_func() |tje_encode_with_func()|tjei_encode_main()|传入的TJEState的成员write_context中包含func
核心函数tjei_encode_main()
及tjei_encode_and_write_MCU()
:
// 此函数输入的是整张图片,在函数中会遍历8x8的小块(MCU),然后调用下面的函数
static int tjei_encode_main(TJEState* state,const unsigned char* src_data,const int width,const int height,const int src_num_components)
// 处理8x8的MCU,JPEG压缩的核心函数
static void tjei_encode_and_write_MCU(TJEState* state,float* mcu,
#if TJE_USE_FAST_DCT float* qt, // Pre-processed quantization matrix.
#elseuint8_t* qt,
#endifuint8_t* huff_dc_len, uint16_t* huff_dc_code, // hfm tablesuint8_t* huff_ac_len, uint16_t* huff_ac_code,int* pred, // DC系数用到的uint32_t* bitbuffer, // Bitstack.uint32_t* location)
tjei_encode_main()
主要是遍历原始图像,并进行分块,主要逻辑如下:
for ( int y = 0; y < height; y += 8 ) {for ( int x = 0; x < width; x += 8 ) {// Block loop: ====for ( int off_y = 0; off_y < 8; ++off_y ) {for ( int off_x = 0; off_x < 8; ++off_x ) {// 块内索引0-63int block_index = (off_y * 8 + off_x); // 获取原始图像数据并转为yuvdu_y[block_index] = luma;du_b[block_index] = cb;du_r[block_index] = cr;}}// 调用MCU压缩编码函数tjei_encode_and_write_MCU(state, du_y, ...);tjei_encode_and_write_MCU(state, du_b, ...);tjei_encode_and_write_MCU(state, du_r, ...);}
}
而tjei_encode_and_write_MCU()
就是真正进行压缩编码的函数,包括DCT变换、量化、编码。
工具函数:tjei_fdct(),tjei_write()
。tjei_write(TJEState* state, const void* data, size_t num_bytes, size_t num_elements)
是将num_bytes X num_elements
大小的数据data
通过state.wc.func()
写入到state.wc.context
,此函数会递归调用自己(如果data数据长度大于1024(TJEI_BUFFER_SIZE),则会递归调用)。
// 此函数会递归调用自己
static void tjei_write(TJEState* state, const void* data, size_t num_bytes, size_t num_elements) {size_t to_write = num_bytes * num_elements;// Cap to the buffer available size and copy memory.size_t capped_count = tjei_min(to_write, TJEI_BUFFER_SIZE - 1 - state->output_buffer_count);memcpy(state->output_buffer + state->output_buffer_count, data, capped_count);state->output_buffer_count += capped_count;assert (state->output_buffer_count <= TJEI_BUFFER_SIZE - 1);// NOTE: (5) tje_encode_with_func()传入的自定义函数在此处被调用// Flush the buffer.if ( state->output_buffer_count == TJEI_BUFFER_SIZE - 1 ) {// context是指已压缩数据要写入的地方,比如一个文件指针。即将output_buffer中的内容写入到context中去state->write_context.func(state->write_context.context, state->output_buffer, (int)state->output_buffer_count);state->output_buffer_count = 0;}// Recursively calling ourselves with the rest of the buffer.if (capped_count < to_write) {tjei_write(state, (uint8_t*)data+capped_count, to_write - capped_count, 1);}
}
实践
下载源码
从github上下载tiny_jpeg.h
(点击这里)以及从开源图像库stb中提取出stb_image.h
,并放入同一文件夹中。
-jpegtiny-test|-stb_image.h // 用于获取输入图片的原始图像数据,如bmp、gif等|-tiny_jpeg.h // 对原始图像数据进行压缩编码|-tinyjpeg_test.c // 测试程序|-lena.bmp // 测试图片
stb_image.h
可加载不同类型的图片,包括JPG, PNG, TGA, BMP, PSD, GIF, HDR, PIC
,并解码出其原始数据(应该是YUV或者RGB),具体得看源码。
测试程序tinyjpeg_test.c
:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define TJE_IMPLEMENTATION
#include "tiny_jpeg.h"int main(int argc, char const *argv[])
{int width, height, num_components, quality;if (argc != 4) {printf("usage:\n");printf("xxx.exe infile.bmp outfile.jpg quality\n");printf("quality:1 2 3, 3-->the output img is bigger");return 0;}quality = (int)(*argv[3]-0x30);printf("the quality is: %d\n", quality);unsigned char* data = stbi_load(argv[1], &width, &height, &num_components, 0);if ( !data ) {puts("Could not find file");return EXIT_FAILURE;}if ( !tje_encode_to_file_at_quality(argv[2], quality, width, height, num_components, data) ) {fprintf(stderr, "Could not write JPEG\n");return EXIT_FAILURE;}return EXIT_SUCCESS;
}
跑通程序
编译命令:gcc tinyjpeg_test.c tiny_jpeg.h stb_image.h -o tinyjpeg_test
执行命令:tinyjpeg_test.exe lena.bmp lena_q1.jpg 1
,1表示quality为1,tiny_jpeg.h
提供了3种quality。quality=3
时量化矩阵qt
的所有元素都为1,即不对DCT系数进行压缩,因此输出图片质量最高,但图片大小也最大;quality=2
时次之、quality=1
最次。可以自行修改源码中的量化矩阵,或者自己增加quality的选择。
3种quality
与原图的对比,几乎差不多:
自行修改源码学习
跑通程序后就可以自行修改源码学习JPEG,比如前面所说改变量化矩阵、将DCT变换修改为Wavelet变换、还可以自己修改分块大小,如4x4、16x16等。还可以计算块与块之间的相似性,运动补偿,找两幅相似的图片进行帧间预测学习。
tiny_jpeg.h
中可以自行定义写数据函数,即将压缩后的数据写到某个地方,默认是调用标准库的fwrite()
将压缩后的数据写入到context
(输出文件的文件指针)。此处自定义写数据函数,将压缩数据直接输出到stdout
,也就是屏幕。
...... // 输出到stdout
ret = tje_encode_with_func(cdj_tje_func, (void *)stdout, quality, width, height, num_components, data);
if ( !ret ) {fprintf(stderr, "Could not write JPEG\n");return EXIT_FAILURE;
}
// 可以自行实现tiny_jpeg.h的写函数(将编码后的数据写到某处)
static void cdj_tje_func(void* context, void* data, int size) {printf("\n\n=======cdj_tje_func was called======\n");// context其实就是stdout,tje_encode_with_func()传入的参数FILE * where2write = (FILE *)context;// stdout是FILE*类型的变量char * data_ch = (char *)data;// 数据太多,只输出10Bfor (int i = 0; i < 10 && i < size; ++i) {fprintf(where2write, "%0x ", data_ch[i]);}
}
如下图,直接将压缩后的数据(或JPEG文件段信息)输出到stdout
,即屏幕:
可以对比最开始调用tjei_write()
写入的内容,即TJEJPEGHeader
结构体:
{ // Write headerTJEJPEGHeader header;// JFIF header.header.SOI = tjei_be_word(0xffd8); // Sequential DCTheader.APP0 = tjei_be_word(0xffe0);/*SOI & APP0 markers*/;uint16_t jfif_len = sizeof(TJEJPEGHeader) - 4header.jfif_len = tjei_be_word(jfif_len);memcpy(header.jfif_id, (void*)tjeik_jfif_id, 5);header.version = tjei_be_word(0x0102);header.units = 0x01; // Dots-per-inchheader.x_density = tjei_be_word(0x0060); // 96 DPIheader.y_density = tjei_be_word(0x0060); // 96 DPIheader.x_thumb = 0;header.y_thumb = 0;tjei_write(state, &header, sizeof(TJEJPEGHeader), 1);
}
学习总结
结构体的1btye
对齐,一是更节省空间、二是在写一些文件头信息时的常用技巧:
#pragma pack(push)
#pragma pack(1)
typedef struct {uint16_t SOI; // start of image// JFIF header.uint16_t APP0; // 图片信息uint16_t version; // 版本uint8_t units;uint16_t x_density;uint16_t y_density;
} TJEJPEGHeader;
#pragma pack(pop)
利用代码块{...}
,使代码更易读。
// NOTE: (3) 值得学习的写法,利用代码块来表示不同部分
// Write JPEG header to file
{ TJEJPEGHeader header;// JFIF header.header.SOI = tjei_be_word(0xffd8); // Sequential DCTheader.APP0 = tjei_be_word(0xffe0);...header.version = tjei_be_word(0x0102);header.units = 0x01; // Dots-per-inchheader.x_density = tjei_be_word(0x0060); // 96 DPIheader.y_density = tjei_be_word(0x0060); // 96 DPI// 将JPEG的文件头header写入到state的wc的context中去,也就是输出文件的文件指针(可以自定义输出位置,甚至输出到串口),只需调用tje_encode_with_func()并提供自定义func并作为参数传入tjei_write(state, &header, sizeof(TJEJPEGHeader), 1);
}
JPEG知识及tinyjpeg.h学习相关推荐
- JNI学习开始篇 基础知识 数据映射及学习资料收集
JNI学习开始篇 基础知识 数据映射及学习资料收集 JNI介绍 JNI(Java Native Interface) ,Java本地接口. 用Java去调用其他语言编写的程序,比如C或C++. JNI ...
- 基于公共知识和一次学习的多任务流量分类
写在前面: 本文翻译供个人研究学习之用,不保证严谨与准确 github链接:https://github.com/WithHades/network_traffic_classification_pa ...
- 知识图谱从入门到应用——知识图谱推理:基于表示学习的知识图谱推理-[嵌入学习]
分类目录:<知识图谱从入门到应用>总目录 前面多次提到过,基于符号逻辑的演绎推理的主要缺点是对知识表示的逻辑结构要求比较高,不论是本体推理还是规则推理,都要求人工定义公理和规则才能完成推理 ...
- 杂谈 | 当前知识蒸馏与迁移学习有哪些可用的开源工具?
所有参与投票的 CSDN 用户都参加抽奖活动 群内公布奖项,还有更多福利赠送 作者&编辑 | 言有三 来源 | 有三AI(ID:yanyousan_ai) [导读]知识蒸馏与迁移学习不仅仅属于 ...
- 知识图谱、深度学习、AutoML,推荐系统与新技术结合将碰撞出怎样的火花?
近日,来自意大利米兰理工大学 Maurizio 团队发表的一篇极具批判性的文章火了.这篇文章剑指推荐系统领域的其他数十篇论文,并通过多项试验证明这些论文中基于深度学习的推荐算法大部分都存在不同程度的数 ...
- 知识图谱与深度学习(新时代·技术新未来)
作者:刘知远,韩旭,孙茂松 出版社:清华大学出版社 品牌:清华大学出版社 出版时间:2020-05-01 知识图谱与深度学习(新时代·技术新未来)
- 线程基础知识——Windows核心编程学习手札系列之六
线程基础知识 --Windows核心编程学习手札系列之六 线程与进程一样由两部分构成:一是线程的内核对象,操作系统用它来对线程实施管理,也是系统用来存放线程统计信息的地方:二是线程堆栈,用于维护线程在 ...
- 【星球知识卡片】深度学习图像降噪有哪些关键技术点,如何学习
大家好,欢迎来到我们的星球知识小卡片专栏,本期给大家分享图像降噪相关的资源. 作者&编辑 | 言有三 1 基本的CNN降噪模型 图像去噪模型的输出是无噪声的图像,与输入图像大小相同,所以可以使 ...
- 【杂谈】当前知识蒸馏与迁移学习有哪些可用的开源工具?
知识蒸馏与迁移学习不仅仅属于模型优化的重要技术之一,也是提升模型跨领域泛化能力的重要技术,那么当前有哪些可用的知识蒸馏和迁移学习开源工具呢? 作者&编辑 | 言有三 1 PaddleSlim ...
最新文章
- Kafka 消息监控 - Kafka Eagle
- HDU-1299 Diophantus of Alexandria 素因子分解
- java内存块_JVM上的并发和Java内存模型之同步块笔记
- vue强制更新$forceUpdate()
- ERP customizing extraction - how extraction function module is determined
- 面试官问我:Redis 内存满了怎么办
- C++(STL):16---deque之常规用法
- Linux的实际操作:关机shutdown、重启reboot、用户注销logout
- Asp.net 类中使用中括号([......])的作用
- 计算机自动化技术要学什么,【经验分享】PLC学习的5个阶段,自动化工程师看看你属于哪个阶段?...
- 记一次转不过弯的递归
- 模式搜索的KMP算法详解与C语言代码实现
- 编程范式--并发编程相关代码
- mysql 5.1 开启慢查询_mysql开启慢查询
- c语言形式参数若为b 4,4月全国计算机等级二级C笔试考试题目
- [译] 关于 SPA,你需要掌握的 4 层 (1)
- 用php做的图书管管理系统,PHP自习室图书馆座位管理系统
- LinkedIn应用开发系列(三) --认证Request token
- aria2c 官方手册中文翻译版
- 软件测试学习之悟空CRM项目测试用例编写
热门文章
- html dom firstchild,HTML DOM firstChild 属性
- 伪随机数与随机数种子
- 如何用ajax做登录页面,ajax如何制作登录页面?登录页面ajax的请求详解(附完整实例)...
- 支持向量机——人脸识别
- YOTA 3:凭何领衔移动阅读?
- win7系统下samba服务器无法登陆,win7系统访问NAS/Samba服务器失败的解决方法
- mysql 1-n,1-1
- python pygame 愤怒的小鸟 (学习阶段-感谢支持)
- 从零单刷数据结构(Java描述)(三)——数组
- 字典(dict)作业