JPEG原理分析及JPEG解码器的调试
JPEG简介
JPEG( Joint Photographic Experts Group)即联合图像专家组,是用于连续色调静态图像压缩的一种标准,文件后缀名为.jpg或.jpeg,是最常用的图像文件格式。
其主要是采用预测编码(DPCM)、离散余弦变换(DCT)以及熵编码的联合编码方式,以去除冗余的图像和彩色数据,属于有损压缩格式,它能够将图像压缩在很小的储存空间,一定程度上会造成图像数据的损伤。尤其是使用过高的压缩比例,将使最终解压缩后恢复的图像质量降低,如果追求高品质图像,则不宜采用过高的压缩比例。
JPEG的性能,用质量与比特率之比来衡量,是相当优越的。
它的优点是:
- 支持极高的压缩率,因此JPEG图像的下载速度大大加快。
- 能够轻松地处理16.8M颜色,可以很好地再现全彩色的图像。
- 在对图像的压缩处理过程中,该图像格式可以允许自由地在最小文件尺寸(最低图像质量)和最大文件尺寸(最高图像质量)之间选择。
- 该格式的文件尺寸相对较小,下载速度快,有利于在带宽并不“富裕”的情况下传输。
JPEG的缺点是:
- 并非所有的浏览器都支持将各种JPEG图像插入网页。
- 压缩时,可能使图像的质量受到损失,因此不适宜用该格式来显示高清晰度的图像。
JPEG文件格式
概述
JPEG文件的存储格式有很多种,但最常用的是JFIF格式 。
JPEG在文件中以Segment的形式组织的,它具有以下特点:
① 标记码:由两个字节构成,其中,前一个字节是固定值0XFF代表了一个标记码的开始,后一个字节不同的值代表着不同的含义。连续多个0XFF可以理解为一个0XFF,并表示一个标记码的开始。另外,标记码在文件中一般是以标记代码的形式出现的。标记码后是2字节的Segment length(含length本身所占用的2字节,不包含0xFF和Marker所占用的字节),之后是该标记对应的payload;
②保存时高位在前,低位在后;
③Data部分中,0xFF后若为0x00,则跳过此字节不予处理
典型标记码
名称 | 标记码固定值 | 负载 | 含义 |
---|---|---|---|
SOI(start of image) | 0xFFD8 | 无 | 表示图像开始 |
APP0(application 0) | 0xFFE0 | 包含了9个具体的字段 | 包含内容如下: ①2字节数据长度; ②5字节标识符,固定值0x4A46494600,即字符串“JFIF0”; ③2字节版本号; ④1字节表示X和Y的密度单位; ⑤2字节X方向像素密度; ⑥2字节Y方向像素密度; ⑦1字节缩略图水平像素数据; ⑧1字节缩略图垂直像素数据; ⑨缩略图RGB位图 |
APPn(application n) | 0XFFE1–0XFFFF | 包含了2个字段 | 包含内容为:2字节数据长度和详细信息(内容不定) |
DQT | 0XFFDB | 包含9个具体字段 | 定义量化表,其包含9个具体字段,其中2字节为数据长度,数据长度-2字节为量化表,量化表包含精度及量化表ID和表项,一般为两个量化表,即亮度和色度各一张 |
SOF0(start of frame) | 0XFFC0 | 包含9个具体字段 | 帧图像开始,其包含9个具体字段,分别为: ①2字节数据长度; ②1字节精度; ③2字节图像高度; ④2字节图像宽度; ⑤1字节颜色分量数; ⑥颜色分量数×3字节颜色分量信息,依次表示1字节颜色分量ID,1字节水平/垂直采样因子(高4位代表水平采样因子,低4位代表垂直采样因子),1字节量化表 |
DHT | 0XFFC4 | 包含2个字段 | 定义Huffman表,其包含的2字段分别为2字节数据长度,数据长度-2字节的Huffman表,一般有一个或多个Huffman表 |
DRI | 0xFFDD | 包含2个字段 | 定义差分编码累计复位的间隔,其包含的2字段分别为2字节数据长度和2字节MCU块的单元中重新开始间隔 |
SOS | 0xFFDA | 包含2个字段 | 扫描开始,其包含字段的具体内容分别为数据长度、颜色分量数和颜色分量信息 |
EOI(end of image) | 0xFFD9 | 无 | 表示图像结束 |
JPEG编解码分析
编码原理
JPEG编码的过程如上图,解码就是编码的逆过程,简单介绍一下编码过程:
1.零偏置(level offset)
JPEG编码将图像分为8×8的块作为数据处理的最小单位,对于灰度级为 2 n 2^n 2n的像素,通过减去 2 n − 1 2^{n-1} 2n−1,将无符号数变成有符号数。以灰度级n=8为例,原来图像的灰度范围是[0,255],减去128之后变为了[-128,127]。经过零偏置后,图像平均亮度降低,像素灰度的绝对值被控制在较小的范围内,有利于后续编码。
2.DCT变换
对零偏置后的图像进行DCT变换,以进行能量集中和去相关,同时去除图像的空间冗余,变换后图像的能量都集中在右上角,同时DCT变换是一种无损变换,在变换过程中没有精度损失。如果有些图片并非8×8的整数倍,那么就需要在边缘进行像素填充。
3.量化
因为人眼对亮度信号比对色差信号更敏感,因此使用了两种量化表,分别是亮度量化值和色差量化值。同时因为人眼对低频敏感高频不太敏感,因此对低频分量采取较细的量化,对高频分量采取较粗的量化,因此细节少的原始图像在压缩时去掉的数据要少
4.DC系数的差分编码
8×8的图像块经过DCT变换之后得到的DC直流系数有两个特点:系数的数值比较大;相邻8×8图像块的DC系数值变化不大(存在冗余),因此可以采用DPCM方法,对相邻图像块之间量化DC系数的差值进行编码,编码方式采用熵编码(Huffman编码),亮度信号与色度信号的DC系数采用不同的Huffman编码表
5.AC系数的zig-zag扫描与游程编码
由于经过DCT变换后,系数大多集中在左上角,也就是低频分量区,因此采用Z字形扫描,按频率的高低顺序读出,这样会出现很多的连零,可以使用RLE游程编码,尤其是在最后,如果都是零,直接给出EOB(End of Block)即可
- 在JPEG编码中,游程编码的形式为:(run,level)
- 表示连续run个0后有值为level的系数
- run最多15个,用4位表示RRRR
- level,类似DC,分成16个类别,用4位SSSS表示类别号,类内索引
- 对(RRRR,SSSS)采用Huffman编码,对类内索引采用定长编码
亮度信号和色度信号的AC系数也有不同的Huffman码表
综上可以知道,在JPEG编码的过程中用到了两张量化表(亮度和色度)以及四张Huffman码表(亮度DC、亮度AC、色度DC和色度AC)
解码原理
解码就是编码的逆过程,其大致流程如下:
- 读取文件
- 解析文件segment
- 解析SOI,判断是否是JPEG文件
- 解析SOF
- 解析DQT,获取量化表相关信息
- 解析SOS
- 解析DHT,获取Huffman码表相关信息
- 解析DRI
- 以MCU为单位进行解码
- 解码Huffman数据
- 解码DC差值
- 重构量化后的系数
- DCT逆变换
- 丢弃填充的行/列
- 反0偏置
- 对丢失的CbCr分量差值(下采样的逆过程)
- YCbCr->RGB
JPEG解码实现程序分析
解码结果
为了方便后面对解码的理解,首先利用JPEG parser查看一下图片的解码信息,其具体含义可以参考**《JPEG 解码》调试报告**
SOF0
SOS
main
分析
对于main函数来说,主要是进行了输入输出文件的获取、输出格式和解码方式的选择等。
int main(int argc, char *argv[])
{
int output_format = TINYJPEG_FMT_YUV420P;
char *output_filename, *input_filename;
clock_t start_time, finish_time;
unsigned int duration;
int current_argument;
int benchmark_mode = 0;
#if TRACE //设定trace,边解码边写入文件
p_trace=fopen(TRACEFILE,"w");
if (p_trace==NULL)
{
printf("trace file open error!");
}
#endif
if (argc < 3)
usage();
current_argument = 1;
while (1)
{
if (strcmp(argv[current_argument], "--benchmark")==0) //strcmp字符串比较,用于比较两个字符串,如果二者相等返回0
benchmark_mode = 1; //如果相等,说明设置了benchmark,因此benchmark_mode置1
else
break;
current_argument++;
}
if (argc < current_argument+2)
usage();
input_filename = argv[current_argument]; //输入文件名,是argv[1]
if (strcmp(argv[current_argument+1],"yuv420p")==0) //argv[2]判断输出格式,这里选择yuv420p
output_format = TINYJPEG_FMT_YUV420P;
else if (strcmp(argv[current_argument+1],"rgb24")==0)
output_format = TINYJPEG_FMT_RGB24;
else if (strcmp(argv[current_argument+1],"bgr24")==0)
output_format = TINYJPEG_FMT_BGR24;
else if (strcmp(argv[current_argument+1],"grey")==0)
output_format = TINYJPEG_FMT_GREY;
else
exitmessage("Bad format: need to be one of yuv420p, rgb24, bgr24, grey\n");
output_filename = argv[current_argument+2]; //输出文件名,设为argv[3]
start_time = clock(); //开始
if (benchmark_mode) //是否多次解码,若设置了benchmark_mode就调用load_multiple_times,否则就调用convert_one_image
load_multiple_times(input_filename, output_filename, output_format);
else
convert_one_image(input_filename, output_filename, output_format); //核心函数
finish_time = clock(); //结束
duration = finish_time - start_time;
snprintf(error_string, sizeof(error_string),"Decoding finished in %u ticks\n", duration);
#if TRACE
fclose(p_trace);
#endif
return 0;
}
本次实验的输出格式为yuv420p,未设置参数benchmark
命令行参数
通过阅读main函数知道应该设置命令行参数如下:
结构体分析
为了更好的理解主要解码函数,先理解程序设计中的三个结构体:huffman_table,component,jdec_private
huffman_table
该结构体用来存储Huffman表
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]; //当码长>9时,交给该表处理
};
component
该结构体用来存储解码信息,这一段定义了水平方向和垂直方向的采样因子,量化表的指针,AC系数和DC系数的Huffman码表的指针。
除了定义解码需要用到的相关内容外,还定义了解码过程中需要临时存储的变量,其中DCT[64]用于保存DCT系数,previous_DC用来保存前一个解码后的DC系数,这是因为在对DC系数编码的时候采用了DPCM+Huffman编码的方式,而DPCM的解码需要有前一个的解码值
struct component //8x8块结构体
{
unsigned int Hfactor;
unsigned int Vfactor; //水平垂直采样因子
float *Q_table; /* Pointer to the quantisation table to use */ //指向该component解码时要用的量化表
struct huffman_table *AC_table;
struct huffman_table *DC_table; //分别对应AC系数和DC系数的Huffman表
//以上是解码过程中要使用的东西,以下是解码过程中临时存储的东西
short int previous_DC; /* Previous DC coefficient */ //存储前一个DC值,用于DPCM解码
//在进行Huffman编码之前,DC系数采用了DPCM,因此每解完一个DC系数就要存起来用于下一个DC系数的解码
short int DCT[64]; /* DCT coef */ //保存DCT的系数
#if SANITY_CHECK
unsigned int cid;
#endif
};
jdec_private
这一部分定义了JPEG数据流结构体,用来指示解码过程中所用到的信息,如图像数据、量化表、Huffman码表等,并定义了存储IDCT解码后的像素值的变量
struct jdec_private
{
/* Public variables */
uint8_t *components[COMPONENTS]; //定义指针数组,指向三种分量用于存放解码后数据的数组的地址
unsigned int width, height; /* Size of the image */ //图像宽高
unsigned int flags;
/* Private variables */
const unsigned char *stream_begin, *stream_end; //标记数据流的开始和结束
unsigned int stream_length; //数据流长度
const unsigned char *stream; /* Pointer to the current stream */ //当前解码流指针,用指向函数的指针来操作函数
unsigned int reservoir, nbits_in_reservoir;
struct component component_infos[COMPONENTS]; //存放三种分量的component信息
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]; //保存每个块经过IDCT解码后的像素
jmp_buf jump_state;
/* Internal Pointer use for colorspace conversion, do not modify it !!! */
uint8_t *plane[COMPONENTS]; //用于彩色空间转换
};
主要解码函数
这一部分是解码用到的核心函数,这一部分的主要操作是将输入的jpeg文件内容进行读取并存入buf,之后利用tinyjpeg_parse_header
函数解码JPEG文件的头信息,调用tinyjpeg_get_size
函数获取文件大小,最后再调用tinyjpeg_decode
函数进行解码,最后根据需要保存为不同形式的文件并释放空间
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; //定义结构体
unsigned char *components[3];
/* Load the Jpeg into memory */ //将所有文件全部读入
fp = fopen(infilename, "rb"); //打开输入文件
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); //将文件内容读入buf
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));
/* Get the size of the image */
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);
/* Save it */
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() */
tinyjpeg_free(jdec);
/* else called just free(jdec); */
free(buf);
return 0;
}
tinyjpeg_init
初始化一个jdec_private
的结构体,并赋给jdec,之后围绕该结构体展开后续的解码操作
struct jdec_private *tinyjpeg_init(void)
{
struct jdec_private *priv;
priv = (struct jdec_private *)calloc(1, sizeof(struct jdec_private));
if (priv == NULL)
return NULL;
return priv;
}
tinyjpeg_parse_header
判断是否是jpeg文件,解析文件信息,通过指针移动进入parse_JFIF
函数,parse_JFIF
函数遍历整个文件,找到不同的标识码,并解析相应标识码对应的信息
int tinyjpeg_parse_header(struct jdec_private *priv, const unsigned char *buf, unsigned int size)
{
int ret;
/* Identify the file */
if ((buf[0] != 0xFF) || (buf[1] != SOI)) //判断是否是jpeg文件
snprintf(error_string, sizeof(error_string),"Not a JPG file ?\n");
priv->stream_begin = buf+2;
priv->stream_length = size-2;
priv->stream_end = priv->stream_begin + priv->stream_length;
ret = parse_JFIF(priv, priv->stream_begin); //解析JFIF(解析各种不同的标签)
return ret;
}
parse_DQT
这段函数用来解码量化表
static int parse_DQT(struct jdec_private *priv, const unsigned char *stream)
{
int qi;
float *table;
const unsigned char *dqt_block_end;
#if TRACE //写到trace文件中
fprintf(p_trace,"> DQT marker\n");
fflush(p_trace);
#endif
dqt_block_end = stream + be16_to_cpu(stream); //量化表最后的位置
stream += 2; /* Skip length */
while (stream < dqt_block_end)
{
qi = *stream++;
#if SANITY_CHECK
if (qi>>4)
snprintf(error_string, sizeof(error_string),"16 bits quantization table is not supported\n");
if (qi>4)
snprintf(error_string, sizeof(error_string),"No more 4 quantization table is supported (got %d)\n", qi);
#endif
table = priv->Q_tables[qi];
build_quantization_table(table, stream); //得到量化表
stream += 64;
}
#if TRACE
fprintf(p_trace,"< DQT marker\n");
fflush(p_trace);
#endif
return 0;
}
parse_DHT
这段函数用来解码Huffman码表,并同时将Huffman码表写入trace文件
static int parse_DHT(struct jdec_private *priv, const unsigned char *stream)
{
unsigned int count, i;
unsigned char huff_bits[17];
int length, index;
length = be16_to_cpu(stream) - 2; //表长(可能有多张表)
stream += 2; /* Skip length */
#if TRACE
fprintf(p_trace,"> DHT marker (length=%d)\n", length);
fflush(p_trace);
#endif
while (length>0) {
index = *stream++;
/* We need to calculate the number of bytes 'vals' will takes */
huff_bits[0] = 0;
count = 0;
for (i=1; i<17; i++) {
huff_bits[i] = *stream++;
count += huff_bits[i];
}
#if SANITY_CHECK
if (count >= HUFFMAN_BITS_SIZE)
snprintf(error_string, sizeof(error_string),"No more than %d bytes is allowed to describe a huffman table", HUFFMAN_BITS_SIZE);
if ( (index &0xf) >= HUFFMAN_TABLES)
snprintf(error_string, sizeof(error_string),"No more than %d Huffman tables is supported (got %d)\n", HUFFMAN_TABLES, index&0xf);
#if TRACE
fprintf(p_trace,"Huffman table %s[%d] length=%d\n", (index&0xf0)?"AC":"DC", index&0xf, count);
fflush(p_trace);
#endif
#endif
if (index & 0xf0 ) //AC系数的Huffman表
build_huffman_table(huff_bits, stream, &priv->HTAC[index&0xf]);
else //DC系数的Huffman表
build_huffman_table(huff_bits, stream, &priv->HTDC[index&0xf]);
length -= 1;
length -= 16;
length -= count;
stream += count;
}
#if TRACE
fprintf(p_trace,"< DHT marker\n");
fflush(p_trace);
#endif
return 0;
}
tinyjpeg_decode
真正解码的函数,以mcu为单位进行解码
int tinyjpeg_decode(struct jdec_private *priv, int pixfmt)
{
unsigned int x, y, xstride_by_mcu, ystride_by_mcu;
unsigned int bytes_per_blocklines[3], bytes_per_mcu[3];
decode_MCU_fct decode_MCU;
const decode_MCU_fct *decode_mcu_table;
const convert_colorspace_fct *colorspace_array_conv;
convert_colorspace_fct convert_to_pixfmt;
if (setjmp(priv->jump_state))
return -1;
/* To keep gcc happy initialize some array */
bytes_per_mcu[1] = 0;
bytes_per_mcu[2] = 0;
bytes_per_blocklines[1] = 0;
bytes_per_blocklines[2] = 0;
decode_mcu_table = decode_mcu_3comp_table;
switch (pixfmt) { //根据不同的存储格式进行不同的操作
case TINYJPEG_FMT_YUV420P: //这种格式使用的decode_mcu_table是decode_mcu_3comp_table
colorspace_array_conv = convert_colorspace_yuv420p;
if (priv->components[0] == NULL)
priv->components[0] = (uint8_t *)malloc(priv->width * priv->height);
if (priv->components[1] == NULL)
priv->components[1] = (uint8_t *)malloc(priv->width * priv->height/4);
if (priv->components[2] == NULL)
priv->components[2] = (uint8_t *)malloc(priv->width * priv->height/4);
bytes_per_blocklines[0] = priv->width;
bytes_per_blocklines[1] = priv->width/4;
bytes_per_blocklines[2] = priv->width/4;
bytes_per_mcu[0] = 8;
bytes_per_mcu[1] = 4;
bytes_per_mcu[2] = 4;
break;
case TINYJPEG_FMT_RGB24:
colorspace_array_conv = convert_colorspace_rgb24;
if (priv->components[0] == NULL)
priv->components[0] = (uint8_t *)malloc(priv->width * priv->height * 3);
bytes_per_blocklines[0] = priv->width * 3;
bytes_per_mcu[0] = 3*8;
break;
case TINYJPEG_FMT_BGR24:
colorspace_array_conv = convert_colorspace_bgr24;
if (priv->components[0] == NULL)
priv->components[0] = (uint8_t *)malloc(priv->width * priv->height * 3);
bytes_per_blocklines[0] = priv->width * 3;
bytes_per_mcu[0] = 3*8;
break;
case TINYJPEG_FMT_GREY:
decode_mcu_table = decode_mcu_1comp_table;
colorspace_array_conv = convert_colorspace_grey;
if (priv->components[0] == NULL)
priv->components[0] = (uint8_t *)malloc(priv->width * priv->height);
bytes_per_blocklines[0] = priv->width;
bytes_per_mcu[0] = 8;
break;
default:
#if TRACE
fprintf(p_trace,"Bad pixel format\n");
fflush(p_trace);
#endif
return -1;
}
//mcu的组织
//decode_mcu_table解码函数以指针的形式写,不同的格式解码函数不同
xstride_by_mcu = ystride_by_mcu = 8;
if ((priv->component_infos[cY].Hfactor | priv->component_infos[cY].Vfactor) == 1) {
decode_MCU = decode_mcu_table[0]; //使用的函数为decode_MCU_1x1_3planes
convert_to_pixfmt = colorspace_array_conv[0];
#if TRACE //根据trace_jpeg文件可以知道样例图片使用的是decode_mcu_table[0]函数
fprintf(p_trace,"Use decode 1x1 sampling\n");
fflush(p_trace);
#endif
} else if (priv->component_infos[cY].Hfactor == 1) {
decode_MCU = decode_mcu_table[1];
convert_to_pixfmt = colorspace_array_conv[1];
ystride_by_mcu = 16;
#if TRACE
fprintf(p_trace,"Use decode 1x2 sampling (not supported)\n");
fflush(p_trace);
#endif
} else if (priv->component_infos[cY].Vfactor == 2) {
decode_MCU = decode_mcu_table[3];
convert_to_pixfmt = colorspace_array_conv[3];
xstride_by_mcu = 16;
ystride_by_mcu = 16;
#if TRACE
fprintf(p_trace,"Use decode 2x2 sampling\n");
fflush(p_trace);
#endif
} else {
decode_MCU = decode_mcu_table[2];
convert_to_pixfmt = colorspace_array_conv[2];
xstride_by_mcu = 16;
#if TRACE
fprintf(p_trace,"Use decode 2x1 sampling\n");
fflush(p_trace);
#endif
}
resync(priv);
/* Don't forget to that block can be either 8 or 16 lines */
bytes_per_blocklines[0] *= ystride_by_mcu;
bytes_per_blocklines[1] *= ystride_by_mcu;
bytes_per_blocklines[2] *= ystride_by_mcu;
bytes_per_mcu[0] *= xstride_by_mcu/8;
bytes_per_mcu[1] *= xstride_by_mcu/8;
bytes_per_mcu[2] *= xstride_by_mcu/8;
/* Just the decode the image by macroblock (size is 8x8, 8x16, or 16x16) */
//循环解码,以mcu为单位进行核心解码
for (y=0; y < priv->height/ystride_by_mcu; y++)
{
//trace("Decoding row %d\n", y);
priv->plane[0] = priv->components[0] + (y * bytes_per_blocklines[0]);
priv->plane[1] = priv->components[1] + (y * bytes_per_blocklines[1]);
priv->plane[2] = priv->components[2] + (y * bytes_per_blocklines[2]);
for (x=0; x < priv->width; x+=xstride_by_mcu)
{
decode_MCU(priv); //核心函数
convert_to_pixfmt(priv);
priv->plane[0] += bytes_per_mcu[0];
priv->plane[1] += bytes_per_mcu[1];
priv->plane[2] += bytes_per_mcu[2];
if (priv->restarts_to_go>0)
{
priv->restarts_to_go--;
if (priv->restarts_to_go == 0)
{
priv->stream -= (priv->nbits_in_reservoir/8);
resync(priv);
if (find_next_rst_marker(priv) < 0)
return -1;
}
}
}
}
#if TRACE
fprintf(p_trace,"Input file size: %d\n", priv->stream_length+2);
fprintf(p_trace,"Input bytes actually read: %d\n", priv->stream - priv->stream_begin + 2);
fflush(p_trace);
#endif
return 0;
}
保存YUV文件
在原本的解码工程中,write_yuv函数将YUV三个分量分别写入了三个文件,因此直接对这个函数进行修改即可
static void write_yuv(const char *filename, int width, int height, unsigned char **components)
{
FILE *F;
char temp[1024];
//yuv写成三个文件
snprintf(temp, 1024, "%s.Y", filename);
F = fopen(temp, "wb");
fwrite(components[0], width, height, F);
fclose(F);
snprintf(temp, 1024, "%s.U", filename);
F = fopen(temp, "wb");
fwrite(components[1], width*height/4, 1, F);
fclose(F);
snprintf(temp, 1024, "%s.V", filename);
F = fopen(temp, "wb");
fwrite(components[2], width*height/4, 1, F);
fclose(F);
printf("write yuv begin!\n");
//yuv都写入一个文件
snprintf(temp, 1024, "%s.yuv", filename);
F = fopen(temp, "wb");
fwrite(components[0], width, height, F); //写Y
fwrite(components[1], width * height / 4, 1, F);
fwrite(components[2], width * height / 4, 1, F); //写UV
fclose(F); //关闭文件
printf("write yuv done!\n");
}
查看结果:
TRACE
在音视频编解码中,RACE是很关键的,它会随时将解码的相关信息写入文件,在后面可以通过查阅TRACE写的文件来理解解码到的信息分别都是什么或判断是否正确解码
#define TRACE 1//add by nxn //设为1表示打开trace,也就是会编解码边将信息写入文件,设为0表示不会编解码边写入文件
#define TRACEFILE "trace_jpeg.txt"//add by nxn //定义输出的trace文件的文件名
#if TRACE //设定trace,边解码边写入文件
.......
#endif
量化矩阵和Huffman码表
在tinyjpeg.h中添加存储量化表和Huffman码表的文件:
FILE* q_trace; //存储量化表
FILE* h_trace; //存储Huffman码表
#define Q_FILE "DQT.txt" //输出量化表
#define H_FILE "Huffman.txt" //输出Huffman码表
在main函数中将两个文件打开:
//打开存储量化表文件
q_trace = fopen(Q_FILE, "w");
if (q_trace == NULL)
{
printf("Q_trace file open error!");
}
//打开存储Huffman码表文件
h_trace = fopen(H_FILE, "w");
if (q_trace == NULL)
{
printf("H_trace file open error!");
}
程序结束后记得关闭文件
量化矩阵
在parse_DQT
的循环函数中添加下面的语句,将量化表的ID写入文件:
//文件中写入量化表ID
fprintf(q_trace, "Quantization_table_index: %d \n", qi);
fflush(q_trace);
之后在build_quantization_table
函数中添加下面的语句,以矩阵的形式输出量化表:
#if TRACE
const unsigned char* z = zigzag; //以矩阵形式输出量化表
for (i = 0; i < 8; i++) {
for (j = 0; j < 8; j++) {
fprintf(q_trace, "%-02d\t", ref_table[*z++]);
}
fprintf(q_trace, '\n');
}
fflush(q_trace);
#endif
调试程序得到结果:其中index=0的量化表是亮度分量的量化表,index=1的量化表是色度分量的量化表
Huffman码表
在parse_DHT
函数的trace中添加下面语句,将Huffman码表的信息写入文件:
fprintf(h_trace, "Huffman table %s index:%d length=%d\n", (index & 0xf0) ? "AC" : "DC", index & 0xf, count);
fflush(h_trace);
在build_huffman_table
函数的trace中添加下面语句,将Huffman码表输出:
fprintf(h_trace, "val=%2.2x code=%8.8x codesize=%2.2d\n", val, code, code_size);
fflush(h_trace);
调试程序得到结果:
-
亮度分量
-
DC:
-
AC:
-
-
色度分量:
-
DC:
-
AC:
-
DC图像、AC图像及其概率分布
DC图像就是直流分量对应的图像,AC图像就是交流分量对应的图像。
对于8×8的宏块来说,其左上角的值就是直流分量的值即DC分量,因为原图是1024×1024,那么输出的DC图像的大小就是128×128。
在实验的过程中设定了存储DCT分量的数组,因此从中取出DCT[0]组成图像即为DC图像,从后面取出交流系数的值就是AC图像,实验中取DCT[1]、DCT[3]和DCT[10]查看不同的AC分量值组成的图像。
因为DC分量的值范围是[-512,512],为了使得图像可以显示,将其调整到0~255的范围,因为AC值比较小,因此直接+128使其能够显示
在tinyjpeg_decode
函数中进行修改即可取出对应的DCT系数:
int tinyjpeg_decode(struct jdec_private *priv, int pixfmt)
{
......
FILE* DCfile;
FILE* ACfile_1, * ACfile_3, * ACfile_10;
DCfile = fopen("dc_0.yuv", "w");
ACfile_1 = fopen("ac_1.yuv", "w");
ACfile_3 = fopen("ac_3.yuv", "w");
ACfile_10 = fopen("ac_10.yuv", "w");
unsigned char* uvbuf = 128; //将DC系数和AC系数认为是Y分量,uv分量统一设置为128
unsigned char* DCbuf, * ACbuf_1, * ACbuf_3, * ACbuf_10;
int count = 0; //统计“Y”分量的数量
......
decode_mcu_table = decode_mcu_3comp_table;
switch (pixfmt) { //根据不同的存储格式进行不同的操作
case TINYJPEG_FMT_YUV420P: //这种格式使用的decode_mcu_table是decode_mcu_3comp_table
colorspace_array_conv = convert_colorspace_yuv420p;
if (priv->components[0] == NULL)
priv->components[0] = (uint8_t *)malloc(priv->width * priv->height);
if (priv->components[1] == NULL)
priv->components[1] = (uint8_t *)malloc(priv->width * priv->height/4);
if (priv->components[2] == NULL)
priv->components[2] = (uint8_t *)malloc(priv->width * priv->height/4);
bytes_per_blocklines[0] = priv->width;
bytes_per_blocklines[1] = priv->width/4;
bytes_per_blocklines[2] = priv->width/4;
bytes_per_mcu[0] = 8;
bytes_per_mcu[1] = 4;
bytes_per_mcu[2] = 4;
break;
......
}
//mcu的组织
//decode_mcu_table解码函数以指针的形式写,不同的格式解码函数不同
xstride_by_mcu = ystride_by_mcu = 8;
if ((priv->component_infos[cY].Hfactor | priv->component_infos[cY].Vfactor) == 1) {
decode_MCU = decode_mcu_table[0]; //使用的函数为decode_MCU_1x1_3planes
convert_to_pixfmt = colorspace_array_conv[0];
#if TRACE //根据trace_jpeg文件可以知道样例图片使用的是decode_mcu_table[0]函数
fprintf(p_trace,"Use decode 1x1 sampling\n");
fflush(p_trace);
#endif
} else if ......
......
//循环解码,以mcu为单位进行核心解码
for (y=0; y < priv->height/ystride_by_mcu; y++)
{
......
for (x=0; x < priv->width; x+=xstride_by_mcu)
{
decode_MCU(priv); //核心函数
DCbuf = (unsigned char)((priv->component_infos->DCT[0] + 512)/4.0);
fwrite(&DCbuf, 1, 1, DCfile);
ACbuf_1 = (unsigned char)(priv->component_infos->DCT[1] + 128);
fwrite(&ACbuf_1, 1, 1, ACfile_1);
ACbuf_3 = (unsigned char)(priv->component_infos->DCT[3] + 128);
fwrite(&ACbuf_3, 1, 1, ACfile_3);
ACbuf_10 = (unsigned char)(priv->component_infos->DCT[10] + 128);
fwrite(&ACbuf_10, 1, 1, ACfile_10);
count++;
......
}
}
#if TRACE
fprintf(p_trace,"Input file size: %d\n", priv->stream_length+2);
fprintf(p_trace,"Input bytes actually read: %d\n", priv->stream - priv->stream_begin + 2);
fflush(p_trace);
#endif
for (int j = 0; j < count * 0.25 * 2; j++)
{
fwrite(&uvbuf, sizeof(unsigned char), 1, DCfile);
fwrite(&uvbuf, sizeof(unsigned char), 1, ACfile_1);
fwrite(&uvbuf, sizeof(unsigned char), 1, ACfile_3);
fwrite(&uvbuf, sizeof(unsigned char), 1, ACfile_10);
}
fclose(DCfile);
fclose(ACfile_1);
fclose(ACfile_3);
fclose(ACfile_10);
return 0;
}
之后编写如下函数进行概率统计:
# include<iostream>
using namespace std;
#pragma warning(disable:4996);
void frequency(unsigned char* buffer, double* frequency, int size) {
for (int i = 0; i < size; i++) {
frequency[buffer[i]] += 1;
}
for (int i = 0; i < 256; i++) {
frequency[i] = frequency[i] / size;
}
}
int main() {
int size = 128 * 128;
FILE* DCfile;
FILE* ACfile_1, * ACfile_3, * ACfile_10;
FILE* ori;
DCfile = fopen("dc_0.yuv", "rb");
if (!DCfile == NULL) {
cout << "open dcfile!" << endl;
}
ACfile_1 = fopen("ac_1.yuv", "rb");
if (!ACfile_1 == NULL) {
cout << "open acfile_1" << endl;
}
ACfile_3 = fopen("ac_3.yuv", "rb");
if (!ACfile_3 == NULL) {
cout << "open acfile_3" << endl;
}
ACfile_10 = fopen("ac_10.yuv", "rb");
if (!ACfile_10 == NULL) {
cout << "open acfile_10" << endl;
}
ori = fopen("outyuv.yuv", "rb");
if (!ori == NULL) {
cout << "open orifile!" << endl;
}
unsigned char* DCbuf_y = new unsigned char[size];
unsigned char* ACbuf_1_y = new unsigned char[size];
unsigned char* ACbuf_3_y = new unsigned char[size];
unsigned char* ACbuf_10_y = new unsigned char[size];
unsigned char* ori_y = new unsigned char[1024 * 1024];
fread(ori_y, 1, 1024 * 1024, ori);
fread(DCbuf_y, 1, size, DCfile);
fread(ACbuf_1_y, 1, size, ACfile_1);
fread(ACbuf_3_y, 1, size, ACfile_3);
fread(ACbuf_10_y, 1, size, ACfile_10);
FILE* DCfre, * ACfre_1, * ACfre_3, *ACfre_10;
FILE* ori_fre;
DCfre = fopen("DCfre.txt", "wb");
if (!DCfre == NULL) {
cout << "dcfre open!" << endl;
}
ACfre_1 = fopen("ACfre_1.txt", "wb");
if (!ACfre_1 == NULL) {
cout << "acfre_1 open!" << endl;
}
ACfre_3 = fopen("ACfre_3.txt", "wb");
if (!ACfre_3 == NULL) {
cout << "acfre_1 open!" << endl;
}
ACfre_10 = fopen("ACfre_10.txt", "wb");
if (!ACfre_10 == NULL) {
cout << "acfre_1 open!" << endl;
}
ori_fre = fopen("orifre.txt", "wb");
if (!ori_fre == NULL) {
cout << "orifre open!" << endl;
}
double freori[256] = { 0 }, fredc[256] = { 0 }, freac1[256] = { 0 }, freac3[256] = { 0 }, freac10[256] = { 0 };
frequency(ori_y, freori, 1024 * 1024);
fprintf(ori_fre, "%s\t%s\n", "symbol", "freq");
for (int i = 0; i < 256; i++)
{
fprintf(ori_fre, "%d\t%f\n", i, freori[i]);
}
frequency(DCbuf_y, fredc, size);
fprintf(DCfre, "%s\t%s\n", "symbol", "freq");
for (int i = 0; i < 256; i++)
{
fprintf(DCfre, "%d\t%f\n", i, fredc[i]);
}
frequency(ACbuf_1_y, freac1, size);
fprintf(ACfre_1, "%s\t%s\n", "symbol", "freq");
for (int i = 0; i < 256; i++)
{
fprintf(ACfre_1, "%d\t%f\n", i, freac1[i]);
}
frequency(ACbuf_3_y, freac3, size);
fprintf(ACfre_3, "%s\t%s\n", "symbol", "freq");
for (int i = 0; i < 256; i++)
{
fprintf(ACfre_3, "%d\t%f\n", i, freac3[i]);
}
frequency(ACbuf_10_y, freac10, size);
fprintf(ACfre_10, "%s\t%s\n", "symbol", "freq");
for (int i = 0; i < 256; i++)
{
fprintf(ACfre_10, "%d\t%f\n", i, freac10[i]);
}
fclose(DCfile);
fclose(ACfile_1);
fclose(ACfile_3);
fclose(ACfile_10);
cout << "end!" << endl;
return 0;
}
得到的结果图和概率分布图如下:
名称 | yuv图像 | 概率分布 |
---|---|---|
原图(只统计Y分量的概率分布) | ![]() | ![]() |
DC系数图 | ![]() | ![]() |
第1个AC系数图 | ![]() | ![]() |
第3个AC系数图 | ![]() | ![]() |
第10个AC系数图 | ![]() | ![]() |
通过概率分布图可以看出,DC图的概率分布与原图的概率分布大致相同,AC系数越往后概率分布越集中在128附近。因为在进行图像输出的时候将AC系数的值抬高了128,因此实际上越往后AC系数的值越集中在0值附近,也就是说高频分量更多的集中在0附近,这说明DCT变换将大部分的能量都集中在了左上角,右下角的值都比较小,利用这一特性可以很好的进行图片的压缩
实验总结
本次实验调试了JPEG图像的解码程序,理解了JPEG编解码的基本过程,学习到了在数据压缩实验中TRACE的用途和使用方法。
关于JPEG编码,我感觉就是利用了人眼的一些视觉特性将一些人眼不敏感的数据和一些冗余数据去掉。
通常会将高度相关的RGB分量转换成相关性较小的YUV分量,零偏置的可以使像素的绝对值减小,之后利用DCT变换进行能量集中,进一步提高数据压缩的空间,也因为DCT变换后DC系数和AC系数的不同特点,量化后对DC系数采取差分+Huffman编码的方式,对AC系数采取Z字形扫描+游程编码+Huffman编码的方式,这样可以进一步提高压缩率。