(一)实验目的
掌握JPEG编解码系统的基本原理。初步掌握复杂的数据压缩算法实现,并能根据理论分析需要实现所对应数据的输出。
JPEG( Joint Photographic Experts Group)是最常用的图像文件格式,其主要是采用预测编码(DPCM)、离散余弦变换(DCT)以及熵编码的联合编码方式,以去除冗余的图像和彩色数据,属于有损压缩格式,它能够将图像压缩在很小的储存空间,一定程度上会造成图像数据的损伤。
(二)JPEG编解码原理
编码器
解码器为编码器的逆过程
2.1 JPEG文件格式介绍
符号 | 含义 |
---|---|
SOI | Start of Image,图像开始 |
EOI | End of Image,图像结束 2字节 |
APP0 | Application,应用程序保留标记0 |
DQT | Define Quantization Table,定义量化表 |
SOF | Start of Frame,帧图像开始 |
DHT | Define Huffman Table,定义哈夫曼表 |
SOS | Start of Scan,扫描开始 12字节 |
2.1.1 SOI & EOI
SOI ,Start of Image, 图像开始
标记代码 2字节 固定值0xFFD8
EOI,End of Image, 图像结束 2字节
标记代码 2字节 固定值0xFFD9
2.1.2 APP0 应用程序保留标记0
标记代码 | 2字节 | 固定值0xFFE0 |
---|---|---|
① 数据长度 | 2字节 | ①~⑨9个字段的总长度 |
② 标识符 | 5字节 | 固定值0x4A46494600,即字符串“JFIF0” |
③ 版本号 | 2字节 | 一般是0x0102,表示JFIF的版本号1.2 |
④ X和Y的密度单位 | 1字节 | 只有三个值可选 0:无单位;1:点数/英寸;2:点数/厘米 |
⑤ X方向像素密度 | 2字节 | 取值范围未知 |
⑥ Y方向像素密度 | 2字节 | 取值范围未知 |
⑦ 缩略图水平像素数目 | 1字节 | 取值范围未知 |
⑧ 缩略图垂直像素数目 | 1字节 | 取值范围未知 |
⑨ 缩略图RGB位图 | 长度可能是3的倍数 | 缩略图RGB位图数据 |
2.1.3 DQT 定义量化表
标记代码 2字节 固定值0xFFDB
包含9个具体字段: ① 数据长度 2字节 字段①和多个字段②的总长度 ② 量化表 数据长度-2字节
a) 精度及量化表ID 1字节
高4位:精度,只有两个可选值 0:8位;1:16位 低4位:量化表ID,取值范围为0~3
b) 表项 (64×(精度+1))字节
例如8位精度的量化表,其表项长度为64×(0+1)=64字节
本标记段中,字段②可以重复出现,表示多个量化表,但最多只能出现4次
2.1.4 SOF0 帧图像开始
标记代码 | 2字节 | 固定值0xFFC0 |
---|---|---|
① 数据长度 | 2字节 | ①~⑥六个字段的总长度 |
② 精度 | 1字节 | 每个数据样本的位数 通常是8位,一般软件都不支持 12位和16位 |
③ 图像高度 | 2字节 | 图像高度(单位:像素) |
④ 图像宽度 | 2字节 | 图像宽度(单位:像素) |
⑤ 颜色分量数 | 1字节 | 只有3个数值可选 1:灰度图;3:YCrCb或YIQ;4:CMYK 而JFIF中使用YCrCb,故这里颜色分量数恒为3 |
⑥颜色分量信息 | 颜色分量数×3字节(通常为9字节) | a)颜色分量ID 1字节 b)水平/垂直采样因子 1字节 高4位:水平采样因子 低4位:垂直采样因子 c) 量化表 1字节 当前分量使用的量化表的ID |
2.1.5 DHT 定义哈夫曼表
标记代码 2字节 固定值0xFFC4
包含2个具体字段:
① 数据长度 2字节
② huffman表 数据长度-2字节
表ID和表类型 1字节
高4位:类型,只有两个值可选
0:DC直流;1:AC交流 低4位:哈夫曼表ID,
注意,DC表和AC表分开编码
不同位数的码字数量 16字节
编码内容 16个不同位数的码字数量之和(字节)本标记段中,字段②可以重复出现(一般4次),也可以只出现1次
2.1.6 SOS 扫描开始
标记代码 2字节 固定值0xFFDA
包含2个具体字段:
①数据长度 2字节 ①~④两个字段的总长度
②颜色分量数 1字节 应该和SOF中的字段⑤的值相同,即: 1:灰度图是;3: YCrCb或YIQ;4:CMYK。
③ 颜色分量信息
a) 颜色分量ID 1字节
b) 直流/交流系数表号 1字节
高4位:直流分量使用的哈夫曼树编号
低4位:交流分量使用的哈夫曼树编号
④ 压缩图像数据
a)谱选择开始 1字节 固定值0x00
b)谱选择结束 1字节 固定值0x3F
c)谱选择 1字节 在基本JPEG中总为00
2.2 JPEG编码步骤
- 零偏置(Level Offset)
对于灰度级是2n的像素,通过减去2(n-1),将无符号的整数值变成有符号数,使像素的绝对值出现3位10进制的概率大大减少 - DCT变换
对每个单独的彩色图像分量,把整个分量图像分成8×8的图像块,并作为二维离散余弦变换DCT的输入。将原本的像素数值进行DCT变换,后续直接对DCT分量进行处理。 - 量化
对DCT分量进行量化,由于人眼对亮度信号比对色差信号更敏感,因此使用了两种量化表:亮度量化值和色差量化值。
根据人眼的视觉特性(对低频敏感,对高频不太敏感)对低频分量采取较细的量化,对高频分量采取较粗的量化。
如果原始图象中细节丰富,则去掉的数据较多,量化后的系数与量化前差别。反之,细节少的原始图象在压缩时去掉的数据少些。 - DC系数差分编码
DC直流系数有两个特点:系数的数值比较大,相邻DC系数值变化不大。
根据上述两个特点,JPEG算法使用了差分脉冲调制编码(DPCM)技术,对相邻图像块之间量化DC系数的差值DIFF进行编码:
DIFF_k=DC_k-DC_{k-1} - AC系数之字形扫描
由于经DCT变换后,系数大多数集中在左上角,即低频分量区,因此采用Z字形按频率的高低顺序读出,可以出现很多连零的机会。可以使用游程编码。尤其在最后,如果都是零,给出 EOB (End of Block)即可。
6. AC系数的游程编码
在JPEG和MPEG编码中规定为:(run,level)
表示连续run个0,后面跟值为level的系数
如:0,2,0,0,3,0,-4,0,0,0,-6,0,0 ,5,7
表示为(1, 2), (2, 3)
2.3 JPEG文件解码流程
1.读入文件的相关信息
2.初步了解图像数据流的结构
3.颜色分量单元的内部解码
4.直流系数的差分编码
5.反量化 & 反Zig-zag编码
6.反离散余弦变换
3 JPEG文件解析
3.1 理解三个结构体的设计目的
- struct huffman_table:存储Huffman码表
/* tinyjpeg-internal.h */
struct huffman_table
{
/* Fast look up table, using HUFFMAN_HASH_NBITS bits we can have directly the symbol,
* if the symbol is <0, then we need to look into the tree table */
short int lookup[HUFFMAN_HASH_SIZE];
/* code size: give the number of bits of a symbol is encoded */
unsigned char code_size[HUFFMAN_HASH_SIZE];
/* some place to store value that is not encoded in the lookup table
* FIXME: Calculate if 256 value is enough to store all values
*/
uint16_t slowtable[16-HUFFMAN_HASH_NBITS][256];
};
- struct component:储存当前8×8像块中有关解码的信息
/* tinyjpeg-internal.h */
struct component
{
unsigned int Hfactor; // 水平采样因子
unsigned int Vfactor; // 垂直采样因子
float* Q_table; // 指向该8×8块使用的量化表
struct huffman_table *AC_table; // 指向该块使用的AC Huffman表
struct huffman_table *DC_table; // 指向该块使用的DC Huffman表
short int previous_DC; // 前一个块的直流DCT系数
short int DCT[64]; // DCT系数数组
#if SANITY_CHECK
unsigned int cid;
#endif
};
- struct jdec_private:JPEG数据流结构体,用于存储JPEG图像宽高、数据流指针、Huffman码表等内容,并包含struct huffman_table和struct component
/* tinyjpeg-internal.h */
struct jdec_private
{
/* Public variables */
uint8_t *components[COMPONENTS]; /* 分别指向YUV三个分量的三个指针 */
unsigned int width, height; /* 图像宽高 */
unsigned int flags;
/* Private variables */
const unsigned char *stream_begin, *stream_end;
unsigned int stream_length;
const unsigned char *stream; /* 指向当前数据流的指针 */
unsigned int reservoir, nbits_in_reservoir;
struct component component_infos[COMPONENTS];
float Q_tables[COMPONENTS][64]; /* quantization tables */
struct huffman_table HTDC[HUFFMAN_TABLES]; /* DC huffman tables */
struct huffman_table HTAC[HUFFMAN_TABLES]; /* AC huffman tables */
int default_huffman_table_initialized;
int restart_interval;
int restarts_to_go; /* MCUs left in this restart interval */
int last_rst_marker_seen; /* Rst marker is incremented each time */
/* Temp space used after the IDCT to store each components */
uint8_t Y[64*4], Cr[64], Cb[64];
jmp_buf jump_state;
/* Internal Pointer use for colorspace conversion, do not modify it !!! */
uint8_t *plane[COMPONENTS];
};
3.2 理解整体框架
convert_one_image函数:读取判断输入文件格式,解码并按照输出格式输出
int convert_one_image(const char *infilename, const char *outfilename, int output_format)
{
//输入文件名称、输出文件名称、输出文件格式
FILE *fp;
unsigned int length_of_file; // 文件大小
unsigned int width, height; // 图像宽、高
unsigned char *buf;
struct jdec_private *jdec; //JPEG数据数据流
unsigned char *components[3];
fp = fopen(infilename, "rb");//读取JPEG图像
if (fp == NULL)
exitmessage("Cannot open filename\n");
length_of_file = filesize(fp);//输入文件的字节大小
buf = (unsigned char *)malloc(length_of_file + 4);
if (buf == NULL)
exitmessage("Not enough memory for loading file\n");
fread(buf, length_of_file, 1, fp);
fclose(fp);
/* Decompress it */
jdec = tinyjpeg_init(); // 初始化
if (jdec == NULL)
exitmessage("Not enough memory to alloc the structure need for decompressing\n");
if (tinyjpeg_parse_header(jdec, buf, length_of_file)<0)//读取头文件,检查图像格式
exitmessage(tinyjpeg_get_errorstring(jdec));
tinyjpeg_get_size(jdec, &width, &height);// 计算图像宽高
snprintf(error_string, sizeof(error_string),"Decoding JPEG image...\n");
if (tinyjpeg_decode(jdec, output_format) < 0) // 解码实际数据
exitmessage(tinyjpeg_get_errorstring(jdec));
/*
* Get address for each plane (not only max 3 planes is supported), and
* depending of the output mode, only some components will be filled
* RGB: 1 plane, YUV420P: 3 planes, GREY: 1 plane
*/
tinyjpeg_get_components(jdec, components);//将compoents结构体的数据传入jdec
switch (output_format)//选择输出图像格式 保存
{
case TINYJPEG_FMT_RGB24:
case TINYJPEG_FMT_BGR24:
write_tga(outfilename, output_format, width, height, components);
break;
case TINYJPEG_FMT_YUV420P:
write_yuv(outfilename, width, height, components);
break;
case TINYJPEG_FMT_GREY:
write_pgm(outfilename, width, height, components);
break;
}
/* Only called this if the buffers were allocated by tinyjpeg_decode() */c
tinyjpeg_free(jdec);
/* else called just free(jdec); */
free(buf);
return 0;
}
tinyjpeg_init初始化函数:动态分配存储内存
struct jdec_private *tinyjpeg_init(void)
{
struct jdec_private *priv;//声明JPEG数据结构体
priv = (struct jdec_private *)calloc(1, sizeof(struct jdec_private));//在内存的动态存储区中分配1个struct jdec_private大小的连续空间
if (priv == NULL)
return NULL;
return priv;
}
tinyjpeg_parse_header函数:解码JPEG头信息(FFD8-SOI)
读取完前两字节后,开始指针后移两位priv->stream_begin=buffer+2,文件未读长度-2,接parse_JFIF函数遍历整个文件,找到不同的标识码,并解析相应标识码对应的信息
int tinyjpeg_parse_header(struct jdec_private *priv, const unsigned char *buf, unsigned int size)
{
int ret;