数据压缩(十一)——JPEG原理分析及JPEG解码器调试


一、JPEG编码原理

  JPEG 是Joint Photographic Experts Group(联合图像专家小组)的缩写,是第一个国际图像压缩标准。JPEG图像压缩算法能够在提供良好的压缩性能的同时,具有比较好的重建质量,被广泛应用于图像、视频处理领域。人们日常碰到的“.jpeg”、‘’.jpg“等指代的是图像数据经压缩编码后在媒体上的封存形式,不能与JPEG压缩标准混为一谈。
在这里插入图片描述

颜色空间

  JPEG标准本身没有规定具体的颜色空间,只是对各分量分别进行编码。
  实现中通常将高度相关RGB颜色空间转换到相关性较小的YUV颜色空间。
RGB to YUV:
Y = ( ( 66 × R + 129 × G + 25 × B + 128 ) > > 8 ) + 16 U = ( ( − 38 × R − 74 × G + 112 × B + 128 ) > > 8 ) + 128 V = ( ( 112 × R − 94 × G − 18 × B + 128 ) > > 8 ) + 128 \begin{aligned} Y=&((66\times R+129\times G+25\times B+128)>>8)+16 \\ U=&((-38\times R-74\times G+112\times B+128)>>8)+128 \\ V=&((112\times R-94\times G-18\times B+128)>>8)+128 \end{aligned} Y=U=V=((66×R+129×G+25×B+128)>>8)+16((38×R74×G+112×B+128)>>8)+128((112×R94×G18×B+128)>>8)+128
YUV to RGB:
C = Y − 16 D = U − 128 E = V − 128 R = c l i p ( ( 298 × C + 409 × E + 128 ) > > 8 ) G = c l i p ( ( 298 × C − 100 × D − 208 × E + 128 ) > > 8 ) B = c l i p ( ( 298 × C + 516 × D + 128 ) > > 8 ) \begin{aligned} C=&Y-16 \\ D=&U-128 \\ E=&V-128 \\ R=&clip((298\times C+409\times E+128)>>8)\\ G=&clip((298\times C-100\times D-208\times E+128)>>8)\\ B=&clip((298\times C+516\times D+128)>>8) \end{aligned} C=D=E=R=G=B=Y16U128V128clip((298×C+409×E+128)>>8)clip((298×C100×D208×E+128)>>8)clip((298×C+516×D+128)>>8)【clip:将值限制在[0,255]的范围内】

零电平偏置

  DC电平偏移的目的是保证输入图像的采样有近似地集中在零附近的动态范围。DC电平偏移执行的图像采样只通过无符号数表示。
  方法:假设图片分量的采样精度为 n n n,那么分量中的每个像素值应减去 2 n − 1 2^{n-1} 2n1

DCT变换

  DCT(DiscreteCosineTransform)是将图像信号在频率域上进行变换,分离出高频和低频信息的处理过程。然后再对图像的高频部分(即图像细节)进行压缩,以实现能量集中和去相关,便于去除空间冗余,以达到压缩图像数据的目的。
  首先将图像划分为多个8*8的矩阵【如果图像的宽和高不是8的整数倍,那么使用图像的边缘像素进行填充】。然后对每一个矩阵作DCT变换。变换后得到一个频率系数矩阵,其中的频率系数都是浮点数。
  经过DCT变换后,图像中的低频分量会集中在左上角,由于图像低频能量高,故而左上角数值大,右下角有较多的0值。

DCT变换的计算:
F ( u , v ) = 1 4 C ( u ) C ( v ) [ Σ i = 0 7 Σ j = 0 7 f ( i , j ) c o s ( 2 i + 1 ) u π 16 c o s ( 2 j + 1 ) v π 16 ] F(u,v)=\frac{1}{4}C(u)C(v)[\Sigma^7_{i=0}\Sigma^7_{j=0}f(i,j)cos\frac{(2i+1)u\pi}{16}cos\frac{(2j+1)v\pi}{16}] F(u,v)=41C(u)C(v)[Σi=07Σj=07f(i,j)cos16(2i+1)uπcos16(2j+1)vπ]
其中, { C ( u ) , C ( v ) = 1 2 u , v = 0 C ( u ) , C ( v ) = 1 o t h e r w i s e \left\{ \begin{aligned} C(u),C(v)=\frac{1}{\sqrt{2}} &&u,v=0 \\ C(u),C(v)=1 && otherwise \end{aligned} \right. C(u),C(v)=2 1C(u),C(v)=1u,v=0otherwise

量化

  JPEG中采用中平型均匀量化器。
在这里插入图片描述
  根据人眼的视觉特性:

  • 人眼对低频敏感,对高频不敏感【对低频进行细量化,高频进行粗量化】
  • 人眼对亮度信号敏感,对色度信号不敏感【亮度和色度分量分别采用不同的量化表】
    JPEG标准建议量化表:
    1.亮度量化表
    [ 16 11 10 16 24 40 51 61 12 12 14 19 26 58 60 55 14 13 16 24 40 57 69 56 14 17 22 29 51 87 80 62 18 22 37 56 68 109 103 77 24 36 55 64 81 104 113 92 49 64 78 87 103 121 120 101 72 92 95 98 112 100 103 99 ] \begin{bmatrix}16&11&10&16&24&40&51&61\\12&12&14&19&26&58&60&55\\14&13&16&24&40&57&69&56\\14&17&22&29&51&87&80&62\\18&22&37&56&68&109&103&77\\24&36&55&64&81&104&113&92\\49&64&78&87&103&121&120&101\\72&92&95&98&112&100&103&99\end{bmatrix} 1612141418244972111213172236649210141622375578951619242956648798242640516881103112405857871091041211005160698010311312010361555662779210199
    2.色度量化表
    [ 17 18 24 47 99 99 99 99 18 21 26 66 99 99 99 99 24 26 56 99 99 99 99 99 47 66 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 ] \begin{bmatrix}17&18&24&47&99&99&99&99\\18&21&26&66&99&99&99&99\\24&26&56&99&99&99&99&99\\47&66&99&99&99&99&99&99\\99&99&99&99&99&99&99&99\\99&99&99&99&99&99&99&99\\99&99&99&99&99&99&99&99\\99&99&99&99&99&99&99&99\\\end{bmatrix} 17182447999999991821266699999999242656999999999947669999999999999999999999999999999999999999999999999999999999999999999999999999
    量化表缩放
    真正的量化表=缩放因子 × \times ×基本量化表

DC系数差分编码

  8×8图像块经过DCT变换之后得到的DC直流系数有两个特点:

  • 系数的数值比较大
  • 相邻8×8图像块的DC系数值变化不大

  根据这两个特点, JPEG算法使用了差分脉冲调制编码(DPCM)技术,对相邻图像块之间量化DC系数的差值DIFF进行huffma编码: D I F F k = D C k − D C k − 1 DIFF_k=DC_k-DC_{k-1} DIFFk=DCkDCk1

AC系数的之字形扫描

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

AC系数的游程编码

  在JPEG和MPEG编码中规定(run,level):表示连续run各0,后面跟值为level的系数

Huffman编码

Huffman表存储方式举例

在这里插入图片描述
  红色部分:huffman表ID和表类型,0x00表示此部分数据描述的是DC直流0号表。
  蓝色部分:共16个字节,为不同位数的码字的数量。
  绿色部分:共10个字节,为编码内容,表示叶子结点的权值。

建立Huffman树/表

  在读出Huffman表的数据后,就要建立Huffman树。具体方法如下:

  1. 第一个码字必定为0.
    如果第一个码字位数为1,则码字为0
    如果第一个码字位数为2,则码字为00
    以此类推
  2. 从第二个码字开始:
    如果它和它前面的码字位数相同,则当前码字为它前面的码字加1;
    如果它的位数比它前面的码字位数大,则当前码字是前面的码字加1后再在后边添若干个0,直至满足位数长度为止。

  以前面的例子为例。
在这里插入图片描述

序号码长码字权值
120004
220105
321006
4311003
54111002
651111001
7611111000
87111111009
981111111007
10911111111008

  上表是建立的DC Huffman表,其权值就是解码时再需要读入的bit位数。
  例:0110101011
  可知,第一个码字是01,且权值为05,则需要再读入5为,即01,10101,对这5位进行译码:21.
  表示直流系数是21.
[注意]直流系数是差分编码的。

  对于交流系数,用交流Huffman表查的该码字对应的权值。权值的高4位表示当前数值前面有多少个连续的零,低4位表示该交流分量数值的二进制位数,也就是接下来需要读入的位数。
  例如:权值为0x31,也就是(3,1)表示此交流系数前有3个0,而此交流系数的具体值还需要读入1个bit的码字才能得到。

二、解码重构

  • 解码Huffman数据
  • 解码DC差值
  • 重构量化后的系数
  • DCT逆变换
  • 丢弃填充的行、列
  • 反0偏置
  • 对对视的CbCr分量插值
  • YCbCr->RGB

三、JPEG标准中的语法结构

Common JPEG MARKERS

JPEG File Interchange Format
在这里插入图片描述
音视频入门-14-JPEG文件格式详解

SOI(Start of Image)图像开始标记

APP0 (Application) 应用程序保留标记0

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 位图数据

APPn (Application) 应用程序保留标记n,其中 n=1~15(任选)

0xFFE1~0xFFF

序号名称长度描述
数据长度2 字节①~② 2 个字段的总长度,即不包括标记代码,但包括本字段
详细信息数据长度-2 字节内容不定

DQT (Define Quantization Table) 定义量化表

0xFFDB

序号名称长度描述
数据长度2 字节字段 ① 和多个字段 ② 的总长度 ,即不包括标记代码,但包括本字段
量化表数据长度-2 字节《量化表表格》

量化表表格:

名称长度描述
精度及量化表 ID1 字节高 4 位:精度,只有两个可选值 0:8位;1:16位;
低 4 位:量化表 ID,取值范围为 0~3
表项(64×(精度+1)) 字节例如 8 位精度的量化表,其表项长度为 64×(0+1)=64 字节

  本标记段中,字段 ② 可以重复出现,表示多个量化表,但最多只能出现 4 次。

SOF0 (Start of Frame) 帧图像开始

0xFFC0

序号名称长度描述
数据长度2 字节①~⑥ 六个字段的总长度,即不包括标记代码,但包括本字段
精度1 字节每个数据样本的位数,通常是 8 位,一般软件都不支持 12 位和 16 位
图像高度2 字节图像高度(单位:像素),如果不支持 DNL 就必须 > 0
图像宽度2 字节图像宽度(单位:像素),如果不支持 DNL 就必须 > 0
颜色分量数1 字节只有 3 个数值可选
1:灰度图;3:YCrCb 或 YIQ;4:CMYK;
而 JFIF 中使用 YCrCb,故这里颜色分量数恒为 3
颜色分量信息颜色分量数×3字节(通常为 9 字节)《颜色分量信息表格》

颜色分量信息:

名称长度描述
颜色分量 ID1 字节——
水平/垂直采样因子1 字节高 4 位:水平采样因子,低 4 位:垂直采样因子
量化表1 字节当前分量使用的量化表的 ID

  本标记段中,字段 ⑥ 应该重复出现,有多少个颜色分量(字段 ⑤),就出现多少次(一般为 3 次)。

DHT(Define Huffman Table)

  里面存储的就是上文中所述的Huffman表,如:
在这里插入图片描述
DHT结构:

  • 标记代码(2字节,0xFFDA)
  • 数据长度(2字节)
  • 颜色分量数(1字节)
  • 颜色分量信息(颜色分量ID,直流/交流系数表号)【这里循环的次数和前面颜色分量数有关,一般为3】
  • 压缩图像数据(谱选择开始0x00,谱选择结束0x3F,谱选择,0x00)

  结束以上字段后,紧接着就是真正的图像信息。图像信息直到遇到一个标记代码就自动结束,一般以EOI标记表示结束。

SOS (Start of Scan) 扫描开始 12字节

0xFFDA

序号名称长度描述
数据长度2 字节①~④ 两个字段的总长度,即不包括标记代码,但包括本字段
颜色分量数1 字节应该和 SOF 中的字段 ⑤ 的值相同,即:
1:灰度图是;3: YCrCb 或 YIQ;4:CMYK。
而 JFIF 中使用 YCrCb,故这里颜色分量数恒为 3
颜色分量信息(a) 颜色分量 ID: 1 字节
(b) 直流/交流系数表号: 1 字节 (高 4 位:直流分量使用的哈夫曼树编号,低 4 位:交流分量使用的哈夫曼树编号)
——
压缩图像数据(a) 谱选择开始:1 字节(固定值 0x00)
(b) 谱选择结束:1 字节(固定值 0x3F)
© 谱选择:1 字节(在基本 JPEG 中总为 0x00)
——

EOI (End of Image) 图像结束标记

0xFFD9

三、调试JPEG解码器程序

2.1 程序中碰到的不常见的知识点

clock_t

clock_t
  只是一个时钟类型,目前不需要过多关注。

#if #endif

C/C++预处理指令#define,#ifdef,#ifndef,#endif…
  #if:如果给定条件为真,则编译下面代码。

calloc

C语言calloc()函数
  calloc() 函数用来动态地分配内存空间并初始化为 0,其原型为:void* calloc (size_t num, size_t size);
  calloc() 在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。
  [返回值]:分配成功返回指向该内存的地址,失败则返回 NULL。
  如果 size 的值为 0,那么返回值会因标准库实现的不同而不同,可能是 NULL,也可能不是,但返回的指针不应该再次被引用。

2.2 程序的使用方式

  由main()函数可以发现,程序特别棒,当命令行参数小于3的时候,输出会显示该如何正确执行该程序。
在这里插入图片描述
  在此,我们获得的提示为:
在这里插入图片描述
  所以,在调试界面键入:
在这里插入图片描述
  获得结果为:
在这里插入图片描述
  以此类推另外几种format的使用方式,在此不再赘述。

2.3 理解程序设计的整体框架

  从main函数开始,是最容易理解程序框架的一种做法(下面是已经删去了无用信息的main函数)

main()函数进行文件的读取

main()函数中进行了各种初始化,将命令行的参数导入。
真正的解码在load_multiple_times和convert_one_image中进行

/*main()函数中进行了各种初始化,将命令行的参数导入。
真正的解码在load_multiple_times和convert_one_image中进行*/
int main(int argc, char *argv[])
{
  int output_format = TINYJPEG_FMT_YUV420P;//4(输出格式:4:2:0)
  char *output_filename, *input_filename;

  clock_t start_time, finish_time;
  unsigned int duration;
  int current_argument;
  int benchmark_mode = 0;//如果可选项[option]存在,那么benchmark_mode=1,否则为0

  if (argc < 3)//如果输入的参数小于3,那么输出一些提示命令提示如何输入命令行参数
    usage();

  current_argument = 1;//目前进行到的argument
  while (1)
   {
     if (strcmp(argv[current_argument], "--benchmark")==0)//调试界面中的可选项[option]
       benchmark_mode = 1;
     else
       break;
     current_argument++;//使得下一次读的时候可以读到下一串字符
   }

  if (argc < current_argument+2)//如果调试界面的长度依然不符合标准
    usage();

  input_filename = argv[current_argument];//输入文件的名字
  if (strcmp(argv[current_argument+1],"yuv420p")==0)//输出模式的选择
    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");//提示format错误
  output_filename = argv[current_argument+2];//输出文件的名字
  start_time = clock();

  if (benchmark_mode)
    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);//将duration的值输入到error_string的缓冲区中
  //fprintf(stderr, "%s\n",error_string);
  return 0;
}

load_multiple_times和convert_one_image之间没有什么太大的区别
load_multiple_times只是将同一张图像解码了1000次,从而多出一个计算解码时间的过程,其他都和convert_one_image一样,所以下面就只分析convert_one_image。

convert_one_image()函数进行解码

convert_one_image()中共进行了3步关键步骤

  1. tinyjpeg_parse_header:解析文件头
  2. tinyjpeg_decode:解析图像数据
  3. 保存输出文件
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;//定义一个buf的缓冲区
  struct jdec_private *jdec;//定义一个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);
  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)//解压缩JPEG图像的头信息
    exitmessage(tinyjpeg_get_errorstring(jdec));

  /* Get the size of the image */
  tinyjpeg_get_size(jdec, &width, &height);//得到长和宽

  DCT_0_mat = (short int*)malloc(width * height / 64 * sizeof(short int));
  DCT_1_mat = (short int*)malloc(width * height / 64 * sizeof(short int));
  DC_pos = 0;
  AC_pos = 0;


  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);//写YUV文件
      write_DC_AC(width, height);//写直流yuv文件和交流yuv文件
      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_parse_header:解析文件头

tinyjpeg_parse_header中最重要的部分是parse_JFIF,前面的部分是用来判断是不是JPEG文件头并且完成文件指针在解码文件头时的初始化。

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);

  return ret;
}

在parse_JFIF完成标记的解析

static int parse_JFIF(struct jdec_private *priv, const unsigned char *stream)
{
  int chuck_len;
  int marker;
  int sos_marker_found = 0;//Starts of scan
  int dht_marker_found = 0;//Define Huffman Table
  const unsigned char *next_chunck;

  /* Parse marker */
  while (!sos_marker_found)
   {
     if (*stream++ != 0xff)
       goto bogus_jpeg_format;
     /* Skip any padding ff byte (this is normal) */
     while (*stream == 0xff)
       stream++;

     marker = *stream++;
     chuck_len = be16_to_cpu(stream);//FF E0 的下一个字节,即FF E0块的大小
     next_chunck = stream + chuck_len;
     switch (marker)
      {
       case SOF://start of frame(baseline DCT)
	 if (parse_SOF(priv, stream) < 0)
	   return -1;
	 break;
       case DQT:
	 if (parse_DQT(priv, stream) < 0)
	   return -1;
	 break;
       case SOS:
	 if (parse_SOS(priv, stream) < 0)
	   return -1;
	 sos_marker_found = 1;
	 break;
       case DHT:
	 if (parse_DHT(priv, stream) < 0)
	   return -1;
	 dht_marker_found = 1;
	 break;
       case DRI:
	 if (parse_DRI(priv, stream) < 0)
	   return -1;
	 break;
       default:
#if TRACE
	fprintf(p_trace,"> Unknown marker %2.2x\n", marker);
	fflush(p_trace);
#endif
	 break;
      }

     stream = next_chunck;
   }

  if (!dht_marker_found) {//如果并没有Huffman码表传输过来
    build_default_huffman_tables(priv);//生成一个本地Huffman码表
  }
  return 0;
bogus_jpeg_format:
  return -1;
}

  以本实验所使用的图片为例,其解析的顺序为:

  • DQT
  • SOF
  • DHT
  • SOS
DQT的解析
  1. 得到量化表长度(可能有多张量化表)
  2. 得到量化表精度
  3. 得到及检查量化表的序号(只能是0-3)
  4. 得到量化表的内容
static int parse_DQT(struct jdec_private *priv, const unsigned char *stream)//这里有点点偷懒,没有实现量化表是16bit的情形
{
  int qi;
  float *table;
  const unsigned char *dqt_block_end;
  
  dqt_block_end = stream + be16_to_cpu(stream);
  stream += 2;	/* Skip length */

  while (stream < dqt_block_end)
   {
     qi = *stream++;
     table = priv->Q_tables[qi];//table指向的是priv->Q_table[qi]所在的地址空间
     build_quantization_table(table, stream);//提取量化表
     stream += 64;
   }
  return 0;
}
SOF的解析
  1. 得到长宽、颜色分量数
  2. 得到每个颜色分量的颜色索引、量化表序号、水平因子和垂直因子
static int parse_SOF(struct jdec_private *priv, const unsigned char *stream)//解码SOF(start of frame)
{
  int i, width, height, nr_components, cid, sampling_factor;
  int Q_table;
  struct component *c;
  print_SOF(stream);

  height = be16_to_cpu(stream+3);//图像高度
  width  = be16_to_cpu(stream+5);//图像宽度
  nr_components = stream[7];//颜色分量数
  stream += 8;
  for (i=0; i<nr_components; i++) {
     cid = *stream++;//颜色索引
     sampling_factor = *stream++;//Sample factor(1字节,高四位水平因子,低四位垂直因子)
     Q_table = *stream++;//量化表号
     c = &priv->component_infos[i];
     c->Vfactor = sampling_factor&0xf;//取出低四位垂直因子
     c->Hfactor = sampling_factor>>4;//取出高四位水平因子
     c->Q_table = priv->Q_tables[Q_table];//量化
  }
  priv->width = width;//图像宽度
  priv->height = height;//图像高度
  return 0;
}
DHT的解析
  1. 得到Huffman表的类型(AC、DC)和序号
  2. 重建Huffman表
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;//整个Huffman编码块除去标志位的长度
  stream += 2;	/* Skip length */

  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 (index & 0xf0 )
       build_huffman_table(huff_bits, stream, &priv->HTAC[index&0xf]);
     else
       build_huffman_table(huff_bits, stream, &priv->HTDC[index&0xf]);

     //这块的作用是为了防止一个DHT块中携带有多张Huffman码表
     length -= 1;
     length -= 16;
     length -= count;
     stream += count;
  }
  return 0;
}
SOS的解析

得到解析每个颜色分量的 DC、AC 值所使用的 Huffman 表序号(与 DHT 中序号对应)

static int parse_SOS(struct jdec_private *priv, const unsigned char *stream)
{
  unsigned int i, cid, table;
  unsigned int nr_components = stream[2];//颜色分量数
  stream += 3;
  for (i=0;i<nr_components;i++) {
     cid = *stream++;
     table = *stream++;
     priv->component_infos[i].AC_table = &priv->HTAC[table&0xf];//将量化表分配给各个通道
     priv->component_infos[i].DC_table = &priv->HTDC[table>>4];//将量化表分配给各个通道
  }
  priv->stream = stream+3;
  return 0;
}
tinyjpeg_decode:解析图像数据

tinyjpeg_decode()最重要的两个函数是decode_MCU(priv)和convert_to_pixfmt(priv);
其他都是初始化

/**
 * Decode and convert the jpeg image into @pixfmt@ image
 *
 * Note: components will be automaticaly allocated if no memory is attached.
 */
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:
       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:
       return -1;
  }

  xstride_by_mcu = ystride_by_mcu = 8;//8*8的宏块
  if ((priv->component_infos[cY].Hfactor | priv->component_infos[cY].Vfactor) == 1) {
     decode_MCU = decode_mcu_table[0];//1×1的类型
     convert_to_pixfmt = colorspace_array_conv[0];
  } 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;
  } 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;
  } else {
     decode_MCU = decode_mcu_table[2];
     convert_to_pixfmt = colorspace_array_conv[2];
     xstride_by_mcu = 16;
  }

  //重新初始化DC系数
  resync(priv);

  /* Don't forget to that block can be either 8 or 16 lines */
  //别忘了该块可以是8或16行
  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) */
  //只需按宏块解码图像(大小为8x8、8x16或16x16)
  unsigned char* dc_buffer = NULL;
  unsigned char* ac_buffer = NULL;

  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;
	       }
	    }
     }
  }
  return 0;
}
decode_MCU()

decode_MCU()是一个指向函数的指针,其作用是指向如下定义中的8个函数:

static const decode_MCU_fct decode_mcu_3comp_table[4] = {
   decode_MCU_1x1_3planes,
   decode_MCU_1x2_3planes,
   decode_MCU_2x1_3planes,
   decode_MCU_2x2_3planes,
};

static const decode_MCU_fct decode_mcu_1comp_table[4] = {
   decode_MCU_1x1_1plane,
   decode_MCU_1x2_1plane,
   decode_MCU_2x1_1plane,
   decode_MCU_2x2_1plane,
};

本实验以decode_MCU_1x1_3planes为例:(进行的是宏块的解码)

/*
 * Decode all the 3 components for 1x1 
 */
static void decode_MCU_1x1_3planes(struct jdec_private *priv)
{
  // Y
  process_Huffman_data_unit(priv, cY);
  IDCT(&priv->component_infos[cY], priv->Y, 8);
  
  // Cb
  process_Huffman_data_unit(priv, cCb);
  IDCT(&priv->component_infos[cCb], priv->Cb, 8);

  // Cr
  process_Huffman_data_unit(priv, cCr);
  IDCT(&priv->component_infos[cCr], priv->Cr, 8);
}
convert_to_pixfmt()

和decode_MCU()一样,也是使用了指向函数的指针,实现的功能是颜色空间的转换。

static const convert_colorspace_fct convert_colorspace_yuv420p[4] = {
   YCrCB_to_YUV420P_1x1,
   YCrCB_to_YUV420P_1x2,
   YCrCB_to_YUV420P_2x1,
   YCrCB_to_YUV420P_2x2,
};

static const convert_colorspace_fct convert_colorspace_rgb24[4] = {
   YCrCB_to_RGB24_1x1,
   YCrCB_to_RGB24_1x2,
   YCrCB_to_RGB24_2x1,
   YCrCB_to_RGB24_2x2,
};

static const convert_colorspace_fct convert_colorspace_bgr24[4] = {
   YCrCB_to_BGR24_1x1,
   YCrCB_to_BGR24_1x2,
   YCrCB_to_BGR24_2x1,
   YCrCB_to_BGR24_2x2,
};

static const convert_colorspace_fct convert_colorspace_grey[4] = {
   YCrCB_to_Grey_1x1,
   YCrCB_to_Grey_1x2,
   YCrCB_to_Grey_2x1,
   YCrCB_to_Grey_2x2,
};

2.4 理解三个结构体的设计目的

2.4.1 struct huffman_table

创立一个快速查找表用于快速解码。另外如果查找失败的话则使用慢速查找表。
最主要的目的就是加速解码的过程。

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];
};

2.4.2 struct component

componet主要是用来保存一个MCU块的信息。每处理一个新的MCU块后,信息都会更新。

struct component 
{
  unsigned int Hfactor;
  unsigned int Vfactor;
  float *Q_table;		/* Pointer to the quantisation table to use */
  struct huffman_table *AC_table;
  struct huffman_table *DC_table;
  short int previous_DC;	/* Previous DC coefficient */
  short int DCT[64];		/* DCT coef */
#if SANITY_CHECK
  unsigned int cid;
#endif
};

2.4.3 struct jdec_private

这个结构体使用的我感觉很随意,就是随时保存图像的基本信息和各个通道的信息,感觉它啥都保存,反正在最后进行信息的保存与整合之前,基本用的都是这个结构体来保存信息或者提取所要的信息。

//解码数据流结构体
struct jdec_private
{
  /* Public variables */
  uint8_t *components[COMPONENTS];//指针数组,数组中每一个元素都是uint8_t类型的指针(uint8_t实际上就是unsigned char)
                                  //在整个程序框架下,这是指向YUV三种块的指针数组
  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];
  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]; //存放YUV分量值的三个数组

  jmp_buf jump_state;//创建一个int类型包含16个值的数组
  /* Internal Pointer use for colorspace conversion, do not modify it !!! */
  uint8_t *plane[COMPONENTS];//内部指针用于颜色空间转换,请勿修改!!!

};

2.5 理解在视音频编解码调试中TRACE的目的和含义

  trace的目的主要是输出中间过程中的某些变量,或者错误信息。方便理解程序运行的过程以及哪部分可能会出错。
  Trace打开的时候,就可以进行上述说明的信息的输出,Trace关闭的话,就是直接跳过这些代码的编译,不进行上说说明信息的输出。

在tinyjpeg.h中,将TRACE的值置为1,就是打开TRACE,将TRACE的值置为0,就是关闭TRACE.

#define  snprintf _snprintf//add by nxn
#define TRACE 1//add by nxn
#define  TRACEFILE "trace_jpeg.txt"//add by nxn
#define QUANTITYFILE "quantity_table.txt"//量化矩阵的输出
#define HUFFMANFILE "huffman_table.txt"//Huffman码表的输出
#define DCCOFFTXTFILE "DC_coff.txt"//直流系数的直接输出
#define ACCOFFTXTFILE "AC_coff.txt"//交流系数的直接输出
#define DCCOFFFILE "DC_coff.yuv"//直流系数的yuv图像
#define ACCOFFFILE "AC_coff.yuv"//交流系数的yuv图像

  寻找到如下所示代码,在这里面进行代码的添加与删除,就可以实现TRACE的修改。

#if TRACE
 
#endif

四、修改JPEG解码器

将输出文件保存为YUV文件

  在loadjpeg.c中添加如下代码:
在这里插入图片描述
  原bmp图为:
在这里插入图片描述
  输出的yuv图为:
在这里插入图片描述

以TXT文件输出所有量化矩阵和HUFFMAN码表

输出量化矩阵

  在tinyjpeg.h中添加如下代码:
在这里插入图片描述
在这里插入图片描述
  在loadjpeg.c中添加如下代码:
在这里插入图片描述
  在tinyjpeg.c中添加如下代码:
在这里插入图片描述
在这里插入图片描述
  结果如下:
在这里插入图片描述

输出Huffman码表

  在tinyjpeg.h中添加如下代码:
在这里插入图片描述
在这里插入图片描述
  在loadjpeg.c中添加如下代码:
在这里插入图片描述
  在tinyjpeg.c中添加如下代码:
在这里插入图片描述
在这里插入图片描述
  部分结果如下:
在这里插入图片描述

输出DC图像并统计其概率分布&输出某一个AC值图像并统计其概率分布

  在tinyjpeg.h中添加如下代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  在loadjpeg.c中添加如下代码:
在这里插入图片描述
在这里插入图片描述
  在tinyjpeg.c中添加如下代码:
在这里插入图片描述
  另外几个具有相似功能的函数内也照这样写入同样的代码。
  在loadjpeg.c中添加如下代码(用来输出直流系数的YUV图像交流系数的YUV图像):
在这里插入图片描述
  由于write_DC_AC()长度太长,所以直接在下面粘贴了代码。

static write_DC_AC(int width, int height)//写直流yuv文件
{
    short int max_DC = -9999;
    short int max_AC = -9999;
    short int min_DC = 9999;
    short int min_AC = 9999;
    int* DC_MAT = (unsigned char*)malloc(256*sizeof(int));
    int* AC_MAT = (unsigned char*)malloc(256*sizeof(int));
    for (int i = 0; i < 256; i++)
    {
        DC_MAT[i] = 0;
        AC_MAT[i] = 0;
    }

    //寻找最大最小值
    for (int i = 0; i < width * height / 64; i++)
    {
        max_DC = max_DC > DCT_0_mat[i] ? max_DC : DCT_0_mat[i];
        max_AC = max_AC > DCT_1_mat[i] ? max_AC : DCT_1_mat[i];
        min_DC = min_DC < DCT_0_mat[i] ? min_DC : DCT_0_mat[i];
        min_AC = min_AC < DCT_1_mat[i] ? min_AC : DCT_1_mat[i];
    }

    //写交流直流图像文件
    unsigned char temp;
    for (int i = 0; i < width * height / 64; i++)
    {
        short int wri;
        wri = ((float)(DCT_0_mat[i] - min_DC) / (float)(max_DC - min_DC)) * 255;//区间变换
       // printf("%d\n", wri);
        temp = (unsigned char)wri;
        fwrite(&temp, 1, 1, DCT_0);
        DC_MAT[wri]++;

        wri = ((float)(DCT_1_mat[i] - min_AC) / (float)(max_AC - min_AC)) * 255;//区间变换
        temp = (unsigned char)wri;
        fwrite(&temp, 1, 1, DCT_1);
        AC_MAT[wri]++;
    }
    temp = 128;
    for (int i = 0; i < width * height / 32; i++)
    {
        fwrite(&temp, 1, 1, DCT_0);
        fwrite(&temp, 1, 1, DCT_1);
    }

    //写概率分布
    fprintf(DCT_0_TXT, "value\tfreq\n");
    fprintf(DCT_1_TXT, "value\tfreq\n");
    for (int i = 0; i < 256; i++)
    {
        int length = width * height / 64;//总数
        fprintf(DCT_0_TXT, "%d\t%10.6f\n", i, ((float)DC_MAT[i] / (float)length));
        fprintf(DCT_1_TXT, "%d\t%10.6f\n", i, ((float)AC_MAT[i] / (float)length));
    }

}

直流和交流图像:
在这里插入图片描述
直流系数的频率分布(是已经变换到0-255的分布)
在这里插入图片描述
交流系数的频率分布(是已经变换到0-255的分布)
在这里插入图片描述
  至此,实验完成。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值