[实验五]JPEG原理分析及JPEG解码器的调试

目录

 

一、JPEG文件格式

1.1 JPEG概述

1.2 Segment

1.2.1 SOI

1.2.2 EOI

1.2.3 APP0

1.2.4 DQT

1.2.5 SOF0

1.2.6 DHT

1.2.7 SOS

1.3 JPEG文件编码

1.3.1 零偏置(Level Offset)

1.3.2 DCT变换

1.3.3 量化

 1.3.4 DC系数差分编码

1.3.5 AC系数之字形扫描

1.3.6 AC系数的游程编码

 1.3 JPEG文件编码

二、JPEG文件解析

2.1 实验程序说明

2.2 将输出文件保存为可供YUVViewer观看的YUV文件

2.3 以txt文件输出所有的量化矩阵和所有的HUFFMAN码表

2.3.1 输出量化矩阵

2.3.2 输出HUFFMAN码表

2.4 输出DC图像和AC图像并统计其概率分布

一、JPEG文件格式

1.1 JPEG概述

JPEG(Joint Photographic Experts Group)是JPEG标准的产物,该标准由国际标准化组织(ISO)制订,是面向连续色调静止图像的一种压缩标准JPEG格式是最常用的图像文件格式,后缀名为.jpg或.jpeg。

JPEG规定了4种运行模式,以满足不同需要,分别是:

  1. 基于DPCM的无损编码模式:压缩比可达2:1
  2. 基于DCT的有损顺序编码模式:压缩比可达10:1以上
  3. 基于DCT的递增编码模式
  4. 基于DCT的分层编码模式

本实验中采用的JPEG文件为基于DCT的有损顺序编码模式。

1.2 Segment

在JPEG文件格式中,不同类型的数据以段(segment)为单位保存在文件中,在不同的JPEG文件中段的多少和长度并不是一定的,一个JPEG文件可以包含多个段。

段的共同特点为:

  • 每一个段的前两个字节为段的标识,用来表示段所属的类型。标识的第一个字节为0xFF,第二个依据不同段的类型而不同。
  • 每一个段中紧接着段标识的后两个字节为段的长度(除去标识之后的长度)
  • 段中的数据都是高位在前低位在后

下面介绍各个段类型

1.2.1 SOI

图像开始,标识为0xFFD8

1.2.2 EOI

图像结束,标识为0xFFD9

1.2.3 APP0

应用程序保留标记0,标识为0xFFE0,包含9个具体字段:

  1. 数据长度,占2字节,为1~9字段的总长度
  2. 标识符,占5字节,固定值0x4A46494600,即字符串“JFIF0”
  3. 版本号,占2字节,一般是0x0102,标识JFIF的版本号1.2
  4. X和Y的密度单位,占1字节,只有三个值可选:0-无单位、1-点数/英寸、2-点数/厘米
  5. X方向像素密度,占2字节
  6. Y方向像素密度,占2字节
  7. 缩略图水平像素数目,占1字节
  8. 缩略图垂直像素数目,占1字节
  9. 缩略图RGB位图

1.2.4 DQT

定义量化表,标识为0xFFDB,包含以下具体字段:

  1. 数据长度,占2字节
  2. 量化表,在DQT段中可以重复出现,表示多个量化表,但最多只能出现4次
    1. 数据长度,占2字节
    2. 精度及量化表ID,占1字节。高4位为精度,只有两个值可选:0-8位、1-16位。低4位为量化表ID,取值范围为0~3
    3. 表项,占(64×(精度+1))字节

1.2.5 SOF0

帧图像开始,标识为0xFFC0,包含9个具体字段:

  1. 数据长度,占2字节
  2. 精度,占1字节,每个数据样本的位数。通常是8位,一般软件都不支持12位和16位
  3. 图像高度,占2字节,单位:像素
  4. 图像宽度,占2字节,单位:像素
  5. 颜色分量数,占1字节,只有3个数值可选:1-灰度图、3-YCrCb或YIQ、4-CMYK
  6. 颜色分量信息,颜色分量数×3字节(通常为9字节)
    1. 颜色分量ID,占1字节
    2. 水平/垂直采样因子,占1字节。高四位:水平采样因子、低四位:垂直采样因子
    3. 量化表,占1字节。当前分量使用的量化表的ID

1.2.6 DHT

定义哈夫曼表,标识符为0xFFC4,包含以下字段:

  1. 数据长度,占2字节
  2. huffman表,占(数据长度-2)字节,在DHT段中可以重复出现(一般4次),也可以只出现1次
  3. 表ID和表类型,占1字节
    • 高4位:类型,只有两个值可选:0-DC直流、1-AC交流
    • 低4位:哈夫曼表ID
  4. 不同位数的码字数量,占16个字节
  5. 16个不同位数的码字数量之和

1.2.7 SOS

扫描开始,标识符为0xFFDA,包含以下字段:

  1. 数据长度,占2字节
  2. 颜色分量数,占1字节。应该和SOF中的字段5的值相同
  3. 颜色分量信息
    1. 颜色分量ID,占1字节
    2. 直流/交流系数表号,占1字节。
      1. 高4位:直流分量使用的哈夫曼树编号
      2. 低4位:交流分量使用的哈夫曼树编号
  4. 压缩图像数据
    1. 谱选择开始,占1字节,固定值0x00
    2. 谱选择结束,占1字节,固定值0x3F
    3. 谱选择,占1字节,在基本JPEG中总为00

1.3 JPEG文件编码

JPEG编码的过程如图所示:

 编码各步骤如下

1.3.1 零偏置(Level Offset)

对于灰度级是2^n的像素,通过减去2^(n-1),将无符号的整数值变成有符号数,使像素的绝对值出现3位10进制的概率大大减少

1.3.2 DCT变换

对每个单独的彩色图像分量,把整个分量图像分成8×8的图像块,并作为二维离散余弦变换DCT的输入。将原本的像素数值进行DCT变换,后续直接对DCT分量进行处理。

1.3.3 量化

对DCT分量进行量化,由于人眼对亮度信号比对色差信号更敏感,因此使用了两种量化表:亮度量化值和色差量化值。

根据人眼的视觉特性(对低频敏感,对高频不太敏感)对低频分量采取较细的量化,对高频分量采取较粗的量化。

如果原始图象中细节丰富,则去掉的数据较多,量化后的系数与量化前差别。反之,细节少的原始图象在压缩时去掉的数据少些。

JPEG提供了两种建议量化表(但现在很少用):

左:亮度量化表      右:色差量化表

 1.3.4 DC系数差分编码

DC直流系数有两个特点:系数的数值比较大,相邻DC系数值变化不大。

根据上述两个特点,JPEG算法使用了差分脉冲调制编码(DPCM)技术,对相邻图像块之间量化DC系数的差值DIFF进行编码:

DIFF_k=DC_k-DC_{k-1}

1.3.5 AC系数之字形扫描

由于经DCT变换后,系数大多数集中在左上角,即低频分量区,因此采用Z字形按频率的高低顺序读出,可以出现很多连零的机会。可以使用游程编码。尤其在最后,如果都是零,给出 EOB (End of Block)即可。

之字形扫描示例

1.3.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)

编码

run:最大值15,用4位标识为RRRR

level:类似DC,分成16个类别,用4位标识为SSSS

对(RRRR,SSSS)联合用Huffman编码

对类内索引用定长码编码

 1.3 JPEG文件编码

JPEG文件的解码过程与编码过程相反,包括以下步骤:

  1. 解码Huffman数据、
  2. 解码DC差值
  3. 重构量化后的系数
  4. DCT逆变换
  5. 丢弃填充的行/列
  6. 反0偏置
  7. 对丢失的CbCr分量插值
  8. 将YCbCr通道数据变为RGB通道数据

二、JPEG文件解析

2.1 实验程序说明

实验程序采用C语言编写,共包含stdint.h、tinyjpeg.h、tinyjpeg-internal.h三个头文件以及tinyjpeg.c、loadjpeg.c、jidctflt.c三个源代码文件。

  • stdint.h定义了程序内变量的类型。
  • tinyjpeg.h声明了tynyjpeg.c中的函数。
  • tinyjpeg-internal.h中定义了jidctflt.c中的函数,同时定义了解码中所用到的结构体如用于存放霍夫曼编码表的huffman_table。
  • tinyjpeg.c中定义了解码所用的一些函数
  • loadjpeg.c中定义了读取jpeg文件的一些函数,同时定义了程序的主入口函数main()
  • jidcflt.c中定义了对数据的一些处理函数

在程序编译完成后会生成jpeg_mindec.exe可执行文件,命令格式如下:

jpeg_mindec.exe [options] <input_filename.jpeg> <format> <output_filename>
  • 第一个参数[options]为固定值--benchmark,可有可无,加上该参数时将对这幅图像解码1000次,验证解码器效果
  • 第二个参数<input_filename.jpeg>为输入的jpeg文件
  • 第三个参数<format>为转换格式,有yuv420p、rgb24、bgr24、gray四种模式可选
  • 第四个参数<output_filename>为输出文件名

实验用测试图像test.jpg如下:

测试文件test.jpg

输入命令: jpeg_mindec.exe test.jpg yuv420p out

最终输出trace_jpeg.txt及out.U、out.V、out.Y三个通道数据文件

trace_jpeg.txt记录了程序运行过程中创建的变量数据,如霍夫曼表等。

out.U、out.V、out.Y三个文件分别存放了以4:2:0取样格式取样得到的三个通道数据。 

2.2 将输出文件保存为可供YUVViewer观看的YUV文件

在loadjpeg.c文件中定义了函数write_yuv()输出.Y、.U和.V三个通道的数据文件,仅需将这三个文件的数据保存在一个文件中即可得到可供YUVViewer观看的YUV文件。

修改write_yuv()函数如下:

static void write_yuv(const char *filename, int width, int height, unsigned char **components) {
    FILE *F;
    char temp[1024];

    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);
    /*将三个通道的数据均保存在yuv文件中*/
    snprintf(temp, 1024, "%s.yuv", filename);
    F = fopen(temp, "wb+");
    fwrite(components[0], width, height, F);
    fwrite(components[1], width, height/4, F);
    fwrite(components[2], width, height/4, F);
    fclose(F);
}

而后以yuv420p模式运行程序,即可生成.yuv文件。

输出文件out.yuv

2.3 以txt文件输出所有的量化矩阵和所有的HUFFMAN码表

2.3.1 输出量化矩阵

在tinyjpeg.c文件中定义了函数build_quantization_table()来建立量化表,修改函数内容即可在运行程序的同时输出量化矩阵

static void build_quantization_table(float *qtable, const unsigned char *ref_table) {

    int i, j;
    static const double aanscalefactor[8] = {
            1.0, 1.387039845, 1.306562965, 1.175875602,
            1.0, 0.785694958, 0.541196100, 0.275899379
    };
    const unsigned char *zz = zigzag;

    FILE* QTfile = fopen("QTfile.txt","a");//打开txt文档
    fprintf(QTfile,"量化矩阵:\n");
    for (i = 0; i < 8; i++) {
        for (j = 0; j < 8; j++) {
            *qtable++ = ref_table[*zz++] * aanscalefactor[i] * aanscalefactor[j];
            fprintf(QTfile, "%d\t", ref_table[*zz]);//将量化矩阵输出到txt文档中
        }
        fprintf(QTfile,"\n");//每输出一行后换行
    }
}

运行程序,生成QTfile.txt文件,量化矩阵如下:

2.3.2 输出HUFFMAN码表

在trace_jpeg.txt文件中会输出霍夫曼码表,通过在头文件中宏定义TRACE的值,而后在源代码文件中多处设有判定TRACE的语句,若预先宏定义其值为1,则会编译相关语句,例如在tinyjpeg.c文件中的build_huffman_table()函数中就有:

#if TRACE
        fprintf(p_trace, "val=%2.2x code=%8.8x codesize=%2.2d\n", val, code, code_size);
        fflush(p_trace);
#endif

上述语句块是用于将霍夫曼码表输出至trace_jpeg.txt文件,为便于查看,让霍夫曼码表输出到另一个文档HTfile.txt中。

修改build_huffman_table()和parse_DHT()函数即可实现将霍夫曼码表输出在指定txt文档中。

修改后的build_huffman_table()函数如下:

static void build_huffman_table(const unsigned char *bits, const unsigned char *vals, struct huffman_table *table) {
    unsigned int i, j, code, code_size, val, nbits;
    unsigned char huffsize[HUFFMAN_BITS_SIZE + 1], *hz;
    unsigned int huffcode[HUFFMAN_BITS_SIZE + 1], *hc;
    int next_free_entry;
    /*
     * Build a temp array
     *   huffsize[X] => numbers of bits to write vals[X]
     */
    hz = huffsize;
    for (i = 1; i <= 16; i++) {
        for (j = 1; j <= bits[i]; j++)
            *hz++ = i;
    }
    *hz = 0;

    memset(table->lookup, 0xff, sizeof(table->lookup));
    for (i = 0; i < (16 - HUFFMAN_HASH_NBITS); i++)
        table->slowtable[i][0] = 0;

    /* Build a temp array
     *   huffcode[X] => code used to write vals[X]
     */
    code = 0;
    hc = huffcode;
    hz = huffsize;
    nbits = *hz;
    while (*hz) {
        while (*hz == nbits) {
            *hc++ = code++;
            hz++;
        }
        code <<= 1;
        nbits++;
    }

    /*
     * Build the lookup table, and the slowtable if needed.
     */
    next_free_entry = -1;
    FILE *HTfile = fopen("HTfile.txt", "a");//打开txt文档
    for (i = 0; huffsize[i]; i++) {
        val = vals[i];
        code = huffcode[i];
        code_size = huffsize[i];

        fprintf(HTfile, "val=%2.2x code=%8.8x codesize=%2.2d\n", val, code, code_size);//输出霍夫曼码表至txt文档
#if TRACE
        fprintf(p_trace, "val=%2.2x code=%8.8x codesize=%2.2d\n", val, code, code_size);
        fflush(p_trace);
#endif
        table->code_size[val] = code_size;
        if (code_size <= HUFFMAN_HASH_NBITS) {
            /*
             * Good: val can be put in the lookup table, so fill all value of this
             * column with value val
             */
            int repeat = 1UL << (HUFFMAN_HASH_NBITS - code_size);
            code <<= HUFFMAN_HASH_NBITS - code_size;
            while (repeat--)
                table->lookup[code++] = val;

        } else {
            /* Perhaps sorting the array will be an optimization */
            uint16_t *slowtable = table->slowtable[code_size - HUFFMAN_HASH_NBITS - 1];
            while (slowtable[0])
                slowtable += 2;
            slowtable[0] = code;
            slowtable[1] = val;
            slowtable[2] = 0;
            /* TODO: NEED TO CHECK FOR AN OVERFLOW OF THE TABLE */
        }

    }
    fclose(HTfile);
}

修改后的parse_DHT()函数如下:

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
    FILE *HTfile = fopen("HTfile.txt", "a");//打开txt文档
    fprintf(HTfile, "> DHT marker (length=%d)\n", length);
    fclose(HTfile);
    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
        HTfile = fopen("HTfile.txt", "a");//打开txt文档
        fprintf(HTfile, "Huffman table %s[%d] length=%d\n", (index & 0xf0) ? "AC" : "DC", index & 0xf,
                count);//输出霍夫曼码表信息
        fclose(HTfile);

        if (index & 0xf0)
            build_huffman_table(huff_bits, stream, &priv->HTAC[index & 0xf]);
        else
            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
    HTfile = fopen("HTfile.txt", "a");//打开txt文档
    fprintf(HTfile, "> DHT marker (length=%d)\n", length);
    fclose(HTfile);
    return 0;
}

运行程序,生成HTfile.txt,输出霍夫曼码表如下:

2.4 输出DC图像和AC图像并统计其概率分布

解码函数为tinyjpeg.c中的tinyjpeg_decode(),因此修改其中内容如下:

    /....../

    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;

    /*修改部分*/
    int file_length = 0;
    FILE *DC_pic = fopen("DC.yuv", "wb+");
    FILE *AC_pic = fopen("AC.yuv", "wb+");
    /*修改部分结束*/

    /* Just the decode the image by macroblock (size is 8x8, 8x16, or 16x16) */
    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);
            /*修改部分开始*/
            unsigned char dc;
            unsigned char ac;
            dc = (unsigned char) ((priv->component_infos->DCT[0] + 512) / 4);//DC系数的取值范围为[-512,511],需要将其转换为[0,255]的范围
            ac = (unsigned char) (priv->component_infos->DCT[1] + 128);//AC系数的取值范围为[-128,127],将其转换为[0,255]的范围
            fwrite(&ac, 1, 1, AC_pic);//写入分量
            file_length++;//像素数+1
            /*修改部分结束*/

            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;
                }
            }
        }
    }
    /*修改部分开始*/
    unsigned char uv = 128;
    for (int i = 0; i < file_length / 4 * 2; i++) {
        fwrite(&uv, 1, 1, DC_pic);
        fwrite(&uv, 1, 1, AC_pic);//写入UV通道数据,黑白图像全置为128
    }
    fclose(DC_pic);//关闭图像文件
    fclose(AC_pic);
    /*修改部分结束*/

    /....../

而后运行程序将同时输出DC系数图像DC.yuv和AC系数图像AC.yuv。其中AC图像中输出的AC系数选取为第一个AC系数。

DC.yuv

AC.yuv

注意由于只输出8x8块中的一个系数,因此图像高宽都会变为原来的1/8,原图像为1024x1024的大小,因此输出图像大小只有128x128。

DC和AC图像对应的概率分布如下:

DC图像概率分布
DC图像概率分布

AC图像概率分布

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值